diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/AdminSettings.sample mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/AdminSettings.sample --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/AdminSettings.sample Tue Apr 11 11:01:01 2006 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/AdminSettings.sample Tue Mar 18 12:56:52 2008 @@ -7,8 +7,6 @@ * privileges to do maintenance work. * * Developers: Do not check AdminSettings.php into Subversion - * - * @package MediaWiki */ /* @@ -27,5 +25,3 @@ * Whether to enable the profileinfo.php script. */ $wgEnableProfileInfo = false; - -?> diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/FAQ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/FAQ --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/FAQ Mon Oct 2 23:08:41 2006 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/FAQ Mon Sep 17 14:35:44 2007 @@ -2,4 +2,4 @@ http://meta.wikimedia.org/wiki/MediaWiki_FAQ. A newer version is available at -http://www.mediawiki.org/wiki/Help:FAQ. +http://www.mediawiki.org/wiki/Manual:FAQ. diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/HISTORY mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/HISTORY --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/HISTORY Tue May 15 14:46:04 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/HISTORY Fri Jan 9 21:30:20 2009 @@ -1,5 +1,1790 @@ Change notes from older releases. For current info see RELEASE-NOTES. +== MediaWiki 1.13 == + +== Changes since 1.13.2 == + +David Remahl of Apple's Product Security team has identified a number of +security issues in previous releases of MediaWiki. Subsequent analysis by the +MediaWiki development team expanded the scope of these vulnerabilities. The +issues with a significant impact are as follows: + +* An XSS vulnerability affecting all MediaWiki installations between 1.13.0 and + 1.13.2. [CVE-2008-5249] +* A local script injection vulnerability affecting Internet Explorer clients for + all MediaWiki installations with uploads enabled. [CVE-2008-5250] +* A local script injection vulnerability affecting clients with SVG scripting + capability (such as Firefox 1.5+), for all MediaWiki installations with SVG + uploads enabled. [CVE-2008-5250] +* A CSRF vulnerability affecting the Special:Import feature, for all MediaWiki + installations since the feature was introduced in 1.3.0. [CVE-2008-5252] + +XSS (cross-site scripting) vulnerabilities allow an attacker to steal an +authorised user's login session, and to act as that user on the wiki. The +authorised user must visit a web page controlled by the attacker in order to +activate the attack. Intranet wikis are vulnerable if the attacker can +determine the intranet URL. + +Local script injection vulnerabilities are like XSS vulnerabilities, except +that the attacker must have an account on the local wiki, and there is no +external site involved. The attacker uploads a script to the wiki, which another +user is tricked into executing, with the effect that the attacker is able to act +as the privileged user. + +CSRF vulnerabilities allow an attacker to act as an authorised user on the wiki, +but unlike an XSS vulnerability, the attacker can only act as the user in a +specific and restricted way. The present CSRF vulnerability allows pages to be +edited, with forged revision histories. Like an XSS vulnerability, the +authorised user must visit the malicious web page to activate the attack. + +These four vulnerabilities are all fixed in this release. + +David Remahl also reminded us of some security-related configuration issues: + +* By default, MediaWiki stores a backup of deleted images in the images/deleted + directory. If you do not want these images to be publically accessible, make + sure this directory is not accessible from the web. MediaWiki takes some steps + to avoid leaking these images, but these measures are not perfect. +* Set display_errors=off in your php.ini to avoid path disclosure via PHP fatal + errors. This is the default on most shared web hosts. +* Enabling MediaWiki's debugging features, such as $wgShowExceptionDetails, may + lead to path disclosure. + +Other changes in this release: + +* Avoid fatal error in profileinfo.php when not configured. +* Add a .htaccess to deleted images directory for additional protection against + exposure of deleted files with known SHA-1 hashes on default installations. +* Avoid streaming uploaded files to the user via index.php. This allows + security-conscious users to serve uploaded files via a different domain, and + thus client-side scripts executed from that domain cannot access the login + cookies. Affects Special:Undelete, img_auth.php and thumb.php. +* When streaming files via index.php, use the MIME type detected from the + file extension, not from the data. This reduces the XSS attack surface. +* Blacklist redirects via Special:Filepath. Such redirects exacerbate any + XSS vulnerabilities involving uploads of files containing scripts. +* Internationalisation updates. + +== Changes since 1.13.1 == + +* Security: Work around misconfiguration by requiring strict comparisons for + in_array in User::isAllowed(). +* (bug 14944) Added $wgShellLocale for configuration of an appropriate locale + to use for LC_CTYPE during shell invocation. For servers that don't have + en_US.utf8. Also added locale detection during install. +* Localisation updates +* Security: Fixed XSS vulnerability in useskin parameter. + +== Changes since 1.13.0 == + +* (bug 15460) Fixed intermittent deadlock errors and poor concurrent + performance for installations without memcached. +* (bug 13770) Fixed DOM module detection for installations with both dom + and domxml. +* (bug 15148) Fixed Special:BlockIP for PostgreSQL +* Fixed SQLite support for non-memcached installations +* Localisation updates, Achinese (ace) added. + +== Changes since 1.13.0rc2 == + +* (bug 13770) Fixed incorrect detection of PHP's DOM module +* Fix regression from r37834: accesskey tooltip hint should be given for the + minor edit and watch labels on the edit page. +* Updated Chinese simplified/traditional conversion tables + +== Changes since 1.13.0rc1 == + +* $wgForwardSearchUrl has been removed entirely. Documented setting since 1.4 + has been $wgSearchForwardUrl. +* (bug 14907) DatabasePostgres::fieldType now defined. +* (bug 14966) Fix SearchEngineDummy class for silently non-functional search + on Sqlite instead of horribly fatal error breaky one. +* (bug 14987) Only fix double redirects on page move when the checkbox is + checked +* (bug 13376) Use $wgPasswordSender, not $wgEmergencyContact, as return + address for page update notification mails. +* API: Registration time of users registered before the DB field was created is now + shown as empty instead of the current time. +* (bug 14904): fragments were lost when redirects were fixed. +* Added magic word __STATICREDIRECT__ to suppress the redirect fixer +* (bug 15035) Revert English linkTrail to /^([a-z]+)(.*)$/sD, as it was before + r36253. Multiple reports of breakage due to old (pre-5.0) PCRE libraries, + both bundled with PHP and packaged with distros such as RHEL. +* (bug 14944) Shell invocation of external programs such as ImageMagick convert + was broken in PHP 5.2.6, if the server had a non-UTF-8 locale. + +=== Configuration changes in 1.13 === + +* New option $wgFeed can be set false to turn off syndication feeds +* (bug 5745) Special:Whatlinkshere now shows up to $wgMaxRedirectLinksRetrieved + links through each redirect instead of hardcoded 500 +* Set $wgUploadSizeWarning to false by default +* Added $wgLBFactoryConf, for generic configuration of multi-master wiki farms +* Removed $wgAlternateMaster, use $wgLBFactoryConf +* (bug 13562) Misspelled option $wgUserNotifedOnAllChanges changed to + $wgUserNotifiedOnAllChanges +* (bug 12860) New option $wgSitemapNamespaces allows sitemaps to be generated + for only some namespaces +* Removed the emailconfirmed implicit group by default. To re-add it, use: + $wgAutopromote['emailconfirmed'] = APCOND_EMAILCONFIRMED; + in your LocalSettings.php. +* (bug 2396) New shared database configuration variables. $wgSharedPrefix allows + you to use a shared database with a different prefix. Or you can now use a local + database and use prefixes to separate wiki and the shared tables. And the new + $wgSharedTables variable allows you to specify a list of tables to share. +* Automatic edit summaries can be disabled with $wgUseAutomaticEditSummaries +* Duplicates of images are now shown on the image page +* $wgRCFilterByAge allows for the list of dates in recent changes special pages to + be filtered to only those within the range of $wgRCMaxAge +* $wgRCLinkLimits and $wgRCLinkDays allow for customization of the list and limits + displayed on the recent changes special pages +* The "createpage" permission is no longer required when uploading if the target + image page already exists +* $wgMaximumMovedPages restricts the number of pages that can be moved at once + (default 100) with the new subpage-move functionality of Special:Movepage +* Hooks display in Special:Version is now disabled by default, use + $wgSpecialVersionShowHooks = true; to enable it. +* $wgActiveUserEditCount sets the number of edits that must be performed over + a certain number of days to be considered active +* $wgActiveUserDays is that number of days +* $wgRateLimitsExcludedGroups has been deprecated in favor of + $wgGroupPermissions[]['noratelimit']. The former still works, however. +* New $wgGroupPermissions option 'move-subpages' added to control bulk-moving + subpages along with pages. Assigned to 'user' and 'sysop' by default. +* New $wgRC2UDPOmitBots allows user to omit bot edits from UDP output. + Default: false +* Removed $wgEnableCascadingProtection option. Disabling cascading protection + is no longer possible. +* $wgMessageCacheType defines now the type of cache used by the MessageCache class, + previously it was choosen based on $wgParserCacheType +* $wgExtensionAliasesFiles option to simplify adding aliases to special pages + provided by extensions, in a similar way to $wgExtensionMessagesFiles +* Added $wgXMLMimeTypes, an array of XML mimetypes we can check for + with MimeMagic. +* Added $wgDirectoryMode, which allows for setting the default CHMOD value when + creating new directories. +* (bug 14843) $wgCookiePrefix can be set by LocalSettings now, false defaults + current behavior. + +=== New features in 1.13 === + +* __HIDDENCAT__ on a category page causes the category to be hidden on the + article page +* Do not show edit permissions errors on a red link click, just redirect to the + article. This is so that readers who don't know what a red link is are not + confused when they are told they are range-blocked. +* Add a new hook ImageBeforeProduceHTML to allow extensions to modify wikitext + image syntax output +* (bug 13100) Added 'preloadtitle' parameter to action=edit§ion=new that + pre-fills the section title field +* (bug 13112) Added Special:RelatedChanges alias to Special:RecentChangesLinked +* (bug 13130) Moved edit token and autosummary fields above edit tools to + reduce broken form submissions +* Add --old-redirects-only option to maintenance/refreshLinks.php, to add old + redirects to the redirect table +* Add links to page and file deletion forms to edit predefined delete reasons +* (bug 13269) Added MediaWiki:Uploadfooter to the bottom of Special:Upload +* (bug 2815) Search results for media now use thumbnail instead of text extract +* When a page doesn't exist, the tab should say "create", not "edit" +* (bug 12882) Added a span with class "patrollink" around "Mark as patrolled" + link on diffs +* Magic word formatnum can now take raw suffix to undo formatting +* Add updatelog table to reliably permit updates that don't change the schema +* Add category table to allow better tracking of category membership counts +** (bug 1212) Give correct membership counts on the pages of large categories +** Use category table for more efficient display of Special:Categories +* (bug 1459) Search for duplicate files by hash: Special:FileDuplicateSearch +* (bug 9447) Added hooks for search result headings +* Image redirects are now enabled by default +* (bug 13450) Email confirmation can now be canceled before the expiration +* (bug 13490) Show upload/file size limit on upload form +* Redesign of Special:UserRights +* Make rev_deleted log entries more intelligible +* (bug 6943) Added PAGESINCATEGORY: magic word +* (bug 13604) Added Special:ListGroupRights +* (bug 6332, 8617) Added message 'mainpage-description' as duplicate of + 'mainpage' and added it to message 'sidebar' +* Automatically add old redirects to the redirect table when needed +* (bug 6934) Allow inclusions, links, redirects to be separately toggled on or + off on Special:WhatLinksHere +* Cache image redirects +* (bug 10457) Organize Special:SpecialPages into sections +* Add a new hook EditPageBeforeConflictDiff to allow extensions like FCKeditor + to modify the output for edit conflicts +* Add class="nested" for
s so fieldsets inside fieldsets get + a slightly less huge margin and padding +* (bug 13527) Use sitemaps.org format 0.9 instead of a Google-specific format +* Allow \C and \Q as TeX commands to match \R, \N, \Z +* On Special:UserRights, when you can add a group you can't remove or remove + one you can't add, a notice is printed to warn you +* (bug 12698) Create PAGESIZE parser function, to return the size of a page +* Allow the "log in / create account" link in the toolbar to have different + text from Special:UserLogin title (new message 'nav-login-createaccount') +* Say "log in / create account" if an anonymous user can create an account, + otherwise just "log in", consistently across skins +* Special:Shortpages and Special:Longpages now returns pages in all content + namespaces, not just NS_MAIN. +* (bug 889) Improve conflict-handling between shared upload repository + and local one +* Update documentation links in auto-generated LocalSettings.php +* (bug 13584) The new hook SkinTemplateToolboxEnd was added. +* (bug 709) Cannot rename/move images and other media files [EXPERIMENTAL] +* Custom rollback summaries now accept the same arguments as the default message +* (bug 12542) Added hooks for expansion of Special:Listusers +* Drop-down AJAX search suggestions (turn on $wgEnableMWSuggest) +* More relevant search snippets (turn on $wgAdvancedSearchHighlighting) +* (bug 13950) Allow users to watch the user/talk pages of users they block. +* (bug 13970) Allow MonoBook-based skins to specify their own print stylesheet +* Show image links on Special:Whatlinkshere +* Use rel="start", "prev", "next" appropriately on Pager-based pages +* Add support for SQLite +* AutoAuthenticate hook renamed to UserLoadFromSession +* (bug 13232) importScript(), importStylesheet() funcs available to custom JS +* (bug 13095) Search by first letters or digits in [[Special:Categories]] +* Users moving a page can now move all subpages automatically as well +* (bug 14259) Localisation message for upload button on Special:Import is now + 'import-upload' instead of 'upload' +* Add information about user group membership to Special:Preferences +* (bug 14146) Wrap usage section on imagepages into
s. +* New layout for Special:Specialpages. Restricted pages are marked but not separated + from other pages in their group. +* (bug 14263) Show a diff of the revert on rollback notification page. +* (bug 13434) Show a warning when hash identical files exist +* Sidebar is now cached for all languages +* The User class now contains a public function called isActiveEditor. Figures + out if a user is active based on at least $wgActiveUserEditCount number of + edits in the last $wgActiveUserDays days. +* SpecialSearchResults hook now passes results by reference, so they can be + changed by extensions. +* Add a new hook LinkerMakeExternalLink to allow extensions to modify the output of + external links. +* (bug 14132) Allow user to disable bot edits from being output to UDP. +* (bug 14328) jsMsg() within Wikibits now accepts a DOM object, not just a string +* (bug 14558) New system message (emailuserfooter) is now added to the footer of + e-mails sent with Special:Emailuser +* Add support for Hijri (Islamic) calendar +* Add a new hook LinkerMakeExternalImage to allow extensions to modify the output + of external (hotlinked) images. +* (bug 14604) Introduced the following features for the LanguageConverter: + Multi-tag support, single conversion flag, remove conversion flag on a single + page, description flag, variant name, multi-variant fallbacks. +* Add zh-mo and zh-my variants for the zh language +* (bugs 4832, 9481, 12890) Special:Recentchangeslinked now has all options that + are in Special:Recentchanges +* Allow an $error message to be passed to ArticleDelete hook +* Allow extensions to modify the user creation form by calling addInputItem(); +* Add meta generator tag to HTML output +* MediawikiPerformAction hook is now passed the Mediawiki object +* Added blank special page Special:BlankPage for benchmarking, etc. +* Foreign repo file descriptions and thumbnails are now cached. +* (bug 11732) Allow localisation of edit button images +* Allow the search box, toolbox and languages box in the Monobook sidebar to be + moved around arbitrarily using special sections in [[MediaWiki:Sidebar]]: + SEARCH, TOOLBOX and LANGUAGES +* Add a new hook NormalizeMessageKey to allow extensions to replace messages before + the database is potentially queried +* (bug 9736) Redirects on Special:Fewestrevisions are now marked as such. +* New date/time formats in Cs localization according to ČSN and PČP. +* Special:Recentchangeslinked now includes changes to transcluded pages and + displayed images; also, the "Show changes to pages linked" checkbox now works on + category pages too, showing all links that are not categorizations +* (bug 4578) Automatically fix redirects broken by a page move + +=== Bug fixes in 1.13 === + +* (bug 10677) Add link to the file description page on the shared repository +* (bug 13084) Increase size of source/destination filename fields in upload form +* (bug 13115) rebuildrecentchanges should print the current value of $wgRCMaxAge +* (bug 13140) Show parent categories in category namespace +* (bug 13149) Correctly format 'fileexists' message on Upload page +* Make the default filepageexists message accurate +* (bug 12988) $wgMinimalPasswordLength no longer breaks create user by email +* (bug 13022) Fix upload from URL on PHP 5.0.x +* (bug 13132) Unable to unprotect pages protected with earlier versions of MediaWiki +* (bug 12723) OpenSearch description name now uses more compact language code + to avoid passing the length limit as often, is customizable per site via + 'opensearch-desc' message. +* (bug 13135) Special:Userrights now passes IDs through form submission + to allow functionality on not-quite-right usernames +* (bug 12575) Prevent duplicate patrol log entries from being created +* (bug 13174) __HIDDENCAT__ now applies only to category pages +* (bug 13031) Add links to user pages in e-mail form +* (bug 13147) Description for categoriespagetext (used in Special:Categories) reworded +* (bug 11561) Fix fatal error when calling action=revert to non-image page +* (bug 12430) Fix call to private method LinkFilter::makeRegex fatal error in + maintenance/cleanupSpam.php +* All skins should have the "mediawiki" class on the body element +* (bug 13019) Message cache for some extensions not loaded at time of editing +* (bug 13247) Prettified ISBN links +* maintenance/refreshLinks.php did not fix page_id 1 with the --new-only option +* (bug 13110) Don't show "Permission error" page if the edit is already rolled + back when using rollback +* (bug 13012) Use content messages for block options when generating the + recentchanges entry +* (bug 13274) Change links for messages to ucfirst +* (bug 13273) Un-hardcode some punctuation (add new messages colon-separator, + autocomment-prefix) +* Parse MediaWiki message translations with a correct language setting on preview +* (bug 13281) Treat X-Forwarded-For, Client-ip and User-Agent headers as + case-insensitive names. +* Adding the fix for lists in RTL wikis to more skins, and fixing the image toc +* (bug 8157) Remove redirects from Special:Unusedtemplates. Patch by WebBoy. +* (bug 10721) Duplicate section anchors with differing case now disambiguated + for Internet Explorer's sake and standards compliance +* (bug 13298) Tighter limits on Special:Newpages limits when embedding +* Email subject in content language instead of sending user's UI language +* (bug 13251) Allow maintenance rebuild scripts to work with Postgres +* (bug 2084) Fixed incorrect regex to match redirects +* (bug 3131) Manually-specified upload destination filename is no longer + overwritten by browsing for a file after you wrote it. +* (bug 7251) Sidebars generated by MediaWiki:Sidebar now have the class + 'generated-sidebar'. +* (bug 13265) Media handler is missing 'image/x-bmp' +* (bug 13407) MediaWiki:Powersearch is used in two places +* (bug 13403) Fix cache invalidation of history pages when old revisions change +* (bug 11563) Deprecated SearchMySQL4 class; merged code to SearchMySQL +* (bug 12801) Fix link in subtitle message in AJAX search +* (bug 13428) Fix regression in protection form layout HTML validity +* (bug 9403) Sanitize newlines from search term input +* (bug 13429) Separate date and time in message sp-newimages-showfrom +* (bug 13137) Allow setting 'editprotected' right separately from 'protect', + so groups may optionally edit protected pages without having 'protect' perms +* Disallow deletion of big pages by means of moving a page to its title and + using the "delete and move" option. +* (bug 13466, 13632) White space differences not shown in diffs +* (bug 1953) Search form now honors namespace selections more reliably +* (bug 12294) Namespace class renamed to MWNamespace for PHP 5.3 compatibility +* PHP 5.3 compatibility fix for wfRunHooks() called with no parameters +* (bug 6447) Trackbacks now work with transactional tables, if enabled +* (bug 6892, 7147) Trackback error handling, optional fields more robust +* (bug 6813) Don't break HTML validator when using trackbacks +* Fix for size checks on SVG images with global 'stroke-width' attribute +* (bug 11874) Inline CSS with !important no longer borken +* (bug 1600) Strip extra == section markup == in new-comment field +* (bug 11325) Wrapped page titles in MonoBook skin spaced more nicely +* (bug 12077) Fix HTML nesting for TOC +* (bug 344) Purge cache for talk/article pages when deleting the other tab +* (bug 13436) Treat image captions correctly when they include option keywords + (like ending with "px" or starting with "upright") +* Trackback display formatting fixed +* Don't die when single-element arrays are passed to SQL query constructors + that have an array index other than 0 +* (bug 13522) Fix fatal error in Parser::extractTagsAndParams +* (bug 13532) Use proper timestamp call when reverting images +* (bug 13543) Updated FAQ link in the installer sidebar +* (bug 13540) Date format in confirmation e-mail now matches message language +* (bug 13554) PHP Notice in old pre-processor when list item is empty. +* (bug 13556) Don't show a blank form if no image is attached in Special:Upload +* (bug 13576) maintenance/rebuildrecentchanges.php fails +* (bug 13441) Allow Special:Recentchanges to show bots only +* (bug 13431) Show true message source in Special:Allmessages&ot=php / xml +* (bug 13463) Login successful page doesn't use user's preferred interface language +* (bug 13630) Fixed warnings for pass by reference at call time in + Special:Revisiondelete when generating the log entry. +* (bug 12064) BeforePageDisplay hook is now called for all skins +* (bug 13624) Fix regression with manual thumb= parameter on images +* (bug 11039) Add missing labels on protection form +* (bug 13458) Preview/edit toolbar spacing now works consistently +* (bug 13433) Fix action=render on Image: pages +* (bug 13678) Fix CSS validation for Monobook +* (bug 13684) Links in Special:ListGroupRights should be in content language +* (bug 13690) Fix PHP notice on accessing some URLs +* Hide (undo) link if user isn't able to edit page +* Invalidate cache of pages that includes images via redirects on upload +* (bug 13705) Don't show rollback link in page history on incorrect revisions +* (bug 13708) Don't set "Search results" title when loading Special:Search + without query +* (bug 13736) Don't show MediaWiki:Anontalkpagetext on non-existant IP addresses +* (bug 13728) Don't trim initial whitespace during section edits +* (bug 13727) Don't delete log entries from recentchanges on page deletion +* (bug 13752) Redirects to sections now work again +* (bug 13725) Upload form watch checkbox state set correctly with wpDestFile +* (bug 13756) Don't show the form and navigation links of Special:Newpages if + the page is included +* When hiding things on WhatLinksHere, generated URLs should hide them too +* Properly escape search terms with regex chars so they appear highlighted in + search results +* (bug 13768) pt_title field encoding fixed +* Do not display empty columns on Special:UserRights if all groups are + changeable or all unchangeable +* Fix fatal error on calling PAGESINCATEGORY with invalid category name +* (bug 13793) Special:Whatlinkshere filters wrong - after paginating instead of before +* (bug 13796) Show links to parent pages even if some of them are missing +* (bug 13816) Filter by main namespace doesn't work on WhatLinksHere +* (bug 13822) Fatal error on some pages when calculating subpage subtitle +* (bug 13824) AJAX search suggestion now works with non-SkinTemplate skins +* Added 'application/x-dia-diagram' MediaWiki's known MIME types +* (bug 13866) skins/common/shared.css - invalid attribute fixing +* Hide edit section links on Special:Undelete +* (bug 13860) Fix "Justify paragraphs" option for Modern skin +* (bug 13168) accessibility links in Modern skin link to wrong anchor id +* (bug 13185) No line break after 'subpages' class in Modern skin +* (bug 13583) No "poweredby" in Modern skin +* (bug 13880) "Printable" link in Modern skin now formats as print mode +* (bug 13885) Bump default $wgSVGMaxSize from 1024 to 2048 pixels +* (bug 13891) Show categories box even if all categories are hidden and user has + "show hidden categories" option on +* (bug 13915) Undefined variable $wltsfield in includes/SpecialWatchlist.php +* (bug 13913) Special:Whatlinkshere now has correct HTML markup +* (bug 13905) Blacklist Mac IE from HttpOnly cookies; it eats them sometimes +* (bug 13922) Fix bad HTML on empty Special:Prefixindex and Special:Allpages +* (bug 13924) Fix bad HTML on power search form +* (bug 13820) Fix updater for rev_parent_id population +* (bug 13925) Fix bad HTML on search results list +* (bug 13934) Fixing the link to GNU General Public License Version 2 +* Show correct accesskey prefix for Firefox 3 beta (Alt-Shift-, not Alt-) +* (bug 13949) Special:PrefixIndex/AllPages paging links contain invalid XML +* (bug 13770) Use Preprocessor_Hash by default to avoid missing DOM module errors +* (bug 13982) Disable ccmeonemails preference when user-to-user mails disabled +* (bug 13615) Update case mappings and normalization to Unicode 5.1.0 + Note that case mappings will only be used if mbstring extension is not present. +* (bug 14044) Don't increment page view counters on views from bot users +* (bug 14042) Calling Database::limitResult() misplaced the comment in the log file +* (bug 14047) Fix regression in installer which hid DB-specific options + Also makes SQLite path configurable in the installer. +* (bug 13546) Follow image redirects on image page +* (bug 12644) Template list on edit page now sorted on preview +* (bug 14058) Support pipe trick for namespaces and interwikis with "-" +* Message name filter on Special:Allmessages now case-insensitive +* (bug 13943) Fix image redirect behaviour on image pages +* (bug 14093) Do 'sysop' => 'protect' magic in Title::isValidMoveOperation +* (bug 14063) Power search form missing
' + ); + wfRunHooks( 'PageHistoryBeforeList', array( &$this->mArticle ) ); - /** + /** * Do the list */ - $pager = new PageHistoryPager( $this ); + $pager = new PageHistoryPager( $this, $year, $month ); $this->linesonpage = $pager->getNumRows(); $wgOut->addHTML( - $pager->getNavigationBar() . - $this->beginHistoryList() . + $pager->getNavigationBar() . + $this->beginHistoryList() . $pager->getBody() . $this->endHistoryList() . $pager->getNavigationBar() ); - wfProfileOut( $fname ); + + wfProfileOut( __METHOD__ ); + } + + /** + * @return string Formatted HTML + * @param int $year + * @param int $month + */ + private function getDateMenu( $year, $month ) { + # Offset overrides year/month selection + if( $month && $month !== -1 ) { + $encMonth = intval( $month ); + } else { + $encMonth = ''; + } + if( $year ) { + $encYear = intval( $year ); + } else if( $encMonth ) { + $thisMonth = intval( gmdate( 'n' ) ); + $thisYear = intval( gmdate( 'Y' ) ); + if( intval($encMonth) > $thisMonth ) { + $thisYear--; + } + $encYear = $thisYear; + } else { + $encYear = ''; + } + return Xml::label( wfMsg( 'year' ), 'year' ) . ' '. + Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) . + ' '. + Xml::label( wfMsg( 'month' ), 'month' ) . ' '. + Xml::monthSelector( $encMonth, -1 ); } - /** @todo document */ + /** + * Creates begin of history list with a submit button + * + * @return string HTML output + */ function beginHistoryList() { - global $wgTitle; + global $wgTitle, $wgScript, $wgEnableHtmlDiff; $this->lastdate = ''; $s = wfMsgExt( 'histlegend', array( 'parse') ); - $s .= '
'; - $prefixedkey = htmlspecialchars($wgTitle->getPrefixedDbKey()); - - // The following line is SUPPOSED to have double-quotes around the - // $prefixedkey variable, because htmlspecialchars() doesn't escape - // single-quotes. - // - // On at least two occasions people have changed it to single-quotes, - // which creates invalid HTML and incorrect display of the resulting - // link. - // - // Please do not break this a third time. Thank you for your kind - // consideration and cooperation. - // - $s .= "\n"; - - $s .= $this->submitButton(); + $s .= Xml::openElement( 'form', array( 'action' => $wgScript, 'id' => 'mw-history-compare' ) ); + $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ); + if( $wgEnableHtmlDiff ) { + $s .= $this->submitButton( wfMsg( 'visualcomparison'), + array( + 'name' => 'htmldiff', + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-visualcomparison' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + $s .= $this->submitButton( wfMsg( 'wikicodecomparison'), + array( + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + } else { + $s .= $this->submitButton( wfMsg( 'compareselectedversions'), + array( + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + } $s .= ''; - $s .= $this->submitButton( array( 'id' => 'historysubmit' ) ); + if( $wgEnableHtmlDiff ) { + $s .= $this->submitButton( wfMsg( 'visualcomparison'), + array( + 'name' => 'htmldiff', + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-visualcomparison' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + $s .= $this->submitButton( wfMsg( 'wikicodecomparison'), + array( + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + } else { + $s .= $this->submitButton( wfMsg( 'compareselectedversions'), + array( + 'class' => 'historysubmit', + 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + } $s .= '
'; return $s; } - /** @todo document */ - function submitButton( $bits = array() ) { - return ( $this->linesonpage > 0 ) - ? wfElement( 'input', array_merge( $bits, - array( - 'class' => 'historysubmit', - 'type' => 'submit', - 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), - 'title' => wfMsg( 'tooltip-compareselectedversions' ).' ['.wfMsg( 'accesskey-compareselectedversions' ).']', - 'value' => wfMsg( 'compareselectedversions' ), - ) ) ) - : ''; + /** + * Creates a submit button + * + * @param array $attributes attributes + * @return string HTML output for the submit button + */ + function submitButton($message, $attributes = array() ) { + # Disable submit button if history has 1 revision only + if( $this->linesonpage > 1 ) { + return Xml::submitButton( $message , $attributes ); + } else { + return ''; + } } /** @@ -170,8 +270,8 @@ * * @todo document some more, and maybe clean up the code (some params redundant?) * - * @param object $row The database row corresponding to the line (or is it the previous line?). - * @param object $next The database row corresponding to the next line (or is it this one?). + * @param Row $row The database row corresponding to the previous line. + * @param mixed $next The database row corresponding to the next line. * @param int $counter Apparently a counter of what row number we're at, counted from the top row = 1. * @param $notificationtimestamp * @param bool $latest Whether this row corresponds to the page's latest revision. @@ -183,104 +283,98 @@ $rev = new Revision( $row ); $rev->setTitle( $this->mTitle ); - $s = '
  • '; $curlink = $this->curLink( $rev, $latest ); $lastlink = $this->lastLink( $rev, $next, $counter ); $arbitrary = $this->diffButtons( $rev, $firstInList, $counter ); $link = $this->revLink( $rev ); - - $user = $this->mSkin->userLink( $rev->getUser(), $rev->getUserText() ) - . $this->mSkin->userToolLinks( $rev->getUser(), $rev->getUserText() ); - - $s .= "($curlink) ($lastlink) $arbitrary"; - + + $s = "($curlink) ($lastlink) $arbitrary"; + if( $wgUser->isAllowed( 'deleterevision' ) ) { $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); if( $firstInList ) { // We don't currently handle well changing the top revision's settings - $del = wfMsgHtml( 'rev-delundel' ); + $del = $this->message['rev-delundel']; } else if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) { - // If revision was hidden from sysops - $del = wfMsgHtml( 'rev-delundel' ); + // If revision was hidden from sysops + $del = $this->message['rev-delundel']; } else { $del = $this->mSkin->makeKnownLinkObj( $revdel, - wfMsg( 'rev-delundel' ), + $this->message['rev-delundel'], 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) . '&oldid=' . urlencode( $rev->getId() ) ); + // Bolden oversighted content + if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) + $del = "$del"; } - $s .= " ($del) "; + $s .= " ($del) "; } - + $s .= " $link"; - #getUser is safe, but this avoids making the invalid untargeted contribs links - if( $row->rev_deleted & Revision::DELETED_USER ) { - $user = '' . wfMsg('rev-deleted-user') . ''; - } - $s .= " $user"; + $s .= " " . $this->mSkin->revUserTools( $rev, true ) . ""; if( $row->rev_minor_edit ) { - $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') ); + $s .= ' ' . Xml::element( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') ); } - if (!is_null($size = $rev->getSize())) { - if ($size == 0) - $stxt = wfMsgHtml('historyempty'); - else - $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) ); - $s .= " $stxt"; + if( !is_null( $size = $rev->getSize() ) && $rev->userCan( Revision::DELETED_TEXT ) ) { + $s .= ' ' . $this->mSkin->formatRevisionSize( $size ); } - #getComment is safe, but this is better formatted - if( $rev->isDeleted( Revision::DELETED_COMMENT ) ) { - $s .= " " . - wfMsgHtml( 'rev-deleted-comment' ) . ""; - } else { - $s .= $this->mSkin->revComment( $rev ); - } - - if ($notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp)) { + $s .= $this->mSkin->revComment( $rev, false, true ); + + if( $notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp) ) { $s .= ' ' . wfMsgHtml( 'updatedmarker' ) . ''; } - #add blurb about text having been deleted - if( $row->rev_deleted & Revision::DELETED_TEXT ) { - $s .= ' ' . wfMsgHtml( 'deletedrev' ); + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $s .= ' ' . wfMsgHtml( 'deletedrev' ) . ''; } - + $tools = array(); - - if ( !is_null( $next ) && is_object( $next ) ) { - if( $wgUser->isAllowed( 'rollback' ) && $latest ) { - $tools[] = '' - . $this->mSkin->buildRollbackLink( $rev ) - . ''; + + if( !is_null( $next ) && is_object( $next ) ) { + if( $latest && $this->mTitle->userCan( 'rollback' ) && $this->mTitle->userCan( 'edit' ) ) { + $tools[] = ''.$this->mSkin->buildRollbackLink( $rev ).''; } - $undolink = $this->mSkin->makeKnownLinkObj( - $this->mTitle, - wfMsgHtml( 'editundo' ), - 'action=edit&undoafter=' . $next->rev_id . '&undo=' . $rev->getId() - ); - $tools[] = "{$undolink}"; + if( $this->mTitle->quickUserCan( 'edit' ) && !$rev->isDeleted( Revision::DELETED_TEXT ) && + !$next->rev_deleted & Revision::DELETED_TEXT ) + { + # Create undo tooltip for the first (=latest) line only + $undoTooltip = $latest + ? array( 'title' => wfMsg( 'tooltip-undo' ) ) + : array(); + $undolink = $this->mSkin->link( + $this->mTitle, + wfMsgHtml( 'editundo' ), + $undoTooltip, + array( 'action' => 'edit', 'undoafter' => $next->rev_id, 'undo' => $rev->getId() ), + array( 'known', 'noclasses' ) + ); + $tools[] = "{$undolink}"; + } } - + if( $tools ) { $s .= ' (' . implode( ' | ', $tools ) . ')'; } - - wfRunHooks( 'PageHistoryLineEnding', array( &$row , &$s ) ); - - $s .= "
  • \n"; - return $s; + wfRunHooks( 'PageHistoryLineEnding', array( $this, &$row , &$s ) ); + + return "
  • $s
  • \n"; } - - /** @todo document */ + + /** + * Create a link to view this revision of the page + * @param Revision $rev + * @returns string + */ function revLink( $rev ) { global $wgLang; $date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true ); if( $rev->userCan( Revision::DELETED_TEXT ) ) { $link = $this->mSkin->makeKnownLinkObj( - $this->mTitle, $date, "oldid=" . $rev->getId() ); + $this->mTitle, $date, "oldid=" . $rev->getId() ); } else { $link = $date; } @@ -290,53 +384,61 @@ return $link; } - /** @todo document */ + /** + * Create a diff-to-current link for this revision for this page + * @param Revision $rev + * @param Bool $latest, this is the latest revision of the page? + * @returns string + */ function curLink( $rev, $latest ) { - $cur = wfMsgExt( 'cur', array( 'escape') ); + $cur = $this->message['cur']; if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) { return $cur; } else { - return $this->mSkin->makeKnownLinkObj( - $this->mTitle, $cur, - 'diff=' . $this->getLatestID() . - "&oldid=" . $rev->getId() ); + return $this->mSkin->makeKnownLinkObj( $this->mTitle, $cur, + 'diff=' . $this->mTitle->getLatestRevID() . "&oldid=" . $rev->getId() ); } } - /** @todo document */ - function lastLink( $rev, $next, $counter ) { - $last = wfMsgExt( 'last', array( 'escape' ) ); - if ( is_null( $next ) ) { + /** + * Create a diff-to-previous link for this revision for this page. + * @param Revision $prevRev, the previous revision + * @param mixed $next, the newer revision + * @param int $counter, what row on the history list this is + * @returns string + */ + function lastLink( $prevRev, $next, $counter ) { + $last = $this->message['last']; + # $next may either be a Row, null, or "unkown" + $nextRev = is_object($next) ? new Revision( $next ) : $next; + if( is_null($next) ) { # Probably no next row return $last; - } elseif ( $next === 'unknown' ) { + } elseif( $next === 'unknown' ) { # Next row probably exists but is unknown, use an oldid=prev link - return $this->mSkin->makeKnownLinkObj( - $this->mTitle, - $last, - "diff=" . $rev->getId() . "&oldid=prev" ); - } elseif( !$rev->userCan( Revision::DELETED_TEXT ) ) { + return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last, + "diff=" . $prevRev->getId() . "&oldid=prev" ); + } elseif( !$prevRev->userCan(Revision::DELETED_TEXT) || !$nextRev->userCan(Revision::DELETED_TEXT) ) { return $last; } else { - return $this->mSkin->makeKnownLinkObj( - $this->mTitle, - $last, - "diff=" . $rev->getId() . "&oldid={$next->rev_id}" - /*, - '', - '', - "tabindex={$counter}"*/ ); + return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last, + "diff=" . $prevRev->getId() . "&oldid={$next->rev_id}" ); } } - /** @todo document */ + /** + * Create radio buttons for page history + * + * @param object $rev Revision + * @param bool $firstInList Is this version the first one? + * @param int $counter A counter of what row number we're at, counted from the top row = 1. + * @return string HTML output for the radio buttons + */ function diffButtons( $rev, $firstInList, $counter ) { if( $this->linesonpage > 1) { $radio = array( 'type' => 'radio', 'value' => $rev->getId(), -# do we really need to flood this on every item? -# 'title' => wfMsgHtml( 'selectolderversionfordiff' ) ); if( !$rev->userCan( Revision::DELETED_TEXT ) ) { @@ -344,10 +446,10 @@ } /** @todo: move title texts to javascript */ - if ( $firstInList ) { - $first = wfElement( 'input', array_merge( - $radio, - array( + if( $firstInList ) { + $first = Xml::element( 'input', array_merge( + $radio, + array( 'style' => 'visibility:hidden', 'name' => 'oldid' ) ) ); $checkmark = array( 'checked' => 'checked' ); @@ -357,124 +459,75 @@ } else { $checkmark = array(); } - $first = wfElement( 'input', array_merge( - $radio, - $checkmark, - array( 'name' => 'oldid' ) ) ); - $checkmark = array(); - } - $second = wfElement( 'input', array_merge( + $first = Xml::element( 'input', array_merge( $radio, $checkmark, - array( 'name' => 'diff' ) ) ); + array( 'name' => 'oldid' ) ) ); + $checkmark = array(); + } + $second = Xml::element( 'input', array_merge( + $radio, + $checkmark, + array( 'name' => 'diff' ) ) ); return $first . $second; } else { return ''; } } - /** @todo document */ - function getLatestId() { - if( is_null( $this->mLatestId ) ) { - $id = $this->mTitle->getArticleID(); - $db = wfGetDB(DB_SLAVE); - $this->mLatestId = $db->selectField( 'page', - "page_latest", - array( 'page_id' => $id ), - 'PageHistory::getLatestID' ); - } - return $this->mLatestId; - } - /** * Fetch an array of revisions, specified by a given limit, offset and - * direction. This is now only used by the feeds. It was previously + * direction. This is now only used by the feeds. It was previously * used by the main UI but that's now handled by the pager. */ function fetchRevisions($limit, $offset, $direction) { - $fname = 'PageHistory::fetchRevisions'; - $dbr = wfGetDB( DB_SLAVE ); - if ($direction == PageHistory::DIR_PREV) + if( $direction == PageHistory::DIR_PREV ) list($dirs, $oper) = array("ASC", ">="); else /* $direction == PageHistory::DIR_NEXT */ list($dirs, $oper) = array("DESC", "<="); - if ($offset) + if( $offset ) $offsets = array("rev_timestamp $oper '$offset'"); else $offsets = array(); $page_id = $this->mTitle->getArticleID(); - $res = $dbr->select( - 'revision', + return $dbr->select( 'revision', Revision::selectFields(), array_merge(array("rev_page=$page_id"), $offsets), - $fname, - array('ORDER BY' => "rev_timestamp $dirs", + __METHOD__, + array( 'ORDER BY' => "rev_timestamp $dirs", 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit) - ); - - $result = array(); - while (($obj = $dbr->fetchObject($res)) != NULL) - $result[] = $obj; - - return $result; + ); } - /** @todo document */ - function getNotificationTimestamp() { - global $wgUser, $wgShowUpdatedMarker; - $fname = 'PageHistory::getNotficationTimestamp'; - - if ($this->mNotificationTimestamp !== NULL) - return $this->mNotificationTimestamp; - - if ($wgUser->isAnon() || !$wgShowUpdatedMarker) - return $this->mNotificationTimestamp = false; - - $dbr = wfGetDB(DB_SLAVE); - - $this->mNotificationTimestamp = $dbr->selectField( - 'watchlist', - 'wl_notificationtimestamp', - array( 'wl_namespace' => $this->mTitle->getNamespace(), - 'wl_title' => $this->mTitle->getDBkey(), - 'wl_user' => $wgUser->getID() - ), - $fname); - - // Don't use the special value reserved for telling whether the field is filled - if ( is_null( $this->mNotificationTimestamp ) ) { - $this->mNotificationTimestamp = false; - } - - return $this->mNotificationTimestamp; - } - /** * Output a subscription feed listing recent edits to this page. * @param string $type */ function feed( $type ) { - require_once 'SpecialRecentchanges.php'; - - global $wgFeedClasses; - if( !isset( $wgFeedClasses[$type] ) ) { - global $wgOut; - $wgOut->addWikiText( wfMsg( 'feed-invalid' ) ); + global $wgFeedClasses, $wgRequest, $wgFeedLimit; + if( !FeedUtils::checkFeedOutput($type) ) { return; } - + $feed = new $wgFeedClasses[$type]( - $this->mTitle->getPrefixedText() . ' - ' . - wfMsgForContent( 'history-feed-title' ), - wfMsgForContent( 'history-feed-description' ), - $this->mTitle->getFullUrl( 'action=history' ) ); + $this->mTitle->getPrefixedText() . ' - ' . + wfMsgForContent( 'history-feed-title' ), + wfMsgForContent( 'history-feed-description' ), + $this->mTitle->getFullUrl( 'action=history' ) ); + + // Get a limit on number of feed entries. Provide a sane default + // of 10 if none is defined (but limit to $wgFeedLimit max) + $limit = $wgRequest->getInt( 'limit', 10 ); + if( $limit > $wgFeedLimit || $limit < 1 ) { + $limit = 10; + } + $items = $this->fetchRevisions($limit, 0, PageHistory::DIR_NEXT); - $items = $this->fetchRevisions(10, 0, PageHistory::DIR_NEXT); $feed->outHeader(); if( $items ) { foreach( $items as $row ) { @@ -485,7 +538,7 @@ } $feed->outFooter(); } - + function feedEmpty() { global $wgOut; return new FeedItem( @@ -493,10 +546,10 @@ $wgOut->parse( wfMsgForContent( 'history-feed-empty' ) ), $this->mTitle->getFullUrl(), wfTimestamp( TS_MW ), - '', + '', $this->mTitle->getTalkPage()->getFullUrl() ); } - + /** * Generate a FeedItem object from a given revision table row * Borrows Recent Changes' feed generation functions for formatting; @@ -508,19 +561,19 @@ function feedItem( $row ) { $rev = new Revision( $row ); $rev->setTitle( $this->mTitle ); - $text = rcFormatDiffRow( $this->mTitle, - $this->mTitle->getPreviousRevisionID( $rev->getId() ), - $rev->getId(), - $rev->getTimestamp(), - $rev->getComment() ); - + $text = FeedUtils::formatDiffRow( $this->mTitle, + $this->mTitle->getPreviousRevisionID( $rev->getId() ), + $rev->getId(), + $rev->getTimestamp(), + $rev->getComment() ); + if( $rev->getComment() == '' ) { global $wgContLang; $title = wfMsgForContent( 'history-feed-item-nocomment', - $rev->getUserText(), - $wgContLang->timeanddate( $rev->getTimestamp() ) ); + $rev->getUserText(), + $wgContLang->timeanddate( $rev->getTimestamp() ) ); } else { - $title = $rev->getUserText() . ": " . $this->stripComment( $rev->getComment() ); + $title = $rev->getUserText() . ": " . FeedItem::stripComment( $rev->getComment() ); } return new FeedItem( @@ -531,34 +584,31 @@ $rev->getUserText(), $this->mTitle->getTalkPage()->getFullUrl() ); } - - /** - * Quickie hack... strip out wikilinks to more legible form from the comment. - */ - function stripComment( $text ) { - return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text ); - } } /** - * @addtogroup Pager + * @ingroup Pager */ class PageHistoryPager extends ReverseChronologicalPager { - public $mLastRow = false, $mPageHistory; - - function __construct( $pageHistory ) { + public $mLastRow = false, $mPageHistory, $mTitle; + + function __construct( $pageHistory, $year='', $month='' ) { parent::__construct(); $this->mPageHistory = $pageHistory; + $this->mTitle =& $this->mPageHistory->mTitle; + $this->getDateCond( $year, $month ); } function getQueryInfo() { - return array( - 'tables' => 'revision', - 'fields' => Revision::selectFields(), - 'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ), - 'options' => array( 'USE INDEX' => 'page_timestamp' ) + $queryInfo = array( + 'tables' => array('revision'), + 'fields' => Revision::selectFields(), + 'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ), + 'options' => array( 'USE INDEX' => array('revision' => 'page_timestamp') ) ); + wfRunHooks( 'PageHistoryPager::getQueryInfo', array( &$this, &$queryInfo ) ); + return $queryInfo; } function getIndexField() { @@ -566,18 +616,18 @@ } function formatRow( $row ) { - if ( $this->mLastRow ) { - $latest = $this->mCounter == 1 && $this->mOffset == ''; + if( $this->mLastRow ) { + $latest = $this->mCounter == 1 && $this->mIsFirst; $firstInList = $this->mCounter == 1; - $s = $this->mPageHistory->historyLine( $this->mLastRow, $row, $this->mCounter++, - $this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList ); + $s = $this->mPageHistory->historyLine( $this->mLastRow, $row, $this->mCounter++, + $this->mTitle->getNotificationTimestamp(), $latest, $firstInList ); } else { $s = ''; } $this->mLastRow = $row; return $s; } - + function getStartBody() { $this->mLastRow = false; $this->mCounter = 1; @@ -585,12 +635,12 @@ } function getEndBody() { - if ( $this->mLastRow ) { - $latest = $this->mCounter == 1 && $this->mOffset == 0; + if( $this->mLastRow ) { + $latest = $this->mCounter == 1 && $this->mIsFirst; $firstInList = $this->mCounter == 1; - if ( $this->mIsBackwards ) { + if( $this->mIsBackwards ) { # Next row is unknown, but for UI reasons, probably exists if an offset has been specified - if ( $this->mOffset == '' ) { + if( $this->mOffset == '' ) { $next = null; } else { $next = 'unknown'; @@ -599,14 +649,11 @@ # The next row is the past-the-end row $next = $this->mPastTheEndRow; } - $s = $this->mPageHistory->historyLine( $this->mLastRow, $next, $this->mCounter++, - $this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList ); + $s = $this->mPageHistory->historyLine( $this->mLastRow, $next, $this->mCounter++, + $this->mTitle->getNotificationTimestamp(), $latest, $firstInList ); } else { $s = ''; } return $s; } } - - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/PageQueryPage.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/PageQueryPage.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/PageQueryPage.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/PageQueryPage.php Mon Oct 6 00:36:39 2008 @@ -3,24 +3,23 @@ /** * Variant of QueryPage which formats the result as a simple link to the page * - * @package MediaWiki - * @addtogroup SpecialPage + * @ingroup SpecialPage */ class PageQueryPage extends QueryPage { /** * Format the result as a simple link to the page * - * @param Skin $skin - * @param object $row Result row + * @param $skin Skin + * @param $row Object: result row * @return string */ public function formatResult( $skin, $row ) { global $wgContLang; $title = Title::makeTitleSafe( $row->namespace, $row->title ); - return $skin->makeKnownLinkObj( $title, - htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) ); + $text = $row->title; + if ($title instanceof Title) + $text = $wgContLang->convert( $title->getPrefixedText() ); + return $skin->link( $title, htmlspecialchars($text), array(), array(), array('known', 'noclasses') ); } } - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Pager.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Pager.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Pager.php Wed Jul 18 05:52:17 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Pager.php Wed Oct 29 15:21:21 2008 @@ -1,8 +1,14 @@ mRequest = $wgRequest; - + # NB: the offset is quoted, not validated. It is treated as an # arbitrary string to support the widest variety of index types. Be # careful outputting it into HTML! $this->mOffset = $this->mRequest->getText( 'offset' ); - + # Use consistent behavior for the limit options $this->mDefaultLimit = intval( $wgUser->getOption( 'rclimit' ) ); list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset(); - + $this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' ); - $this->mIndexField = $this->getIndexField(); $this->mDb = wfGetDB( DB_SLAVE ); + + $index = $this->getIndexField(); + $order = $this->mRequest->getVal( 'order' ); + if( is_array( $index ) && isset( $index[$order] ) ) { + $this->mOrderType = $order; + $this->mIndexField = $index[$order]; + } elseif( is_array( $index ) ) { + # First element is the default + reset( $index ); + list( $this->mOrderType, $this->mIndexField ) = each( $index ); + } else { + # $index is not an array + $this->mOrderType = null; + $this->mIndexField = $index; + } + + if( !isset( $this->mDefaultDirection ) ) { + $dir = $this->getDefaultDirections(); + $this->mDefaultDirection = is_array( $dir ) + ? $dir[$this->mOrderType] + : $dir; + } } /** - * Do the query, using information from the object context. This function - * has been kept minimal to make it overridable if necessary, to allow for + * Do the query, using information from the object context. This function + * has been kept minimal to make it overridable if necessary, to allow for * result sets formed from multiple DB queries. */ function doQuery() { @@ -107,15 +148,35 @@ $this->mResult = $this->reallyDoQuery( $this->mOffset, $queryLimit, $descending ); $this->extractResultInfo( $this->mOffset, $queryLimit, $this->mResult ); $this->mQueryDone = true; - + $this->preprocessResults( $this->mResult ); $this->mResult->rewind(); // Paranoia wfProfileOut( $fname ); } + + /** + * Return the result wrapper. + */ + function getResult() { + return $this->mResult; + } + + /** + * Set the offset from an other source than $wgRequest + */ + function setOffset( $offset ) { + $this->mOffset = $offset; + } + /** + * Set the limit from an other source than $wgRequest + */ + function setLimit( $limit ) { + $this->mLimit = $limit; + } /** - * Extract some useful data from the result object for use by + * Extract some useful data from the result object for use by * the navigation bar, put it into $this */ function extractResultInfo( $offset, $limit, ResultWrapper $res ) { @@ -180,6 +241,7 @@ $fields = $info['fields']; $conds = isset( $info['conds'] ) ? $info['conds'] : array(); $options = isset( $info['options'] ) ? $info['options'] : array(); + $join_conds = isset( $info['join_conds'] ) ? $info['join_conds'] : array(); if ( $descending ) { $options['ORDER BY'] = $this->mIndexField; $operator = '>'; @@ -191,7 +253,7 @@ $conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset ); } $options['LIMIT'] = intval( $limit ); - $res = $this->mDb->select( $tables, $fields, $conds, $fname, $options ); + $res = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); return new ResultWrapper( $this->mDb, $res ); } @@ -203,7 +265,7 @@ protected function preprocessResults( $result ) {} /** - * Get the formatted result list. Calls getStartBody(), formatRow() and + * Get the formatted result list. Calls getStartBody(), formatRow() and * getEndBody(), concatenates the results and returns them. */ function getBody() { @@ -238,17 +300,28 @@ /** * Make a self-link */ - function makeLink($text, $query = NULL) { + function makeLink($text, $query = null, $type=null) { if ( $query === null ) { return $text; + } + if( $type == 'prev' || $type == 'next' ) { + $attrs = "rel=\"$type\""; + } elseif( $type == 'first' ) { + $attrs = "rel=\"start\""; } else { - return $this->getSkin()->makeKnownLinkObj( $this->getTitle(), $text, - wfArrayToCGI( $query, $this->getDefaultQuery() ) ); + # HTML 4 has no rel="end" . . . + $attrs = ''; + } + + if( $type ) { + $attrs .= " class=\"mw-{$type}link\"" ; } + return $this->getSkin()->makeKnownLinkObj( $this->getTitle(), $text, + wfArrayToCGI( $query, $this->getDefaultQuery() ), '', '', $attrs ); } /** - * Hook into getBody(), allows text to be inserted at the start. This + * Hook into getBody(), allows text to be inserted at the start. This * will be called even if there are no rows in the result set. */ function getStartBody() { @@ -263,15 +336,15 @@ } /** - * Hook into getBody(), for the bit between the start and the + * Hook into getBody(), for the bit between the start and the * end when there are no rows */ function getEmptyBody() { return ''; } - + /** - * Title used for self-links. Override this if you want to be able to + * Title used for self-links. Override this if you want to be able to * use a title other than $wgTitle */ function getTitle() { @@ -290,8 +363,8 @@ } /** - * Get an array of query parameters that should be put into self-links. - * By default, all parameters passed in the URL are used, except for a + * Get an array of query parameters that should be put into self-links. + * By default, all parameters passed in the URL are used, except for a * short blacklist. */ function getDefaultQuery() { @@ -301,6 +374,9 @@ unset( $this->mDefaultQuery['dir'] ); unset( $this->mDefaultQuery['offset'] ); unset( $this->mDefaultQuery['limit'] ); + unset( $this->mDefaultQuery['order'] ); + unset( $this->mDefaultQuery['month'] ); + unset( $this->mDefaultQuery['year'] ); } return $this->mDefaultQuery; } @@ -316,16 +392,16 @@ } /** - * Get a query array for the prev, next, first and last links. + * Get a URL query array for the prev, next, first and last links. */ function getPagingQueries() { if ( !$this->mQueryDone ) { $this->doQuery(); } - + # Don't announce the limit everywhere if it's the default $urlLimit = $this->mLimit == $this->mDefaultLimit ? '' : $this->mLimit; - + if ( $this->mIsFirst ) { $prev = false; $first = false; @@ -354,7 +430,7 @@ $links = array(); foreach ( $queries as $type => $query ) { if ( $query !== false ) { - $links[$type] = $this->makeLink( $linkTexts[$type], $queries[$type] ); + $links[$type] = $this->makeLink( $linkTexts[$type], $queries[$type], $type ); } elseif ( isset( $disabledTexts[$type] ) ) { $links[$type] = $disabledTexts[$type]; } else { @@ -374,21 +450,21 @@ } foreach ( $this->mLimitsShown as $limit ) { $links[] = $this->makeLink( $wgLang->formatNum( $limit ), - array( 'offset' => $offset, 'limit' => $limit ) ); + array( 'offset' => $offset, 'limit' => $limit ), 'num' ); } return $links; } /** - * Abstract formatting function. This should return an HTML string + * Abstract formatting function. This should return an HTML string * representing the result row $row. Rows will be concatenated and * returned by getBody() */ abstract function formatRow( $row ); /** - * This function should be overridden to provide all parameters - * needed for the main paged query. It returns an associative + * This function should be overridden to provide all parameters + * needed for the main paged query. It returns an associative * array with the following elements: * tables => Table(s) for passing to Database::select() * fields => Field(s) for passing to Database::select(), may be * @@ -398,54 +474,123 @@ abstract function getQueryInfo(); /** - * This function should be overridden to return the name of the - * index field. + * This function should be overridden to return the name of the index fi- + * eld. If the pager supports multiple orders, it may return an array of + * 'querykey' => 'indexfield' pairs, so that a request with &count=querykey + * will use indexfield to sort. In this case, the first returned key is + * the default. + * + * Needless to say, it's really not a good idea to use a non-unique index + * for this! That won't page right. */ abstract function getIndexField(); + + /** + * Return the default sorting direction: false for ascending, true for de- + * scending. You can also have an associative array of ordertype => dir, + * if multiple order types are supported. In this case getIndexField() + * must return an array, and the keys of that must exactly match the keys + * of this. + * + * For backward compatibility, this method's return value will be ignored + * if $this->mDefaultDirection is already set when the constructor is + * called, for instance if it's statically initialized. In that case the + * value of that variable (which must be a boolean) will be used. + * + * Note that despite its name, this does not return the value of the + * $this->mDefaultDirection member variable. That's the default for this + * particular instantiation, which is a single value. This is the set of + * all defaults for the class. + */ + protected function getDefaultDirections() { return false; } } /** * IndexPager with an alphabetic list and a formatted navigation bar - * @addtogroup Pager + * @ingroup Pager */ abstract class AlphabeticPager extends IndexPager { - public $mDefaultDirection = false; - - function __construct() { - parent::__construct(); - } - - /** - * Shamelessly stolen bits from ReverseChronologicalPager, d - * didn't want to do class magic as may be still revamped + /** + * Shamelessly stolen bits from ReverseChronologicalPager, + * didn't want to do class magic as may be still revamped */ function getNavigationBar() { global $wgLang; - + + if( isset( $this->mNavigationBar ) ) { + return $this->mNavigationBar; + } + + $opts = array( 'parsemag', 'escapenoentities' ); $linkTexts = array( - 'prev' => wfMsgHtml( "prevn", $this->mLimit ), - 'next' => wfMsgHtml( 'nextn', $this->mLimit ), - 'first' => wfMsgHtml('page_first'), /* Introduced the message */ - 'last' => wfMsgHtml( 'page_last' ) /* Introduced the message */ + 'prev' => wfMsgExt( 'prevn', $opts, $wgLang->formatNum( $this->mLimit ) ), + 'next' => wfMsgExt( 'nextn', $opts, $wgLang->formatNum($this->mLimit ) ), + 'first' => wfMsgExt( 'page_first', $opts ), + 'last' => wfMsgExt( 'page_last', $opts ) ); - + $pagingLinks = $this->getPagingLinks( $linkTexts ); $limitLinks = $this->getLimitLinks(); $limits = implode( ' | ', $limitLinks ); - - $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); + + $this->mNavigationBar = + "({$pagingLinks['first']} | {$pagingLinks['last']}) " . + wfMsgHtml( 'viewprevnext', $pagingLinks['prev'], + $pagingLinks['next'], $limits ); + + if( !is_array( $this->getIndexField() ) ) { + # Early return to avoid undue nesting + return $this->mNavigationBar; + } + + $extra = ''; + $first = true; + $msgs = $this->getOrderTypeMessages(); + foreach( array_keys( $msgs ) as $order ) { + if( $first ) { + $first = false; + } else { + $extra .= ' | '; + } + + if( $order == $this->mOrderType ) { + $extra .= wfMsgHTML( $msgs[$order] ); + } else { + $extra .= $this->makeLink( + wfMsgHTML( $msgs[$order] ), + array( 'order' => $order ) + ); + } + } + + if( $extra !== '' ) { + $this->mNavigationBar .= " ($extra)"; + } + return $this->mNavigationBar; - + } + + /** + * If this supports multiple order type messages, give the message key for + * enabling each one in getNavigationBar. The return type is an associa- + * tive array whose keys must exactly match the keys of the array returned + * by getIndexField(), and whose values are message keys. + * @return array + */ + protected function getOrderTypeMessages() { + return null; } } /** * IndexPager with a formatted navigation bar - * @addtogroup Pager + * @ingroup Pager */ abstract class ReverseChronologicalPager extends IndexPager { public $mDefaultDirection = true; + public $mYear; + public $mMonth; function __construct() { parent::__construct(); @@ -457,26 +602,74 @@ if ( isset( $this->mNavigationBar ) ) { return $this->mNavigationBar; } + $nicenumber = $wgLang->formatNum( $this->mLimit ); $linkTexts = array( - 'prev' => wfMsgHtml( "prevn", $this->mLimit ), - 'next' => wfMsgHtml( 'nextn', $this->mLimit ), - 'first' => wfMsgHtml('histlast'), + 'prev' => wfMsgExt( 'pager-newer-n', array( 'parsemag' ), $nicenumber ), + 'next' => wfMsgExt( 'pager-older-n', array( 'parsemag' ), $nicenumber ), + 'first' => wfMsgHtml( 'histlast' ), 'last' => wfMsgHtml( 'histfirst' ) ); $pagingLinks = $this->getPagingLinks( $linkTexts ); $limitLinks = $this->getLimitLinks(); $limits = implode( ' | ', $limitLinks ); - - $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . + + $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); return $this->mNavigationBar; } + + function getDateCond( $year, $month ) { + $year = intval($year); + $month = intval($month); + // Basic validity checks + $this->mYear = $year > 0 ? $year : false; + $this->mMonth = ($month > 0 && $month < 13) ? $month : false; + // Given an optional year and month, we need to generate a timestamp + // to use as "WHERE rev_timestamp <= result" + // Examples: year = 2006 equals < 20070101 (+000000) + // year=2005, month=1 equals < 20050201 + // year=2005, month=12 equals < 20060101 + if ( !$this->mYear && !$this->mMonth ) { + return; + } + if ( $this->mYear ) { + $year = $this->mYear; + } else { + // If no year given, assume the current one + $year = gmdate( 'Y' ); + // If this month hasn't happened yet this year, go back to last year's month + if( $this->mMonth > gmdate( 'n' ) ) { + $year--; + } + } + if ( $this->mMonth ) { + $month = $this->mMonth + 1; + // For December, we want January 1 of the next year + if ($month > 12) { + $month = 1; + $year++; + } + } else { + // No month implies we want up to the end of the year in question + $month = 1; + $year++; + } + // Y2K38 bug + if ( $year > 2032 ) { + $year = 2032; + } + $ymd = (int)sprintf( "%04d%02d01", $year, $month ); + if ( $ymd > 20320101 ) { + $ymd = 20320101; + } + $this->mOffset = $this->mDb->timestamp( "${ymd}000000" ); + } } /** * Table-based display with a user-selectable sort order - * @addtogroup Pager + * @ingroup Pager */ abstract class TablePager extends IndexPager { var $mSort; @@ -501,7 +694,7 @@ global $wgStylePath; $tableClass = htmlspecialchars( $this->getTableClass() ); $sortClass = htmlspecialchars( $this->getSortHeaderClass() ); - + $s = "\n"; $fields = $this->getFieldNames(); @@ -528,7 +721,7 @@ $alt = htmlspecialchars( wfMsg( 'ascending_abbrev' ) ); } $image = htmlspecialchars( "$wgStylePath/common/images/$image" ); - $link = $this->makeLink( + $link = $this->makeLink( "\"$alt\"" . htmlspecialchars( $name ), $query ); $s .= "\n"; @@ -540,7 +733,7 @@ } } $s .= "\n"; - return $s; + return $s; } function getEndBody() { @@ -646,8 +839,8 @@ } /** - * Get elements for use in a method="get" form. - * Resubmits all defined elements of the $_GET array, except for a + * Get elements for use in a method="get" form. + * Resubmits all defined elements of the $_GET array, except for a * blacklist, passed in the $blacklist parameter. */ function getHiddenFields( $blacklist = array() ) { @@ -673,10 +866,10 @@ $url = $this->getTitle()->escapeLocalURL(); $msgSubmit = wfMsgHtml( 'table_pager_limit_submit' ); return - "" . - wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) . + "" . + wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) . "\n\n" . - $this->getHiddenFields( 'limit' ) . + $this->getHiddenFields( array('limit','title') ) . "\n"; } @@ -690,7 +883,7 @@ /** * Format a table cell. The return value should be HTML, but use an empty - * string not   for empty cells. Do not include the . + * string not   for empty cells. Do not include the . * * The current result row is available as $this->mCurrentRow, in case you * need more context. @@ -712,4 +905,3 @@ */ abstract function getFieldNames(); } - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/PatrolLog.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/PatrolLog.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/PatrolLog.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/PatrolLog.php Mon Apr 14 03:45:50 2008 @@ -31,7 +31,7 @@ return false; } } - + /** * Generate the log action text corresponding to a patrol log item * @@ -67,7 +67,7 @@ return ''; } } - + /** * Prepare log parameters for a patrolled change * @@ -82,6 +82,4 @@ (int)$auto ); } - } - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Profiler.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Profiler.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Profiler.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Profiler.php Mon Oct 6 03:30:38 2008 @@ -1,31 +1,47 @@ profileIn($functionname); + $wgProfiler->profileIn( $functionname ); } /** + * Stop profiling of a function * @param $functioname name of the function we have profiled */ -function wfProfileOut($functionname = 'missing') { +function wfProfileOut( $functionname = 'missing' ) { global $wgProfiler; - $wgProfiler->profileOut($functionname); + $wgProfiler->profileOut( $functionname ); } -function wfGetProfilingOutput($start, $elapsed) { +/** + * Returns a profiling output to be stored in debug file + * + * @param float $start + * @param float $elapsed time elapsed since the beginning of the request + */ +function wfGetProfilingOutput( $start, $elapsed ) { global $wgProfiler; - return $wgProfiler->getOutput($start, $elapsed); + return $wgProfiler->getOutput( $start, $elapsed ); } +/** + * Close opened profiling sections + */ function wfProfileClose() { global $wgProfiler; $wgProfiler->close(); @@ -39,8 +55,8 @@ } /** + * @ingroup Profiler * @todo document - * @addtogroup Profiler */ class Profiler { var $mStack = array (), $mWorkStack = array (), $mCollated = array (); @@ -57,38 +73,48 @@ } } - function profileIn($functionname) { + /** + * Called by wfProfieIn() + * @param $functionname string + */ + function profileIn( $functionname ) { global $wgDebugFunctionEntry; - if ($wgDebugFunctionEntry && function_exists('wfDebug')) { - wfDebug(str_repeat(' ', count($this->mWorkStack)).'Entering '.$functionname."\n"); + + if( $wgDebugFunctionEntry ){ + $this->debug( str_repeat( ' ', count( $this->mWorkStack ) ) . 'Entering ' . $functionname . "\n" ); } - $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), $this->getTime(), memory_get_usage()); + + $this->mWorkStack[] = array( $functionname, count( $this->mWorkStack ), $this->getTime(), memory_get_usage() ); } + /** + * Called by wfProfieOut() + * @param $functionname string + */ function profileOut($functionname) { + global $wgDebugFunctionEntry; + $memory = memory_get_usage(); $time = $this->getTime(); - global $wgDebugFunctionEntry; - - if ($wgDebugFunctionEntry && function_exists('wfDebug')) { - wfDebug(str_repeat(' ', count($this->mWorkStack) - 1).'Exiting '.$functionname."\n"); + if( $wgDebugFunctionEntry ){ + $this->debug( str_repeat( ' ', count( $this->mWorkStack ) - 1 ) . 'Exiting ' . $functionname . "\n" ); } $bit = array_pop($this->mWorkStack); if (!$bit) { - wfDebug("Profiling error, !\$bit: $functionname\n"); + $this->debug("Profiling error, !\$bit: $functionname\n"); } else { - //if ($wgDebugProfiling) { - if ($functionname == 'close') { + //if( $wgDebugProfiling ){ + if( $functionname == 'close' ){ $message = "Profile section ended by close(): {$bit[0]}"; - wfDebug( "$message\n" ); + $this->debug( "$message\n" ); $this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 ); } - elseif ($bit[0] != $functionname) { + elseif( $bit[0] != $functionname ){ $message = "Profiling error: in({$bit[0]}), out($functionname)"; - wfDebug( "$message\n" ); + $this->debug( "$message\n" ); $this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 ); } //} @@ -98,70 +124,86 @@ } } + /** + * called by wfProfileClose() + */ function close() { - while (count($this->mWorkStack)) { - $this->profileOut('close'); + while( count( $this->mWorkStack ) ){ + $this->profileOut( 'close' ); } } + /** + * called by wfGetProfilingOutput() + */ function getOutput() { - global $wgDebugFunctionEntry; + global $wgDebugFunctionEntry, $wgProfileCallTree; $wgDebugFunctionEntry = false; - if (!count($this->mStack) && !count($this->mCollated)) { + if( !count( $this->mStack ) && !count( $this->mCollated ) ){ return "No profiling output\n"; } $this->close(); - global $wgProfileCallTree; - if ($wgProfileCallTree) { + if( $wgProfileCallTree ){ return $this->getCallTree(); } else { return $this->getFunctionReport(); } } - function getCallTree($start = 0) { - return implode('', array_map(array (& $this, 'getCallTreeLine'), $this->remapCallTree($this->mStack))); + /** + * returns a tree of function call instead of a list of functions + */ + function getCallTree() { + return implode( '', array_map( array( &$this, 'getCallTreeLine' ), $this->remapCallTree( $this->mStack ) ) ); } - function remapCallTree($stack) { - if (count($stack) < 2) { + /** + * Recursive function the format the current profiling array into a tree + * + * @param $stack profiling array + */ + function remapCallTree( $stack ) { + if( count( $stack ) < 2 ){ return $stack; } $outputs = array (); - for ($max = count($stack) - 1; $max > 0;) { + for( $max = count( $stack ) - 1; $max > 0; ){ /* Find all items under this entry */ $level = $stack[$max][1]; $working = array (); - for ($i = $max -1; $i >= 0; $i --) { - if ($stack[$i][1] > $level) { + for( $i = $max -1; $i >= 0; $i-- ){ + if( $stack[$i][1] > $level ){ $working[] = $stack[$i]; } else { break; } } - $working = $this->remapCallTree(array_reverse($working)); - $output = array (); - foreach ($working as $item) { - array_push($output, $item); + $working = $this->remapCallTree( array_reverse( $working ) ); + $output = array(); + foreach( $working as $item ){ + array_push( $output, $item ); } - array_unshift($output, $stack[$max]); + array_unshift( $output, $stack[$max] ); $max = $i; - array_unshift($outputs, $output); + array_unshift( $outputs, $output ); } - $final = array (); - foreach ($outputs as $output) { - foreach ($output as $item) { + $final = array(); + foreach( $outputs as $output ){ + foreach( $output as $item ){ $final[] = $item; } } return $final; } + /** + * Callback to get a formatted line for the call tree + */ function getCallTreeLine($entry) { - list ($fname, $level, $start, /* $x */, $end) = $entry; + list( $fname, $level, $start, /* $x */, $end) = $entry; $delta = $end - $start; $space = str_repeat(' ', $level); @@ -182,66 +224,72 @@ return $ru['ru_utime.tv_sec'].' '.$ru['ru_utime.tv_usec'] / 1e6; } + /** + * Returns a list of profiled functions. + * Also log it into the database if $wgProfileToDatabase is set to true. + */ function getFunctionReport() { + global $wgProfileToDatabase; + $width = 140; $nameWidth = $width - 65; $format = "%-{$nameWidth}s %6d %13.3f %13.3f %13.3f%% %9d (%13.3f -%13.3f) [%d]\n"; $titleFormat = "%-{$nameWidth}s %6s %13s %13s %13s %9s\n"; $prof = "\nProfiling data\n"; - $prof .= sprintf($titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem'); + $prof .= sprintf( $titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem' ); $this->mCollated = array (); $this->mCalls = array (); $this->mMemory = array (); # Estimate profiling overhead $profileCount = count($this->mStack); - wfProfileIn('-overhead-total'); - for ($i = 0; $i < $profileCount; $i ++) { - wfProfileIn('-overhead-internal'); - wfProfileOut('-overhead-internal'); + wfProfileIn( '-overhead-total' ); + for( $i = 0; $i < $profileCount; $i ++ ){ + wfProfileIn( '-overhead-internal' ); + wfProfileOut( '-overhead-internal' ); } - wfProfileOut('-overhead-total'); + wfProfileOut( '-overhead-total' ); # First, subtract the overhead! - foreach ($this->mStack as $entry) { + foreach( $this->mStack as $entry ){ $fname = $entry[0]; $start = $entry[2]; $end = $entry[4]; $elapsed = $end - $start; $memory = $entry[5] - $entry[3]; - if ($fname == '-overhead-total') { + if( $fname == '-overhead-total' ){ $overheadTotal[] = $elapsed; $overheadMemory[] = $memory; } - elseif ($fname == '-overhead-internal') { + elseif( $fname == '-overhead-internal' ){ $overheadInternal[] = $elapsed; } } - $overheadTotal = array_sum($overheadTotal) / count($overheadInternal); - $overheadMemory = array_sum($overheadMemory) / count($overheadInternal); - $overheadInternal = array_sum($overheadInternal) / count($overheadInternal); + $overheadTotal = array_sum( $overheadTotal ) / count( $overheadInternal ); + $overheadMemory = array_sum( $overheadMemory ) / count( $overheadInternal ); + $overheadInternal = array_sum( $overheadInternal ) / count( $overheadInternal ); # Collate - foreach ($this->mStack as $index => $entry) { + foreach( $this->mStack as $index => $entry ){ $fname = $entry[0]; $start = $entry[2]; $end = $entry[4]; $elapsed = $end - $start; $memory = $entry[5] - $entry[3]; - $subcalls = $this->calltreeCount($this->mStack, $index); + $subcalls = $this->calltreeCount( $this->mStack, $index ); - if (!preg_match('/^-overhead/', $fname)) { + if( !preg_match( '/^-overhead/', $fname ) ){ # Adjust for profiling overhead (except special values with elapsed=0 - if ( $elapsed ) { + if( $elapsed ) { $elapsed -= $overheadInternal; $elapsed -= ($subcalls * $overheadTotal); $memory -= ($subcalls * $overheadMemory); } } - if (!array_key_exists($fname, $this->mCollated)) { + if( !array_key_exists( $fname, $this->mCollated ) ){ $this->mCollated[$fname] = 0; $this->mCalls[$fname] = 0; $this->mMemory[$fname] = 0; @@ -258,20 +306,19 @@ $this->mOverhead[$fname] += $subcalls; } - $total = @ $this->mCollated['-total']; + $total = @$this->mCollated['-total']; $this->mCalls['-overhead-total'] = $profileCount; # Output - arsort($this->mCollated, SORT_NUMERIC); - foreach ($this->mCollated as $fname => $elapsed) { + arsort( $this->mCollated, SORT_NUMERIC ); + foreach( $this->mCollated as $fname => $elapsed ){ $calls = $this->mCalls[$fname]; $percent = $total ? 100. * $elapsed / $total : 0; $memory = $this->mMemory[$fname]; $prof .= sprintf($format, substr($fname, 0, $nameWidth), $calls, (float) ($elapsed * 1000), (float) ($elapsed * 1000) / $calls, $percent, $memory, ($this->mMin[$fname] * 1000.0), ($this->mMax[$fname] * 1000.0), $this->mOverhead[$fname]); - global $wgProfileToDatabase; - if ($wgProfileToDatabase) { - Profiler :: logToDB($fname, (float) ($elapsed * 1000), $calls); + if( $wgProfileToDatabase ){ + self::logToDB($fname, (float) ($elapsed * 1000), $calls, (float) ($memory) ); } } $prof .= "\nTotal: $total\n\n"; @@ -298,39 +345,53 @@ } /** - * @static + * Log a function into the database. + * + * @param $name string: function name + * @param $timeSum float + * @param $eventCount int: number of times that function was called */ - function logToDB($name, $timeSum, $eventCount) { + static function logToDB( $name, $timeSum, $eventCount, $memorySum ){ # Do not log anything if database is readonly (bug 5375) if( wfReadOnly() ) { return; } - # Warning: $wguname is a live patch, it should be moved to Setup.php - global $wguname, $wgProfilePerHost; + global $wgProfilePerHost; - $fname = 'Profiler::logToDB'; - $dbw = wfGetDB(DB_MASTER); - if (!is_object($dbw)) + $dbw = wfGetDB( DB_MASTER ); + if( !is_object( $dbw ) ) return false; $errorState = $dbw->ignoreErrors( true ); - $profiling = $dbw->tableName('profiling'); $name = substr($name, 0, 255); - $encname = $dbw->strencode($name); - - if ($wgProfilePerHost) { - $pfhost = $wguname['nodename']; + + if( $wgProfilePerHost ){ + $pfhost = wfHostname(); } else { $pfhost = ''; } - - $sql = "UPDATE $profiling "."SET pf_count=pf_count+{$eventCount}, "."pf_time=pf_time + {$timeSum} ". - "WHERE pf_name='{$encname}' AND pf_server='{$pfhost}'"; - $dbw->query($sql); + + // Kludge + $timeSum = ($timeSum >= 0) ? $timeSum : 0; + $memorySum = ($memorySum >= 0) ? $memorySum : 0; + + $dbw->update( 'profiling', + array( + "pf_count=pf_count+{$eventCount}", + "pf_time=pf_time+{$timeSum}", + "pf_memory=pf_memory+{$memorySum}", + ), + array( + 'pf_name' => $name, + 'pf_server' => $pfhost, + ), + __METHOD__ ); + $rc = $dbw->affectedRows(); if ($rc == 0) { $dbw->insert('profiling', array ('pf_name' => $name, 'pf_count' => $eventCount, - 'pf_time' => $timeSum, 'pf_server' => $pfhost ), $fname, array ('IGNORE')); + 'pf_time' => $timeSum, 'pf_memory' => $memorySum, 'pf_server' => $pfhost ), + __METHOD__, array ('IGNORE')); } // When we upgrade to mysql 4.1, the insert+update // can be merged into just a insert with this construct added: @@ -344,10 +405,14 @@ * Get the function name of the current profiling section */ function getCurrentSection() { - $elt = end($this->mWorkStack); + $elt = end( $this->mWorkStack ); return $elt[0]; } - + + /** + * Get function caller + * @param $level int + */ static function getCaller( $level ) { $backtrace = wfDebugBacktrace(); if ( isset( $backtrace[$level] ) ) { @@ -362,6 +427,13 @@ return $caller; } + /** + * Add an entry in the debug log file + * @param $s string to output + */ + function debug( $s ) { + if( function_exists( 'wfDebug' ) ) { + wfDebug( $s ); + } + } } - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/ProfilerSimple.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/ProfilerSimple.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/ProfilerSimple.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/ProfilerSimple.php Tue May 20 13:13:28 2008 @@ -1,18 +1,22 @@ mWorkStack[] = array( '-total', 0, $wgRequestTime,$this->getCpuTime($wgRUstart)); @@ -72,10 +76,14 @@ $message = "Profile section ended by close(): {$ofname}"; $functionname = $ofname; $this->debug( "$message\n" ); + $this->mCollated[$message] = array( + 'real' => 0.0, 'count' => 1); } elseif ($ofname != $functionname) { $message = "Profiling error: in({$ofname}), out($functionname)"; $this->debug( "$message\n" ); + $this->mCollated[$message] = array( + 'real' => 0.0, 'count' => 1); } $entry =& $this->mCollated[$functionname]; $elapsedcpu = $this->getCpuTime() - $octime; @@ -101,7 +109,7 @@ if ( function_exists( 'getrusage' ) ) { if ( $ru == null ) $ru = getrusage(); - return ($ru['ru_utime.tv_sec'] + $ru['ru_stime.tv_sec'] + ($ru['ru_utime.tv_usec'] + + return ($ru['ru_utime.tv_sec'] + $ru['ru_stime.tv_sec'] + ($ru['ru_utime.tv_usec'] + $ru['ru_stime.tv_usec']) * 1e-6); } else { return 0; @@ -115,11 +123,4 @@ list($a,$b)=explode(" ",$time); return (float)($a+$b); } - - function debug( $s ) { - if (function_exists( 'wfDebug' ) ) { - wfDebug( $s ); - } - } } - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/ProfilerSimpleUDP.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/ProfilerSimpleUDP.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/ProfilerSimpleUDP.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/ProfilerSimpleUDP.php Tue May 20 13:13:28 2008 @@ -1,17 +1,19 @@ mCollated['-total']['real'] < $this->mMinimumTime ) { # Less than minimum, ignore @@ -37,4 +39,3 @@ socket_sendto($sock,$packet,$plength,0x100,$wgUDPProfilerHost,$wgUDPProfilerPort); } } - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/ProfilerStub.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/ProfilerStub.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/ProfilerStub.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/ProfilerStub.php Tue May 20 13:13:28 2008 @@ -1,26 +1,48 @@ mArticle =& $article; - $this->mTitle =& $article->mTitle; + $this->mArticle = $article; + $this->mTitle = $article->mTitle; + $this->mApplicableTypes = $this->mTitle->exists() ? $wgRestrictionTypes : array('create'); - if( $this->mTitle ) { - $this->mTitle->loadRestrictions(); + $this->mCascade = $this->mTitle->areRestrictionsCascading(); - foreach( $wgRestrictionTypes as $action ) { - // Fixme: this form currently requires individual selections, - // but the db allows multiples separated by commas. - $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) ); + // The form will be available in read-only to show levels. + $this->mPermErrors = $this->mTitle->getUserPermissionsErrors('protect',$wgUser); + $this->disabled = wfReadOnly() || $this->mPermErrors != array(); + $this->disabledAttrib = $this->disabled + ? array( 'disabled' => 'disabled' ) + : array(); + + $this->mReason = $wgRequest->getText( 'mwProtect-reason' ); + $this->mReasonSelection = $wgRequest->getText( 'wpProtectReasonSelection' ); + $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade', $this->mCascade ); + + foreach( $this->mApplicableTypes as $action ) { + // Fixme: this form currently requires individual selections, + // but the db allows multiples separated by commas. + $this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) ); + + if ( !$this->mRestrictions[$action] ) { + // No existing expiry + $existingExpiry = ''; + } else { + $existingExpiry = $this->mTitle->getRestrictionExpiry( $action ); } + $this->mExistingExpiry[$action] = $existingExpiry; - $this->mCascade = $this->mTitle->areRestrictionsCascading(); + $requestExpiry = $wgRequest->getText( "mwProtect-expiry-$action" ); + $requestExpirySelection = $wgRequest->getVal( "wpProtectExpirySelection-$action" ); - if ( $this->mTitle->mRestrictionsExpiry == 'infinity' ) { - $this->mExpiry = 'infinite'; - } else if ( strlen($this->mTitle->mRestrictionsExpiry) == 0 ) { - $this->mExpiry = ''; + if ( $requestExpiry ) { + // Custom expiry takes precedence + $this->mExpiry[$action] = $requestExpiry; + $this->mExpirySelection[$action] = 'othertime'; + } elseif ( $requestExpirySelection ) { + // Expiry selected from list + $this->mExpiry[$action] = ''; + $this->mExpirySelection[$action] = $requestExpirySelection; + } elseif ( $existingExpiry == 'infinity' ) { + // Existing expiry is infinite, use "infinite" in drop-down + $this->mExpiry[$action] = ''; + $this->mExpirySelection[$action] = 'infinite'; + } elseif ( $existingExpiry ) { + // Use existing expiry in its own list item + $this->mExpiry[$action] = ''; + $this->mExpirySelection[$action] = $existingExpiry; } else { - $this->mExpiry = wfTimestamp( TS_RFC2822, $this->mTitle->mRestrictionsExpiry ); + // Final default: infinite + $this->mExpiry[$action] = ''; + $this->mExpirySelection[$action] = 'infinite'; + } + + $val = $wgRequest->getVal( "mwProtect-level-$action" ); + if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) { + // Prevent users from setting levels that they cannot later unset + if( $val == 'sysop' ) { + // Special case, rewrite sysop to either protect and editprotected + if( !$wgUser->isAllowed('protect') && !$wgUser->isAllowed('editprotected') ) + continue; + } else { + if( !$wgUser->isAllowed($val) ) + continue; + } + $this->mRestrictions[$action] = $val; } } + } - // The form will be available in read-only to show levels. - $this->disabled = !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $wgUser->isBlocked(); - $this->disabledAttrib = $this->disabled - ? array( 'disabled' => 'disabled' ) - : array(); + /** + * Get the expiry time for a given action, by combining the relevant inputs. + * Returns a 14-char timestamp or "infinity", or false if the input was invalid + */ + function getExpiry( $action ) { + if ( $this->mExpirySelection[$action] == 'existing' ) { + return $this->mExistingExpiry[$action]; + } elseif ( $this->mExpirySelection[$action] == 'othertime' ) { + $value = $this->mExpiry[$action]; + } else { + $value = $this->mExpirySelection[$action]; + } + if ( $value == 'infinite' || $value == 'indefinite' || $value == 'infinity' ) { + $time = Block::infinity(); + } else { + $unix = strtotime( $value ); - if( $wgRequest->wasPosted() ) { - $this->mReason = $wgRequest->getText( 'mwProtect-reason' ); - $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' ); - $this->mExpiry = $wgRequest->getText( 'mwProtect-expiry' ); - - foreach( $wgRestrictionTypes as $action ) { - $val = $wgRequest->getVal( "mwProtect-level-$action" ); - if( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) { - $this->mRestrictions[$action] = $val; - } + if ( !$unix || $unix === -1 ) { + return false; } + + // Fixme: non-qualified absolute times are not in users specified timezone + // and there isn't notice about it in the ui + $time = wfTimestamp( TS_MW, $unix ); } + return $time; } - + function execute() { global $wgRequest, $wgOut; if( $wgRequest->wasPosted() ) { if( $this->save() ) { - $article = new Article( $this->mTitle ); - $q = $article->isRedirect() ? 'redirect=no' : ''; + $q = $this->mArticle->isRedirect() ? 'redirect=no' : ''; $wgOut->redirect( $this->mTitle->getFullUrl( $q ) ); } } else { @@ -91,10 +170,9 @@ function show( $err = null ) { global $wgOut, $wgUser; - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); if( is_null( $this->mTitle ) || - !$this->mTitle->exists() || $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { $wgOut->showFatalError( wfMsg( 'badarticleerror' ) ); return; @@ -114,33 +192,25 @@ $titles .= '* [[:' . $title->getPrefixedText() . "]]\n"; } - $notice = wfMsgExt( 'protect-cascadeon', array('parsemag'), count($cascadeSources) ) . "\r\n$titles"; - - $wgOut->addWikiText( $notice ); + $wgOut->wrapWikiMsg( "$1\n$titles", array( 'protect-cascadeon', count($cascadeSources) ) ); } - $wgOut->setPageTitle( wfMsg( 'confirmprotect' ) ); - $wgOut->setSubtitle( wfMsg( 'protectsub', $this->mTitle->getPrefixedText() ) ); + $sk = $wgUser->getSkin(); + $titleLink = $sk->makeLinkObj( $this->mTitle ); + $wgOut->setPageTitle( wfMsg( 'protect-title', $this->mTitle->getPrefixedText() ) ); + $wgOut->setSubtitle( wfMsg( 'protect-backlink', $titleLink ) ); # Show an appropriate message if the user isn't allowed or able to change # the protection settings at this time if( $this->disabled ) { - if( $wgUser->isAllowed( 'protect' ) ) { - if( $wgUser->isBlocked() ) { - # Blocked - $message = 'protect-locked-blocked'; - } else { - # Database lock - $message = 'protect-locked-dblock'; - } - } else { - # Permission error - $message = 'protect-locked-access'; + if( wfReadOnly() ) { + $wgOut->readOnlyPage(); + } elseif( $this->mPermErrors ) { + $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $this->mPermErrors ) ); } } else { - $message = 'protect-text'; + $wgOut->addWikiMsg( 'protect-text', $this->mTitle->getPrefixedText() ); } - $wgOut->addWikiText( wfMsg( $message, wfEscapeWikiText( $this->mTitle->getPrefixedText() ) ) ); $wgOut->addHTML( $this->buildForm() ); @@ -149,138 +219,276 @@ function save() { global $wgRequest, $wgUser, $wgOut; - - if( $this->disabled ) { + # Permission check! + if ( $this->disabled ) { $this->show(); return false; } $token = $wgRequest->getVal( 'wpEditToken' ); - if( !$wgUser->matchEditToken( $token ) ) { + if ( !$wgUser->matchEditToken( $token ) ) { $this->show( wfMsg( 'sessionfailure' ) ); return false; } - - if ( strlen( $this->mExpiry ) == 0 ) { - $this->mExpiry = 'infinite'; - } - - if ( $this->mExpiry == 'infinite' || $this->mExpiry == 'indefinite' ) { - $expiry = Block::infinity(); - } else { - # Convert GNU-style date, on error returns -1 for PHP <5.1 and false for PHP >=5.1 - $expiry = strtotime( $this->mExpiry ); - - if ( $expiry < 0 || $expiry === false ) { + + # Create reason string. Use list and/or custom string. + $reasonstr = $this->mReasonSelection; + if ( $reasonstr != 'other' && $this->mReason != '' ) { + // Entry from drop down menu + additional comment + $reasonstr .= ': ' . $this->mReason; + } elseif ( $reasonstr == 'other' ) { + $reasonstr = $this->mReason; + } + $expiry = array(); + foreach( $this->mApplicableTypes as $action ) { + $expiry[$action] = $this->getExpiry( $action ); + if( empty($this->mRestrictions[$action]) ) + continue; // unprotected + if ( !$expiry[$action] ) { $this->show( wfMsg( 'protect_expiry_invalid' ) ); return false; } - - $expiry = wfTimestamp( TS_MW, $expiry ); - - if ( $expiry < wfTimestampNow() ) { + if ( $expiry[$action] < wfTimestampNow() ) { $this->show( wfMsg( 'protect_expiry_old' ) ); return false; } + } + + # They shouldn't be able to do this anyway, but just to make sure, ensure that cascading restrictions aren't being applied + # to a semi-protected page. + global $wgGroupPermissions; + + $edit_restriction = $this->mRestrictions['edit']; + $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' ); + if ($this->mCascade && ($edit_restriction != 'protect') && + !(isset($wgGroupPermissions[$edit_restriction]['protect']) && $wgGroupPermissions[$edit_restriction]['protect'] ) ) + $this->mCascade = false; + if ($this->mTitle->exists()) { + $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $reasonstr, $this->mCascade, $expiry ); + } else { + $ok = $this->mTitle->updateTitleProtection( $this->mRestrictions['create'], $reasonstr, $expiry['create'] ); } - $ok = $this->mArticle->updateRestrictions( $this->mRestrictions, $this->mReason, $this->mCascade, $expiry ); if( !$ok ) { throw new FatalError( "Unknown error at restriction save time." ); } - + if( $wgRequest->getCheck( 'mwProtectWatch' ) ) { $this->mArticle->doWatch(); } elseif( $this->mTitle->userIsWatching() ) { $this->mArticle->doUnwatch(); } - return $ok; } + /** + * Build the input form + * + * @return $out string HTML form + */ function buildForm() { - global $wgUser; + global $wgUser, $wgLang; + + $mProtectreasonother = Xml::label( wfMsg( 'protectcomment' ), 'wpProtectReasonSelection' ); + $mProtectreason = Xml::label( wfMsg( 'protect-otherreason' ), 'mwProtect-reason' ); $out = ''; if( !$this->disabled ) { $out .= $this->buildScript(); - // The submission needs to reenable the move permission selector - // if it's in locked mode, or some browsers won't submit the data. - $out .= wfOpenElement( 'form', array( - 'id' => 'mw-Protect-Form', - 'action' => $this->mTitle->getLocalUrl( 'action=protect' ), - 'method' => 'post', - 'onsubmit' => 'protectEnable(true)' ) ); - - $out .= wfElement( 'input', array( - 'type' => 'hidden', - 'name' => 'wpEditToken', - 'value' => $wgUser->editToken() ) ); - } - - $out .= "
    $link
    and and
    "; - $out .= ""; - $out .= "\n"; - foreach( $this->mRestrictions as $action => $required ) { - /* Not all languages have V_x <-> N_x relation */ - $out .= "\n"; + $out .= Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $this->mTitle->getLocalUrl( 'action=protect' ), + 'id' => 'mw-Protect-Form', 'onsubmit' => 'ProtectionForm.enableUnchainedInputs(true)' ) ); + $out .= Xml::hidden( 'wpEditToken',$wgUser->editToken() ); } - $out .= "\n"; - $out .= "\n"; - foreach( $this->mRestrictions as $action => $selected ) { - $out .= "\n"; - } - $out .= "\n"; - - // JavaScript will add another row with a value-chaining checkbox - - $out .= "\n"; - $out .= "
    " . wfMsgHtml( 'restriction-' . $action ) . "
    \n"; - $out .= $this->buildSelector( $action, $selected ); - $out .= "
    \n"; - $out .= "\n"; - $out .= "\n"; + $out .= Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, wfMsg( 'protect-legend' ) ) . + Xml::openElement( 'table', array( 'id' => 'mwProtectSet' ) ) . + Xml::openElement( 'tbody' ); - global $wgEnableCascadingProtection; - if( $wgEnableCascadingProtection ) - $out .= '\n"; + foreach( $this->mRestrictions as $action => $selected ) { + /* Not all languages have V_x <-> N_x relation */ + $msg = wfMsg( 'restriction-' . $action ); + if( wfEmptyMsg( 'restriction-' . $action, $msg ) ) { + $msg = $action; + } + $out .= "" . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) . + ""; + } - $out .= $this->buildExpiryInput(); + $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' ); + // JavaScript will add another row with a value-chaining checkbox + if( $this->mTitle->exists() ) { + $out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table2' ) ) . + Xml::openElement( 'tbody' ); + $out .= ' + + + \n"; + $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' ); + } + + # Add manual and custom reason field/selects as well as submit if( !$this->disabled ) { - $out .= "\n"; - $out .= "\n"; - $out .= "\n"; + $out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table3' ) ) . + Xml::openElement( 'tbody' ); + $out .= " + + + + + + + + + + + + + + + + \n"; + $out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' ); + } + $out .= Xml::closeElement( 'fieldset' ); + + if ( $wgUser->isAllowed( 'editinterface' ) ) { + $linkTitle = Title::makeTitleSafe( NS_MEDIAWIKI, 'protect-dropdown' ); + $link = $wgUser->getSkin()->Link ( $linkTitle, wfMsgHtml( 'protect-edit-reasonlist' ) ); + $out .= '

    ' . $link . '

    '; } - $out .= "\n"; - $out .= "
    ' . $this->buildCascadeInput() . "
    ". + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, $msg ) . + Xml::openElement( 'table', array( 'id' => "mw-protect-table-$action" ) ) . + "
    " . $this->buildSelector( $action, $selected ) . "
    "; + + $reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection', + wfMsgForContent( 'protect-dropdown' ), + wfMsgForContent( 'protect-otherreason-op' ), + $this->mReasonSelection, + 'mwProtect-reason', 4 ); + $scExpiryOptions = wfMsgForContent( 'protect-expiry-options' ); + + $showProtectOptions = ($scExpiryOptions !== '-' && !$this->disabled); + + $mProtectexpiry = Xml::label( wfMsg( 'protectexpiry' ), "mwProtectExpirySelection-$action" ); + $mProtectother = Xml::label( wfMsg( 'protect-othertime' ), "mwProtect-$action-expires" ); + + $expiryFormOptions = ''; + if ( $this->mExistingExpiry[$action] && $this->mExistingExpiry[$action] != 'infinity' ) { + $timestamp = $wgLang->timeanddate( $this->mExistingExpiry[$action] ); + $d = $wgLang->date( $this->mExistingExpiry[$action] ); + $t = $wgLang->time( $this->mExistingExpiry[$action] ); + $expiryFormOptions .= + Xml::option( + wfMsg( 'protect-existing-expiry', $timestamp, $d, $t ), + 'existing', + $this->mExpirySelection[$action] == 'existing' + ) . "\n"; + } + + $expiryFormOptions .= Xml::option( wfMsg( 'protect-othertime-op' ), "othertime" ) . "\n"; + foreach( explode(',', $scExpiryOptions) as $option ) { + if ( strpos($option, ":") === false ) { + $show = $value = $option; + } else { + list($show, $value) = explode(":", $option); + } + $show = htmlspecialchars($show); + $value = htmlspecialchars($value); + $expiryFormOptions .= Xml::option( $show, $value, $this->mExpirySelection[$action] === $value ) . "\n"; + } + # Add expiry dropdown + if( $showProtectOptions && !$this->disabled ) { + $out .= " + + + +
    + {$mProtectexpiry} + " . + Xml::tags( 'select', + array( + 'id' => "mwProtectExpirySelection-$action", + 'name' => "wpProtectExpirySelection-$action", + 'onchange' => "ProtectionForm.updateExpiryList(this)", + 'tabindex' => '2' ) + $this->disabledAttrib, + $expiryFormOptions ) . + "
    "; + } + # Add custom expiry field + $attribs = array( 'id' => "mwProtect-$action-expires", 'onkeyup' => 'ProtectionForm.updateExpiry(this)' ) + $this->disabledAttrib; + $out .= " + + +
    " . + $mProtectother . + '' . + Xml::input( "mwProtect-expiry-$action", 50, $this->mExpiry[$action], $attribs ) . + '
    '; + $out .= "
    ' . + Xml::checkLabel( wfMsg( 'protect-cascade' ), 'mwProtect-cascade', 'mwProtect-cascade', + $this->mCascade, $this->disabledAttrib ) . + "
    " . $this->buildReasonInput() . "
    " . $this->buildWatchInput() . "
    " . $this->buildSubmit() . "
    + {$mProtectreasonother} + + {$reasonDropDown} +
    + {$mProtectreason} + " . + Xml::input( 'mwProtect-reason', 60, $this->mReason, array( 'type' => 'text', + 'id' => 'mwProtect-reason', 'maxlength' => 255 ) ) . + "
    " . + Xml::checkLabel( wfMsg( 'watchthis' ), + 'mwProtectWatch', 'mwProtectWatch', + $this->mTitle->userIsWatching() || $wgUser->getOption( 'watchdefault' ) ) . + "
    " . + Xml::submitButton( wfMsg( 'confirm' ), array( 'id' => 'mw-Protect-submit' ) ) . + "
    \n"; - if ( !$this->disabled ) { - $out .= "\n"; - $out .= $this->buildCleanupScript(); + $out .= Xml::closeElement( 'form' ) . + $this->buildCleanupScript(); } return $out; } function buildSelector( $action, $selected ) { - global $wgRestrictionLevels; + global $wgRestrictionLevels, $wgUser; + + $levels = array(); + foreach( $wgRestrictionLevels as $key ) { + //don't let them choose levels above their own (aka so they can still unprotect and edit the page). but only when the form isn't disabled + if( $key == 'sysop' ) { + //special case, rewrite sysop to protect and editprotected + if( !$wgUser->isAllowed('protect') && !$wgUser->isAllowed('editprotected') && !$this->disabled ) + continue; + } else { + if( !$wgUser->isAllowed($key) && !$this->disabled ) + continue; + } + $levels[] = $key; + } + $id = 'mwProtect-level-' . $action; $attribs = array( 'id' => $id, 'name' => $id, - 'size' => count( $wgRestrictionLevels ), - 'onchange' => 'protectLevelsUpdate(this)', + 'size' => count( $levels ), + 'onchange' => 'ProtectionForm.updateLevels(this)', ) + $this->disabledAttrib; - $out = wfOpenElement( 'select', $attribs ); - foreach( $wgRestrictionLevels as $key ) { + $out = Xml::openElement( 'select', $attribs ); + foreach( $levels as $key ) { $out .= Xml::option( $this->getOptionLabel( $key ), $key, $key == $selected ); } - $out .= "\n"; + $out .= Xml::closeElement( 'select' ); return $out; } @@ -302,56 +510,11 @@ } } - function buildReasonInput() { - $id = 'mwProtect-reason'; - return wfElement( 'label', array( - 'id' => "$id-label", - 'for' => $id ), - wfMsg( 'protectcomment' ) ) . - '' . - wfElement( 'input', array( - 'size' => 60, - 'name' => $id, - 'id' => $id, - 'value' => $this->mReason ) ); - } - - function buildCascadeInput() { - $id = 'mwProtect-cascade'; - $ci = wfCheckLabel( wfMsg( 'protect-cascade' ), $id, $id, $this->mCascade, $this->disabledAttrib); - return $ci; - } - - function buildExpiryInput() { - $attribs = array( 'id' => 'expires' ) + $this->disabledAttrib; - return '' - . '' - . '' . Xml::input( 'mwProtect-expiry', 60, $this->mExpiry, $attribs ) . '' - . ''; - } - - function buildWatchInput() { - global $wgUser; - return Xml::checkLabel( - wfMsg( 'watchthis' ), - 'mwProtectWatch', - 'mwProtectWatch', - $this->mTitle->userIsWatching() || $wgUser->getOption( 'watchdefault' ) - ); - } - - function buildSubmit() { - return wfElement( 'input', array( - 'id' => 'mw-Protect-submit', - 'type' => 'submit', - 'value' => wfMsg( 'confirm' ) ) ); - } - function buildScript() { global $wgStylePath, $wgStyleVersion; - return ''; + return Xml::tags( 'script', array( + 'type' => 'text/javascript', + 'src' => $wgStylePath . "/common/protect.js?$wgStyleVersion.1" ), '' ); } function buildCleanupScript() { @@ -359,13 +522,21 @@ $script = 'var wgCascadeableLevels='; $CascadeableLevels = array(); foreach( $wgRestrictionLevels as $key ) { - if ( isset($wgGroupPermissions[$key]['protect']) && $wgGroupPermissions[$key]['protect'] ) { - $CascadeableLevels[]="'" . wfEscapeJsString($key) . "'"; + if ( (isset($wgGroupPermissions[$key]['protect']) && $wgGroupPermissions[$key]['protect']) || $key == 'protect' ) { + $CascadeableLevels[] = "'" . Xml::escapeJsString( $key ) . "'"; } } $script .= "[" . implode(',',$CascadeableLevels) . "];\n"; - $script .= 'protectInitialize("mwProtectSet","' . wfEscapeJsString( wfMsg( 'protect-unchain' ) ) . '")'; - return ''; + $options = (object)array( + 'tableId' => 'mw-protect-table-move', + 'labelText' => wfMsg( 'protect-unchain' ), + 'numTypes' => count($this->mApplicableTypes), + 'existingMatch' => 1 == count( array_unique( $this->mExistingExpiry ) ), + ); + $encOptions = Xml::encodeJsVar( $options ); + + $script .= "ProtectionForm.init($encOptions)"; + return Xml::tags( 'script', array( 'type' => 'text/javascript' ), $script ); } /** @@ -374,13 +545,7 @@ */ function showLogExtract( &$out ) { # Show relevant lines from the protection log: - $out->addHTML( "

    " . htmlspecialchars( LogPage::logName( 'protect' ) ) . "

    \n" ); - $logViewer = new LogViewer( - new LogReader( - new FauxRequest( - array( 'page' => $this->mTitle->getPrefixedText(), - 'type' => 'protect' ) ) ) ); - $logViewer->showList( $out ); + $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'protect' ) ) ); + LogEventsList::showLogExtract( $out, 'protect', $this->mTitle->getPrefixedText() ); } - -} \ No newline at end of file +} diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/ProxyTools.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/ProxyTools.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/ProxyTools.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/ProxyTools.php Wed Dec 10 17:58:24 2008 @@ -1,6 +1,7 @@ $tempValue ) { + $set[ strtoupper( $tempName ) ] = $tempValue; + } + $index = strtoupper ( 'X-Forwarded-For' ); + $index2 = strtoupper ( 'Client-ip' ); } else { // Subject to spoofing with headers like X_Forwarded_For $set = $_SERVER; $index = 'HTTP_X_FORWARDED_FOR'; $index2 = 'CLIENT-IP'; } + #Try a couple of headers if( isset( $set[$index] ) ) { return $set[$index]; @@ -39,8 +44,11 @@ function wfGetAgent() { if( function_exists( 'apache_request_headers' ) ) { // More reliable than $_SERVER due to case and -/_ folding - $set = apache_request_headers(); - $index = 'User-Agent'; + $set = array (); + foreach ( apache_request_headers() as $tempName => $tempValue ) { + $set[ strtoupper( $tempName ) ] = $tempValue; + } + $index = strtoupper ( 'User-Agent' ); } else { // Subject to spoofing with headers like X_Forwarded_For $set = $_SERVER; @@ -59,7 +67,7 @@ * @return string */ function wfGetIP() { - global $wgIP; + global $wgIP, $wgUsePrivateIPs; # Return cached result if ( !empty( $wgIP ) ) { @@ -83,14 +91,16 @@ $xff = array_reverse( $xff ); $ipchain = array_merge( $ipchain, $xff ); } - + # Step through XFF list and find the last address in the list which is a trusted server # Set $ip to the IP address given by that trusted server, unless the address is not sensible (e.g. private) foreach ( $ipchain as $i => $curIP ) { $curIP = IP::canonicalize( $curIP ); if ( wfIsTrustedProxy( $curIP ) ) { - if ( isset( $ipchain[$i + 1] ) && IP::isPublic( $ipchain[$i + 1] ) ) { - $ip = $ipchain[$i + 1]; + if ( isset( $ipchain[$i + 1] ) ) { + if( $wgUsePrivateIPs || IP::isPublic( $ipchain[$i + 1 ] ) ) { + $ip = $ipchain[$i + 1]; + } } } else { break; @@ -112,9 +122,8 @@ function wfIsTrustedProxy( $ip ) { global $wgSquidServers, $wgSquidServersNoPurge; - if ( in_array( $ip, $wgSquidServers ) || - in_array( $ip, $wgSquidServersNoPurge ) || - wfIsAOLProxy( $ip ) + if ( in_array( $ip, $wgSquidServers ) || + in_array( $ip, $wgSquidServersNoPurge ) ) { $trusted = true; } else { @@ -130,7 +139,7 @@ */ function wfProxyCheck() { global $wgBlockOpenProxies, $wgProxyPorts, $wgProxyScriptPath; - global $wgUseMemCached, $wgMemc, $wgProxyMemcExpiry; + global $wgMemc, $wgProxyMemcExpiry; global $wgProxyKey; if ( !$wgBlockOpenProxies ) { @@ -140,14 +149,9 @@ $ip = wfGetIP(); # Get MemCached key - $skip = false; - if ( $wgUseMemCached ) { - $mcKey = wfMemcKey( 'proxy', 'ip', $ip ); - $mcValue = $wgMemc->get( $mcKey ); - if ( $mcValue ) { - $skip = true; - } - } + $mcKey = wfMemcKey( 'proxy', 'ip', $ip ); + $mcValue = $wgMemc->get( $mcKey ); + $skip = (bool)$mcValue; # Fork the processes if ( !$skip ) { @@ -165,9 +169,7 @@ exec( "php $params &>/dev/null &" ); } # Set MemCached key - if ( $wgUseMemCached ) { - $wgMemc->set( $mcKey, 1, $wgProxyMemcExpiry ); - } + $wgMemc->set( $mcKey, 1, $wgProxyMemcExpiry ); } } @@ -210,54 +212,4 @@ wfProfileOut( $fname ); return $ret; } - -/** - * TODO: move this list to the database in a global IP info table incorporating - * trusted ISP proxies, blocked IP addresses and open proxies. - * @return bool - */ -function wfIsAOLProxy( $ip ) { - $ranges = array( - '64.12.96.0/19', - '149.174.160.0/20', - '152.163.240.0/21', - '152.163.248.0/22', - '152.163.252.0/23', - '152.163.96.0/22', - '152.163.100.0/23', - '195.93.32.0/22', - '195.93.48.0/22', - '195.93.64.0/19', - '195.93.96.0/19', - '195.93.16.0/20', - '198.81.0.0/22', - '198.81.16.0/20', - '198.81.8.0/23', - '202.67.64.128/25', - '205.188.192.0/20', - '205.188.208.0/23', - '205.188.112.0/20', - '205.188.146.144/30', - '207.200.112.0/21', - ); - - static $parsedRanges; - if ( is_null( $parsedRanges ) ) { - $parsedRanges = array(); - foreach ( $ranges as $range ) { - $parsedRanges[] = IP::parseRange( $range ); - } - } - - $hex = IP::toHex( $ip ); - foreach ( $parsedRanges as $range ) { - if ( $hex >= $range[0] && $hex <= $range[1] ) { - return true; - } - } - return false; -} - - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/QueryPage.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/QueryPage.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/QueryPage.php Tue Sep 4 15:29:47 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/QueryPage.php Fri Dec 19 03:54:51 2008 @@ -1,13 +1,15 @@ value ) ) { $value = $row->value; } else { - $value = ''; + $value = 0; } $insertSql .= '(' . @@ -305,25 +309,23 @@ # Fetch the timestamp of this update $tRes = $dbr->select( 'querycache_info', array( 'qci_timestamp' ), array( 'qci_type' => $type ), $fname ); $tRow = $dbr->fetchObject( $tRes ); - + if( $tRow ) { $updated = $wgLang->timeAndDate( $tRow->qci_timestamp, true, true ); - $cacheNotice = wfMsg( 'perfcachedts', $updated ); $wgOut->addMeta( 'Data-Cache-Time', $tRow->qci_timestamp ); $wgOut->addInlineScript( "var dataCacheTime = '{$tRow->qci_timestamp}';" ); + $wgOut->addWikiMsg( 'perfcachedts', $updated ); } else { - $cacheNotice = wfMsg( 'perfcached' ); + $wgOut->addWikiMsg( 'perfcached' ); } - - $wgOut->addWikiText( $cacheNotice ); - + # If updates on this page have been disabled, let the user know # that the data set won't be refreshed for now global $wgDisableQueryPageUpdate; if( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) { - $wgOut->addWikiText( wfMsg( 'querypage-no-updates' ) ); + $wgOut->addWikiMsg( 'querypage-no-updates' ); } - + } } @@ -335,26 +337,26 @@ $this->preprocessResults( $dbr, $res ); - $wgOut->addHtml( XML::openElement( 'div', array('class' => 'mw-spcontent') ) ); - + $wgOut->addHTML( XML::openElement( 'div', array('class' => 'mw-spcontent') ) ); + # Top header and navigation if( $shownavigation ) { - $wgOut->addHtml( $this->getPageHeader() ); + $wgOut->addHTML( $this->getPageHeader() ); if( $num > 0 ) { - $wgOut->addHtml( '

    ' . wfShowingResults( $offset, $num ) . '

    ' ); + $wgOut->addHTML( '

    ' . wfShowingResults( $offset, $num ) . '

    ' ); # Disable the "next" link when we reach the end $paging = wfViewPrevNext( $offset, $limit, $wgContLang->specialPage( $sname ), wfArrayToCGI( $this->linkParameters() ), ( $num < $limit ) ); - $wgOut->addHtml( '

    ' . $paging . '

    ' ); + $wgOut->addHTML( '

    ' . $paging . '

    ' ); } else { # No results to show, so don't bother with "showing X of Y" etc. # -- just let the user know and give up now - $wgOut->addHtml( '

    ' . wfMsgHtml( 'specialpage-empty' ) . '

    ' ); - $wgOut->addHtml( XML::closeElement( 'div' ) ); + $wgOut->addHTML( '

    ' . wfMsgHtml( 'specialpage-empty' ) . '

    ' ); + $wgOut->addHTML( XML::closeElement( 'div' ) ); return; } } - + # The actual results; specialist subclasses will want to handle this # with more than a straight list, so we hand them the info, plus # an OutputPage, and let them get on with it @@ -367,14 +369,14 @@ # Repeat the paging links at the bottom if( $shownavigation ) { - $wgOut->addHtml( '

    ' . $paging . '

    ' ); + $wgOut->addHTML( '

    ' . $paging . '

    ' ); } - $wgOut->addHtml( XML::closeElement( 'div' ) ); - + $wgOut->addHTML( XML::closeElement( 'div' ) ); + return $num; } - + /** * Format and output report results using the given information plus * OutputPage @@ -388,12 +390,12 @@ */ protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) { global $wgContLang; - + if( $num > 0 ) { $html = array(); if( !$this->listoutput ) $html[] = $this->openList( $offset ); - + # $res might contain the whole 1,000 rows, so we read up to # $num [should update this to use a Pager] for( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) { @@ -407,7 +409,7 @@ : "{$line}\n"; } } - + # Flush the final result if( $this->tryLastResult() ) { $row = null; @@ -421,37 +423,47 @@ : "{$line}\n"; } } - + if( !$this->listoutput ) $html[] = $this->closeList(); - + $html = $this->listoutput ? $wgContLang->listToText( $html ) : implode( '', $html ); - - $out->addHtml( $html ); + + $out->addHTML( $html ); } } - + function openList( $offset ) { return "\n
      \n"; } - + function closeList() { return "
    \n"; } /** * Do any necessary preprocessing of the result object. - * You should pass this by reference: &$db , &$res [although probably no longer necessary in PHP5] */ - function preprocessResults( &$db, &$res ) {} + function preprocessResults( $db, $res ) {} /** * Similar to above, but packaging in a syndicated feed instead of a web page */ function doFeed( $class = '', $limit = 50 ) { - global $wgFeedClasses; + global $wgFeed, $wgFeedClasses; + + if ( !$wgFeed ) { + global $wgOut; + $wgOut->addWikiMsg( 'feed-unavailable' ); + return; + } + + global $wgFeedLimit; + if( $limit > $wgFeedLimit ) { + $limit = $wgFeedLimit; + } if( isset($wgFeedClasses[$class]) ) { $feed = new $wgFeedClasses[$class]( @@ -522,7 +534,7 @@ } function feedDesc() { - return wfMsg( 'tagline' ); + return wfMsgExt( 'tagline', 'parsemag' ); } function feedUrl() { @@ -530,5 +542,3 @@ return $title->getFullURL(); } } - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/RawPage.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/RawPage.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/RawPage.php Tue Jul 17 11:50:50 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/RawPage.php Fri Jan 2 06:07:12 2009 @@ -7,6 +7,7 @@ * License: GPL (http://www.gnu.org/copyleft/gpl.html) * * @author Gabriel Wicke + * @file */ /** @@ -15,41 +16,45 @@ */ class RawPage { var $mArticle, $mTitle, $mRequest; - var $mOldId, $mGen, $mCharset; + var $mOldId, $mGen, $mCharset, $mSection; var $mSmaxage, $mMaxage; var $mContentType, $mExpandTemplates; function __construct( &$article, $request = false ) { - global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType; + global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType, $wgGroupPermissions; $allowedCTypes = array('text/x-wiki', $wgJsMimeType, 'text/css', 'application/x-zope-edit'); $this->mArticle =& $article; $this->mTitle =& $article->mTitle; - if ( $request === false ) { + if( $request === false ) { $this->mRequest =& $wgRequest; } else { $this->mRequest = $request; } $ctype = $this->mRequest->getVal( 'ctype' ); - $smaxage = $this->mRequest->getIntOrNull( 'smaxage', $wgSquidMaxage ); + $smaxage = $this->mRequest->getIntOrNull( 'smaxage' ); $maxage = $this->mRequest->getInt( 'maxage', $wgSquidMaxage ); + $this->mExpandTemplates = $this->mRequest->getVal( 'templates' ) === 'expand'; $this->mUseMessageCache = $this->mRequest->getBool( 'usemsgcache' ); + $this->mSection = $this->mRequest->getIntOrNull( 'section' ); + $oldid = $this->mRequest->getInt( 'oldid' ); - switch ( $wgRequest->getText( 'direction' ) ) { + + switch( $wgRequest->getText( 'direction' ) ) { case 'next': # output next revision, or nothing if there isn't one - if ( $oldid ) { + if( $oldid ) { $oldid = $this->mTitle->getNextRevisionId( $oldid ); } $oldid = $oldid ? $oldid : -1; break; case 'prev': # output previous revision, or nothing if there isn't one - if ( ! $oldid ) { + if( ! $oldid ) { # get the current revision so we can get the penultimate one $this->mArticle->getTouched(); $oldid = $this->mArticle->mLatest; @@ -62,15 +67,15 @@ break; } $this->mOldId = $oldid; - + # special case for 'generated' raw things: user css/js $gen = $this->mRequest->getVal( 'gen' ); - if($gen == 'css') { + if( $gen == 'css' ) { $this->mGen = $gen; if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage; if($ctype == '') $ctype = 'text/css'; - } elseif ($gen == 'js') { + } elseif( $gen == 'js' ) { $this->mGen = $gen; if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage; if($ctype == '') $ctype = $wgJsMimeType; @@ -78,14 +83,25 @@ $this->mGen = false; } $this->mCharset = $wgInputEncoding; - $this->mSmaxage = intval( $smaxage ); + + # Force caching for CSS and JS raw content, default: 5 minutes + if( is_null($smaxage) and ($ctype=='text/css' or $ctype==$wgJsMimeType) ) { + global $wgForcedRawSMaxage; + $this->mSmaxage = intval($wgForcedRawSMaxage); + } else { + $this->mSmaxage = intval( $smaxage ); + } $this->mMaxage = $maxage; - - // Output may contain user-specific data; vary for open sessions - $this->mPrivateCache = ( $this->mSmaxage == 0 ) || - ( session_id() != '' ); - - if ( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) { + + # Output may contain user-specific data; + # vary generated content for open sessions and private wikis + if( $this->mGen or !$wgGroupPermissions['*']['read'] ) { + $this->mPrivateCache = $this->mSmaxage == 0 || session_id() != ''; + } else { + $this->mPrivateCache = false; + } + + if( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) { $this->mContentType = 'text/x-wiki'; } else { $this->mContentType = $ctype; @@ -110,9 +126,8 @@ } else { $url = $_SERVER['PHP_SELF']; } - - $ua = @$_SERVER['HTTP_USER_AGENT']; - if( strcmp( $wgScript, $url ) && strpos( $ua, 'MSIE' ) !== false ) { + + if( strcmp( $wgScript, $url ) ) { # Internet Explorer will ignore the Content-Type header if it # thinks it sees a file extension it recognizes. Make sure that # all raw requests are done through the script node, which will @@ -134,6 +149,18 @@ # allow the client to cache this for 24 hours $mode = $this->mPrivateCache ? 'private' : 'public'; header( 'Cache-Control: '.$mode.', s-maxage='.$this->mSmaxage.', max-age='.$this->mMaxage ); + + if( HTMLFileCache::useFileCache() ) { + $cache = new HTMLFileCache( $this->mTitle, 'raw' ); + if( $cache->isFileCacheGood( /* Assume up to date */ ) ) { + $cache->loadFromFileCache(); + $wgOut->disable(); + return; + } else { + ob_start( array(&$cache, 'saveToFileCache' ) ); + } + } + $text = $this->getRawText(); if( !wfRunHooks( 'RawPageViewBeforeOutput', array( &$this, &$text ) ) ) { @@ -146,13 +173,15 @@ function getRawText() { global $wgUser, $wgOut, $wgRequest; - if($this->mGen) { + if( $this->mGen ) { $sk = $wgUser->getSkin(); - $sk->initPage($wgOut); - if($this->mGen == 'css') { - return $sk->getUserStylesheet(); - } else if($this->mGen == 'js') { - return $sk->getUserJs(); + if( !StubObject::isRealObject( $wgOut ) ) + $wgOut->_unstub( 2 ); + $sk->initPage( $wgOut ); + if( $this->mGen == 'css' ) { + return $sk->generateUserStylesheet(); + } else if( $this->mGen == 'js' ) { + return $sk->generateUserJs(); } } else { return $this->getArticleText(); @@ -164,7 +193,7 @@ $text = ''; if( $this->mTitle ) { // If it's a MediaWiki message we can just hit the message cache - if ( $this->mUseMessageCache && $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + if( $this->mUseMessageCache && $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { $key = $this->mTitle->getDBkey(); $text = wfMsgForContentNoTrans( $key ); # If the message doesn't exist, return a blank @@ -174,10 +203,15 @@ } else { // Get it from the DB $rev = Revision::newFromTitle( $this->mTitle, $this->mOldId ); - if ( $rev ) { + if( $rev ) { $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() ); header( "Last-modified: $lastmod" ); - $text = $rev->getText(); + + if( !is_null($this->mSection ) ) { + global $wgParser; + $text = $wgParser->getSection ( $rev->getText(), $this->mSection ); + } else + $text = $rev->getText(); $found = true; } } @@ -191,7 +225,7 @@ # have the pages. header( "HTTP/1.0 404 Not Found" ); } - + // Special-case for empty CSS/JS // // Internet Explorer for Mac handles empty files badly; @@ -205,19 +239,18 @@ $this->mContentType == 'text/javascript' ) ) { return "/* Empty */"; } - + return $this->parseArticleText( $text ); } function parseArticleText( $text ) { - if ( $text === '' ) + if( $text === '' ) return ''; else - if ( $this->mExpandTemplates ) { + if( $this->mExpandTemplates ) { global $wgParser; return $wgParser->preprocess( $text, $this->mTitle, new ParserOptions() ); } else return $text; } } - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/RecentChange.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/RecentChange.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/RecentChange.php Mon Aug 20 23:57:54 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/RecentChange.php Tue Dec 23 15:56:19 2008 @@ -1,7 +1,4 @@ loadFromRow( $row ); return $rc; } - public static function newFromCurRow( $row, $rc_this_oldid = 0 ) - { + public static function newFromCurRow( $row ) { $rc = new RecentChange; - $rc->loadFromCurRow( $row, $rc_this_oldid ); + $rc->loadFromCurRow( $row ); $rc->notificationtimestamp = false; $rc->numberofWatchingusers = false; return $rc; } - + /** * Obtain the recent change with a given rc_id value * @@ -80,7 +80,7 @@ return NULL; } } - + /** * Find the first recent change matching some specific conditions * @@ -108,27 +108,23 @@ # Accessors - function setAttribs( $attribs ) - { + public function setAttribs( $attribs ) { $this->mAttribs = $attribs; } - function setExtra( $extra ) - { + public function setExtra( $extra ) { $this->mExtra = $extra; } - function &getTitle() - { - if ( $this->mTitle === false ) { + public function &getTitle() { + if( $this->mTitle === false ) { $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] ); } return $this->mTitle; } - function getMovedToTitle() - { - if ( $this->mMovedToTitle === false ) { + public function getMovedToTitle() { + if( $this->mMovedToTitle === false ) { $this->mMovedToTitle = Title::makeTitle( $this->mAttribs['rc_moved_to_ns'], $this->mAttribs['rc_moved_to_title'] ); } @@ -136,23 +132,22 @@ } # Writes the data in this object to the database - function save() - { - global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPPort, $wgRC2UDPPrefix; + public function save() { + global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPOmitBots; $fname = 'RecentChange::save'; $dbw = wfGetDB( DB_MASTER ); - if ( !is_array($this->mExtra) ) { + if( !is_array($this->mExtra) ) { $this->mExtra = array(); } $this->mExtra['lang'] = $wgLocalInterwiki; - if ( !$wgPutIPinRC ) { + if( !$wgPutIPinRC ) { $this->mAttribs['rc_ip'] = ''; } - ## If our database is strict about IP addresses, use NULL instead of an empty string - if ( $dbw->strictIPs() and $this->mAttribs['rc_ip'] == '' ) { + # If our database is strict about IP addresses, use NULL instead of an empty string + if( $dbw->strictIPs() and $this->mAttribs['rc_ip'] == '' ) { unset( $this->mAttribs['rc_ip'] ); } @@ -162,7 +157,7 @@ $this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'rc_rc_id_seq' ); ## If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL - if ( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id']==0 ) { + if( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id']==0 ) { unset ( $this->mAttribs['rc_cur_id'] ); } @@ -172,59 +167,27 @@ # Set the ID $this->mAttribs['rc_id'] = $dbw->insertId(); - # Update old rows, if necessary - if ( $this->mAttribs['rc_type'] == RC_EDIT ) { - $lastTime = $this->mExtra['lastTimestamp']; - #$now = $this->mAttribs['rc_timestamp']; - #$curId = $this->mAttribs['rc_cur_id']; - - # Don't bother looking for entries that have probably - # been purged, it just locks up the indexes needlessly. - global $wgRCMaxAge; - $age = time() - wfTimestamp( TS_UNIX, $lastTime ); - if( $age < $wgRCMaxAge ) { - # live hack, will commit once tested - kate - # Update rc_this_oldid for the entries which were current - # - #$oldid = $this->mAttribs['rc_last_oldid']; - #$ns = $this->mAttribs['rc_namespace']; - #$title = $this->mAttribs['rc_title']; - # - #$dbw->update( 'recentchanges', - # array( /* SET */ - # 'rc_this_oldid' => $oldid - # ), array( /* WHERE */ - # 'rc_namespace' => $ns, - # 'rc_title' => $title, - # 'rc_timestamp' => $dbw->timestamp( $lastTime ) - # ), $fname - #); - } - - # Update rc_cur_time - #$dbw->update( 'recentchanges', array( 'rc_cur_time' => $now ), - # array( 'rc_cur_id' => $curId ), $fname ); - } - # Notify external application via UDP - if ( $wgRC2UDPAddress ) { - $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); - if ( $conn ) { - $line = $wgRC2UDPPrefix . $this->getIRCLine(); - socket_sendto( $conn, $line, strlen($line), 0, $wgRC2UDPAddress, $wgRC2UDPPort ); - socket_close( $conn ); - } + if( $wgRC2UDPAddress && ( !$this->mAttribs['rc_bot'] || !$wgRC2UDPOmitBots ) ) { + self::sendToUDP( $this->getIRCLine() ); } # E-mail notifications - global $wgUseEnotif; - if( $wgUseEnotif ) { - # this would be better as an extension hook - global $wgUser; - include_once( "UserMailer.php" ); + global $wgUseEnotif, $wgShowUpdatedMarker, $wgUser; + if( $wgUseEnotif || $wgShowUpdatedMarker ) { + // Users + if( $this->mAttribs['rc_user'] ) { + $editor = ($wgUser->getId() == $this->mAttribs['rc_user']) ? + $wgUser : User::newFromID( $this->mAttribs['rc_user'] ); + // Anons + } else { + $editor = ($wgUser->getName() == $this->mAttribs['rc_user_text']) ? + $wgUser : User::newFromName( $this->mAttribs['rc_user_text'], false ); + } + # FIXME: this would be better as an extension hook $enotif = new EmailNotification(); $title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] ); - $enotif->notifyOnPageChange( $wgUser, $title, + $enotif->notifyOnPageChange( $editor, $title, $this->mAttribs['rc_timestamp'], $this->mAttribs['rc_comment'], $this->mAttribs['rc_minor'], @@ -236,14 +199,105 @@ } /** + * Send some text to UDP + * @param string $line + * @param string $prefix + * @param string $address + * @return bool success + */ + public static function sendToUDP( $line, $address = '', $prefix = '' ) { + global $wgRC2UDPAddress, $wgRC2UDPPrefix, $wgRC2UDPPort; + # Assume default for standard RC case + $address = $address ? $address : $wgRC2UDPAddress; + $prefix = $prefix ? $prefix : $wgRC2UDPPrefix; + # Notify external application via UDP + if( $address ) { + $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); + if( $conn ) { + $line = $prefix . $line; + wfDebug( __METHOD__ . ": sending UDP line: $line\n" ); + socket_sendto( $conn, $line, strlen($line), 0, $address, $wgRC2UDPPort ); + socket_close( $conn ); + return true; + } else { + wfDebug( __METHOD__ . ": failed to create UDP socket\n" ); + } + } + return false; + } + + /** + * Remove newlines and carriage returns + * @param string $line + * @return string + */ + public static function cleanupForIRC( $text ) { + return str_replace(array("\n", "\r"), array("", ""), $text); + } + + /** * Mark a given change as patrolled * * @param mixed $change RecentChange or corresponding rc_id + * @param bool $auto for automatic patrol + * @return See doMarkPatrolled(), or null if $change is not an existing rc_id */ - public static function markPatrolled( $change ) { - $rcid = $change instanceof RecentChange - ? $change->mAttribs['rc_id'] - : $change; + public static function markPatrolled( $change, $auto = false ) { + $change = $change instanceof RecentChange + ? $change + : RecentChange::newFromId($change); + if( !$change instanceof RecentChange ) { + return null; + } + return $change->doMarkPatrolled( $auto ); + } + + /** + * Mark this RecentChange as patrolled + * + * NOTE: Can also return 'rcpatroldisabled', 'hookaborted' and 'markedaspatrollederror-noautopatrol' as errors + * @param bool $auto for automatic patrol + * @return array of permissions errors, see Title::getUserPermissionsErrors() + */ + public function doMarkPatrolled( $auto = false ) { + global $wgUser, $wgUseRCPatrol, $wgUseNPPatrol; + $errors = array(); + // If recentchanges patrol is disabled, only new pages + // can be patrolled + if( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute('rc_type') != RC_NEW ) ) { + $errors[] = array('rcpatroldisabled'); + } + // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol" + $right = $auto ? 'autopatrol' : 'patrol'; + $errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $wgUser ) ); + if( !wfRunHooks('MarkPatrolled', array($this->getAttribute('rc_id'), &$wgUser, false)) ) { + $errors[] = array('hookaborted'); + } + // Users without the 'autopatrol' right can't patrol their + // own revisions + if( $wgUser->getName() == $this->getAttribute('rc_user_text') && !$wgUser->isAllowed('autopatrol') ) { + $errors[] = array('markedaspatrollederror-noautopatrol'); + } + if( $errors ) { + return $errors; + } + // If the change was patrolled already, do nothing + if( $this->getAttribute('rc_patrolled') ) { + return array(); + } + // Actually set the 'patrolled' flag in RC + $this->reallyMarkPatrolled(); + // Log this patrol event + PatrolLog::record( $this, $auto ); + wfRunHooks( 'MarkPatrolledComplete', array($this->getAttribute('rc_id'), &$wgUser, false) ); + return array(); + } + + /** + * Mark this RecentChange patrolled, without error checking + * @return int Number of affected rows + */ + public function reallyMarkPatrolled() { $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'recentchanges', @@ -251,25 +305,20 @@ 'rc_patrolled' => 1 ), array( - 'rc_id' => $rcid + 'rc_id' => $this->getAttribute('rc_id') ), __METHOD__ ); + return $dbw->affectedRows(); } # Makes an entry in the database corresponding to an edit - public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, - $oldId, $lastTimestamp, $bot = "default", $ip = '', $oldSize = 0, $newSize = 0, - $newId = 0) + public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId, + $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0 ) { - - if ( $bot === 'default' ) { - $bot = $user->isAllowed( 'bot' ); - } - - if ( !$ip ) { + if( !$ip ) { $ip = wfGetIP(); - if ( !$ip ) { + if( !$ip ) { $ip = ''; } } @@ -283,7 +332,7 @@ 'rc_type' => RC_EDIT, 'rc_minor' => $minor ? 1 : 0, 'rc_cur_id' => $title->getArticleID(), - 'rc_user' => $user->getID(), + 'rc_user' => $user->getId(), 'rc_user_text' => $user->getName(), 'rc_comment' => $comment, 'rc_this_oldid' => $newId, @@ -292,10 +341,15 @@ 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', 'rc_ip' => $ip, - 'rc_patrolled' => 0, + 'rc_patrolled' => intval($patrol), 'rc_new' => 0, # obsolete 'rc_old_len' => $oldSize, - 'rc_new_len' => $newSize + 'rc_new_len' => $newSize, + 'rc_deleted' => 0, + 'rc_logid' => 0, + 'rc_log_type' => null, + 'rc_log_action' => '', + 'rc_params' => '' ); $rc->mExtra = array( @@ -305,7 +359,7 @@ 'newSize' => $newSize, ); $rc->save(); - return( $rc->mAttribs['rc_id'] ); + return $rc; } /** @@ -313,18 +367,15 @@ * Note: the title object must be loaded with the new id using resetArticleID() * @todo Document parameters and return */ - public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = 'default', - $ip='', $size = 0, $newId = 0 ) + public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot, + $ip='', $size=0, $newId=0, $patrol=0 ) { - if ( !$ip ) { + if( !$ip ) { $ip = wfGetIP(); - if ( !$ip ) { + if( !$ip ) { $ip = ''; } } - if ( $bot === 'default' ) { - $bot = $user->isAllowed( 'bot' ); - } $rc = new RecentChange; $rc->mAttribs = array( @@ -335,7 +386,7 @@ 'rc_type' => RC_NEW, 'rc_minor' => $minor ? 1 : 0, 'rc_cur_id' => $title->getArticleID(), - 'rc_user' => $user->getID(), + 'rc_user' => $user->getId(), 'rc_user_text' => $user->getName(), 'rc_comment' => $comment, 'rc_this_oldid' => $newId, @@ -344,10 +395,15 @@ 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', 'rc_ip' => $ip, - 'rc_patrolled' => 0, - 'rc_new' => 1, # obsolete + 'rc_patrolled' => intval($patrol), + 'rc_new' => 1, # obsolete 'rc_old_len' => 0, - 'rc_new_len' => $size + 'rc_new_len' => $size, + 'rc_deleted' => 0, + 'rc_logid' => 0, + 'rc_log_type' => null, + 'rc_log_action' => '', + 'rc_params' => '' ); $rc->mExtra = array( @@ -357,15 +413,17 @@ 'newSize' => $size ); $rc->save(); - return( $rc->mAttribs['rc_id'] ); + return $rc; } # Makes an entry in the database corresponding to a rename public static function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false ) { - if ( !$ip ) { + global $wgRequest; + + if( !$ip ) { $ip = wfGetIP(); - if ( !$ip ) { + if( !$ip ) { $ip = ''; } } @@ -379,12 +437,12 @@ 'rc_type' => $overRedir ? RC_MOVE_OVER_REDIRECT : RC_MOVE, 'rc_minor' => 0, 'rc_cur_id' => $oldTitle->getArticleID(), - 'rc_user' => $user->getID(), + 'rc_user' => $user->getId(), 'rc_user_text' => $user->getName(), 'rc_comment' => $comment, 'rc_this_oldid' => 0, 'rc_last_oldid' => 0, - 'rc_bot' => $user->isAllowed( 'bot' ) ? 1 : 0, + 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0, 'rc_moved_to_ns' => $newTitle->getNamespace(), 'rc_moved_to_title' => $newTitle->getDBkey(), 'rc_ip' => $ip, @@ -392,6 +450,11 @@ 'rc_patrolled' => 1, 'rc_old_len' => NULL, 'rc_new_len' => NULL, + 'rc_deleted' => 0, + 'rc_logid' => 0, # notifyMove not used anymore + 'rc_log_type' => null, + 'rc_log_action' => '', + 'rc_params' => '' ); $rc->mExtra = array( @@ -410,14 +473,14 @@ RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true ); } - # A log entry is different to an edit in that previous revisions are - # not kept - public static function notifyLog( $timestamp, &$title, &$user, $comment, $ip='', - $type, $action, $target, $logComment, $params ) + public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip='', + $type, $action, $target, $logComment, $params, $newId=0 ) { - if ( !$ip ) { + global $wgRequest; + + if( !$ip ) { $ip = wfGetIP(); - if ( !$ip ) { + if( !$ip ) { $ip = ''; } } @@ -426,17 +489,17 @@ $rc->mAttribs = array( 'rc_timestamp' => $timestamp, 'rc_cur_time' => $timestamp, - 'rc_namespace' => $title->getNamespace(), - 'rc_title' => $title->getDBkey(), + 'rc_namespace' => $target->getNamespace(), + 'rc_title' => $target->getDBkey(), 'rc_type' => RC_LOG, 'rc_minor' => 0, - 'rc_cur_id' => $title->getArticleID(), - 'rc_user' => $user->getID(), + 'rc_cur_id' => $target->getArticleID(), + 'rc_user' => $user->getId(), 'rc_user_text' => $user->getName(), - 'rc_comment' => $comment, + 'rc_comment' => $logComment, 'rc_this_oldid' => 0, 'rc_last_oldid' => 0, - 'rc_bot' => $user->isAllowed( 'bot' ) ? 1 : 0, + 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0, 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', 'rc_ip' => $ip, @@ -444,30 +507,29 @@ 'rc_new' => 0, # obsolete 'rc_old_len' => NULL, 'rc_new_len' => NULL, + 'rc_deleted' => 0, + 'rc_logid' => $newId, + 'rc_log_type' => $type, + 'rc_log_action' => $action, + 'rc_params' => $params ); $rc->mExtra = array( 'prefixedDBkey' => $title->getPrefixedDBkey(), 'lastTimestamp' => 0, - 'logType' => $type, - 'logAction' => $action, - 'logComment' => $logComment, - 'logTarget' => $target, - 'logParams' => $params + 'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage ); $rc->save(); } # Initialises the members of this object from a mysql row object - function loadFromRow( $row ) - { + public function loadFromRow( $row ) { $this->mAttribs = get_object_vars( $row ); - $this->mAttribs["rc_timestamp"] = wfTimestamp(TS_MW, $this->mAttribs["rc_timestamp"]); - $this->mExtra = array(); + $this->mAttribs['rc_timestamp'] = wfTimestamp(TS_MW, $this->mAttribs['rc_timestamp']); + $this->mAttribs['rc_deleted'] = $row->rc_deleted; // MUST be set } # Makes a pseudo-RC entry from a cur row - function loadFromCurRow( $row ) - { + public function loadFromCurRow( $row ) { $this->mAttribs = array( 'rc_timestamp' => wfTimestamp(TS_MW, $row->rev_timestamp), 'rc_cur_time' => $row->rev_timestamp, @@ -490,9 +552,12 @@ 'rc_new' => $row->page_is_new, # obsolete 'rc_old_len' => $row->rc_old_len, 'rc_new_len' => $row->rc_new_len, + 'rc_params' => isset($row->rc_params) ? $row->rc_params : '', + 'rc_log_type' => isset($row->rc_log_type) ? $row->rc_log_type : null, + 'rc_log_action' => isset($row->rc_log_action) ? $row->rc_log_action : null, + 'rc_log_id' => isset($row->rc_log_id) ? $row->rc_log_id: 0, + 'rc_deleted' => $row->rc_deleted // MUST be set ); - - $this->mExtra = array(); } /** @@ -509,12 +574,11 @@ * Gets the end part of the diff URL associated with this object * Blank if no diff link should be displayed */ - function diffLinkTrail( $forceCur ) - { - if ( $this->mAttribs['rc_type'] == RC_EDIT ) { + public function diffLinkTrail( $forceCur ) { + if( $this->mAttribs['rc_type'] == RC_EDIT ) { $trail = "curid=" . (int)($this->mAttribs['rc_cur_id']) . "&oldid=" . (int)($this->mAttribs['rc_last_oldid']); - if ( $forceCur ) { + if( $forceCur ) { $trail .= '&diff=0' ; } else { $trail .= '&diff=' . (int)($this->mAttribs['rc_this_oldid']); @@ -525,49 +589,45 @@ return $trail; } - function cleanupForIRC( $text ) { - return str_replace(array("\n", "\r"), array("", ""), $text); - } - - function getIRCLine() { - global $wgUseRCPatrol; + protected function getIRCLine() { + global $wgUseRCPatrol, $wgUseNPPatrol, $wgRC2UDPInterwikiPrefix, $wgLocalInterwiki; // FIXME: Would be good to replace these 2 extract() calls with something more explicit // e.g. list ($rc_type, $rc_id) = array_values ($this->mAttribs); [or something like that] extract($this->mAttribs); extract($this->mExtra); - $titleObj =& $this->getTitle(); - if ( $rc_type == RC_LOG ) { - $title = Namespace::getCanonicalName( $titleObj->getNamespace() ) . $titleObj->getText(); + if( $rc_type == RC_LOG ) { + $titleObj = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL ); } else { - $title = $titleObj->getPrefixedText(); + $titleObj =& $this->getTitle(); } - $title = $this->cleanupForIRC( $title ); - - $bad = array("\n", "\r"); - $empty = array("", ""); $title = $titleObj->getPrefixedText(); - $title = str_replace($bad, $empty, $title); + $title = self::cleanupForIRC( $title ); - // FIXME: *HACK* these should be getFullURL(), hacked for SSL madness --brion 2005-12-26 - if ( $rc_type == RC_LOG ) { + if( $rc_type == RC_LOG ) { $url = ''; - } elseif ( $rc_new && $wgUseRCPatrol ) { - $url = $titleObj->getInternalURL("rcid=$rc_id"); - } else if ( $rc_new ) { - $url = $titleObj->getInternalURL(); - } else if ( $wgUseRCPatrol ) { - $url = $titleObj->getInternalURL("diff=$rc_this_oldid&oldid=$rc_last_oldid&rcid=$rc_id"); } else { - $url = $titleObj->getInternalURL("diff=$rc_this_oldid&oldid=$rc_last_oldid"); + if( $rc_type == RC_NEW ) { + $url = "oldid=$rc_this_oldid"; + } else { + $url = "diff=$rc_this_oldid&oldid=$rc_last_oldid"; + } + if( $wgUseRCPatrol || ($rc_type == RC_NEW && $wgUseNPPatrol) ) { + $url .= "&rcid=$rc_id"; + } + // XXX: *HACK* this should use getFullURL(), hacked for SSL madness --brion 2005-12-26 + // XXX: *HACK^2* the preg_replace() undoes much of what getInternalURL() does, but we + // XXX: need to call it so that URL paths on the Wikimedia secure server can be fixed + // XXX: by a custom GetInternalURL hook --vyznev 2008-12-10 + $url = preg_replace( '/title=[^&]*&/', '', $titleObj->getInternalURL( $url ) ); } - if ( isset( $oldSize ) && isset( $newSize ) ) { + if( isset( $oldSize ) && isset( $newSize ) ) { $szdiff = $newSize - $oldSize; - if ($szdiff < -500) { + if($szdiff < -500) { $szdiff = "\002$szdiff\002"; - } elseif ($szdiff >= 0) { + } elseif($szdiff >= 0) { $szdiff = '+' . $szdiff ; } $szdiff = '(' . $szdiff . ')' ; @@ -575,20 +635,35 @@ $szdiff = ''; } - $user = $this->cleanupForIRC( $rc_user_text ); + $user = self::cleanupForIRC( $rc_user_text ); + + if( $rc_type == RC_LOG ) { + $targetText = $this->getTitle()->getPrefixedText(); + $comment = self::cleanupForIRC( str_replace("[[$targetText]]","[[\00302$targetText\00310]]",$actionComment) ); + $flag = $rc_log_action; + } else { + $comment = self::cleanupForIRC( $rc_comment ); + $flag = ($rc_new ? "N" : "") . ($rc_minor ? "M" : "") . ($rc_bot ? "B" : ""); + } - if ( $rc_type == RC_LOG ) { - $logTargetText = $logTarget->getPrefixedText(); - $comment = $this->cleanupForIRC( str_replace( $logTargetText, "\00302$logTargetText\00310", $rc_comment ) ); - $flag = $logAction; + if ( $wgRC2UDPInterwikiPrefix === true ) { + $prefix = $wgLocalInterwiki; + } elseif ( $wgRC2UDPInterwikiPrefix ) { + $prefix = $wgRC2UDPInterwikiPrefix; } else { - $comment = $this->cleanupForIRC( $rc_comment ); - $flag = ($rc_minor ? "M" : "") . ($rc_new ? "N" : ""); + $prefix = false; } + if ( $prefix !== false ) { + $titleString = "\00314[[\00303$prefix:\00307$title\00314]]"; + } else { + $titleString = "\00314[[\00307$title\00314]]"; + } + # see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003, # no colour (\003) switches back to the term default - $fullString = "\00314[[\00307$title\00314]]\0034 $flag\00310 " . + $fullString = "$titleString\0034 $flag\00310 " . "\00302$url\003 \0035*\003 \00303$user\003 \0035*\003 $szdiff \00310$comment\003\n"; + return $fullString; } @@ -596,33 +671,16 @@ * Returns the change size (HTML). * The lengths can be given optionally. */ - function getCharacterDifference( $old = 0, $new = 0 ) { - global $wgRCChangedSizeThreshold, $wgLang; - + public function getCharacterDifference( $old = 0, $new = 0 ) { if( $old === 0 ) { $old = $this->mAttribs['rc_old_len']; } if( $new === 0 ) { $new = $this->mAttribs['rc_new_len']; } - if( $old === NULL || $new === NULL ) { return ''; } - - $szdiff = $new - $old; - $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape'), - $wgLang->formatNum($szdiff) ); - - if( $szdiff < $wgRCChangedSizeThreshold ) { - return '(' . $formatedSize . ')'; - } elseif( $szdiff === 0 ) { - return '(' . $formatedSize . ')'; - } elseif( $szdiff > 0 ) { - return '(+' . $formatedSize . ')'; - } else { - return '(' . $formatedSize . ')'; - } + return ChangesList::showCharacterDifference( $old, $new ); } } - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/RefreshLinksJob.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/RefreshLinksJob.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/RefreshLinksJob.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/RefreshLinksJob.php Fri Sep 12 05:37:31 2008 @@ -2,6 +2,8 @@ /** * Background job to update links for a given title. + * + * @ingroup JobQueue */ class RefreshLinksJob extends Job { @@ -17,7 +19,7 @@ global $wgParser; wfProfileIn( __METHOD__ ); - $linkCache =& LinkCache::singleton(); + $linkCache = LinkCache::singleton(); $linkCache->clear(); if ( is_null( $this->title ) ) { @@ -46,3 +48,86 @@ } } +/** + * Background job to update links for a given title. + * Newer version for high use templates. + * + * @ingroup JobQueue + */ +class RefreshLinksJob2 extends Job { + + function __construct( $title, $params, $id = 0 ) { + parent::__construct( 'refreshLinks2', $title, $params, $id ); + } + + /** + * Run a refreshLinks2 job + * @return boolean success + */ + function run() { + global $wgParser; + + wfProfileIn( __METHOD__ ); + + $linkCache = LinkCache::singleton(); + $linkCache->clear(); + + if( is_null( $this->title ) ) { + $this->error = "refreshLinks2: Invalid title"; + wfProfileOut( __METHOD__ ); + return false; + } + if( !isset($this->params['start']) || !isset($this->params['end']) ) { + $this->error = "refreshLinks2: Invalid params"; + wfProfileOut( __METHOD__ ); + return false; + } + $start = intval($this->params['start']); + $end = intval($this->params['end']); + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'templatelinks', 'page' ), + array( 'page_namespace', 'page_title' ), + array( + 'page_id=tl_from', + "tl_from >= '$start'", + "tl_from <= '$end'", + 'tl_namespace' => $this->title->getNamespace(), + 'tl_title' => $this->title->getDBkey() + ), __METHOD__ + ); + + # Not suitable for page load triggered job running! + # Gracefully switch to refreshLinks jobs if this happens. + if( php_sapi_name() != 'cli' ) { + $jobs = array(); + while( $row = $dbr->fetchObject( $res ) ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $jobs[] = new RefreshLinksJob( $title, '' ); + } + Job::batchInsert( $jobs ); + return true; + } + # Re-parse each page that transcludes this page and update their tracking links... + while( $row = $dbr->fetchObject( $res ) ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $revision = Revision::newFromTitle( $title ); + if ( !$revision ) { + $this->error = 'refreshLinks: Article not found "' . $title->getPrefixedDBkey() . '"'; + wfProfileOut( __METHOD__ ); + return false; + } + wfProfileIn( __METHOD__.'-parse' ); + $options = new ParserOptions; + $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() ); + wfProfileOut( __METHOD__.'-parse' ); + wfProfileIn( __METHOD__.'-update' ); + $update = new LinksUpdate( $title, $parserOutput, false ); + $update->doUpdate(); + wfProfileOut( __METHOD__.'-update' ); + wfProfileOut( __METHOD__ ); + } + + return true; + } +} diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Revision.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Revision.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Revision.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Revision.php Tue Dec 30 16:43:54 2008 @@ -1,6 +1,7 @@ $title->getNamespace(), + 'page_title' => $title->getDBkey() + ); + if ( $id ) { + // Use the specified ID + $conds['rev_id'] = $id; + } elseif ( wfGetLB()->getServerCount() > 1 ) { + // Get the latest revision ID from the master + $dbw = wfGetDB( DB_MASTER ); + $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); + $conds['rev_id'] = $latest; } else { - $matchId = 'page_latest'; + // Use a join to get the latest revision + $conds[] = 'rev_id=page_latest'; } - return Revision::newFromConds( - array( "rev_id=$matchId", - 'page_id=rev_page', - 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbkey() ) ); + $conds[] = 'page_id=rev_page'; + return Revision::newFromConds( $conds ); } /** @@ -59,7 +71,7 @@ * @access public * @static */ - public static function loadFromId( &$db, $id ) { + public static function loadFromId( $db, $id ) { return Revision::loadFromConds( $db, array( 'page_id=rev_page', 'rev_id' => intval( $id ) ) ); @@ -99,7 +111,7 @@ * @access public * @static */ - public static function loadFromTitle( &$db, $title, $id = 0 ) { + public static function loadFromTitle( $db, $title, $id = 0 ) { if( $id ) { $matchId = intval( $id ); } else { @@ -110,7 +122,7 @@ array( "rev_id=$matchId", 'page_id=rev_page', 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbkey() ) ); + 'page_title' => $title->getDBkey() ) ); } /** @@ -125,13 +137,13 @@ * @access public * @static */ - public static function loadFromTimestamp( &$db, &$title, $timestamp ) { + public static function loadFromTimestamp( $db, $title, $timestamp ) { return Revision::loadFromConds( $db, array( 'rev_timestamp' => $db->timestamp( $timestamp ), 'page_id=rev_page', 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbkey() ) ); + 'page_title' => $title->getDBkey() ) ); } /** @@ -145,7 +157,7 @@ private static function newFromConds( $conditions ) { $db = wfGetDB( DB_SLAVE ); $row = Revision::loadFromConds( $db, $conditions ); - if( is_null( $row ) ) { + if( is_null( $row ) && wfGetLB()->getServerCount() > 1 ) { $dbw = wfGetDB( DB_MASTER ); $row = Revision::loadFromConds( $dbw, $conditions ); } @@ -186,11 +198,11 @@ * @access public * @static */ - public static function fetchAllRevisions( &$title ) { + public static function fetchAllRevisions( $title ) { return Revision::fetchFromConds( wfGetDB( DB_SLAVE ), array( 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbkey(), + 'page_title' => $title->getDBkey(), 'page_id=rev_page' ) ); } @@ -204,12 +216,12 @@ * @access public * @static */ - public static function fetchRevision( &$title ) { + public static function fetchRevision( $title ) { return Revision::fetchFromConds( wfGetDB( DB_SLAVE ), array( 'rev_id=page_latest', 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDbkey(), + 'page_title' => $title->getDBkey(), 'page_id=rev_page' ) ); } @@ -225,44 +237,58 @@ * @static */ private static function fetchFromConds( $db, $conditions ) { + $fields = self::selectFields(); + $fields[] = 'page_namespace'; + $fields[] = 'page_title'; + $fields[] = 'page_latest'; $res = $db->select( array( 'page', 'revision' ), - array( 'page_namespace', - 'page_title', - 'page_latest', - 'rev_id', - 'rev_page', - 'rev_text_id', - 'rev_comment', - 'rev_user_text', - 'rev_user', - 'rev_minor_edit', - 'rev_timestamp', - 'rev_deleted', - 'rev_len' ), + $fields, $conditions, - 'Revision::fetchRow', + __METHOD__, array( 'LIMIT' => 1 ) ); $ret = $db->resultObject( $res ); return $ret; } /** - * Return the list of revision fields that should be selected to create + * Return the list of revision fields that should be selected to create * a new revision. */ static function selectFields() { - return array( + return array( 'rev_id', 'rev_page', 'rev_text_id', 'rev_timestamp', 'rev_comment', - 'rev_minor_edit', - 'rev_user', 'rev_user_text,'. + 'rev_user', + 'rev_minor_edit', 'rev_deleted', - 'rev_len' + 'rev_len', + 'rev_parent_id' + ); + } + + /** + * Return the list of text fields that should be selected to read the + * revision text + */ + static function selectTextFields() { + return array( + 'old_text', + 'old_flags' + ); + } + /** + * Return the list of page fields that should be selected from page table + */ + static function selectPageFields() { + return array( + 'page_namespace', + 'page_title', + 'page_latest' ); } @@ -281,16 +307,21 @@ $this->mMinorEdit = intval( $row->rev_minor_edit ); $this->mTimestamp = $row->rev_timestamp; $this->mDeleted = intval( $row->rev_deleted ); - + + if( !isset( $row->rev_parent_id ) ) + $this->mParentId = is_null($row->rev_parent_id) ? null : 0; + else + $this->mParentId = intval( $row->rev_parent_id ); + if( !isset( $row->rev_len ) || is_null( $row->rev_len ) ) $this->mSize = null; else - $this->mSize = intval( $row->rev_len ); + $this->mSize = intval( $row->rev_len ); if( isset( $row->page_latest ) ) { - $this->mCurrent = ( $row->rev_id == $row->page_latest ); - $this->mTitle = Title::makeTitle( $row->page_namespace, - $row->page_title ); + $this->mCurrent = ( $row->rev_id == $row->page_latest ); + $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); + $this->mTitle->resetArticleID( $this->mPage ); } else { $this->mCurrent = false; $this->mTitle = null; @@ -317,7 +348,8 @@ $this->mTimestamp = isset( $row['timestamp'] ) ? strval( $row['timestamp'] ) : wfTimestamp( TS_MW ); $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0; $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null; - + $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null; + // Enforce spacing trimming on supplied text $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null; $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null; @@ -338,23 +370,34 @@ */ /** + * Get revision ID * @return int */ - function getId() { + public function getId() { return $this->mId; } /** + * Get text row ID * @return int */ - function getTextId() { + public function getTextId() { return $this->mTextId; } /** + * Get parent revision ID (the original previous page revision) + * @return int + */ + public function getParentId() { + return $this->mParentId; + } + + /** * Returns the length of the text in this revision, or null if unknown. + * @return int */ - function getSize() { + public function getSize() { return $this->mSize; } @@ -362,7 +405,7 @@ * Returns the title of the page associated with this entry. * @return Title */ - function getTitle() { + public function getTitle() { if( isset( $this->mTitle ) ) { return $this->mTitle; } @@ -375,7 +418,7 @@ 'Revision::getTitle' ); if( $row ) { $this->mTitle = Title::makeTitle( $row->page_namespace, - $row->page_title ); + $row->page_title ); } return $this->mTitle; } @@ -384,23 +427,35 @@ * Set the title of the revision * @param Title $title */ - function setTitle( $title ) { + public function setTitle( $title ) { $this->mTitle = $title; } /** + * Get the page ID * @return int */ - function getPage() { + public function getPage() { return $this->mPage; } /** - * Fetch revision's user id if it's available to all users + * Fetch revision's user id if it's available to the specified audience. + * If the specified audience does not have access to it, zero will be + * returned. + * + * @param integer $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the ID regardless of permissions + * + * * @return int */ - function getUser() { - if( $this->isDeleted( self::DELETED_USER ) ) { + public function getUser( $audience = self::FOR_PUBLIC ) { + if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { + return 0; + } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER ) ) { return 0; } else { return $this->mUser; @@ -411,16 +466,26 @@ * Fetch revision's user id without regard for the current user's permissions * @return string */ - function getRawUser() { + public function getRawUser() { return $this->mUser; } /** - * Fetch revision's username if it's available to all users + * Fetch revision's username if it's available to the specified audience. + * If the specified audience does not have access to the username, an + * empty string will be returned. + * + * @param integer $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * * @return string */ - function getUserText() { - if( $this->isDeleted( self::DELETED_USER ) ) { + public function getUserText( $audience = self::FOR_PUBLIC ) { + if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { + return ""; + } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER ) ) { return ""; } else { return $this->mUserText; @@ -431,16 +496,26 @@ * Fetch revision's username without regard for view restrictions * @return string */ - function getRawUserText() { + public function getRawUserText() { return $this->mUserText; } - + /** - * Fetch revision comment if it's available to all users + * Fetch revision comment if it's available to the specified audience. + * If the specified audience does not have access to the comment, an + * empty string will be returned. + * + * @param integer $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * * @return string */ - function getComment() { - if( $this->isDeleted( self::DELETED_COMMENT ) ) { + function getComment( $audience = self::FOR_PUBLIC ) { + if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) { + return ""; + } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT ) ) { return ""; } else { return $this->mComment; @@ -451,14 +526,14 @@ * Fetch revision comment without regard for the current user's permissions * @return string */ - function getRawComment() { + public function getRawComment() { return $this->mComment; } /** * @return bool */ - function isMinor() { + public function isMinor() { return (bool)$this->mMinorEdit; } @@ -466,97 +541,135 @@ * int $field one of DELETED_* bitfield constants * @return bool */ - function isDeleted( $field ) { + public function isDeleted( $field ) { return ($this->mDeleted & $field) == $field; } + + /** + * Get the deletion bitfield of the revision + */ + public function getVisibility() { + return (int)$this->mDeleted; + } /** - * Fetch revision text if it's available to all users + * Fetch revision text if it's available to the specified audience. + * If the specified audience does not have the ability to view this + * revision, an empty string will be returned. + * + * @param integer $audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * + * * @return string */ - function getText() { - if( $this->isDeleted( self::DELETED_TEXT ) ) { + public function getText( $audience = self::FOR_PUBLIC ) { + if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) { + return ""; + } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT ) ) { return ""; } else { return $this->getRawText(); } } - + + /** + * Alias for getText(Revision::FOR_THIS_USER) + */ + public function revText() { + return $this->getText( self::FOR_THIS_USER ); + } + /** * Fetch revision text without regard for view restrictions * @return string */ - function getRawText() { + public function getRawText() { if( is_null( $this->mText ) ) { // Revision text is immutable. Load on demand: $this->mText = $this->loadText(); } return $this->mText; } - - /** - * Fetch revision text if it's available to THIS user - * @return string - */ - function revText() { - if( !$this->userCan( self::DELETED_TEXT ) ) { - return ""; - } else { - return $this->getRawText(); - } - } /** * @return string */ - function getTimestamp() { + public function getTimestamp() { return wfTimestamp(TS_MW, $this->mTimestamp); } /** * @return bool */ - function isCurrent() { + public function isCurrent() { return $this->mCurrent; } /** + * Get previous revision for this title * @return Revision */ - function getPrevious() { - $prev = $this->mTitle->getPreviousRevisionID( $this->mId ); - if ( $prev ) { - return Revision::newFromTitle( $this->mTitle, $prev ); - } else { - return null; + public function getPrevious() { + if( $this->getTitle() ) { + $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() ); + if( $prev ) { + return Revision::newFromTitle( $this->getTitle(), $prev ); + } } + return null; } /** * @return Revision */ - function getNext() { - $next = $this->mTitle->getNextRevisionID( $this->mId ); - if ( $next ) { - return Revision::newFromTitle( $this->mTitle, $next ); - } else { - return null; + public function getNext() { + if( $this->getTitle() ) { + $next = $this->getTitle()->getNextRevisionID( $this->getId() ); + if ( $next ) { + return Revision::newFromTitle( $this->getTitle(), $next ); + } + } + return null; + } + + /** + * Get previous revision Id for this page_id + * This is used to populate rev_parent_id on save + * @param Database $db + * @return int + */ + private function getPreviousRevisionId( $db ) { + if( is_null($this->mPage) ) { + return 0; } + # Use page_latest if ID is not given + if( !$this->mId ) { + $prevId = $db->selectField( 'page', 'page_latest', + array( 'page_id' => $this->mPage ), + __METHOD__ ); + } else { + $prevId = $db->selectField( 'revision', 'rev_id', + array( 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ), + __METHOD__, + array( 'ORDER BY' => 'rev_id DESC' ) ); + } + return intval($prevId); } - /**#@-*/ /** * Get revision text associated with an old or archive row * $row is usually an object from wfFetchRow(), both the flags and the text * field must be included - * @static - * @param integer $row Id of a row + * + * @param object $row The text data * @param string $prefix table prefix (default 'old_') * @return string $text|false the text requested */ public static function getRevisionText( $row, $prefix = 'old_' ) { - $fname = 'Revision::getRevisionText'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); # Get data $textField = $prefix . 'text'; @@ -571,7 +684,7 @@ if( isset( $row->$textField ) ) { $text = $row->$textField; } else { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } @@ -580,7 +693,7 @@ $url=$text; @list(/* $proto */,$path)=explode('://',$url,2); if ($path=="") { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } $text=ExternalStore::fetchFromURL($url); @@ -600,21 +713,23 @@ $obj = unserialize( $text ); if ( !is_object( $obj ) ) { // Invalid object - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return false; } $text = $obj->getText(); } global $wgLegacyEncoding; - if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) { + if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) ) { # Old revisions kept around in a legacy encoding? # Upconvert on demand. + # ("utf8" checked for compatibility with some broken + # conversion scripts 2008-12-30) global $wgInputEncoding, $wgContLang; $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding, $text ); } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $text; } @@ -625,11 +740,10 @@ * data is compressed, and 'utf-8' if we're saving in UTF-8 * mode. * - * @static * @param mixed $text reference to a text * @return string */ - function compressRevisionText( &$text ) { + public static function compressRevisionText( &$text ) { global $wgCompressRevisions; $flags = array(); @@ -655,30 +769,22 @@ * @param Database $dbw * @return int */ - function insertOn( &$dbw ) { + public function insertOn( $dbw ) { global $wgDefaultExternalStore; - - $fname = 'Revision::insertOn'; - wfProfileIn( $fname ); + + wfProfileIn( __METHOD__ ); $data = $this->mText; $flags = Revision::compressRevisionText( $data ); # Write to external storage if required - if ( $wgDefaultExternalStore ) { - if ( is_array( $wgDefaultExternalStore ) ) { - // Distribute storage across multiple clusters - $store = $wgDefaultExternalStore[mt_rand(0, count( $wgDefaultExternalStore ) - 1)]; - } else { - $store = $wgDefaultExternalStore; - } + if( $wgDefaultExternalStore ) { // Store and get the URL - $data = ExternalStore::insert( $store, $data ); - if ( !$data ) { - # This should only happen in the case of a configuration error, where the external store is not valid - throw new MWException( "Unable to store text to external storage $store" ); + $data = ExternalStore::insertToDefault( $data ); + if( !$data ) { + throw new MWException( "Unable to store text to external storage" ); } - if ( $flags ) { + if( $flags ) { $flags .= ','; } $flags .= 'external'; @@ -692,7 +798,7 @@ 'old_id' => $old_id, 'old_text' => $data, 'old_flags' => $flags, - ), $fname + ), __METHOD__ ); $this->mTextId = $dbw->insertId(); } @@ -713,11 +819,15 @@ 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ), 'rev_deleted' => $this->mDeleted, 'rev_len' => $this->mSize, - ), $fname + 'rev_parent_id' => $this->mParentId ? $this->mParentId : $this->getPreviousRevisionId( $dbw ) + ), __METHOD__ ); $this->mId = !is_null($rev_id) ? $rev_id : $dbw->insertId(); - wfProfileOut( $fname ); + + wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) ); + + wfProfileOut( __METHOD__ ); return $this->mId; } @@ -726,23 +836,21 @@ * Currently hardcoded to the 'text' table storage engine. * * @return string - * @access private */ - function loadText() { - $fname = 'Revision::loadText'; - wfProfileIn( $fname ); - + private function loadText() { + wfProfileIn( __METHOD__ ); + // Caching may be beneficial for massive use of external storage global $wgRevisionCacheExpiry, $wgMemc; $key = wfMemcKey( 'revisiontext', 'textid', $this->getTextId() ); if( $wgRevisionCacheExpiry ) { $text = $wgMemc->get( $key ); if( is_string( $text ) ) { - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $text; } } - + // If we kept data for lazy extraction, use it now... if ( isset( $this->mTextRow ) ) { $row = $this->mTextRow; @@ -750,32 +858,33 @@ } else { $row = null; } - + if( !$row ) { // Text data is immutable; check slaves first. $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'text', array( 'old_text', 'old_flags' ), array( 'old_id' => $this->getTextId() ), - $fname); + __METHOD__ ); } - if( !$row ) { + if( !$row && wfGetLB()->getServerCount() > 1 ) { // Possible slave lag! $dbw = wfGetDB( DB_MASTER ); $row = $dbw->selectRow( 'text', array( 'old_text', 'old_flags' ), array( 'old_id' => $this->getTextId() ), - $fname); + __METHOD__ ); } - $text = Revision::getRevisionText( $row ); - - if( $wgRevisionCacheExpiry ) { + $text = self::getRevisionText( $row ); + + # No negative caching -- negative hits on text rows may be due to corrupted slave servers + if( $wgRevisionCacheExpiry && $text !== false ) { $wgMemc->set( $key, $text, $wgRevisionCacheExpiry ); } - - wfProfileOut( $fname ); + + wfProfileOut( __METHOD__ ); return $text; } @@ -794,18 +903,17 @@ * @param bool $minor * @return Revision */ - function newNullRevision( &$dbw, $pageId, $summary, $minor ) { - $fname = 'Revision::newNullRevision'; - wfProfileIn( $fname ); + public static function newNullRevision( $dbw, $pageId, $summary, $minor ) { + wfProfileIn( __METHOD__ ); $current = $dbw->selectRow( array( 'page', 'revision' ), - array( 'page_latest', 'rev_text_id' ), + array( 'page_latest', 'rev_text_id', 'rev_len' ), array( 'page_id' => $pageId, 'page_latest=rev_id', ), - $fname ); + __METHOD__ ); if( $current ) { $revision = new Revision( array( @@ -813,15 +921,17 @@ 'comment' => $summary, 'minor_edit' => $minor, 'text_id' => $current->rev_text_id, + 'parent_id' => $current->page_latest, + 'len' => $current->rev_len ) ); } else { $revision = null; } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $revision; } - + /** * Determine if the current user is allowed to view a particular * field of this revision, if it's marked as deleted. @@ -830,11 +940,11 @@ * self::DELETED_USER * @return bool */ - function userCan( $field ) { + public function userCan( $field ) { if( ( $this->mDeleted & $field ) == $field ) { global $wgUser; $permission = ( $this->mDeleted & self::DELETED_RESTRICTED ) == self::DELETED_RESTRICTED - ? 'hiderevision' + ? 'suppressrevision' : 'deleterevision'; wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" ); return $wgUser->isAllowed( $permission ); @@ -846,21 +956,27 @@ /** * Get rev_timestamp from rev_id, without loading the rest of the row + * @param Title $title * @param integer $id */ - static function getTimestampFromID( $id ) { + static function getTimestampFromId( $title, $id ) { $dbr = wfGetDB( DB_SLAVE ); - $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', - array( 'rev_id' => $id ), __METHOD__ ); - if ( $timestamp === false ) { + $conds = array( 'rev_id' => $id ); + $conds['rev_page'] = $title->getArticleId(); + $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); + if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) { # Not in slave, try master $dbw = wfGetDB( DB_MASTER ); - $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', - array( 'rev_id' => $id ), __METHOD__ ); + $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); } - return $timestamp; + return wfTimestamp( TS_MW, $timestamp ); } - + + /** + * Get count of revisions per page...not very efficient + * @param Database $db + * @param int $id, page id + */ static function countByPageId( $db, $id ) { $row = $db->selectRow( 'revision', 'COUNT(*) AS revCount', array( 'rev_page' => $id ), __METHOD__ ); @@ -869,7 +985,12 @@ } return 0; } - + + /** + * Get count of revisions per page...not very efficient + * @param Database $db + * @param Title $title + */ static function countByTitle( $db, $title ) { $id = $title->getArticleId(); if( $id ) { @@ -886,6 +1007,3 @@ define( 'MW_REV_DELETED_COMMENT', Revision::DELETED_COMMENT ); define( 'MW_REV_DELETED_USER', Revision::DELETED_USER ); define( 'MW_REV_DELETED_RESTRICTED', Revision::DELETED_RESTRICTED ); - - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Sanitizer.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Sanitizer.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Sanitizer.php Fri Aug 31 00:48:45 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Sanitizer.php Tue Jan 6 21:31:30 2009 @@ -20,7 +20,8 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * - * @addtogroup Parser + * @file + * @ingroup Parser */ /** @@ -327,12 +328,9 @@ /** * XHTML sanitizer for MediaWiki - * @addtogroup Parser + * @ingroup Parser */ class Sanitizer { - const NONE = 0; - const INITIAL_NONLETTER = 1; - /** * Cleans up HTML, removes dangerous tags and attributes, and * removes HTML comments @@ -383,7 +381,7 @@ $htmlelements = array_merge( $htmlsingle, $htmlpairs, $htmlnest ); # Convert them all to hashtables for faster lookup - $vars = array( 'htmlpairs', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags', + $vars = array( 'htmlpairs', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags', 'htmllist', 'listtags', 'htmlsingleallowed', 'htmlelements' ); foreach ( $vars as $var ) { $$var = array_flip( $$var ); @@ -419,7 +417,7 @@ $optstack = array(); array_push ($optstack, $ot); while ( ( ( $ot = @array_pop( $tagstack ) ) != $t ) && - isset( $htmlsingleallowed[$ot] ) ) + isset( $htmlsingleallowed[$ot] ) ) { array_push ($optstack, $ot); } @@ -582,7 +580,7 @@ return Sanitizer::validateAttributes( $attribs, Sanitizer::attributeWhitelist( $element ) ); } - + /** * Take an array of attribute names and values and normalize or discard * illegal values for the given whitelist. @@ -615,8 +613,11 @@ } } - if ( $attribute === 'id' ) - $value = Sanitizer::escapeId( $value ); + if ( $attribute === 'id' ) { + global $wgEnforceHtmlIds; + $value = Sanitizer::escapeId( $value, + $wgEnforceHtmlIds ? 'noninitial' : 'xml' ); + } // If this attribute was previously set, override it. // Output should only have one attribute of each name. @@ -624,12 +625,11 @@ } return $out; } - + /** - * Merge two sets of HTML attributes. - * Conflicting items in the second set will override those - * in the first, except for 'class' attributes which will be - * combined. + * Merge two sets of HTML attributes. Conflicting items in the second set + * will override those in the first, except for 'class' attributes which + * will be combined (if they're both strings). * * @todo implement merging for other attributes such as style * @param array $a @@ -638,20 +638,16 @@ */ static function mergeAttributes( $a, $b ) { $out = array_merge( $a, $b ); - if( isset( $a['class'] ) - && isset( $b['class'] ) - && $a['class'] !== $b['class'] ) { - - $out['class'] = implode( ' ', - array_unique( - preg_split( '/\s+/', - $a['class'] . ' ' . $b['class'], - -1, - PREG_SPLIT_NO_EMPTY ) ) ); + if( isset( $a['class'] ) && isset( $b['class'] ) + && is_string( $a['class'] ) && is_string( $b['class'] ) + && $a['class'] !== $b['class'] ) { + $classes = preg_split( '/\s+/', "{$a['class']} {$b['class']}", + -1, PREG_SPLIT_NO_EMPTY ); + $out['class'] = implode( ' ', array_unique( $classes ) ); } return $out; } - + /** * Pick apart some CSS and check it for forbidden or unsafe structures. * Returns a sanitized string, or false if it was just too evil. @@ -666,7 +662,7 @@ // Remove any comments; IE gets token splitting wrong $stripped = StringUtils::delimiterReplace( '/*', '*/', ' ', $stripped ); - + $value = $stripped; // ... and continue checks @@ -678,7 +674,7 @@ # haxx0r return false; } - + return $value; } @@ -725,7 +721,7 @@ * @return HTML-encoded text fragment */ static function encodeAttribute( $text ) { - $encValue = htmlspecialchars( $text ); + $encValue = htmlspecialchars( $text, ENT_QUOTES ); // Whitespace is normalized during attribute decoding, // so if we've been passed non-spaces we must encode them @@ -781,28 +777,55 @@ * name attributes * @see http://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with the id attribute * - * @param string $id Id to validate - * @param int $flags Currently only two values: Sanitizer::INITIAL_NONLETTER - * (default) permits initial non-letter characters, - * such as if you're adding a prefix to them. - * Sanitizer::NONE will prepend an 'x' if the id - * would otherwise start with a nonletter. + * @param string $id Id to validate + * @param mixed $options String or array of strings (default is array()): + * 'noninitial': This is a non-initial fragment of an id, not a full id, + * so don't pay attention if the first character isn't valid at the + * beginning of an id. + * 'xml': Don't restrict the id to be HTML4-compatible. This option + * allows any alphabetic character to be used, per the XML standard. + * Therefore, it also completely changes the type of escaping: instead + * of weird dot-encoding, runs of invalid characters (mostly + * whitespace) are just compressed into a single underscore. * @return string */ - static function escapeId( $id, $flags = Sanitizer::INITIAL_NONLETTER ) { - static $replace = array( - '%3A' => ':', - '%' => '.' - ); - - $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) ); - $id = str_replace( array_keys( $replace ), array_values( $replace ), $id ); - - if( ~$flags & Sanitizer::INITIAL_NONLETTER - && !preg_match( '/[a-zA-Z]/', $id[0] ) ) { - // Initial character must be a letter! - $id = "x$id"; + static function escapeId( $id, $options = array() ) { + $options = (array)$options; + + if ( !in_array( 'xml', $options ) ) { + # HTML4-style escaping + static $replace = array( + '%3A' => ':', + '%' => '.' + ); + + $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) ); + $id = str_replace( array_keys( $replace ), array_values( $replace ), $id ); + + if ( !preg_match( '/^[a-zA-Z]/', $id ) + && !in_array( 'noninitial', $options ) ) { + // Initial character must be a letter! + $id = "x$id"; + } + return $id; } + + # XML-style escaping. For the patterns used, see the XML 1.0 standard, + # 5th edition, NameStartChar and NameChar: + $nameStartChar = ':a-zA-Z_\xC0-\xD6\xD8-\xF6\xF8-\x{2FF}\x{370}-\x{37D}' + . '\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}' + . '\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}'; + $nameChar = $nameStartChar . '.\-0-9\xB7\x{0300}-\x{036F}' + . '\x{203F}-\x{2040}'; + # Replace _ as well so we don't get multiple consecutive underscores + $id = preg_replace( "/([^$nameChar]|_)+/u", '_', $id ); + $id = trim( $id, '_' ); + + if ( !preg_match( "/^[$nameStartChar]/u", $id ) + && !in_array( 'noninitial', $options ) ) { + $id = "_$id"; + } + return $id; } @@ -826,6 +849,22 @@ } /** + * Given HTML input, escape with htmlspecialchars but un-escape entites. + * This allows (generally harmless) entities like   to survive. + * + * @param string $html String to escape + * @return string Escaped input + */ + static function escapeHtmlAllowEntities( $html ) { + # It seems wise to escape ' as well as ", as a matter of course. Can't + # hurt. + $html = htmlspecialchars( $html, ENT_QUOTES ); + $html = str_replace( '&', '&', $html ); + $html = Sanitizer::normalizeCharReferences( $html ); + return $html; + } + + /** * Regex replace callback for armoring links against further processing. * @param array $matches * @return string @@ -843,7 +882,7 @@ * @param string * @return array */ - static function decodeTagAttributes( $text ) { + public static function decodeTagAttributes( $text ) { $attribs = array(); if( trim( $text ) == '' ) { @@ -920,7 +959,7 @@ self::normalizeWhitespace( Sanitizer::normalizeCharReferences( $text ) ) ); } - + private static function normalizeWhitespace( $text ) { return preg_replace( '/\r\n|[\x20\x0d\x0a\x09]/', @@ -972,8 +1011,8 @@ /** * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD, - * return the named entity reference as is. If the entity is a - * MediaWiki-specific alias, returns the HTML equivalent. Otherwise, + * return the named entity reference as is. If the entity is a + * MediaWiki-specific alias, returns the HTML equivalent. Otherwise, * returns HTML-escaped text of pseudo-entity source (eg &foo;) * * @param string $name @@ -1110,7 +1149,8 @@ } /** - * @todo Document it a bit + * Foreach array key (an allowed HTML element), return an array + * of allowed attributes * @return array */ static function setupAttributeWhitelist() { @@ -1219,7 +1259,7 @@ # 11.2.6 'td' => array_merge( $common, $tablecell, $tablealign ), 'th' => array_merge( $common, $tablecell, $tablealign ), - + # 13.2 # Not usually allowed, but may be used for extension-style hooks # such as when it is rasterized @@ -1250,7 +1290,7 @@ 'rb' => $common, 'rt' => $common, #array_merge( $common, array( 'rbspan' ) ), 'rp' => $common, - + # MathML root element, where used for extensions # 'title' may not be 100% valid here; it's XHTML # http://www.w3.org/TR/REC-MathML/ @@ -1300,7 +1340,7 @@ return $out; } - static function cleanUrl( $url, $hostname=true ) { + static function cleanUrl( $url ) { # Normalize any HTML entities in input. They will be # re-escaped by makeExternalLink(). $url = Sanitizer::decodeCharReferences( $url ); @@ -1343,5 +1383,3 @@ } } - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchEngine.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchEngine.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchEngine.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchEngine.php Mon Jan 5 15:46:43 2009 @@ -1,11 +1,19 @@ test prefix:Main Page/Archive + */ + function transformSearchTerm( $term ) { + return $term; + } + /** * If an exact title match can be find, or a very slightly close match, * return the title. If no match, returns NULL. @@ -59,48 +80,42 @@ if (is_null($title)) return NULL; - if ( $title->getNamespace() == NS_SPECIAL || $title->exists() ) { + if ( $title->getNamespace() == NS_SPECIAL || $title->isExternal() + || $title->exists() ) { return $title; } # Now try all lower case (i.e. first letter capitalized) # $title = Title::newFromText( $wgContLang->lc( $term ) ); - if ( $title->exists() ) { + if ( $title && $title->exists() ) { return $title; } # Now try capitalized string # $title = Title::newFromText( $wgContLang->ucwords( $term ) ); - if ( $title->exists() ) { + if ( $title && $title->exists() ) { return $title; } # Now try all upper case # $title = Title::newFromText( $wgContLang->uc( $term ) ); - if ( $title->exists() ) { + if ( $title && $title->exists() ) { return $title; } # Now try Word-Caps-Breaking-At-Word-Breaks, for hyphenated names etc $title = Title::newFromText( $wgContLang->ucwordbreaks($term) ); - if ( $title->exists() ) { + if ( $title && $title->exists() ) { return $title; } - global $wgCapitalLinks, $wgContLang; - if( !$wgCapitalLinks ) { - // Catch differs-by-first-letter-case-only - $title = Title::newFromText( $wgContLang->ucfirst( $term ) ); - if ( $title->exists() ) { - return $title; - } - $title = Title::newFromText( $wgContLang->lcfirst( $term ) ); - if ( $title->exists() ) { - return $title; - } + // Give hooks a chance at better match variants + $title = null; + if( !wfRunHooks( 'SearchGetNearMatch', array( $term, &$title ) ) ) { + return $title; } } @@ -109,7 +124,7 @@ # Entering an IP address goes to the contributions page if ( ( $title->getNamespace() == NS_USER && User::isIP($title->getText() ) ) || User::isIP( trim( $searchterm ) ) ) { - return SpecialPage::getTitleFor( 'Contributions', $title->getDbkey() ); + return SpecialPage::getTitleFor( 'Contributions', $title->getDBkey() ); } @@ -117,11 +132,11 @@ if ( $title->getNamespace() == NS_USER ) { return $title; } - + # Go to images that exist even if there's no local page. # There may have been a funny upload, or it may be on a shared # file repository such as Wikimedia Commons. - if( $title->getNamespace() == NS_IMAGE ) { + if( $title->getNamespace() == NS_FILE ) { $image = wfFindFile( $title ); if( $image ) { return $title; @@ -139,12 +154,12 @@ if( preg_match( '/^"([^"]+)"$/', $searchterm, $matches ) ) { return SearchEngine::getNearMatch( $matches[1] ); } - + return NULL; } public static function legalSearchChars() { - return "A-Za-z_'0-9\\x80-\\xFF\\-"; + return "A-Za-z_'.0-9\\x80-\\xFF\\-"; } /** @@ -172,6 +187,37 @@ } /** + * Parse some common prefixes: all (search everything) + * or namespace names + * + * @param string $query + */ + function replacePrefixes( $query ){ + global $wgContLang; + + if( strpos($query,':') === false ) + return $query; // nothing to do + + $parsed = $query; + $allkeyword = wfMsgForContent('searchall').":"; + if( strncmp($query, $allkeyword, strlen($allkeyword)) == 0 ){ + $this->namespaces = null; + $parsed = substr($query,strlen($allkeyword)); + } else if( strpos($query,':') !== false ) { + $prefix = substr($query,0,strpos($query,':')); + $index = $wgContLang->getNsIndex($prefix); + if($index !== false){ + $this->namespaces = array($index); + $parsed = substr($query,strlen($prefix)+1); + } + } + if(trim($parsed) == '') + return $query; // prefix was the whole query + + return $parsed; + } + + /** * Make a list of searchable namespaces and their canonical names. * @return array */ @@ -185,7 +231,96 @@ } return $arr; } - + + /** + * Extract default namespaces to search from the given user's + * settings, returning a list of index numbers. + * + * @param User $user + * @return array + * @static + */ + public static function userNamespaces( &$user ) { + $arr = array(); + foreach( SearchEngine::searchableNamespaces() as $ns => $name ) { + if( $user->getOption( 'searchNs' . $ns ) ) { + $arr[] = $ns; + } + } + return $arr; + } + + /** + * Find snippet highlight settings for a given user + * + * @param User $user + * @return array contextlines, contextchars + * @static + */ + public static function userHighlightPrefs( &$user ){ + //$contextlines = $user->getOption( 'contextlines', 5 ); + //$contextchars = $user->getOption( 'contextchars', 50 ); + $contextlines = 2; // Hardcode this. Old defaults sucked. :) + $contextchars = 75; // same as above.... :P + return array($contextlines, $contextchars); + } + + /** + * An array of namespaces indexes to be searched by default + * + * @return array + * @static + */ + public static function defaultNamespaces(){ + global $wgNamespacesToBeSearchedDefault; + + return array_keys($wgNamespacesToBeSearchedDefault, true); + } + + /** + * Get a list of namespace names useful for showing in tooltips + * and preferences + * + * @param unknown_type $namespaces + */ + public static function namespacesAsText( $namespaces ){ + global $wgContLang; + + $formatted = array_map( array($wgContLang,'getFormattedNsText'), $namespaces ); + foreach( $formatted as $key => $ns ){ + if ( empty($ns) ) + $formatted[$key] = wfMsg( 'blanknamespace' ); + } + return $formatted; + } + + /** + * An array of "project" namespaces indexes typically searched + * by logged-in users + * + * @return array + * @static + */ + public static function projectNamespaces() { + global $wgNamespacesToBeSearchedDefault, $wgNamespacesToBeSearchedProject; + + return array_keys( $wgNamespacesToBeSearchedProject, true ); + } + + /** + * An array of "project" namespaces indexes typically searched + * by logged-in users in addition to the default namespaces + * + * @return array + * @static + */ + public static function defaultAndProjectNamespaces() { + global $wgNamespacesToBeSearchedDefault, $wgNamespacesToBeSearchedProject; + + return array_keys( $wgNamespacesToBeSearchedDefault + + $wgNamespacesToBeSearchedProject, true); + } + /** * Return a 'cleaned up' search string * @@ -203,19 +338,14 @@ * @return SearchEngine */ public static function create() { - global $wgDBtype, $wgSearchType; + global $wgSearchType; + $dbr = wfGetDB( DB_SLAVE ); if( $wgSearchType ) { $class = $wgSearchType; - } elseif( $wgDBtype == 'mysql' ) { - $class = 'SearchMySQL4'; - } else if ( $wgDBtype == 'postgres' ) { - $class = 'SearchPostgres'; - } else if ( $wgDBtype == 'oracle' ) { - $class = 'SearchOracle'; } else { - $class = 'SearchEngineDummy'; + $class = $dbr->getSearchEngine(); } - $search = new $class( wfGetDB( DB_SLAVE ) ); + $search = new $class( $dbr ); $search->setLimitOffset(0,0); return $search; } @@ -244,11 +374,41 @@ function updateTitle( $id, $title ) { // no-op } + + /** + * Get OpenSearch suggestion template + * + * @return string + * @static + */ + public static function getOpenSearchTemplate() { + global $wgOpenSearchTemplate, $wgServer, $wgScriptPath; + if( $wgOpenSearchTemplate ) { + return $wgOpenSearchTemplate; + } else { + $ns = implode( '|', SearchEngine::defaultNamespaces() ); + if( !$ns ) $ns = "0"; + return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace='.$ns; + } + } + + /** + * Get internal MediaWiki Suggest template + * + * @return string + * @static + */ + public static function getMWSuggestTemplate() { + global $wgMWSuggestTemplate, $wgServer, $wgScriptPath; + if($wgMWSuggestTemplate) + return $wgMWSuggestTemplate; + else + return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace={namespaces}'; + } } - /** - * @addtogroup Search + * @ingroup Search */ class SearchResultSet { /** @@ -303,15 +463,47 @@ } /** - * Some search modes return a suggested alternate term if there are - * no exact hits. Check hasSuggestion() first. + * @return string suggested query, null if none + */ + function getSuggestionQuery(){ + return null; + } + + /** + * @return string HTML highlighted suggested query, '' if none + */ + function getSuggestionSnippet(){ + return ''; + } + + /** + * Return information about how and from where the results were fetched, + * should be useful for diagnostics and debugging * * @return string - * @access public */ - function getSuggestion() { - return ''; + function getInfo() { + return null; + } + + /** + * Return a result set of hits on other (multiple) wikis associated with this one + * + * @return SearchResultSet + */ + function getInterwikiResults() { + return null; + } + + /** + * Check if there are results on other wikis + * + * @return boolean + */ + function hasInterwikiResults() { + return $this->getInterwikiResults() != null; } + /** * Fetches next search result, or false. @@ -322,7 +514,7 @@ function next() { return false; } - + /** * Frees the result set, if applicable. * @ access public @@ -334,11 +526,52 @@ /** - * @addtogroup Search + * @ingroup Search + */ +class SearchResultTooMany { + ## Some search engines may bail out if too many matches are found +} + + +/** + * @fixme This class is horribly factored. It would probably be better to have + * a useful base class to which you pass some standard information, then let + * the fancy self-highlighters extend that. + * @ingroup Search */ class SearchResult { - function SearchResult( $row ) { + var $mRevision = null; + var $mImage = null; + + function __construct( $row ) { $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); + if( !is_null($this->mTitle) ){ + $this->mRevision = Revision::newFromTitle( $this->mTitle ); + if( $this->mTitle->getNamespace() === NS_FILE ) + $this->mImage = wfFindFile( $this->mTitle ); + } + } + + /** + * Check if this is result points to an invalid title + * + * @return boolean + * @access public + */ + function isBrokenTitle(){ + if( is_null($this->mTitle) ) + return true; + return false; + } + + /** + * Check if target page is missing, happens when index is out of date + * + * @return boolean + * @access public + */ + function isMissingRevision(){ + return !$this->mRevision && !$this->mImage; } /** @@ -355,20 +588,613 @@ function getScore() { return null; } + + /** + * Lazy initialization of article text from DB + */ + protected function initText(){ + if( !isset($this->mText) ){ + if($this->mRevision != null) + $this->mText = $this->mRevision->getText(); + else // TODO: can we fetch raw wikitext for commons images? + $this->mText = ''; + + } + } + + /** + * @param array $terms terms to highlight + * @return string highlighted text snippet, null (and not '') if not supported + */ + function getTextSnippet($terms){ + global $wgUser, $wgAdvancedSearchHighlighting; + $this->initText(); + list($contextlines,$contextchars) = SearchEngine::userHighlightPrefs($wgUser); + $h = new SearchHighlighter(); + if( $wgAdvancedSearchHighlighting ) + return $h->highlightText( $this->mText, $terms, $contextlines, $contextchars ); + else + return $h->highlightSimple( $this->mText, $terms, $contextlines, $contextchars ); + } + + /** + * @param array $terms terms to highlight + * @return string highlighted title, '' if not supported + */ + function getTitleSnippet($terms){ + return ''; + } + + /** + * @param array $terms terms to highlight + * @return string highlighted redirect name (redirect to this page), '' if none or not supported + */ + function getRedirectSnippet($terms){ + return ''; + } + + /** + * @return Title object for the redirect to this page, null if none or not supported + */ + function getRedirectTitle(){ + return null; + } + + /** + * @return string highlighted relevant section name, null if none or not supported + */ + function getSectionSnippet(){ + return ''; + } + + /** + * @return Title object (pagename+fragment) for the section, null if none or not supported + */ + function getSectionTitle(){ + return null; + } + + /** + * @return string timestamp + */ + function getTimestamp(){ + if( $this->mRevision ) + return $this->mRevision->getTimestamp(); + else if( $this->mImage ) + return $this->mImage->getTimestamp(); + return ''; + } + + /** + * @return int number of words + */ + function getWordCount(){ + $this->initText(); + return str_word_count( $this->mText ); + } + + /** + * @return int size in bytes + */ + function getByteSize(){ + $this->initText(); + return strlen( $this->mText ); + } + + /** + * @return boolean if hit has related articles + */ + function hasRelated(){ + return false; + } + + /** + * @return interwiki prefix of the title (return iw even if title is broken) + */ + function getInterwikiPrefix(){ + return ''; + } } /** - * @addtogroup Search + * Highlight bits of wikitext + * + * @ingroup Search */ -class SearchEngineDummy { - function search( $term ) { - return null; +class SearchHighlighter { + var $mCleanWikitext = true; + + function SearchHighlighter($cleanupWikitext = true){ + $this->mCleanWikitext = $cleanupWikitext; } - function setLimitOffset($l, $o) {} - function legalSearchChars() {} - function update() {} - function setnamespaces() {} - function searchtitle() {} - function searchtext() {} + + /** + * Default implementation of wikitext highlighting + * + * @param string $text + * @param array $terms Terms to highlight (unescaped) + * @param int $contextlines + * @param int $contextchars + * @return string + */ + public function highlightText( $text, $terms, $contextlines, $contextchars ) { + global $wgLang, $wgContLang; + global $wgSearchHighlightBoundaries; + $fname = __METHOD__; + + if($text == '') + return ''; + + // spli text into text + templates/links/tables + $spat = "/(\\{\\{)|(\\[\\[[^\\]:]+:)|(\n\\{\\|)"; + // first capture group is for detecting nested templates/links/tables/references + $endPatterns = array( + 1 => '/(\{\{)|(\}\})/', // template + 2 => '/(\[\[)|(\]\])/', // image + 3 => "/(\n\\{\\|)|(\n\\|\\})/"); // table + + // FIXME: this should prolly be a hook or something + if(function_exists('wfCite')){ + $spat .= '|()'; // references via cite extension + $endPatterns[4] = '/()|(<\/ref>)/'; + } + $spat .= '/'; + $textExt = array(); // text extracts + $otherExt = array(); // other extracts + wfProfileIn( "$fname-split" ); + $start = 0; + $textLen = strlen($text); + $count = 0; // sequence number to maintain ordering + while( $start < $textLen ){ + // find start of template/image/table + if( preg_match( $spat, $text, $matches, PREG_OFFSET_CAPTURE, $start ) ){ + $epat = ''; + foreach($matches as $key => $val){ + if($key > 0 && $val[1] != -1){ + if($key == 2){ + // see if this is an image link + $ns = substr($val[0],2,-1); + if( $wgContLang->getNsIndex($ns) != NS_FILE ) + break; + + } + $epat = $endPatterns[$key]; + $this->splitAndAdd( $textExt, $count, substr( $text, $start, $val[1] - $start ) ); + $start = $val[1]; + break; + } + } + if( $epat ){ + // find end (and detect any nested elements) + $level = 0; + $offset = $start + 1; + $found = false; + while( preg_match( $epat, $text, $endMatches, PREG_OFFSET_CAPTURE, $offset ) ){ + if( array_key_exists(2,$endMatches) ){ + // found end + if($level == 0){ + $len = strlen($endMatches[2][0]); + $off = $endMatches[2][1]; + $this->splitAndAdd( $otherExt, $count, + substr( $text, $start, $off + $len - $start ) ); + $start = $off + $len; + $found = true; + break; + } else{ + // end of nested element + $level -= 1; + } + } else{ + // nested + $level += 1; + } + $offset = $endMatches[0][1] + strlen($endMatches[0][0]); + } + if( ! $found ){ + // couldn't find appropriate closing tag, skip + $this->splitAndAdd( $textExt, $count, substr( $text, $start, strlen($matches[0][0]) ) ); + $start += strlen($matches[0][0]); + } + continue; + } + } + // else: add as text extract + $this->splitAndAdd( $textExt, $count, substr($text,$start) ); + break; + } + + $all = $textExt + $otherExt; // these have disjunct key sets + + wfProfileOut( "$fname-split" ); + + // prepare regexps + foreach( $terms as $index => $term ) { + // manually do upper/lowercase stuff for utf-8 since PHP won't do it + if(preg_match('/[\x80-\xff]/', $term) ){ + $terms[$index] = preg_replace_callback('/./us',array($this,'caseCallback'),$terms[$index]); + } else { + $terms[$index] = $term; + } + } + $anyterm = implode( '|', $terms ); + $phrase = implode("$wgSearchHighlightBoundaries+", $terms ); + + // FIXME: a hack to scale contextchars, a correct solution + // would be to have contextchars actually be char and not byte + // length, and do proper utf-8 substrings and lengths everywhere, + // but PHP is making that very hard and unclean to implement :( + $scale = strlen($anyterm) / mb_strlen($anyterm); + $contextchars = intval( $contextchars * $scale ); + + $patPre = "(^|$wgSearchHighlightBoundaries)"; + $patPost = "($wgSearchHighlightBoundaries|$)"; + + $pat1 = "/(".$phrase.")/ui"; + $pat2 = "/$patPre(".$anyterm.")$patPost/ui"; + + wfProfileIn( "$fname-extract" ); + + $left = $contextlines; + + $snippets = array(); + $offsets = array(); + + // show beginning only if it contains all words + $first = 0; + $firstText = ''; + foreach($textExt as $index => $line){ + if(strlen($line)>0 && $line[0] != ';' && $line[0] != ':'){ + $firstText = $this->extract( $line, 0, $contextchars * $contextlines ); + $first = $index; + break; + } + } + if( $firstText ){ + $succ = true; + // check if first text contains all terms + foreach($terms as $term){ + if( ! preg_match("/$patPre".$term."$patPost/ui", $firstText) ){ + $succ = false; + break; + } + } + if( $succ ){ + $snippets[$first] = $firstText; + $offsets[$first] = 0; + } + } + if( ! $snippets ) { + // match whole query on text + $this->process($pat1, $textExt, $left, $contextchars, $snippets, $offsets); + // match whole query on templates/tables/images + $this->process($pat1, $otherExt, $left, $contextchars, $snippets, $offsets); + // match any words on text + $this->process($pat2, $textExt, $left, $contextchars, $snippets, $offsets); + // match any words on templates/tables/images + $this->process($pat2, $otherExt, $left, $contextchars, $snippets, $offsets); + + ksort($snippets); + } + + // add extra chars to each snippet to make snippets constant size + $extended = array(); + if( count( $snippets ) == 0){ + // couldn't find the target words, just show beginning of article + $targetchars = $contextchars * $contextlines; + $snippets[$first] = ''; + $offsets[$first] = 0; + } else{ + // if begin of the article contains the whole phrase, show only that !! + if( array_key_exists($first,$snippets) && preg_match($pat1,$snippets[$first]) + && $offsets[$first] < $contextchars * 2 ){ + $snippets = array ($first => $snippets[$first]); + } + + // calc by how much to extend existing snippets + $targetchars = intval( ($contextchars * $contextlines) / count ( $snippets ) ); + } + + foreach($snippets as $index => $line){ + $extended[$index] = $line; + $len = strlen($line); + if( $len < $targetchars - 20 ){ + // complete this line + if($len < strlen( $all[$index] )){ + $extended[$index] = $this->extract( $all[$index], $offsets[$index], $offsets[$index]+$targetchars, $offsets[$index]); + $len = strlen( $extended[$index] ); + } + + // add more lines + $add = $index + 1; + while( $len < $targetchars - 20 + && array_key_exists($add,$all) + && !array_key_exists($add,$snippets) ){ + $offsets[$add] = 0; + $tt = "\n".$this->extract( $all[$add], 0, $targetchars - $len, $offsets[$add] ); + $extended[$add] = $tt; + $len += strlen( $tt ); + $add++; + } + } + } + + //$snippets = array_map('htmlspecialchars', $extended); + $snippets = $extended; + $last = -1; + $extract = ''; + foreach($snippets as $index => $line){ + if($last == -1) + $extract .= $line; // first line + elseif($last+1 == $index && $offsets[$last]+strlen($snippets[$last]) >= strlen($all[$last])) + $extract .= " ".$line; // continous lines + else + $extract .= ' ... ' . $line; + + $last = $index; + } + if( $extract ) + $extract .= ' ... '; + + $processed = array(); + foreach($terms as $term){ + if( ! isset($processed[$term]) ){ + $pat3 = "/$patPre(".$term.")$patPost/ui"; // highlight word + $extract = preg_replace( $pat3, + "\\1\\2\\3", $extract ); + $processed[$term] = true; + } + } + + wfProfileOut( "$fname-extract" ); + + return $extract; + } + + /** + * Split text into lines and add it to extracts array + * + * @param array $extracts index -> $line + * @param int $count + * @param string $text + */ + function splitAndAdd(&$extracts, &$count, $text){ + $split = explode( "\n", $this->mCleanWikitext? $this->removeWiki($text) : $text ); + foreach($split as $line){ + $tt = trim($line); + if( $tt ) + $extracts[$count++] = $tt; + } + } + + /** + * Do manual case conversion for non-ascii chars + * + * @param unknown_type $matches + */ + function caseCallback($matches){ + global $wgContLang; + if( strlen($matches[0]) > 1 ){ + return '['.$wgContLang->lc($matches[0]).$wgContLang->uc($matches[0]).']'; + } else + return $matches[0]; + } + + /** + * Extract part of the text from start to end, but by + * not chopping up words + * @param string $text + * @param int $start + * @param int $end + * @param int $posStart (out) actual start position + * @param int $posEnd (out) actual end position + * @return string + */ + function extract($text, $start, $end, &$posStart = null, &$posEnd = null ){ + global $wgContLang; + + if( $start != 0) + $start = $this->position( $text, $start, 1 ); + if( $end >= strlen($text) ) + $end = strlen($text); + else + $end = $this->position( $text, $end ); + + if(!is_null($posStart)) + $posStart = $start; + if(!is_null($posEnd)) + $posEnd = $end; + + if($end > $start) + return substr($text, $start, $end-$start); + else + return ''; + } + + /** + * Find a nonletter near a point (index) in the text + * + * @param string $text + * @param int $point + * @param int $offset to found index + * @return int nearest nonletter index, or beginning of utf8 char if none + */ + function position($text, $point, $offset=0 ){ + $tolerance = 10; + $s = max( 0, $point - $tolerance ); + $l = min( strlen($text), $point + $tolerance ) - $s; + $m = array(); + if( preg_match('/[ ,.!?~!@#$%^&*\(\)+=\-\\\|\[\]"\'<>]/', substr($text,$s,$l), $m, PREG_OFFSET_CAPTURE ) ){ + return $m[0][1] + $s + $offset; + } else{ + // check if point is on a valid first UTF8 char + $char = ord( $text[$point] ); + while( $char >= 0x80 && $char < 0xc0 ) { + // skip trailing bytes + $point++; + if($point >= strlen($text)) + return strlen($text); + $char = ord( $text[$point] ); + } + return $point; + + } + } + + /** + * Search extracts for a pattern, and return snippets + * + * @param string $pattern regexp for matching lines + * @param array $extracts extracts to search + * @param int $linesleft number of extracts to make + * @param int $contextchars length of snippet + * @param array $out map for highlighted snippets + * @param array $offsets map of starting points of snippets + * @protected + */ + function process( $pattern, $extracts, &$linesleft, &$contextchars, &$out, &$offsets ){ + if($linesleft == 0) + return; // nothing to do + foreach($extracts as $index => $line){ + if( array_key_exists($index,$out) ) + continue; // this line already highlighted + + $m = array(); + if ( !preg_match( $pattern, $line, $m, PREG_OFFSET_CAPTURE ) ) + continue; + + $offset = $m[0][1]; + $len = strlen($m[0][0]); + if($offset + $len < $contextchars) + $begin = 0; + elseif( $len > $contextchars) + $begin = $offset; + else + $begin = $offset + intval( ($len - $contextchars) / 2 ); + + $end = $begin + $contextchars; + + $posBegin = $begin; + // basic snippet from this line + $out[$index] = $this->extract($line,$begin,$end,$posBegin); + $offsets[$index] = $posBegin; + $linesleft--; + if($linesleft == 0) + return; + } + } + + /** + * Basic wikitext removal + * @protected + */ + function removeWiki($text) { + $fname = __METHOD__; + wfProfileIn( $fname ); + + //$text = preg_replace("/'{2,5}/", "", $text); + //$text = preg_replace("/\[[a-z]+:\/\/[^ ]+ ([^]]+)\]/", "\\2", $text); + //$text = preg_replace("/\[\[([^]|]+)\]\]/", "\\1", $text); + //$text = preg_replace("/\[\[([^]]+\|)?([^|]]+)\]\]/", "\\2", $text); + //$text = preg_replace("/\\{\\|(.*?)\\|\\}/", "", $text); + //$text = preg_replace("/\\[\\[[A-Za-z_-]+:([^|]+?)\\]\\]/", "", $text); + $text = preg_replace("/\\{\\{([^|]+?)\\}\\}/", "", $text); + $text = preg_replace("/\\{\\{([^|]+\\|)(.*?)\\}\\}/", "\\2", $text); + $text = preg_replace("/\\[\\[([^|]+?)\\]\\]/", "\\1", $text); + $text = preg_replace_callback("/\\[\\[([^|]+\\|)(.*?)\\]\\]/", array($this,'linkReplace'), $text); + //$text = preg_replace("/\\[\\[([^|]+\\|)(.*?)\\]\\]/", "\\2", $text); + $text = preg_replace("/<\/?[^>]+>/", "", $text); + $text = preg_replace("/'''''/", "", $text); + $text = preg_replace("/('''|<\/?[iIuUbB]>)/", "", $text); + $text = preg_replace("/''/", "", $text); + + wfProfileOut( $fname ); + return $text; + } + + /** + * callback to replace [[target|caption]] kind of links, if + * the target is category or image, leave it + * + * @param array $matches + */ + function linkReplace($matches){ + $colon = strpos( $matches[1], ':' ); + if( $colon === false ) + return $matches[2]; // replace with caption + global $wgContLang; + $ns = substr( $matches[1], 0, $colon ); + $index = $wgContLang->getNsIndex($ns); + if( $index !== false && ($index == NS_FILE || $index == NS_CATEGORY) ) + return $matches[0]; // return the whole thing + else + return $matches[2]; + + } + + /** + * Simple & fast snippet extraction, but gives completely unrelevant + * snippets + * + * @param string $text + * @param array $terms + * @param int $contextlines + * @param int $contextchars + * @return string + */ + public function highlightSimple( $text, $terms, $contextlines, $contextchars ) { + global $wgLang, $wgContLang; + $fname = __METHOD__; + + $lines = explode( "\n", $text ); + + $terms = implode( '|', $terms ); + $max = intval( $contextchars ) + 1; + $pat1 = "/(.*)($terms)(.{0,$max})/i"; + + $lineno = 0; + + $extract = ""; + wfProfileIn( "$fname-extract" ); + foreach ( $lines as $line ) { + if ( 0 == $contextlines ) { + break; + } + ++$lineno; + $m = array(); + if ( ! preg_match( $pat1, $line, $m ) ) { + continue; + } + --$contextlines; + $pre = $wgContLang->truncate( $m[1], -$contextchars, ' ... ' ); + + if ( count( $m ) < 3 ) { + $post = ''; + } else { + $post = $wgContLang->truncate( $m[3], $contextchars, ' ... ' ); + } + + $found = $m[2]; + + $line = htmlspecialchars( $pre . $found . $post ); + $pat2 = '/(' . $terms . ")/i"; + $line = preg_replace( $pat2, + "\\1", $line ); + + $extract .= "${line}\n"; + } + wfProfileOut( "$fname-extract" ); + + return $extract; + } + } +/** + * Dummy class to be used when non-supported Database engine is present. + * @fixme Dummy class should probably try something at least mildly useful, + * such as a LIKE search through titles. + * @ingroup Search + */ +class SearchEngineDummy extends SearchEngine { + // no-op +} diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchMySQL.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchMySQL.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchMySQL.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchMySQL.php Mon Dec 22 07:31:15 2008 @@ -18,11 +18,71 @@ # http://www.gnu.org/copyleft/gpl.html /** - * Search engine hook base class for MySQL. - * Specific bits for MySQL 3 and 4 variants are in child classes. - * @addtogroup Search + * @file + * @ingroup Search + */ + +/** + * Search engine hook for MySQL 4+ + * @ingroup Search */ class SearchMySQL extends SearchEngine { + var $strictMatching = true; + + /** @todo document */ + function __construct( $db ) { + $this->db = $db; + } + + /** + * Parse the user's query and transform it into an SQL fragment which will + * become part of a WHERE clause + */ + function parseQuery( $filteredText, $fulltext ) { + global $wgContLang; + $lc = SearchEngine::legalSearchChars(); // Minus format chars + $searchon = ''; + $this->searchTerms = array(); + + # FIXME: This doesn't handle parenthetical expressions. + $m = array(); + if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/', + $filteredText, $m, PREG_SET_ORDER ) ) { + foreach( $m as $terms ) { + if( $searchon !== '' ) $searchon .= ' '; + if( $this->strictMatching && ($terms[1] == '') ) { + $terms[1] = '+'; + } + $searchon .= $terms[1] . $wgContLang->stripForSearch( $terms[2] ); + if( !empty( $terms[3] ) ) { + // Match individual terms in result highlighting... + $regexp = preg_quote( $terms[3], '/' ); + if( $terms[4] ) { + $regexp = "\b$regexp"; // foo* + } else { + $regexp = "\b$regexp\b"; + } + } else { + // Match the quoted term in result highlighting... + $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' ); + } + $this->searchTerms[] = $regexp; + } + wfDebug( "Would search with '$searchon'\n" ); + wfDebug( 'Match with /' . implode( '|', $this->searchTerms ) . "/\n" ); + } else { + wfDebug( "Can't understand search query '{$filteredText}'\n" ); + } + + $searchon = $this->db->strencode( $searchon ); + $field = $this->getIndexField( $fulltext ); + return " MATCH($field) AGAINST('$searchon' IN BOOLEAN MODE) "; + } + + public static function legalSearchChars() { + return "\"*" . parent::legalSearchChars(); + } + /** * Perform a full text search query and return a result set. * @@ -67,9 +127,12 @@ * @private */ function queryNamespaces() { - $namespaces = implode( ',', $this->namespaces ); - if ($namespaces == '') { + if( is_null($this->namespaces) ) + return ''; # search all + if ( !count( $this->namespaces ) ) { $namespaces = '0'; + } else { + $namespaces = $this->db->makeList( $this->namespaces ); } return 'AND page_namespace IN (' . $namespaces . ')'; } @@ -154,7 +217,7 @@ 'si_page' => $id, 'si_title' => $title, 'si_text' => $text - ), 'SearchMySQL4::update' ); + ), __METHOD__ ); } /** @@ -170,13 +233,13 @@ $dbw->update( 'searchindex', array( 'si_title' => $title ), array( 'si_page' => $id ), - 'SearchMySQL4::updateTitle', + __METHOD__, array( $dbw->lowPriorityOption() ) ); } } /** - * @addtogroup Search + * @ingroup Search */ class MySQLSearchResultSet extends SearchResultSet { function MySQLSearchResultSet( $resultSet, $terms ) { @@ -200,10 +263,8 @@ return new SearchResult( $row ); } } - + function free() { $this->mResultSet->free(); } } - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchMySQL4.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchMySQL4.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchMySQL4.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchMySQL4.php Thu Jun 19 17:02:23 2008 @@ -18,51 +18,17 @@ # http://www.gnu.org/copyleft/gpl.html /** + * @file + * @ingroup Search + */ + +/** * Search engine hook for MySQL 4+ - * @addtogroup Search + * This class retained for backwards compatibility... + * The meat's been moved to SearchMySQL, since the 3.x variety is gone. + * @ingroup Search + * @deprecated */ class SearchMySQL4 extends SearchMySQL { - var $strictMatching = true; - - /** @todo document */ - function SearchMySQL4( $db ) { - $this->db = $db; - } - - /** @todo document */ - function parseQuery( $filteredText, $fulltext ) { - global $wgContLang; - $lc = SearchEngine::legalSearchChars(); - $searchon = ''; - $this->searchTerms = array(); - - # FIXME: This doesn't handle parenthetical expressions. - $m = array(); - if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/', - $filteredText, $m, PREG_SET_ORDER ) ) { - foreach( $m as $terms ) { - if( $searchon !== '' ) $searchon .= ' '; - if( $this->strictMatching && ($terms[1] == '') ) { - $terms[1] = '+'; - } - $searchon .= $terms[1] . $wgContLang->stripForSearch( $terms[2] ); - if( !empty( $terms[3] ) ) { - $regexp = preg_quote( $terms[3], '/' ); - if( $terms[4] ) $regexp .= "[0-9A-Za-z_]+"; - } else { - $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' ); - } - $this->searchTerms[] = $regexp; - } - wfDebug( "Would search with '$searchon'\n" ); - wfDebug( 'Match with /\b' . implode( '\b|\b', $this->searchTerms ) . "\b/\n" ); - } else { - wfDebug( "Can't understand search query '{$filteredText}'\n" ); - } - - $searchon = $this->db->strencode( $searchon ); - $field = $this->getIndexField( $fulltext ); - return " MATCH($field) AGAINST('$searchon' IN BOOLEAN MODE) "; - } + /* whee */ } - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchOracle.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchOracle.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchOracle.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchOracle.php Mon Dec 22 07:31:15 2008 @@ -18,8 +18,13 @@ # http://www.gnu.org/copyleft/gpl.html /** + * @file + * @ingroup Search + */ + +/** * Search engine hook base class for Oracle (ConText). - * @addtogroup Search + * @ingroup Search */ class SearchOracle extends SearchEngine { function __construct($db) { @@ -70,9 +75,12 @@ * @private */ function queryNamespaces() { - $namespaces = implode(',', $this->namespaces); - if ($namespaces == '') { + if( is_null($this->namespaces) ) + return ''; + if ( !count( $this->namespaces ) ) { $namespaces = '0'; + } else { + $namespaces = $this->db->makeList( $this->namespaces ); } return 'AND page_namespace IN (' . $namespaces . ')'; } @@ -137,7 +145,10 @@ 'WHERE page_id=si_page AND ' . $match; } - /** @todo document */ + /** + * Parse a user input search string, and return an SQL fragment to be used + * as part of a WHERE clause + */ function parseQuery($filteredText, $fulltext) { global $wgContLang; $lc = SearchEngine::legalSearchChars(); @@ -163,9 +174,9 @@ } } - $searchon = $this->db->strencode(join(',', $q)); + $searchon = $this->db->addQuotes(join(',', $q)); $field = $this->getIndexField($fulltext); - return " CONTAINS($field, '$searchon', 1) > 0 "; + return " CONTAINS($field, $searchon, 1) > 0 "; } /** @@ -208,7 +219,7 @@ } /** - * @addtogroup Search + * @ingroup Search */ class OracleSearchResultSet extends SearchResultSet { function __construct($resultSet, $terms) { @@ -231,5 +242,3 @@ return new SearchResult($row); } } - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchPostgres.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchPostgres.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchPostgres.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchPostgres.php Mon Dec 22 07:31:15 2008 @@ -18,18 +18,23 @@ # http://www.gnu.org/copyleft/gpl.html /** + * @file + * @ingroup Search + */ + +/** * Search engine hook base class for Postgres - * @addtogroup Search + * @ingroup Search */ class SearchPostgres extends SearchEngine { - function SearchPostgres( $db ) { + function __construct( $db ) { $this->db = $db; } /** * Perform a full text search query via tsearch2 and return a result set. - * Currently searches a page's current title (page.page_title) and + * Currently searches a page's current title (page.page_title) and * latest revision article text (pagecontent.old_text) * * @param string $term - Raw search term @@ -37,17 +42,31 @@ * @access public */ function searchTitle( $term ) { - $resultSet = $this->db->resultObject( $this->db->query( $this->searchQuery( $term , 'titlevector', 'page_title' ))); + $q = $this->searchQuery( $term , 'titlevector', 'page_title' ); + $olderror = error_reporting(E_ERROR); + $resultSet = $this->db->resultObject( $this->db->query( $q, 'SearchPostgres', true ) ); + error_reporting($olderror); + if (!$resultSet) { + // Needed for "Query requires full scan, GIN doesn't support it" + return new SearchResultTooMany(); + } return new PostgresSearchResultSet( $resultSet, $this->searchTerms ); } function searchText( $term ) { - $resultSet = $this->db->resultObject( $this->db->query( $this->searchQuery( $term, 'textvector', 'old_text' ))); + $q = $this->searchQuery( $term, 'textvector', 'old_text' ); + $olderror = error_reporting(E_ERROR); + $resultSet = $this->db->resultObject( $this->db->query( $q, 'SearchPostgres', true ) ); + error_reporting($olderror); + if (!$resultSet) { + return new SearchResultTooMany(); + } return new PostgresSearchResultSet( $resultSet, $this->searchTerms ); } /* * Transform the user's search string into a better form for tsearch2 + * Returns an SQL fragment consisting of quoted text to search for. */ function parseQuery( $term ) { @@ -122,11 +141,13 @@ $this->db->getServerVersion(); $wgDBversion = $this->db->numeric_version; } + $prefix = $wgDBversion < 8.3 ? "'default'," : ''; + # Get the SQL fragment for the given term $searchstring = $this->parseQuery( $term ); ## We need a separate query here so gin does not complain about empty searches - $SQL = "SELECT to_tsquery('default',$searchstring)"; + $SQL = "SELECT to_tsquery($prefix $searchstring)"; $res = $this->db->doQuery($SQL); if (!$res) { ## TODO: Better output (example to catch: one 'two) @@ -148,22 +169,25 @@ } $rankscore = $wgDBversion > 8.2 ? 5 : 1; + $rank = $wgDBversion < 8.3 ? 'rank' : 'ts_rank'; $query = "SELECT page_id, page_namespace, page_title, ". - "rank($fulltext, to_tsquery('default',$searchstring), $rankscore) AS score ". + "$rank($fulltext, to_tsquery($prefix $searchstring), $rankscore) AS score ". "FROM page p, revision r, pagecontent c WHERE p.page_latest = r.rev_id " . - "AND r.rev_text_id = c.old_id AND $fulltext @@ to_tsquery('default',$searchstring)"; + "AND r.rev_text_id = c.old_id AND $fulltext @@ to_tsquery($prefix $searchstring)"; } ## Redirects if (! $this->showRedirects) - $query .= ' AND page_is_redirect = 0'; ## IS FALSE + $query .= ' AND page_is_redirect = 0'; ## Namespaces - defaults to 0 - if ( count($this->namespaces) < 1) - $query .= ' AND page_namespace = 0'; - else { - $namespaces = implode( ',', $this->namespaces ); - $query .= " AND page_namespace IN ($namespaces)"; + if( !is_null($this->namespaces) ){ // null -> search all + if ( count($this->namespaces) < 1) + $query .= ' AND page_namespace = 0'; + else { + $namespaces = $this->db->makeList( $this->namespaces ); + $query .= " AND page_namespace IN ($namespaces)"; + } } $query .= " ORDER BY score DESC, page_id DESC"; @@ -179,9 +203,9 @@ function update( $pageid, $title, $text ) { ## We don't want to index older revisions - $SQL = "UPDATE pagecontent SET textvector = NULL WHERE old_id = ". - "(SELECT rev_text_id FROM revision WHERE rev_page = $pageid ". - "ORDER BY rev_text_id DESC LIMIT 1 OFFSET 1)"; + $SQL = "UPDATE pagecontent SET textvector = NULL WHERE old_id IN ". + "(SELECT rev_text_id FROM revision WHERE rev_page = " . intval( $pageid ) . + " ORDER BY rev_text_id DESC OFFSET 1)"; $this->db->doQuery($SQL); return true; } @@ -193,11 +217,11 @@ } ## end of the SearchPostgres class /** - * @addtogroup Search + * @ingroup Search */ class PostgresSearchResult extends SearchResult { - function PostgresSearchResult( $row ) { - $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); + function __construct( $row ) { + parent::__construct($row); $this->score = $row->score; } function getScore() { @@ -206,10 +230,10 @@ } /** - * @addtogroup Search + * @ingroup Search */ class PostgresSearchResultSet extends SearchResultSet { - function PostgresSearchResultSet( $resultSet, $terms ) { + function __construct( $resultSet, $terms ) { $this->mResultSet = $resultSet; $this->mTerms = $terms; } @@ -231,6 +255,3 @@ } } } - - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchUpdate.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchUpdate.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SearchUpdate.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SearchUpdate.php Thu Jun 19 17:02:23 2008 @@ -1,7 +1,7 @@ mId, $this->mNamespace, $this->mTitle, &$text ) ); - + # Perform the actual update $search->update($this->mId, Title::indexTitle( $this->mNamespace, $this->mTitle ), $text); - + wfProfileOut( $fname ); } } /** * Placeholder class - * @addtogroup Search + * @ingroup Search */ class SearchUpdateMyISAM extends SearchUpdate { # Inherits everything } - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Setup.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Setup.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Setup.php Sun Jul 22 10:45:12 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Setup.php Tue Dec 23 18:34:35 2008 @@ -10,7 +10,7 @@ if( !defined( 'MEDIAWIKI' ) ) { echo "This file is part of MediaWiki, it is not a valid entry point.\n"; exit( 1 ); -} +} # The main wiki script and things like database # conversion and maintenance scripts all share a @@ -58,12 +58,28 @@ $wgFileStore['deleted']['directory'] = "{$wgUploadDirectory}/deleted"; } +/** + * Unconditional protection for NS_MEDIAWIKI since otherwise it's too easy for a + * sysadmin to set $wgNamespaceProtection incorrectly and leave the wiki insecure. + * + * Note that this is the definition of editinterface and it can be granted to + * all users if desired. + */ +$wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface'; + +/** + * The canonical names of namespaces 6 and 7 are, as of v1.14, "File" + * and "File_talk". The old names "Image" and "Image_talk" are + * retained as aliases for backwards compatibility. + */ +$wgNamespaceAliases['Image'] = NS_FILE; +$wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; /** * Initialise $wgLocalFileRepo from backwards-compatible settings */ if ( !$wgLocalFileRepo ) { - $wgLocalFileRepo = array( + $wgLocalFileRepo = array( 'class' => 'LocalRepo', 'name' => 'local', 'directory' => $wgUploadDirectory, @@ -101,7 +117,7 @@ 'fetchDescription' => $wgFetchCommonsDescriptions, ); } else { - $wgForeignFileRepos[] = array( + $wgForeignFileRepos[] = array( 'class' => 'FSRepo', 'name' => 'shared', 'directory' => $wgSharedUploadDirectory, @@ -114,8 +130,9 @@ ); } } - -require_once( "$IP/includes/AutoLoader.php" ); +if ( !class_exists( 'AutoLoader' ) ) { + require_once( "$IP/includes/AutoLoader.php" ); +} wfProfileIn( $fname.'-exception' ); require_once( "$IP/includes/Exception.php" ); @@ -137,12 +154,6 @@ $wgIP = false; # Load on demand # Can't stub this one, it sets up $_GET and $_REQUEST in its constructor $wgRequest = new WebRequest; -if ( function_exists( 'posix_uname' ) ) { - $wguname = posix_uname(); - $wgNodeName = $wguname['nodename']; -} else { - $wgNodeName = ''; -} # Useful debug output if ( $wgCommandLineMode ) { @@ -159,6 +170,19 @@ wfDebug( $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . "\n" ); } +if( $wgRCFilterByAge ) { + ## Trim down $wgRCLinkDays so that it only lists links which are valid + ## as determined by $wgRCMaxAge. + ## Note that we allow 1 link higher than the max for things like 56 days but a 60 day link. + sort($wgRCLinkDays); + for( $i = 0; $i < count($wgRCLinkDays); $i++ ) { + if( $wgRCLinkDays[$i] >= $wgRCMaxAge / ( 3600 * 24 ) ) { + $wgRCLinkDays = array_slice( $wgRCLinkDays, 0, $i+1, false ); + break; + } + } +} + if ( $wgSkipSkin ) { $wgSkipSkins[] = $wgSkipSkin; } @@ -181,24 +205,35 @@ $parserMemc =& wfGetParserCacheStorage(); wfDebug( 'Main cache: ' . get_class( $wgMemc ) . - "\nMessage cache: " . get_class( $messageMemc ) . - "\nParser cache: " . get_class( $parserMemc ) . "\n" ); + "\nMessage cache: " . get_class( $messageMemc ) . + "\nParser cache: " . get_class( $parserMemc ) . "\n" ); wfProfileOut( $fname.'-memcached' ); + +## Most of the config is out, some might want to run hooks here. +wfRunHooks( 'SetupAfterCache' ); + wfProfileIn( $fname.'-SetupSession' ); -if ( $wgDBprefix ) { - $wgCookiePrefix = $wgDBname . '_' . $wgDBprefix; -} elseif ( $wgSharedDB ) { - $wgCookiePrefix = $wgSharedDB; -} else { - $wgCookiePrefix = $wgDBname; +# Set default shared prefix +if( $wgSharedPrefix === false ) $wgSharedPrefix = $wgDBprefix; + +if( !$wgCookiePrefix ) { + if ( $wgSharedDB && $wgSharedPrefix && in_array('user',$wgSharedTables) ) { + $wgCookiePrefix = $wgSharedDB . '_' . $wgSharedPrefix; + } elseif ( $wgSharedDB && in_array('user',$wgSharedTables) ) { + $wgCookiePrefix = $wgSharedDB; + } elseif ( $wgDBprefix ) { + $wgCookiePrefix = $wgDBname . '_' . $wgDBprefix; + } else { + $wgCookiePrefix = $wgDBname; + } } $wgCookiePrefix = strtr($wgCookiePrefix, "=,; +.\"'\\[", "__________"); # If session.auto_start is there, we can't touch session name # -if( !ini_get( 'session.auto_start' ) ) +if( !wfIniGetBool( 'session.auto_start' ) ) session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' ); if( !$wgCommandLineMode && ( $wgRequest->checkSessionCookie() || isset( $_COOKIE[$wgCookiePrefix.'Token'] ) ) ) { @@ -213,20 +248,6 @@ wfProfileOut( $fname.'-SetupSession' ); wfProfileIn( $fname.'-globals' ); -if ( !$wgDBservers ) { - $wgDBservers = array(array( - 'host' => $wgDBserver, - 'user' => $wgDBuser, - 'password' => $wgDBpassword, - 'dbname' => $wgDBname, - 'type' => $wgDBtype, - 'load' => 1, - 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT - )); -} - -$wgLoadBalancer = new StubObject( 'wgLoadBalancer', 'LoadBalancer', - array( $wgDBservers, false, $wgMasterWaitTimeout, true ) ); $wgContLang = new StubContLang; // Now that variant lists may be available... @@ -235,9 +256,10 @@ $wgUser = new StubUser; $wgLang = new StubUserLang; $wgOut = new StubObject( 'wgOut', 'OutputPage' ); -$wgParser = new StubObject( 'wgParser', 'Parser' ); -$wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache', - array( $parserMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry, wfWikiID() ) ); +$wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) ); + +$wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache', + array( $messageMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry, wfWikiID() ) ); wfProfileOut( $fname.'-globals' ); wfProfileIn( $fname.'-User' ); @@ -245,7 +267,7 @@ # Skin setup functions # Entries can be added to this variable during the inclusion # of the extension file. Skins can then perform any necessary initialisation. -# +# foreach ( $wgSkinExtensionFunctions as $func ) { call_user_func( $func ); } @@ -262,14 +284,11 @@ $wgDeferredUpdateList = array(); $wgPostCommitUpdateList = array(); -if ( $wgAjaxSearch ) $wgAjaxExportList[] = 'wfSajaxSearch'; if ( $wgAjaxWatch ) $wgAjaxExportList[] = 'wfAjaxWatch'; if ( $wgAjaxUploadDestCheck ) $wgAjaxExportList[] = 'UploadForm::ajaxGetExistsWarning'; if( $wgAjaxLicensePreview ) $wgAjaxExportList[] = 'UploadForm::ajaxGetLicensePreview'; -wfSeedRandom(); - # Placeholders in case of DB error $wgTitle = null; $wgArticle = null; @@ -294,10 +313,18 @@ wfRunHooks( 'LogPageLogHeader', array( &$wgLogHeaders ) ); wfRunHooks( 'LogPageActionText', array( &$wgLogActions ) ); +if( !empty($wgNewUserLog) ) { + # Add a new log type + $wgLogTypes[] = 'newusers'; + $wgLogNames['newusers'] = 'newuserlogpage'; + $wgLogHeaders['newusers'] = 'newuserlogpagetext'; + $wgLogActions['newusers/newusers'] = 'newuserlogentry'; // For compatibility with older log entries + $wgLogActions['newusers/create'] = 'newuserlog-create-entry'; + $wgLogActions['newusers/create2'] = 'newuserlog-create2-entry'; + $wgLogActions['newusers/autocreate'] = 'newuserlog-autocreate-entry'; +} wfDebug( "Fully initialised\n" ); $wgFullyInitialised = true; wfProfileOut( $fname.'-extensions' ); wfProfileOut( $fname ); - - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SiteConfiguration.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SiteConfiguration.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SiteConfiguration.php Thu Jun 28 21:19:14 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SiteConfiguration.php Tue Aug 26 11:10:12 2008 @@ -5,101 +5,347 @@ * meaning that require_once() fails to detect that it is including the same * file again. We use DIY C-style protection as a workaround. */ -if (!defined('SITE_CONFIGURATION')) { -define('SITE_CONFIGURATION', 1); + +// Hide this pattern from Doxygen, which spazzes out at it +/// @cond +if( !defined( 'SITE_CONFIGURATION' ) ){ +define( 'SITE_CONFIGURATION', 1 ); +/// @endcond /** * This is a class used to hold configuration settings, particularly for multi-wiki sites. - * */ class SiteConfiguration { - var $suffixes = array(); - var $wikis = array(); - var $settings = array(); - var $localVHosts = array(); - - /** */ - function get( $setting, $wiki, $suffix, $params = array() ) { - if ( array_key_exists( $setting, $this->settings ) ) { - if ( array_key_exists( $wiki, $this->settings[$setting] ) ) { - $retval = $this->settings[$setting][$wiki]; - } elseif ( array_key_exists( $suffix, $this->settings[$setting] ) ) { - $retval = $this->settings[$setting][$suffix]; - } elseif ( array_key_exists( 'default', $this->settings[$setting] ) ) { - $retval = $this->settings[$setting]['default']; - } else { - $retval = NULL; - } - } else { - $retval = NULL; + + /** + * Array of suffixes, for self::siteFromDB() + */ + public $suffixes = array(); + + /** + * Array of wikis, should be the same as $wgLocalDatabases + */ + public $wikis = array(); + + /** + * The whole array of settings + */ + public $settings = array(); + + /** + * Array of domains that are local and can be handled by the same server + */ + public $localVHosts = array(); + + /** + * A callback function that returns an array with the following keys (all + * optional): + * - suffix: site's suffix + * - lang: site's lang + * - tags: array of wiki tags + * - params: array of parameters to be replaced + * The function will receive the SiteConfiguration instance in the first + * argument and the wiki in the second one. + * if suffix and lang are passed they will be used for the return value of + * self::siteFromDB() and self::$suffixes will be ignored + */ + public $siteParamsCallback = null; + + /** + * Retrieves a configuration setting for a given wiki. + * @param $settingName String ID of the setting name to retrieve + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + * @return Mixed the value of the setting requested. + */ + public function get( $settingName, $wiki, $suffix = null, $params = array(), $wikiTags = array() ) { + $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags ); + return $this->getSetting( $settingName, $wiki, $params ); + } + + /** + * Really retrieves a configuration setting for a given wiki. + * + * @param $settingName String ID of the setting name to retrieve. + * @param $wiki String Wiki ID of the wiki in question. + * @param $params Array: array of parameters. + * @return Mixed the value of the setting requested. + */ + protected function getSetting( $settingName, $wiki, /*array*/ $params ){ + $retval = null; + if( array_key_exists( $settingName, $this->settings ) ) { + $thisSetting =& $this->settings[$settingName]; + do { + // Do individual wiki settings + if( array_key_exists( $wiki, $thisSetting ) ) { + $retval = $thisSetting[$wiki]; + break; + } elseif( array_key_exists( "+$wiki", $thisSetting ) && is_array( $thisSetting["+$wiki"] ) ) { + $retval = $thisSetting["+$wiki"]; + } + + // Do tag settings + foreach( $params['tags'] as $tag ) { + if( array_key_exists( $tag, $thisSetting ) ) { + if ( isset( $retval ) && is_array( $retval ) && is_array( $thisSetting[$tag] ) ) { + $retval = self::arrayMerge( $retval, $thisSetting[$tag] ); + } else { + $retval = $thisSetting[$tag]; + } + break 2; + } elseif( array_key_exists( "+$tag", $thisSetting ) && is_array($thisSetting["+$tag"]) ) { + if( !isset( $retval ) ) + $retval = array(); + $retval = self::arrayMerge( $retval, $thisSetting["+$tag"] ); + } + } + // Do suffix settings + $suffix = $params['suffix']; + if( !is_null( $suffix ) ) { + if( array_key_exists( $suffix, $thisSetting ) ) { + if ( isset($retval) && is_array($retval) && is_array($thisSetting[$suffix]) ) { + $retval = self::arrayMerge( $retval, $thisSetting[$suffix] ); + } else { + $retval = $thisSetting[$suffix]; + } + break; + } elseif( array_key_exists( "+$suffix", $thisSetting ) && is_array($thisSetting["+$suffix"]) ) { + if (!isset($retval)) + $retval = array(); + $retval = self::arrayMerge( $retval, $thisSetting["+$suffix"] ); + } + } + + // Fall back to default. + if( array_key_exists( 'default', $thisSetting ) ) { + if( is_array( $retval ) && is_array( $thisSetting['default'] ) ) { + $retval = self::arrayMerge( $retval, $thisSetting['default'] ); + } else { + $retval = $thisSetting['default']; + } + break; + } + } while ( false ); } - if ( !is_null( $retval ) && count( $params ) ) { - foreach ( $params as $key => $value ) { - $retval = str_replace( '$' . $key, $value, $retval ); + if( !is_null( $retval ) && count( $params['params'] ) ) { + foreach ( $params['params'] as $key => $value ) { + $retval = $this->doReplace( '$' . $key, $value, $retval ); } } return $retval; } - /** */ - function getAll( $wiki, $suffix, $params ) { + /** + * Type-safe string replace; won't do replacements on non-strings + * private? + */ + function doReplace( $from, $to, $in ) { + if( is_string( $in ) ) { + return str_replace( $from, $to, $in ); + } elseif( is_array( $in ) ) { + foreach( $in as $key => $val ) { + $in[$key] = $this->doReplace( $from, $to, $val ); + } + return $in; + } else { + return $in; + } + } + + /** + * Gets all settings for a wiki + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + * @return Array Array of settings requested. + */ + public function getAll( $wiki, $suffix = null, $params = array(), $wikiTags = array() ) { + $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags ); $localSettings = array(); - foreach ( $this->settings as $varname => $stuff ) { - $value = $this->get( $varname, $wiki, $suffix, $params ); + foreach( $this->settings as $varname => $stuff ) { + $append = false; + $var = $varname; + if ( substr( $varname, 0, 1 ) == '+' ) { + $append = true; + $var = substr( $varname, 1 ); + } + + $value = $this->getSetting( $varname, $wiki, $params ); + if ( $append && is_array( $value ) && is_array( $GLOBALS[$var] ) ) + $value = self::arrayMerge( $value, $GLOBALS[$var] ); if ( !is_null( $value ) ) { - $localSettings[$varname] = $value; + $localSettings[$var] = $value; } } return $localSettings; } - /** */ - function getBool( $setting, $wiki, $suffix ) { - return (bool)($this->get( $setting, $wiki, $suffix )); + /** + * Retrieves a configuration setting for a given wiki, forced to a boolean. + * @param $settingName String ID of the setting name to retrieve + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + * @return bool The value of the setting requested. + */ + public function getBool( $setting, $wiki, $suffix = null, $wikiTags = array() ) { + return (bool)($this->get( $setting, $wiki, $suffix, array(), $wikiTags ) ); } - /** */ + /** Retrieves an array of local databases */ function &getLocalDatabases() { return $this->wikis; } - /** */ + /** A no-op */ function initialise() { } - /** */ - function extractVar( $setting, $wiki, $suffix, &$var, $params ) { - $value = $this->get( $setting, $wiki, $suffix, $params ); + /** + * Retrieves the value of a given setting, and places it in a variable passed by reference. + * @param $settingName String ID of the setting name to retrieve + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $var Reference The variable to insert the value into. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + */ + public function extractVar( $setting, $wiki, $suffix, &$var, $params = array(), $wikiTags = array() ) { + $value = $this->get( $setting, $wiki, $suffix, $params, $wikiTags ); if ( !is_null( $value ) ) { $var = $value; } } - /** */ - function extractGlobal( $setting, $wiki, $suffix, $params ) { - $value = $this->get( $setting, $wiki, $suffix, $params ); + /** + * Retrieves the value of a given setting, and places it in its corresponding global variable. + * @param $settingName String ID of the setting name to retrieve + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + */ + public function extractGlobal( $setting, $wiki, $suffix = null, $params = array(), $wikiTags = array() ) { + $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags ); + $this->extractGlobalSetting( $setting, $wiki, $params ); + } + + public function extractGlobalSetting( $setting, $wiki, $params ) { + $value = $this->getSetting( $setting, $wiki, $params ); if ( !is_null( $value ) ) { - $GLOBALS[$setting] = $value; + if (substr($setting,0,1) == '+' && is_array($value)) { + $setting = substr($setting,1); + if ( is_array($GLOBALS[$setting]) ) { + $GLOBALS[$setting] = self::arrayMerge( $GLOBALS[$setting], $value ); + } else { + $GLOBALS[$setting] = $value; + } + } else { + $GLOBALS[$setting] = $value; + } } } - /** */ - function extractAllGlobals( $wiki, $suffix, $params ) { + /** + * Retrieves the values of all settings, and places them in their corresponding global variables. + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + */ + public function extractAllGlobals( $wiki, $suffix = null, $params = array(), $wikiTags = array() ) { + $params = $this->mergeParams( $wiki, $suffix, $params, $wikiTags ); foreach ( $this->settings as $varName => $setting ) { - $this->extractGlobal( $varName, $wiki, $suffix, $params ); + $this->extractGlobalSetting( $varName, $wiki, $params ); } } /** + * Return specific settings for $wiki + * See the documentation of self::$siteParamsCallback for more in-depth + * documentation about this function + * + * @param $wiki String + * @return array + */ + protected function getWikiParams( $wiki ){ + static $default = array( + 'suffix' => null, + 'lang' => null, + 'tags' => array(), + 'params' => array(), + ); + + if( !is_callable( $this->siteParamsCallback ) ) + return $default; + + $ret = call_user_func_array( $this->siteParamsCallback, array( $this, $wiki ) ); + # Validate the returned value + if( !is_array( $ret ) ) + return $default; + + foreach( $default as $name => $def ){ + if( !isset( $ret[$name] ) || ( is_array( $default[$name] ) && !is_array( $ret[$name] ) ) ) + $ret[$name] = $default[$name]; + } + + return $ret; + } + + /** + * Merge params beetween the ones passed to the function and the ones given + * by self::$siteParamsCallback for backward compatibility + * Values returned by self::getWikiParams() have the priority. + * + * @param $wiki String Wiki ID of the wiki in question. + * @param $suffix String The suffix of the wiki in question. + * @param $params Array List of parameters. $.'key' is replaced by $value in + * all returned data. + * @param $wikiTags Array The tags assigned to the wiki. + * @return array + */ + protected function mergeParams( $wiki, $suffix, /*array*/ $params, /*array*/ $wikiTags ){ + $ret = $this->getWikiParams( $wiki ); + + if( is_null( $ret['suffix'] ) ) + $ret['suffix'] = $suffix; + + $ret['tags'] = array_unique( array_merge( $ret['tags'], $wikiTags ) ); + + $ret['params'] += $params; + + // Automatically fill that ones if needed + if( !isset( $ret['params']['lang'] ) && !is_null( $ret['lang'] ) ) + $ret['params']['lang'] = $ret['lang']; + if( !isset( $ret['params']['site'] ) && !is_null( $ret['suffix'] ) ) + $ret['params']['site'] = $ret['suffix']; + + return $ret; + } + + /** * Work out the site and language name from a database name * @param $db */ - function siteFromDB( $db ) { - $site = NULL; - $lang = NULL; + public function siteFromDB( $db ) { + // Allow override + $def = $this->getWikiParams( $db ); + if( !is_null( $def['suffix'] ) && !is_null( $def['lang'] ) ) + return array( $def['suffix'], $def['lang'] ); + + $site = null; + $lang = null; foreach ( $this->suffixes as $suffix ) { - if ( substr( $db, -strlen( $suffix ) ) == $suffix ) { + if ( $suffix === '' ) { + $site = ''; + $lang = $db; + break; + } elseif ( substr( $db, -strlen( $suffix ) ) == $suffix ) { $site = $suffix == 'wiki' ? 'wikipedia' : $suffix; $lang = substr( $db, 0, strlen( $db ) - strlen( $suffix ) ); break; @@ -109,11 +355,37 @@ return array( $site, $lang ); } - /** */ - function isLocalVHost( $vhost ) { + /** + * Returns true if the given vhost is handled locally. + * @param $vhost String + * @return bool + */ + public function isLocalVHost( $vhost ) { return in_array( $vhost, $this->localVHosts ); } -} -} + /** + * Merge multiple arrays together. + * On encountering duplicate keys, merge the two, but ONLY if they're arrays. + * PHP's array_merge_recursive() merges ANY duplicate values into arrays, + * which is not fun + */ + static function arrayMerge( $array1/* ... */ ) { + $out = $array1; + for( $i=1; $i < func_num_args(); $i++ ) { + foreach( func_get_arg( $i ) as $key => $value ) { + if ( isset($out[$key]) && is_array($out[$key]) && is_array($value) ) { + $out[$key] = self::arrayMerge( $out[$key], $value ); + } elseif ( !isset($out[$key]) || !$out[$key] && !is_numeric($key) ) { + // Values that evaluate to true given precedence, for the primary purpose of merging permissions arrays. + $out[$key] = $value; + } elseif ( is_numeric( $key ) ) { + $out[] = $value; + } + } + } + return $out; + } +} +} diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SiteStats.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SiteStats.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/SiteStats.php Thu Aug 9 08:27:50 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/SiteStats.php Fri Sep 26 11:35:11 2008 @@ -7,6 +7,7 @@ static $row, $loaded = false; static $admins, $jobs; static $pageCount = array(); + static $groupMemberCounts = array(); static function recache() { self::load( true ); @@ -27,10 +28,10 @@ $dbr = wfGetDB( DB_SLAVE ); self::$row = $dbr->selectRow( 'site_stats', '*', false, __METHOD__ ); } - + self::$loaded = true; } - + static function loadAndLazyInit() { wfDebug( __METHOD__ . ": reading site_stats from slave\n" ); $row = self::doLoad( wfGetDB( DB_SLAVE ) ); @@ -40,24 +41,24 @@ wfDebug( __METHOD__ . ": site_stats damaged or missing on slave\n" ); $row = self::doLoad( wfGetDB( DB_MASTER ) ); } - + if( !self::isSane( $row ) ) { // Normally the site_stats table is initialized at install time. // Some manual construction scenarios may leave the table empty or // broken, however, for instance when importing from a dump into a // clean schema with mwdumper. wfDebug( __METHOD__ . ": initializing damaged or missing site_stats\n" ); - + global $IP; require_once "$IP/maintenance/initStats.inc"; - + ob_start(); wfInitStats(); ob_end_clean(); - + $row = self::doLoad( wfGetDB( DB_MASTER ) ); } - + if( !self::isSane( $row ) ) { wfDebug( __METHOD__ . ": site_stats persistently nonsensical o_O\n" ); } @@ -93,17 +94,43 @@ return self::$row->ss_users; } + static function activeUsers() { + self::load(); + return self::$row->ss_active_users; + } + static function images() { self::load(); return self::$row->ss_images; } + /** + * @deprecated Use self::numberingroup('sysop') instead + */ static function admins() { - if ( !isset( self::$admins ) ) { - $dbr = wfGetDB( DB_SLAVE ); - self::$admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), __METHOD__ ); + wfDeprecated(__METHOD__); + return self::numberingroup('sysop'); + } + + /** + * Find the number of users in a given user group. + * @param string $group Name of group + * @return int + */ + static function numberingroup($group) { + if ( !isset( self::$groupMemberCounts[$group] ) ) { + global $wgMemc; + $key = wfMemcKey( 'SiteStats', 'groupcounts', $group ); + $hit = $wgMemc->get( $key ); + if ( !$hit ) { + $dbr = wfGetDB( DB_SLAVE ); + $hit = $dbr->selectField( 'user_groups', 'COUNT(*)', + array( 'ug_group' => $group ), __METHOD__ ); + $wgMemc->set( $key, $hit, 3600 ); + } + self::$groupMemberCounts[$group] = $hit; } - return self::$admins; + return self::$groupMemberCounts[$group]; } static function jobs() { @@ -117,7 +144,7 @@ } return self::$jobs; } - + static function pagesInNs( $ns ) { wfProfileIn( __METHOD__ ); if( !isset( self::$pageCount[$ns] ) ) { @@ -185,55 +212,35 @@ $fname = 'SiteStatsUpdate::doUpdate'; $dbw = wfGetDB( DB_MASTER ); - # First retrieve the row just to find out which schema we're in - $row = $dbw->selectRow( 'site_stats', '*', false, $fname ); - $updates = ''; $this->appendUpdate( $updates, 'ss_total_views', $this->mViews ); $this->appendUpdate( $updates, 'ss_total_edits', $this->mEdits ); $this->appendUpdate( $updates, 'ss_good_articles', $this->mGood ); + $this->appendUpdate( $updates, 'ss_total_pages', $this->mPages ); + $this->appendUpdate( $updates, 'ss_users', $this->mUsers ); - if ( isset( $row->ss_total_pages ) ) { - # Update schema if required - if ( $row->ss_total_pages == -1 && !$this->mViews ) { - $dbr = wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow') ); - list( $page, $user ) = $dbr->tableNamesN( 'page', 'user' ); - - $sql = "SELECT COUNT(page_namespace) AS total FROM $page"; - $res = $dbr->query( $sql, $fname ); - $pageRow = $dbr->fetchObject( $res ); - $pages = $pageRow->total + $this->mPages; - - $sql = "SELECT COUNT(user_id) AS total FROM $user"; - $res = $dbr->query( $sql, $fname ); - $userRow = $dbr->fetchObject( $res ); - $users = $userRow->total + $this->mUsers; - - if ( $updates ) { - $updates .= ','; - } - $updates .= "ss_total_pages=$pages, ss_users=$users"; - } else { - $this->appendUpdate( $updates, 'ss_total_pages', $this->mPages ); - $this->appendUpdate( $updates, 'ss_users', $this->mUsers ); - } - } if ( $updates ) { $site_stats = $dbw->tableName( 'site_stats' ); $sql = $dbw->limitResultForUpdate("UPDATE $site_stats SET $updates", 1); + + # Need a separate transaction because this a global lock $dbw->begin(); $dbw->query( $sql, $fname ); $dbw->commit(); } - - /* - global $wgDBname, $wgTitle; - if ( $this->mGood && $wgDBname == 'enwiki' ) { - $good = $dbw->selectField( 'site_stats', 'ss_good_articles', '', $fname ); - error_log( $good . ' ' . $wgTitle->getPrefixedDBkey() . "\n", 3, '/home/wikipedia/logs/million.log' ); - } - */ + } + + public static function cacheUpdate( $dbw ) { + $dbr = wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow') ); + # Get non-bot users than did some recent action other than making accounts. + # If account creation is included, the number gets inflated ~20+ fold on enwiki. + $activeUsers = $dbr->selectField( 'recentchanges', 'COUNT( DISTINCT rc_user_text )', + array( 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers' OR rc_log_type IS NULL" ), + __METHOD__ ); + $dbw->update( 'site_stats', + array( 'ss_active_users' => intval($activeUsers) ), + array( 'ss_row_id' => 1 ), __METHOD__, array( 'LIMIT' => 1 ) + ); } } - diff -urN mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Skin.php mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Skin.php --- mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.11.0/includes/Skin.php Thu Aug 23 18:34:12 2007 +++ mediawiki-1.11.0-to-1.14.0.proposal/mediawiki-1.14.0/includes/Skin.php Tue Jan 6 22:37:01 2009 @@ -1,26 +1,26 @@ addLink( array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) ); + } if( false !== $wgFavicon ) { $out->addLink( array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) ); } # OpenSearch description link - $out->addLink( array( - 'rel' => 'search', + $out->addLink( array( + 'rel' => 'search', 'type' => 'application/opensearchdescription+xml', - 'href' => "$wgScriptPath/opensearch_desc.php", - 'title' => "$wgSitename ({$wgLanguageNames[$wgLanguageCode]})", + 'href' => wfScript( 'opensearch_desc' ), + 'title' => wfMsgForContent( 'opensearch-desc' ), )); $this->addMetadataLinks($out); $this->mRevisionId = $out->mRevisionId; - + $this->preloadExistence(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /** @@ -203,8 +226,8 @@ $lb = new LinkBatch( $titles ); $lb->execute(); } - - function addMetadataLinks( &$out ) { + + function addMetadataLinks( OutputPage $out ) { global $wgTitle, $wgEnableDublinCoreRdf, $wgEnableCreativeCommonsRdf; global $wgRightsPage, $wgRightsUrl; @@ -240,13 +263,25 @@ } } - function outputPage( &$out ) { - global $wgDebugComments; + function setMembers(){ + global $wgTitle, $wgUser; + $this->mTitle = $wgTitle; + $this->mUser = $wgUser; + $this->userpage = $wgUser->getUserPage()->getPrefixedText(); + $this->usercss = false; + } + function outputPage( OutputPage $out ) { + global $wgDebugComments; wfProfileIn( __METHOD__ ); + + $this->setMembers(); $this->initPage( $out ); - $out->out( $out->headElement() ); + // See self::afterContentHook() for documentation + $afterContent = $this->afterContentHook(); + + $out->out( $out->headElement( $this ) ); $out->out( "\ngetBodyOptions(); @@ -264,10 +299,12 @@ $out->out( $out->mBodytext . "\n" ); $out->out( $this->afterContent() ); + + $out->out( $afterContent ); $out->out( $this->bottomScripts() ); - $out->out( $out->reportTime() ); + $out->out( wfReportTime() ); $out->out( "\n" ); wfProfileOut( __METHOD__ ); @@ -276,14 +313,14 @@ static function makeVariablesScript( $data ) { global $wgJsMimeType; - $r = "\n"; + $r[] = "/*]]>*/\n"; - return $r; + return implode( "\n\t\t", $r ); } /** @@ -296,27 +333,42 @@ global $wgScript, $wgStylePath, $wgUser; global $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgLang; global $wgTitle, $wgCanonicalNamespaceNames, $wgOut, $wgArticle; - global $wgBreakFrames, $wgRequest; + global $wgBreakFrames, $wgRequest, $wgVariantArticlePath, $wgActionPaths; global $wgUseAjax, $wgAjaxWatch; + global $wgVersion, $wgEnableAPI, $wgEnableWriteAPI; + global $wgRestrictionTypes, $wgLivePreview; + global $wgMWSuggestTemplate, $wgDBname, $wgEnableMWSuggest; $ns = $wgTitle->getNamespace(); $nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText(); + $separatorTransTable = $wgContLang->separatorTransformTable(); + $separatorTransTable = $separatorTransTable ? $separatorTransTable : array(); + $compactSeparatorTransTable = array( + implode( "\t", array_keys( $separatorTransTable ) ), + implode( "\t", $separatorTransTable ), + ); + $digitTransTable = $wgContLang->digitTransformTable(); + $digitTransTable = $digitTransTable ? $digitTransTable : array(); + $compactDigitTransTable = array( + implode( "\t", array_keys( $digitTransTable ) ), + implode( "\t", $digitTransTable ), + ); - $vars = array( + $vars = array( 'skin' => $data['skinname'], 'stylepath' => $wgStylePath, 'wgArticlePath' => $wgArticlePath, 'wgScriptPath' => $wgScriptPath, 'wgScript' => $wgScript, + 'wgVariantArticlePath' => $wgVariantArticlePath, + 'wgActionPaths' => (object)$wgActionPaths, 'wgServer' => $wgServer, 'wgCanonicalNamespace' => $nsname, - 'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBKey() ), + 'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBkey() ), 'wgNamespaceNumber' => $wgTitle->getNamespace(), 'wgPageName' => $wgTitle->getPrefixedDBKey(), 'wgTitle' => $wgTitle->getText(), 'wgAction' => $wgRequest->getText( 'action', 'view' ), - 'wgRestrictionEdit' => $wgTitle->getRestrictions( 'edit' ), - 'wgRestrictionMove' => $wgTitle->getRestrictions( 'move' ), 'wgArticleId' => $wgTitle->getArticleId(), 'wgIsArticle' => $wgOut->isArticle(), 'wgUserName' => $wgUser->isAnon() ? NULL : $wgUser->getName(), @@ -325,9 +377,23 @@ 'wgContentLanguage' => $wgContLang->getCode(), 'wgBreakFrames' => $wgBreakFrames, 'wgCurRevisionId' => isset( $wgArticle ) ? $wgArticle->getLatest() : 0, + 'wgVersion' => $wgVersion, + 'wgEnableAPI' => $wgEnableAPI, + 'wgEnableWriteAPI' => $wgEnableWriteAPI, + 'wgSeparatorTransformTable' => $compactSeparatorTransTable, + 'wgDigitTransformTable' => $compactDigitTransTable, ); + + if( $wgUseAjax && $wgEnableMWSuggest && !$wgUser->getOption( 'disablesuggest', false )){ + $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate(); + $vars['wgDBname'] = $wgDBname; + $vars['wgSearchNamespaces'] = SearchEngine::userNamespaces( $wgUser ); + $vars['wgMWSuggestMessages'] = array( wfMsg('search-mwsuggest-enabled'), wfMsg('search-mwsuggest-disabled')); + } + + foreach( $wgRestrictionTypes as $type ) + $vars['wgRestriction' . ucfirst( $type )] = $wgTitle->getRestrictions( $type ); - global $wgLivePreview; if ( $wgLivePreview && $wgUser->getOption( 'uselivepreview' ) ) { $vars['wgLivepreviewMessageLoading'] = wfMsg( 'livepreview-loading' ); $vars['wgLivepreviewMessageReady'] = wfMsg( 'livepreview-ready' ); @@ -343,32 +409,34 @@ $vars['wgAjaxWatch'] = $msgs; } + wfRunHooks('MakeGlobalVariablesScript', array(&$vars)); + return self::makeVariablesScript( $vars ); } function getHeadScripts( $allowUserJs ) { global $wgStylePath, $wgUser, $wgJsMimeType, $wgStyleVersion; - $r = self::makeGlobalVariablesScript( array( 'skinname' => $this->getSkinName() ) ); + $vars = self::makeGlobalVariablesScript( array( 'skinname' => $this->getSkinName() ) ); - $r .= "\n"; + $r = array( "" ); global $wgUseSiteJs; if ($wgUseSiteJs) { $jsCache = $wgUser->isLoggedIn() ? '&smaxage=0' : ''; - $r .= "\n"; + "\">"; } if( $allowUserJs && $wgUser->isLoggedIn() ) { $userpage = $wgUser->getUserPage(); $userjs = htmlspecialchars( self::makeUrl( $userpage->getPrefixedText().'/'.$this->getSkinName().'.js', 'action=raw&ctype='.$wgJsMimeType)); - $r .= '\n"; + $r[] = '"; } - return $r; + return $vars . "\t\t" . implode ( "\n\t\t", $r ); } /** @@ -395,38 +463,24 @@ $wgRequest->getVal( 'wpEditToken' ) ); } - # get the user/site-specific stylesheet, SkinTemplate loads via RawPage.php (settings are cached that way) - function getUserStylesheet() { - global $wgStylePath, $wgRequest, $wgContLang, $wgSquidMaxage, $wgStyleVersion; - $sheet = $this->getStylesheet(); - $s = "@import \"$wgStylePath/common/shared.css?$wgStyleVersion\";\n"; - $s .= "@import \"$wgStylePath/common/oldshared.css?$wgStyleVersion\";\n"; - $s .= "@import \"$wgStylePath/$sheet?$wgStyleVersion\";\n"; - if($wgContLang->isRTL()) $s .= "@import \"$wgStylePath/common/common_rtl.css?$wgStyleVersion\";\n"; - - $query = "usemsgcache=yes&action=raw&ctype=text/css&smaxage=$wgSquidMaxage"; - $s .= '@import "' . self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI ) . "\";\n" . - '@import "' . self::makeNSUrl( ucfirst( $this->getSkinName() . '.css' ), $query, NS_MEDIAWIKI ) . "\";\n"; - - $s .= $this->doGetUserStyles(); - return $s."\n"; - } - /** - * This returns MediaWiki:Common.js, and derived classes may add other JS. - * Despite its name, it does *not* return any custom user JS from user - * subpages. The returned script is sitewide and publicly cacheable and - * therefore must not include anything that varies according to user, - * interface language, etc. (although it may vary by skin). See - * makeGlobalVariablesScript for things that can vary per page view and are - * not cacheable. + * generated JavaScript action=raw&gen=js + * This returns MediaWiki:Common.js and MediaWiki:[Skinname].js concate- + * nated together. For some bizarre reason, it does *not* return any + * custom user JS from subpages. Huh? * - * @return string Raw JavaScript to be returned + * There's absolutely no reason to have separate Monobook/Common JSes. + * Any JS that cares can just check the skin variable generated at the + * top. For now Monobook.js will be maintained, but it should be consi- + * dered deprecated. + * + * @return string */ - public function getUserJs() { + public function generateUserJs() { + global $wgStylePath; + wfProfileIn( __METHOD__ ); - global $wgStylePath; $s = "/* generated javascript */\n"; $s .= "var skin = '" . Xml::escapeJsString( $this->getSkinName() ) . "';\n"; $s .= "var stylepath = '" . Xml::escapeJsString( $wgStylePath ) . "';"; @@ -435,48 +489,38 @@ if ( !wfEmptyMsg ( 'common.js', $commonJs ) ) { $s .= $commonJs; } + + $s .= "\n\n/* MediaWiki:".ucfirst( $this->getSkinName() ).".js */\n"; + // avoid inclusion of non defined user JavaScript (with custom skins only) + // by checking for default message content + $msgKey = ucfirst( $this->getSkinName() ).'.js'; + $userJS = wfMsgForContent($msgKey); + if ( !wfEmptyMsg( $msgKey, $userJS ) ) { + $s .= $userJS; + } + wfProfileOut( __METHOD__ ); return $s; } /** - * Return html code that include User stylesheets + * generate user stylesheet for action=raw&gen=css */ - function getUserStyles() { - $s = "\n"; + public function generateUserStylesheet() { + wfProfileIn( __METHOD__ ); + $s = "/* generated user stylesheet */\n" . + $this->reallyGenerateUserStylesheet(); + wfProfileOut( __METHOD__ ); return $s; } - + /** - * Some styles that are set by user through the user settings interface. + * Split for easier subclassing in SkinSimple, SkinStandard and SkinCologneBlue */ - function doGetUserStyles() { - global $wgUser, $wgUser, $wgRequest, $wgTitle, $wgAllowUserCss; - - $s = ''; - - if( $wgAllowUserCss && $wgUser->isLoggedIn() ) { # logged in - if($wgTitle->isCssSubpage() && $this->userCanPreview( $wgRequest->getText( 'action' ) ) ) { - $s .= $wgRequest->getText('wpTextbox1'); - } else { - $userpage = $wgUser->getUserPage(); - $s.= '@import "'.self::makeUrl( - $userpage->getPrefixedText().'/'.$this->getSkinName().'.css', - 'action=raw&ctype=text/css').'";'."\n"; - } - } - - return $s . $this->reallyDoGetUserStyles(); - } - - function reallyDoGetUserStyles() { + protected function reallyGenerateUserStylesheet(){ global $wgUser; $s = ''; - if (($undopt = $wgUser->getOption("underline")) != 2) { + if (($undopt = $wgUser->getOption("underline")) < 2) { $underline = $undopt ? 'underline' : 'none'; $s .= "a { text-decoration: $underline; }\n"; } @@ -487,22 +531,19 @@ a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; - text-decoration: inherit; } a.new:after, #quickbar a.new:after { content: "?"; color: #CC2200; - text-decoration: $underline; } a.stub:after, #quickbar a.stub:after { content: "!"; color: #772233; - text-decoration: $underline; } END; } if( $wgUser->getOption( 'justify' ) ) { - $s .= "#article, #bodyContent { text-align: justify; }\n"; + $s .= "#article, #bodyContent, #mw_content { text-align: justify; }\n"; } if( !$wgUser->getOption( 'showtoc' ) ) { $s .= "#toc { display: none; }\n"; @@ -513,6 +554,86 @@ return $s; } + /** + * @private + */ + function setupUserCss( OutputPage $out ) { + global $wgRequest, $wgContLang, $wgUser; + global $wgAllowUserCss, $wgUseSiteCss, $wgSquidMaxage, $wgStylePath; + + wfProfileIn( __METHOD__ ); + + $this->setupSkinUserCss( $out ); + + $siteargs = array( + 'action' => 'raw', + 'maxage' => $wgSquidMaxage, + ); + + // Add any extension CSS + foreach( $out->getExtStyle() as $tag ) { + $out->addStyle( $tag['href'] ); + } + + // If we use the site's dynamic CSS, throw that in, too + // Per-site custom styles + if( $wgUseSiteCss ) { + global $wgHandheldStyle; + $query = wfArrayToCGI( array( + 'usemsgcache' => 'yes', + 'ctype' => 'text/css', + 'smaxage' => $wgSquidMaxage + ) + $siteargs ); + # Site settings must override extension css! (bug 15025) + $out->addStyle( self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI ) ); + $out->addStyle( self::makeNSUrl( 'Print.css', $query, NS_MEDIAWIKI ), 'print' ); + if( $wgHandheldStyle ) { + $out->addStyle( self::makeNSUrl( 'Handheld.css', $query, NS_MEDIAWIKI ), 'handheld' ); + } + $out->addStyle( self::makeNSUrl( $this->getSkinName() . '.css', $query, NS_MEDIAWIKI ) ); + } + + if( $wgUser->isLoggedIn() ) { + // Ensure that logged-in users' generated CSS isn't clobbered + // by anons' publicly cacheable generated CSS. + $siteargs['smaxage'] = '0'; + $siteargs['ts'] = $wgUser->mTouched; + } + // Per-user styles based on preferences + $siteargs['gen'] = 'css'; + if( ( $us = $wgRequest->getVal( 'useskin', '' ) ) !== '' ) { + $siteargs['useskin'] = $us; + } + $out->addStyle( self::makeUrl( '-', wfArrayToCGI( $siteargs ) ) ); + + // Per-user custom style pages + if( $wgAllowUserCss && $wgUser->isLoggedIn() ) { + $action = $wgRequest->getVal('action'); + # If we're previewing the CSS page, use it + if( $this->mTitle->isCssSubpage() && $this->userCanPreview( $action ) ) { + $previewCss = $wgRequest->getText('wpTextbox1'); + // @FIXME: properly escape the cdata! + $this->usercss = "/**/"; + } else { + $out->addStyle( self::makeUrl($this->userpage . '/' . $this->getSkinName() .'.css', + 'action=raw&ctype=text/css' ) ); + } + } + + wfProfileOut( __METHOD__ ); + } + + /** + * Add skin specific stylesheets + * @param $out OutputPage + */ + function setupSkinUserCss( OutputPage $out ) { + $out->addStyle( 'common/shared.css' ); + $out->addStyle( 'common/oldshared.css' ); + $out->addStyle( $this->getStylesheet() ); + $out->addStyle( 'common/common_rtl.css', '', '', 'rtl' ); + } + function getBodyOptions() { global $wgUser, $wgTitle, $wgOut, $wgRequest, $wgContLang; @@ -523,23 +644,33 @@ } else $a = array( 'bgcolor' => '#FFFFFF' ); if($wgOut->isArticle() && $wgUser->getOption('editondblclick') && - $wgTitle->userCan( 'edit' ) ) { + $wgTitle->quickUserCan( 'edit' ) ) { $s = $wgTitle->getFullURL( $this->editUrlOptions() ); - $s = 'document.location = "' .wfEscapeJSString( $s ) .'";'; + $s = 'document.location = "' .Xml::escapeJsString( $s ) .'";'; $a += array ('ondblclick' => $s); } $a['onload'] = $wgOut->getOnloadHandler(); - if( $wgUser->getOption( 'editsectiononrightclick' ) ) { - if( $a['onload'] != '' ) { - $a['onload'] .= ';'; - } - $a['onload'] .= 'setupRightClickEdit()'; - } - $a['class'] = 'ns-'.$wgTitle->getNamespace().' '.($wgContLang->isRTL() ? "rtl" : "ltr"). - ' '.Sanitizer::escapeClass( 'page-'.$wgTitle->getPrefixedText() ); + $a['class'] = + 'mediawiki' . + ' '.( $wgContLang->isRTL() ? "rtl" : "ltr" ). + ' '.$this->getPageClasses( $wgTitle ) . + ' skin-'. Sanitizer::escapeClass( $this->getSkinName( ) ); return $a; } + + function getPageClasses( $title ) { + $numeric = 'ns-'.$title->getNamespace(); + if( $title->getNamespace() == NS_SPECIAL ) { + $type = "ns-special"; + } elseif( $title->isTalkPage() ) { + $type = "ns-talk"; + } else { + $type = "ns-subject"; + } + $name = Sanitizer::escapeClass( 'page-'.$title->getPrefixedText() ); + return "$numeric $type $name"; + } /** * URL to the logo @@ -577,11 +708,11 @@ $s .= "\n
    \n
    \n" . "\n\n"; - $shove = ($qb != 0); - $left = ($qb == 1 || $qb == 3); - if($wgContLang->isRTL()) $left = !$left; + $shove = ( $qb != 0 ); + $left = ( $qb == 1 || $qb == 3 ); + if( $wgContLang->isRTL() ) $left = !$left; - if ( !$shove ) { + if( !$shove ) { $s .= "'; } elseif( $left ) { @@ -620,9 +751,9 @@ } - function getCategoryLinks () { + function getCategoryLinks() { global $wgOut, $wgTitle, $wgUseCategoryBrowser; - global $wgContLang; + global $wgContLang, $wgUser; if( count( $wgOut->mCategoryLinks ) == 0 ) return ''; @@ -634,15 +765,37 @@ $dir = $wgContLang->isRTL() ? 'rtl' : 'ltr'; $embed = ""; $pop = ''; - $t = $embed . implode ( "{$pop} {$sep} {$embed}" , $wgOut->mCategoryLinks ) . $pop; - $msg = wfMsgExt( 'pagecategories', array( 'parsemag', 'escape' ), count( $wgOut->mCategoryLinks ) ); - $s = $this->makeLinkObj( Title::newFromText( wfMsgForContent('pagecategorieslink') ), $msg ) - . ': ' . $t; + $allCats = $wgOut->getCategoryLinks(); + $s = ''; + $colon = wfMsgExt( 'colon-separator', 'escapenoentities' ); + if ( !empty( $allCats['normal'] ) ) { + $t = $embed . implode ( "{$pop} {$sep} {$embed}" , $allCats['normal'] ) . $pop; + + $msg = wfMsgExt( 'pagecategories', array( 'parsemag', 'escapenoentities' ), count( $allCats['normal'] ) ); + $s .= ''; + } + + # Hidden categories + if ( isset( $allCats['hidden'] ) ) { + if ( $wgUser->getBoolOption( 'showhiddencats' ) ) { + $class ='mw-hidden-cats-user-shown'; + } elseif ( $wgTitle->getNamespace() == NS_CATEGORY ) { + $class = 'mw-hidden-cats-ns-shown'; + } else { + $class = 'mw-hidden-cats-hidden'; + } + $s .= "
    " . + wfMsgExt( 'hidden-categories', array( 'parsemag', 'escapenoentities' ), count( $allCats['hidden'] ) ) . + $colon . $embed . implode( "$pop $sep $embed", $allCats['hidden'] ) . $pop . + "
    "; + } # optional 'dmoz-like' category browser. Will be shown under the list # of categories an article belong to - if($wgUseCategoryBrowser) { + if( $wgUseCategoryBrowser ){ $s .= '

    '; # get a big array of the parents tree @@ -665,7 +818,7 @@ * @param &skin Object: skin passed by reference * @return String separated by >, terminate with "\n" */ - function drawCategoryBrowser($tree, &$skin) { + function drawCategoryBrowser( $tree, &$skin ){ $return = ''; foreach ($tree as $element => $parent) { if (empty($parent)) { @@ -676,16 +829,24 @@ $return .= Skin::drawCategoryBrowser($parent, $skin) . ' > '; } # add our current element to the list - $eltitle = Title::NewFromText($element); - $return .= $skin->makeLinkObj( $eltitle, $eltitle->getText() ) ; + $eltitle = Title::newFromText($element); + $return .= $skin->link( $eltitle, $eltitle->getText() ) ; } return $return; } function getCategories() { $catlinks=$this->getCategoryLinks(); - if(!empty($catlinks)) { - return ""; + + $classes = 'catlinks'; + + if( strpos( $catlinks, '
    \n" . $this->logoText() . '