diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/AdminSettings.sample mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/AdminSettings.sample --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/AdminSettings.sample 2005-07-02 22:22:08.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/AdminSettings.sample 2006-04-11 11:01:01.000000000 -0400 @@ -6,7 +6,7 @@ * measure to allow using a separate user account with higher * privileges to do maintenance work. * - * Developers: Do not check AdminSettings.php into CVS! + * Developers: Do not check AdminSettings.php into Subversion * * @package MediaWiki */ diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/COPYING mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/COPYING --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/COPYING 2004-03-01 03:14:12.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/COPYING 2006-04-05 03:43:17.000000000 -0400 @@ -2,7 +2,7 @@ Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -305,7 +305,7 @@ You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Also add information on how to contact you by electronic and paper mail. diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/FAQ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/FAQ --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/FAQ 2004-09-04 02:25:23.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/FAQ 2006-10-02 23:08:41.000000000 -0400 @@ -1 +1,5 @@ -The FAQ is at: http://meta.wikimedia.org/wiki/MediaWiki_FAQ +The original MediaWiki FAQ can be found at +http://meta.wikimedia.org/wiki/MediaWiki_FAQ. + +A newer version is available at +http://www.mediawiki.org/wiki/Help:FAQ. diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/HISTORY mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/HISTORY --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/HISTORY 2005-05-01 23:57:42.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/HISTORY 2007-05-15 14:46:04.000000000 -0400 @@ -1,10 +1,3938 @@ Change notes from older releases. For current info see RELEASE-NOTES. +== MediaWiki 1.10 == + +This is the Spring 2007 branch release of MediaWiki. + +MediaWiki is now using a "continuous integration" development model with +quarterly snapshot releases. The latest development code is always kept +"ready to run", and in fact runs our own sites on Wikipedia. + +Release branches will continue to receive security updates for about a year +from first release, but nonessential bugfixes and feature developments +will be made on the development trunk and appear in the next quarterly release. + +Those wishing to use the latest code instead of a branch release can obtain +it from source control: http://www.mediawiki.org/wiki/Download_from_SVN + +== Configuration changes == + +* A new switch $wgCommandLineDarkBg used by maintenance scripts (parserTests.php). + It lets you specify if your terminal use a dark background, the colorized + output will be made lighter making things easier to read. +* The minimum permissions needed to edit a page in each namespace can now be + customized via the $wgNamespaceProtection array. By default, editing pages in + the MediaWiki namespace requires "editinterface" permission, as before. +* Allow restriction of autoconfirmed permission by edit count. New global setting + $wgAutoConfirmCount (defaulting to zero, naturally). +* Added rate limiter for Special:Emailuser +* Private logs can now be created using $wgLogRestrictions +* (Bug 8590) limited HTML is now always enabled ($wgUserHtml = true). +* Deprecated $wgUseImageResize, thumbnailing will be enabled unconditionally. + +== New features since 1.9 == + +* (bug 6937) Introduce "statistics-footer" message, appended to + Special:Statistics +* (bug 6638) List block flags in block log entries +* (bugs 5051, 5376) Tooltips and accesskeys no longer require JavaScript +* Added SkinTemplateOutputPageBeforeExec hook before SkinTemplate::outputPage() + starts page output + (http://lists.wikimedia.org/pipermail/wikitech-l/2007-January/028554.html) +* Introduce "cascading protection" -- implicit protection on pages transcluded + into a page protected with this option enabled +* (bug 8567) Added hook RawPageViewBeforeOutput just before the text is blown + out in action=raw, so extensions might influence the output. +* (bug 3446) Add user preference to hide page content below diffs, can be + overridden by adding diffonly=1 or diffonly=0 to the URL of the diff page +* Add 'purge' privilege to replace the hardcoded check for login state in + determining whether action=purge can be done via GET. Switching the + permission on for anons can be helpful for benchmarking. +* (bug 7842) Link back to deleted revision list from deleted revision preview +* (bug 8619) Add user-aware "unblock" link to Special:Blockip +* (bug 8522) Provide a "delete" link on Special:Brokenredirects for users with + the appropriate permission +* (bug 8628) Add user-aware block list link to Special:Blockip +* (bug 8621) Log revisions marked as patrolled +* Introduce "BookInformation" hook; see docs/hooks.txt for more details +* Add title prefix search for Special:Undelete +* Remove full-archive list from Special:Undelete +* (bug 8136) Introduce 'ArticleUndelete' hook; see docs/hooks.txt for more info +* (bug 8712) Expose user groups as a JavaScript global +* Introduce 'CustomEditor' hook; see docs/hooks.txt for more information +* New special page, Special:Protectedpages, which shows all protected pages + and their protection status (full protection status is not pulled out due + to performance considerations, so it just shows "full protected" or + "semi protected". +* (bug 4133) Allow page protections to be made with an expiry date, in the same + format as block expiry dates. Existing protections are assumed to be infinite, + as are protections made with the new field left blank. +* (bug 8535) Allow certain vertical alignment attributes to be used as image + keywords +* (bug 6987) Allow perrow, widths, and heights attributes for +* (bug 3678) Allow disabling MediaWiki:Aboutsite in the same way as + MediaWiki:Disclaimers; Also means that if any of the footer links are + disabled in the wiki's default language (by setting to "-"), they'll also + be disabled in other languages too (e.g. if the user specifies uselang=fr). +* Sort log types in Special:Log +* Added a classname ("mw-toolbar-editbutton") and unique IDs to the edit + toolbar buttons +* Hide irrelevant block options in Special:Blockip based on whether an + IP address/range or username is listed. (Dynamic using JS.) +* (bug 9032) Make quickbarSettings localizable through Special:Allmessages +* (bug 7782) Standardisation of file info at image description pages. +* (bug 1035) View contributions / recentchanges for an IP range. +* (bug 8747) When unwatching pages from Special:Watchlist/edit, put the + confirmation messages in a proper list with a CSS class and id. +* (bug 9118) Show relevant log fragments on deletion confirmatio page +* (bug 9009) Add username entry field to Special:Contributions +* (bug 1723) Article size in history +* (bug 9223) Disallow magic tilde sequences in page titles and usernames +* (bug 6997) Link from Special:log/block to unblock form +* (bug 9117) Link from Special:log/delete to undelete form +* Link from Special:log/protect to change protection form +* (bug 1196) Add IPv6 support added to blocks, more consistancy for IPv6 contribs +* (bug 3984) Searching in logs by title% +* Show thumbnail of existing image if image exists already under this filename +* (bug 5546) Watchlist reflects logged actions like move, protection, undelete +* Support protocols other than HTTP in LinkFilter, use $wgUrlProtocols +* (bug 3069) Warning on upload of scaled down images +* Warning on upload of images with uppercase extension if image with lowercase + extension exists +* (bug 4624) Namespace selection for Special:Whatlinkshere +* Introduce PageHistoryBeforeList and PageHistoryLineEnding hooks; see docs/hooks.txt + for more information +* (bug 9397) Introduce "sp-contributions-footer" and "sp-contributions-footer-anon" + messages, shown at the end of Special:Contributions as appropriate for the target +* (bug 8421) Expose current action in JavaScript globals (as 'wgAction') +* (bug 9069) Use galleries in query pages dedicated to images +* (bug 9177) Installer now warns of various conditions affecting session.save_path + which can lead to broken session storage +* (bug 9046) Special page to list pages without language links +* (bug 9508) Special page to list articles with the fewest revisions +* Introduce 'FileUpload' hook; see docs/hooks.txt for more information +* Introduce 'SearchUpdate' hook; see docs/hooks.txt for more information +* Introduce 'mywatchlist' message; used on personal menu to link to watchlist page +* Introduce magic word {{NUMBEROFEDITS}} +* Introduced media handlers for file-type specific operations. +* Improved error reporting for image thumbnailing +* Added sharpening option for ImageMagick thumbnailing +* (bug 9656) Autosummaries will be generated for deletion of pages longer than + 500 characters +* Predefined block reasons added to Special:Blockip +* (bug 9196) Installer now check that zend.ze1_compatibility_mode is off +* (bug 9697) Introduce 'InternalParseBeforeLinks' hook; see docs/hooks.txt for more information +* 'contribsub' message changed to 'contribsub2' with two parameters to permit + better localization. Change is reverse-compatible and can be ignored for + most wikis. +* Adding a 'reason' field to Special:Userrights + +== Bugfixes since 1.9 == + +* (bug 7292) Fix site statistics when moving pages in/out of content namespaces +* (bug 8531) Correct local name of Lingála +* Made the PLURAL: parser function return singular on -1 per default +* Fixed up the AjaxSearch +* Fix SpecialVersion->formatCredits input. Version and Url parameters should be + null to be treated properly with isset. +* Page restrictions moved into a new, dedicated table +* Correct tooltip accesskey hint for Opera on the Macintosh + (uses Shift-Esc-, not Ctrl-). +* (bug 8002) Math should render left-to-right even in right-to-left wikis +* Pass e-mail and real name fields to AuthPlugin::addUser, as additional + optional fields, which may be considered useful at registration time. +* PostgreSQL upgrade scripts fixed and updated +* (bug 8613) Fix error when viewing "Recent Changes" and using Postgres. +* Initialise site_stats table at upgrade time if data was missing +* (bug 7250) Updated Unicode normalization tables to Unicode 5.0 +* Unmaintained Oracle support files have been removed. +* Use browser default for printing size, don't force to 11pt +* (bug 8632) Fix regression in page protection null edit update +* (bug 8407) Disallow indexing of "printable" versions +* (bug 8643) Correctly escape the page-specific CSS class for non-Monobook skins +* (bug 8629) Document $wgFilterCallback +* (bug 1000) Clarify warning about memory_limit in installer +* Suppress PHP warning about set_time_limit in installer when safe mode is on +* (bug 3000) Fall back to SCRIPT_NAME plus QUERY_STRING when REQUEST_URI is + not available, as on IIS with PHP-CGI +* Missing interwiki row for English Wikipedia restored (as "wikipedia:") +* use configured cache servers for mctest.php +* bucket details in mcc.php +* fix input validation and remove debugging code in compressOld +* full ID range for moveToExternal +* fix resolveStubs.php for compatibility with older serialized data +* maximum line length for bar graphs in getLagTimes.php +* recognize specieswiki in rebuildInterwiki.inc +* profile unicode cleanup in Xml +* log slow parses in Article.php +* profile wfMsgReal +* log mkdir failures +* profile AutoLoader +* rebuild empty DjVu metadata containing '' +* security fix for DjVu metadata retrieval +* Undelete page list can use plural marker +* (bug 8638) Fix update from 1.4 and earlier +* (bug 8641) Fix order of updates to ipblocks table +* (bug 8678) Fix detection of self-links for numeric titles in Parser +* (bug 6171) Magically close tags in tables when not using Tidy. +* Sanitizer now correctly escapes lonely '>' occurring before the first wikitag. +* Ignore self closing on closing tags ( '' now gives '') +* (bug 8673) Minor fix for web service API content-type header +* Fix API revision list on PHP 5.2.1; bad reference assignment +* (bug 8688) Handle underscores/spaces in Special:Blockip and Special:Ipblocklist + in a consistent manner +* (bug 8701) Check database lock status when blocking/unblocking users +* ParserOptions and ParserOutput classes are now in their own files +* (bug 8708) Namespace translations for Zealandic language +* Renamed constructor methods to PHP 5 __construct reserved name +* (bug 8715) Warn users when editing an interface message whether or not the + message page exists +* ar: fix the 'create a new page' on search page when no exact match found +* (bug 8703) Corrected talk and image namespace name for Limburgish (li) +* (bug 8671) Expose "wpDestFile" as a parameter to "uploadtext" +* (bug 8403) Respect bad image list exceptions in galleries on wiki pages +* Allow sending per-user contribution requests to "contributions" query group +* (bug 3717) Update user count for AuthPlugin account autocreation +* (bug 8719) Firefox release notes lie! Fix tooltips for Firefox 2 on x11; + accesskeys default settings appear to be same as Windows. +* Added an option to make Linker::userToolLinks() show the contribs link + red when the user has no edits. Linker::userToolLinksRedContribs() is an + alias to that which should be used to make it more self documentating. +* (bug 8749) Bring MySQL 5 table defs back into sync +* (bug 8751) Set session cookies to HTTPS-only to match other cookies +* (bug 8652) Catch exceptions generated by malformed XML in multipage media +* (bug 8782) Help text in Makefile +* (bug 8777) Suppress 'previous' link on Special:Allpages when at first page +* (bug 8774) Fix path for GNU FDL rights icon on new installs +* Fix multipage selector drop-down for DjVu images to work when title + is passed as a query string parameter; we have to pass the title as + a form parameter or it gets dropped from the form submission URL +* (bug 8819) Fix full path disclosure in with skins dependencies +* Fixed bug affecting HTML formatting in sortable table column titles +* Merged table sorting code into wikibits.js +* (bug 8711) Stop floats in previews from spilling into edit area +* (bug 8858) Safer handling when $wgImageLimits is changed. Added a note + in DefaultSettings to make it clear. +* (bug 4268) Fixed data-loss bug in compressOld batch text compression + affecting pages which had null edits (move, protect, etc) as second + edit in a batch group. Isolated and patched by Travis Derouin. +* Fix for paths in 1.4->1.5 special-case updater script +* (bug 8789) AJAX search: IE users can now use the return key +* (bug 6844) Use and tags to emphase the differences +* (bug 6684) Fix improper javascript array iteration +* (bug 4347) use MailAddress object for reply-to +* Add AlphabeticPager abstract class +* Use faster AlphabeticPager for Special:Categories +* (bug 8875) Show printable link in MonoBook sidebar for locally nonexistent + pages; perhaps useful for categories and shared images +* Clean up session checks to better handle the case where the session was + opened during the current request. May help with some caching corner + cases. +* (bug 8897) Fix whitespace removal for interlanguage links with link prefix +* Add 'ParserTestTables' hook to expand the list of temporary tables copied + by the parser test harness; use for extensions which require the presence + of other tables while they work. +* Message names changed for AlphabeticPager introduced with r19758 + for better localisations. +* (bug 8944) The deprecated is_a() function is used in StubObjects.php +* (bug 8992) Fix a remaining raw use of REQUEST_URI in history +* (bug 8999) User.php gives "undefined user editcount" PHP notice. +* (bug 8984) Fix a database error in Special:Recentchangeslinked + when using the Postgres database. +* Moved the main ob_start() from the default LocalSettings.php to WebStart.php. + The ob_start() section should preferably be removed from older + LocalSettings.php files. +* Give Content-Length header for HTTP/1.0 clients. +* Partial support for Flash cross-domain-policy filtering. +* Lazy-initialize site_stats row on load when empty. Somewhat kinder to + dump-based installations, avoiding PHP warnings when NUMBEROFARTICLES + and such are used. +* Add 'charset' to Content-Type headers on various HTTP error responses + to forestall additional UTF-7-autodetect XSS issues. PHP sends only + 'text/html' by default when the script didn't specify more details, + which some inconsiderate browsers consider a license to autodetect + the deadly, hard-to-escape UTF-7. + This fixes an issue with the Ajax interface error message on MSIE when + $wgUseAjax is enabled (not default configuration); this UTF-7 variant + on a previously fixed attack vector was discovered by Moshe BA from BugSec: + http://www.bugsec.com/articles.php?Security=24 +* Trackback responses now specify XML content type +* (bug 9044) Send a comment with action=raw pages in CSS/JS output mode + to work around IE/Mac bug where empty pages time out verrrrryyyyy slowly, + particularly with new keepalive-friendly HTTP on Wikipedia +* (bug 8919) Suppress paging links and related messages where there are no + rows to list for query pages +* (bug 9057) Standardize MediaWiki: namespace for oc +* (bug 8132) Suppress "Pages in this category" heading in categories when + there are none +* (bug 8958) Handle search operators better when using tsearch2 (Postgres) +* (bug 8799) Use redirect table for Special:BrokenRedirects and + Special:DoubleRedirects +* (bug 8918) Enable PLURAL option for MediaWiki:showingresults and + MediaWiki:showingresultsnum +* (bug 9122) Fix minor display issue in RTL with section edit link margin +* (bug 5805) Enable PLURAL option for some messages of watchlist and statistic +* (bug 3953) Work around poor display of parenthesis in the in other + languages section of MonoBook skin +* (bug 8539) Enable PLURAL option for another message of recentchanges. +* (bug 8728) MediaWiki:Badfiletype splitted into 3 messages +* (bug 9131) Allow SpecialContributions to work with Postgres +* (bug 9155) Allow footer info to wrap in Monobook +* (bug 8847) Strip spurious #fragments from request URI to fix redirect + loops on some server configurations +* (bug 9097) column "pr_pagetype" does not exist +* (bug 9217) Balance wfProfile calls in Skin::outputPage +* (bug 9222) PostgreSQL updater should not be version-specific +* Fix fallback implementation of mb_strlen so it works and isn't insanely + slow for large strings, since it's used for page edit lengths +* (bug 8815) Setting password in initUser() breaks LdapAuthentication plugin +* (bug 9256) Add a quick note to index.php header comments +* Make Special:Listusers caseinsensitive for first letter +* Default tidy.conf has been moved from extensions module into includes. +* Ignore lonely ''''' +* (bug 9244) When calling edit page for nonexistent section, generate error + inside of just discarding edits, since edit links sometimes go to the wrong + place. +* (bug 9019) No warning during upload if image description page exists, but no + image +* (bug 8582) Allow thumbnailing when imagesize has a space. +* (bug 8716) Change math_inputhash and math_outputhash to bytea for Postgres +* (bug 9343) Correct internal name for Wolof language +* (bug 9363) Fix Postgres error on Recentchangeslinked +* (bug 5142) Fixed call of hook ArticleViewHeader +* (bug 4777) Separate prev/next messages for Special:Whatlinkshere +* Merge approx 15 missing Wikipedia language codes into wikipedia-interwiki.sql + based on Jeff Merkey's mediawiki-1.9.3.WG-20070316.tar.gz.bz2 archive. +* (bug 9411) Fix for shared image descriptions using query-string titles +* (bug 4756) Add user tool links for self created accounts at special:log + instead of sometimes broken block links from newuserlog extension +* (bug 5817) Special:Recentchangeslinked now shows red link for nonexistent + target page instead of silently redirecting +* (bug 8914) Don't transform colons in {{anchorencode:}} +* (bug 9241) Handle edit section links and include size links for cached + templates the same as the first transclusion. +* (bug 9466) "Rollback failed" page doesn't format edit comment +* (bug 9472) Invalid XHTML on cached special pages +* (bug 9472) Invalid XHTML on Special:Newpages +* (bug 4764) "My contributions" not bold when viewing own contributions +* (bug 9194) Add {{PLURAL:...}} to navigation bar of Special:Whatlinkshere +* (bug 9033) Use a more specific error message when users are not able/allowed + to edit page protection levels due to a block, database lock or permissions +* Fixed $wgFeedLimit +* (bug 9270) Corrected help namespace name for Dutch Lower Saxon (nds-nl) +* (bug 929, 4215) Expose "rcdays" user preference in Special:Preferences +* (bug 9554) Extension-provided group name messages not used +* (bug 9565) Translate template namespace name for Hindi (hi) +* (bug 8599) Correct localized names of zh-variants +* (bug 3366) Require skins based on SkinTemplate to override the skinname + property. +* (bug 9220) Removed obsoletes functions in install-utils.inc. +* Removed obsoletes Title::getRelatedCache and Title:touchArray +* (bug 7285) Check MySQL username length during install +* (bug 6910) Correct date/time formats in Vietnamese (vi) +* (bug 9608) Correctly use ORDER BY in dumpLinks.php +* (bug 9609) Correctly use ORDER BY in SpecialWhatlinkshere.php +* Special:Random and Special:Randomredirect now try harder to send the user to + a random page, and will give an error message if none really can be found + instead of sending the user to the main page like they used to +* Fix object variable used for displaying "not-patrolled" CSS class on list +* Fixed interaction of page parameter to ImagePage with the HTML file cache +* Fixed MIME type for SVG files, will be silently changed from image/svg + to image/svg+xml after loading from the database. +* Workaround for djvutoxml bug #1704049 (poor performance). Use djvudump + instead. +* Fixed odd behaviour in ImagePage on DjVu thumbnailing errors +* (bug 5439) "Go" title search will now jump to shared/foreign Image: and + MediaWiki: pages that have not been locally edited. +* (bug 9630) Limits links in Whatlinkshere forgot about namespace filter +* Fixed upgrade for the non-standard MySQL schemas +* Disable MySQL's strict mode at session start for MySQL 4.1+, to avoid the + various problems that occur when it is on. +* (bug 9585) Fix regression in tidy usage in Special:Undelete previews +* (bug 3826) Normalize some invalid cookie name characters when setting + up $wgCookiePrefix. Completes application of patch by Anders Kaseorg. +* (bug 9649) Fix RTL form alignment for Special:Movepage +* (bug 9582) Members of bot group now mark edits patrolled by default +* (bug 9669) Fix limit ordering for rebuildrecentchanges; broken since + converted from 1.4 to 1.5 schema +* (bug 9682) Revert PHP 5.1 dependency on warning suppression for SVN info +* (bug 5959) Anchors dropped from stub links +* (bug 3348) Some additional weak password checks: password which is same + as username will now be rejected. +* (bug 8602) Converted Special:Contributions to use an IndexPager. The + interpretation of the offset parameter has changed, and the go parameter + has been removed. +* (bug 6204) Fixes for indentation with $wgMaxTocLevel: + - don't emit too many list close tags after an invisible header + - don't emit too many final list close tags if last header is invisible + - don't emit TOC when there are no visible headers +* (bug 7629) Fix $wgBrowserBlackList to avoid false positive on MSIE + when certain plugins are present which alter the user agent + + +== Maintenance == + +* New script maintenance/language/checkExtensioni18n.php used to check i18n + progress in the extension repository. +* Running maintenance/parserTests.php with '--record' option, will now + automatically attempt to create the required tables +* --purge option to do additional parser-cache purging for purgeList.php +* Fix hardcoded background color in parserTests.php +* parserTests.php : removed the 'light' option for --color argument, replacing + it with a new global switch : $wgCommandLineDarkBg +* (bug 8780) Clarify message for command-line scripts if LocalSettings.php + exists but is not readable +* dumpBackup / importDump now work with PostgreSQL +* (bug 8975) Use "Maintenance script" as the default username for importImages.php + and importTextFile.php scripts +* (bug 8933) Fix maintenance/reassignEdits.php script +* (bug 9440) Added "mediawikiwiki" interwiki prefix to MediaWiki.org +* (bug 2979) Import now gracefully skips invalid titles with a warning +* Restore '--norc' option for maintenance/importTextFile.php +* Help information for maintenance/importTextFile.php now easier to read on + consoles +* Doxygen documentation now show the revision number of each file, generate + graphs using dot and include a search engine. + + +== Languages updated == + +* Arabic (ar) +* Aramaic (arc) +* Aymara (ay) +* Belarusian normative (be) +* Belarusian alternative (be-x-old) +* Bulgarian (bg) +* Bihara (bh) +* Breton (br) +* Catalan (ca) +* Czech (cs) +* Danish (da) +* German (de) +* Greek (el) +* Esperanto (eo) +* Spanish (es) +* Estonian (et) +* Basque (eu) +* Finnish (fi) +* Võro (fiu-vro) +* French (fr) +* Hebrew (he) +* Hindi (hi) +* Upper Sorbian (hsb) +* Hungarian (hu) +* Armenian (hy) +* Indonesian (id) +* Italian (it) +* Japanese (ja) +* Javanese (jv) +* Georgian (ka) +* Kabyle (kab) +* Kazakh (kk) +* Korean (ko) +* Kashmiri (ks) +* Ripuarian (ksh) +* Latin (la) +* Luganda (lg) +* Limburgish (li) +* Lithuanian (lt) +* Latvian (lv) +* Marathi (mr) +* Low Saxon (nds) +* Dutch Lower Saxon (nds-nl) +* Nepali (ne) +* Nepal Bhasa (new) +* Dutch (nl) +* Occitan (oc) +* Pali (pi) +* Polish (pl) +* Romanian (ro) +* Russian (ru) +* Sanskrit (sa) +* Sicilian (scn) +* Slovak (sk) +* Sundanese (su) +* Swedish (sv) +* Tahitian (ty) +* Ukrainian (uk) +* Urdu (ur) +* Uzbek (uz) +* Vietnamese (vi) +* Zealandic (zea) +* Old Chinese / Late Middle Chinese (zh-classical) +* Chinese (PRC) (zh-cn) +* Chinese (Taiwan) (zh-tw) +* Cantonese (zh-yue) + +== Compatibility == + +MediaWiki 1.10 requires PHP 5 (5.1 recommended). PHP 4 is no longer supported. + +PHP 5.0.x fails on 64-bit systems due to serious bugs with array processing: +http://bugs.php.net/bug.php?id=34879 +Upgrade affected systems to PHP 5.1 or higher. + +MySQL 3.23.x is no longer supported; some older hosts may need to upgrade. +At this time we still recommend 4.0, but 4.1/5.0 will work fine in most cases. + + +== Upgrading == + +1.10 has several database changes since 1.9, and will not work without schema +updates. + +If upgrading from before 1.7, you may want to run refreshLinks.php to ensure +new database fields are filled with data. + +If you are upgrading from MediaWiki 1.4.x or earlier, some major database +changes are made, and there is a slightly higher chance that things could +break. Don't forget to always back up your database before upgrading! + +See the file UPGRADE for more detailed upgrade instructions. + = MediaWiki release notes = Security reminder: MediaWiki does not require PHP's register_globals setting since version 1.2.0. If you have it on, turn it *off* if you can. + +== Changes since 1.8 == + +* (bug 8200) Make category lists sorted by name when using Postgres. +* (bug 7841) Support 'IGNORE' inserts for Postgres, fixes watchlist + adding problem. +* (bug 6835) Removing the includes/Parser.php::getTemplateArgs() function, + because it seems to be unused. +* (bug 7139) Increasing the visual width of the edit summary field on larger + screen sizes, for the default monobook skin. +* Fix PHP notice and estimates for dumpBackup.php and friends +* Improved register_globals paranoia checks +* (bug 7545) Fix PHP version check on install +* Disable PHP exception backtrace printing unless $wgShowExceptionDetails + is set. Backtraces may contain sensitive information in function call + parameters. +* (bug 6164) Avoid smashing Cite state if message transformation triggers + during bad image list check, by skipping message transformation. + This isn't a good permanent fix. +* (bug 6918) Stopped borders and backgrounds from showing through floated + tables in Monobook +* (bug 6868) Un-hardcode section edit link style +* (bug 3205) Stop right floats from stacking horizontally in non-Monobook skins +* Added global $wgStyleVersion to centralize bumping CSS and JS file versions + for cache-friendly style and script updating +* (bug 7562) Fix non-ASCII namespaces on Windows/XAMPP servers +* Friendlier check for PHP 5 in command-line scripts; it's common for parallel + PHP 4 and 5 installations to interfere on the command-line. +* Fix regression in autoconfirm permission check +* (bug 3015) Add CSS ids to subcategory and page sections on category pages +* (bug 7587) Fix erroneous id for specialpage tab, enabling informative popup +* (bug 7599) Fix thumbnail purging, PHP notices on HTCP image page purge +* (bug 7581) Update language name for cbk-zam +* (bug 7444) Update namespace translations for Telugu (te), kept old values as + alias for compatibility +* (bug 4525) Move section links down visually to same level as headings + (editsection links are now inside the heading elements) +* Workaround for http://bugs.php.net/bug.php?id=31892 , PATH_INFO and hence + URLs of the style /index.php/Main_Page were broken on some CGI installations. +* (bug 7623) Validate custom HTML id's correctly in Monobook interface +* (bug 2241) Fix collision of 'w' and 'd' accesskeys +* (bug 5795) CSS class added to body based on page name for page-specific + styling +* (bug 6276) Stopped search field from getting too large in Cologne Blue +* (bug 7644) User creations that are aborted by hooks shouldn't be counted + against account creations per day limit +* (bug 7636) Show Firefox 2 users correct accesskey prefix +* (bug 6427) Block blocked IPs from using the mail password function + to allow blocking of flooders +* Include common.css from classic-style skins in main HTML with the bump URL +* (bug 7607) Add Karakalpak (kaa) to Names.php and stub message file for linktrail +* (bug 7582) Add 'tog-nolangconversion' to MessagesEn.php. + This key is need for languages with variants (zh, sr, kk) +* (bug 7606) MediaWiki messages for "rss" and "atom" missing +* (bug 7609) Add some more '*-summary' messages to MessagesEn.php with empty + strings to allow better localisation via Special:Allmessages. Mark this new + messages as optional for localisation. +* Fix user_newpass upgrade for prefixed tables (reported by Fyren) +* (bug 7663) Include language variant switcher links on Nostalgia skin +* (bug 6531) Fix PHP fatal error on installation page with bad username input. +* (bug 6977) Remove 404 link for autogenerated database documentation. +* (bug 7369) Allow "Show Changes" without requiring edit token. +* (bug 7687) Fix movetalk box checks itself when confirming a delete and move. +* (bug 7684) Obey watchcreated preference for Special:Upload watch checkbox +* (bug 7686) Include id attribute on delete form confirmation button +* Allow compound interwiki prefixes in $wgImportSources +* (bug 7304) Added redirect table to store redirect targets. +* Added querycachetwo table (similar to querycache but has two titles) +* PageArchive can now return a Revision object for more convenient processing + of deleted revision data +* Added 'UndeleteShowRevision' hook in Special:Undelete +* Error message on attempt to view invalid or missing deleted revisions +* Remove unsightly "_" from namespace in Special:Allpages, Special:Prefixindex +* (bug 3224) Allow minor edits by bots to skip new message notification on + user talk pages. This can be disabled by adjusting the 'nominornewtalk' + permission. Patch by Werdna. +* (bug 7741) MATH: fixed broken syntax of underbrace etc. Fixed arrays +* Fix purging for updated SVG files +* (bug 7745) Add id attribute to search button in Monobook +* (bug 7749) MATH: added some more LaTeX symbols, e.g. parallel, diamond, ast, ... +* (bug 7304) Added code in Article.php to keep redirect table up to date. +* Made special page names case-insensitive and localisable. Care has been taken + to maintain backwards compatibility. +* Used special page subpages in a few more places, instead of query parameters. +* (bug 7758) Added wrapper span to "templates used" explanation to allow CSS + styling (class="mw-templatesUsedExplanation"). +* Added {{#special:}} parser function, to give the local default title for + special pages +* (bug 7766) Remove redundant / from AJAX requests, can break some servers +* Add tab links from extensions to classic-based skins (SkinTemplateTab hook) + Provides better cross-skin compatibility for extensions using the modern + skin hooks, such as Oversight +* Moved variant language links on Cologne Blue and Nostalgia to before the + login/logout link +* Fix for parser tests with MySQL 5 in strict mode +* Added block option "enable autoblocks" +* Amend Special:Ipblocklist to note when a block has autoblock DISABLED. +* (bug 7780) Fix regression in editing redirects +* Add whitespace above "templates included on this page" using CSS, not + hardcoded line break. +* Remove entries from redirect table on article deletion +* (bug 7788) Force section headers in new section links for users who have + 'prompt for blank edit summaries' on. +* (bug 1133) Special:Emailuser: add an option to send yourself a copy of your mail. +* (bug 461) Allow "Categories:" link at bottom of pages to be customized via + pagecategorieslink message. +* Sort the list of skins in "My Preferences" -> Skins by alphabetical order. +* (bug 7785) Postgres compatibility for timestamps in RC feeds +* (bug 7550) Normalize user parameter normally on Special:Log +* (bug 7294) Fix PATH search for diff3 on install +* Various fixes related to the blocking change re: autoblocks. On inserting + an IP block, the ipb_enable_autoblock field is now automagically blanked, + because it doesn't make any sense for an IP. Additionally, IP blocks + without the ipb_enable_autoblock option no longer show up as "autoblock + disabled" on Special:Ipblocklist. +* (bug 7774) MATH: aded more amstex functions +* (bug 1182) MATH: fixed inconsistent rendering of upper case Greek letters in TeX +* Fix regression in streaming page dump generation +* (bug 7801) Add support for parser function hooks in parser tests +* checkUsernames.php now uses wfDebugLog instead of hardcoded path to log +* (bug 7810) Update talk namespaces for Occitan +* Allow case-sensitive URLs to be used for uploading from URLs. +* (bug 1109) Correct fix for compressed 304 responses when additional output + buffers have been installed within the compression handler +* (bug 7819) Move automatic redirect edit summary after pre-save transform + to work properly with subst: fun +* (bug 7826) Fix typos in two English messages. +* (bug 5365) Stop users being prompted to enter an edit summary for null edits, + if they have selected that option in preferences. +* (bug 5936) Show an 'm' to the left of the edit summary on diff pages for minor edits. +* (bug 7820) Improve error reporting for uploads via URL. +* (bug 5149) When autoblocks are enabled, retroactively apply an autoblock to the most + recently used IP of a user when they are blocked. +* Add an index on (rc_user_text,rc_timestamp) on the recentchanges table. This will + make CheckUser.php and the new retroactive autoblock functionality faster. +* Fix regression in Special:Undelete for revisions deleted under MediaWiki 1.4 + with compression or legacy encoding +* (bug 6737) Fixes for MySQL 5 schema in strict mode +* Approximate height for client-side scaling fallback instead of passing -1 + into the HTML output. +* Make the DNSBL to check for proxy blocking configurable via $wgSorbsUrl +* Add experimental recording/reporting mode to parser tests runner, to + compare changes against the previous run. + Additional tables 'testrun' and 'testitem' are in maintenance/testRunner.sql, + source this and pass --record option to parserTests.php +* Make the set of default parser test input files extensible via + $wgParserTestFiles. This can now be appended to by extensions or local + configuration files so that extension or custom tests can be automatically + run along with the main batch. +* Run PHP install version checks on update.php so command-line updaters see + new version requirements +* Do a check for the PHP 5.0.x 64-bit bug, since this is much more disruptive + as of MW 1.8 than it used to be. Install or upgrade now aborts with a + warning and a request to upgrade. +* (bug 6440) Updated indexes to improve backlinking queries (links, templates, images) +* Switched 'anon-only' block mode to default for IP blocks +* (bug 3687, 7892) Add distinct heading for media files in category display, + with count. +* (bug 1578) Add different icons for external links to audio, video, or PDF in + Monobook. +* Made autoblocks block account creation if the user block has that option enabled. +* Add auto-summaries to blankings and large removals without summaries. +* (bug 7811) Allow preview of edit summaries. +* (bug 6839) Wikibits.js minor changes to make JS-lint happier. +* (bug 7932) Make sure that edit toolbar clears floats so it appears correctly. +* (bug 6873) When viewing old revisions, add link to diff to current version. +* (bug 3315) Provide rollback link directly on history page. +* Replace 'old-revision-navigation' message with 'revision-info' and + 'revision-nav' messages, wrapped in divs with appropriate id's. +* (bug 4178) MediaWiki:Common.js will now be included for all users if + $wgUseSiteJs is enabled, in addition to (if applicable) MediaWiki:Monobook.js + and user JS subpages. +* (bug 7918) "Templates used on this page" changes during preview to reflect + any added or removed templates, and works as expected for section edits. +* (bug 7919) "Templates used on this page" is now shown for read-only pages. +* (bug 7688) When viewing diff, section anchors in autosummary jump to section + on current page instead of loading the latest version. +* (bug 7970) Use current connection explicitly on Database::getServerVersion +* (bug 2001) Tables with class="sortable" can now be dynamically sorted via + JavaScript. +* Added autosummary for new pages with 500 or less characters, and refactor + the autosummary code so it's all done in one function. doEdit is getting too + big! +* (bug 7554) The correct MIME type for SVG images is now displayed on the + image page (image/svg+xml, not image/svg). +* (bug 7883) Added autoblock whitelisting feature, using which specific ranges + can be protected from autoblocking. These ranges are specified, in list format, + in the autoblock_whitelist system message. +* Added placeholders for text injection by hooks to EditPage.php +* (bug 8009) Automatic edit summary for redirects is not filled for edits in existing pages +* Installer support for experimental MySQL 4.1/5.0 binary-safe schema +* Use INSERT IGNORE for db-based BagOStuff add/insert, for more memcache-like + behavior when keys already exist on add (instead of dying with an error...) +* Add a hook 'UploadForm:initial' before the upload form is generated, and two + member variable for text injection into the form, which can be filled by the hooks. +* (bug 6295) Add a "revision patching" functionality, where an edit can be undone + (with a functionality similar to diff rev1 rev2 | patch -R rev3 -o rev3). + This is triggered by including &undo=revid in an edit URL. A link to a URL + that will undo a given edit is shown on NEW revision headers on diff pages. + The link leads to a "Show Changes" page showing what will be done to undo the + edit. +* Fix display of link in "already rolled back" message for image/category pages +* (bug 6016) Left-aligned images should stack vertically, like right-aligned + images, not horizontally. +* Patch from LeonWP: added UploadForm:BeforeProcessing hook in SpecialUpload.php +* Add AuthPluginSetup hook to override $wgAuth after configuration +* Fix regression in authentication hook auto-creation on login +* (bug 8110) Allow spaces in ISBNs +* (bug 8024) Introduce "send me copies of emails I send to others" preference +* Added 'EditPage::attemptSave' hook before an article is saved. +* (bug 8083) Applied patch for sk localisation +* Add a backslash character to the edit token, to prevent edits via certain + broken proxies that mangle such characters in form submissions +* (bug 7461) Allow overwriting pages using importTextFile.php +* (bug 7946) importTextFile.php doesn't perform pre-save transform +* (bug 8117) {{REVISIONTIMESTAMP}} showed weird default if $wgLocalTZoffset set; + now uses current time for previews and if timestamp can't be loaded from DB +* {{REVISIONTIMESTAMP}} now uses site local timezone instead of user timezone + to ensure consistent behavior +* {{REVISIONTIMESTAMP}} and friends should now work on non-MySQL backends +* (bug 7671) Observe canonical media namespace prefix in Linker::formatComment +* Added js variable wgCurRevisionId to the output +* (bug 8141) Cleanup of Parser::doTableStuff, patch by AzaTht +* (bug 8042) Make miser mode caching limits settable via $wgQueryCacheLimit + instead of hardcoding to 1000 +* Enable QueryPage classes to override list formatting +* (bug 5485) Show number of intervening revisions in diff view +* (bug 8100) Fix XHTML validity in Taiwanese localization +* Added redirect to section feature. Use it wisely. +* Added a configuration variable allowing the "break out of framesets" feature + to be switched on and off ($wgBreakFrames). Off by default. +* Allow Xml::check() $attribs parameter to override 'value' attribute +* DB schema change: added two columns (rc_old_len and rc_new_len) to the recentchanges table to store + the text lengths before and after the edit +* (bug 1085) Made Special:Recentchanges show the character difference between the changed revisions +* Removed a redundant tag from diff pages that was causing display issues for some users +* (bug 8203) The keyboard shortcut for "log out" was removed, because users were pressing it + when they intended to press the shortcut for "preview". +* (bug 8148) Handle non-removable output buffers gracefully when cleaning + buffers for HTTP 304 responses, StreamFile, and Special:Export. + Duplicated code merged into wfResetOutputBuffers() and wfClearOutputBuffers() +* Special:AllPages : 'next page' link now point to the first title of the next + chunk instead of pointing to the last title of current chunk. +* (bug 4673) Special:AllPages : add a 'previous' link (new message 'prevpage') +* (bug 8121) wfRandom() was not between 0 and 1 +* Add static method Parser::createAssocArgs($args), so parser functions can + use the same code to parse arguments as the templates do. +* Change behavior of logins using the temporary e-mailed password (as stored + in user_newpassword hash field). Instead of just logging in silently and + leaving the previous user_password field in place indefinitely, the user + is now prompted to set a new password. + + The password-changing form is at Special:Resetpass; currently it's only + usable for changing from the temporary password during login, but it + could perhaps be generalized, replacing the subform in preferences. + + Once the new password is set successfully, the temporary password is wiped + so it cannot be used to login a second time, and the login process + is completed. +* Suppress 'mail new password' button on login form if $wgAuth forbids + changing user passwords; it wouldn't work very well... +* Consolidate password length checks and $wgAuth manipulation into + User::setPassword() to avoid duplicate code in different places + that set passwords. +* User::setPassword() now throws PasswordError exceptions if the password + is illegal or cannot be set via $wgAuth. These can be caught and a human- + readable error message displayed by UI code. +* Added Title::isSubpage() +* (bug 8241) Don't consider user pages of User:Foo.css to be CSS subpages +* Set an explicit class on framed thumbnail inner divs and images, changed some + CSS to use these instead of using descendent selectors. +* Accept null parameter to User::setPassword() as indicating the password + field should be cleared to an unusable state. Login will only be possible + after the password is reset, for instance by e-mail. +* (bug 6394) Invalidate the password set for "by e-mail" account creations + to avoid accidental empty password creations. +* Made the show change size function work on page moves, page creations, and + log entries. Also fixed it in the javascript recentchanges. +* (bug 8239) correctly get 50 new contributions when clicking '(50 next)' +* (bug 2259) Fix old regression where e-mail addresses were no longer + confirmed on login with mailed password. +* Add a notification about the confirmation mail sent during account + creation, so people don't immediately go off to request a second one. +* Add a warning on Special:Confirmemail if a code was already sent and has + not yet expired. +* Add user_editcount field to provide data for heuristics on account use. + Incremented on edit, with lazy initialization from past revision data. + Can batch-initialize with maintenance/initEditCount.php (not yet friendly + to replication environments, this will do all accounts in one query). +* Allow raw SQL subsections in Database::update() SET portion as well as + for WHERE portion. Handy for increments and such. +* User::getOption now accept a default value to override default user values + this makes it consistent with WebRequest::get* methods. Corrected code in + various places accordingly. +* (bug 8264) Fix JavaScript global vars for XHTML mode +* Make $wgSiteNotice value wikitext again, for consistency with editable + MediaWiki:Sitenotice and MediaWiki:Anonnotice. +* (bug 8044) When redirecting from the canonical name of the special page + to the localised one, parameters/subpages are omitted +* (bug 8164) Special:Booksources should use GET for form submission +* Rewrite Special:Booksources to clean up interface and remove redundant code +* (bug 7925) Change Special:Allmessages message name filter javascript to be + a bit more responsive and easier on the CPU +* (bug 4488) Support watching pages on deletion; introduces new user preference +* Minor restructuring of Special:Preferences; "watch pages I edit" and "watch + pages I create" options now accessible under "Watchlist" options +* (bug 8153) doesn't work in site notice +* (bug 6690) wfMsgNoTrans() transforms messages +* (bug 8274) Wrap edit tools in a
with a specified class +* Detect PHP 5.0.x 64-bit bug and abort in WebStart.php; too many things break + mysteriously otherwise (detection code copied from install-utils.inc) +* (bug 8295) Change handling of
tags in doBlockLevels() to match that + of
+* (bug 8110) Make magic ISBN linking stricter: only match ten-digit sequences + (plus optional ISBN-13 prefix) with no immediately following alphanumeric + character, disallow multiple consecutive internal redirects +* (bug 2785) Accept optional colon prefix in links when formatting comments +* Don't show "you can view and copy the source of this page" message for + pages which don't exist +* (bug 8310) Blank line added to top of 'post' when page is blank +* (bug 8109) Template parameters ignored in "recentchangestext" +* Gracefully skip redirect-to-fragment on WebKit versions less than 420; + it messes up on current versions of Safari but is ok in the latest + nightlies. Checking the version number will allow it to automatically + work when new releases of Safari appear. +* Fix regression in thumb styles; size and padding didn't match with + new arrangement. +* (bug 8333) Fix quick user data update on login password change on + replication database setups. User data is now pulled from master + instead of slave in User::loadFromDatabase, ensuring that it is + fresh and accurate when read and then saved back into cache. + This was breaking with the Special:Rename operation which + automatically logs the user in with the new password after changing + it; pulling from slave meant the record was often not the updated + one. +* (bug 8335) Set image width to the first valid parameter found. +* (bug 8350) Fix watchlist viewing bug when using Postgres. +* (bug 6603) When warning about invalid file extensions, output the bit + of the extension we actually checked +* (bug 7669) Drop defaults on BLOB/TEXT columns for better compatibility + with MySQL's strict mode, often enabled by the Windows installer. + The defaults are ignored anyway when strict mode is off... +* (bug 7685) Use explicit values for ar_text and ar_flags when deleting, + for better compatibility with MySQL's strict mode +* Update default interwiki values to reflect changed location of ursine: +* (bug 5411) Remove autopatrol preference +* Users who have the "autopatrol" permission will have their edits marked as + patrolled automatically +* Users who do not have the "autopatrol" permission will no longer be able + to mark their own edits as patrolled +* Introduce 'PingLimiter' hook; see docs/hooks.txt for more information +* (bug 532) Tweaked alt text for some interface messages +* (bug 8231) Gave useful alt text to the main on image pages +* (bug 371) Remove alt text for "Enlarge" icon on thumbnails +* Initialize user_editcount to 0 instead of NULL for newly created accounts +* (bug 3696) Strip LRM and RLM characters from titles to work around the + problem some people have where titles cut-and-pasted from lists include + the bidi override characters appended to the lists. + A more thorough blacklist for forbidden and translatable characters would + be wise, though, as might a cleaner method for the lists in the first place. +* Fix regression in email password resets on read-restricted sites +* Set tabindex on fields in deletion form so you don't have to tab through + the links in the sitenotice +* (bug 8271) Show full time and date on viewer for individual deleted + revisions +* (bug 8214) Output file size limit and actual file size in appropriate units + on Special:Upload +* (bug 8016) Purge objectcache table during upgrade processes - use the --nopurge + option to prevent this when running maintenance/update.php +* (bug 7612) Remove superfluous link to Special:Categories from result items + on Special:Mostcategories +* {{PLURAL:}} now handles formatted numbers correctly +* (bug 8331) Added the change size value to watchlists; therefore made + watchlists use RecentChange::newFromRow() instead of newFromCurRow() +* (bug 8351) Fix undo for simple reverts +* (bug 6856) User::clearNotification() does not respect read-only mode +* (bug 6853) Use a checkbox on the installer form to indicate that a superuser + account should be used; this is clearer than the old check which relied on + the password never being an obscure value +* Remove old unused watchlist cache, which was a leftover from the old schema + where watchlists were more expensive to generate +* Minor cosmetic changes to Special:Userrights +* Added wgCanonicalSpecialPageName to JavaScript variables +* Fix image deleting when using Postgres. +* Output both source and destination titles in maintenance/moveBatch.php +* Added basic parser tests for language variants +* Enable selflinks and categories to be written in some of the language variants +* Prevent conversion of JavaScript code in language variants +* Output software version number in maintenance/parserTests.php +* (bug 7169) Use Ajax to watch/unwatch articles if enabled +* Make variant table caching a little more robust, using main language code + in cache key. Probably this is still a bit wonky, though. Was breaking + parser tests when Chinese tables were getting loaded into Serbian code. +* (bug 8380) Be nicer about blank lines in deleteBatch.php +* (bug 8401) Fix regression in SORBS lookup for some DNS setups +* Use raw file descriptor in posix_isatty() check to avoid warning on + Linux systems with at least some versions of PHP +* (bug 5908) Allow overriding the default category sort key for all items on + a page using {{DEFAULTSORT}} +* (bug 6449) Throw a more definitive error message when installation fails + due to an invalid database name +* (bug 5827) Use full text for option link labels on Special:Watchlist +* (bug 8018) Allow hiding minor edits from the watchlist +* (bug 8427) MonoBook RTL IE 7.0 tweaks failed when sidebar's navigation + section is renamed; no longer relies on first section name +* Stabilize client-side table sorting even if the underlying Javascript sort() + implementation is unstable +* Add hook for extensions to add user information to the panel in preferences, + next to the user name and ID. +* (bug 8392) Display protection status of transcluded pages in the edit page + template list. Patch by Fyren, with i18n naming tweak. +* Fix for interwiki transclusion where target wiki uses query string for title +* Resolve namespaces on interwiki Title objects using canonical namespace names + if possible (should not happen, though, outside interwiki transclusion... and + maybe not even then, but it does) +* (bug 8447) Fix SQL typo breaking non-default $wgHitcounterUpdateFreq +* Do not allow previews of deleted images to be cached +* Add global variable $wgDefaultLanguageVariant used to set the default language + variant of a wiki to something different than the main language code +* Add 'variant' option to parserTests - runs test with the given variant as + preferred, utilize it for more parser tests of language variants code +* (bug 6503) Fix bug that stopped certain irrelevant links from being hidden + for printing +* Avoid PHP warning in Creative Commons metadata when a creative commons + license is not actually set up +* (bug 8463) Don't print external link icons for Monobook +* (bug 8461) Support watching pages on move +* (bug 8041) Work around bug with debug_backtrace when Zend Optimizer is + loaded by skipping the function. Use wfDebugBacktrace() wrapper function. +* Reduce config file clutter by setting various script and upload paths + based on $IP or $wgScriptPath in Setup.php. They can still be explicitly + overridden in LocalSettings.php if desired... +* Attempt to detect redirect loops for the canonical title redirect, and + give some hints to the poor confused administrator. +* Introduce new flag 'R' - raw output for language variant escape tags +* Advise users when updates for a query page have been disabled using + $wgDisableQueryPageUpdate +* (bug 8413) Improve comments for $wgNamespaceRobotPolicies +* (bug 8330) Show "bytes" suffix on recent changes diff counter + optionally... if set in rc-changes-size message (default empty for now) +* (bug 8489) Support basic links in caption attribute +* (bug 8485) Correct Lingala number formatting +* The MediaWiki namespace is no longer pre-filled with default messages on + install. All default messages will be removed from the MediaWiki namespace + on upgrade. +* Recentchanges RSS/Atom feeds now use a separate message for the description + to avoid cluttering it with useless wiki formatting +* (bug 8417) Handle EXIF unknown dates +* (bug 8372) Return nothing on empty tags. +* New maintenance script to show the cached statistics : showStats.php. +* Count deleted edits when regenerating total edits in maintenance/initStats.php +* (bug 3706) Allow users to be exempted from IP blocks. The ipblock-exempt permission + key has been added to enable this behaviour, by default assigned to sysops. +* (bug 7948) importDump.php now warn that Recentchanges need to be rebuild. +* (bug 7667) allow XHTML namespaces customization +* (bug 8531) Correct local name of Lingála (patch by Raymond) +* Fix regression with default lock file and cache directories; threw visible + warning with open_basedir + + +== 1.8 Compatibility changes == + +=== Zend Optimizer === + +A bug in some versions of PHP 5 and Zend Optimizer which was triggered under +MediaWiki 1.8.x has been worked around by disabling some internal debugging +features when Zend Optimizer is loaded. This should solve some common +"blank page" problems. + +=== PHP 5.0 64-bit === + +MediaWiki now checks for a condition where PHP 5.0.x corrupts array data +on 64-bit systems and warns you to upgrade PHP to solve the problem. This +bug causes Special: pages to fail on affected systems under MediaWiki 1.8 +and higher, and subtler data corruption on earlier versions. + +The only known workaround is to upgrade PHP to 5.1 or later, which you +probably should do anyway for security reasons! + +=== MySQL 5 === + +MediaWiki should now install and run correctly on MySQL 5.0 and higher when +MySQL's "strict mode" is enabled. (This is now the default for many Windows +installations, though it seems to remain off by default on Unix.) + +This fixes errors about "cannot default default value for BLOB/TEXT fields". + +=== ImageMagick === + +Note that ImageMagick older than 6.x may no longer work for image resizing +due to use of the -thumbnail option. + + +== 1.8 Behavior changes == + +=== Localized special pages === + +The names of Special: pages can now be localized, so links and URLs to them +are more legible in languages that aren't English. + +Not all languages have included localized names yet. + +=== E-mail password === + +Users are now required to set a new password for themselves when they first +log in with a newly generated e-mailed password. + +Requesting passwords frequently is prevented to reduce abusive mailbombing. + +=== Undo revision === + +An "undo" link now appears in diff view for easier reverting of older edits. +When GNU diff3 is available for edit conflict merging, this can make it much +easier to "undo" the changes of an older edit when there are surrounding +changes elsewhere in the page. + +The changes must be manually reviewed and approved, as with conventional +full-revision reverts. + +=== Blocking === + +User blocks can be set to disable the automatic blocking of IP addresses the +account logs in with. + + +== 1.8 Database changes == + +* new 'redirect' table stores data on page redirects +* new 'querycachetwo' table used for some cached special pages +* 'ipblocks' table adds 'ipb_enable_autoblock' +* 'recentchanges' table adds 'rc_old_len', 'rc_new_len' for size tracking +* 'user' table has added 'user_newpass_time' and 'user_editcount' fields +* some indexes have been updated on 'recentchanges' + +== 1.8 Configuration changes == + +Several configuration options have changed since 1.8: + +=== $wgEnableAPI === + +The experimental machine API interface is now enabled by default, read-only. +You can disable it by setting $wgEnableAPI = false; in LocalSettings.php. + +=== $wgPathInfo === + +The use of PATH_INFO (the text after the script name in 'index.php/Blah') +is controlled by the $wgUsePathInfo setting. This is now explicitly disabled +for CGI, apache2filter, and ISAPI configurations of PHP, for more consistency +with the autodetection from the installer. + +In some rarer configurations you may have to switch $wgUsePathInfo from false +to true or, perhaps, from true to false to make things work properly if bad +PATH_INFO data comes through the server. + +The wiki now tries to detect this condition and should show you an error +message describing what to change instead of sending the browser into an +infinite redirect loop. + +=== $wgScript and other path settings === + +The following configuration variables are now automatically set in Setup.php +if they are not overridden in LocalSettings.php: + +from $wgScriptPath: + + $wgScript + | \- $wgArticlePath + + $wgRedirectScript + + $wgStylePath + + $wgUploadPath + \- $wgLogo + + $wgMathPath + +from $IP: + - $wgStyleDirectory + + $wgUploadDirectory + \- $wgMathDirectory + + $wgTmpDirectory + +Newly generated configuration files will by default include only $wgScriptPath +(hardcoded from the installer) and $IP (detected at runtime). + +Old configuration files which specify all these values explicitly should +continue to work just fine, but if you use the defaults you can remove them +to reduce clutter. + +=== $wgGroupPermissions === + +The sysop group now holds the "autopatrol" and "ipblock-exempt" rights by +default. + +"autopatrol" replaces the preference for marking ones own edits patrolled +by default; users holding this permission will automatically have their +edits patrolled, while others cannot mark their own edits as patrolled +even if they have patrolling rights. + +"ipblock-exempt" excludes the user from IP blocks; accounts which are blocked +explicitly by name will still be blocked, however. This is given to sysops +to minimize annoyance from accidental "collateral damage"; remember that a +sysop will be able to lift the block if they desire. + +The bot group now holds the "nominornewtalk" right. A user with this right +will not trigger new message notifications when making minor edits to user +talk pages. This is meant to minimize annoyance from maintenance bot +processes. + +=== $wgUseWatchlistCache === + +Watchlist caching has been removed. The feature was not maintained, and has +been unnecessary since switching to the 'recentchanges' database table +reduced server pressure for Wikipedia's watchlists. + +=== $wgBreakFrames === + +MediaWiki in the past attempted to detect when it was embedded in a frameset +and "break out" of it, assuming it to be hostile. + +This behavior is now disabled by default, but can be reenabled by setting +$wgBreakFrames to true in LocalSettings.php. + + +== 1.8 New settings == + +=== $wgVariantArticlePath === + +For languages with script variant support (Chinese, Serbian, and others), +it's possible to use alternate URL paths to select the variant for article +display, setting $wgVariantArticlePath. + +Documentation for this setting would be useful. + +=== $wgMaxMsgCacheEntrySize === + +The message cache can now skip items larger than a given size; this allows +it to better handle the primary caching case when large CSS and JS blobs are +present. + +=== $wgStyleVersion === + +When making significant changes to skin stylesheets and JavaScript files, +you can append a string to this variable to tweak the generated URLs, +forcing newly rendered pages to bring in a fresh version despite server- +or browser-side caching. + +Normally this will be set in the course of MediaWiki development, but +if doing development on a custom skin you may wish to poke it as well. + +=== $wgRCShowChangedSize === + +Special:Recentchanges and Special:Watchlist now show the number of bytes +added or removed to an article to give an idea of the size of the edit. +This information was previously available only in the IRC update feeds. + +To disable this site-wide, set $wgRCShowChangedSize to false. +(Individual users can suppress the data in custom CSS.) + +Adjust $wgRCChangedSizeThreshold to trigger highlighting of particularly +large changes. + +The formatting of the size figure can be adjusted through the +[[MediaWiki:Rc-change-size]] message. + +=== $wgQueryCacheLimit === + +The number of rows stored for "expensive" special pages in miser mode +can now be adjusted up or down from the default 1000. + +=== $wgDisableQueryPageUpdate === + +Individual "expensive" special pages can be skipped in processing by +updateSpecialPages if added to this list. + +=== $wgSorbsUrl === + +The base hostname for the DNS-based proxy blacklist can now be overridden +when $wgEnableSorbs is set, to use a different blacklist instead of SORBS. +The blacklist would need to respond the same was as SORBS; any positive +response will be taken as a proxy. + +=== $wgAjaxWatch === + +Experimental AJAX mode for the watch/unwatch tabs to execute inline. +Does not include the UI messages describing how to reach the watchlist, +so you may not want it on a general-audience site just yet. + +=== $wgParserTestFiles === + +MediaWiki's parser test suite can now be expanded with additional test +files. Custom extensions can add their test files to this array, and +they will be run along with the main tests by maintenance/parserTests.php + + +== Changes since 1.7 == + +* Introduced AjaxResponse object, superceding AjaxCachePolicy +* Changes to sajax_do_call: optionally accept an element to fill instead of a + callback function; take the target function or element as a third parameter; + pass the full XMLHttpRequest object to the handler function, instead of just + the resultText value; use HTTP response codes to report errors. +* (bug 6562) Removed unmaintained ParserXml.php for now +* History paging overlap bug fixed +* (bug 6586) Regression in "unblocked" subtitle +* Don't put empty-page message into view-source when page text is blank +* (bug 6587) Remove redundant "allnonarticles" message +* Block improvements: Allow blocks on anonymous users only. Optionally allow + or disallow account creation from blocked IP addresses. Prevent duplicate + blocks. Fixed the problem of expiry and unblocking erroneously affecting + multiple blocks. Fixed confusing lack of error message when a blocked user + attempts to create an account. Fixed inefficiency of Special:Ipblocklist in + the presence of large numbers of blocks; added indexes and implemented an + indexed pager. +* (bug 6448) Allow filtering of Special:Newpages according to username +* (bug 6618) Improve permissions/error detection in Special:Lockdb +* Quick hack for extension testing: parser test doesn't create new message + cache object. +* (bug 6299) Maintain parser's revision ID across recursive calls to fix + {{REVISIONID}} when Cite extension is used +* (bug 6622) Removed deprecated function Image::newFromTitle +* (bug 6627) Fix regression in Special:Ipblocklist with table prefix +* Removed forced dereferencements (new() returns a reference in PHP5) +* Note about $wgUploadSizeWarning using byte +* (bug 6592) Add most viewed pages summary to Special:Statistics +* Pre-strip characters ignored in IDNs from URLs so they can't be used + to break the blacklists for regular URLs +* Fix regression in blocking of user accounts +* (bug 6635) Fix regression searching for range blocks on Ipblocklist +* Fix regression searching Ipblocklist with ugly URLs +* (bug 6639) Use a consistent default for upload directories +* Preserve entered reason when reporting unconfirmed lock on Special:Lockdb +* (bug 6642) Don't offer to unlock the database when it isn't locked +* cleanupTitles.php changed from --dry-run option to --fix, so default + behavior is now a non-invasive check as with namespaceDupes.php +* (bug 6660) Fix behaviour of EditPage::blockedPage() when the article does + not exist; now doesn't show the source box if the user hasn't provided it + (blocked mid-edit) and the page doesn't exist +* Improve default value of "blockedtext" +* (bug 6680) Added localisation for Dutch bookstore list (nl) +* Renamed maintainace script redundanttrans.php to unusedMessages.php - clearer usage +* Fix regression which allowed some blocked users to create additional accounts +* (bug 6657) Fix Hungarian linktrail +* (bug 6751) Fix preview of blanked section with edit on first preview option +* (bug 5456) Separate MediaWiki:Search into messages for both noun and verb, + introduced 'MediaWiki:Searchbutton' +* Made lines from initialiseMessages() appear as list items during installation +* Moved the bulk of the localisation data from the Language*.php files to the + Messages*.php files. Deleted most of the Languages*.php files. +* Introduced "stub global" framework to provide deferred initialisation of core + modules. +* Removed placeholder values for $wgTitle and $wgArticle, these variables will + now be null during the initialisation process, until they are set by index.php + or another entry point. +* Added DBA cache type, for BDB-style caches. +* Removed custom date format functions, replacing them with a format string in + the style of PHP's date(). Used string identifiers instead of integer + identifiers, in both the language files and user preferences. Migration should + be transparent in most cases. +* Simplified the initialisation API for LoadBalancer objects. +* Removed the broken altencoding feature. +* Moved default user options and toggles from Language to User. Language objects + are still able to define default preference overrides and extra user toggles, + via a slightly different interface. +* Don't include the date option in the parser cache rendering hash unless + $wgUseDynamicDates is enabled. +* Merged LanguageUtf8 with Language. Removed LanguageUtf8.php. +* Removed inclusion of language files from the bottom of Language.php. This is + now consistently done from Language::factory(). +* Add the name of the executing maintenance script to the debug log. Start the + profiler during maintenance scripts. +* Added "serialized" directory, for storing precompiled data in serialized form. +* Fix regression in auto-set NS_PROJECT_TALK namespace +* Fix regression in ordering of namespaces +* (bug 6806, 6030) Added several global JS variables for article path, user name, + page title, etc. +* hooks registered with addOnloadHook are now called at the one of the html body + by all skins. +* Split ajax aided search from core ajax framework. Use wgUseAjax to enable the + framework and wgAjaxSearch to enable the suggest feature for the search box. +* Added experimental installer for extensions. + See maintenance/installExtension.php +* Added Tajic (tg) language file. +* (bug 6903) Added Cantonese localisation (zh-yue) +* Fix regression in Korean and Japanese date formatting (day of week) +* (bug 6919) Add English alias magic words for Tatar (tt) language file. +* (bug 6753) Fixed broken Kazakh linktrail (kk) +* (bug 6700) Added Kazakh language variants to Names.php +* (bug 6827) some i18n specific maintenance scripts fails after merge of localisation-work branch +* Throwed an exception for the deprecated functions OutputPage::sysopRequired and + OutputPage::developerRequired - use OutputPage::permissionRequired instead. +* Removed the deprecated functions User::isSysop, User::isBureaucrat and User::isDeveloper - + use User::isAllowed instead. +* (bug 769) OutputPage::permissionRequired() should suggest groups with the needed permission +* (bug 6971) Fix regression in Special:Export history view +* Revamped Special:Imagelist +* (bug 7000) updated MessagesPl.php +* (bug 6946) Fix unexpected behavior change with GET hits to Special:Export +* (bug 1866) Improve navigation on Special:Listusers; user now a starting + point as with Special:Allpages, rather than a pure limit. +* Clean up tab order on Special:Blockip +* (bug 5969) Clean up tab order on Special:Userlogin forms +* (bug 3512) namespaceDupes now handles spaces and initial caps properly +* (bug 7037) Fix regression in login tab order +* (bug 7031) Report missing email on 'email password' instead of false success +* (bug 7010) Don't send email notifications for watched talk pages when user + has selected to receive only updates for their own talk page +* Added {{CURRENTHOUR}} +* Added [[:Image:Foo.png]] style links to the pagelinks table +* Avoid duplicate revision imports with Special:Import +* (bug 7054) Validate email address before sending email confirmation message +* (bug 7061) Format title on "from (page)" links on Special:Allpages +* (bug 7044) Introduce "padleft" and "padright" colon functions +* Pass page title as parameters to "linkshere" and "nolinkshere" and update + default message text +* Allows to upload from publicy accessible URL. Set $wgAllowCopyUploads = true ; in LocalSettings.php + Limited to $wgMaxUploadSize (default:100MB); URL upload is limited to sysops by default, and displayed as a second line if appropriate +* (bug 832) Return to user page after emailing a user +* (bug 366) Add local-system-timezone equivalents for date/time variables +* (bug 7109) Fix Atom feed version number in header links +* (bug 7075) List registered parser function hooks on Special:Version +* (bug 7059) Introduce "anchorencode" colon function +* Include SVN revision number in {{CURRENTVERSION}} output, where applicable +* Fix bug in wfRunHooks which caused corruption of objects in the hook list +* (bug 4979) Use simplified email addresses when running on Windows +* (bug 4434) Show block log fragment on Special:Blockip +* [[MediaWiki:Disambiguationspage]] may optionally contain wiki links to any number + of disambiguation templates. +* [[Special:Disambiguations]] now shows pages in NS:0 that link to any pages that embed + any of the templates listed at [[MediaWiki:Disambiguationspage]]. +* Fix formatting of titles on Special:Undelete +* (bug 7026) Fix action=raw&templates=expand +* (bug 6976) Add namespace and direction classes to classic skins +* (bug 7144) Don't "return to main" from OutputPage::loginToUse() if the the user can't + read the main page in the first place +* (bug 7188) Fix minor borkage in HTMLForm +* (bug 6675) Replaced message 'watchthis' with new message 'watchthisupload in Special:Upload +* Add a quickie script dumpSisterSites.php for generating a page list in the + format for WSR-1 SisterSites support +* (bug 7223) Monobook.js is used for site content, should not be localized +* Set default disabled values for DjVu render options +* Added Xml::option() for generating
  • ,
    ,
    ) +* (bug 5709) Allow customisation of separator for categories +* (bug 5684) Introduce Special:Randomredirect +* (bug 5611) Add a name attribute to the text box containing source text in + read-only pages +* Indicate when a protected page is an interface message ("protectedinterface") +* (bug 4259) Indicate when a protected page being edited is an interface message + ("editinginterface") +* (bug 4834) Fix XHTML output when using $wgMaxTocLevel +* Pass login link to "whitelistedittext" containing 'returnto' parameter +* (bug 5728): mVersion missing from User::__sleep() leading to constant cache + miss +* Updated maintenance/transstat.php so it can show duplicate messages +* Improvements to update scripts; print out the version, check for superuser + credentials before attempting a connection, and produce a friendlier error if + the connection fails +* (bug 5005) Fix XHTML output. +* (bug 5315) "Expires: -1" HTTP header made strictly valid (using 1970 date). +* (bug 4825) note in DefaultSettings.php about 'profiling' table creation +* Remove unneeded extra whitespace at top of Special:Categories +* (bug 5679) time units are now using local numerals +* (bug 5751) Updates to Portuguese localisation files +* (bug 5741) Introduce {{NUMBEROFUSERS}} magic word +* (bug 93) tags and tildes in templates +* The returnto parameter is now actually used by SpecialUserlogin.php +* Parser can now know that it is parsing an interface message +* (bug 4737) MediaWiki:Viewcount supports {{PLURAL}} now +* Fix bug in wfMsgExt under PHP 5.1.2 +* (bug 5761) Project talk namespace broken in Xal, Os, Udm and Cv +* Rewrite reassignEdits script to be more efficient; support optional updates to + recent changes table; add reporting and silent modes +* Cleaned up formatNum usage in langfiles +* (bug 5716) Warn when a user tries to upload a file which was previously + deleted +* (bug 5565) Add a class attribute to the table on Special:Allpages +* "lang=xx" option for parser test cases to set content language +* (bug 5764) Friulian translation updated +* (bug 5757) Fix premature cutoff in LanguageConverter with extra end markers +* (bug 5516) Show appropriate "return to" link on blocked page +* (bug 5377) Do not auto-login when creating an account as another user +* (bug 5284) Special redirect pages should remember parameters +* Suppress 7za output on dumpBackup +* (bug 5338) Reject extra initial colons in title +* (bug 5487) Escape self-closed HTML pair tags +* Add "raw suffix" magic word for some magic words, e.g. {{NUMBEROFUSERS|R}} + will produce a count minus formatting +* Fix Parser::cleanSig() to use Parser::startExternalParse() and choose an + appropriate output format given the scope of the clean +* (bug 5593) Change "bureaucrat log" to "rights log" +* Show a boilerplate "(none)" in place of a blank within the log action text for + user rights +* (bug 137) Commented out translations for copyrightwarning which mention GNU FDL +* (bug 5723) Don't count pages linked to from the MediaWiki namespace as "wanted" +* (bug 5696) Add a third parameter, $3, to "rcnote", passing the current time + formatted according to the current user's settings +* (bug 5780) Thousands and decimal separators for Norwegian +* Updated initStats maintenance script +* (bug 5767) Fix date formats in Vietnamese locale +* (bug 361) URL in URL, they were almost fixed. Now they are. +* (bug 4876) Add __NEWSECTIONLINK__ magic word to force the "new section" link/tab to + show up on specific pages on demand +* Bidi-aid on list pages +* (bug 5782) Allow entries in the bad image list to use canonical namespace names +* (bug 5789) Treat "loginreqpagetext" as wikitext +* Sanitizer: now handles nested
  • in
      or
        +* (bug 5796) We require MySQL >=4.0.14 +* Add 'EmailConfirmed' hook +* New findhooks.php script to find undocumented hooks. +* Silently ignore errors on profiling table update. +* (bug 5801) Correct handling of underscores in Special:Listusers +* Clean up Special:Listusers; add an "(all)" label to the group selection box +* (bug 5812) Use appropriate link colour in Special:Mostlinked +* (bug 5802) {{CURRENTMONTHNAME}} variable broken in Vietnamese locale +* (bug 5817) Appropriate handling for Special:Recentchangeslinked where the target + page doesn't exist +* Special:Randompage now additionally accepts English namespace name as parameter +* (bug 2981) Really fixed linktrail for Tamil (ta) +* Disallow substituting Special pages when included into a page +* (bug 5587) Clean up the languages from references to the Groups special page +* Added new group-X and group-X-member messages +* Rewritten removeUnusedAccounts to be more efficient, print names of inactive + accounts +* Redirect Special:Userlist to Special:Listusers +* Introduce $wgAllowTitlesInSVG, which allows the attribute in uploaded files + bearing the image/svg MIME type. Disabled by default due to the vast majority of + web servers being hideously misconfigured. See DefaultSettings.php for more details. +* Changed default LocalSettings.php to append the previous include path when setting it +* (bug 5837) Use "members" for the value descriptor in Special:Categories, + Special:Wantedcategories and Special:Mostlinkedcategories. +* (bug 3309) Allow comments when undeleting pages +* Clean up Special:Undelete a bit +* (bug 5805) messages nbytes, ncategories can now use {{plural:}} +* Clean up Special:Imagelist a bit +* (bug 5838) Namespace names for Nds-NL +* (bug 5749) Added Tyvan language files +* (bug 5791) Fix SQL syntax in Special:BrokenRedirects, was causing incorrect data to show +* (bug 5839) Prevent access to Special:Confirmemail for logged-out users +* (bug 5853) Update for Portuguese messages (pt) +* (bug 5851) Use Cyrillic for Kirghiz language name +* (bug 5841) Allow the 'EditFilter' hook to return a non-fatal error message +* (bug 5846) Link to individual group description pages in Special:Listusers +* (bug 5857) Update for German localisation (de) +* (bug 5858) Update for Russian language (ru) +* (bug 5860) Update for Indonesian language (id) +* (bug 1120) Update for Czech language (Cs) +* Added many missing formatNum calls +* Added grammar function to Belarusian (be) +* (bug 5819) Add 'PersonalUrls' hook +* (bug 5862) Update of Belarusian language (be) +* (bug 5886) Update for Portuguese messages (pt) +* (bug 5586) <gallery> treated text as links +* (bug 5878) Update for Indonesian language (id) +* (bug 5697) Update for Malay language (ms) +* (bug 5890) Update for German language (de) +* (bug 5889) Name for Sindhi language should appear as سنڌي +* --force-normal parameter on dump scripts to force check for ICU extension +* (bug 5895) Update for Dutch language (nl) +* (bug 5891) Linktrail for Polish language (pl) +* User::isBureaucrat , User::isDeveloper , User::isSysop deprecated in + v1.6 now die with a backtrace. They will be removed in v1.8 +* dumpTextPass now skips goes to database for entries that were blank in the + previous dump, as this may indicate a broken dump. +* dumpTextPass progress includes percentage of items prefetched +* dumpTextPass can now use 7zip files for prefetch +* (bug 5915) Update to Indonesian localisation (id) +* (bug 5913) Update for German localisation (de) +* (bug 5905) Plural support for Bosnian localisation (bs) +* Groups which won't hit the rate limiter now configurable with + $wgRateLimitsExcludedGroups +* (bug 5806) {{plural:}} support instead of "twin" MediaWiki messages +* (bug 5931) Update for Polish language (pl) +* Ignore the user and user talk namespaces on Special:Wantedpages +* Introduce NUMBEROFPAGES magic word +* (bug 5833) Introduce CURRENTVERSION magic word +* (bug 5370) Allow throttling of password reminder requests with the rate limiter +* (bug 5683) Respect parser output marked as uncacheable when saving +* (bug 5918) Links autonumbering now work for all defined protocols +* (bug 5935) Improvement to German localisation (de) +* (bug 5937) Register links from gallery captions with the parent parser output + object so that link tables receive those updates too +* (bug 5845) Introduce BASEPAGENAME and BASEPAGENAMEE magic words +* (bug 5941) Use content language when getting the administrator page title for + Special:Statistics +* (bug 5949) Update to Indonesian localisation (id) +* (bug 5862) Update of Belarusian translation (be) +* (bug 5950) Improvements to French localisation +* (bug 5805) {{plural:}} support for counters in some special pages +* (bug 5952) Improvement to German localisation (de) +* Rename conflicting metadata help message to "metadata_help" (was "metadata") + and treat it as wiki text +* Improve preferences input filtering +* Maintenance script to import multiple files into the wiki +* (bug 5957) Update for Hebrew language (he) +* (bug 5962) Update for Italian language (it) +* (bug 5961) Update for Portuguese localisation (pt) +* (bug 5849) Remove some hard-coded references to "Wikipedia" in messages +* (bug 5967) Improvement to German localisation (de) +* (bug 5962) Update for Italian language (it) +* Suppress images in galleries which appear on the bad image list (when rendering + for a wiki page; galleries in special pages and categories are unaffected) +* Maintenance script to remove orphaned revisions from the database +* (bug 5991) Update for Russian language (ru) +* (bug 6001) PAGENAMEE and FULLPAGENAMEE don't work in FULLURL and LOCALURL magic + words +* (bug 5958) Switch Uzbek language name to use latin script +* (bug 839) Add URLENCODE magic word +* (bug 6004) Update for Polish language (pl) +* (bug 5971) Improvement to German localisation (de) +* (bug 4873) Don't overwrite the subtitle navigation when viewing a redirect page + that isn't current +* (bug 2203) Namespace updates for Thai +* Fix breakage in parser test suite which caused incorrect reporting of the failure of + {{NUMBEROFFILES}}. Now initialises the site_stats table with some dumb data. Updated + the expected output for {{NUMBEROFARTICLES}} to reflect this. +* (bug 6009) Use {{ns:project}} in messages where appropriate +* (bug 6012) Update to Indonesian localisation (id) +* (bug 6017) Update list of bookstores in German localisation files +* (bug 5187) Allow programmatically bypassing username validation, for scripts +* (bug 6025) SpecialImport: wrong message when no file selected +* (bug 6015) EditPage: add spacing in the boxes "edit is minor" and "watch this" +* (bug 6018) Userrights: new message when no user specified ('nouserspecified') +* (bug 2015) Add "\sim" to ~ conversion for HTML rendering +* (bug 6029) Improvement to German localisation (de) +* (bug 5015) Update be: magic words +* (bug 3974) Add parameter for site URL to "passwordremindertext" +* (bug 6039) Update for Portuguese localisation (pt) +* (bug 764) Add CREATE TEMPORARY TABLES to default database permissions +* Big update to Swedish localisation (sv) +* Use appropriate HTML functions to create the tool links on image pages, so they don't + look garbled when tidy isn't on +* (bug 5511) Fix URL-encoding of usernames in links on Special:Ipblocklist +* (bug 6046) Update to Indonesian localisation (id) #15 +* (bug 5523) $wgNoFollowNsExceptions to allow disabling rel="nofollow" in + specially-selected namespaces. +* (bug 6055) Fix for HTML/JS injection bug in variable handler (found by Nick Jenkins) +* Reordered wiki table handling and __TOC__ extraction in the parser to better + handle some overlapping tag cases. +* Only the first __TOC__ is now turned into a TOC +* (bug 4610) Indicate patrolled status on watchlists and allow users to mark + changes as patrolled using the diff links there +* Add 'DiffViewHeader' hook called before diff page output +* (bug 6051) Improvement to German localisation (de) +* (bug 6054) Update to Indonesian localisation (id) #16 +* Add {{CURRENTTIMESTAMP}} magic word +* (bug 6061) Improper escaping in some html forms +* (bug 6065) Remove underscore when using NAMESPACE and TALKSPACE magics. +* (bug 6074) Correct squid purging of offsite upload URLs +* To simplify the lives of extension developers, the logging type arrays + can now be appended to directly by an extension setup function. It is + no longer necessary to write four separate functions just to add a + custom log type. +* (bug 6057) Count "licenses" as a message (and show it in Special:Allmessages) +* Added $wgGrammarForms global +* Fixed hardcoded 'done.' when removing watchlist entries. +* (bug 5962) Update for Italian language (it) +* (bug 6086) Remove vestigial attempt to call Article::validate() +* wfHostname() function for consistent server hostname use in debug messages +* Send thumbnailing error messages to 'thumbnail' log group +* wfShellexec() now accepts an optional parameter to receive the exit code +* Failed, but not zero-length, thumbnail renderings are now removed. + Should help clean up when rsvg fails in weird ways. +* (bug 6081) Change description for Turkmen language +* Increase robustness of parser placeholders; fixes some glitches when + adjacent to identifier-ish constructs such as URLs. +* Shut up the parser test whining about files in a temp directory. +* (bug 6098) Add Aragonese language support (an) +* (bug 6101) Update for Russian language (ru) +* Add $wgIgnoreImageErrors to suppress error messages for thumbnail rendering + problems. If errors are transitory, this should reduce annoying messages + making it into cached display. +* (bug 6103) Wrap self-links in a CSS class ("selflink") +* (bug 6102) For consistency with other markup, normalize all HTML-encoded + character entities in URLs, not just ampersands. This allows use of eg + = when making URLs for template parameters. +* Markup anality: escape </ as <\/ in toolbar javascript for pure correctness + under HTML-compatible browsers. +* (bug 5077) Added hook 'BeforePageDisplay' to SkinTemplate::outputPage +* Replace fatally changed 'uploadnewversion' with 'uploadnewversion-linktext' +* (bug 472) Syndication feeds for the last few edits of page history +* Format edit comments in Recent Changes feed +* Switch incorrectly ordered column headers on Recent Changes feed diffs +* (bug 6117) Use message for history feed description, add German localization +* (bug 1017) fixed thumbnails of animated gifs. +* Add APC as object caching option +* Update to Albanian localization (sq) +* (bug 6099) Introduce {{DIRECTIONMARK}} magic word (with {{DIRMARK}} as an alias) +* Use optimized php5-only microtime() +* Add possibility to store local message cache as PHP executable script +* Fix profiling table definition +* (bug 6040) Run pre-save transform before calculating the diff. when doing a + "show changes" operation in the editor +* (bug 4033) Respect $wgStyleDirectory when checking available skins +* Remove hideous backslashes from MessagesBr.php +* Fix APC object cache issues, add functionality to installer +* (bug 6133) Update strip state as we work. This mostly fixes extensions + used in Cite.php <ref> tags when Tidy is on. +* (bug 6139) Workaround for transclusion oddities in Vietnamese upload text +* (bug 6136) Update to Catalan language (ca) +* Update to Japanese localization (ja) +* Add /usr/local/bin to the diff3 search paths in the installer +* (bug 6106) Update to Indonesian localisation (id) #17 +* (bug 6125) Add links to edit old versions to diff views +* (bug 5127) Auto edit summary when creating/editing redirect page +* (bug 3926) Introduce {{#language:}} magic word +* Fix section links from edit comments for [[:Image:Bla.jpg]] in section titles +* (bug 6126) Allow fallback to customized primary language when user language + message contains '-'; fixes licenses selector on Commons configuration after + recent addition of the message to Messages.php +* (bug 5527) Batch up job queue insertions for, hopefully, better survivability + of lock contention etc. Duplicates are now removed at pop time instead of + at insert time. +* When showing the "blah has been undeleted" page, make sure it's a blue link +* parserTests.php accepts a --file parameter to run an alternate test sutie +* parser tests can now test extensions using !!hooks sections +* Fix oddity with open tag parameters getting stuck on </li> +* (bug 5384) Fix <!-- comments --> in <ref> extension +* Nesting of different tag extensions and comments should now work more + consistently and more safely. A cleaner, one-pass tag strip lets the + 'outer' tag either take source (<nowiki>-style) or pass it down to + further parsing (<ref>-style). There should no longer be surprise + expansion of foreign extensions inside HTML output, or differences + in behavior based on the order tags are loaded. +* (bug 885) Pre-save transform no longer silently appends close tags +* Pre-save transform no longer changes the case of close tags +* (bug 6164) Fix regression with <gallery> resetting <ref> state +* Hackaround for IE 7 wrapping bug in MonoBook footer +* New message sp-newimages-showfrom replaces rclistfrom on special:newimages +* Improve handling of ;: definition list construct with overlapping or + nested HTML tags +* (bug 6171) Fix sanitizing of HTML-elements with an optional closing + tag. The sanitizer still needs to learn how to make well-formed XML + in this case. +* Fix fatal error when specifying illegal name for manual thumbnail +* (bug 6184) Use shinier Linker::userLink() to make user links in + Special:Undelete +* (bug 6170) Update for Kashubian translation (csb) +* (bug 6191) Update to Indonesian translation (id) #18 +* (bug 6114) Update to Walloon localization (wa) +* Added $wgNamespaceRobotPolicies to allow customisation of robot policies on a + per-namespace basis. +* Add <ol> to the list of block elements for doBlockLevels; avoids <p>s being + interspersed into your ordered lists. +* (bug 5021) Transcluding the same special page twice now works +* Add 'SiteNoticeBefore' and 'SiteNoticeAfter' hooks +* (bug 6182) Date passed in "sp-newimages-showfrom" not adjusted to user time + preferences +* (bug 2587) Fix for section editing with comment prefix +* (bug 2607) Fix for section editing with mix of wiki and HTML headings +* (bug 3342) Fix for section editing with headings wrapped in <noinclude> +* (bug 3476) Fix for section editing with faux headings in extensions +* (bug 5272) Fix for section editing with HTML-heading subsections +* Fix for bogus wiki headings improperly detected with following text +* Fix for HTML headings improperly not detected with preceding/following text +* Section extraction and replacement functions merged into one implementation + on the Parser object, so they can't get out of sync with each other. +* Edit security precautions in raw HTML mode, etc +* (bug 6197) Update to Indonesian translation (id) #19 +* (bug 6175) Improvement to German translation (de) +* Redirect Special:Logs to Special:Log +* (bug 6206) Linktrail for Swedish localization (se) +* (bug 3202) Attributes now allowed on <pre> tags +* Sanitizer::validateTagAttributes now available to discard illegal/unsafe + attribute values from an array. +* (bug 3837) Leave <center> as is instead of doing an unsafe text replacement + to <div class="center">. <center> is perfectly valid in the target doctype + (XHTML 1.0 Transitional), while the replacement didn't catch all cases and + could even result in invalid output from valid input. +* (bug 4280) Use 'noindex,nofollow' instead of 'noindex,follow' for default + meta robots tag on diff view and special pages. Should reduce impact of + robots on scrolling special pages, diffs etc on sites where robots.txt + doesn't forbid access. +* Regression fix: suppress warning about session failure when clicking to + edit with 'preview on first edit' enabled. +* (bug 6230) Regression fix: <nowiki> in [URL link text] +* Added AutoLoader.php, which loads classes without need of require_once() +* (bug 5981) Add plural function Slovenian (sl) +* (bug 5945) Introduce {{CONTENTLANGUAGE}} magic word +* {{PLURAL}} can now take up to five forms +* (bug 6243) Fix email for usernames containing dots when using PEAR::Mail +* Remove a number of needless {{ns:project}}-type transforms from messages files. These + usages already have separate label text. Such transforms are wasteful on each page view. +* Update to Yiddish localization (yi) +* (bug 6254) Update to Indonesian translation (id) #20 +* (bug 6255) Fix transclusions starting with "#" or "*" in HTML attributes +* Whitespace now normalized more or less properly in HTML attributes +* Fix regression(?) in behavior of initial-whitespace-pre in <center> +* (bug 6260) Update to Interlingua localization (ia) +* Update to Vlax Romany localization (rmy) +* Update to Latin translation (la) +* Update to Dutch translation (nl) +* Avoid some notices in page history with bad input +* Use double quoted consistently on attributes in linker output; preparing + for new normalization code when tidy not in use +* Replace "nogomatch" with "noexactmatch" and place the magic colon in the messages + themselves. Some minor tweaks to the actual message content. +* Introduce $wgContentNamespaces which allows for articles to exist in namespaces other + than the main namespace, and still be counted as valid content in the site statistics. +* (bug 5932) Introduce {{PAGESINNAMESPACE}} magic word +* Disable $wgAllowExternalImages by default. +* (bug 2700) Nice things like link completion and signatures now work in <gallery> tags. +* Cancel output buffering in StreamFile; when used inside gzip buffering this + could cause funny timeout behavior as the Content-Length was wrong. +* Return correct content-type header with 304 responses for StreamFile; + it confuses Safari if you let it return "text/html". +* (bug 6280) Correct GRAMMAR for Slovenian localisation (sl) +* (bug 6162) Change date format for Dutch Low Saxon (nds-nl) +* (bug 6296) Update to Indonesian localisation (id) #21 +* Introduce EditFormPreloadText hook, see docs/hooks.txt for more information +* (bug 4054) Add "boteditletter" to recent changes flags +* Update to Catalan localization (ca) +* (bug 2099) Deleted image files can now be archived and undeleted. + Set $wgSaveDeletedFiles on and an appropriate directory path in + $wgFileStore['deleted']['directory'] +* (bug 6324) Fix regression in enhanced RC alignment +* Introduce {{NUMBEROFADMINS}} magic word +* Update to Slovak translation (sk) +* Update to Alemannic localization (gsw) +* (bug 6300) Bug fixes for sr: variants +* namespaceDupes.php can now accept an arbitrary prefix, for checking rogue + interwikis and such. Not yet fully automated. +* (bug 6344) Add Special:Uncategorizedimages page +* (bug 6357) Update to Russian translation (ru) +* Workaround possible bug in Firefox nightlies by properly removing the + Content-Encoding header instead of sending explicit 'identity' value + in StreamFile +* (bug 6304) Show timestamp for current revision in diff pages +* Vertically align current version with old version header in diff display +* (bug 6174) Remove redundant "emailforlost" message +* (bug 6189) Show an error to an unprivilleged user trying to create account +* (bug 6365) Show user information in the "old revision" navigation links +* Introduce 'FetchChangesList' hook; see docs/hooks.txt for more information +* (bug 6345) Update to Indonesian localisation (id) #22 +* (bug 6279) Add genitive month names to Slovenian localisation +* (bug 6351) Update to German translation (de) +* Respect language directionality when displaying arrow in Special:Brokenredirects +* Remove unused "validation" table definitions from the schema files +* (bug 6398) Work around apparent PCRE bug breaking section editing when + massively-indented preformatted text immediately followed a header +* (bug 6392) Fix misbehaving <br /> in preferences form +* Add translated magic words to Hebrew localization +* (bug 6396) Change name for Chuvash language +* Introduce optional (off by default) language selector bar for user login + and registration. Customisable via the "loginlanguagelinks" message, the + links will preserve "returnto" values. If the user creates an account while + using such a link, then the language in use will be saved as their language + preference. +* Make sure '~~~' '~~~~' '~~~~~' are removed in Nickname preference. +* Rename "ipusuccess" to "unblocked", change the format (now wiki text) +* (bug 2316) Add "caption" attribute to <gallery> tag +* Allow setting the skin object that ImageGallery will use; needed during parse + operations (the skin must come from the ParserOptions, not $wgUser) +* Fix notice in MacBinary detection debug data for files of certain lengths +* (bug 6131) Add type detection for DjVu files, allowing them to be uploaded + with validity checking and size detection. No inline thumbnailing yet, + but could be added in the future. +* (bug 6423) Don't update newtalk flag if page content didn't change (null edits + were causing the newtalk flag to trigger inappropriately) +* Parser functions are now set using magic words. +* (bug 6428) Incorrect form action URL on Special:Newimages with hidebots = 0 set +* (bug 4990) Show page source to blocked users on edits, or their modified version + if blocked during an edit +* (bug 5903) When requesting the raw source of a non-existent message page, + return blank content (as opposed to the message key) +* Improve default blank content of MediaWiki:Common.css and MediaWiki:Monobook.css +* (bug 6434) Allow customisation of submit button text on Special:Export +* (bug 6314) Add user tool links on page histories +* Fix display of file-type icons in galleries when $wgIgnoreImageErrors is off +* (bug 6438) Update to Indonesian translation (id) #23 +* Adding the language code parameter to the hook "LanguageGetMagic", to allow + localizble extensions magic words. +* Update to Romanian translation (ro) +* Update to Esperanto translation (eo) +* Check for preg_match() existence when installing and die out whining about PCRE + if it's not there, instead of throwing a fatal error +* (bug 672) Add MathAfterTexvc hook +* Update to Piedmontese localization (pms) +* dumpBackup can optionally compress via dbzip2 +* (bug 2483) Run link updates on change via XML import +* (bug 2481) List imported pages during Special:Import +* (bug 2482) Log and RC entries for Special:Import events +* Allow fetching all revisions from transwiki Special:Import +* Allow fetching all revisions from Special:Export GET request +* Disable output buffering on Special:Export; should help with streaming + large numbers of history items. +* Allow setting a maximum number of revisions for history Special:Export; + pages with more than $wgExportMaxHistory revisions are excluded from + export when history is requested. +* Fix transwiki import of pages with space in name +* Save null edit when importing pages through Special:Import +* Update to Korean translation (ko) +* Show a more specific message when an anonymous user tries to access Special:Watchlist +* (bug 3278) Paging links in Special:Prefixindex +* Added Latvian localization (lv) +* (bug 6472) Fix regression in Special:Export with multiple pages +* Update to Macedonian translation (mk) +* Allow page moves over historyless self-redirects. Such are usually created + as part of namespace rearrangements, and it's easier to clean them up if + we can move over them. +* Show some error results in moveBatch.php +* (bug 6479) Allow specification of the skin to use during HTML dumps +* (bug 6461) Link to page histories in Special:Newpages +* (bug 6484) Don't do message transformations when preloading messages for editing +* (bug 6201) Treat spaces as underscores in parameters to {{ns:}} +* (bug 6006) Allow hiding the password change fields using an authentication plugin +* (bug 6489) Use appropriate link colour on Special:Shortpages +* Added formatnum magic word +* Added Javanese localization (jv) +* (bug 6491) Apply bad image list in category galleries +* (bug 6488) Show relevant log fragment in Special:Movepage +* Fix potential PHP notice in Special:Blockme when $wgBlockOpenProxies is true +* Use mysql_real_escape_string instead of addslashes for string escaping in + the MySQL Database class. This may fix some rare breakage with binary fields. + Note that MediaWiki does not support the multibyte character sets where a + "dumb" byte replacement can be actively dangerous; UTF-8 is always safe + in this regard due to the bit patterns which make head and tail bytes + distinct. +* (bug 6497) Use $wgMetaNamespaceTalk for Esperanto if set +* (bug 6498) Use localized forms for image size in Special:Undelete +* (bug 6485) Update to Indonesian translation (id) #24 +* Extension messages translation is now possible. +* Add target namespace override selector for transwiki imports. + $wgImportTargetNamespace specifies the default, to be used for + Wiktionary's 'Transwiki:' namespace etc. +* (bug 6506) Update to German localisation (de) +* (bug 502) Avoid silly tabs on bad title by using virtual special page +* (bug 6511) Add diff links to old revision navigation bar +* (bug 6511) Replace 'oldrevisionnavigation' message with 'old-revision-navigation' +* Fix regression in Polish genitive month forms +* (bug 4037) Make input handling in Special:Allpages and Special:Prefixindex + more consistent: Accept just a namespace prefix and a colon, reject input + with interwiki prefixes, otherwise do what Title::makeTitleSafe() does. +* (bug 6516) Update to Russian translation +* New 'allpagesbadtitle' message for Special:Allpages, based on 'badtitletext'. +* Rename "searchquery" to "searchsubtitle" and support wiki text in it +* Introduce updateArticleCount maintenance script which uses a better check that + reflects what Article::isCountable() tests for +* Introduce 'BadImage' hook; see docs/hooks.txt for more information +* Add "searchsubtitleinvalid" message for searches that are not valid titles. +* (bug 5962) Update to Italian localisation +* (bug 6530) Update to Indonesian localisation (id) #25 +* (bug 6523) Fix SVG issue in rebuildImages.php +* (bug 6512) Link to page-specific logs on page histories +* (bug 6504) Allow configuring session name with $wgSessionName +* (bug 6185) Add standard user tool links to log page views +* Update to Venetian translation (vec) +* Update to Slovenian translation (sl) +* Add standard user tool links to deleted revision list +* Separate out EditPage's getContent bits from regular Article getContent. + Cleans up read-only-mode warning on empty pages and neats up some code. +* (bug 6565) Strict JavaScript writing +* (bug 6570) Update to Indonesian localisation (id) #26 +* Added Telugu translation (te) +* Update to Catalan translation (ca) +* (bug 6560) Avoid PHP notice when trimming ISBN whitespace +* Added namespace translation to Kannada (ka) +* (bug 6566) Improve input validation on timestamp conversion +* Implicit group "emailconfirmed" for all users whose email addresses are confirmed +* (bug 6577) Avoid multiline parser breakage on <pre> with newline in attribute +* (bug 6771) Make old revisions of MediaWiki pages available with action=raw + +== Changes since 1.5 == + +* (bug 2885) More PHP 5.1 fixes: skin, search, log, undelete + +Code quality: +* Use strval() to make sure we don't accidentally get null on bad revision + text loads or other fields mucking up XML export output +* Clean up duplicate code for selection of changeslist style +* Correct blob caching to reduce redundant blob loads on backups +* (bug 3182) Clear link cache during import to prevent memory leak +* Fixed possible infinite loop in formatComment +* Wrap message page insertions in a transaction to speed up installation +* Avoid notice warning on edit with no User-Agent header +* (bug 3649) Remove obsolete, broken moveCustomMessages script +* Avoid numerous redundant latest-revision lookups in history +* Require PHP 4.3.2 or higher strictly now. +* Tweak infinite-template-handling loop for PHP 5.1.1 string handling change +* Remove unused OutputPage::addCookie() +* Fix for short_open_tag off again; please don't break this, guys +* (bug 4507) Adjust FULLPAGENAMEE escaping to standard form +* (bug 5302) Merge the two #p-search .pBody statements in monobook css. + +Database: +* Finally dropped MySQL 3.23.x support +* Oracle support +* (bug 3056) MySQL 3 compatibility fix: USE INDEX instead of FORCE INDEX +* Update all stats fields on recount.sql +* (bug 3227) Fix SQL injection introduced in experimental code +* Fix table prefix usage in Block::enumBlocks +* (bug 3448) Set page_len on undelete +* (bug 3506) Avoid MySQL error when Listusers returns no results +* Skip update of disused 'rc_cur_time' field (todo: discard the field) +* (bug 3735) Fix to run under MySQL 5's strict mode +* (bug 3786) Experimental support for MySQL 4.1/5.0 utf8 charset mode + NOTE: Enabling this may break existing wikis, and still doesn't + work for all Unicode characters due to MySQL limitations. +* MySQL 5.0 strict mode fix for moving unwatched pages +* Ability to set the table name for external storage servers +* Update ipblocks table in MySQL 5 table defs +* Removed FulltextStoplist.php, no longer used (was for MySQL 3.x workaround) +* Added templatelinks table, to track template inclusions. User-visible effects + will be: + * (inclusion) tag for inclusions in Special:Whatlinkshere + * More accurate list of used templates on the edit page + * More reliable cache invalidation when templates outside the template + namespace are changed +* Respect database prefix in dumpHTML.inc +* Removed read-only check from Database::query() +* Added externallinks table, to track links to arbitrary URLs +* Added job table, for deferred processing of jobs. The immediate application is + to complete the link table refresh operation when templates are changed. +* Don't change the password of the MySQL root user. + +Documentation: +* (bug 3306) Document $wgLocalTZoffset + +Hooks: +(list not complete) +* Move ArticleSave hook execution into Article insert/update functions, + so they get called on non-EditPage actions that use these functions + to create or update pages. +* Added EditFilter hook, and output callback on EditPage::showEditForm() + for a place to add in captcha-type extensions in the edit flow +* (bug 3684) Fix typo in fatal error backtraces in Hooks.php +* Fix for hook callbacks on objects containing no fields +* Add a hook for additional user creation throttle / limiter extensions +* Use $wgOut->parse() in wfGetSiteNotice() instead of creating a new parser + instance. This allows use of extension hooks if required. +* Added AutoAuthenticate hook for external User object suppliers +* Added 'PageRenderingHash' hook for changing the parser cache hash key + from an extension that changes rendering based on nonstandard options. +* Add 'GetInternalURL' hook to match the GetFullURL and GetLocalURL ones +* (bug 4456) Add hook for marking article patrolled +* Add UserRights hook, fires after a user's group memberships are changed + +Images: +* Support SVG rendering with rsvg +* Cap arbitrary SVG renders to given image size or $wgSVGMaxSize pixels wide +* (bug 3127) Render large SVGs at image page size correctly +* Fix scaling of non-integer SVG unit sizes +* (bug 2800) Don't scale up small images on |thumb| without explicit size +* Use the real file link instead of the default-size rasterized version for + large SVG images on image description page +* Include the file name/type/size line for non-resized images +* (bug 3489) PHP 5.1 compat problem with captioned images +* (bug 3643) Fix image page display of large images with resizing disabled +* Added a limit to the size of image files which can be thumbnailed +* (bug 3806) Gracefully fall back to client-side scaling on |thumb| image + that passes $wgMaxImageArea +* (bug 153) Adjust thumbnail size calculations to match consistently; + patch by David Benbennick +* (bug 4162) Add $wgThumbnailEpoch timestamp to force old thumbs to + be rerendered on demand, sitewide +* (bug 1850) Additional fixes so existing local and remote images + get a blue link even if there's no local description page +* Avoid FATAL ERROR when creating thumbnail of non-existing image +* (bug 4207) Wrong image size when using 100x200px syntax to scale image up + patch by David Benbennick +* Don't delete thumbnails when refreshing exif metadata. This caused thumbs + to vanish mysteriously from time to time for files that didn't have metadata. +* (bug 4426) Add link to user_talk page on image pages +* Support a custom convert command for thumbnailing. See DefaultSettings.php + and the comments for $wgCustomConvertCommand, for more information. +* UserCan hook now allows advisory return values, rather than mandatory ones. + +Installer: +* (bug 3782) Throw fatal installation warning if mbstring.func_overload on. + Why do people invent these crazy options that change language semantics? +* Fixed installer bugs 921 and 3914 (issues with using root and so forth) +* (bug 4258) Use ugly urls for ISAPI by default + patch by Rob Church +* Improve installer + * Use a superuser account (such as root), if specifed, to create tables + * Don't overwrite conservative permissions on the mySQL user with ALL + permissions, if said user exists + * Changes to some of the wording of explanations for fields +* (bug 1734) granting db permissions failed with db usernames containg '-' +* Add basic check for session support in PHP and die if not present + +Maintenance: +* Fix problem reported on mailing list where re-initialising stats didn't work (can't insert + duplicate rows with the same id field) +* Added --conf option to command line scripts, allowing the user to specify a + different LocalSettings.php. +* Maintenance script to delete unused text records +* Maintenance script to delete non-current revisions +* Maintenance script to wipe a page and all revisions from the database +* Maintenance script to reassign edits from one user to another +* Maintenance script to find and remove links to a given domain (cleanupSpam.php) +* Fix --report interval option for dumpTextPass + +i18n / Languages: +* Partial support for Basque language (from wikipedia and meta) +* (bug 3141) Partial support for Breton language (thanks Fulup). +* Support for venitian language +* (bug 1334) LanguageGa.php update +* Finnish date format was hardcoded, now implemented properly +* (bug 3190) Added some date format choices for language sr +* (bug 2753) Some namespaces were not translated in LanguageTa.php (Tamil) +* (bug 3204) Fix typo breaking special pages in fy localization +* (bug 3177) Estonian date formats not implemented in LanguageEt.php +* (bug 1020) Changing user interface language does not work immediately +* (bug 3271) Updated LanguageNn.php for HEAD +* Experimental feature to allow translation of block expiry times + Implementation only for Finnish currently +* (bug 3304) Language file for Croatian (LanguageHr.php) +* (bug 2143) Update Vietnamese interface +* (bug 3063) Remove some hardcodings from Hebrew localisation +* (bug 3408) Bulgarian formatNum corrected +* (bug 1512) Disable x-code interp on Esperanto URLs for now, it does more + harm than good under current system by breaking incoming URLs with "ux". + (Editing is not affected, just URLs.) +* (bug 1423) LanguageJa.php update +* Fix language name for dv +* (bug 3503) Update LanguageSq.php from sq.wikipedia.org messages +* (bug 3629) Fix date & time format for Frisian +* (bug 3334) Namespace changes for Polish +* (bug 3580) Change default Dutch language file to more neutral +* (bug 3656) LanguageHr.php - added convertPlural +* (bug 3414) LanguageBe.php - added convertPlural +* (bug 3163) Full translation of LanguageBr +* (bug 3617) Update for portuguese language (pt) +* Namespaces hacks on LanguagePl +* (bug 3682) LanguageSr.php - added convertPlural +* (bug 3694) LanguageTr.php update +* (bug 3711) Removed invisible unicode characters from LanguageHu +* (bug 2981) Linktrail for Tamil (ta) +* (bug 3722) Update of Arabic language (ar) Namespace changes +* Removed hardcoded Norwegian (no) project namespaces +* (bug 2324) image for redirects should be without text and oriented according to content language +* (bug 3666) Don't spew PHP warnings in prefs on unrecognized site language +* (bug 3817) Use localized date formats in preferences; 'no preference' option + localizable as 'datedefault' message. Tweaked lots of languages files... +* (bug 2721) Regression: Use European number separators for vi: wikis +* (bug 3961) minor languageDe changes +* (bug 1984) LanguageKo.php (Korean) update +* (bug 3804) update of LanguageWa.php file +* (bug 3886) Update for Portuguese language (pt) +* (bug 4020) Update namespaces for ms +* (bug 3922) bidi embedding overrides on category links +* (bug 4061) Update of Slovene namespace names (LanguageSl.php) +* (bug 4064) LanguageDe comma changes +* (bug 3922) Further tweaks to bidi overrides in category list for old + versions of Safari and Konqueror +* Fix custom namespaces on wikis set for Portuguese +* (bug 4153) Fix block length localizations in Greek +* (bug 3844) ab: av: ba: ce: & kv: now inherit from LanguageRu.php + ii: & za: now inherit from LanguageZn_cn.php +* (bug 4165) Correct validation for user language selection (data taint) +* (bug 4192) Remove silly 'The Free Encyclopedia' default sitesubtitle +* Use content-lang for sitenotice +* (bug 4233) Update LanguageJa.php +* (bug 4279) Small correction to LanguageDa.php +* (bug 4108, 4336) Remove trailing whitespace from various messages, which + mucks up message updating to create dupe entries +* (bug 4389) Fix math options on zh-hk and zh-tw (but not localized) +* (bug 4392) Update of LanguageSr.php +* (bug 4382) Frisian numeric format +* (bug 4424) Update for Spanish language (es) 100% messages translated +* (bug 4425) Typos in Polish translation +* (bug 4436) Update for Turkish language (tr) +* (bug 4413) Update of Farsi language file (LanguageFa.php) +* Update for LanguageSr (Serbian): magic words +* (bug 137) MediaWiki:Copyrightwarning hardcoding +* (bug 4457) Update for Portuguese language (pt) +* convertPlural breakage fixed a little +* (bug 4144) Support for Sudanese language (Basa Sunda) +* Big cleanup: + - Removed obsolote, badly or untranslated messages + - Removed references to wikipedia/wikimedia etc in messages + - Other cleanup, like removing html and javascript and extension calls + - Removed hardcoded namespaces: Tt, Ms, Ia, Ga, Fo, Bn, Csb, He, Nv, Oc, Tlh + - Removed some useless backwards compatibility hacks + - Fixed formatnum on many languages +* wgAmericanDates check produced incorrect results in languages that don't have + a such distinction +* (bug 4548) Update for Portuguese language (pt): time format +* (bug 4530) Use consistent name for Kurdish +* Tweak default "upload disabled" text +* (bug 4504) Use site language for namespace name resolution +* (bug 4510) Correct Barnes & Noble bookstore URLs +* (bug 3991) Allow the operation of wikicode on Protect move only text +* (bug 4267) Switch dv sd ug ks arc languages to RTL +* Default main page content improved per bug 4690 +* (bug 4615) Update for Portuguese language (pt) +* Separated MessagesSl.php as the other languages. +* (bug 4960) Add additional namespaces variants to Yiddish for compatibility +* (bug 4805) Removed more wikipedia-references from MessagesUk.php +* (bug 5015) Update magic words translation in LanguageBe.php +* (bug 4859) Update for Portuguese messages (pt) +* (bug 4788) One string for MessagesPl +* Restriction types now use restriction-* messages instead of ui messages +* (bug 4685) Slovenian LanguageSl.php hardcodes project namespace +* (bug 5097) Fix Hungarian language (hu): thousands separator +* (bug 5098) Update for Portuguese messages (pt) +* (bug 5113) Spelling error in French language file +* (bug 5105) Magic words for LanguageAr.php +* (bug 3993) Variants for Serbian language +* Typo in English messages file +* (bug 4114) Spacing in watchlist rows (in editing mode) +* Update default "exporttext" to reflect that Special:Import exists +* (bug 4960) Add additional namespaces variants to Yi projects: Yiddish Wikinews fix +* (bug 5357) Add the icon near the user name also in RTL interfaces +* (bug 5156) Update for Hebrew language (he) +* (bug 4497,4704,5010) Added some new language codes. +* (bug 5362) Piedmontese added +* (bug 5349) Update for Portuguese messages (pt) +* (bug 3573) Finished full Greek translation: namespaces +* (bug 5288) Initial localisation for Az +* (bug 4361) Fix "allmessagesnotsupportedui" so it doesn't refer to nonexisting + page +* Tweak wording of "allmessagesnotsupporteddb" + +Parser: +* (bug 2522) {{CURRENTDAY2}} now shows the current day number with two digits +* (bug 3210) Fix Media: links with remote image URL path +* (bug 3405) Don't use raw letters as aliases of MSGNW: and SUBST: +* (bug 3412) Clean up date format handling so ~~~~-sigs work with default + format as designed. Documentation comments updated. +* Fix Parser::unstrip on PHP 5.1.0RC4 +* (bug 3797) Don't expand variables and sigs in comments +* Allow parser cache on redirect targets +* Run wikitext-escaping on plaintext sigs (no wiki markup, just name) +* Check for unbalanced HTML tags on raw sigs (markup allowed, but show + a warning in prefs and use default sig if not balanced) +* Respect <noinclude> and <includeonly> during {{subst:}} expansion as well as + ordinary templates. +* Support <includeonly> in templates loaded through preload= parameter +* (bug 3979) Save correct {{REVISIONID}} into parser cache on edit +* Substitute {{REVISIONID}} correctly in diff display +* (bug 1850) Allow red-links on image pages linked with [[:image:foo]] +* Fix XML validity checks in parser tests on PHP 5.1 +* (bug 4377) "[" is not valid in URLs +* (bug 4453) fix for __TOC__ dollar-number breakage +* Convert unnecessary URL escape codes in external links to their equivalent + character before doing anything with them. This prevents certain kinds of + spam filter evasion. +* (bug 4783) : Fix for "{{ns:0}} does not render" +* Improved support for interwiki transclusion +* (bug 1850) Image link to nonexistent file fixed. +* (bug 5167) Add {{SUBPAGENAME}} and {{SUBPAGENAMEE}} variables +* (bug 4949) Missing : in "addedwatchtext" for English and Spanish +* Allow user-defined functions, which work in a similar way to {{GRAMMAR:}} + etc. Registered via an interface similar to tag hooks. + +Upload: +* (bug 2527) Always set destination filename when new file is selected +* (bug 3076) Support MacBinary-encoded uploads from IE/Mac +* (bug 2554) Tell users they are uploading too large file +* Support for a license selection box on Special:Upload, configurable from MediaWiki:Licenses +* Add 'reupload' and 'reupload-shared' permission keys to restrict new uploads + overwriting existing files; default is the old behavior (allowed). + +Security: +* (bug 3244) Fix remote image loading hack, JavaScript injection on MSIE +* (bug 3280) Respect 'move' group permission on page moves +* (bug 2613) Clear saved passwords from the form +* IP privacy fix for blocklist search on autoblocks +* Security fix for <math> +* Security fix for tables +* Security fix for Special:Upload license selection list +* Add UploadVerification hook for custom file upload validation/security checks +* Blacklist additional MSIE CSS safety tricks +* Fix meta robots tag on Special:Version again to avoid listing vulnerable + versions for convenient harvesting by automated worms +* Sanitizer CSS comment processing order fix +* Forbid usernames that can be interpreted as titles with namespaces, as that + leads to hard-to-manage names. +* (bug 4071) Generate passwords long enough for $wgMinimalPasswordLength +* Add createpage and createtalk permission keys, allowing a quick + switch to disable page creation for anonymous users. +* (bug 675) Add page protection level for unregistered/new accounts +* User::isNewbie now uses the registration date and $wgAutoconfirmAge +* Add 'deletedhistory' permission key for ability to view deleted history + list via Special:Undelete. Default is off, replicating the 1.5 behavior, + but it can be turned back on for random users to replicate the previous + 1.6 dev behavior. +* Set cookies to secure mode based on use of HTTPS or $wgCookieSecure +* (bug 4371) Disallow tilde character in signatures +* Removed broken wgAllowAnonymousMinor and added new group right minoredit +* Added detection for WMF files (application/x-msmetafile), added this + MIME type to the default blacklist. Prevented inline display of images + which are not of known image types. This is in response to + http://en.wikipedia.org/wiki/Windows_Metafile_vulnerability +* Blocked users can no longer roll back, change the protection of, or delete/undelete pages +* Protect against spoofing of X-Forwarded-For header +* XSS issue : now sanitize search query input (fixed in 1.5rc3) +* Remove deprecated $wgOnlySysopsCanPatrol references; use User::isAllowed( 'patrol' ) + per bug 5282. Patch by Alan Harder. +* Prevent registration/login with the username "MediaWiki default" + +Special Pages: +* Rearranged Special:Movepage form to reduce confusion between destination + title and reason input boxes +* (bug 1956) Hide bot uploads from Special:Newimages +* (bug 3220) Fix escaping of block URLs in Recentchanges +* (bug 3284) Ipblocklist paging, substring search +* Allow filtering of robot edits in Special:Watchlist by stting + $wgFilterRobotsWL = true. +* Fix interlanguage links on special pages when extra namespaces configured +* (bug 3475) anon contrib links on Special:Newpages +* Special:Import/importDump fixes: report XML parse errors, accept <minor/> +* (bug 2369) Add separate message for input box on Special:Prefixindex +* (bug 3798) DoubleRedirects no longer has hard coded arrows +* (bug 3803) Fix links on Special:Wantedcategories with miser mode off +* Fix Special:BrokenRedirects on MySQL 5.0 +* (bug 3807) Fix 'all' in namespaces drop-down on contribs, rc +* Fail gracefully on invalid namespace in Special:Newpages +* (bug 3762) Define missing Special:Import UI messages +* (bug 3761) Avoid deprecation warnings in Special:Import +* (bug 2894) Enhanced Recent Changes link fixes +* (bug 4059) fix 'hide minor edits' on Recentchangeslinked +* (bug 146) List number of category members in Special:Categories + (patch by Joel Nothman) +* (bug 4090) Fix diff links in Special:Recentchangeslinked +* (bug 4093) '&bot=1' in Special:Contributions now propagate to other links +* Fix display of old recentchanges records for page moves +* (bug 360) Let Whatlinkshere track [[:image:foo]] links +* (bug 3073) Keep search parameter on paging in Special:Newimages +* Removed Special:Validate, it's been superseded by the Review extension +* (bug 4359) red [[user:#id]] links generated in [[special:Log]] +* (bug 1996) Special page to list redirects +* (bug 4334) Add "watch" links to Special:Unwatchedpages +* Generate target user page links in Special:Ipblocklist where appropriate + (i.e. not an autoblock) +* Generate link to talk page of the blocker in Special:Ipblocklist, move + contribs. link of the target next to their name +* (bug 2714) Backlink from special:whatlinkshere was hard set as 'existing' +* Move parentheses out of <a> link in Special:Contributions +* (bug 3192): properly check 'limit' parameter on Special:Contributions +* (bug 3187) watchlist text refer to unexistent "Stop watching" action +* Add block, block log and general log links to Special:Contributions +* Add contributions link to block log items +* Added optional "hide own edits" feature to Special:Recentchanges +* (bug 5018) Anchors for each message in Special:Allmessages +* Introduce $wgWantedPagesThreshold per bug 5011; Special:Wantedpages will not + list pages with less than this number of links. Defaults to 1. +* (bug 4319) Don't show a "create account" link on the login form when + account creation is disabled. +* JavaScript filter for Special:Allmessages +* (bug 3047) Don't mention talk pages on Special:Movepage when there isn't one +* Show links to user page, talk page and contributions page on Special:Newpages +* Special:Export can now export a list of all contributors to an article (off by default) +* (bug 5372) Add number of files to Special:Statistics +* (bug 2871) Links to talk pages in watchlist editing view +* (bug 5385) Allow hiding anonymous edits on Special:Recentchanges +* (bug 2544) Illogical error reporting order in Special:Userlogin +* (bug 5409) Hide "show/hide patrolled edits" in Special:Recentchanges if patrolling + is disabled +* (bug 5447) Convert first letter of username to uppercase before searching in Special:Listusers +* (bug 759) Wrap redirects on the watchlist editing page in a span, class "watchlistredir" +* (bug 1862) Namespace filtering in watchlists + +Misc.: +* PHP 4.1 compatibility fix: don't use new_link parameter to mysql_connect + if running prior to 4.2.0 as it causes the call to fail +* (bug 3117) Fix display of upload size and type with tidy on +* (bug 2323) Remove "last" tabindex from history page +* (bug 3116) Division by zero on [[Image:Foo.png|123x123px|]] +* Fix display of read-only lockfile message +* Include software-visible client IP address in Special:Version comment + as a proxy debugging aid +* (bug 3170) Page Title failed to obey MediaWiki:Pagetitle. + wikititlesuffix was removed +* Add ability to break off certain debug topics into additional log files; + use $wgDebugLogGroups to configure and wfDebugLog() to log. +* Edit conflict on recreation of deleted page +* (bug 3216) Don't show empty warning page when no warnings. +* (bug 3218) Use proper quoting on history Compare Revisions button +* Fix upgrade from 1.4 due to version number check breakage [for rc future] +* Fix upgrade from 1.4 with no old revisions +* Remove "info" editing toolbar that was shown in browsers which do not +fully support the editing toolbar, but was found to be too confusing. +* Don't override edit conflict suppression on section edits; section merging + should provide the expected transparency here and fits usage patterns better. +* (bug 3292) Fix move-over-redirect test when current entries are not plaintext +* (bug 2078) Don't hide watch tab on preview +* Fix regressions in ChangesList traditional layout +* Fix edit on double-click for move-protected pages in Classic skin +* (bug 3485) Fix bogus warning about filename capitalization when off +* (bug 2570) Add 'watch this page' checkbox on uploads, watch uploads + by default when 'watchdefault' option is on +* Add options to dumpBackup.php for making split/partial dumps by page id +* Added filter options, compression piping, and multiple output streams for + dumpBackup.php +* (bug 3595) Warn and abort if importDump.php called in read-only mode. +* (bug 3598) Update message cache on message page deletion, patch by Tietew +* Added separate noarticletext and newarticletext messages for logged in and anon users. +* (bug 3332) Installation now uses Monobook, validates, plus usability improvements. +* (bug 3660) Update diff3 detection to work with Windows/Cygwin +* (bug 2330) Don't do funny thinks with "links" in MediaWiki:Undeletedtext +* Two-pass data dump for friendliness to the DB (--stub, then dumpTextPass.php) +* Data dump 'prefetch' mode to read normalized text from a prior dump + (requires PHP 5, XMLReader extension) +* (bug 2773) Print style sheet no longer overrides RTL text direction +* (bug 2938) Update MediaWiki:Exporttext to be more general +* Various fixes +* Fix wfMsg*() replacements; args containing literal $[2-9] were wiped +* Added @import for [[MediaWiki:Common.css]] to all skins +* Edit box now remembers scrollbar position on preview +* (bug 3816) Throw edit conflict instead of fatal error when a page is + moved or deleted during section edit +* (bug 3771) Handle internal functions in backtrace in wfAbruptExit() +* (bug 3291) 'last' diff link for last history line when not at end +* (bug 3667) Add missing global in page move code +* (bug 2885) Remove unnecessary reference parameter which broke classic skin + talk notification on PHP 5.0.5 +* (bug 3852) "Redirected from" link no longer obscured on double-redirects +* changed directory hierarchy in images/math/. System upgrades from old to + new hierarchy on the fly. +* (bug 3487) Fix category edit preview with preview-on-bottom +* (bug 918) Search index incorrectly joined words at == headings == +* (bug 3877) Render math images into temp directory, then move to hashed + subdir so you can render new math images and have them work +* (bug 2392) Fix Atom items content type, upgrade to Atom 1.0 +* Allow $wgFeedCacheTimeout of 0 to disable feed caching +* Fix WebRequest::getRequestURL() to strip off the host bits squid prepends +* Require POST for action=purge, to stop bots from purging the cache +* Added local message cache feature ($wgLocalMessageCache), to reduce bandwidth + requirements to the memcached server. +* (bug 3562) for go search, try Caps-Variants-Broken-At-Non-Whitespace +* (bug 2569) Use PATH_SEPARATOR instead of trying to guess based on + DIRECTORY_SEPARATOR (was wrong on NetWare) +* (bug 2740) Accept image deletions on 'enter' submit from MSIE +* (bug 3939) Don't try to load text for interwiki redirect target +* (bug 3948) Avoid notice warning in debug statement in bad search +* Recognize Special:Search consistently so read whitelist works +* (bug 3999) Change atom 1.0 feed id; had been unnecessarily complex due to + unclear language in the spec. Now using the URL, same as the permalink, + which someone else will probably whine about because it's not 'perma' + enough or something. +* (bug 4014) Fix include mode for Allpages on small page sets +* (bug 3996) Fix text for new entries in RC RSS/Atom feed +* (bug 3065) Update both watched namespaces when renaming pages +* Changed mail form to have a bigger message entry box (like for editing + a page +* Fix ulimit parameters for wfShellExec when memory_limit is specified in 'm' +* (bug 2111) Collapsable exif metadata table, clean up display +* Reduce fractions in display of exif exposure time +* (bug 4048) Optional footer link to site privacy policy +* Don't die() when update.php reaches the end of the warning count +* (bug 1915) Fix edit links when 'direction' used with 'oldid'; + using revision ID reported via OutputPage; Skin::editUrlOptions() +* Remove obsolete 'redirect=no' on some edit links +* Include oldid for the second revision on edit link on diff view +* (bug 4035) Fix prev/next revision links on edit page +* (bug 4100, 3049) Add 'edittools' message to hold edit tools, put it + on Special:Upload as well as edit, rearrange edit page pieces a bit. + Copyright warning now above the buttons to ensure it's visible, + template list at the bottom so it can grow. +* Optional summary parameter to action=rollback, for user javascript +* (bug 4167) Fix regression caused by patch for bug 153 +* (bug 4169) Use $wgLegalTitleChars in pipe trick conversions +* (bug 4170) Decode HTML character escapes in sort key +* (bug 4201) Fix user-talk mode for Enotif, and general code cleanup +* (bug 4214) Skip redundant action text inserts into the HTML <title> +* (bug 4212) Skip redundant meta-robots tag for default settings +* Fix regression: old version missing from edit links in Nostalgia skin +* (bug 1600) Trigger edit conflict on duplicate section=new submissions +* (bug 4001) Use local variables properly in wikibits.js akeytt() +* Fix regression: old version missing from edit links on CSS/JS pages +* (bug 3211) Include Date, To mail headers when using PEAR::Mail +* (bug 3407) Fix encoding of subject and from/to headers on notification + mails; userMailer() now takes a MailAddress wrapper object instead of + a raw string to abstract things a level. +* Fixed --server override on dumpTextPass.php +* Added plugin interface for dumpBackup, so additional filters and output + sink types can be registered at runtime from an extension +* (bug 349) Fix for some numeric differences not being highlighted + patch by Andrius Ramanauskas +* (bug 4298) Include rc_id on enhanced RC singleton diff links for patrolling +* Did some refactoring on ChangesList.php merging dupe code +* (bug 1586) Fix interwiki generator for wikimedia obscure domains +* (bug 3493) Mark edits patrolled when they are reverted + patch by Leon Planken +* Removed experimental Amethyst skin from default set +* Upgrade old skin preferences properly at Special:Preferences + (used to spontaneously switch to Classic skin for old numeric pref records) +* (bug 3424) Update page_touched for category members on category page creation +* Log views show message when no matches +* Fix raw sitenotice display on database error +* Fix autoconfirm check for old accounts +* (bug 4368) Don't show useless empty preview on new section creation +* Don't show useless empty preview on new page creation +* (bug 4411) Fix messages diff link for classic skin +* (bug 4385) Separate parser cache entries for non-editing users, so section + edit links don't vanish / appear unwanted on protected pages +* (bug 2726, 3397) Fix [[Special:]] and [[:Image]] links in action=render +* (bug 4419) Remove obsolete magnify.png.old +* Removed $wgUseCategoryMagic option, categories are now enabled unconditionally +* (bug 3318) UI workarounds for disabled items in license selector + MSIE/Win: items now grayed out, JS will revert to 'non selected' if clicked + Safari: JS will revert to 'non selected' if clicked (but not gray) + MSIE/Mac: indented items now visible (JS hack) +* (bug 714) "plainlinks" class issues in IE, Opera +* (bug 4317) Inconsistent "broken redirects" messages +* Default interface text for "selflinks" tweaked +* (bug 3194) default implementation of translateBlockExpiry + which uses ipboptions +* (bug 4446) $wgExportAllowHistory option to explicitly disable history in + Special:Export form, 'exportnohistory' message to translate live hack. +* Maintenance script to delete unused user accounts +* (bug 912) Search box easier to reach in text browsers (lynx, links) +* $wgParserCacheExpireTime added +* Skip loading of RecentChange.php except where needed +* Enforce $wgSVGMaxSize when rendering, even for SVGs with a very large source + size. This is necessary to limit server memory usage. +* Cleanup and error checking on Special:Listredirects +* Clear up some instances of old OutputPage::sysopRequired() function usage +* Improve "upload disabled" notice +* Move parts of index.php to include/Wiki.php in an attempt to both cleanup index.php + and create a MediaWiki-class mediaWiki base object +* (bug 4104) Added OutputPageBeforeHTML hook for tweaking primary wiki output + HTML on final output (cached or not) +* Avoid PHP notice on command-line scripts if empty argument is passed ('') +* (bug 4571) Partial fix hack for {{fulllurl:}} in action=render +* (bug 3502) Bowtie symbol for TeX +* (bug 4000) Support for \textstyle et al. in <math> +* (bug 1663) support color in TeX formulas +* (bug 2026) missing glue around \not= (TeX) +* (bug 4576) Missing '>' broke license selector's first option in IE, Opera +* Override $wgLocaltimezone in parser tests for us outside Iceland and UK +* Fix extra whitespace at end of Wiki.php, DESTROYS XML OUTPUT +* Remove redundant 'echo' statements from MonoBook.php +* (bug 1103) Fix up redirect handling for images, categories + Redirects are now followed from the top-level, outside of the Article + content loading and viewing, for clarity and consistency. +* (bug 4104) 'OutputPageBeforeHTML' hook to postprocess article HTML on + page view (comes after parser cache, if used). Patch by ThomasV. +* Linker::formatComment corrupted the passed title object on PHP 5 + if the comment included a section link. Use clone() to make a safe copy. +* Add wfClone() wrapper since we're still using PHP 4 on some servers. +* Remove obsolete killthread.php +* Added wfDie() wrapper, and some manual die(-1), to force the return code + to the shell to return nonzero when we crap out with an error. +* Allow input of the stub from a compressed file instead of stdin + for dumpTextPass.php; easier to get errors back on the shell +* Added an attractive space on the namespace selector on contribs +* Move PHP 5-friendly XHTML doctype hack to Sanitizer, use for sig checks. + Fixes use of named entities in sigs on PHP 5 +* (bug 4482) Include move comment on the null edit as well as the redirect +* (bug 3990) Use existing session name if session.auto_start is on + Fixes checks for open sessions, such as the cookie warning on login. + Patch by Zbigniew Braniecki. +* Add cache-safe alternate sitenotice for anonymous users. (MediaWiki:Anonnotice) + This is displayed instead of the regular sitenotice, if it exists. If not, the + regular sitenotice shows. If that doesn't exist, the value of $wgSiteNotice is used, + and if that's null, then nothing is shown. +* Spit the generated LocalSettings code out during the installer as an aid + to debugging issues. (Keep this?) +* Use __FILE__ to form path in new LocalSettings.php, so it stays accurate + when the directory is relocated for typical usage. +* Auto-update $wgCacheEpoch when LocalSettings.php changes on new installs. + For typical usage this will be a light burden and should reduce confusion + when the configuration is edited. +* Fix $wgCacheEpoch's effect on client-side caching. +* (bug 1122) gray out 'older revision' when viewing first article revision. +* Clearer message in DefaultSettings.php: edit LocalSettings.php instead +* MonoBook skin top link id changed from "contentTop" to "top" (shared with + name attribute) +* (bug 3350) Missing label for move talk page checkbox. +* (bug 2108) Sort entries when using category browser +* (bug 2393) Fix MIME type for Atom feeds ( application/rss+atom ) +* Add ".deps.php" include-file preloaders for some dynamically-loaded + language and skin classes. Should help with the broken base-class + problem under PHP 5 with APC as opcode cache. See details: + http://mail.wikipedia.org/pipermail/wikitech-l/2006-January/033660.html +* Small changes to tabs in Monobook skin c/o Chris Ware +* (bug 4679) Work around buggy basename() function in PHP5, which breaks + uploads of files starting with multibyte characters on Linux. + wfBaseName() doesn't suffer this bug, and understands backslash on + both Unix and Windows. +* (bug 3603) headscripts variable not hooked up to MonoBook skin +* Allow local cdb-based interwiki cache +* Use the "block", not the "protect" permission, when determining whether to + show a "block user" link in the toolbox +* Fix backup dump text prefetch for XMLReader constant changes in PHP 5.1 +* Suppress useless percentage indicator on output from 7za during dumps +* (bug 4633) Add (previous 200) (next 200) also above catlinks +* (bug 4686) Fix regression where ?diff=0&oldid=0 caused fatal error on + pages with only one revision. Fixes message diff link on first edit. +* Fix dependence on hardcoded UNIQ_PREFIX in LanguageConverter.php +* Do not check lag on external storage servers +* Do not tidy interface messages (unless full tidy is set) +* Do not trust equality propagation and give more hints to MySQL + optimizer for revision fetches (avoids index scans) +* Use revision rate for ETA in dump generation; it tends to be more stable + than the per-page count for full-history dumps. +* Include timestamp in wfDebugLog breakouts +* (bug 4469) Namespace-specific notice to be displayed below site-notice + Edit messages like "MediaWiki:Namespacenotice-" plus namespace name + which is blank for main namespace, or like e.g. "User_talk" +* Adjust user login/creation form hooks to work with a captcha plugin +* (bug 1284) Inline styles for diffs in Recent Changes RSS/Atom feeds +* (bug 4824) IE7 beta 2 broke compatibility with PNG logo workarounds, + and seems to work ok with other bits. No longer including the IE + workarounds JavaScript for IE 7 and above. +* Fix extra namespace for Bulgarian +* (bug 4303) Add $wgFavicon to change the shorticon icon link from + the default /favicon.ico or disable it (if set to false) +* (bug 3347) strip linebreaks in math error source +* (bug 4841) Warning for non-logged-in edits +* (bug 4867) Leave invalid EXIF date fields unformatted instead of + showing a bogus current timestamp +* Reset $wgActionPaths during parser test; corrects some false failures + in the automated test report. +* (bug 4875) Define a div containing the shared image description +* (bug 4860) Expose Title->userCan() as Hooks +* (bug 4828) Fix genitive month-name variable for cs, pl, uk +* (bug 4842) Fix 'show number of watching users' with enhanced RC +* (bug 4889) Fix image talk namespace for Tamil +* (bug 4147) Added cleanupWatchlist.php to clear out bogus watchlist entries +* (partial bug 3456) Disable auto redirect to Main Page after account creation +* (bug 4824) Separate out IE7 CSS compat hacks, fix for RTL pages +* Added support for wikidiff2 and similar external diff engines. +* Allow cookies to be shared between multiple wikis with a shared user database +* Blocking some Unicode whitespace characters in usernames. Should check + if some or all should be blocked from all page titles. +* Unknown log types no longer throw notices everywhere in RecentChanges +* (bug 4502, 5017) Don't render potentially hostile deleted page contents + on Special:Undelete by default; show source, with an optional preview. + The revisions list no longer shows the latest text by default, so it can + still be operated if the text is hostile. +* (bug 5013) Check for existence on "return to" links +* Removed trailing whitespace on a bunch more messages. +* Fix missing bad title check in Special:Booksources +* Remove empty booksources string in fy +* Avoid corrupting <gallery> inside <!-- comment --> +* Remove legacy PHPTal code, hasn't been maintained in ages. +* Tweak Userlogin include order for APC issue +* Don't try to link to current page on protection tab +* More exact checking in Title::equals() to fox moves of numerically similar + page titles. (Odd hex title bug on 64-bit.) +* Fix explicit s-maxage=0 on raw pages; should help with proxy issues in + generated stylesheets... hopefully... +* (bug 4685) More fixes for Slovenian project namespace +* Fixed and enhanced a little the Live Preview, which had been broken for some time +* Added article size limit, $wgMaxArticleSize +* (bug 4974) Don't follow redirected talk page on "new messages" link +* (bug 4970) Make category paging limits configurable +* (bug 4535) Warn user when editing CSS or JS subpage of a skin that doesn't exist +* Make Live Preview an user preference, still controllable by the global variable +* Rename the stub LanguageAls / LanguageGem_alsation to LanguageGsw to follow + updated language code assignments +* (bug 5081) Remove bogus fix for invalid characters in links which simply + broke use of legitimate multiple whitespace characters in bracketed link. +* (bug 4838) Add relative oldids (prev, next, cur) for raw pages + Patch by Lupin +* (bug 5086) Force image resize dimensions on ImageMagick, as for instance + "-resize 100x35!"; some thumbs were off due to differences in rounding and + would be generated smaller than expected. +* (bug 5062) Width sometimes one pixel short when using maximum heights +* Purge thumbnails and metadata cache for action=purge on an image page +* (bug 4273) Bounce back with a message when attempting to submit a new comment + with an empty main textbox (user probably hit Enter in subject field) +* (bug 5141) Gracefully handle the new account link when createaccount off +* (bug 5150 and related) Fix missing ID attribute in HTML namespace selector +* (bug 5152) Proper HTML escaping on subpage breadcrumbs +* (bug 4855) Section edit links now have the section name in the title attribute. +* (bug 2115) Support shift-selecting multiple checkboxes with JavaScript. +* (bug 5161) Don't try to load template list for nonexistent pages +* (bug 5228) Workaround for broken LanguageConverter title overrides; avoid + unnecessary hidden UI work when watch/unwatch is performed on edit +* Fixed bogus master fallback in external storage +* (bug 5246) Add speak:none to "hiddenStructure" class in main.css +* Further work on rev_deleted; changed to a bitfield with several data-hiding + options. Not yet ready for production use; Special:Revisiondelete is + incomplete, and the flags are not preserved across page deletion/undeletion. + To try it; add the 'deleterevision' permission to a privileged group. +* (bug 5270) Fix broken linktrail for br, cv, fr, hr, nn, oc, ta, wa +* Add a clickable contribs link in user tool links (rc, watchlist, diff view) + to see how people like it. (There was one in the old hacked-up diff view.) +* (bug 5236) Load wikibits.js before site-customized javascript +* (bug 4119) Workaround for <nowiki> following link in Walloon; remove capitals + from linktrail, as they're not used anywhere else. +* (bug 4781) Output links with the percent-encoding they're supplied with; + save the normalization for internal link storage. The normalization is a bit + buggy and can make incorrect foldings in the query string and such, so isn't + reliable beyond the hostname where it's used for the spam bulk checker. +* Don't URL-decode in the title attribute for URL links; it can produce false + results that don't code back to their original values. +* (bug 4611) Add user preference (default on) to add new pages to creators's watchlist +* (bug 5286) Fix regression in display of missing/bad revision IDs +* (bug 4729) Add user preference that marks a user's edits as patrolled if user is able to +* (bug 4630) Add user preference to prompt users when entering blank edit summaries +* Added optional suggest feature for the search box. Set wgUseAjax to true to + enable it. +* (bug 5277) Use audio/midi rather that audio/mid +* (bug 5410) Use namespace name when a custom namespace's nstab-NS message is nonexistent +* (bug 5432) Fix inconsistencies in cookie names when using table prefixes +* Additional protections against HTML breakage in table parsing +* (bug 5355) Include skin name and style JS settings in page source; + fixes regression where Opera 6/7 and KHTML CSS fixes weren't applied + when wikibits.js was moved up before user JS inclusion. +* Added $wgColorErrors: if set, database error messages will be highlighted + when running command-line scripts in a Unix terminal. +* (bug 5195) rebuildrecentchanges.php works again; Database::insertSelect now + has a parameter for select options. +* Fix updateSearchIndex.php for new schema +* Fix bogus "filename too short" error when uploading files with a period in the base + name, e.g. "Mr. Zee.png" +* (bug 2139) Show page title in subtitle when viewing "read only" page +* (bug 5452) Update language name for Cree + + + +---- + +== MediaWiki 1.5.8 == + +March 26, 2006 + +MediaWiki 1.5.8 is a security and bugfix maintenance release. + +A bug in decoding of certain encoded links could allow injection of raw +HTML into page output; this could potentially lead to XSS attacks. + +Some minor UI fixes were also made, see the change log at the bottom of +this file. + + +== MediaWiki 1.5.7 == + +March 2, 2006 + +MediaWiki 1.5.7 is a bugfix maintenance release. + +Most importantly, a security issue in the installer has been fixed. The bug +affects new installations of 1.5.6 only. If the user specified the MySQL root +password, to allow the installer to create an unprivileged account, the +installer would not only create the new account but also change the root +password to be equal to the password of the new account. + +Anyone affected by this bug will need to change the root password back +manually. For information about how to change passwords in MySQL please see: +http://dev.mysql.com/doc/refman/5.1/en/passwords.html + +This version includes fixes for compatibility with Internet Explorer 7 +beta 2, and various other bugs; see the full changelog at the end of +the release notes. + + +== MediaWiki 1.5.6 == + +January 19, 2006 + +MediaWiki 1.5.6 is a security and bugfix maintenance release. + +A bug in edit comment formatting could send PHP into an infinite loop +if certain malformed links were included. In most installations, this +would cause the script to fail after PHP's 30-second failsafe timeout. + +Some improvements have been made to the installer which should make +installation possible on a system with a broken MySQL "root" account. + +For several other minor fixes, see the complete changelog at the end +of this file. + + +== MediaWiki 1.5.5 == + +January 5, 2006 + +MediaWiki 1.5.5 is a security and bugfix maintenance release. + +Detection for uploads of Windows Metafile (.wmf) images has been added +to help protect against a client-side vulnerability in unpatched Microsoft +Windows operating systems. + +Sites which have enabled uploads and added non-standard file types +(such as .ogg, .doc, or .pdf) should upgrade to this release to ensure +that malicious .wmf files can't be uploaded with a fake extension; +such files could put visitors to the site at risk. + +For more details on this, see: +http://en.wikipedia.org/wiki/Windows_Metafile_vulnerability + +Additionally, a maintenance script removeUnusedAccounts.php has been added; +this replaces an older Perl script which had not been updated for the new +schema in 1.5. + + +== MediaWiki 1.5.4 == + +December 21, 2005 + +MediaWiki 1.5.4 is a security and bugfix maintenance release. + +A hardcoded internal placeholder string has been replaced with a random +one. This closes a hole where security checks in inline style attributes +could be bypassed, injecting JavaScript code that could execute in +Microsoft Internet Explorer. + +Other browsers would not be vulnerable. + +Several minor fixes are included in this release, most notably a fix +to clear the "you have new messages" flag properly for usernames +containing spaces when e-mail notification is enabled. + +See the changelog at the end of the release notes for a full list of +fixes. + + +== MediaWiki 1.5.3 == + +December 4, 2005 + +MediaWiki 1.5.3 is a security and bugfix maintenance release. + +Validation of the user language option was broken by a code change in +May 2005, opening the possibility of remote code execution as this +parameter is used in forming a class name dynamically created with +eval(). + +The validation has been corrected in this version. All prior 1.5 release +and prelease versions are affected; 1.4 and earlier and not affected. + +Additionally several bugs have been fixed; see the changelog later in +this file for a complete list. + + +== MediaWiki 1.5.2 == + +November 2, 2005 + +MediaWiki 1.5.2 is a bugfix maintenance release. + +A change in PHP 4.4.1 and PHP 5.1.0RC broke handling of extension and +<pre> sections, causing garbage data to be inserted in output and saved +edits. This version works around the change. + +Several other glitches with MySQL 5.0 and PHP 5.0.5 were also fixed; +see the change log below for a complete list. + + +== MediaWiki 1.5.1 == + +October 26, 2005 + +MediaWiki 1.5.1 is a bugfix and security maintenance release, and is a +recommended upgrade for all installations. + +This release includes further corrections to the inline CSS style sanitation +which works around a JavaScript "feature" on Microsoft Internet Explorer. +Users of Microsoft Internet Explorer for Windows may be vulnerable to +XSS injections on prior versions; users of standards-compliant browsers +are not vulnerable. + +Major fixes include: +* Image pages work again with resizing disabled +* Works in MySQL 5.0 strict mode + +There is experimental support in this release for explicitly declaring +the UTF-8 charset in the database; this has been tested with MySQL 5.0.15 +but should work on 4.1 as well. + +IMPORTANT: Changing this setting on an existing wiki may produce interesting +data corruption, depending on server configuration. Page contents should, +usually, be unaffected, but page titles and other items may be. Limitations +in MySQL's Unicode support mean that characters outside the BMP cannot be used +in page titles or various other fields when using this mode. + +Table definitions are in maintenance/mysql5/tables.sql, and the runtime +option to send 'SET NAMES utf8' is set by $wgDBmysql5 = true. + +(MySQL 3.23.x and 4.0.x do not support character set declarations; on these +versions MediaWiki simply works with UTF-8 data and MySQL is blissfully +unaware of it.) + + + +== MediaWiki 1.5.0 final == + +October 5, 2005 + +MediaWiki 1.5.0 is the new stable release branch of MediaWiki, and is +recommended for all new installations. + +Any wikis running a 1.5 beta or release candidate are strongly recommended +to upgrade to the final release, which includes a number of bug fixes and +a security fix for CSS bugs in Microsoft Internet Explorer. + +IMPORTANT: Running a 1.3 or 1.4 wiki and don't want to jump to 1.5 yet? +Be sure to upgrade to 1.3.17 or 1.4.11, also released today. Versions +prior to 1.3.16 and 1.4.10 have a serious data corruption bug which is +triggered by a spambot known to operate in the wild. + + +=== What's new in 1.5? === + +Schema: + The core table schema has changed significantly. This should make better + use of the database's cache and disk I/O, and make significantly speed up + rename and delete operations on pages with very long edit histories. + + Unfortunately this does mean upgrading a wiki of size from 1.4 will require + some downtime for the schema restructuring, but future storage backend + changes should be able to integrate into the new system more easily. + +Permalinks: + The current revision of a page now has a permanent 'oldid' number assigned + immediately, and the id numbers are now preserved across deletion/undeletion. + A permanent reference to the current revision of a page is now just a matter + of going to the 'history' tab and copying the first link in the list. + +Page move log: + Renames of pages are now recorded in Special:Log and the page history. + A handy revert link is available from the log for sysops. + +Editing diff: + Ever lost track of what you'd done so far during an edit? A 'Show diff' + button on the edit page now makes it easy to remember. + +Uploads: + It's now possible to specify the final filename of an upload distinct + from the original filename on your disk. + + An image link for a missing file will now take you straight to the upload page. + + More metadata is pre-extracted from uploaded images, which will ease pressure + on disk or NFS volumes used to store images. EXIF metadata is displayed on + the image description page if PHP is configured with the necessary module. + + If .svg files are added to the upload whitelist, you can choose to render + them to rasterized .png images for inline display using one of several + external helper programs. See DefaultSettings.php for SVG options. + +User accounts: + There are some changes to the user permissions system, with assignable + groups. Note that this does *not* allow you to make pages which are only + accessible to certain groups. + + For details see: http://meta.wikimedia.org/wiki/Help:User_rights + +E-mail: + User-to-user e-mail can now be restricted to require a mail-back confirmation + first to reduce potential for abuse with false addresses. + + Updates to user talk pages and watchlist entries can optionally send e-mail + notifications. + +External hooks: + A somewhat experimental interface for hooking in an external editor + application is included. + +And... + A bunch of stuff we forgot to mention. + + +=== What's gone? === + +Latin-1: + Wikis must now be encoded in Unicode UTF-8; this has been the default for + some time, but some languages could optionally be installed in Latin-1 mode. + This is no longer supported. + + You can check if your current wiki is in Latin-1 mode by using your browser's + "view source"; look for a line like this: + + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + + If it says charset=utf-8, you're ready. If it says charset=iso8859-1, + you may need to convert your data. (English-language wikis avoiding + any accented characters may be able to get away without conversion.) + +MySQL 3.x: + Some optimization hacks for MySQL 3.x have been removed as part of the schema + clean-up (specifically, the inverse_timestamp fields). + + MediaWiki 1.5 may still run on 3.x, but wikis of non-trivial size should + very seriously consider upgrading to a more modern release. MySQL 3.x support + will probably be entirely dropped in the next major release. + +Special:Maintenance + These tools were, ironically enough, not really maintained. This special + page has been removed; insofar as some of its pieces were useful and haven't + already been supplanted by other special pages they should be rewritten in + an efficient and safe manner in the future. + + +=== Caveats === + +Upgrade: + Wikis in Latin-1 encoding are no longer supported; only Unicode UTF-8. + A new option $wgLegacyEncoding is provided to allow on-the-fly recoding of + old page text entries, but other metadata fields (titles, comments etc) need + to be pre-converted. The standard upgrade process does not yet fully automate + this, but you can try the alternate partial-upgrader in upgrade1_5.php. + + The upgrade from 1.4 to 1.5 schema has not been tested for all cases, so + it's possible you may experience problems in some combinations. + +Backups: + The text entries of deleted pages are no longer removed from the main + text table on deletion. If you provide public backup dumps of your databases, + you will probably want to use the new XML-format dump generator, available + as maintenance/dumpBackup.php. + + For more information on how we run our own public data dumps at Wikimedia, + see http://meta.wikimedia.org/wiki/Data_dumps + +PostgreSQL: + The table definitions for PostgreSQL install are out of date. PostgreSQL + support may return in later releases, pending appropriate patches. + +MySQL 4.1+: + Some users may encounter installation problems with MySQL 4.1 or higher + due to strange charset encoding / collation configurations. Try setting + to 'latin1' or 'utf8' if you encounter problems. + + + +== MediaWiki 1.5 release candidate 4 == + +August 29, 2005 + +MediaWiki 1.5rc4 is a preview release of the new 1.5 release series. +It fixes compatibility with PHP 5.1, and corrects two cross-site scripting +security bugs: + +* <math> tags were handled incorrectly when TeX rendering support is off, + as in the default configuration. +* Extension or <nowiki> sections in Wiki table syntax could bypass HTML + style attribute restrictions for cross-site scripting attacks against + Microsoft Internet Explorer + +Wikis where the optional math support has been *enabled* are not vulnerable +to the first, but are vulnerable to the second. + + + +== MediaWiki 1.5 release candidate 3 == + +August 24, 2005 + +MediaWiki 1.5rc3 is a preview release of the new 1.5 release series. +It fixes several major problems in 1.5rc2: + +* Fixed a cross-site scripting injection in the search form + (broken since 1.5beta1) + +* Fixed upgrades from 1.4 database schema + (broken since 1.5rc2) + +1.3 and 1.4 releases are not vulnerable to the XSS bug, but anyone +running an earlier 1.5 beta or release candidate should upgrade +immediately. + + +== MediaWiki 1.5 release candidate 2 == + +August 23, 2005 + +MediaWiki 1.5rc2 is a preview release of the new 1.5 release series. +Numerous bug fixes since last beta, plus a security fix; see change +log below for full details. + +A flaw in the interaction between extensions and HTML attribute +sanitization was discovered which could allow unauthorized use +of offsite resources in style sheets, and possible exploitation +of a JavaScript injection feature on Microsoft Internet Explorer. + +This version expands the returned text and properly checks it +before output. + +A 1.5rc1 release was mistakenly made from the incorrect source code +branch; 1.5rc2 is identical to the actual 1.5rc1 in revision control +except for version number. + + +== MediaWiki 1.5 beta 4 == + +July 30, 2005 + +MediaWiki 1.5 beta 4 is a preview release of the new 1.5 release series. +A number of bugs have been fixed since beta 3; see the full changelist below. + + +== MediaWiki 1.5 beta 3 == + +July 7, 2005 + +MediaWiki 1.5 beta 3 is a preview release of the new 1.5 release +series, with a security update over beta 2. + +Incorrect escaping of a parameter in the page move template could +be used to inject JavaScript code by getting a victim to visit a +maliciously constructed URL. Users of vulnerable releases are +recommended to upgrade to this release. + +Vulnerable versions: +* 1.5 preview series: n <= 1.5beta2 vulnerable, fixed in 1.5beta3 +* 1.4 stable series: 1.4beta6 <= n <= 1.4.5 vulnerable, fixed in 1.4.6 +* 1.3 legacy series: not vulnerable + +This release also includes several bug fixes and localization updates. +See the changelog at the end of this file for a detailed list. + + + +== MediaWiki 1.5 beta 2 == + +July 5, 2005 + +MediaWiki 1.5 beta 2 is a preview release of the new 1.5 release series. +While most exciting new bugs should have been ironed out at this point, +third-party wiki operators should probably not run this beta release +on a public site without closely following additional development. + +Anyone who _has_ been running beta 1 is very very strongly advised to +upgrade to beta 2, as it fixes many bugs from the previous beta including +a couple of HTML and SQL injections. + +This release should be followed by one or two release candidates and +a 1.5.0 final within the next few weeks. + +Beta upgraders, note there are some minor database changes. For upgrades +from 1.4, see the file UPGRADE for details on significant database and +configuration file changes. + +Beta 2 includes a preliminary command-line XML wiki dump importer tool, +maintenance/importDump.php, paired with maintenance/dumpBackup.php. +These use the same format as Special:Export and Special:Import, able +to package a wiki's entire page set independent of the backend database +and compression format. + + +== MediaWiki 1.5 beta 1 == + +June 26, 2005 + +MediaWiki 1.5 beta 1 is a preview release, pretty much feature complete, +of the new 1.5 release series. There are several known and likely a number +of unknown bugs; it is not recommended to use this release in a production +environment but would be recommended for testing in mind of an upcoming +deployment. + +A number of significant changes have been made since the alpha releases, +including database changes and a reworking of the user permissions settings. +See the file UPGRADE for details of upgrading and changing your prior +configuration settings for the new system. + + + +== MediaWiki 1.5 alpha 2 == + +June 3, 2005 + +MediaWiki 1.5 alpha 2 includes a lot of bug fixes, feature merges, +and a security update. + +Incorrect handling of page template inclusions made it possible to +inject JavaScript code into HTML attributes, which could lead to +cross-site scripting attacks on a publicly editable wiki. + +Vulnerable releases and fix: +* 1.5 prerelease: fixed in 1.5alpha2 +* 1.4 stable series: fixed in 1.4.5 +* 1.3 legacy series: fixed in 1.3.13 +* 1.2 series no longer supported; upgrade to 1.4.5 strongly recommended + + +== MediaWiki 1.5 alpha 1 == + +May 3, 2005 + +This is a testing preview release, being put out mainly to aid testers in +finding installation bugs and other major problems. It is strongly recommended +NOT to run a live production web site on this alpha release. + +** WARNING: USE OF THIS ALPHA RELEASE MAY INFEST YOUR HOUSE WITH ** +** TERMITES, ROT YOUR TEETH, GROW HAIR ON YOUR PALMS, AND PASTE ** +** INNUENDO INTO YOUR C.V. RIGHT BEFORE A JOB INTERVIEW! ** +** DON'T SAY WE DIDN'T WARN YOU, MAN. WE TOTALLY DID RIGHT HERE. ** + + +=== Smaller changes since 1.4 === + +Various bugfixes, small features, and a few experimental things: + +* 'live preview' reduces preview reload burden on supported browsers +* support for external editors for files and wiki pages: + http://meta.wikimedia.org/wiki/Help:External_editors +* Schema reworking: http://meta.wikimedia.org/wiki/Proposed_Database_Schema_Changes/October_2004 +* (bug 15) Allow editors to view diff of their change before actually submitting an edit +* (bug 190) Hide your own edits on the watchlist +* (bug 510): Special:Randompage now works for other namespaces than NS_MAIN. +* (bug 1015) support for the full wikisyntax in <gallery> captions. +* (bug 1105) A "Destination filename" (save as) added to Special:Upload Upload. +* (bug 1352) Images on description pages now get thumbnailed regardless of whether the thumbnail is larger than the original. +* (bug 1662) A new magicword, {{CURRENTMONTHABBREV}} returns the abbreviation of the current month +* (bug 1668) 'Date format' supported for other languages than English, see: + http://mail.wikipedia.org/pipermail/wikitech-l/2005-March/028364.html +* (bug 1739) A new magicword, {{REVISIONID}} give you the article or diff database + revision id, useful for proper citation. +* (bug 1998) Updated the Russian translation. +* (bug 2064) Configurable JavaScript mimetype with $wgJsMimeType +* (bug 2084) Fixed a regular expression in includes/Title.php that was accepting invalid syntax like #REDIRECT [[foo] in redirects +* It's now possible to invert the namespace selection at Special:Allpages and Special:Contributions +* No longer using sorbs.net to check for open proxies by default. +* What was $wgDisableUploads is now $wgEnableUploads, and should be set to true if one wishes to enable uploads. +* Supplying a reason for a block is no longer mandatory +* Language conversion support for category pages +* $wgStyleSheetDirectory is no longer an alias for $wgStyleDirectory; +* Special:Movepage can now take paramaters like Special:Movepage/Page_to_move + (used to just be able to take paramaters via a GET request like index.php?title=Special:Movepage&target=Page_to_move) +* (bug 2151) The delete summary now includes editor name, if only one has edited the article. +* (bug 2105) Fixed from argument to the PHP mail() function. A missing space could prevent sending mail with some versions of sendmail. +* (bug 2228) Updated the Slovak translation +* ...and more! + + +=== Changes since 1.5alpha1 === + +* (bug 73) Category sort key is set to file name when adding category to + file description from upload page (previously it would be set to + "Special:Upload", causing problems with category paging) +* (bug 419) The contents of the navigation toolbar are now editable through + the MediaWiki namespace on the MediaWiki:navbar page. +* (bug 498) The Views heading in MonoBook.php is now localizable +* (bug 898) The wiki can now do advanced sanity check on uploaded files + including virus checks using external programs. +* (bug 1692) Fix margin on unwatch tab +* (bug 1906) Generalize project namespace for Latin localization, update namespaces +* (bug 1975) The name for Limburgish (li) changed from "Lèmburgs" to "Limburgs +* (bug 2019) Wrapped the output of Special:Version in <div dir='ltr'> in order + to preserve the correct flow of text on RTL wikis. +* (bug 2067) Fixed crash on empty quoted HTML attribute +* (bug 2075) Corrected namespace definitions in Tamil localization +* (bug 2079) Removed links to Special:Maintenance from movepagetext message +* (bug 2094) Multiple use of a template produced wrong results in some cases +* (bug 2095) Triple-closing-bracket thing partly fixed +* (bug 2110) "noarticletext" should not display on Image page for "sharedupload" media +* (bug 2150) Fix tab indexes on edit form +* (bug 2152) Add missing bgcolor to attribute whitelist for <td> and <th> +* (bug 2176) Section edit 'show changes' button works correctly now +* (bug 2178) Use temp dir from environment in parser tests +* (bug 2217) Negative ISO years were incorrectly converted to BC notation +* (bug 2234) allow special chars in database passwords during install +* Deprecated the {{msg:template}} syntax for referring to templates, {{msg: is + now the wikisyntax representation of wfMsgForContent() +* Fix for reading incorrectly re-gzipped HistoryBlob entries +* HistoryBlobStub: the last-used HistoryBlob is kept open to speed up + multiple-revision pulls +* Add $wgLegacySchemaConversion update-time option to reduce amount of + copying during the schema upgrade: creates HistoryBlobCurStub reference + records in text instead of copying all the cur_text fields. Requires + that the cur table be left in place until/unless such fields are migrated + into the main text store. +* Special:Export now includes page, revision, and user id numbers by + default (previously this was disabled for no particular reason) +* dumpBackup.php can dump the full database to Export XML, with current + revisions only or complete histories. +* The group table was renamed to groups because "group" is a reserved word in + SQL which caused some inconveniances. +* New fileicons for c, cpp, deb, dvi, exe, h, html, iso, java, mid, mov, o, + ogg, pdf, ps, rm, rpm, tar, tex, ttf and txt files based on the KDE + crystalsvg theme. +* Fixed a bug in Special:Newimages that made it impossible to search for '0' +* Added language variant support for Icelandic, now supports "Íslenzka" +* The #p-nav id in MonoBook is now #p-navigation +* Putting $4 in msg:userstatstext will now give the percentage of + admnistrators out of normal users. +* links and brokenlinks tables merged to pagelinks; this will reduce pain + dealing with moves and deletes of widely-linked pages. +* Add validate table and val_ip column through the updater. +* Simple rate limiter for edits and page moves; set $wgRateLimits + (somewhat experimental; currently needs memcached) +* (bug 2262) Hide math preferences when TeX is not enabled +* (bug 2267) Don't generate thumbnail at the same size as the source image. +* Fix rebuildtextindex.inc for new schema +* Remove linkscc table code, no longer used. +* (bug 2271) Use faster text-only link replacement in image alt text + instead of rerunning expensive link lookup and HTML generation. +* Only build the HTML attribute whitelist tree once. +* Replace wfMungeToUtf8 and do_html_entity_decode with a single function + that does both numeric and named chars: Sanitizer::decodeCharReferences +* Removed some obsolete UTF-8 converter functions +* Fix function comment in debug dump of SQL statements +* (bug 2275) Update search index more or less right on page move +* (bug 2053) Move comment whitespace trimming from edit page to save; + leaves the whitespace from the section comment there on preview. +* (bug 2274) Respect stub threshold in category page list +* (bug 2173) Fatal error when removing an article with an empty title from the watchlist +* Removed -f parameter from mail() usage, likely to cause failures and bounces. +* (bug 2130) Fixed interwiki links with fragments +* (bug 684) Accept an attribute parameter array on parser hook tags +* (bug 814) Integrate AuthPlugin changes to support Ryan Lane's external + LDAP authentication plugin +* (bug 2034) Armor HTML attributes against template inclusion and links munging + +=== Changes since 1.5alpha2 === + +* (bug 2319) Fix parse hook tag matching +* (bug 2329) Fix title formatting in several special pages +* (bug 2223) Add unique index on user_name field to prevent duplicate accounts +* (bug 1976) fix shared user database with a table prefix set +* (bug 2334) Accept null for attribs in wfElement without PHP warning +* (bug 2309) Allow templates and template parameters in HTML attribute zone, + with proper validation checks. (regression from fix for 2304) +* Disallow close tags and enforce empty tags for <hr> and <br> +* Changed user_groups format quite a bit. +* (bug 2368) Avoid fatally breaking PHP 4.1.2 in a debug line +* (bug 2367) Insert correct redirect link record on page move +* (bug 2372) Fix rendering of empty-title inline interwiki links +* (bug 2384) Fix typo in regex for IP address checking +* (bug 650) Prominently link MySQL 4.1 help page in installer if a possible + version conflict is detected +* (bug 2394) Undo incompatible breakage to {{msg:}} compatiblity includes +* (bug 1322) Use a shorter cl_sortkey field to avoid breaking on MySQL 4.1 + when the default charset is set to utf8 +* (bug 2400) don't send confirmation mail on account creation if + $wgEmailAuthentication is false. +* (bug 2172) Fix problem with nowiki beeing replaced by marker strings + when a template with a gallery was used. +* Guard Special:Userrights against form submission forgery +* (bug 2408) page_is_new was inverted (whoops!) +* Added wfMsgHtml() function for escaping messages and leaving params intact +* Fix ordering of Special:Listusers; fix groups list so it shows all groups + when searching for a specific group and can't be split across pages +* (bug 1702) Display a handy upload link instead of a useless blank link + for [[media:]] links to nonexistent files. +* (bug 873) Fix usage of createaccount permission; replaces $wgWhitelistAccount +* (bug 1805) Initialise $wgContLang before $wgUser +* (bug 2277) Added Friulian language file +* (bug 2457) The "Special page" href now links to the current special page + rather than to "". +* (bug 1120) Updated the Czech translation +* A new magic word, {{SCRIPTPATH}}, returns $wgScriptPath +* A new magic word, {{SERVERNAME}}, returns $wgServerName +* A new magic word, {{NUMBEROFFILES}}, returns the number of rows in the image table +* Special:Imagelist displays titles with " " instead of "_" +* Less gratuitous munging of content sample in delete summary +* badaccess/badaccesstext to supercede sysop*, developer* messages +* Changed $wgGroupPermissions to more cut-n-paste-friendly format +* 'developer' group deprecated by default +* Special:Upload now uses 'upload' permission instead of hardcoding login check +* Add 'importupload' permission to disable direct uploads to Special:Import +* (bug 2459) Correct escaping in Special:Log prev/next links +* (bug 2462 etc) Taking out the experimental dash conversion; it broke too many + things for the current parser to handle cleanly +* (bug 2467) Added a Turkish language file +* Fixed a bug in Special:Contributions that caused the namespace selection to + be forgotten between submits +* Special:Watchlist/edit now has namespace subheadings +* (bug 1714) the "Save page" button now has right margin to seperate it from + "Show preview" and "Show changes" +* Special:Statistics now supports action=raw, useful for bots designed to + harwest e.g. article counts from multiple wikis. +* The copyright confirmation box at Special:Upload is now turned off by default + and can be turned back on by setting $wgCopyrightAffirmation to a true value. +* Restored prior text for password reminder button and e-mail, replacing + the factually inaccurate text that was there. +* (bug 2178) Fix temp dir check again +* (bug 2488) Format 'deletedtext' message as wikitext +* (bug 750) Keep line endings consistent in LocalSettings.php +* (bug 1577) Add 'printable version' tab in MonoBook for people who don't + realize you can just hit print to get a nicely formatted printable page. +* Trim whitespace from option values to weather line-ending corruption problems +* Fixed a typo in the Romanian language file (NS_MESIA => NS_MEDIA) +* (bug 2504) Updated the Finnish translation +* (bug 2506, 2512) Updated the Nynorsk translation +* (bug 996) Replace $wgWhitelistEdit with 'edit' permission; fixup UPGRADE + documentation about edit and read whitelists. +* (bug 2515) Fix incremental link table update +* Removed some wikipedia-specifica from LanguageXx.php's +* (bug 2496) Allow MediaWiki:edithelppage to point to external page +* Added a versionRequired() function to OutputPage, useful for extension + writers that want to control what version of MediaWiki their extension + can be used with. +* Serialized user objects now checked for versioning +* Fix for interwiki link regression +* Printable link shorter in monobook +* Experimental Latin-1-and-replication-friendly upgrader script +* (bug 2520) Don't show enotif options when disabled + +== Changes since 1.5beta1 == + +* (bug 2531) Changed the interwiki name for sh (Serbocroatian) to + Srpskohrvatski/Српскохрватски (was Српскохрватски (Srbskohrvatski)) +* Nonzero return code for command-line scripts on wfDebugDieBacktrace() +* Conversion fix for empty old table in upgrade1_5.php +* Try reading revisions from master if no result on slave +* (bug 2538) Suppress notice on user serialized checks +* Fix paging on Special:Contributions +* (bug 2541) Fix unprotect tab +* (bug 1242) category list now show on edit page +* Skip sidebar entries where link text is '-' +* Convert non-UTF-8 URL parameters even if referer is local +* (bug 2460) <img> width & height properly filled when resizing image +* (bug 2273) deletion log comment used user interface langage +* Try reading revision _text_ from master if no result on slave +* Use content-language message cache for raw view of message pages +* (bug 2530) Not displaying talk pages on Special:Watchlist/edit +* Fixed a bug that would occour if $wgCapitalLinks was set to false, a user + agent could create a username that began with a lower case letter that was + not in the ASCII character set ( now user $wgContLang->ucfirst() instead of + PHP ucfirst() ) +* Moved the user name / password validity checking from + LoginForm::addNewAccountInternal() to two new functions, + User::isValidUserName() and User::isValidPassword(), extensions can now do + these checks without rewriting code. +* Fix $wgSiteNotice when MediaWiki:Sitenotice is set to default '-' +* Fixed a bug where the watchlist count without talk pages would be off by a + factor of two. +* upgrade1_5.php uses insert ignore, allows to skip image info initialization +* Fix namespaces in category list. +* Add rebuildImages.php to update image metadata fields +* Special:Ancientpages is expensive in new schema for now +* (bug 2568) Fixed a logic error in the Special:Statistics code which caused + the displayed percentage of admins to be totally off. +* (bug 2560) Don't show blank width/height attributes for missing size +* Don't show bogus messages about watchlist notifications when disabled +* Don't show old debug messages in watchlist +* (bug 2576) Fix recording of transclusion links +* (bug 2577) Allow sysops to enter non-standard block times +* Fixed a bug where Special:Contributions wouldn't remember the 'invert' + status between next/previous buttons. +* Move MonoBook printable link from tab to sidebar +* (bug 2567) Fix HTML escaping on category titles in list +* (bug 2562) Show rollback link for current revisions on diff pages +* (bug 2583) Add --missinig option on rebuildImages.php to add db entries + for uploaded files that don't have them +* (bug 2572) Fix edit conflict handling +* (bug 2595) Show "Earlier" and "Latest" links on history go to the first/last + page in the article history pager. +* Don't show empty-page text in 'Show changes' on new page +* (bug 2591) Check for end, fix limits on Whatlinkshere +* (bug 2584) Fix output of subcategory list +* (bug 2597) Don't crash when undeleting an image description page +* (bug 2564) Don't show "editingold" warning for recent revision +* Various code cleanup and HTML escaping fixlets +* Copy IRC-over-UDP update option from REL1_4 +* (bug 2548) Keep summary on 'show changes' of section edit +* Move center on toc to title part to avoid breaking .toc style usage +* HTML sanitizer: correct multiple attributes by keeping last, not first +* (bug 2614) Fix section edit links on diff-to-current with oldid set + Also fix navigation links on current-with-oldid view. +* (bug 2620) Return to prior behavior for some more things (such as + subpage parent links) on current-diff view. +* (bug 2618) Fix regression from another fix; show initial preview for + categories only if the page does not exist. +* (bug 2625) Keep group & user settings when paging in Listusers +* (bug 2627) Fix regression: diff radio button initial selection +* Copy fix for old search URLs with Lucene search plugin from REL1_4 +* (bug 619) Don't use incompatible diff3 executable on non-Linux systems. +* (bug 2631) Fix Hebrew namespaces. +* (bug 2630) Indicate no-longer-valid cached entries in BrokenRedirects list +* (bug 2644, 2645) "cur" diff links in page history, watchlist and + recentchanges should specify current ID explicitly. +* (bug 2609) Fix text justification preferenced with MonoBook skin. +* (bug 2594) Display article tab as red for non-existent articles. +* (bug 2656) Fix regression: prevent blocked users from reverting images +* (bug 2629) Automatically capitalize usernames again instead of + rejecting lowercase with a useless error message +* (bug 2661) Fix link generation in contribs +* Add support for &preload=Page_name (load text of an existing page into +edit area) and &editintro=Page_name (load text of an existing page instead +of MediaWiki:Newpagetext) to &action=edit, if page is new. +* (bugs 2633, 2672, 2685, 2695) Fix Estonian, Portuguese, Italian, Finnish and + Spanish numeric formatting +* Fixed Swedish numeric formatting +* (bug 2658) Fix signature time, localtime to match timezone offset again +* Files from shared repositories (e.g. commons) now display with their + image description pages when viewed on local wikis. +* Restore compatibility namespace aliases for French Wikipedia +* Fix diff order on Enhanced RC 'changes' link +* (bug 2650) Fix national date type display on wikis that don't support + dynamic date conversion. +* FiveUpgrade: large table hacks, install iw_trans update before links +* (bug 2648) Rename namespaces in Afrikaanse +* Special:Booksources checks if custom list page exists before using it +* (bug 1170) Fixed linktrail for da: and ru: +* (bug 2683) Really fix apostrophe escaping for toolbox tips +* (bug 923) Fix title and subtitle for rclinked special page +* (bug 2642) watchdetails message in several languages used <a></a> instead of [ ] +* (bug 2181) basic CSB language localisation by Tomasz G. Sienicki (thanks for the patch) +* Fix correct use of escaping in edit toolbar bits +* Removed language conversion support from Icelandic +* (bug 2616) Fix proportional image scaling, giving correct height +* (bug 2640) Include width and height attributes on unscaled images +* Workaround for mysterious problem with bogus epoch If-Last-Modified reqs +* (bug 1109) Suppress compressed output on 304 responses +* (bug 2674) Include some site configuration info in export data: + namespaces definitions, case-sensitivity, site name, version. +* Use xml:space="preserve" hint on export <text> elements +* Make language variant selection work again for zh + +== Changes since 1.5beta2 == + +* Escaped & correctly in Special:Contributions +* (bug 2534) Hide edit sections with CSS to make right click to edit section work +* (bug 2708) Avoid undefined notice on cookieless login attempt +* (bug 2188) Correct template namespace for Greek localization +* Fixed number formatting for Dutch +* (bug 1355) add class noprint to commonPrint.css +* (bug 2350) Massive update for Limburgish (li) language using Wikipédia +* Massive update for Arab (ar) language using Wikipédia +* (bug 1560) Massive update for Kurdish (ku) language using Wikipédia +* (bug 2709) Some messages were not read from database +* (bug 2416) Don't allow search engine robots to index or follow nonexisting articles +* Fix escaping in page move template. +* (bug 153) Discrepancy between thumbnail size and <img> height attribute + +== Changes since 1.5beta3 == + +* Fix talk page move handling +* (bug 2721) New language file for Vietnamese with the Vietnamese number notation +* (bug 2749)   would appear as a literal in image galleries for Cs, Fr, Fur, Pl and Sv +* (bug 787) external links being rendered when they only have one slash +* Fixed a missing typecast in Language::dateFormat() that would cause some + interesting errors with signitures. +* (bug 2764) Number format for Nds +* (bug 1553) Stop forcing lowercase in Monobook skin for German language. +* (bug 1064) Implements Special:Unusedcategories +* (bug 2311) New language file for Macedonian +* Fix nohistory message on empty page history +* Fix fatal error in history when validation on +* Cleaned up email notification message formatting +* Finally fixed Special:Disambiguations that was broke since SCHEMA_WORK +* (bug 2761) fix capitalization of "i" in Turkish +* (bug 2789) memcached image metadata now cleared after deletion +* Add serialized version number to image metadata cache records +* (bug 2780) Fix thumbnail generation with GD for new image schema +* (bug 2791) Slovene numeric format +* (bug 655) Provide empty search form when searching for nothing +* Nynorsk numeric format fix +* (bug 2825) Fix regression in newtalk notifications for anons w/ enotif off +* (bug 2833) Fix bug in previous fix +* With $wgCapitalLinks off, accept off-by-first-letter-case in 'go' match +* Optional parameters for [[Special:Listusers]] +* (bug 2832) [[Special:Listadmins]] redirects to [[Special:Listusers/sysop]] +* (bug 785) Parser did not get out of <pre> with list elements +* Some shared upload fixes +* (bug 2768) section=new on nonexistent talk page does not add heading +* support preload= parameter for section=new +* show comment subject in preview when using section=new +* use comment form when creating a new talk page +* (bug 460) Properly handle <center> tags as a block. +* Undo inconsistent editing behavior change +* (bug 2835) Back out fix for bug 2802, caused regressions in category sort +* PHP 4.1.2 compatibility fix: define floatval() equivalent if missing +* (bug 2901) Number format for Catalan +* Special:Allpages performance hacks: index memcached caching, removed + inverse checkbox, use friendlier relative offsets in index build +* Bring back "Chick" skin for mobile devices. It needs testing. +* Fix spelling of $wgForwardSearchUrl in DefaultSettings.php +* Specify USE INDEX on Allpages chunk queries, sometimes gets lost + due to bogus optimization +* (bug 275) Section duplication fix +* Remove unused use of undefined variable in UserMailer +* Fix notice on search index update due to non-array +* (bug 2885) Fix fatal errors and notices in PHP 5.1.0beta3 +* (bug 2931) Fix additional notices on reference use in PHP 4.4.0 +* (bug 2774) Add three new $wgHooks to LogPage which enable extensions to add + their own logtypes, see extensions/Renameuser/SpecialRenameuser.php for an + example of this. +* (bug 740) Messages from extensions now appear in Special:Allmessages +* (bug 2857) fixed parsing of lists in <pre> sections +* (bug 796) Trackback support +* Fix 1.5 regression: weird, backwards diff links on new pages in enhanced RC + are now suppressed as before. +* New skin: Simple +* "uselang" and "useskin" URL parameters can now be used in the URL when + viewing a page, to change the language and skin of a page respectively. +* Skins can now be previewed in preferences +* (bug 2943) AuthPlugin::getCanonicalName() name canonicalization hook, + patch from robla +* Wrap revision insert & page update in a transaction, rollback on late + edit conflict. +* (bug 2953) 'other' didn't work in Special:Blockip when localized +* (bug 2958) Rollback and delete auto-summary should be in the project's + content language +* Removed useless protectreason message +* Spelling fix: $wgUrlProtcols -> $wgUrlProtocols +* Switch Moldovan local name to cyrillic +* Fix typo in undefined array index access prevention +* (bug 2947) Update namespaces for sr localization +* (bug 2952) Added Asturian language file with translated namespaces +* (bug 2676) Apply a protective transformation on editing input/output + for browsers that hit the Unicode blacklist. Patch by plugwash. +* (bug 2999) Fix encoding conversion of pl_title in upgrade1_5.php +* compressOld.php disabled, as it's known to be broken. + + +=== Changes since 1.5beta4 === + +* Fix Special:Allmessages under PHP 5 +* (bug 2911) Special:Watchlist allowed only one type of limit at a time +* (bug 693) Special:Allmessages is excessively wide and redundant +* (bug 3001) Updated and applied live hack for recentchanges-based watchlist +* (bug 145) Finish 'exclude redirect' implementation in search form +* Rearranged Special:Movepage form to reduce confusion between destination + title and reason input boxes +* (bug 2527) Always set destination filename when new file is selected +* (bug 3056) MySQL 3 compatibility fix: USE INDEX instead of FORCE INDEX +* PHP 4.1 compatibility fix: don't use new_link parameter to mysql_connect + if running prior to 4.2.0 as it causes the call to fail +* (bug 3117) Fix display of upload size and type with tidy on +* (bug 1487) invalid html on empty list in banlist +* (bug 3017) Hotkey conflict for delete and show changes +* made pixel unit translateable and blocklistline now eats infiniteblock + and expiringblock +* (bug 3092) Wrong numerical separator for big numbers in Serbian. +* (bug 2855) Credit for a uniq author showed its realname even with + $wgAllowRealName=false. +* New special page: SpecialMostlinked +* (bug 2393) Fix MIME type for Atom feeds ( application/rss+atom ) +* Fix display of read-only lockfile message +* Added a new hook, 'AddNewAccount', which is run after account creation +* Update all stats fields on recount.sql +* Include software-visible client IP address in Special:Version comment + as a proxy debugging aid +* (bug 3162) Fix 'undefined property page_is_new' error on watchlist +* (bug 1734) granting db permissions failed with db usernames containg '-' +* (bug 3170) wikititlesuffix was removed, use pagetitle instead +* (bug 3187) watchlist text refer to unexistent "Stop watching" action +* (bug 3190) Added some date format choices for language sr +* (bug 1334) LanguageGa.php update +* (bug 1020) Changing user interface language does not work immediately +* (bug 2753) Some namespaces were not translated in LanguageTa.php (Tamil) +* (bug 3204) Fix typo breaking special pages in fy localization +* (bug 3210) Fix Media: links with remote image URL path +* (bug 3220) Fix escaping of block URLs in Recentchanges +* (bug 3238): Updated LanguageNn.php for 1_5 branch +* (bug 3192): properly check 'limit' parameter on Special:Contributions +* (bug 3244) Fix remote image loading hack, JavaScript injection on MSIE +* Fix URL sanitization in HTML attributes, which broke in this branch +* (bug 3475) anon contrib links on Special:Newpages + + +=== Changes since 1.5rc2 === + +* Fix upgrade from 1.4 due to version number check breakage +* Fix upgrade from 1.4 with no old revisions +* (bug 2108) Sort entries when using category browser +* XSS issue : now sanitize search query input + + +=== Changes since 1.5rc3 === + +* (bug 3280) Respect 'move' group permission on page moves +* (bug 2885) More PHP 5.1 fixes: skin, search, log, undelete +* Security fix for <math> +* Security fix for tables + + +=== Changes since 1.5rc4 === + +* (bug 3292) Fix move-over-redirect test when current entries are not plaintext +* (bug 2078) Don't hide watch tab on preview +* (bug 3306) Document $wgLocalTZoffset +* Support SVG rendering with rsvg +* Cap arbitrary SVG renders to given image size or $wgSVGMaxSize pixels wide +* (bug 3127) Render large SVGs at image page size correctly +* (bug 3448) Set page_len on undelete +* (bug 2800) Don't scale up small iamges on |thumb| without explicit size +* Use the real file link instead of the default-size rasterized version for + large SVG images on image description page +* Include the file name/type/size line for non-resized images +* (bug 3412) Clean up date format handling so ~~~~-sigs work with default + format as designed. Documentation comments updated. +* (bug 1423) LanguageJa.php update +* (bug 3405) Don't use raw letters as aliases of MSGNW: and SUBST: +* (bug 3485) Fix bogus warning about filename capitalization when off +* (bug 2792) Update rebuildrecentchanges.inc for new schema +* Special:Import/importDump fixes: report XML parse errors, accept <minor/> +* (bug 3489) PHP 5.1 compat problem with captioned images +* (bug 3350) Missing label for move talk page checkbox. +* (bug 2570) Add 'watch this page' checkbox on uploads, watch uploads + by default when 'watchdefault' option is on +* (bug 3182) Clear link cache during import to prevent memory leak +* (bug 3573) Full Greek Translation +* (bug 3595) Warn and abort if importDump.php called in read-only mode. +* (bug 3598) Update message cache on message page deletion, patch by Tietew +* Blacklist additional MSIE CSS safety tricks + + +=== Changes since 1.5.0 === + +* (bug 3629) Fix date & time format for Frisian +* (bug 3641) Fix handling of unrecognized file uploads with known extensions +* (bug 3643) Fix image page display of large images with resizing disabled +* Fix meta robots tag on Special:Version again to avoid listing vulnerable + versions for convenient harvesting by automated worms +* (bug 3684) Fix typo in fatal error backtraces in Hooks.php +* Backport fix for reference usage notice in Special:Search on PHP 4.4.0 +* Backport database connect error display fix from HEAD +* (bug 2773) Print style sheet no longer overrides RTL text direction +* MonoBook skin top link id changed from "contentTop" to "top" (shared with + name attribute) +* Wrap message page insertions in a transaction to speed up installation +* Fix Special:MovePage invalid HTML attribute for reason textarea +* Avoid notice warning on edit with no User-Agent header +* (bug 3734) Swapped out obsolete recount.sql with initStats.php +* (bug 3735) Fix to run under MySQL 5's strict mode +* (bug 3786) Experimental support for MySQL 4.1/5.0 utf8 charset mode + NOTE: Enabling this may break existing wikis, and still doesn't + work for all Unicode characters due to MySQL limitations. +* Sanitizer CSS comment processing order fix + + +=== Changes since 1.5.1 === + +* Fix Special:BrokenRedirects on MySQL 5.0 +* (bug 3809) Backport fix for detecting diff3 failure +* MySQL 5.0 strict mode fix for moving unwatched pages +* (bug 3782) Throw fatal installation warning if mbstring.func_overload on. + Why do people invent these crazy options that change language semantics? +* (bug 3762) Define missing Special:Import UI messages +* (bug 3771) Handle internal functions in backtrace in wfAbruptExit() +* (bug 3649) Remove obsolete, broken moveCustomMessages script +* (bug 3667) Add missing global in page move code +* (bug 3761) Avoid deprecation warnings in Special:Import +* (bug 2885) Remove unnecessary reference parameter which broke classic skin + talk notification on PHP 5.0.5 +* (bug 3845) Update attribute.php for 1.5 schema +* Fix Parser::unstrip on PHP 4.4.1 and PHP 5.1.0RC4 + + +=== Changes since 1.5.2 === + +* (bug 3612) Remove old broken version of maintenance/compressOld.php + The working version is in maintenance/storage/compressOld.php +* (bug 2740) Accept image deletions on 'enter' submit from MSIE +* (bug 3933) specify XML namespace for Atom 0.3 feeds +* (bug 3939) Don't try to load text for interwiki redirect target +* (bug 3948) Avoid notice warning in debug statement in bad search +* Recognize Special:Search consistently so read whitelist works +* (bug 4013) typo in fr +* (bug 3996) Fix text for new entries in RC RSS/Atom feed +* (bug 2894) Enhanced Recent Changes link fixes +* (bug 3065) Update both watched namespaces when renaming pages +* Move parentheses out of <a> link in Special:Contributions +* (bug 4071) Generate passwords long enough for $wgMinimalPasswordLength +* (bug 4035) Fix prev/next revision links on edit page +* (bug 4165) Correct validation for user language selection (data taint) +* Clearer message in DefaultSettings.php: edit LocalSettings.php instead + + +=== Changes since 1.5.3 === + +* (bug 3805) Clear 'new messages' flag properly in enotif mode + for usernames containing spaces +* (bug 2714) Backlink from special:whatlinkshere was hard set as 'existing' +* (bug 4249) Typo in entities2literals.pl +* (bug 4233) Update for japanese language +* (bug 4279) Small correction to LanguageDa.php +* (bug 4267) Switch dv sd ug ks arc languages to RTL +* (bug 3991) Allow the operation of wikicode on Protect move only text +* Added AutoAuthenticate hook for external User object suppliers +* Parser internal placeholder string now fully randomized for safety + +=== Changes since 1.5.4 === + +* Maintenance script to delete unused user accounts +* Added detection for WMF files (application/x-msmetafile), added this + MIME type to the default blacklist. Prevented inline display of images + which are not of known image types. This is in response to + http://en.wikipedia.org/wiki/Windows_Metafile_vulnerability + +=== Changes since 1.5.5 === + +* (bug 4258) When installing under IIS, $wgArticlePath = "$wgScript?title=$1" + should be set +* (bug 4510) Correct Barnes & Noble bookstore URLs +* (bug 4504) Use site language for namespace name resolution +* Installer fixes from HEAD backported; now uses a more sensible method of + establishing which mySQL user to use, which clears up bug 921 et al. Minor + changes to installer. +* Fix problem reported on mailing list where re-initialising stats didn't work + (can't insert duplicate rows with the same id field) +* (bug 1122) gray out 'older revision' when viewing first article revision. +* Respect database prefix in dumpHTML.inc +* Minor improvements to removeUnusedAccounts.php maintenance script +* Fix for single-digit week numbers from {{CURRENTWEEK}}, broken by PHP 4.4.1 +* Removed read-only check from Database::query() +* Added --conf option to command line scripts, allowing the user to specify a + different LocalSettings.php. + +=== Changes since 1.5.6 === + +* Default main page content improved per bug 4690 +* Fix dependence on hardcoded UNIQ_PREFIX in LanguageConverter.php +* Fixed Special:Unlockdb +* Maintenance script to delete unused text records +* Maintenance script to delete non-current revisions +* Maintenance script to wipe a page and all revisions from the database +* (bug 4768) Wrong Russian translation (typo) +* Performance bugfix: propagate equality manually for Revision fetches +* (bug 4773) PHP fatal error when invalid title passed to Special:Export +* Added missing table defs. for transcache to installer schemas +* (bug 4824) IE7 beta 2 broke compatibility with PNG logo workarounds, + and seems to work ok with other bits. No longer including the IE + workarounds JavaScript for IE 7 and above. +* (bug 2532) Image directory structure migration bug +* (bug 4881) Correction to the fix for 1487; Ipblocklist showed 'no blocks' + message at the end of the list even if there were blocks. +* (bug 4805) Removed more wikipedia-references from LanguageUk.php +* Introduce $wgWantedPagesThreshold per bug 5011; Special:Wantedpages will not + list pages with less than this number of links. Defaults to 1. +* Allow customisation of paging limits for items in categories using the + $wgCategoryPagingLimit global, per bug 4970. +* Improve "nogomatch" text to make it more obvious that a page can be created. +* (bug 5113) Spelling error in French language file +* Don't change the password of the MySQL root user. + +=== Changes since 1.5.7 === + +* (bug 5180) User login page shows inappropriate email blurb +* Add the "AbortNewAccount" hook on account creation; see hooks.txt for more info. +* Update default "exporttext" to reflect that Special:Import exists +* Add links to useful material to the default main page content +* Fix fragment HTML injection + +=== Changes since 1.5.8 === + +* Fixed obvious mistakes in Finnish (fi) translation +* Fixed obvious mistakes in Kurdish (ku) translation +* Merge two #p-search .pBody statements i monobook/main.css +* (bug 5156) Update for Hebrew language (he) translation +* Add the "UserRights" hook on user group changes; see hooks.txt for more info. +* Translated "listingcontinuesabbrev" for German + +=== Caveats === + +Some output, particularly involving user-supplied inline HTML, may not +produce 100% valid or well-formed XHTML output. Testers are welcome to +set $wgMimeType = "application/xhtml+xml"; to test for remaining problem +cases, but this is not recommended on live sites. (This must be set for +MathML to display properly in Mozilla.) + +---- + == MediaWiki 1.4.3 == (released 2005-04-28) diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/INSTALL mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/INSTALL --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/INSTALL 2005-04-13 16:30:54.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/INSTALL 2007-01-08 21:54:59.000000000 -0500 @@ -4,15 +4,11 @@ Starting with MediaWiki 1.2.0, it's possible to install and configure the wiki "in-place", as long as you have -the necessary prerequesites available. - -In 1.3.0 the old command-line installer has been removed. -A new command-line installer/upgrader may come soon... +the necessary prerequisites available. Required software: -* Web server with PHP 4.1.2 or higher (4.3.x is preferred) -* A MySQL server. 4.0.x is preferred, but 3.2.x should - work as well. +* Web server with PHP 5.x or higher. +* A MySQL server, 4.0.14 or higher OR a Postgres server, 8.1 or higher MediaWiki is developed and tested mainly on Unix/Linux platforms, but should work on Windows as well. @@ -26,6 +22,14 @@ Don't forget to check the RELEASE-NOTES file... + +Additional documentation is available online, which may include more +detailed notes on particular operating systems and workarounds for +difficult hosting environments: + +http://meta.wikimedia.org/wiki/Help:Installation + + ********************** WARNING ************************** REMEMBER: ALWAYS BACK UP YOUR DATABASE BEFORE ATTEMPTING @@ -42,6 +46,15 @@ directory tree. Rename it from "mediawiki-1.x.x" to something nice, like "wiki", since it'll be in your URL. + +-----------------------------------------------------------+ + | Hint: If you plan to use a fancy URL-rewriting scheme | + | to prettify your URLs, you should put the files in a | + | *different* directory from the virtual path where page | + | names will appear. | + | | + | See: http://meta.wikimedia.org/wiki/Rewrite_rules | + +-----------------------------------------------------------+ + To run the install script, you'll need to temporarily make the 'config' subdirectory writable by the web server. The simplest way to do this on a Unix/Linux system is to make @@ -60,6 +73,16 @@ move it back up to the main wiki directory, and the wiki should now be working. + +------------------------------------------------------------+ + | Security hint: if you have limited access on your server | + | and cannot change ownership of files, you might want to | + | *copy* instead of *move* LocalSettings.php. | + | | + | This will make the file owned by your user account | + | instead of by the web server, which is safer in case | + | another user's account is compromised. | + +------------------------------------------------------------+ + Once the wiki is set up, you should remove the config directory, or at least make it not world-writable (though it will refuse to config again if the wiki is set up). @@ -75,7 +98,9 @@ of what you're doing!) and keep track of major changes to the software, including performance improvements and security patches. -http://mail.wikipedia.org/mailman/listinfo/mediawiki-l (site admin support) +http://lists.wikimedia.org/mailman/listinfo/mediawiki-announce (low traffic) + +http://lists.wikimedia.org/mailman/listinfo/mediawiki-l (site admin support) -http://mail.wikipedia.org/mailman/listinfo/wikitech-l (development) +http://lists.wikimedia.org/mailman/listinfo/wikitech-l (development) diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/README mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/README --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/README 2005-06-26 00:16:06.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/README 2006-05-30 18:06:04.000000000 -0400 @@ -1,76 +1,103 @@ -2005-06-12 +2006-04-05 -MediaWiki ---------- +For system requirements, installation and upgrade details, see the files RELEASE-NOTES, +INSTALL, and UPGRADE. -MediaWiki is the software used for Wikipedia (http://www.wikipedia.org) and the +== MediaWiki == + +MediaWiki is the software used for Wikipedia [http://www.wikipedia.org/] and the other Wikimedia Foundation websites. Compared to other wikis, it has an excellent range of features and support for high-traffic websites using -multiple servers (Wikipedia peaks in the 2500+ requests per second range -as of June 2005). +multiple servers (Wikimedia sites peak in the 5000+ requests per second range +as of November 2005). While quite usable on smaller sites, you may find you have to "roll your own" local documentation, and some aspects of configuration may seem overcomplicated because MediaWiki is primarily targeted as an in-house tool. -See the files RELEASE-NOTES, INSTALL, and UPGRADE for details on system -requirements and installation procedure. - - The MediaWiki software was written by: - * Lee Daniel Crocker - * Magnus Manske - * Jan Hidders - * Brion Vibber - * Axel Boldt - * Geoffrey T. Dairiki - * Tomasz Wegrzanowski - * Erik Moeller - * Tim Starling - * Gabriel Wicke - * Ashar Voultoiz - * Evan Prodromou - * Several others - -These developers hold the copyright to this work, and it is licensed under the -terms of the GNU General Public License, version 2 (see -http://www.fsf.org/licenses/gpl.html). Derivative works and later versions of -the code will also be considered free software licensed under the same terms. - -The newly-founded Wikimedia Foundation currently has no legal rights to the -software, although copyright may be assigned to it at a later date. Wikimedia -has not funded any of the development work. - -Sections of code written exclusively by Lee Crocker or Erik Moeller are also -released into the public domain, which does not impair the obligations of users -under the GPL for use of the whole code or other sections thereof. +* Lee Daniel Crocker +* Magnus Manske +* Jan Hidders +* Brion Vibber +* Axel Boldt +* Geoffrey T. Dairiki +* Tomasz Wegrzanowski +* Erik Moeller +* Tim Starling +* Gabriel Wicke +* Ashar Voultoiz +* Evan Prodromou +* Ævar Arnfjörð Bjarmason +* Niklas Laxström +* Domas Mituzas +* Rob Church +* Jens Frank +* Several others + +The contributors hold the copyright to this work, and it is licensed +under the terms of the GNU General Public License, version 2 or later[1] +(see http://www.fsf.org/licenses/gpl.html). Derivative works and later +versions of the code must be free software licensed under the same +terms. This includes "extensions" that use MediaWiki functions or +variables; see http://www.gnu.org/licenses/gpl-faq.html#GPLAndPlugins +for details. + +The Wikimedia Foundation currently has no legal rights to the software. + +[1] Sections of code written exclusively by Lee Crocker or Erik Moeller are +also released into the public domain, which does not impair the obligations of +users under the GPL for use of the whole code or other sections thereof. + +[2] MediaWiki makes use of the Sajax Toolkit by modernmethod, + http://www.modernmethod.com/sajax/ + which has the following license: + + 'This work is licensed under the Creative Commons Attribution + License. To view a copy of this license, visit + http://creativecommons.org/licenses/by/2.0/ or send a letter + to Creative Commons, 559 Nathan Abbott Way, + Stanford, California 94305, USA.' -Many thanks to the Wikipedia regulars for testing and suggestions. +Many thanks to the Wikimedia regulars for testing and suggestions. -The code is currently maintained at SourceForge under the project "wikipedia", -module name "phase3". You can view the code in CVS there: +The official website for mediawiki is located at: http://www.mediawiki.org/ +The code is currently maintained in a Subversion repository +at svn.wikimedia.org. See http://www.mediawiki.org/wiki/Subversion +for details. + Please report bugs and make feature requests in our Bugzilla system: - http://bugzilla.wikimedia.org + http://bugzilla.wikimedia.org/ Documentation and discussion on new features may be found at: - http://meta.wikimedia.org/wiki/MediaWiki_FAQ - http://meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide - http://meta.wikimedia.org/wiki/MediaWiki_development - + http://www.mediawiki.org/wiki/Help:FAQ + http://www.mediawiki.org/wiki/Documentation + http://www.mediawiki.org/wiki/Development + +Extensions are listed at: + + http://meta.wikimedia.org/wiki/Category:MediaWiki_extensions If you are setting up your own wiki based on this software, it is highly recommended that you subscribe to mediawiki-announce: - http://mail.wikipedia.org/mailman/listinfo/mediawiki-announce + http://mail.wikimedia.org/mailman/listinfo/mediawiki-announce The mailing list is very low volume, and is intended primarily for announcements of new versions, bug fixes, and security issues. +A higher volume support mailing list can be found at: + + http://mail.wikimedia.org/mailman/listinfo/mediawiki-l + +Developer discussion takes place at: + + http://mail.wikimedia.org/mailman/listinfo/wikitech-l -For installation and upgrade details, see the files RELEASE-NOTES, INSTALL, -and UPGRADE. +There is also a development and support channel #mediawiki on +irc.freenode.net, and an unoffical support forum at www.mwusers.com. diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/RELEASE-NOTES mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/RELEASE-NOTES --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/RELEASE-NOTES 2006-03-26 14:38:24.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/RELEASE-NOTES 2007-09-10 17:36:51.000000000 -0400 @@ -3,1113 +3,651 @@ Security reminder: MediaWiki does not require PHP's register_globals setting since version 1.2.0. If you have it on, turn it *off* if you can. -== MediaWiki 1.5.8 == +== MediaWiki 1.11.0 == -March 26, 2006 +September 10, 2007 -MediaWiki 1.5.8 is a security and bugfix maintenance release. +This is the Fall 2007 snapshot release of MediaWiki. -A bug in decoding of certain encoded links could allow injection of raw -HTML into page output; this could potentially lead to XSS attacks. +MediaWiki is now using a "continuous integration" development model with +quarterly snapshot releases. The latest development code is always kept +"ready to run", and in fact runs our own sites on Wikipedia. -Some minor UI fixes were also made, see the change log at the bottom of -this file. +Release branches will continue to receive security updates for about a year +from first release, but nonessential bugfixes and feature developments +will be made on the development trunk and appear in the next quarterly release. +Those wishing to use the latest code instead of a branch release can obtain +it from source control: http://www.mediawiki.org/wiki/Download_from_SVN -== MediaWiki 1.5.7 == +== Changes since 1.11.0rc1 == -March 2, 2006 +A possible HTML/XSS injection vector in the API pretty-printing mode has +been found and fixed. -MediaWiki 1.5.7 is a bugfix maintenance release. +The vulnerability may be worked around in an unfixed version by simply +disabling the API interface if it is not in use, by adding this to +LocalSettings.php: -Most importantly, a security issue in the installer has been fixed. The bug -affects new installations of 1.5.6 only. If the user specified the MySQL root -password, to allow the installer to create an unprivileged account, the -installer would not only create the new account but also change the root -password to be equal to the password of the new account. + $wgEnableAPI = false; -Anyone affected by this bug will need to change the root password back -manually. For information about how to change passwords in MySQL please see: -http://dev.mysql.com/doc/refman/5.1/en/passwords.html +(This is the default setting in 1.8.x.) -This version includes fixes for compatibility with Internet Explorer 7 -beta 2, and various other bugs; see the full changelog at the end of -the release notes. - - -== MediaWiki 1.5.6 == - -January 19, 2006 - -MediaWiki 1.5.6 is a security and bugfix maintenance release. - -A bug in edit comment formatting could send PHP into an infinite loop -if certain malformed links were included. In most installations, this -would cause the script to fail after PHP's 30-second failsafe timeout. - -Some improvements have been made to the installer which should make -installation possible on a system with a broken MySQL "root" account. - -For several other minor fixes, see the complete changelog at the end -of this file. - - -== MediaWiki 1.5.5 == - -January 5, 2006 - -MediaWiki 1.5.5 is a security and bugfix maintenance release. - -Detection for uploads of Windows Metafile (.wmf) images has been added -to help protect against a client-side vulnerability in unpatched Microsoft -Windows operating systems. - -Sites which have enabled uploads and added non-standard file types -(such as .ogg, .doc, or .pdf) should upgrade to this release to ensure -that malicious .wmf files can't be uploaded with a fake extension; -such files could put visitors to the site at risk. - -For more details on this, see: -http://en.wikipedia.org/wiki/Windows_Metafile_vulnerability - -Additionally, a maintenance script removeUnusedAccounts.php has been added; -this replaces an older Perl script which had not been updated for the new -schema in 1.5. - - -== MediaWiki 1.5.4 == - -December 21, 2005 - -MediaWiki 1.5.4 is a security and bugfix maintenance release. - -A hardcoded internal placeholder string has been replaced with a random -one. This closes a hole where security checks in inline style attributes -could be bypassed, injecting JavaScript code that could execute in -Microsoft Internet Explorer. - -Other browsers would not be vulnerable. - -Several minor fixes are included in this release, most notably a fix -to clear the "you have new messages" flag properly for usernames -containing spaces when e-mail notification is enabled. - -See the changelog at the end of the release notes for a full list of -fixes. - - -== MediaWiki 1.5.3 == - -December 4, 2005 - -MediaWiki 1.5.3 is a security and bugfix maintenance release. - -Validation of the user language option was broken by a code change in -May 2005, opening the possibility of remote code execution as this -parameter is used in forming a class name dynamically created with -eval(). - -The validation has been corrected in this version. All prior 1.5 release -and prelease versions are affected; 1.4 and earlier and not affected. - -Additionally several bugs have been fixed; see the changelog later in -this file for a complete list. - - -== MediaWiki 1.5.2 == - -November 2, 2005 - -MediaWiki 1.5.2 is a bugfix maintenance release. - -A change in PHP 4.4.1 and PHP 5.1.0RC broke handling of extension and -<pre> sections, causing garbage data to be inserted in output and saved -edits. This version works around the change. - -Several other glitches with MySQL 5.0 and PHP 5.0.5 were also fixed; -see the change log below for a complete list. - - -== MediaWiki 1.5.1 == - -October 26, 2005 - -MediaWiki 1.5.1 is a bugfix and security maintenance release, and is a -recommended upgrade for all installations. - -This release includes further corrections to the inline CSS style sanitation -which works around a JavaScript "feature" on Microsoft Internet Explorer. -Users of Microsoft Internet Explorer for Windows may be vulnerable to -XSS injections on prior versions; users of standards-compliant browsers -are not vulnerable. - -Major fixes include: -* Image pages work again with resizing disabled -* Works in MySQL 5.0 strict mode - -There is experimental support in this release for explicitly declaring -the UTF-8 charset in the database; this has been tested with MySQL 5.0.15 -but should work on 4.1 as well. - -IMPORTANT: Changing this setting on an existing wiki may produce interesting -data corruption, depending on server configuration. Page contents should, -usually, be unaffected, but page titles and other items may be. Limitations -in MySQL's Unicode support mean that characters outside the BMP cannot be used -in page titles or various other fields when using this mode. - -Table definitions are in maintenance/mysql5/tables.sql, and the runtime -option to send 'SET NAMES utf8' is set by $wgDBmysql5 = true. - -(MySQL 3.23.x and 4.0.x do not support character set declarations; on these -versions MediaWiki simply works with UTF-8 data and MySQL is blissfully -unaware of it.) - - - -== MediaWiki 1.5.0 final == - -October 5, 2005 - -MediaWiki 1.5.0 is the new stable release branch of MediaWiki, and is -recommended for all new installations. - -Any wikis running a 1.5 beta or release candidate are strongly recommended -to upgrade to the final release, which includes a number of bug fixes and -a security fix for CSS bugs in Microsoft Internet Explorer. - -IMPORTANT: Running a 1.3 or 1.4 wiki and don't want to jump to 1.5 yet? -Be sure to upgrade to 1.3.17 or 1.4.11, also released today. Versions -prior to 1.3.16 and 1.4.10 have a serious data corruption bug which is -triggered by a spambot known to operate in the wild. - - -=== What's new in 1.5? === - -Schema: - The core table schema has changed significantly. This should make better - use of the database's cache and disk I/O, and make significantly speed up - rename and delete operations on pages with very long edit histories. - - Unfortunately this does mean upgrading a wiki of size from 1.4 will require - some downtime for the schema restructuring, but future storage backend - changes should be able to integrate into the new system more easily. - -Permalinks: - The current revision of a page now has a permanent 'oldid' number assigned - immediately, and the id numbers are now preserved across deletion/undeletion. - A permanent reference to the current revision of a page is now just a matter - of going to the 'history' tab and copying the first link in the list. - -Page move log: - Renames of pages are now recorded in Special:Log and the page history. - A handy revert link is available from the log for sysops. - -Editing diff: - Ever lost track of what you'd done so far during an edit? A 'Show diff' - button on the edit page now makes it easy to remember. - -Uploads: - It's now possible to specify the final filename of an upload distinct - from the original filename on your disk. - - An image link for a missing file will now take you straight to the upload page. - - More metadata is pre-extracted from uploaded images, which will ease pressure - on disk or NFS volumes used to store images. EXIF metadata is displayed on - the image description page if PHP is configured with the necessary module. - - If .svg files are added to the upload whitelist, you can choose to render - them to rasterized .png images for inline display using one of several - external helper programs. See DefaultSettings.php for SVG options. - -User accounts: - There are some changes to the user permissions system, with assignable - groups. Note that this does *not* allow you to make pages which are only - accessible to certain groups. - - For details see: http://meta.wikimedia.org/wiki/Help:User_rights - -E-mail: - User-to-user e-mail can now be restricted to require a mail-back confirmation - first to reduce potential for abuse with false addresses. - - Updates to user talk pages and watchlist entries can optionally send e-mail - notifications. - -External hooks: - A somewhat experimental interface for hooking in an external editor - application is included. - -And... - A bunch of stuff we forgot to mention. - - -=== What's gone? === - -Latin-1: - Wikis must now be encoded in Unicode UTF-8; this has been the default for - some time, but some languages could optionally be installed in Latin-1 mode. - This is no longer supported. - - You can check if your current wiki is in Latin-1 mode by using your browser's - "view source"; look for a line like this: - - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> - - If it says charset=utf-8, you're ready. If it says charset=iso8859-1, - you may need to convert your data. (English-language wikis avoiding - any accented characters may be able to get away without conversion.) - -MySQL 3.x: - Some optimization hacks for MySQL 3.x have been removed as part of the schema - clean-up (specifically, the inverse_timestamp fields). - - MediaWiki 1.5 may still run on 3.x, but wikis of non-trivial size should - very seriously consider upgrading to a more modern release. MySQL 3.x support - will probably be entirely dropped in the next major release. - -Special:Maintenance - These tools were, ironically enough, not really maintained. This special - page has been removed; insofar as some of its pieces were useful and haven't - already been supplanted by other special pages they should be rewritten in - an efficient and safe manner in the future. - - -=== Caveats === - -Upgrade: - Wikis in Latin-1 encoding are no longer supported; only Unicode UTF-8. - A new option $wgLegacyEncoding is provided to allow on-the-fly recoding of - old page text entries, but other metadata fields (titles, comments etc) need - to be pre-converted. The standard upgrade process does not yet fully automate - this, but you can try the alternate partial-upgrader in upgrade1_5.php. - - The upgrade from 1.4 to 1.5 schema has not been tested for all cases, so - it's possible you may experience problems in some combinations. - -Backups: - The text entries of deleted pages are no longer removed from the main - text table on deletion. If you provide public backup dumps of your databases, - you will probably want to use the new XML-format dump generator, available - as maintenance/dumpBackup.php. - - For more information on how we run our own public data dumps at Wikimedia, - see http://meta.wikimedia.org/wiki/Data_dumps - -PostgreSQL: - The table definitions for PostgreSQL install are out of date. PostgreSQL - support may return in later releases, pending appropriate patches. - -MySQL 4.1+: - Some users may encounter installation problems with MySQL 4.1 or higher - due to strange charset encoding / collation configurations. Try setting - to 'latin1' or 'utf8' if you encounter problems. - - - -== MediaWiki 1.5 release candidate 4 == - -August 29, 2005 - -MediaWiki 1.5rc4 is a preview release of the new 1.5 release series. -It fixes compatibility with PHP 5.1, and corrects two cross-site scripting -security bugs: - -* <math> tags were handled incorrectly when TeX rendering support is off, - as in the default configuration. -* Extension or <nowiki> sections in Wiki table syntax could bypass HTML - style attribute restrictions for cross-site scripting attacks against - Microsoft Internet Explorer - -Wikis where the optional math support has been *enabled* are not vulnerable -to the first, but are vulnerable to the second. - - - -== MediaWiki 1.5 release candidate 3 == - -August 24, 2005 - -MediaWiki 1.5rc3 is a preview release of the new 1.5 release series. -It fixes several major problems in 1.5rc2: - -* Fixed a cross-site scripting injection in the search form - (broken since 1.5beta1) - -* Fixed upgrades from 1.4 database schema - (broken since 1.5rc2) - -1.3 and 1.4 releases are not vulnerable to the XSS bug, but anyone -running an earlier 1.5 beta or release candidate should upgrade -immediately. - - -== MediaWiki 1.5 release candidate 2 == - -August 23, 2005 - -MediaWiki 1.5rc2 is a preview release of the new 1.5 release series. -Numerous bug fixes since last beta, plus a security fix; see change -log below for full details. - -A flaw in the interaction between extensions and HTML attribute -sanitization was discovered which could allow unauthorized use -of offsite resources in style sheets, and possible exploitation -of a JavaScript injection feature on Microsoft Internet Explorer. - -This version expands the returned text and properly checks it -before output. - -A 1.5rc1 release was mistakenly made from the incorrect source code -branch; 1.5rc2 is identical to the actual 1.5rc1 in revision control -except for version number. - - -== MediaWiki 1.5 beta 4 == - -July 30, 2005 - -MediaWiki 1.5 beta 4 is a preview release of the new 1.5 release series. -A number of bugs have been fixed since beta 3; see the full changelist below. - - -== MediaWiki 1.5 beta 3 == - -July 7, 2005 - -MediaWiki 1.5 beta 3 is a preview release of the new 1.5 release -series, with a security update over beta 2. - -Incorrect escaping of a parameter in the page move template could -be used to inject JavaScript code by getting a victim to visit a -maliciously constructed URL. Users of vulnerable releases are -recommended to upgrade to this release. +Not vulnerable versions: +* 1.11 >= 1.11.0 +* 1.10 >= 1.10.2 +* 1.9 >= 1.9.4 +* 1.8 >= 1.8.5 Vulnerable versions: -* 1.5 preview series: n <= 1.5beta2 vulnerable, fixed in 1.5beta3 -* 1.4 stable series: 1.4beta6 <= n <= 1.4.5 vulnerable, fixed in 1.4.6 -* 1.3 legacy series: not vulnerable - -This release also includes several bug fixes and localization updates. -See the changelog at the end of this file for a detailed list. - - - -== MediaWiki 1.5 beta 2 == - -July 5, 2005 - -MediaWiki 1.5 beta 2 is a preview release of the new 1.5 release series. -While most exciting new bugs should have been ironed out at this point, -third-party wiki operators should probably not run this beta release -on a public site without closely following additional development. - -Anyone who _has_ been running beta 1 is very very strongly advised to -upgrade to beta 2, as it fixes many bugs from the previous beta including -a couple of HTML and SQL injections. - -This release should be followed by one or two release candidates and -a 1.5.0 final within the next few weeks. - -Beta upgraders, note there are some minor database changes. For upgrades -from 1.4, see the file UPGRADE for details on significant database and -configuration file changes. - -Beta 2 includes a preliminary command-line XML wiki dump importer tool, -maintenance/importDump.php, paired with maintenance/dumpBackup.php. -These use the same format as Special:Export and Special:Import, able -to package a wiki's entire page set independent of the backend database -and compression format. - - -== MediaWiki 1.5 beta 1 == - -June 26, 2005 - -MediaWiki 1.5 beta 1 is a preview release, pretty much feature complete, -of the new 1.5 release series. There are several known and likely a number -of unknown bugs; it is not recommended to use this release in a production -environment but would be recommended for testing in mind of an upcoming -deployment. - -A number of significant changes have been made since the alpha releases, -including database changes and a reworking of the user permissions settings. -See the file UPGRADE for details of upgrading and changing your prior -configuration settings for the new system. - - - -== MediaWiki 1.5 alpha 2 == - -June 3, 2005 - -MediaWiki 1.5 alpha 2 includes a lot of bug fixes, feature merges, -and a security update. - -Incorrect handling of page template inclusions made it possible to -inject JavaScript code into HTML attributes, which could lead to -cross-site scripting attacks on a publicly editable wiki. - -Vulnerable releases and fix: -* 1.5 prerelease: fixed in 1.5alpha2 -* 1.4 stable series: fixed in 1.4.5 -* 1.3 legacy series: fixed in 1.3.13 -* 1.2 series no longer supported; upgrade to 1.4.5 strongly recommended - - -== MediaWiki 1.5 alpha 1 == - -May 3, 2005 - -This is a testing preview release, being put out mainly to aid testers in -finding installation bugs and other major problems. It is strongly recommended -NOT to run a live production web site on this alpha release. - -** WARNING: USE OF THIS ALPHA RELEASE MAY INFEST YOUR HOUSE WITH ** -** TERMITES, ROT YOUR TEETH, GROW HAIR ON YOUR PALMS, AND PASTE ** -** INNUENDO INTO YOUR C.V. RIGHT BEFORE A JOB INTERVIEW! ** -** DON'T SAY WE DIDN'T WARN YOU, MAN. WE TOTALLY DID RIGHT HERE. ** - - -=== Smaller changes since 1.4 === - -Various bugfixes, small features, and a few experimental things: - -* 'live preview' reduces preview reload burden on supported browsers -* support for external editors for files and wiki pages: - http://meta.wikimedia.org/wiki/Help:External_editors -* Schema reworking: http://meta.wikimedia.org/wiki/Proposed_Database_Schema_Changes/October_2004 -* (bug 15) Allow editors to view diff of their change before actually submitting an edit -* (bug 190) Hide your own edits on the watchlist -* (bug 510): Special:Randompage now works for other namespaces than NS_MAIN. -* (bug 1015) support for the full wikisyntax in <gallery> captions. -* (bug 1105) A "Destination filename" (save as) added to Special:Upload Upload. -* (bug 1352) Images on description pages now get thumbnailed regardless of whether the thumbnail is larger than the original. -* (bug 1662) A new magicword, {{CURRENTMONTHABBREV}} returns the abbreviation of the current month -* (bug 1668) 'Date format' supported for other languages than English, see: - http://mail.wikipedia.org/pipermail/wikitech-l/2005-March/028364.html -* (bug 1739) A new magicword, {{REVISIONID}} give you the article or diff database - revision id, useful for proper citation. -* (bug 1998) Updated the Russian translation. -* (bug 2064) Configurable JavaScript mimetype with $wgJsMimeType -* (bug 2084) Fixed a regular expression in includes/Title.php that was accepting invalid syntax like #REDIRECT [[foo] in redirects -* It's now possible to invert the namespace selection at Special:Allpages and Special:Contributions -* No longer using sorbs.net to check for open proxies by default. -* What was $wgDisableUploads is now $wgEnableUploads, and should be set to true if one wishes to enable uploads. -* Supplying a reason for a block is no longer mandatory -* Language conversion support for category pages -* $wgStyleSheetDirectory is no longer an alias for $wgStyleDirectory; -* Special:Movepage can now take paramaters like Special:Movepage/Page_to_move - (used to just be able to take paramaters via a GET request like index.php?title=Special:Movepage&target=Page_to_move) -* (bug 2151) The delete summary now includes editor name, if only one has edited the article. -* (bug 2105) Fixed from argument to the PHP mail() function. A missing space could prevent sending mail with some versions of sendmail. -* (bug 2228) Updated the Slovak translation -* ...and more! - - -=== Changes since 1.5alpha1 === - -* (bug 73) Category sort key is set to file name when adding category to - file description from upload page (previously it would be set to - "Special:Upload", causing problems with category paging) -* (bug 419) The contents of the navigation toolbar are now editable through - the MediaWiki namespace on the MediaWiki:navbar page. -* (bug 498) The Views heading in MonoBook.php is now localizable -* (bug 898) The wiki can now do advanced sanity check on uploaded files - including virus checks using external programs. -* (bug 1692) Fix margin on unwatch tab -* (bug 1906) Generalize project namespace for Latin localization, update namespaces -* (bug 1975) The name for Limburgish (li) changed from "Lèmburgs" to "Limburgs -* (bug 2019) Wrapped the output of Special:Version in <div dir='ltr'> in order - to preserve the correct flow of text on RTL wikis. -* (bug 2067) Fixed crash on empty quoted HTML attribute -* (bug 2075) Corrected namespace definitions in Tamil localization -* (bug 2079) Removed links to Special:Maintenance from movepagetext message -* (bug 2094) Multiple use of a template produced wrong results in some cases -* (bug 2095) Triple-closing-bracket thing partly fixed -* (bug 2110) "noarticletext" should not display on Image page for "sharedupload" media -* (bug 2150) Fix tab indexes on edit form -* (bug 2152) Add missing bgcolor to attribute whitelist for <td> and <th> -* (bug 2176) Section edit 'show changes' button works correctly now -* (bug 2178) Use temp dir from environment in parser tests -* (bug 2217) Negative ISO years were incorrectly converted to BC notation -* (bug 2234) allow special chars in database passwords during install -* Deprecated the {{msg:template}} syntax for referring to templates, {{msg: is - now the wikisyntax representation of wfMsgForContent() -* Fix for reading incorrectly re-gzipped HistoryBlob entries -* HistoryBlobStub: the last-used HistoryBlob is kept open to speed up - multiple-revision pulls -* Add $wgLegacySchemaConversion update-time option to reduce amount of - copying during the schema upgrade: creates HistoryBlobCurStub reference - records in text instead of copying all the cur_text fields. Requires - that the cur table be left in place until/unless such fields are migrated - into the main text store. -* Special:Export now includes page, revision, and user id numbers by - default (previously this was disabled for no particular reason) -* dumpBackup.php can dump the full database to Export XML, with current - revisions only or complete histories. -* The group table was renamed to groups because "group" is a reserved word in - SQL which caused some inconveniances. -* New fileicons for c, cpp, deb, dvi, exe, h, html, iso, java, mid, mov, o, - ogg, pdf, ps, rm, rpm, tar, tex, ttf and txt files based on the KDE - crystalsvg theme. -* Fixed a bug in Special:Newimages that made it impossible to search for '0' -* Added language variant support for Icelandic, now supports "Íslenzka" -* The #p-nav id in MonoBook is now #p-navigation -* Putting $4 in msg:userstatstext will now give the percentage of - admnistrators out of normal users. -* links and brokenlinks tables merged to pagelinks; this will reduce pain - dealing with moves and deletes of widely-linked pages. -* Add validate table and val_ip column through the updater. -* Simple rate limiter for edits and page moves; set $wgRateLimits - (somewhat experimental; currently needs memcached) -* (bug 2262) Hide math preferences when TeX is not enabled -* (bug 2267) Don't generate thumbnail at the same size as the source image. -* Fix rebuildtextindex.inc for new schema -* Remove linkscc table code, no longer used. -* (bug 2271) Use faster text-only link replacement in image alt text - instead of rerunning expensive link lookup and HTML generation. -* Only build the HTML attribute whitelist tree once. -* Replace wfMungeToUtf8 and do_html_entity_decode with a single function - that does both numeric and named chars: Sanitizer::decodeCharReferences -* Removed some obsolete UTF-8 converter functions -* Fix function comment in debug dump of SQL statements -* (bug 2275) Update search index more or less right on page move -* (bug 2053) Move comment whitespace trimming from edit page to save; - leaves the whitespace from the section comment there on preview. -* (bug 2274) Respect stub threshold in category page list -* (bug 2173) Fatal error when removing an article with an empty title from the watchlist -* Removed -f parameter from mail() usage, likely to cause failures and bounces. -* (bug 2130) Fixed interwiki links with fragments -* (bug 684) Accept an attribute parameter array on parser hook tags -* (bug 814) Integrate AuthPlugin changes to support Ryan Lane's external - LDAP authentication plugin -* (bug 2034) Armor HTML attributes against template inclusion and links munging - -=== Changes since 1.5alpha2 === - -* (bug 2319) Fix parse hook tag matching -* (bug 2329) Fix title formatting in several special pages -* (bug 2223) Add unique index on user_name field to prevent duplicate accounts -* (bug 1976) fix shared user database with a table prefix set -* (bug 2334) Accept null for attribs in wfElement without PHP warning -* (bug 2309) Allow templates and template parameters in HTML attribute zone, - with proper validation checks. (regression from fix for 2304) -* Disallow close tags and enforce empty tags for <hr> and <br> -* Changed user_groups format quite a bit. -* (bug 2368) Avoid fatally breaking PHP 4.1.2 in a debug line -* (bug 2367) Insert correct redirect link record on page move -* (bug 2372) Fix rendering of empty-title inline interwiki links -* (bug 2384) Fix typo in regex for IP address checking -* (bug 650) Prominently link MySQL 4.1 help page in installer if a possible - version conflict is detected -* (bug 2394) Undo incompatible breakage to {{msg:}} compatiblity includes -* (bug 1322) Use a shorter cl_sortkey field to avoid breaking on MySQL 4.1 - when the default charset is set to utf8 -* (bug 2400) don't send confirmation mail on account creation if - $wgEmailAuthentication is false. -* (bug 2172) Fix problem with nowiki beeing replaced by marker strings - when a template with a gallery was used. -* Guard Special:Userrights against form submission forgery -* (bug 2408) page_is_new was inverted (whoops!) -* Added wfMsgHtml() function for escaping messages and leaving params intact -* Fix ordering of Special:Listusers; fix groups list so it shows all groups - when searching for a specific group and can't be split across pages -* (bug 1702) Display a handy upload link instead of a useless blank link - for [[media:]] links to nonexistent files. -* (bug 873) Fix usage of createaccount permission; replaces $wgWhitelistAccount -* (bug 1805) Initialise $wgContLang before $wgUser -* (bug 2277) Added Friulian language file -* (bug 2457) The "Special page" href now links to the current special page - rather than to "". -* (bug 1120) Updated the Czech translation -* A new magic word, {{SCRIPTPATH}}, returns $wgScriptPath -* A new magic word, {{SERVERNAME}}, returns $wgServerName -* A new magic word, {{NUMBEROFFILES}}, returns the number of rows in the image table -* Special:Imagelist displays titles with " " instead of "_" -* Less gratuitous munging of content sample in delete summary -* badaccess/badaccesstext to supercede sysop*, developer* messages -* Changed $wgGroupPermissions to more cut-n-paste-friendly format -* 'developer' group deprecated by default -* Special:Upload now uses 'upload' permission instead of hardcoding login check -* Add 'importupload' permission to disable direct uploads to Special:Import -* (bug 2459) Correct escaping in Special:Log prev/next links -* (bug 2462 etc) Taking out the experimental dash conversion; it broke too many - things for the current parser to handle cleanly -* (bug 2467) Added a Turkish language file -* Fixed a bug in Special:Contributions that caused the namespace selection to - be forgotten between submits -* Special:Watchlist/edit now has namespace subheadings -* (bug 1714) the "Save page" button now has right margin to seperate it from - "Show preview" and "Show changes" -* Special:Statistics now supports action=raw, useful for bots designed to - harwest e.g. article counts from multiple wikis. -* The copyright confirmation box at Special:Upload is now turned off by default - and can be turned back on by setting $wgCopyrightAffirmation to a true value. -* Restored prior text for password reminder button and e-mail, replacing - the factually inaccurate text that was there. -* (bug 2178) Fix temp dir check again -* (bug 2488) Format 'deletedtext' message as wikitext -* (bug 750) Keep line endings consistent in LocalSettings.php -* (bug 1577) Add 'printable version' tab in MonoBook for people who don't - realize you can just hit print to get a nicely formatted printable page. -* Trim whitespace from option values to weather line-ending corruption problems -* Fixed a typo in the Romanian language file (NS_MESIA => NS_MEDIA) -* (bug 2504) Updated the Finnish translation -* (bug 2506, 2512) Updated the Nynorsk translation -* (bug 996) Replace $wgWhitelistEdit with 'edit' permission; fixup UPGRADE - documentation about edit and read whitelists. -* (bug 2515) Fix incremental link table update -* Removed some wikipedia-specifica from LanguageXx.php's -* (bug 2496) Allow MediaWiki:edithelppage to point to external page -* Added a versionRequired() function to OutputPage, useful for extension - writers that want to control what version of MediaWiki their extension - can be used with. -* Serialized user objects now checked for versioning -* Fix for interwiki link regression -* Printable link shorter in monobook -* Experimental Latin-1-and-replication-friendly upgrader script -* (bug 2520) Don't show enotif options when disabled - -== Changes since 1.5beta1 == - -* (bug 2531) Changed the interwiki name for sh (Serbocroatian) to - Srpskohrvatski/Српскохрватски (was Српскохрватски (Srbskohrvatski)) -* Nonzero return code for command-line scripts on wfDebugDieBacktrace() -* Conversion fix for empty old table in upgrade1_5.php -* Try reading revisions from master if no result on slave -* (bug 2538) Suppress notice on user serialized checks -* Fix paging on Special:Contributions -* (bug 2541) Fix unprotect tab -* (bug 1242) category list now show on edit page -* Skip sidebar entries where link text is '-' -* Convert non-UTF-8 URL parameters even if referer is local -* (bug 2460) <img> width & height properly filled when resizing image -* (bug 2273) deletion log comment used user interface langage -* Try reading revision _text_ from master if no result on slave -* Use content-language message cache for raw view of message pages -* (bug 2530) Not displaying talk pages on Special:Watchlist/edit -* Fixed a bug that would occour if $wgCapitalLinks was set to false, a user - agent could create a username that began with a lower case letter that was - not in the ASCII character set ( now user $wgContLang->ucfirst() instead of - PHP ucfirst() ) -* Moved the user name / password validity checking from - LoginForm::addNewAccountInternal() to two new functions, - User::isValidUserName() and User::isValidPassword(), extensions can now do - these checks without rewriting code. -* Fix $wgSiteNotice when MediaWiki:Sitenotice is set to default '-' -* Fixed a bug where the watchlist count without talk pages would be off by a - factor of two. -* upgrade1_5.php uses insert ignore, allows to skip image info initialization -* Fix namespaces in category list. -* Add rebuildImages.php to update image metadata fields -* Special:Ancientpages is expensive in new schema for now -* (bug 2568) Fixed a logic error in the Special:Statistics code which caused - the displayed percentage of admins to be totally off. -* (bug 2560) Don't show blank width/height attributes for missing size -* Don't show bogus messages about watchlist notifications when disabled -* Don't show old debug messages in watchlist -* (bug 2576) Fix recording of transclusion links -* (bug 2577) Allow sysops to enter non-standard block times -* Fixed a bug where Special:Contributions wouldn't remember the 'invert' - status between next/previous buttons. -* Move MonoBook printable link from tab to sidebar -* (bug 2567) Fix HTML escaping on category titles in list -* (bug 2562) Show rollback link for current revisions on diff pages -* (bug 2583) Add --missinig option on rebuildImages.php to add db entries - for uploaded files that don't have them -* (bug 2572) Fix edit conflict handling -* (bug 2595) Show "Earlier" and "Latest" links on history go to the first/last - page in the article history pager. -* Don't show empty-page text in 'Show changes' on new page -* (bug 2591) Check for end, fix limits on Whatlinkshere -* (bug 2584) Fix output of subcategory list -* (bug 2597) Don't crash when undeleting an image description page -* (bug 2564) Don't show "editingold" warning for recent revision -* Various code cleanup and HTML escaping fixlets -* Copy IRC-over-UDP update option from REL1_4 -* (bug 2548) Keep summary on 'show changes' of section edit -* Move center on toc to title part to avoid breaking .toc style usage -* HTML sanitizer: correct multiple attributes by keeping last, not first -* (bug 2614) Fix section edit links on diff-to-current with oldid set - Also fix navigation links on current-with-oldid view. -* (bug 2620) Return to prior behavior for some more things (such as - subpage parent links) on current-diff view. -* (bug 2618) Fix regression from another fix; show initial preview for - categories only if the page does not exist. -* (bug 2625) Keep group & user settings when paging in Listusers -* (bug 2627) Fix regression: diff radio button initial selection -* Copy fix for old search URLs with Lucene search plugin from REL1_4 -* (bug 619) Don't use incompatible diff3 executable on non-Linux systems. -* (bug 2631) Fix Hebrew namespaces. -* (bug 2630) Indicate no-longer-valid cached entries in BrokenRedirects list -* (bug 2644, 2645) "cur" diff links in page history, watchlist and - recentchanges should specify current ID explicitly. -* (bug 2609) Fix text justification preferenced with MonoBook skin. -* (bug 2594) Display article tab as red for non-existent articles. -* (bug 2656) Fix regression: prevent blocked users from reverting images -* (bug 2629) Automatically capitalize usernames again instead of - rejecting lowercase with a useless error message -* (bug 2661) Fix link generation in contribs -* Add support for &preload=Page_name (load text of an existing page into -edit area) and &editintro=Page_name (load text of an existing page instead -of MediaWiki:Newpagetext) to &action=edit, if page is new. -* (bugs 2633, 2672, 2685, 2695) Fix Estonian, Portuguese, Italian, Finnish and - Spanish numeric formatting -* Fixed Swedish numeric formatting -* (bug 2658) Fix signature time, localtime to match timezone offset again -* Files from shared repositories (e.g. commons) now display with their - image description pages when viewed on local wikis. -* Restore compatibility namespace aliases for French Wikipedia -* Fix diff order on Enhanced RC 'changes' link -* (bug 2650) Fix national date type display on wikis that don't support - dynamic date conversion. -* FiveUpgrade: large table hacks, install iw_trans update before links -* (bug 2648) Rename namespaces in Afrikaanse -* Special:Booksources checks if custom list page exists before using it -* (bug 1170) Fixed linktrail for da: and ru: -* (bug 2683) Really fix apostrophe escaping for toolbox tips -* (bug 923) Fix title and subtitle for rclinked special page -* (bug 2642) watchdetails message in several languages used <a></a> instead of [ ] -* (bug 2181) basic CSB language localisation by Tomasz G. Sienicki (thanks for the patch) -* Fix correct use of escaping in edit toolbar bits -* Removed language conversion support from Icelandic -* (bug 2616) Fix proportional image scaling, giving correct height -* (bug 2640) Include width and height attributes on unscaled images -* Workaround for mysterious problem with bogus epoch If-Last-Modified reqs -* (bug 1109) Suppress compressed output on 304 responses -* (bug 2674) Include some site configuration info in export data: - namespaces definitions, case-sensitivity, site name, version. -* Use xml:space="preserve" hint on export <text> elements -* Make language variant selection work again for zh - -== Changes since 1.5beta2 == - -* Escaped & correctly in Special:Contributions -* (bug 2534) Hide edit sections with CSS to make right click to edit section work -* (bug 2708) Avoid undefined notice on cookieless login attempt -* (bug 2188) Correct template namespace for Greek localization -* Fixed number formatting for Dutch -* (bug 1355) add class noprint to commonPrint.css -* (bug 2350) Massive update for Limburgish (li) language using Wikipédia -* Massive update for Arab (ar) language using Wikipédia -* (bug 1560) Massive update for Kurdish (ku) language using Wikipédia -* (bug 2709) Some messages were not read from database -* (bug 2416) Don't allow search engine robots to index or follow nonexisting articles -* Fix escaping in page move template. -* (bug 153) Discrepancy between thumbnail size and <img> height attribute - -== Changes since 1.5beta3 == - -* Fix talk page move handling -* (bug 2721) New language file for Vietnamese with the Vietnamese number notation -* (bug 2749)   would appear as a literal in image galleries for Cs, Fr, Fur, Pl and Sv -* (bug 787) external links being rendered when they only have one slash -* Fixed a missing typecast in Language::dateFormat() that would cause some - interesting errors with signitures. -* (bug 2764) Number format for Nds -* (bug 1553) Stop forcing lowercase in Monobook skin for German language. -* (bug 1064) Implements Special:Unusedcategories -* (bug 2311) New language file for Macedonian -* Fix nohistory message on empty page history -* Fix fatal error in history when validation on -* Cleaned up email notification message formatting -* Finally fixed Special:Disambiguations that was broke since SCHEMA_WORK -* (bug 2761) fix capitalization of "i" in Turkish -* (bug 2789) memcached image metadata now cleared after deletion -* Add serialized version number to image metadata cache records -* (bug 2780) Fix thumbnail generation with GD for new image schema -* (bug 2791) Slovene numeric format -* (bug 655) Provide empty search form when searching for nothing -* Nynorsk numeric format fix -* (bug 2825) Fix regression in newtalk notifications for anons w/ enotif off -* (bug 2833) Fix bug in previous fix -* With $wgCapitalLinks off, accept off-by-first-letter-case in 'go' match -* Optional parameters for [[Special:Listusers]] -* (bug 2832) [[Special:Listadmins]] redirects to [[Special:Listusers/sysop]] -* (bug 785) Parser did not get out of <pre> with list elements -* Some shared upload fixes -* (bug 2768) section=new on nonexistent talk page does not add heading -* support preload= parameter for section=new -* show comment subject in preview when using section=new -* use comment form when creating a new talk page -* (bug 460) Properly handle <center> tags as a block. -* Undo inconsistent editing behavior change -* (bug 2835) Back out fix for bug 2802, caused regressions in category sort -* PHP 4.1.2 compatibility fix: define floatval() equivalent if missing -* (bug 2901) Number format for Catalan -* Special:Allpages performance hacks: index memcached caching, removed - inverse checkbox, use friendlier relative offsets in index build -* Bring back "Chick" skin for mobile devices. It needs testing. -* Fix spelling of $wgForwardSearchUrl in DefaultSettings.php -* Specify USE INDEX on Allpages chunk queries, sometimes gets lost - due to bogus optimization -* (bug 275) Section duplication fix -* Remove unused use of undefined variable in UserMailer -* Fix notice on search index update due to non-array -* (bug 2885) Fix fatal errors and notices in PHP 5.1.0beta3 -* (bug 2931) Fix additional notices on reference use in PHP 4.4.0 -* (bug 2774) Add three new $wgHooks to LogPage which enable extensions to add - their own logtypes, see extensions/Renameuser/SpecialRenameuser.php for an - example of this. -* (bug 740) Messages from extensions now appear in Special:Allmessages -* (bug 2857) fixed parsing of lists in <pre> sections -* (bug 796) Trackback support -* Fix 1.5 regression: weird, backwards diff links on new pages in enhanced RC - are now suppressed as before. -* New skin: Simple -* "uselang" and "useskin" URL parameters can now be used in the URL when - viewing a page, to change the language and skin of a page respectively. -* Skins can now be previewed in preferences -* (bug 2943) AuthPlugin::getCanonicalName() name canonicalization hook, - patch from robla -* Wrap revision insert & page update in a transaction, rollback on late - edit conflict. -* (bug 2953) 'other' didn't work in Special:Blockip when localized -* (bug 2958) Rollback and delete auto-summary should be in the project's - content language -* Removed useless protectreason message -* Spelling fix: $wgUrlProtcols -> $wgUrlProtocols -* Switch Moldovan local name to cyrillic -* Fix typo in undefined array index access prevention -* (bug 2947) Update namespaces for sr localization -* (bug 2952) Added Asturian language file with translated namespaces -* (bug 2676) Apply a protective transformation on editing input/output - for browsers that hit the Unicode blacklist. Patch by plugwash. -* (bug 2999) Fix encoding conversion of pl_title in upgrade1_5.php -* compressOld.php disabled, as it's known to be broken. - - -=== Changes since 1.5beta4 === - -* Fix Special:Allmessages under PHP 5 -* (bug 2911) Special:Watchlist allowed only one type of limit at a time -* (bug 693) Special:Allmessages is excessively wide and redundant -* (bug 3001) Updated and applied live hack for recentchanges-based watchlist -* (bug 145) Finish 'exclude redirect' implementation in search form -* Rearranged Special:Movepage form to reduce confusion between destination - title and reason input boxes -* (bug 2527) Always set destination filename when new file is selected -* (bug 3056) MySQL 3 compatibility fix: USE INDEX instead of FORCE INDEX -* PHP 4.1 compatibility fix: don't use new_link parameter to mysql_connect - if running prior to 4.2.0 as it causes the call to fail -* (bug 3117) Fix display of upload size and type with tidy on -* (bug 1487) invalid html on empty list in banlist -* (bug 3017) Hotkey conflict for delete and show changes -* made pixel unit translateable and blocklistline now eats infiniteblock - and expiringblock -* (bug 3092) Wrong numerical separator for big numbers in Serbian. -* (bug 2855) Credit for a uniq author showed its realname even with - $wgAllowRealName=false. -* New special page: SpecialMostlinked -* (bug 2393) Fix MIME type for Atom feeds ( application/rss+atom ) -* Fix display of read-only lockfile message -* Added a new hook, 'AddNewAccount', which is run after account creation -* Update all stats fields on recount.sql -* Include software-visible client IP address in Special:Version comment - as a proxy debugging aid -* (bug 3162) Fix 'undefined property page_is_new' error on watchlist -* (bug 1734) granting db permissions failed with db usernames containg '-' -* (bug 3170) wikititlesuffix was removed, use pagetitle instead -* (bug 3187) watchlist text refer to unexistent "Stop watching" action -* (bug 3190) Added some date format choices for language sr -* (bug 1334) LanguageGa.php update -* (bug 1020) Changing user interface language does not work immediately -* (bug 2753) Some namespaces were not translated in LanguageTa.php (Tamil) -* (bug 3204) Fix typo breaking special pages in fy localization -* (bug 3210) Fix Media: links with remote image URL path -* (bug 3220) Fix escaping of block URLs in Recentchanges -* (bug 3238): Updated LanguageNn.php for 1_5 branch -* (bug 3192): properly check 'limit' parameter on Special:Contributions -* (bug 3244) Fix remote image loading hack, JavaScript injection on MSIE -* Fix URL sanitization in HTML attributes, which broke in this branch -* (bug 3475) anon contrib links on Special:Newpages - - -=== Changes since 1.5rc2 === - -* Fix upgrade from 1.4 due to version number check breakage -* Fix upgrade from 1.4 with no old revisions -* (bug 2108) Sort entries when using category browser -* XSS issue : now sanitize search query input - - -=== Changes since 1.5rc3 === - -* (bug 3280) Respect 'move' group permission on page moves -* (bug 2885) More PHP 5.1 fixes: skin, search, log, undelete -* Security fix for <math> -* Security fix for tables - - -=== Changes since 1.5rc4 === - -* (bug 3292) Fix move-over-redirect test when current entries are not plaintext -* (bug 2078) Don't hide watch tab on preview -* (bug 3306) Document $wgLocalTZoffset -* Support SVG rendering with rsvg -* Cap arbitrary SVG renders to given image size or $wgSVGMaxSize pixels wide -* (bug 3127) Render large SVGs at image page size correctly -* (bug 3448) Set page_len on undelete -* (bug 2800) Don't scale up small iamges on |thumb| without explicit size -* Use the real file link instead of the default-size rasterized version for - large SVG images on image description page -* Include the file name/type/size line for non-resized images -* (bug 3412) Clean up date format handling so ~~~~-sigs work with default - format as designed. Documentation comments updated. -* (bug 1423) LanguageJa.php update -* (bug 3405) Don't use raw letters as aliases of MSGNW: and SUBST: -* (bug 3485) Fix bogus warning about filename capitalization when off -* (bug 2792) Update rebuildrecentchanges.inc for new schema -* Special:Import/importDump fixes: report XML parse errors, accept <minor/> -* (bug 3489) PHP 5.1 compat problem with captioned images -* (bug 3350) Missing label for move talk page checkbox. -* (bug 2570) Add 'watch this page' checkbox on uploads, watch uploads - by default when 'watchdefault' option is on -* (bug 3182) Clear link cache during import to prevent memory leak -* (bug 3573) Full Greek Translation -* (bug 3595) Warn and abort if importDump.php called in read-only mode. -* (bug 3598) Update message cache on message page deletion, patch by Tietew -* Blacklist additional MSIE CSS safety tricks - - -=== Changes since 1.5.0 === - -* (bug 3629) Fix date & time format for Frisian -* (bug 3641) Fix handling of unrecognized file uploads with known extensions -* (bug 3643) Fix image page display of large images with resizing disabled -* Fix meta robots tag on Special:Version again to avoid listing vulnerable - versions for convenient harvesting by automated worms -* (bug 3684) Fix typo in fatal error backtraces in Hooks.php -* Backport fix for reference usage notice in Special:Search on PHP 4.4.0 -* Backport database connect error display fix from HEAD -* (bug 2773) Print style sheet no longer overrides RTL text direction -* MonoBook skin top link id changed from "contentTop" to "top" (shared with - name attribute) -* Wrap message page insertions in a transaction to speed up installation -* Fix Special:MovePage invalid HTML attribute for reason textarea -* Avoid notice warning on edit with no User-Agent header -* (bug 3734) Swapped out obsolete recount.sql with initStats.php -* (bug 3735) Fix to run under MySQL 5's strict mode -* (bug 3786) Experimental support for MySQL 4.1/5.0 utf8 charset mode - NOTE: Enabling this may break existing wikis, and still doesn't - work for all Unicode characters due to MySQL limitations. -* Sanitizer CSS comment processing order fix - - -=== Changes since 1.5.1 === - -* Fix Special:BrokenRedirects on MySQL 5.0 -* (bug 3809) Backport fix for detecting diff3 failure -* MySQL 5.0 strict mode fix for moving unwatched pages -* (bug 3782) Throw fatal installation warning if mbstring.func_overload on. - Why do people invent these crazy options that change language semantics? -* (bug 3762) Define missing Special:Import UI messages -* (bug 3771) Handle internal functions in backtrace in wfAbruptExit() -* (bug 3649) Remove obsolete, broken moveCustomMessages script -* (bug 3667) Add missing global in page move code -* (bug 3761) Avoid deprecation warnings in Special:Import -* (bug 2885) Remove unnecessary reference parameter which broke classic skin - talk notification on PHP 5.0.5 -* (bug 3845) Update attribute.php for 1.5 schema -* Fix Parser::unstrip on PHP 4.4.1 and PHP 5.1.0RC4 - - -=== Changes since 1.5.2 === - -* (bug 3612) Remove old broken version of maintenance/compressOld.php - The working version is in maintenance/storage/compressOld.php -* (bug 2740) Accept image deletions on 'enter' submit from MSIE -* (bug 3933) specify XML namespace for Atom 0.3 feeds -* (bug 3939) Don't try to load text for interwiki redirect target -* (bug 3948) Avoid notice warning in debug statement in bad search -* Recognize Special:Search consistently so read whitelist works -* (bug 4013) typo in fr -* (bug 3996) Fix text for new entries in RC RSS/Atom feed -* (bug 2894) Enhanced Recent Changes link fixes -* (bug 3065) Update both watched namespaces when renaming pages -* Move parentheses out of <a> link in Special:Contributions -* (bug 4071) Generate passwords long enough for $wgMinimalPasswordLength -* (bug 4035) Fix prev/next revision links on edit page -* (bug 4165) Correct validation for user language selection (data taint) -* Clearer message in DefaultSettings.php: edit LocalSettings.php instead - - -=== Changes since 1.5.3 === - -* (bug 3805) Clear 'new messages' flag properly in enotif mode - for usernames containing spaces -* (bug 2714) Backlink from special:whatlinkshere was hard set as 'existing' -* (bug 4249) Typo in entities2literals.pl -* (bug 4233) Update for japanese language -* (bug 4279) Small correction to LanguageDa.php -* (bug 4267) Switch dv sd ug ks arc languages to RTL -* (bug 3991) Allow the operation of wikicode on Protect move only text -* Added AutoAuthenticate hook for external User object suppliers -* Parser internal placeholder string now fully randomized for safety - -=== Changes since 1.5.4 === - -* Maintenance script to delete unused user accounts -* Added detection for WMF files (application/x-msmetafile), added this - MIME type to the default blacklist. Prevented inline display of images - which are not of known image types. This is in response to - http://en.wikipedia.org/wiki/Windows_Metafile_vulnerability - -=== Changes since 1.5.5 === - -* (bug 4258) When installing under IIS, $wgArticlePath = "$wgScript?title=$1" - should be set -* (bug 4510) Correct Barnes & Noble bookstore URLs -* (bug 4504) Use site language for namespace name resolution -* Installer fixes from HEAD backported; now uses a more sensible method of - establishing which mySQL user to use, which clears up bug 921 et al. Minor - changes to installer. -* Fix problem reported on mailing list where re-initialising stats didn't work - (can't insert duplicate rows with the same id field) -* (bug 1122) gray out 'older revision' when viewing first article revision. -* Respect database prefix in dumpHTML.inc -* Minor improvements to removeUnusedAccounts.php maintenance script -* Fix for single-digit week numbers from {{CURRENTWEEK}}, broken by PHP 4.4.1 -* Removed read-only check from Database::query() -* Added --conf option to command line scripts, allowing the user to specify a - different LocalSettings.php. - -=== Changes since 1.5.6 === - -* Default main page content improved per bug 4690 -* Fix dependence on hardcoded UNIQ_PREFIX in LanguageConverter.php -* Fixed Special:Unlockdb -* Maintenance script to delete unused text records -* Maintenance script to delete non-current revisions -* Maintenance script to wipe a page and all revisions from the database -* (bug 4768) Wrong Russian translation (typo) -* Performance bugfix: propagate equality manually for Revision fetches -* (bug 4773) PHP fatal error when invalid title passed to Special:Export -* Added missing table defs. for transcache to installer schemas -* (bug 4824) IE7 beta 2 broke compatibility with PNG logo workarounds, - and seems to work ok with other bits. No longer including the IE - workarounds JavaScript for IE 7 and above. -* (bug 2532) Image directory structure migration bug -* (bug 4881) Correction to the fix for 1487; Ipblocklist showed 'no blocks' - message at the end of the list even if there were blocks. -* (bug 4805) Removed more wikipedia-references from LanguageUk.php -* Introduce $wgWantedPagesThreshold per bug 5011; Special:Wantedpages will not - list pages with less than this number of links. Defaults to 1. -* Allow customisation of paging limits for items in categories using the - $wgCategoryPagingLimit global, per bug 4970. -* Improve "nogomatch" text to make it more obvious that a page can be created. -* (bug 5113) Spelling error in French language file -* Don't change the password of the MySQL root user. - -=== Changes since 1.5.7 === - -* (bug 5180) User login page shows inappropriate email blurb -* Add the "AbortNewAccount" hook on account creation; see hooks.txt for more info. -* Update default "exporttext" to reflect that Special:Import exists -* Add links to useful material to the default main page content -* Fix fragment HTML injection +* 1.11 <= 1.11.0rc1 +* 1.10 <= 1.10.1 +* 1.9 <= 1.9.3 +* 1.8 <= 1.8.4 (if $wgEnableAPI has been switched on) + +MediaWiki 1.7 and below are not affected as they do not include +the faulty function, however the BotQuery extension is similarly +vulnerable unless updated to the latest SVN version. + + +== Configuration changes since 1.10 == + +* $wgThumbUpright - Adjust width of upright images when parameter 'upright' is + used +* $wgAddGroups, $wgRemoveGroups - Finer control over who can assign which + usergroups +* $wgEnotifImpersonal, $wgEnotifUseJobQ - Bulk mail options for large sites +* $wgShowHostnames - Expose server host names through the API and HTML comments +* $wgSaveDeletedFiles has been removed, the feature is now enabled unconditionally + +== New features since 1.10 == + +* (bug 8868) Separate "blocked" message for autoblocks +* Adding expiry of block to block messages +* Links to redirect pages in categories are wrapped in + <span class="redirect-in-category"></span> +* Introduced 'ImageOpenShowImageInlineBefore' hook; see docs/hooks.txt for + more information +* (bug 9628) Show warnings about slave lag on Special:Contributions, + Special:Watchlist +* (bug 8818) Expose "wpDestFile" as parameter $1 to "uploaddisabledtext" +* Introducing new image keyword 'upright' and corresponding variable + $wgThumbUpright. This allows better proportional view of upright images + related to landscape images on a page without nailing the width of upright + images to a fix value which makes views for anon unproportional and user + preferences useless +* (bug 6072) Introducing 'border' keyword to the [[Image:]] syntax +* Introducing 'frameless' keyword to [[Image:]] syntax which respects the + user preferences for image width like 'thumb' but without a frame. +* (bug 7960) Link to "what links here" for each "what links here" entry +* Added support for configuration of an arbitrary number of commons-style + file repositories. +* Added a Content-Disposition header to thumb.php output +* Improved thumb.php error handling +* Display file history on local image description pages of shared images +* Added $wgArticleRobotPolicies +* (bug 10076) Additional parameter $7 added to MediaWiki:Blockedtext + containing, the ip, ip range, or username whose block is affecting the +* (bug 7691) Show relevant lines from the deletion log when re-creating a + previously deleted article +* Added variables 'wgRestrictionEdit' and 'wgRestrictionMove' for JS to header +* (bug 9898) Allow viewing all namespaces in Special:Newpages +* (bug 10139) Introduce 'EditSectionLink' and 'EditSectionLinkForOther' hooks; + see docs/hooks.txt for details +* (bug 9769) Provide "watch this page" toggle on protection form +* (bug 9886) Provide clear example "stub link" in Special:Preferences +* (bug 10055) Populate email address and real name properties of User objects + passed to the 'AbortNewAccount' hook +* Show result of Special:Booksources in wiki content language always, it's + normally better maintained than the generic list from the standard message + files +* (bug 7997) Allow users to be blocked from using Special:Emailuser +* (bug 8989) Blacklist 'mhtml' and 'mht' files from upload +* (bug 8760) Allow wiki links in "protectexpiry" message +* (bug 5908) Add "DEFAULTSORTKEY" and "DEFAULTCATEGORYSORT" aliases for + "DEFAULTSORT" magic word +* (bug 10181) Support the XCache object caching mechanism +* (bug 9058) Introduce '--aconf' option for all maintenance scripts, to provide + a path to the AdminSettings.php file +* (bug 8781) Remind users to check file permissions for LocalSettings.php + post-installation +* Use shared.css for all skins and oldshared.css in place of common.css for + pre-Monobook skins. As always, modifications should go in-wiki to MediaWiki: + Common.css and MediaWiki:Monobook.css. +* (bug 8869) Introduce Special:Uncategorizedtemplates +* (bug 8734) Different log message when article protection level is changed +* (bug 8458, 10338) Limit custom signature length to $wgMaxSigChars Unicode + characters +* (bug 10096) Added an ability to query interwiki map table +* On reupload, add a null revision to the image description page +* Group log output by date +* Kurdish interface latin/arabic writing system with transliteration +* Support wiki text in all query page headers +* Add 'Orphanedpages' as an alias to Special:Lonelypages +* (bug 9328) Use "revision-info-current" message in place of "revision-info" + when viewing the current revision of a page, if available +* (bug 8890) Enable wiki text for "license" message +* Throw a showstopper exception when a hook function fails to return a value. + Forgetting to give a 'true' return value is a very common error which tends + to cause hard-to-track-down interactions between extensions. +* Use $wgJobClasses to determine the correct Job to instantiate for a particular + queued task; allows extensions to introduce custom jobs +* (bug 10326) AJAX-based page watching and unwatching has been cleaned up and + enabled by default. +* Added option to install to MyISAM +* (bug 9250) Remove hardcoded minimum image name length of three characters +* Fixed DISPLAYTITLE behaviour to reject titles which don't normalise to the + same title as the current page, and enabled per default +* Wrap site CSS and JavaScript in a <pre> tag, like user JS/CSS +* (bug 10196) Add classes and dir="ltr" to the <pre>s on CSS and JS pages (new + classes: mw-code, mw-css, mw-js) +* (bug 6711) Add $wgAddGroups and $wgRemoveGroups to allow finer control over + usergroup assignment. +* Introduce 'UserEffectiveGroups' hook; see docs/hooks.txt for more information +* (bug 10387) Detect and handle '.php5' extension environments at install time +* Introduce 'ShowRawCssJs' hook; see docs/hooks.txt for more information +* (bug 10404) Show rights log for the selected user in Special:Userrights +* New javascript for upload page that will show a warning if a file with the + "destination filename" already exists. +* Add 'editsection-brackets' message to allow localization (or removal) of the + brackets in the "[edit]" link for sections +* (bug 10437) Move texvc styling to shared.css +* Introduce "raw editing" mode for the watchlist, to allow bulk additions, + removals, and convenient exporting of watchlist contents +* Show "undo" links in page histories +* Option to jump to specified time period in user contributions +* Improved feedback on "rollback success" page +* Show distinct 'namespaceprotected' message to users when namespace protection + prevents page editing +* (bug 9936) Per-edit suppression of preview-on-first edit with "preview=no" +* Allow showing a one-off preview on first edit with "preview=yes" +* (bug 9151) Remove timed redirects on "Return to X" pages for accessibility. +* Link to user logs in toolbox when viewing a user page +* (bug 10508) Allow HTML attributes on <gallery> +* (bug 1962) Allow HTML attributes on <math> +* (bug 10530) Introduce optional "sp-contributions-explain" message for + additional explanation in Special:Contributions +* (bug 10520) Preview licences during upload via AJAX (toggle with + $wgAjaxLicensePreview) +* New Parser::setTransparentTagHook for parser extension and template + compatibility +* Introduced 'ContributionsToolLinks' hook; see docs/hooks.txt for more + information +* Add a message if category is empty +* Add CSS compatibility for Opera 9.5 +* Remove largely untested handheld stylesheet, which was causing more trouble + than good. Proper handheld support will be added at a future date. For now, + display should be acceptable either with CSS turned off or when using a so- + phisticated handheld browser. +* (bug 3173) Option to offer exported pages as a download, rather than displaying + inline, as in most browsers +* Pass the user as an argument to 'isValidPassword' hook callbacks; see + docs/hooks.txt for more information +* Introduce 'UserGetRights' hook; see docs/hooks.txt for more information +* (bug 9595) Pass new Revision to the 'ArticleInsertComplete' and + 'ArticleSaveComplete' hooks; see docs/hooks.txt for more information +* (bug 9575) Accept upload description from GET parameters +* Skip the difference engine cache when 'action=purge' is used while requesting + a difference page, to allow refreshing the cache in case of errors +* (bug 10701) Link to Special:Listusers in default Special:Statistics messages +* Improved file history presentation +* (bug 10739) Users can now enter comments when reverting files +* Improved handling of permissions errors +* (bug 10793) "Mark patrolled" links will now be shown for users with + patrol permissions on all eligible diff pages +* (bug 10655) Show standard tool links for blocked users in block log messages +* Show standard tool links for blocked users in Special:Ipblocklist +* Miscellaneous aesthetic improvements to Special:Ipblocklist +* (bug 10826) Added link trail with Cyrillic characters for Mongolian language +* (bug 10859) Introduce 'UserGetImplicitGroups' hook; see docs/hooks.txt for + more information +* (bug 10832) Include user information when viewing a deleted revision +* (bug 10872) Fall back to sane defaults when generating protection selector + labels for custom restriction levels +* Show edit count in user preferences +* Improved support for audio/video extensions +* (bug 10937) Distinguish overwritten files in upload log +* Introduce 'ArticleUpdateBeforeRedirect' hook; see docs/hooks.txt for more + information +* Confirmation is now required when deleting old versions of files +* (bug 7535) Users can now enter comments when deleting old versions of files +* (bug 11001) Submit Special:Newpages as a GET, rather than a POST request +* The <strong></strong> around links to watched pages in change lists now + has a class - "mw-watched" +* (bug 9002) Provide a "view/restore deleted edits" link on Special:Upload + when a destination filename is provided that corresponds with previous + deleted files +* Make the "invalid special page" message clearer +* Add accesskey 's' and tooltip to 'upload file' button at Special:Upload +* Introduced 'SkinAfterBottomScripts' hook; see docs/hooks.txt for + more information +* (bug 11095) Honour "preview on first edit" preference when preloading + text for a non-existent page +* (bug 11022) Use a more accurate page title for Special:Whatlinkshere and + Special:Recentchangeslinked +* Add link to user contributions in normal watchlist edit mode +* (bug 9426) Add 'newsectionheaderdefaultlevel' message to allow + modification of the heading formatting for new sections when section=new + argument is supplied +* (bug 10836) Add 'newsectionsummary' message to allow modification of the + text that prefixes a new section link in Recent Changes + +== Bugfixes since 1.10 == + +* (bug 9712) Use Arabic comma in date/time formats for Arabic and Farsi +* (bug 9670) Follow redirects when render edit section links to transcluded + templates. +* (bug 6204) Fix incorrect unindentation with $wgMaxTocLevel +* (bug 3431) Suppress "next page" link in Special:Search at end of results +* Don't show unblock form if the user doesn't have permission to use it + (cosmetic change, no vulnerabilities existed) +* Subtitle success message when unblocking a block ID instead of a pseudo link + like [[User:#123|#123]] +* Use the standard HTTP fetch functions when retrieving remote wiki pages + through transwiki, so we can take advantage of cURL goodies if available +* Disable user JavaScript on Special:Userlogin, Special:Resetpass and + Special:Preferences, to avoid a compromised script sniffing passwords, etc. +* (bug 9854, 3770) Clip overflow text in gallery boxes for visual cleanliness + instead of letting it flow outside the box or trigger ugly scroll bars. +* Tooltips for print version and permalink +* Links to the MediaWiki namespace for system messages having their default + values are no longer shown as nonexistent (e.g., in red) +* Special:Ipblocklist differentiates between empty list and no search results. +* (bug 5375) profiling does not respect read-only mode. +* (bug 7070) monobook/user.gif has antialias artifacts +* (bug 9123) Safer way when applying $wgLocalTZoffset +* (bug 9896) Documentation for $wgSquidServers and X-FORWARDED-FOR +* (bug 9417) Uploading new versions of images when using Postgres no longer + throws warnings. +* (bug 9908) Using tsearch2 with Postgres 8.1 no longer gives an error. +* (bug 1438) Fix for diff table layout on very wide lines. + Diff style rules have been broken out to common/diff.css, + and the dupes removed from the default skin files. + Skins can still override the default rules. +* (bug 1229) Balance columns in diff display evenly +* Right-align diff line numbers in RTL language display +* (bug 9332) Fix instructions in tests/README +* (bug 9813) Reject usernames containing '#' to avoid silent truncation + of fragments during the normalisation process +* (bug 7989) RSS feeds content now use black text when using white background. +* (bug 9971) Typo in a french language message. +* (bug 9973) Changed size was shown in advanced recentchanges collapsible items + with $wgRCShowChangedSized = false. +* Fix PHP strict standards warning in enhanced recent changes. +* (bug 5850) Added hexadecimal html entities comments for $digitTransformTable + entries. +* (bug 7432) Change language name for Aromanian (roa-rup) +* (bug 908) Unexistent special pages now generate a red link. +* (bug 7899) Added \hline and \vline to the list of allowed TeX commands +* (bug 7993) support mathematical symbol classes +* (bug 10007) Allow Block IP to work with Postgrs again. +* Add Google Wireless Transcoder to the Unicode editing blacklist +* (bug 10083) Fix for Special:Version breakage on PHP 5.2 with some hooks +* (bug 3624) TeX: \ker, \hom, \arg, \dim treated like \sin & \cos +* (bug 10132, 10134) Restore back-compatibility Image::imageUrl() function +* (bug 10113) Fix double-click for view source on protected pages +* (bug 10117) Special:Wantedpages doesn't handle invalid titles in result + set [now prints out a warning] +* (bug 10118) Introduced Special:Mostlinkedtemplates, report which lists + templates with a high number of inclusion links +* (bug 10104) Fixed Database::getLag() for PostgreSQL and Oracle +* (bug 9820) session.save_path check no longer halts installation, but + warns of possible bad values +* (bug 9978) Fixed session.save_path validation when using extended + configuration format, e.g. "5;/tmp" +* Don't generate a diff link in the patrol log if the page doesn't exist +* (bug 10067) Translations for former skins removed from message files +* (bug 9993) Force $wgShowExceptionDetails on during installation +* (bug 9980) Validate administrator username and password during + installation +* (bug 9383) Don't set a default value for BLOB column in rc-deleted + database patch +* (bug 10149) Don't show full template list on section-0 edit +* (bug 9909) Ensure access to binary fields in the math table use encodeBlob() + and decodeBlob() +* (bug 6743) Don't link broken image links to the upload form when uploads + are disabled +* (bug 9679) Improve documentation for $wgSiteNotice +* (bug 10215) Show custom editing introduction when editing existing pages +* (bug 10223) Fix edit link in noarticletext localizations for fr, oc +* (bug 10247) Fix IP address regex to avoid false positive IPv6 matches +* (bug 9948) Workaround for diff regression with old Mozilla versions +* (bug 10265) Fix regression in category image gallery paging +* (bug 8577) Fix some weird misapplications of time zones. + {{CURRENT*}} functions now consistently use UTC as intended, while + {{LOCAL*}} functions return local time per server config or $wgLocaltimezone. + Signature dates for Japanese and other languages including weekday now show + the correct day to match the rest of the time in local time. +* Escape the output of magic variables that return page name or part of it +* (bug 10309) Initialise parser state properly in extractSections(), fixes + some cases where section edits broke because tags were improperly stripped +* Avoid PHP notice errors when doing HTTP proxy purges for an empty list +* As intended, *skip* the HTTP proxy purges when doing HTCP purges +* (bug 9696) Fix handling of brace transformations in "pagemovedtext" +* (bug 10325) Fix regression in form action on Special:Listusers +* Fixed installation on MyISAM or old InnoDB with charset=utf8, was giving + overlong key errors. +* Fixed zero-padding issues with MySQL 5 binary schema +* (bug 10344) Don't follow a redirect after changing its protection level +* (bug 10333) Correct date format in Slovenian +* (bug 10160) Show error message for unknown namespace on Special:Allpages and + Special:Prefixindex; making forms prettier for RTL wikis. +* (bug 10334) Replace normal spaces before percent (%) signs with non-breaking + spaces +* (bug 10372) namespaceDupes.php no longer ignores namespace aliases +* (bug 10198) namespaceDupes.php no longer ignores interwiki prefixes +* namespaceDupes.php should work better for initial-lowercase wikis +* (bug 10377) "Permanent links" to revisions still work if the page is moved + and the redirect deleted +* (bug 7071) Properly handle an 'oldid' passed to view or edit that doesn't + match the given title. Fixes inconsistencies with talk, history, edit links. +* (bug 10397) Fix AJAX watch error fallback when we receive a bogus result +* (bug 10396) Fix AJAX error when $wgScriptPath/index.php is not valid; + using $wgScript now included in JS info +* Use native XMLHttpRequest class in preference to ActiveX on IE 7; this + avoids the "ActiveX "Do you want to allow ActiveX?" prompt when something + security settings are cranked this way and AJAX-y gets used. +* Delay AJAX watch initialization until click so IE 6 with ugly security + settings doesn't prompt you until you use the link. +* (bug 10401) Provide non-redirecting link to original title in Special:Movepage +* Fix broken handling of log views for page titles consisting of one + or more zeros, e.g. "0", "00" etc. +* Fix read permission check for special pages with subpage parameters, e.g. + Special:Confirmemail +* Fix read permission check for unreadable page titles which are numerically + equivalent to a whitelisted title +* '?>' closing tag removed from all files to help avoid problems with extraneous + whitespace (broken XML feeds, etc.) +* Don't use garbled parser cache output when viewing custom CSS or JavaScript + pages +* (bug 10406) Fix Special:Listusers filter form for non-ASCII localizations +* Fix empty message checks for message names containing & + This corrects some odd behavior with sidebar items and custom namespaces + containing ampersands. +* (bug 10375) Change thousands separator character to   for Latin (la) +* (bug 10477) Fix AJAX watch for Farsi on Firefox: JavaScript encoding tweak +* (bug 10496) Fix broken DISTINCT option logic in database backend +* Fix CSS media declaration for "screen, projection"; was causing some + validation issues +* (bug 10495) $wgMemcachedDebug set twice in includes/DefaultSettings.php +* (bug 10316) Prevent inconsistent cached skin settings in gen=js by setting + the intended skin directly in the URL. +* (bug 9903) Don't mark redirects in categories as stubs +* (bug 6965) Cannot include "Template:R" with {{R}} (magic word conflict) +* Padding parser functions now work with strings like '0' that evaluate to false +* (bug 10332) Title->userCan( 'edit' ) may return false positive +* Fix bug with <nowiki> in front of links for wikis where linkPrefixExtension is true +* (bug 10552) Suppress rollback link in history for single-revision pages +* (bug 10538) Gracefully handle invalid input on move success page +* Fix for Esperanto double-x-encoding in move success page +* (bug 10526) Fix toolbar/insertTags behavior for IE 6/7 and Opera (8+) + Now matches the selection behavior on Mozilla / Safari. + Patch by Alex Smotrov. +* Don't show non-functional toolbar buttons on Opera 7 anymore +* (bug 9151) Fix relative subpage links with section fragments +* (bug 10560) Adding a space between category letter heading and "continues" +* (bug 4650) Keep impossibly large/small counts off Special:Statistics +* (bug 10608) PHP notice when installing with PostgreSQL +* (bug 10615) Fix for transwiki import when CURL not available +* (bug 8054) Return search page for empty search requests with ugly URLs +* (bug 10572) Force refresh after clearing visitation timestamps on watchlist +* (bug 10631) Warn when illegal characters are removed from filename at upload +* Fix several JavaScript bugs under MSIE 5/Macintosh +* (bug 10591) Use Arabic numerals (0,1,2...) for the Malayam language +* (bug 10642) Fix shift-click checkbox behavior for Opera 9.0+ and 6.0 +* Work around Safari bug with pages ending in ".gz" or ".tgz" +* Removed obsolete maintenance/changeuser.sql script; use RenameUser extension +* (bug 2735) "Preview" shown in title bar for action=submit on special pages +* Removed "restore" links from the deletion log embedded in Special:Undelete +* Improved error reporting and robustness for file delete/undelete. +* Improved speed of file delete by storing the SHA-1 hash in image/oldimage +* Fixed leading zero in base 36 SHA-1 hash +* Protection form no longer produces JavaScript errors +* (bug 10741) File histories show "delete" links for non-sysops +* (bug 10744) Treat "noarticletext" and "noarticletextanon" as wiki text when + used on a non-existent page with "action=info" +* Fix escaping of raw message text when used on a non-existent page with + "action=info" +* (bug 10683) Fix inconsistent handling of URL-encoded titles in links + used in redirects (i.e. they now work) +* (bug 8878) Changes to $dateFormats in German localization (removing unused, + nonexistent formats, putting time after date) +* (bug 10769) Database::update() should return boolean result +* Fix preference checkbox display for right-to-left languages which caused + them to be hidden in IE in some cases +* Fix upload form display in right-to-left languages +* Fixed regression in blocking of username '0' +* (bug 9437) Don't overwrite edit form submission handler when setting up + edit box scroll position preserve/restore behaviour +* (bug 10805) Fix "undo" link when viewing the diff of the most recent + change to a page using "diff=0" +* (bug 10765) img_auth.php will now refuse logged-out requests where + $wgWhitelistRead is undefined, instead of (incorrectly) honouring them +* Fixed img_auth.php file name extraction for whitelist checking +* Tweak spacing of email preference display +* Table sorting JavaScript prefers textContent over innerText to allow hidden + sort keys to work on Safari +* (bug 4530) Fix local name of Kurdish language +* (bug 10830) Fix local name of Haitian Creole language +* Fix invalid XHTML in Special:Protectedpages +* Fix comments in contributions and log pages for right-to-left languages +* Make installer include_path-independent, so it should work on hosts which + disable user setting of PHP include_path setting +* glob() is horribly unreliable and doesn't work on some systems, including + free.fr shared hosting. No longer using it in Language::getLanguageNames() +* (bug 10763) Fix multi-insert logic for PostgreSQL +* Fix invalid XHTML when viewing a deleted revision +* Fix syntax error in translations of magic words in Romanian language +* (bug 8737) Fix warnings caused by incorrect use of `/dev/null` when piping + process error output under Windows +* (bug 7890) Don't list redirects to special pages in Special:BrokenRedirects +* (bug 10783) Resizing PNG-24 images with GD no longer causes all alpha + channel transparency to be lost and transparent pixels to be turned black +* (bug 9339) General error pages were transforming messages and their parameters + in the wrong order +* (bug 9026) Incorrect heading numbering when viewing Special:Statistics with + "auto-numbered headings" enabled +* Fixed invalid XHTML in Special:Upload +* (bug 11013) Make sure dl() is available before attempting to use it to check + available databases in installer +* Resizing transparent GIF images with GD now retains transparency by skipping + resampling +* (bug 11065) Fix regression in handling of wiki-formatted EXIF metadata +* Double encoding broke Special:Newpages for some languages +* Adding a newline before the statistics footer, to prevent parsing problems +* Preventing the TOC from appearing in Special:Statistics +* (bug 11082) Fix check for fully-specced table names in Database::tableName +* (bug 11067) Fix regression in upload conflict thumbnail display +* (bug 10985) Resolved cached entries on Special:DoubleRedirects were being + supressed, breaking paging - now strikes out "fixed" results +* (bug 8393) <sup> and <sub> need to be preserved (without attributes) for + entries in the table of contents +* (bug 11114) Fix regression in read-only mode error display during editing +* Force non-MySQL databases to use an ORDER BY in SpecialAllpages to ensure + that the first page_title is truly the first page title. +* (bug 10836) Change the summary on creating of new section +* Inclusion of Special:Wantedpages now works again + +== API changes since 1.10 == + +Full API documentation is available at http://www.mediawiki.org/wiki/API + +* New properties: links, templates, images, langlinks, categories, external + links +* Breaking Change: imagelinks renamed into imageusage (il->iu) +* Bug fix: incorrect generator behavior in some cases +* JSON format allows an optional callback function to wrap the result. +* Login module disabled until a more secure solution can be implemented +* (bug 9938) Querying by revision identifier returns the most recent revision + for the corresponding page, rather than the requested revision +* (bug 8772) Filter page revision queries by user +* (bug 9927) User contributions queries do not accept IP addresses +* Watchlist feed now reports a proper feed item when the user is not logged in +* Watchlist feed date bug fixed - automatically shows one last day +* Watchlist feed now allows to specify number of hours to monitor +* list=allpages now returns a list instead of a map in JSON format +* Breaking Change: in json, revisions are now returned as a list, not as a map. +* Add: prop=info can show page is new flag, current page length, and visit + counter. +* Change: Query watchlist now shows flags only when explicitly requested with + wlparam=flags +* rc_this_oldid (textid) is no longer accessible from query watchlist +* action=usercontribs: additional filtering by ucshow=; selection of needed + fields with ucprop=; the textid (rev_text_id) is no longer being exposed +* (bug 9970) Breaking Change: backlinks, embeddedin and imageusage now return + lists in JSON instead of a map, and do not return anything when titles do + not exist +* (bug 9121) Introduced indexpageids query parameter to list the page_id + values of all returned page items +* (bug 10147) Now interwiki titles are not processed but added to a separate + "interwiki" section of the output. +* Added categorymembers list to query for pages in a category. +* (bug 10260) Show page protection status +* (bug 10392) Include MediaWiki version details in version output +* (bug 10411) Site language in meta=siteinfo +* (bug 10391) action=help doesn't return help if format is fancy markup +* backlinks, embeddedin and imageusage lists should use (bl|ei|iu)title parameter + instead of titles. Titles for these lists is obsolete and might stop working soon. +* Added prop=imageinfo - gets image properties and upload history +* (bug 10211) Added db server replication lag information in meta=siteinfo +* Added external url search within wiki pages (list=exturlusage) +* Added link enumeration (list=alllinks) +* Added registered users enumeration (list=allusers) +* Added full text search in titles and content (list=search) +* (bug 10684) Expanded list=allusers functionality +* Possible breaking change: prop=revisions no longer includes pageid for rvprop=ids +* Added rvprop=size to prop=revisions (The size will not be shown if it is NULL in the database) +* list=allpages now allows to filter by article min/max size and protection status +* Added site statistics (siprop=statistics for meta=siteinfo) +* (bug 10902) Unable to fetch user contributions from IP addresses +* `list=usercontribs` no longer requires that the user exist +* (bug 10971) `aufrom` parameter doesn't work with spaces +* Fix username handling issue with `auprefix` parameter +* Treat underscores as spaces for `aufrom` and `auprefix` parameters +* Added edit/delete/... token retrieval to prop=info +* Added meta=userinfo - logged-in user information, group membership, rights +* (bug 11072) Fix regression in API image history query +* (bug 11115) Adding SHA1 hash to imageinfo query +* (bug 10898) API does not return an edit token for non-existent pages +* (bug 10890) Timestamp support for categorymembers query +* (bug 10980) Add exclude redirects on backlinks +* IPv6 titles in User namespace are normalized (run cleanupTitles.php to fix any old stray pages) + +== Maintenance script changes since 1.10 == + +* Add support for wgMaxTocLevel option in parserTests +* (bug 6823) Disable article view counter in maintenance/dumpHTML.php +* Fix maintenance/importImages.php so it doesn't barf PHP errors when no + suitable files are found, and make the list of extensions an option (defaults + to $wgFileExtensions) +* Add option to maintenance/createAndPromote.php to give the user bureaucrat + permissions (--bureaucrat) +* Allow overwriting existing files with a conflicting name using + maintenance/importImages.php +* (bug 10266) Use native newlines when rebuilding a messages file. + +== Languages updated since 1.10 == + +* Afrikaans (af) +* Arabic (ar) +* Bikol (bcl) +* Bulgarian (bg) +* Catalan (ca) +* Danish (da) +* German (de) +* Greek (el) +* Esperanto (eo) +* Spanish (es) +* Estonian (et) +* Extremaduran (ext) +* Farsi (fa) +* Finnish (fi) +* Vöro (fiu-vro) +* French (fr) +* Français Cadien (frc) (new) +* Franco-Provençal/Arpetan (frp) +* Galician (gl) +* Hakka (hak) +* Hebrew (he) +* Upper Sorbian (hsb) +* Haitian (ht) +* Indonesian (id) +* Icelandic (is) +* Italian (it) +* Japanese (ja) +* Georgian (ka) +* Kabyle (kab) +* Kazakh (kk) +* Korean (ko) +* Kinaray-a (krj) (new) +* Kurdish (ku) +* Latin (la) +* Lao (lo) +* Lithuanian (lt) +* Latviešu (lv) +* Malayalam (ml) +* Bahasa Melayu (ms) +* Burmese (my) +* Low German (nds) +* Dutch (nl) +* Norwegian (no) +* Occitan (oc) +* Punjabi (Gurmukhi) (pa) +* Polish (pl) +* Piedmontese (pms) +* Portuguese (pt) +* Romani (rmy) +* Romanian (ro) +* Aromanian (roa-rup) +* Russian (ru) +* Sakha (sah) +* Sango (se) (new) +* Slovak (sk) +* Slovenian (sl) +* Shona (sn) +* Somali (so) +* Albanian (sq) +* Sundanese (su) +* Swedish (sv) +* Tamil (ta) +* Thai (th) +* Tigrinya (ti) +* Setswana (tn) +* Tok Pisin (tpi) +* Uyghur (ug) +* Volapük (vo) +* Winaray (war) (new) +* Yiddish (yi) +* Old Chinese / Late Middle Chinese (zh-classical) +* Chinese (PRC) (zh-cn) +* Chinese (Taiwan) (zh-tw) +* Cantonese (zh-yue) + +== Compatibility == + +MediaWiki 1.11 requires PHP 5 (5.1 recommended). PHP 4 is no longer supported. + +PHP 5.0.x fails on 64-bit systems due to serious bugs with array processing: +http://bugs.php.net/bug.php?id=34879 +Upgrade affected systems to PHP 5.1 or higher. + +MySQL 3.23.x is no longer supported; some older hosts may need to upgrade. +At this time we still recommend 4.0, but 4.1/5.0 will work fine in most cases. + + +== Upgrading == + +1.11 has several database changes since 1.10, and will not work without schema +updates. + +If upgrading from before 1.7, you may want to run refreshLinks.php to ensure +new database fields are filled with data. + +If you are upgrading from MediaWiki 1.4.x or earlier, some major database +changes are made, and there is a slightly higher chance that things could +break. Don't forget to always back up your database before upgrading! +See the file UPGRADE for more detailed upgrade instructions. === Caveats === @@ -1120,16 +658,17 @@ MathML to display properly in Mozilla.) -For notes on 1.4.x and older releases, see HISTORY. +For notes on 1.10.x and older releases, see HISTORY. === Online documentation === Documentation for both end-users and site administrators is currently being -built up on Meta-Wikipedia, and is covered under the GNU Free Documentation -License: +built up on MediaWiki.org, and is covered under the GNU Free Documentation +License (except for pages that explicitly state that their contents are in +the public domain) : - http://meta.wikipedia.org/wiki/Help:Contents + http://www.mediawiki.org/wiki/Documentation === Mailing list === @@ -1137,10 +676,11 @@ A MediaWiki-l mailing list has been set up distinct from the Wikipedia wikitech-l list: - http://mail.wikipedia.org/mailman/listinfo/mediawiki-l + http://lists.wikimedia.org/mailman/listinfo/mediawiki-l A low-traffic announcements-only list is also available: - http://mail.wikipedia.org/mailman/listinfo/mediawiki-announce + + http://lists.wikimedia.org/mailman/listinfo/mediawiki-announce It's highly recommended that you sign up for one of these lists if you're going to run a public MediaWiki, so you can be notified of security fixes. diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/UPGRADE mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/UPGRADE --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/UPGRADE 2005-10-05 22:45:41.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/UPGRADE 2007-08-23 14:56:40.000000000 -0400 @@ -1,29 +1,116 @@ -== The basic theory == +This file provides an overview of the MediaWiki upgrade process. For help with +specific problems, check -Generally, within a stable release series (e.g. 1.4.0, 1.4.1, etc) there -are no required database changes, and upgrading should require no more -than copying the new source files over the old ones. - -If there are larger changes, such as upgrading from one release series -to another (e.g. from 1.3.12 to 1.4.3), then you may need to update the -database schema and configuration. - -Basically, to upgrade a wiki you: -* Back up your data! (See Backups! below) -* Extract the new archive. If you can do this in a clean directory that's - great, but it should work to extract over the old files too. This may - be easier if you have images etc in place and don't want to move them - around, but remember to back up first! -* Run the installer to upgrade the database schema (if necessary). +* the documentation at http://www.mediawiki.org +* the mediawiki-l mailing list archive at + http://lists.wikimedia.org/pipermail/mediawiki-l/ +* the bug tracker at http://bugzilla.wikimedia.org +for information and workarounds to common issues. -== IMPORTANT: Upgrading to 1.5 == +== Overview == -Major changes have been made to the schema from 1.4.x. The updater -has not been fully tested for all conditions, and might well break -under some combinations of versions. +Comprehensive documentation on upgrading to the latest version of the software +is available at http://www.mediawiki.org/wiki/Manual:Upgrading_MediaWiki. + +=== Consult the release notes === + +Before doing anything, stop and consult the release notes supplied with the new +version of the software. These detail bug fixes, new features and functionality, +and any particular points that may need to be noted during the upgrade +procedure. + +=== Backup first === + +It is imperative that, prior to attempting an upgrade of the database schema, +you take a complete backup of your wiki database and files and verify it. While +the upgrade scripts are somewhat robust, there is no guarantee that things will +not fail, leaving the database in an inconsistent state. + +http://www.mediawiki.org/wiki/Manual:Backing_up_a_wiki provides an overview of +the upgrade process. You should also refer to the documentation for your +database management system for information on backing up a database, and to +your operating system documentation for information on making copies of files. + +=== Perform the file upgrade === + +Download the files for the new version of the software. These are available +as a compressed "tar" archive from the Wikimedia Download Service +(http://download.wikimedia.org/mediawiki). + +You can also obtain the new files directly from our Subversion source code +repository, via a checkout or export operation. + +Replace the existing MediaWiki files with the new. You should preserve the +LocalSettings.php file, AdminSettings.php file (if present), and the +"extensions" and "images" directories. + +Depending upon your configuration, you may also need to preserve additional +directories, including a custom upload directory ($wgUploadDirectory), +deleted file archives, and any custom skins. + +=== Perform the database upgrade === + +You will need an AdminSettings.php file set up in the correct format; see +AdminSettings.sample in the wiki root for more information and examples. + +From the command line, browse to the "maintenance" directory and run the +update.php script to check and update the schema. This will insert missing +tables, update existing tables, and move data around as needed. In most cases, +this is successful and nothing further needs to be done. + +=== Check configuration settings === + +The names of configuration variables, and their default values and purposes, +can change between release branches, e.g. $wgDisableUploads in 1.4 is replaced +with $wgEnableUploads in later versions. When upgrading, consult the release +notes to check for configuration changes which would alter the expected +behaviour of MediaWiki. + +=== Test === + +It makes sense to test your wiki immediately following any kind of maintenance +procedure, and especially after upgrading; check that page views and edits work +normally and that special pages continue to function, etc. and correct errors +and quirks which reveal themselves. + +You should also test any extensions, and upgrade these if necessary. -NEVER EVER ATTEMPT TO PERFORM AN UPGRADE WITHOUT BACKING UP FIRST! +== Upgrading from 1.8 wikis == + +MediaWiki 1.9 and later no longer keep default localized message text +in the database; 'MediaWiki:'-namespace pages that do not exist in the +database are simply transparently filled-in on demand. + +The upgrade process will delete any 'MediaWiki:' pages which are left +in the default state (last edited by 'MediaWiki default'). This may +take a few moments, similar to the old initial setup. + +Note that the large number of deletions may cause older edits to expire +from the list on Special:Recentchanges, although the deletions themselves +will be hidden by default. (Click "show bot edits" to list them.) + + +See RELEASE-NOTES for more details about new and changed options. + + +== Upgrading from 1.7 wikis == + +$wgDefaultUserOptions now contains all the defaults, not only overrides. +If you're setting this as a complete array(), you may need to change it +to set only specific items as recommended in DefaultSettings.php. + +== Upgrading from 1.6 wikis == + +$wgLocalTZoffset was in hours, it is now using minutes. +Link autonumbering got fixed (#5918) for protocols other than http. + - 'irc://irc.server.tld/' render as a link with a chat icon + - '[irc://irc.server.tld]' render as an autonumbered link: [1] + +== Upgrading from pre-1.5 wikis == + +Major changes have been made to the schema from 1.4.x. The updater +has not been fully tested for all conditions, and might well break. On a large site, the schema update might take a long time. It might explode, or leave your database half-done or otherwise badly hurting. @@ -34,11 +121,18 @@ 'upgrade1_5.php', can do this -- run it prior to 'update.php' or the web upgrader. +If you absolutely cannot make the UTF-8 upgrade work, you can try +doing it by hand: dump your old database, convert the dump file +using iconv as described here: +http://portal.suse.com/sdb/en/2004/05/jbartsh_utf-8.html +and then reimport it. You can also convert filenames using convmv, +but note that the old directory hashes will no longer be valid, +so you will also have to move them to new destinations. + Message changes: -* A number of additional UI messages have been chagned from HTML to +* A number of additional UI messages have been changed from HTML to wikitext, and will need to be manually fixed if customized. - === Configuration changes from 1.4.x: === $wgDisableUploads has been replaced with $wgEnableUploads. @@ -108,19 +202,28 @@ a database password.) To back up the database, use the tools provided by your service provider -(if applicable) or the standard mysqldump program. +(if applicable) or the standard mysqldump or pg_dump programs. For general help on mysqldump: http://dev.mysql.com/doc/mysql/en/mysqldump.html WARNING: If using MySQL 4.1.x, mysqldump's charset conversion may in some cases damage data in your wiki. If necessary, set the charset -option to 'latin1' to avoid the conversion. Fore more info see: -http://mail.wikipedia.org/pipermail/wikitech-l/2004-November/026359.html +option to 'latin1' to avoid the conversion. + +For general help on pg_dump: +http://www.postgresql.org/docs/current/static/app-pgdump.html == Caveats == +=== Postgres === + +Postgres support is new, and much of the upgrade instructions may +not apply. The schema was changed significantly from 1.7 to 1.8, +so you will need to at least use the update.php or web installer, +as described above. + === Upgrading from 1.4.2 or earlier === diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/config/index.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/config/index.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/config/index.php 2006-03-01 21:55:15.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/config/index.php 2007-08-22 11:05:30.000000000 -0400 @@ -16,129 +16,191 @@ # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html error_reporting( E_ALL ); header( "Content-type: text/html; charset=utf-8" ); @ini_set( "display_errors", true ); -?><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" - "http://www.w3.org/TR/html4/loose.dtd"> +# In case of errors, let output be clean. +$wgRequestTime = microtime( true ); + +# Attempt to set up the include path, to fix problems with relative includes +$IP = dirname( dirname( __FILE__ ) ); +define( 'MW_INSTALL_PATH', $IP ); + +# Define an entry point and include some files +define( "MEDIAWIKI", true ); +define( "MEDIAWIKI_INSTALL", true ); + +// Run version checks before including other files +// so people don't see a scary parse error. +require_once( "$IP/install-utils.inc" ); +install_version_checks(); + +require_once( "$IP/includes/Defines.php" ); +require_once( "$IP/includes/DefaultSettings.php" ); +require_once( "$IP/includes/AutoLoader.php" ); +require_once( "$IP/includes/MagicWord.php" ); +require_once( "$IP/includes/Namespace.php" ); +require_once( "$IP/includes/ProfilerStub.php" ); +require_once( "$IP/includes/GlobalFunctions.php" ); +require_once( "$IP/includes/Hooks.php" ); + +# If we get an exception, the user needs to know +# all the details +$wgShowExceptionDetails = true; + +## Databases we support: + +$ourdb = array(); +$ourdb['mysql']['fullname'] = 'MySQL'; +$ourdb['mysql']['havedriver'] = 0; +$ourdb['mysql']['compile'] = 'mysql'; +$ourdb['mysql']['bgcolor'] = '#ffe5a7'; +$ourdb['mysql']['rootuser'] = 'root'; + +$ourdb['postgres']['fullname'] = 'PostgreSQL'; +$ourdb['postgres']['havedriver'] = 0; +$ourdb['postgres']['compile'] = 'pgsql'; +$ourdb['postgres']['bgcolor'] = '#aaccff'; +$ourdb['postgres']['rootuser'] = 'postgres'; + +?> +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8"> - <meta name="robots" content="noindex,nofollow"> - <title>MediaWiki 1.5 Installation + MediaWiki <?php echo( $wgVersion ); ?> Installation - - + @import "../skins/monobook/main.css"; -
        -
        - - - -
        - -

        MediaWiki is - Copyright (C) 2001-2006 by Magnus Manske, Brion Vibber, Lee Daniel Crocker, - Tim Starling, Erik Möller, Gabriel Wicke and others.

        - - - -

        This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version.

        - -

        This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details.

        - -

        You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - or read it online

        -
        + .env-check { + font-size: 90%; + margin: 1em 0 1em 2.5em; + } - + .config-input { + clear: left; + zoom: 100%; /* IE hack */ + } -

        MediaWiki installation

        + .config-section .config-desc { + clear: left; + margin: 0 0 2em 18em; + padding-top: 1em; + font-size: 85%; + } + .iput-text, .iput-password { + width: 14em; + margin-right: 1em; + } + + .error { + color: red; + background-color: #fff; + font-weight: bold; + left: 1em; + font-size: 100%; + } + + .error-top { + color: red; + background-color: #FFF0F0; + border: 2px solid red; + font-size: 130%; + font-weight: bold; + padding: 1em 1.5em; + margin: 2em 0 1em; + } + + ul.plain { + list-style-type: none; + list-style-image: none; + float: left; + margin: 0; + padding: 0; + } + + .btn-install { + font-weight: bold; + font-size: 110%; + padding: .2em .3em; + } + + .license { + font-size: 85%; + padding-top: 3em; + } + + span.success-message { + font-weight: bold; + font-size: 110%; + color: green; + } + .success-box { + font-size: 130%; + } + + + + + + +
        +
        +
        +
        + +

        MediaWiki Installation

        Wiki is configured. - -

        Already configured... return to the wiki.

        + $script = defined('MW_INSTALL_PHP5_EXT') ? 'index.php5' : 'index.php'; + dieout( "

        Setup has completed, your wiki is configured.

        -

        (You should probably remove this directory for added security.)

        " ); +

        Please delete the /config directory for extra security.

        " ); } if( file_exists( "./LocalSettings.php" ) ) { @@ -160,12 +222,14 @@
         	cd /path/to/wiki
         	chmod a+w config
        -	
        " ); + + +

        Afterwards retry to start the setup.

        " ); } -require_once( "install-utils.inc" ); -require_once( "maintenance/updaters.inc" ); +require_once( "$IP/install-utils.inc" ); +require_once( "$IP/maintenance/updaters.inc" ); class ConfigData { function getEncoded( $data ) { @@ -175,31 +239,101 @@ function getSitename() { return $this->getEncoded( $this->Sitename ); } function getSysopName() { return $this->getEncoded( $this->SysopName ); } function getSysopPass() { return $this->getEncoded( $this->SysopPass ); } + + function setSchema( $schema, $engine ) { + $this->DBschema = $schema; + if ( !preg_match( '/^\w*$/', $engine ) ){ + $engine = 'InnoDB'; + } + switch ( $this->DBschema ) { + case 'mysql5': + $this->DBTableOptions = "ENGINE=$engine, DEFAULT CHARSET=utf8"; + $this->DBmysql5 = 'true'; + break; + case 'mysql5-binary': + $this->DBTableOptions = "ENGINE=$engine, DEFAULT CHARSET=binary"; + $this->DBmysql5 = 'true'; + break; + default: + $this->DBTableOptions = "TYPE=$engine"; + $this->DBmysql5 = 'false'; + } + $this->DBengine = $engine; + + # Set the global for use during install + global $wgDBTableOptions; + $wgDBTableOptions = $this->DBTableOptions; + } } ?> -

        Please include all of the lines below when reporting installation problems.

        + +

        Checking environment...

        -
          +

          Please include all of the lines below when reporting installation problems.

          +
            PHP " . phpversion() . " installed\n"; -print "
          • PHP " . phpversion() . ": ok
          • \n"; +error_reporting( 0 ); +$phpdatabases = array(); +foreach (array_keys($ourdb) as $db) { + $compname = $ourdb[$db]['compile']; + if( extension_loaded( $compname ) || ( mw_have_dl() && dl( "{$compname}." . PHP_SHLIB_SUFFIX ) ) ) { + array_push($phpdatabases, $db); + $ourdb[$db]['havedriver'] = 1; + } +} +error_reporting( E_ALL ); + +if (!$phpdatabases) { + print "Could not find a suitable database driver!
              "; + foreach (array_keys($ourdb) AS $db) { + $comp = $ourdb[$db]['compile']; + $full = $ourdb[$db]['fullname']; + print "
            • For $full, compile PHP using --with-$comp, " + ."or install the $comp.so module
            • \n"; + } + dieout( "
          " ); +} + +print "
        • Found database drivers for:"; +$DefaultDBtype = ''; +foreach (array_keys($ourdb) AS $db) { + if ($ourdb[$db]['havedriver']) { + if ( $DefaultDBtype == '' ) { + $DefaultDBtype = $db; + } + print " ".$ourdb[$db]['fullname']; + } +} +print "
        • \n"; if( ini_get( "register_globals" ) ) { ?> -
        • Warning: PHP's - register_globals - option is enabled. MediaWiki will work correctly, but this setting - increases your exposure to potential security vulnerabilities in PHP-based - software running on your server. You should disable it if you are able.
        • +
        • +
          + Warning: + PHP's register_globals option is enabled. Disable it if you can. +
          + MediaWiki will work, but your server is more exposed to PHP-based security vulnerabilities. +
        • Fatal: zend.ze1_compatibility_mode is active! + This option causes horrible bugs with MediaWiki; you cannot install or use + MediaWiki unless this option is disabled. +

          Cannot install wiki.

          " ); + dieout( "

        Cannot install MediaWiki.

        " ); } if( ini_get( "safe_mode" ) ) { @@ -246,22 +389,12 @@ } $sapi = php_sapi_name(); -$conf->prettyURLs = true; print "
      1. PHP server API is $sapi; "; -switch( $sapi ) { -case "apache": -case "apache2handler": - print "ok, using pretty URLs (index.php/Page_Title)"; - break; -default: - print "unknown; "; -case "cgi": -case "cgi-fcgi": -case "apache2filter": -case "isapi": - print "using ugly URLs (index.php?title=Page_Title)"; - $conf->prettyURLs = false; - break; +$script = defined('MW_INSTALL_PHP5_EXT') ? 'index.php5' : 'index.php'; +if( $wgUsePathInfo ) { + print "ok, using pretty URLs ($script/Page_Title)"; +} else { + print "using ugly URLs ($script?title=Page_Title)"; } print "
      2. \n"; @@ -274,20 +407,49 @@ If you're running Mandrake, install the php-xml package." ); } +# Check for session support +if( !function_exists( 'session_name' ) ) + dieout( "PHP's session module is missing. MediaWiki requires session support in order to function." ); + +# session.save_path doesn't *have* to be set, but if it is, and it's +# not valid/writable/etc. then it can cause problems +$sessionSavePath = mw_get_session_save_path(); +$ssp = htmlspecialchars( $sessionSavePath ); +# Warn the user if it's not set, but let them proceed +if( !$sessionSavePath ) { + print "
      3. Warning: A value for session.save_path + has not been set in PHP.ini. If the default value causes problems with + saving session data, set it to a valid path which is read/write/execute + for the user your web server is running under.
      4. "; +} elseif ( is_dir( $sessionSavePath ) && is_writable( $sessionSavePath ) ) { + # All good? Let the user know + print "
      5. Session save path ({$ssp}) appears to be valid.
      6. "; +} else { + # Something not right? Warn the user, but let them proceed + print "
      7. Warning: Your session.save_path value ({$ssp}) + appears to be invalid or is not writable. PHP needs to be able to save data to + this location for correct session operation.
      8. "; +} + +# Check for PCRE support +if( !function_exists( 'preg_match' ) ) + dieout( "The PCRE support module appears to be missing. MediaWiki requires the + Perl-compatible regular expression functions." ); + $memlimit = ini_get( "memory_limit" ); $conf->raiseMemory = false; if( empty( $memlimit ) || $memlimit == -1 ) { print "
      9. PHP is configured with no memory_limit.
      10. \n"; } else { - print "
      11. PHP's memory_limit is " . htmlspecialchars( $memlimit ) . ". If this is too low, installation may fail! "; - $n = IntVal( $memlimit ); + print "
      12. PHP's memory_limit is " . htmlspecialchars( $memlimit ) . ". "; + $n = intval( $memlimit ); if( preg_match( '/^([0-9]+)[Mm]$/', trim( $memlimit ), $m ) ) { - $n = IntVal( $m[1] * (1024*1024) ); + $n = intval( $m[1] * (1024*1024) ); } if( $n < 20*1024*1024 ) { print "Attempting to raise limit to 20M... "; if( false === ini_set( "memory_limit", "20M" ) ) { - print "failed."; + print "failed.
        " . htmlspecialchars( $memlimit ) . " seems too low, installation may fail!"; } else { $conf->raiseMemory = true; print "ok."; @@ -296,32 +458,45 @@ print "
      13. \n"; } -$conf->zlib = function_exists( "gzencode" ); -if( $conf->zlib ) { - print "
      14. Have zlib support; enabling output compression.
      15. \n"; -} else { - print "
      16. No zlib support.
      17. \n"; -} - $conf->turck = function_exists( 'mmcache_get' ); if ( $conf->turck ) { print "
      18. Turck MMCache installed
      19. \n"; } + +$conf->xcache = function_exists( 'xcache_get' ); +if( $conf->xcache ) + print "
      20. XCache installed
      21. "; + +$conf->apc = function_exists('apc_fetch'); +if ($conf->apc ) { + print "
      22. APC installed
      23. "; +} + $conf->eaccel = function_exists( 'eaccelerator_get' ); if ( $conf->eaccel ) { - $conf->turck = 'eaccelerator'; - print "
      24. eAccelerator installed
      25. \n"; + $conf->turck = 'eaccelerator'; + print "
      26. eAccelerator installed
      27. \n"; } -if (!$conf->turck && !$conf->eaccel) { - print "
      28. Neither Turck MMCache nor eAccelerator are installed, " . - "can't use object caching functions
      29. \n"; + +if( !( $conf->turck || $conf->eaccel || $conf->apc || $conf->xcache ) ) { + echo( '
      30. Couldn\'t find Turck MMCache, + eAccelerator, + APC or XCache; + cannot use these for object caching.
      31. ' ); } $conf->diff3 = false; -$diff3locations = array("/usr/bin", "/opt/csw/bin", "/usr/gnu/bin", "/usr/sfw/bin") + explode(":", getenv("PATH")); -$diff3names = array("gdiff3", "diff3"); +$diff3locations = array_merge( + array( + "/usr/bin", + "/usr/local/bin", + "/opt/csw/bin", + "/usr/gnu/bin", + "/usr/sfw/bin" ), + explode( PATH_SEPARATOR, getenv( "PATH" ) ) ); +$diff3names = array( "gdiff3", "diff3", "diff3.exe" ); -$diff3versioninfo = array('$1 --version 2>&1', 'diff3 (GNU diffutils)'); +$diff3versioninfo = array( '$1 --version 2>&1', 'diff3 (GNU diffutils)' ); foreach ($diff3locations as $loc) { $exe = locate_executable($loc, $diff3names, $diff3versioninfo); if ($exe !== false) { @@ -336,7 +511,7 @@ print "
      32. GNU diff3 not found.
      33. "; $conf->ImageMagick = false; -$imcheck = array( "/usr/bin", "/usr/local/bin", "/sw/bin", "/opt/local/bin" ); +$imcheck = array( "/usr/bin", "/opt/csw/bin", "/usr/local/bin", "/sw/bin", "/opt/local/bin" ); foreach( $imcheck as $dir ) { $im = "$dir/convert"; if( file_exists( $im ) ) { @@ -359,16 +534,33 @@ } } -$conf->UseImageResize = $conf->HaveGD || $conf->ImageMagick; - -# $conf->IP = "/Users/brion/Sites/inplace"; $conf->IP = dirname( dirname( __FILE__ ) ); print "
      34. Installation directory: " . htmlspecialchars( $conf->IP ) . "
      35. \n"; -# $conf->ScriptPath = "/~brion/inplace"; -$conf->ScriptPath = preg_replace( '{^(.*)/config.*$}', '$1', $_SERVER["PHP_SELF"] ); # was SCRIPT_NAME + +// PHP_SELF isn't available sometimes, such as when PHP is CGI but +// cgi.fix_pathinfo is disabled. In that case, fall back to SCRIPT_NAME +// to get the path to the current script... hopefully it's reliable. SIGH +$path = ($_SERVER["PHP_SELF"] === '') + ? $_SERVER["SCRIPT_NAME"] + : $_SERVER["PHP_SELF"]; + +$conf->ScriptPath = preg_replace( '{^(.*)/config.*$}', '$1', $path ); print "
      36. Script URI path: " . htmlspecialchars( $conf->ScriptPath ) . "
      37. \n"; + + +// We may be installing from *.php5 extension file, if so, print message +$conf->ScriptExtension = '.php'; +if (defined('MW_INSTALL_PHP5_EXT')) { + $conf->ScriptExtension = '.php5'; + print "
      38. Installing MediaWiki with php5 file extensions
      39. \n"; +} else { + print "
      40. Installing MediaWiki with php file extensions
      41. \n"; +} + + +print "
      42. Environment checked. You can install MediaWiki.
      43. \n"; $conf->posted = ($_SERVER["REQUEST_METHOD"] == "POST"); $conf->Sitename = ucfirst( importPost( "Sitename", "" ) ); @@ -376,30 +568,47 @@ ? 'root@localhost' : $_SERVER["SERVER_ADMIN"]; $conf->EmergencyContact = importPost( "EmergencyContact", $defaultEmail ); + $conf->DBtype = importPost( "DBtype", $DefaultDBtype ); +?> + +DBserver = importPost( "DBserver", "localhost" ); $conf->DBname = importPost( "DBname", "wikidb" ); $conf->DBuser = importPost( "DBuser", "wikiuser" ); $conf->DBpassword = importPost( "DBpassword" ); $conf->DBpassword2 = importPost( "DBpassword2" ); - $conf->DBprefix = importPost( "DBprefix" ); - $conf->DBmysql5 = (importPost( "DBmysql5" ) == "true") ? "true" : "false"; - $conf->RootUser = importPost( "RootUser", "root" ); - $conf->RootPW = importPost( "RootPW", "-" ); - $conf->LanguageCode = importPost( "LanguageCode", "en" ); $conf->SysopName = importPost( "SysopName", "WikiSysop" ); $conf->SysopPass = importPost( "SysopPass" ); $conf->SysopPass2 = importPost( "SysopPass2" ); + $conf->RootUser = importPost( "RootUser", "root" ); + $conf->RootPW = importPost( "RootPW", "" ); + $useRoot = importCheck( 'useroot', false ); + $conf->LanguageCode = importPost( "LanguageCode", "en" ); + + ## MySQL specific: + $conf->DBprefix = importPost( "DBprefix" ); + $conf->setSchema( + importPost( "DBschema", "mysql4" ), + importPost( "DBengine", "InnoDB" ) ); + + ## Postgres specific: + $conf->DBport = importPost( "DBport", "5432" ); + $conf->DBmwschema = importPost( "DBmwschema", "mediawiki" ); + $conf->DBts2schema = importPost( "DBts2schema", "public" ); /* Check for validity */ $errs = array(); if( $conf->Sitename == "" || $conf->Sitename == "MediaWiki" || $conf->Sitename == "Mediawiki" ) { - $errs["Sitename"] = "Must not be blank or \"MediaWiki\"."; + $errs["Sitename"] = "Must not be blank or \"MediaWiki\""; } if( $conf->DBuser == "" ) { $errs["DBuser"] = "Must not be blank"; } -if( $conf->DBpassword == "" ) { +if( ($conf->DBtype == 'mysql') && (strlen($conf->DBuser) > 16) ) { + $errs["DBuser"] = "Username too long"; +} +if( $conf->DBpassword == "" && $conf->DBtype != "postgres" ) { $errs["DBpassword"] = "Must not be blank"; } if( $conf->DBpassword != $conf->DBpassword2 ) { @@ -409,11 +618,47 @@ $errs["DBprefix"] = "Invalid table prefix"; } -if( $conf->SysopPass == "" ) { - $errs["SysopPass"] = "Must not be blank"; -} -if( $conf->SysopPass != $conf->SysopPass2 ) { - $errs["SysopPass2"] = "Passwords don't match!"; +error_reporting( E_ALL ); + +/** + * Initialise $wgLang and $wgContLang to something so we can + * call case-folding methods. Per Brion, this is English for + * now, although we could be clever and initialise to the + * user-selected language. + */ +$wgContLang = Language::factory( 'en' ); +$wgLang = $wgContLang; + +/** + * We're messing about with users, so we need a stub + * authentication plugin... + */ +$wgAuth = new AuthPlugin(); + +/** + * Validate the initial administrator account; username, + * password checks, etc. + */ +if( $conf->SysopName ) { + # Check that the user can be created + $u = User::newFromName( $conf->SysopName ); + if( is_a($u, 'User') ) { // please do not use instanceof, it breaks PHP4 + # Various password checks + if( $conf->SysopPass != '' ) { + if( $conf->SysopPass == $conf->SysopPass2 ) { + if( !$u->isValidPassword( $conf->SysopPass ) ) { + $errs['SysopPass'] = "Bad password"; + } + } else { + $errs['SysopPass2'] = "Passwords don't match"; + } + } else { + $errs['SysopPass'] = "Cannot be blank"; + } + unset( $u ); + } else { + $errs['SysopName'] = "Bad username"; + } } $conf->License = importRequest( "License", "none" ); @@ -421,7 +666,7 @@ $conf->RightsUrl = "http://www.gnu.org/copyleft/fdl.html"; $conf->RightsText = "GNU Free Documentation License 1.2"; $conf->RightsCode = "gfdl"; - $conf->RightsIcon = '${wgStylePath}/common/images/gnu-fdl.png'; + $conf->RightsIcon = '${wgScriptPath}/skins/common/images/gnu-fdl.png'; } elseif( $conf->License == "none" ) { $conf->RightsUrl = $conf->RightsText = $conf->RightsCode = $conf->RightsIcon = ""; } else { @@ -450,10 +695,10 @@ } /* default values for installation */ -$conf->Email =importRequest("Email", "email_enabled"); -$conf->Emailuser=importRequest("Emailuser", "emailuser_enabled"); -$conf->Enotif =importRequest("Enotif", "enotif_allpages"); -$conf->Eauthent =importRequest("Eauthent", "eauthent_enabled"); +$conf->Email = importRequest("Email", "email_enabled"); +$conf->Emailuser = importRequest("Emailuser", "emailuser_enabled"); +$conf->Enotif = importRequest("Enotif", "enotif_allpages"); +$conf->Eauthent = importRequest("Eauthent", "eauthent_enabled"); if( $conf->posted && ( 0 == count( $errs ) ) ) { do { /* So we can 'continue' to end prematurely */ @@ -461,189 +706,339 @@ /* Load up the settings and get installin' */ $local = writeLocalSettings( $conf ); + echo "
      44. \n"; + echo "

        Generating configuration file...

        \n"; + echo "
      45. \n"; + $wgCommandLineMode = false; chdir( ".." ); - eval($local); + $ok = eval( $local ); + if( $ok === false ) { + dieout( "Errors in generated configuration; " . + "most likely due to a bug in the installer... " . + "Config file was: " . + "
        " .
        +				htmlspecialchars( $local ) .
        +				"
        " . + "
    " ); + } + $conf->DBtypename = ''; + foreach (array_keys($ourdb) as $db) { + if ($conf->DBtype === $db) + $conf->DBtypename = $ourdb[$db]['fullname']; + } + if ( ! strlen($conf->DBtype)) { + $errs["DBpicktype"] = "Please choose a database type"; + continue; + } + + if (! $conf->DBtypename) { + $errs["DBtype"] = "Unknown database type '$conf->DBtype'"; + continue; + } + print "
  • Database type: {$conf->DBtypename}
  • \n"; + $dbclass = 'Database'.ucfirst($conf->DBtype); + $wgDBtype = $conf->DBtype; $wgDBadminuser = "root"; $wgDBadminpassword = $conf->RootPW; + + ## Mysql specific: $wgDBprefix = $conf->DBprefix; + + ## Postgres specific: + $wgDBport = $conf->DBport; + $wgDBmwschema = $conf->DBmwschema; + $wgDBts2schema = $conf->DBts2schema; + $wgCommandLineMode = true; - $wgUseDatabaseMessages = false; /* FIXME: For database failure */ - require_once( "includes/Setup.php" ); + $wgUseDatabaseMessages = false; /* FIXME: For database failure */ + require_once( "$IP/includes/Setup.php" ); chdir( "config" ); - require_once( "maintenance/InitialiseMessages.inc" ); - $wgTitle = Title::newFromText( "Installation script" ); - $mysqlOldClient = version_compare( mysql_get_client_info(), "4.1.0", "lt" ); - if( $mysqlOldClient ) { - print "
  • PHP is linked with old MySQL client libraries. If you are - using a MySQL 4.1 server and have problems connecting to the database, - see http://dev.mysql.com/doc/mysql/en/old-client.html for help.
  • \n"; - } - - # Determine how we're going to connect to the database - if( $conf->RootPW == '-' ) { - # Regular user - $conf->Root = false; - $db_user = $wgDBuser; - $db_pass = $wgDBpassword; - } else { - # Superuser - $conf->Root = true; - $db_user = $conf->RootUser; - $db_pass = $conf->RootPW; - } - - # Now attempt the connection - echo( "
  • Connecting to $wgDBname on $wgDBserver as $db_user..." ); - $wgDatabase = Database::newFromParams( $wgDBserver, $db_user, $db_pass, "", 1 ); - if( $wgDatabase->isOpen() ) { - # We're in; set up a few variables - $ok = true; - echo( "success.
  • \n" ); - $wgDatabase->ignoreErrors( true ); - $myver = mysql_get_server_info( $wgDatabase->mConn ); - $wgDBadminuser = $db_user; - $wgDBadminpassword = $db_pass; - } else { - # There was an error; if we recognise it, give some useful feedback - $ok = false; - $errno = mysql_errno(); - $errtx = htmlspecialchars( mysql_error() ); - echo( "failed with error $errno: $errtx.
  • \n" ); - switch( $errno ) { - case 1045: - case 2000: - # Authentication - if( $conf->Root ) { - # The superuser details are wrong - $errs["RootUser"] = "Check username"; - $errs["RootPW"] = "and password"; - } else { - # The regular user details are wrong - $errs["DBuser"] = "Check username"; - $errs["DBpassword"] = "and password"; - } - break; - default: - # Something else - $errs["DBserver"] = "Couldn't connect to database"; - break; - } # switch - } # conn att - - if( !$ok ) continue; - - # Print out the mySQL version and enable mySQL 4 enhancements as needed - echo( "
  • Connected to $myver" ); - if( version_compare( $myver, "4.0.0" ) >= 0 ) { - echo( "; using enhancements for mySQL 4.
  • " ); - $conf->DBmysql4 = true; - $local = writeLocalSettings( $conf ); - } - - # Check for possible authentication problems re. password encryption in newer mySQL versions - $mysqlNewAuth = version_compare( $myver, "4.1.0", "ge" ); - if( $mysqlNewAuth && $mysqlOldClient ) { - echo( "
  • You are using mySQL 4.1, however, PHP is linked to older client libraries. If you encounter authentication problems, see http://dev.mysql.com/doc/mysql/en/old-client.html for pertinent solutions.
  • \n" ); - } - - # Check versions with regards to character sets, cough up an error if there are inconsistencies - if( $wgDBmysql5 ) { - if( $mysqlNewAuth ) { - echo( "
  • Enabling mySQL 4.1/5.0 character set mode.
  • \n" ); + error_reporting( E_ALL ); + print "
  • Loading class: $dbclass"; + $dbc = new $dbclass; + + if( $conf->DBtype == 'mysql' ) { + $mysqlOldClient = version_compare( mysql_get_client_info(), "4.1.0", "lt" ); + if( $mysqlOldClient ) { + print "
  • PHP is linked with old MySQL client libraries. If you are + using a MySQL 4.1 server and have problems connecting to the database, + see http://dev.mysql.com/doc/mysql/en/old-client.html for help.
  • \n"; + } + $ok = true; # Let's be optimistic + + # Decide if we're going to use the superuser or the regular database user + $conf->Root = $useRoot; + if( $conf->Root ) { + $db_user = $conf->RootUser; + $db_pass = $conf->RootPW; } else { - echo( "
  • mySQL 4.1/5.0 character set mode has been enabled, however, an older version of mySQL has been detected. This will likely cause the installation to fail.
  • \n" ); + $db_user = $wgDBuser; + $db_pass = $wgDBpassword; } - } - @$sel = mysql_select_db( $wgDBname, $wgDatabase->mConn ); - if( $sel ) { - print "
  • Database " . htmlspecialchars( $wgDBname ) . " exists
  • \n"; - } else { - $err = mysql_errno(); - if ( $err != 1049 ) { - print "
    • Error selecting database $wgDBname: $err " . htmlspecialchars( mysql_error() ) . - "
    "; - continue; + # Attempt to connect + echo( "
  • Attempting to connect to database server as $db_user..." ); + $wgDatabase = Database::newFromParams( $wgDBserver, $db_user, $db_pass, '', 1 ); + + # Check the connection and respond to errors + if( $wgDatabase->isOpen() ) { + # Seems OK + $ok = true; + $wgDBadminuser = $db_user; + $wgDBadminpassword = $db_pass; + echo( "success.
  • \n" ); + $wgDatabase->ignoreErrors( true ); + $myver = $wgDatabase->getServerVersion(); + } else { + # There were errors, report them and back out + $ok = false; + $errno = mysql_errno(); + $errtx = htmlspecialchars( mysql_error() ); + switch( $errno ) { + case 1045: + case 2000: + echo( "failed due to authentication errors. Check passwords." ); + if( $conf->Root ) { + # The superuser details are wrong + $errs["RootUser"] = "Check username"; + $errs["RootPW"] = "and password"; + } else { + # The regular user details are wrong + $errs["DBuser"] = "Check username"; + $errs["DBpassword"] = "and password"; + } + break; + case 2002: + case 2003: + default: + # General connection problem + echo( "failed with error [$errno] $errtx.\n" ); + $errs["DBserver"] = "Connection failed"; + break; + } # switch + } #conn. att. + + if( !$ok ) { continue; } + + } else /* not mysql */ { + error_reporting( E_ALL ); + $wgSuperUser = ''; + ## Possible connect as a superuser + if( $useRoot ) { + $wgDBsuperuser = $conf->RootUser; + echo( "
  • Attempting to connect to database \"postgres\" as superuser \"$wgDBsuperuser\"..." ); + $wgDatabase = $dbc->newFromParams($wgDBserver, $wgDBsuperuser, $conf->RootPW, "postgres", 1); + if (!$wgDatabase->isOpen()) { + print " error: " . $wgDatabase->lastError() . "
  • \n"; + $errs["DBserver"] = "Could not connect to database as superuser"; + $errs["RootUser"] = "Check username"; + $errs["RootPW"] = "and password"; + continue; + } } - $res = $wgDatabase->query( "CREATE DATABASE `$wgDBname`" ); - if( !$res ) { - print "
  • Couldn't create database " . - htmlspecialchars( $wgDBname ) . - "; try with root access or check your username/pass.
  • \n"; - $errs["RootPW"] = "<- Enter"; - continue; + echo( "
  • Attempting to connect to database \"$wgDBname\" as \"$wgDBuser\"..." ); + $wgDatabase = $dbc->newFromParams($wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1); + if (!$wgDatabase->isOpen()) { + print " error: " . $wgDatabase->lastError() . "
  • \n"; + } else { + $myver = $wgDatabase->getServerVersion(); } - print "
  • Created database " . htmlspecialchars( $wgDBname ) . "
  • \n"; } - $wgDatabase->selectDB( $wgDBname ); + if ( !$wgDatabase->isOpen() ) { + $errs["DBserver"] = "Couldn't connect to database"; + continue; + } + + print "
  • Connected to $myver"; + if ($conf->DBtype == 'mysql') { + if( version_compare( $myver, "4.0.14" ) < 0 ) { + dieout( " -- mysql 4.0.14 or later required. Aborting." ); + } + $mysqlNewAuth = version_compare( $myver, "4.1.0", "ge" ); + if( $mysqlNewAuth && $mysqlOldClient ) { + print "; You are using MySQL 4.1 server, but PHP is linked + to old client libraries; if you have trouble with authentication, see + http://dev.mysql.com/doc/mysql/en/old-client.html for help."; + } + if( $wgDBmysql5 ) { + if( $mysqlNewAuth ) { + print "; enabling MySQL 4.1/5.0 charset mode"; + } else { + print "; MySQL 4.1/5.0 charset mode enabled, + but older version detected; will likely fail."; + } + } + print "
  • \n"; + + @$sel = $wgDatabase->selectDB( $wgDBname ); + if( $sel ) { + print "
  • Database " . htmlspecialchars( $wgDBname ) . " exists
  • \n"; + } else { + $err = mysql_errno(); + $databaseSafe = htmlspecialchars( $wgDBname ); + if( $err == 1102 /* Invalid database name */ ) { + print "
    • {$databaseSafe} is not a valid database name.
    "; + continue; + } elseif( $err != 1049 /* Database doesn't exist */ ) { + print "
    • Error selecting database {$databaseSafe}: {$err} "; + print htmlspecialchars( mysql_error() ) . "
    "; + continue; + } + print "
  • Attempting to create database...
  • "; + $res = $wgDatabase->query( "CREATE DATABASE `$wgDBname`" ); + if( !$res ) { + print "
  • Couldn't create database " . + htmlspecialchars( $wgDBname ) . + "; try with root access or check your username/pass.
  • \n"; + $errs["RootPW"] = "<- Enter"; + continue; + } + print "
  • Created database " . htmlspecialchars( $wgDBname ) . "
  • \n"; + } + $wgDatabase->selectDB( $wgDBname ); + } + else if ($conf->DBtype == 'postgres') { + if( version_compare( $myver, "PostgreSQL 8.0" ) < 0 ) { + dieout( " Postgres 8.0 or later is required. Aborting." ); + } + } if( $wgDatabase->tableExists( "cur" ) || $wgDatabase->tableExists( "revision" ) ) { print "
  • There are already MediaWiki tables in this database. Checking if updates are needed...
  • \n"; - # Create user if required - if ( $conf->Root ) { - $conn = Database::newFromParams( $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1 ); - if ( $conn->isOpen() ) { - print "
  • DB user account ok
  • \n"; - $conn->close(); - } else { - print "
  • Granting user permissions..."; - if( $mysqlOldClient && $mysqlNewAuth ) { - print " If the next step fails, see http://dev.mysql.com/doc/mysql/en/old-client.html for help."; + if ( $conf->DBtype == 'mysql') { + # Determine existing default character set + if ( $wgDatabase->tableExists( "revision" ) ) { + $revision = $wgDatabase->escapeLike( $conf->DBprefix . 'revision' ); + $res = $wgDatabase->query( "SHOW TABLE STATUS LIKE '$revision'" ); + $row = $wgDatabase->fetchObject( $res ); + if ( !$row ) { + echo "
  • SHOW TABLE STATUS query failed!
  • \n"; + $existingSchema = false; + $existingEngine = false; + } else { + if ( preg_match( '/^latin1/', $row->Collation ) ) { + $existingSchema = 'mysql4'; + } elseif ( preg_match( '/^utf8/', $row->Collation ) ) { + $existingSchema = 'mysql5'; + } elseif ( preg_match( '/^binary/', $row->Collation ) ) { + $existingSchema = 'mysql5-binary'; + } else { + $existingSchema = false; + echo "
  • Warning: Unrecognised existing collation
  • \n"; + } + if ( isset( $row->Engine ) ) { + $existingEngine = $row->Engine; + } else { + $existingEngine = $row->Type; + } + } + if ( $existingSchema && $existingSchema != $conf->DBschema ) { + print "
  • Warning: you requested the {$conf->DBschema} schema, " . + "but the existing database has the $existingSchema schema. This upgrade script ". + "can't convert it, so it will remain $existingSchema.
  • \n"; + $conf->setSchema( $existingSchema, $conf->DBengine ); + } + if ( $existingEngine && $existingEngine != $conf->DBengine ) { + print "
  • Warning: you requested the {$conf->DBengine} storage " . + "engine, but the existing database uses the $existingEngine engine. This upgrade " . + "script can't convert it, so it will remain $existingEngine.
  • \n"; + $conf->setSchema( $conf->DBschema, $existingEngine ); + } + } + + # Create user if required + if ( $conf->Root ) { + $conn = $dbc->newFromParams( $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1 ); + if ( $conn->isOpen() ) { + print "
  • DB user account ok
  • \n"; + $conn->close(); + } else { + print "
  • Granting user permissions..."; + if( $mysqlOldClient && $mysqlNewAuth ) { + print " If the next step fails, see http://dev.mysql.com/doc/mysql/en/old-client.html for help."; + } + print "
  • \n"; + dbsource( "../maintenance/users.sql", $wgDatabase ); } - print "\n"; - dbsource( "../maintenance/users.sql", $wgDatabase ); } } - print "
    \n";
    +			print "
    \n";
     			chdir( ".." );
     			flush();
     			do_all_updates();
     			chdir( "config" );
    -
     			print "
    \n"; - print "
  • Finished update checks.
  • \n"; + print "
    • Finished update checks.
    • \n"; } else { + # Determine available storage engines if possible + if ( $conf->DBtype == 'mysql' && version_compare( $myver, "4.1.2", "ge" ) ) { + $res = $wgDatabase->query( 'SHOW ENGINES' ); + $found = false; + while ( $row = $wgDatabase->fetchObject( $res ) ) { + if ( $row->Engine == $conf->DBengine ) { + $found = true; + break; + } + } + $wgDatabase->freeResult( $res ); + if ( !$found && $conf->DBengine != 'MyISAM' ) { + echo "
    • Warning: {$conf->DBengine} storage engine not available, " . + "using MyISAM instead
    • \n"; + $conf->setSchema( $conf->DBschema, 'MyISAM' ); + } + } + # FIXME: Check for errors print "
    • Creating tables..."; - if( $wgDBmysql5 ) { - print " using MySQL 5 table defs..."; - dbsource( "../maintenance/mysql5/tables.sql", $wgDatabase ); - } else { - print " using MySQL 3/4 table defs..."; + if ($conf->DBtype == 'mysql') { dbsource( "../maintenance/tables.sql", $wgDatabase ); + dbsource( "../maintenance/interwiki.sql", $wgDatabase ); + } else if ($conf->DBtype == 'postgres') { + $wgDatabase->setup_database(); + } + else { + $errs["DBtype"] = "Do not know how to handle database type '$conf->DBtype'"; + continue; } - dbsource( "../maintenance/interwiki.sql", $wgDatabase ); + print " done.
    • \n"; - print "
    • Initializing data..."; + print "
    • Initializing data...
    • \n"; $wgDatabase->insert( 'site_stats', - array( 'ss_row_id' => 1, - 'ss_total_views' => 0, - 'ss_total_edits' => 0, - 'ss_good_articles' => 0 ) ); - - - # Set up the DB user if we (i) can and (ii) need to - if( $conf->Root ) { - $db = Database::newFromParams( $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1 ); - if( $db->isOpen() ) { - $db->close(); + array ( 'ss_row_id' => 1, + 'ss_total_views' => 0, + 'ss_total_edits' => 0, + 'ss_good_articles' => 0 ) ); + + # Set up the "regular user" account *if we can, and if we need to* + if( $conf->Root and $conf->DBtype == 'mysql') { + # See if we need to + $wgDatabase2 = $dbc->newFromParams( $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1 ); + if( $wgDatabase2->isOpen() ) { + # Nope, just close the test connection and continue + $wgDatabase2->close(); + echo( "
    • User $wgDBuser exists. Skipping grants.
    • \n" ); } else { - print "
    • Granting user permissions...
    • \n"; + # Yes, so run the grants + echo( "
    • Granting user permissions to $wgDBuser on $wgDBname..." ); dbsource( "../maintenance/users.sql", $wgDatabase ); - $db->close(); + echo( "success.
    • \n" ); } } if( $conf->SysopName ) { $u = User::newFromName( $conf->getSysopName() ); - if ( 0 == $u->idForName() ) { + if ( !$u ) { + print "
    • Warning: Skipped sysop account creation - invalid username!
    • \n"; + } + else if ( 0 == $u->idForName() ) { $u->addToDatabase(); $u->setPassword( $conf->getSysopPass() ); $u->saveSettings(); @@ -665,31 +1060,22 @@ $newid = $article->insertOn( $wgDatabase ); $revision = new Revision( array( 'page' => $newid, - 'text' => wfMsg( 'mainpagetext' ) . "\n\n" . wfMsg( 'mainpagedocfooter' ), + 'text' => wfMsg( 'mainpagetext' ) . "\n\n" . wfMsgNoTrans( 'mainpagedocfooter' ), 'comment' => '', 'user' => 0, 'user_text' => 'MediaWiki default', ) ); $revid = $revision->insertOn( $wgDatabase ); $article->updateRevisionOn( $wgDatabase, $revision ); - - print "
    • ";
      -			initialiseMessages();
      -			print "
    • \n"; } /* Write out the config file now that all is well */ + print "
    • \n"; print "

      Creating LocalSettings.php...

      \n\n"; - $localSettings = "<" . "?php$endl$local$endl?" . ">"; + $localSettings = "<" . "?php$endl$local"; // Fix up a common line-ending problem (due to CVS on Windows) $localSettings = str_replace( "\r\n", "\n", $localSettings ); - - if( version_compare( phpversion(), "4.3.2" ) >= 0 ) { - $xt = "xt"; # Refuse to overwrite an existing file - } else { - $xt = "wt"; # 'x' is not available prior to PHP 4.3.2. We did check above, but race conditions blah blah - } - $f = fopen( "LocalSettings.php", $xt ); + $f = fopen( "LocalSettings.php", 'xt' ); if( $f == false ) { dieout( "

      Couldn't write out LocalSettings.php. Check that the directory permissions are correct and that there isn't already a file of that name here...

      \n" . @@ -698,11 +1084,12 @@ } if(fwrite( $f, $localSettings ) ) { fclose( $f ); + print "

    \n"; writeSuccessMessage(); } else { fclose( $f ); die("

    An error occured while writing the config/LocalSettings.php file. Check user rights and disk space then try again.

    \n"); - + print "\n"; } } while( false ); @@ -717,43 +1104,37 @@ /* Display options form */ if( $conf->posted ) { - echo "

    Something's not quite right yet; make sure everything below is filled out correctly.

    \n"; + echo "

    Something's not quite right yet; make sure everything below is filled out correctly.

    \n"; } ?> -
    +

    Site config

    -
    -
    +
    +
    -
    -
    - Your site name should be a relatively short word. It'll appear as the namespace - name for 'meta' pages as well as throughout the user interface. Good site names - are things like "Wikipedia" and - "OpenFacts"; avoid punctuation, - which may cause problems. -
    +
    +

    + Preferably a short word without punctuation, i.e. "Wikipedia".
    + Will appear as the namespace name for "meta" pages, and throughout the interface. +

    -
    +
    -
    -
    - This will be used as the return address for password reminders and - may be displayed in some error conditions so visitors can get in - touch with you. It is also be used as the default sender address of e-mail - notifications (enotifs). -
    +
    +

    + Displayed to users in some error messages, used as the return address for password reminders, and used as the default sender address of e-mail notifications. +

    -
    - +
    + -
    -
    - You may select the language for the user interface of the wiki... - Some localizations are less complete than others. Unicode (UTF-8 encoding) - is used for all localizations. -
    + +

    + Select the language for your wiki's interface. Some localizations aren't fully complete. Unicode (UTF-8) is used for all localizations. +

    -
    - -
    Select one:
    +
    +
      -
    • +
    • ScriptPath}/config/index.php?License=cc&RightsUrl=[license_url]&RightsText=[license_name]&RightsCode=[license_code]&RightsIcon=[license_button]" ); + $script = defined('MW_INSTALL_PHP5_EXT') ? 'index.php5' : 'index.php'; + $exit = urlencode( "$wgServer{$conf->ScriptPath}/config/$script?License=cc&RightsUrl=[license_url]&RightsText=[license_name]&RightsCode=[license_code]&RightsIcon=[license_button]" ); $icon = urlencode( "$wgServer$wgUploadPath/wiki.png" ); $ccApp = htmlspecialchars( "http://creativecommons.org/license/?partner=$partner&exit_url=$exit&partner_icon_url=$icon" ); - print "choose"; - ?> (link will wipe out any other data in this form!) + print "choose"; + ?> License == "cc" ) { ?>
        -
      • RightsIcon ) . "\" alt='icon' />", "hidden" ); ?>
      • +
      • RightsIcon ) . "\" alt='(Creative Commons icon)' />", "hidden" ); ?>
      • RightsText ), "hidden" ); ?>
      • RightsCode ), "hidden" ); ?>
      • RightsUrl ) . "\">" . htmlspecialchars( $conf->RightsUrl ) . "", "hidden" ); ?>
      • @@ -796,45 +1175,49 @@
      -
    -
    - MediaWiki can include a basic license notice, icon, and machine-readable - copyright metadata if your wiki's content is to be licensed under - the GNU FDL or a Creative Commons license. If you're not sure, leave - it at "none". -
    - - -
    - -
    -
    - -
    -
    - -
    -
    - A sysop user account can lock or delete pages, block problematic IP - addresses from editing, and other maintenance tasks. If creating a new - wiki database, a sysop account will be created with the given name - and password. -
    + +

    + A notice, icon, and machine-readable copyright metadata will be displayed for the license you pick. +

    + + +
    + +
    +
    + +
    +
    + +
    +

    + An admin can lock/delete pages, block users from editing, and do other maintenance tasks.
    + A new account will be added only when creating a new wiki database. +

    + The password cannot be the same as the username. +

    -
    - -
    Select one:
    +
    +
      -
    • +
    • turck ) { echo "
    • "; aField( $conf, "Shm", "Turck MMCache", "radio", "turck" ); echo "
    • "; } - ?> - xcache ) { + echo( '
    • ' ); + aField( $conf, 'Shm', 'XCache', 'radio', 'xcache' ); + echo( '
    • ' ); + } + if ( $conf->apc ) { + echo "
    • "; + aField( $conf, "Shm", "APC", "radio", "apc" ); + echo "
    • "; + } if ( $conf->eaccel ) { echo "
    • "; aField( $conf, "Shm", "eAccelerator", "radio", "eaccel" ); @@ -842,166 +1225,203 @@ } ?>
    • -
    -
    -
    - Using a shared memory system such as Turck MMCache, eAccelerator, or Memcached will speed - up MediaWiki significantly. Memcached is the best solution but needs to be - installed. Specify the server addresses and ports in a comma-separted list. Only - use Turck shared memory if the wiki will be running on a single Apache server. - +
    + +

    + An object caching system such as memcached will provide a significant performance boost, + but needs to be installed. Provide the server addresses and ports in a comma-separated list. +

    + MediaWiki can also detect and support eAccelerator, Turck MMCache, APC, and XCache, but + these should not be used if the wiki will be running on multiple application servers. +

    +

    E-mail, e-mail notification and authentication setup

    -
    -
    - -
    Select one:
    - +
    +
    +
      -
    • -
    • +
    • +
    -
    -
    - Use this to disable all e-mail functions (send a password reminder, user-to-user e-mail and e-mail notification), - if sending e-mails on your server doesn't work. -
    -
    - -
    Select one:
    + +

    + Use this to disable all e-mail functions (password reminders, user-to-user e-mail, and e-mail notifications) + if sending mail doesn't work on your server. +

    +
    +
      -
    • -
    • +
    • +
    -
    -
    - Use this to disable only the user-to-user e-mail function (EmailUser). -
    -
    - -
    Select one:
    - + +

    + The user-to-user e-mail feature (Special:Emailuser) lets the wiki act as a relay to allow users to exchange e-mail without publicly advertising their e-mail address. +

    +
    +
      -
    • -
    • -
    • +
    • +
    • +
    -
    -
    + +

    - E-mail notification sends a notification e-mail to a user, when the user_talk page is changed - and/or when watch-listed pages are changed, depending on the above settings. - When testing this feature, be reminded, that obviously an e-mail address must be present in your preferences - and that your own changes never trigger notifications to be sent to yourself.

    - -

    Users get corresponding options to select or deselect in their users' preferences. - The user options are not shown on the preference page, if e-mail notification is disabled.

    + For this feature to work, an e-mail address must be present for the user account, and the notification + options in the user's preferences must be enabled. Also note the + authentication option below. When testing the feature, keep in mind that your own changes will never trigger notifications to be sent to yourself.

    -

    There are additional options for fine tuning in /includes/DefaultSettings.php .

    -
    - -
    - -
    Select one:
    +

    There are additional options for fine tuning in /includes/DefaultSettings.php; copy these to your LocalSettings.php and edit them there to change them.

    + +
    +
      -
    • -
    • +
    • +
    -
    -
    -

    E-mail address authentication uses a scheme to authenticate e-mail addresses of the users. The user who initially enters or changes his/her stored e-mail address - gets a link with a token mailed to that address. The stored e-mail address is authenticated at the moment the user comes back to the wiki via the link.

    - -

    The e-mail address stays authenticated as long as the user does not change it; the time of authentication is indicated - on the user preference page.

    - -

    If the option is enabled, only authenticated e-mail addresses can receive EmailUser mails and/or - e-mail notification mails.

    -
    + +
    +

    If this option is enabled, users have to confirm their e-mail address using a magic link sent to them whenever they set or change it, and only authenticated e-mail addresses can receive mails from other users and/or + change notification mails. Setting this option is recommended for public wikis because of potential abuse of the e-mail features above.

    +
    -
    +

    Database config

    -
    -
    -
    - If your database server isn't on your web server, enter the name - or IP address here. -
    - -
    -
    -
    -
    -
    +
    +
    + +$errs[DBpicktype]\n"; ?> +
    +
    + +
    +

    + If your database server isn't on your web server, enter the name or IP address here. +

    + +
    +
    +
    +
    +

    If you only have a single user account and database available, enter those here. If you have database root access (see below) - you can specify new accounts/databases to be created. -

    + you can specify new accounts/databases to be created. This account + will not be created if it pre-exists. If this is the case, ensure that it + has SELECT, INSERT, UPDATE, and DELETE permissions on the MediaWiki database. +

    + +
    + + checked="checked" /> +   +
    +
    + +
    +
    + +
    -
    -
    +

    + If the database user specified above does not exist, or does not have access to create + the database (if needed) or tables within it, please check the box and provide details + of a superuser account, such as root, which does. +

    + + +
    +

    If you need to share one database between multiple wikis, or - MediaWiki and another web application, you may choose to + between MediaWiki and another web application, you may choose to add a prefix to all the table names to avoid conflicts.

    Avoid exotic characters; something like mw_ is good.

    -
    - -
    + + +
    Select one:
      -
    • -
    • +
    • +
    -
    -
    + +

    + InnoDB is best for public web installations, since it has good concurrency + support. MyISAM may be faster in single-user installations. MyISAM databases + tend to get corrupted more often than InnoDB databases. +

    +
    +
    Select one:
    +
      +
    • +
    • +
    • +
    +
    +

    EXPERIMENTAL: You can enable explicit Unicode charset support for MySQL 4.1 and 5.0 servers. This is not well tested and may cause things to break. If upgrading an older installation, leave in backwards-compatible mode. -

    +

    + -
    - -
    - -
    - -
    -
    - If the database user specified above does not exist, or does not have permissions to create - the database or tables required, please provide details of a superuser account, such as root, - which does. If this is not needed, leave the password set to -. -
    + +
    +
    +
    +
    +

    The username specified above (at "DB username") will have its search path set to the above schemas, + so it is recommended that you create a new user. The above schemas are generally correct: + only change them if you are sure you need to.

    +
    + -
    +
    - -
    -
    + + + + + @@ -1010,25 +1430,35 @@ /* -------------------------------------------------------------------------------------- */ function writeSuccessMessage() { - global $conf; + $script = defined('MW_INSTALL_PHP5_EXT') ? 'index.php5' : 'index.php'; if ( ini_get( 'safe_mode' ) && !ini_get( 'open_basedir' ) ) { echo <<

    Installation successful!

    To complete the installation, please do the following:

    1. Download config/LocalSettings.php with your FTP client or file manager
    2. Upload it to the parent directory
    3. Delete config/LocalSettings.php
    4. -
    5. Start using your wiki! +
    6. Start using your wiki!

    If you are in a shared hosting environment, do not just move LocalSettings.php remotely. LocalSettings.php is currently owned by the user your webserver is running under, which means that anyone on the same server can read your database password! Downloading it and uploading it again will hopefully change the ownership to a user ID specific to you.

    + EOT; } else { - echo "

    Installation successful! Move the config/LocalSettings.php file into the parent directory, then follow - this link to your wiki.

    \n"; + echo << +

    +Installation successful! +Move the config/LocalSettings.php file to the parent directory, then follow + this link to your wiki.

    +

    You should change file permissions for LocalSettings.php as required to +prevent other users on the server reading passwords and altering configuration data.

    + +EOT; } } @@ -1046,14 +1476,9 @@ } function writeLocalSettings( $conf ) { - $conf->DBmysql4 = @$conf->DBmysql4 ? 'true' : 'false'; - $conf->UseImageResize = $conf->UseImageResize ? 'true' : 'false'; $conf->PasswordSender = $conf->EmergencyContact; - $zlib = ($conf->zlib ? "" : "# "); $magic = ($conf->ImageMagick ? "" : "# "); $convert = ($conf->ImageMagick ? $conf->ImageMagick : "/usr/bin/convert" ); - $pretty = ($conf->prettyURLs ? "" : "# "); - $ugly = ($conf->prettyURLs ? "# " : ""); $rights = ($conf->RightsUrl) ? "" : "# "; $hashedUploads = $conf->safeMode ? '' : '# '; @@ -1063,6 +1488,8 @@ $mcservers = var_export( $conf->MCServerArray, true ); break; case 'turck': + case 'xcache': + case 'apc': case 'eaccel': $cacheType = 'CACHE_ACCEL'; $mcservers = 'array()'; @@ -1116,15 +1543,27 @@ $slconf['RightsIcon'] = $conf->RightsIcon; } - $sep = (DIRECTORY_SEPARATOR == "\\") ? ";" : ":"; $localsettings = " # This file was automatically generated by the MediaWiki installer. # If you make manual changes, please keep track in case you need to # recreate them later. +# +# See includes/DefaultSettings.php for all configurable settings +# and their default values, but don't forget to make changes in _this_ +# file, not there. + +# If you customize your file layout, set \$IP to the directory that contains +# the other MediaWiki files. It will be used as a base to locate files. +if( defined( 'MW_INSTALL_PATH' ) ) { + \$IP = MW_INSTALL_PATH; +} else { + \$IP = dirname( __FILE__ ); +} + +\$path = array( \$IP, \"\$IP/includes\", \"\$IP/languages\" ); +set_include_path( implode( PATH_SEPARATOR, \$path ) . PATH_SEPARATOR . get_include_path() ); -\$IP = \"{$slconf['IP']}\"; -ini_set( \"include_path\", \".$sep\$IP$sep\$IP/includes$sep\$IP/languages\" ); -require_once( \"includes/DefaultSettings.php\" ); +require_once( \"\$IP/includes/DefaultSettings.php\" ); # If PHP's memory limit is very low, some operations may fail. " . ($conf->raiseMemory ? '' : '# ' ) . "ini_set( 'memory_limit', '20M' );" . " @@ -1133,36 +1572,29 @@ if ( isset( \$_SERVER ) && array_key_exists( 'REQUEST_METHOD', \$_SERVER ) ) { die( \"This script must be run from the command line\\n\" ); } -} elseif ( empty( \$wgNoOutputBuffer ) ) { - ## Compress output if the browser supports it - {$zlib}if( !ini_get( 'zlib.output_compression' ) ) @ob_start( 'ob_gzhandler' ); } +## Uncomment this to disable output compression +# \$wgDisableOutputCompression = true; \$wgSitename = \"{$slconf['Sitename']}\"; -\$wgScriptPath = \"{$slconf['ScriptPath']}\"; -\$wgScript = \"\$wgScriptPath/index.php\"; -\$wgRedirectScript = \"\$wgScriptPath/redirect.php\"; - -## If using PHP as a CGI module, use the ugly URLs -{$pretty}\$wgArticlePath = \"\$wgScript/\$1\"; -{$ugly}\$wgArticlePath = \"\$wgScript?title=\$1\"; - -\$wgStylePath = \"\$wgScriptPath/skins\"; -\$wgStyleDirectory = \"\$IP/skins\"; -\$wgLogo = \"\$wgStylePath/common/images/wiki.png\"; +## The URL base path to the directory containing the wiki; +## defaults for all runtime URL paths are based off of this. +\$wgScriptPath = \"{$slconf['ScriptPath']}\"; +\$wgScriptExtension = \"{$slconf['ScriptExtension']}\"; -\$wgUploadPath = \"\$wgScriptPath/images\"; -\$wgUploadDirectory = \"\$IP/images\"; +## For more information on customizing the URLs please see: +## http://www.mediawiki.org/wiki/Manual:Short_URL -\$wgEnableEmail = $enableemail; -\$wgEnableUserEmail = $enableuseremail; +\$wgEnableEmail = $enableemail; +\$wgEnableUserEmail = $enableuseremail; \$wgEmergencyContact = \"{$slconf['EmergencyContact']}\"; -\$wgPasswordSender = \"{$slconf['PasswordSender']}\"; +\$wgPasswordSender = \"{$slconf['PasswordSender']}\"; ## For a detailed description of the following switches see -## http://meta.wikimedia.org/Enotif and http://meta.wikimedia.org/Eauthent +## http://www.mediawiki.org/wiki/Extension:Email_notification +## and http://www.mediawiki.org/wiki/Extension:Email_notification ## There are many more options for fine tuning available see ## /includes/DefaultSettings.php ## UPO means: this is also a user preference option @@ -1170,26 +1602,33 @@ \$wgEnotifWatchlist = $enotifwatchlist; # UPO \$wgEmailAuthentication = $eauthent; +\$wgDBtype = \"{$slconf['DBtype']}\"; \$wgDBserver = \"{$slconf['DBserver']}\"; \$wgDBname = \"{$slconf['DBname']}\"; \$wgDBuser = \"{$slconf['DBuser']}\"; \$wgDBpassword = \"{$slconf['DBpassword']}\"; + +# MySQL specific settings \$wgDBprefix = \"{$slconf['DBprefix']}\"; -# If you're on MySQL 3.x, this next line must be FALSE: -\$wgDBmysql4 = {$conf->DBmysql4}; +# MySQL table options to use during installation or update +\$wgDBTableOptions = \"{$slconf['DBTableOptions']}\"; # Experimental charset support for MySQL 4.1/5.0. \$wgDBmysql5 = {$conf->DBmysql5}; +# Postgres specific settings +\$wgDBport = \"{$slconf['DBport']}\"; +\$wgDBmwschema = \"{$slconf['DBmwschema']}\"; +\$wgDBts2schema = \"{$slconf['DBts2schema']}\"; + ## Shared memory settings \$wgMainCacheType = $cacheType; \$wgMemCachedServers = $mcservers; ## To enable image uploads, make sure the 'images' directory -## is writable, then uncomment this: -# \$wgEnableUploads = true; -\$wgUseImageResize = {$conf->UseImageResize}; +## is writable, then set this to true: +\$wgEnableUploads = false; {$magic}\$wgUseImageMagick = true; {$magic}\$wgImageMagickConvertCommand = \"{$convert}\"; @@ -1201,10 +1640,7 @@ ## If you have the appropriate support software installed ## you can enable inline LaTeX equations: -# \$wgUseTeX = true; -\$wgMathPath = \"{\$wgUploadPath}/math\"; -\$wgMathDirectory = \"{\$wgUploadDirectory}/math\"; -\$wgTmpDirectory = \"{\$wgUploadDirectory}/tmp\"; +\$wgUseTeX = false; \$wgLocalInterwiki = \$wgSitename; @@ -1214,7 +1650,7 @@ ## Default skin: you can change the default skin. Use the internal symbolic ## names, ie 'standard', 'nostalgia', 'cologneblue', 'monobook': -# \$wgDefaultSkin = 'monobook'; +\$wgDefaultSkin = 'monobook'; ## For attaching licensing metadata to pages, and displaying an ## appropriate copyright notice / icon. GNU Free Documentation @@ -1227,7 +1663,13 @@ # \$wgRightsCode = \"{$slconf['RightsCode']}\"; # Not yet used \$wgDiff3 = \"{$slconf['diff3']}\"; -"; + +# When you make changes to this configuration file, this will make +# sure that cached pages are cleared. +\$configdate = gmdate( 'YmdHis', @filemtime( __FILE__ ) ); +\$wgCacheEpoch = max( \$wgCacheEpoch, \$configdate ); + "; ## End of setting the $localsettings string + // Keep things in Unix line endings internally; // the system will write out as local text type. return str_replace( "\r\n", "\n", $localsettings ); @@ -1253,19 +1695,30 @@ return importVar( $_POST, $name, $default ); } +function importCheck( $name ) { + return isset( $_POST[$name] ); +} + function importRequest( $name, $default = "" ) { return importVar( $_REQUEST, $name, $default ); } -function aField( &$conf, $field, $text, $type = "", $value = "" ) { +$radioCount = 0; + +function aField( &$conf, $field, $text, $type = "text", $value = "", $onclick = '' ) { + global $radioCount; if( $type != "" ) { $xtype = "type=\"$type\""; } else { $xtype = ""; } - if(!(isset($id)) or ($id == "") ) $id = $field; + $id = $field; $nolabel = ($type == "radio") || ($type == "hidden"); + + if ($type == 'radio') + $id .= $radioCount++; + if( $nolabel ) { echo "\t\t\n"; @@ -1293,19 +1752,20 @@ } function getLanguageList() { - global $wgLanguageNames; + global $wgLanguageNames, $IP; if( !isset( $wgLanguageNames ) ) { - $wgContLanguageCode = "xxx"; - function wfLocalUrl( $x ) { return $x; } - function wfLocalUrlE( $x ) { return $x; } - require_once( "languages/Names.php" ); + require_once( "$IP/languages/Names.php" ); } $codes = array(); - $d = opendir( "../languages" ); + $d = opendir( "../languages/messages" ); + /* In case we are called from the root directory */ + if (!$d) + $d = opendir( "languages/messages"); while( false !== ($f = readdir( $d ) ) ) { - if( preg_match( '/Language([A-Z][a-z_]+)\.php$/', $f, $m ) ) { + $m = array(); + if( preg_match( '/Messages([A-Z][a-z_]+)\.php$/', $f, $m ) ) { $code = str_replace( '_', '-', strtolower( $m[1] ) ); if( isset( $wgLanguageNames[$code] ) ) { $name = $code . ' - ' . $wgLanguageNames[$code]; @@ -1329,13 +1789,14 @@ $names = array($names); foreach ($names as $name) { - if (file_exists("$loc/$name")) { + $command = "$loc".DIRECTORY_SEPARATOR."$name"; + if (file_exists($command)) { if (!$versioninfo) - return "$loc/$name"; + return $command; - $file = str_replace('$1', "$loc/$name", $versioninfo[0]); + $file = str_replace('$1', $command, $versioninfo[0]); if (strstr(`$file`, $versioninfo[1]) !== false) - return "$loc/$name"; + return $command; } } return false; @@ -1349,7 +1810,7 @@ if ( !function_exists( 'fsockopen' ) ) { $errstr = "Can't connect to memcached, fsockopen() not present"; } - if ( !$errstr && count( $hostport ) != 2 ) { + if ( !$errstr && count( $hostport ) != 2 ) { $errstr = 'Please specify host and port'; var_dump( $hostport ); } @@ -1385,7 +1846,75 @@ } return $errstr; } + +function database_picker($conf) { + global $ourdb; + print "\n"; + foreach(array_keys($ourdb) as $db) { + if ($ourdb[$db]['havedriver']) { + print "
  • "; + aField( $conf, "DBtype", $ourdb[$db]['fullname'], 'radio', $db, 'onclick'); + print "
  • \n"; + } + } + print "\n"; +} + +function database_switcher($db) { + global $ourdb; + $color = $ourdb[$db]['bgcolor']; + $full = $ourdb[$db]['fullname']; + print "
    $full specific options\n"; +} + +function printListItem( $item ) { + print "
  • $item
  • "; +} + ?> +
    +
    +

    This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version.

    + +

    This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details.

    + +

    You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + or read it online

    +
    + + + + +
    + + +
    + +

    MediaWiki is Copyright © 2001-2007 by Magnus Manske, Brion Vibber, Lee Daniel Crocker, Tim Starling, Erik Möller, Gabriel Wicke and others.

    +
    +
    + + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/README mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/README --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/README 2005-08-18 12:06:57.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/README 2007-05-31 18:21:36.000000000 -0400 @@ -1,10 +1,10 @@ -[July 5th 2005] +[May 31st 2007] The 'docs' directory contain various text files that should help you understand the most importants classes in MediaWiki. -API documentation is sometime generated and uploaded at: - http://wikipedia.sourceforge.net/doc/ +API documentation is automatically generated and updated daily at: + http://svn.wikimedia.org/doc/ You can get a fresh version using 'make doc' or mwdocgen.php in the ../maintenance/ directory. @@ -13,5 +13,5 @@ For end user / administrators, most of the documentation is located online at: - http://meta.wikimedia.org/wiki/Help:Help + http://www.mediawiki.org/wiki/Project:Help diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/deferred.txt mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/deferred.txt --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/deferred.txt 2005-08-17 15:48:56.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/deferred.txt 2007-01-08 18:28:35.000000000 -0500 @@ -17,3 +17,11 @@ the list is almost always going to have just one item on it, if that, so it's not worth the trouble. + +Since 1.6 there is a 'job queue' in the jobs table, which is used +to update link tables of transcluding pages after edits; this +may be extended in the future to more general background tasks. + +Job queue items are fetched out of the queue and run either +at a random rate during regular page views (by default) or by +a batch process which can be run via maintenance/runJobs.php. diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/design.txt mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/design.txt --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/design.txt 2005-08-23 17:48:07.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/design.txt 2007-06-26 17:03:59.000000000 -0400 @@ -1,5 +1,8 @@ This is a brief overview of the new design. +More thorough and up-to-date information is available on the documentation +wiki at http://www.mediawiki.org/ + Primary source files/objects: index.php @@ -31,8 +34,7 @@ calling output() to send it all. It could be easily changed to send incrementally if that becomes useful, but I prefer the flexibility. This should also do the output encoding. - The system allocates a global one in $wgOut. This class - also handles converting wikitext format to HTML. + The system allocates a global one in $wgOut. Title Represents the title of an article, and does all the work @@ -42,10 +44,15 @@ don't involve their text, such as access rights. Article - Encapsulates access to the "cur" table of the database. The + Encapsulates access to the "page" table of the database. The object represents a an article, and maintains state such as text (in Wikitext format), flags, etc. + Revision + Encapsulates individual page revision data and access to the + revision/text/blobs storage system. Higher-level code should + never touch text storage directly; this class mediates it. + Skin Encapsulates a "look and feel" for the wiki. All of the functions that render HTML, and make choices about how to @@ -61,7 +68,9 @@ Language Represents the language used for incidental text, and also has some character encoding functions and other locale stuff. - A global one is allocated in $wgLang. + The current user interface language is instantiated as $wgLang, + and the local content language as $wgContLang; be sure to use + the *correct* language object depending upon the circumstances. LinkCache Keeps information on existence of articles. See LINKCACHE.TXT. @@ -86,13 +95,14 @@ its own line or the statement that opened the block--that's confusing as hell. - - PHP doesn't have "private" member variables of functions, - so I've used the comment "/* private */" in some places to - indicate my intent. Don't access things marked that way - from outside the class def--use the accessor functions (or - make your own if you need them). Yes, even some globals - are marked private, because PHP is broken and doesn't - allow static class variables. + - Certain functions and class members are marked with + /* private */, rather than being marked as such. This is a + hold-over from PHP 4, which didn't support proper visibilities. + You should not access things marked in this manner outside the + class/inheritance line as this code is subjected to be updated + in a manner that enforces this at some time in the near future, + and things will break. New code should use the standard method of + setting visibilities as normal. - Member variables are generally "mXxx" to distinguish them. This should make it easier to spot errors of forgetting the @@ -115,14 +125,4 @@ Other conventions: Top-level functions are wfFuncname(), names of session variables are wsName, cookies wcName, and form field - values wpName ("p" for "POST"). - - - Be kind to your release manager and don't use CVS keywords (Id, - Revision, etc.) to mark file versions. They make merging code - between different branches a pain for CVS, and are kind of sketchy - for versions after that. (Yes, you can use the '-kk' flag so that - merges ignore keywords, but that messes up binary files. See - https://www.cvshome.org/docs/manual/cvs-1.11.18/cvs_5.html#SEC64). - - - + values wpName ("p" for "POST"). \ No newline at end of file diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/globals.txt mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/globals.txt --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/globals.txt 2005-08-17 15:48:56.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/globals.txt 2006-01-04 21:05:53.000000000 -0500 @@ -1,29 +1,74 @@ globals.txt -PHP loves globals. I hate them. This is not a great -combination, but I manage. I could get rid of most of -them by having a single "HTTP request" object, and using -it to hold everything that's now global (which is exactly -what I'd do in a Java servlet). But that's really -awkward in PHP, and wouldn't really provide much benefit -in readability or maintainability, so I go with the flow -of PHP and use globals. Here's documentation on the -important globals used by the system. +Globals are evil. The original MediaWiki code relied on +globals for processing context far too often. MediaWiki +development since then has been a story of slowly moving +context out of global variables and into objects. Storing +processing context in object member variables allows those +objects to be reused in a much more flexible way. Consider +the elegance of: + + # Generate the article HTML as if viewed by a web request + $article = new Article( Title::newFromText( $t ) ); + $article->view(); + +versus + + # Save current globals + $oldTitle = $wgTitle; + $oldArticle = $wgArticle; + + # Generate the HTML + $wgTitle = Title::newFromText( $t ); + $wgArticle = new Article; + $wgArticle->view(); + + # Restore globals + $wgTitle = $oldTitle + $wgArticle = $oldArticle + +Some of the current MediaWiki developers have an idle +fantasy that some day, globals will be eliminated from +MediaWiki entirely, replaced by an application object which +would be passed to constructors. Whether that would be an +efficient, convenient solution remains to be seen, but +certainly PHP 5 makes such object-oriented programming +models easier than they were in previous versions. + +For the time being though, MediaWiki programmers will have +to work in an environment with some global context. At the +time of writing, 418 globals were initialised on startup by +MediaWiki. 304 of these were configuration settings, which +are documented in DefaultSettings.php. There is no +comprehensive documentation for the remaining 114 globals, +however some of the most important ones are listed below. +They are typically initialised either in index.php or in +Setup.php. + $wgOut OutputPage object for HTTP response. +$wgUser + User object for the user associated with the current + request. + $wgTitle Title object created from the request URL. $wgLang - Language object for this request. + Language object selected by user preferences -$wgArticle - Article object corresponsing to $wgTitle. +$wgContLang + Language object associated with the wiki being + viewed. -$wgLinkCache - LinkCache object. +$wgArticle + Article object corresponding to $wgTitle. -... +$wgParser + Parser object. Parser extensions register their + hooks here. +$wgLoadBalancer + A LoadBalancer object, manages database connections. diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/hooks.txt mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/hooks.txt --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/hooks.txt 2006-03-08 14:49:34.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/hooks.txt 2007-08-28 17:53:50.000000000 -0400 @@ -35,20 +35,20 @@ title to all uppercase letters. Currently, in MediaWiki code, we would handle this as follows (note: not real code, here): - function showAnArticle($article) { - global $wgReverseTitle, $wgCapitalizeTitle; - - if ($wgReverseTitle) { - wfReverseTitle($article); - } - - if ($wgCapitalizeTitle) { - wfCapitalizeTitle($article); - } - - # code to actually show the article goes here - } - + function showAnArticle($article) { + global $wgReverseTitle, $wgCapitalizeTitle; + + if ($wgReverseTitle) { + wfReverseTitle($article); + } + + if ($wgCapitalizeTitle) { + wfCapitalizeTitle($article); + } + + # code to actually show the article goes here + } + An extension writer, or a local admin, will often add custom code to the function -- with or without a global variable. For example, someone wanting email notification when an article is shown may add: @@ -75,15 +75,15 @@ option-specific stuff in our mainline code. Using hooks, the function becomes: - function showAnArticle($article) { + function showAnArticle($article) { - if (wfRunHooks('ArticleShow', array(&$article))) { - - # code to actually show the article goes here - - wfRunHooks('ArticleShowComplete', array(&$article)); + if (wfRunHooks('ArticleShow', array(&$article))) { + + # code to actually show the article goes here + + wfRunHooks('ArticleShowComplete', array(&$article)); + } } - } We've cleaned up the code here by removing clumps of weird, infrequently used code and moving them off somewhere else. It's much @@ -96,24 +96,24 @@ in showAnArticle, deleteAnArticle, exportArticle, etc., we can concentrate it all in an extension file: - function reverseArticleTitle($article) { - # ... - } + function reverseArticleTitle($article) { + # ... + } - function reverseForExport($article) { - # ... - } + function reverseForExport($article) { + # ... + } The setup function for the extension just has to add its hook functions to the appropriate events: - setupTitleReversingExtension() { - global $wgHooks; - - $wgHooks['ArticleShow'][] = 'reverseArticleTitle'; - $wgHooks['ArticleDelete'][] = 'reverseArticleTitle'; - $wgHooks['ArticleExport'][] = 'reverseForExport'; - } + setupTitleReversingExtension() { + global $wgHooks; + + $wgHooks['ArticleShow'][] = 'reverseArticleTitle'; + $wgHooks['ArticleDelete'][] = 'reverseArticleTitle'; + $wgHooks['ArticleExport'][] = 'reverseForExport'; + } Having all this code related to the title-reversion option in one place means that it's easier to read and understand; you don't have to @@ -124,8 +124,8 @@ performance at runtime. Admins who want to have all the reversed titles can add: - require_once('extensions/ReverseTitle.php'); - + require_once('extensions/ReverseTitle.php'); + ...to their LocalSettings.php file; those of us who don't want or need it can just leave it out. @@ -143,31 +143,31 @@ Hooks are registered by adding them to the global $wgHooks array for a given event. All the following are valid ways to define hooks: - $wgHooks['EventName'][] = 'someFunction'; # function, no data - $wgHooks['EventName'][] = array('someFunction', $someData); - $wgHooks['EventName'][] = array('someFunction'); # weird, but OK - - $wgHooks['EventName'][] = $object; # object only - $wgHooks['EventName'][] = array($object, 'someMethod'); - $wgHooks['EventName'][] = array($object, 'someMethod', $someData); - $wgHooks['EventName'][] = array($object); # weird but OK + $wgHooks['EventName'][] = 'someFunction'; # function, no data + $wgHooks['EventName'][] = array('someFunction', $someData); + $wgHooks['EventName'][] = array('someFunction'); # weird, but OK + + $wgHooks['EventName'][] = $object; # object only + $wgHooks['EventName'][] = array($object, 'someMethod'); + $wgHooks['EventName'][] = array($object, 'someMethod', $someData); + $wgHooks['EventName'][] = array($object); # weird but OK When an event occurs, the function (or object method) will be called with the optional data provided as well as event-specific parameters. The above examples would result in the following code being executed when 'EventName' happened: - # function, no data - someFunction($param1, $param2) - # function with data - someFunction($someData, $param1, $param2) - - # object only - $object->onEventName($param1, $param2) - # object with method - $object->someMethod($param1, $param2) - # object with method and data - $object->someMethod($someData, $param1, $param2) + # function, no data + someFunction($param1, $param2) + # function with data + someFunction($someData, $param1, $param2) + + # object only + $object->onEventName($param1, $param2) + # object with method + $object->someMethod($param1, $param2) + # object with method and data + $object->someMethod($someData, $param1, $param2) Note that when an object is the hook, and there's no specified method, the default method called is 'onEventName'. For different events this @@ -176,8 +176,8 @@ The extra data is useful if we want to use the same function or object for different purposes. For example: - $wgHooks['ArticleSaveComplete'][] = array('ircNotify', 'TimStarling'); - $wgHooks['ArticleSaveComplete'][] = array('ircNotify', 'brion'); + $wgHooks['ArticleSaveComplete'][] = array('ircNotify', 'TimStarling'); + $wgHooks['ArticleSaveComplete'][] = array('ircNotify', 'brion'); This code would result in ircNotify being run twice when an article is saved: once for 'TimStarling', and once for 'brion'. @@ -195,12 +195,12 @@ users to a custom system (LDAP, another PHP program, whatever), you could do: - $wgHooks['UserLogin'][] = array('ldapLogin', $ldapServer); - - function ldapLogin($username, $password) { - # log user into LDAP - return false; - } + $wgHooks['UserLogin'][] = array('ldapLogin', $ldapServer); + + function ldapLogin($username, $password) { + # log user into LDAP + return false; + } Returning false makes less sense for events where the action is complete, and will normally be ignored. @@ -210,14 +210,15 @@ A calling function or method uses the wfRunHooks() function to run the hooks related to a particular event, like so: - class Article { - # ... - function protect() { - global $wgUser; - if (wfRunHooks('ArticleProtect', array(&$this, &$wgUser))) { - # protect the article - wfRunHooks('ArticleProtectComplete', array(&$this, &$wgUser)); - } + class Article { + # ... + function protect() { + global $wgUser; + if (wfRunHooks('ArticleProtect', array(&$this, &$wgUser))) { + # protect the article + wfRunHooks('ArticleProtectComplete', array(&$this, &$wgUser)); + } + } } wfRunHooks() returns true if the calling function should continue @@ -237,13 +238,28 @@ This is a list of known events and parameters; please add to it if you're going to add events to the MediaWiki code. -'AbortNewAccount': Prior to user account creation -$user: User object representing the account to be created -$abortError: Error message that will be passed back to the form if -account creation is cancelled +'AbortLogin': Return false to cancel account login. +$user: the User object being authenticated against +$password: the password being submitted, not yet checked for validity +&$retval: a LoginForm class constant to return from authenticateUserData(); + default is LoginForm::ABORTED. Note that the client may be using + a machine API rather than the HTML user interface. + +'AbortNewAccount': Return false to cancel account creation. +$user: the User object about to be created (read-only, incomplete) +$message: out parameter: error message to display on abort 'AddNewAccount': after a user account is created -null: This hook passes null as an argument +$user: the User object that was created. (Parameter added in 1.7) + +'AjaxAddScript': Called in output page just before the initialisation +of the javascript ajax engine. The hook is only called when ajax +is enabled ( $wgUseAjax = true; ). + +'AlternateEdit': before checking if an user can edit a page and +before showing the edit form ( EditPage::edit() ). This is triggered +on &action=edit. +$EditPage : the EditPage object 'ArticleDelete': before an article is deleted $article: the article (object) being deleted @@ -255,6 +271,17 @@ $user: the user that deleted the article $reason: the reason the article was deleted +'ArticleInsertComplete': After an article is created +$article: Article created +$user: User creating the article +$text: New content +$summary: Edit summary/comment +$isMinor: Whether or not the edit was marked as minor +$isWatch: (No longer used) +$section: (No longer used) +$flags: Flags passed to Article::doEdit() +$revision: New Revision of the article + 'ArticleProtect': before an article is protected $article: the article being protected $user: the user doing the protection @@ -278,6 +305,17 @@ $iswatch: watch flag $section: section # +'ArticleSaveComplete': After an article has been updated +$article: Article modified +$user: User performing the modification +$text: New content +$summary: Edit summary/comment +$isMinor: Whether or not the edit was marked as minor +$isWatch: (No longer used) +$section: (No longer used) +$flags: Flags passed to Article::doEdit() +$revision: New Revision of the article + 'ArticleSaveComplete': after an article is saved $article: the article (object) saved $user: the user (object) who saved the article @@ -287,9 +325,60 @@ $iswatch: watch flag $section: section # +wfRunHooks( 'ArticleSaveComplete', array( &$this, &$wgUser, $text, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); + +'ArticleUndeleted': When one or more revisions of an article are restored +$title: Title corresponding to the article restored +$create: Whether or not the restoration caused the page to be created +(i.e. it didn't exist before) + +'ArticleViewHeader': Before the parser cache is about to be tried for article viewing. +&$pcache: whether to try the parser cache or not +&$outputDone: whether the output for this page finished or not + +'ArticleUpdateBeforeRedirect': After a page is updated (usually on save), before the user is redirected back to the page +&$article: the article +&$sectionanchor: The section anchor link (e.g. "#overview" ) +&$extraq: Extra query parameters which can be added via hooked functions + +'AuthPluginSetup': update or replace authentication plugin object ($wgAuth) +Gives a chance for an extension to set it programattically to a variable class. +&$auth: the $wgAuth object, probably a stub + 'AutoAuthenticate': called to authenticate users on external/environmental means $user: writes user object to this parameter +'BadImage': When checking against the bad image list +$name: Image name being checked +&$bad: Whether or not the image is "bad" + +Change $bad and return false to override. If an image is "bad", it is not +rendered inline in wiki pages or galleries in category pages. + +'BeforeGalleryFindFile': before an image is fetched for a gallery +&$gallery,: the gallery object +&$nt: the image title +&$time: image timestamp + +'BeforePageDisplay': Prior to outputting a page +$out: OutputPage object + +'BeforeParserFetchTemplateAndtitle': before a template is fetched by Parser +&$parser: Parser object +&$title: title of the template +&$skip: skip this template and link it? +&$id: the id of the revision being parsed + +'BeforeParserMakeImageLinkObj': before an image is rendered by Parser +&$parser: Parser object +&$nt: the image title +&$skip: skip this image and link it? +&$time: the image timestamp + +'BeforeParserrenderImageGallery': before an image gallery is rendered by Parser +&$parser: Parser object +&$ig: ImageGallery object + 'BlockIp': before an IP address or user is blocked $block: the Block object about to be saved $user: the user _doing_ the block (not the one being blocked) @@ -298,6 +387,76 @@ $block: the Block object that was saved $user: the user who did the block (not the one being blocked) +'BookInformation': Before information output on Special:Booksources +$isbn: ISBN to show information for +$output: OutputPage object in use + +'CategoryPageView': before viewing a categorypage in CategoryPage::view +$catpage: CategoryPage instance + +'ContributionsToolLinks': Change tool links above Special:Contributions +$id: User identifier +$title: User page title +&$tools: Array of tool links + +'CustomEditor': When invoking the page editor +$article: Article being edited +$user: User performing the edit + +Return true to allow the normal editor to be used, or false +if implementing a custom editor, e.g. for a special namespace, +etc. + +'DiffViewHeader': called before diff display +$diff: DifferenceEngine object that's calling +$oldRev: Revision object of the "old" revision (may be null/invalid) +$newRev: Revision object of the "new" revision + +'EditPage::attemptSave': called before an article is +saved, that is before insertNewArticle() is called +&$editpage_Obj: the current EditPage object + +'EditFormPreloadText': Allows population of the edit form when creating new pages +&$text: Text to preload with +&$title: Title object representing the page being created + +'EditPage::showEditForm:fields': allows injection of form field into edit form +&$editor: the EditPage instance for reference +&$out: an OutputPage instance to write to +return value is ignored (should always return true) + +'EditFilter': Perform checks on an edit +$editor: Edit form (see includes/EditPage.php) +$text: Contents of the edit box +$section: Section being edited +&$error: Error message to return + +Return false to halt editing; you'll need to handle error messages, etc. yourself. +Alternatively, modifying $error and returning true will cause the contents of $error +to be echoed at the top of the edit form as wikitext. Return true without altering +$error to allow the edit to proceed. + +'EditSectionLink': Override the return value of Linker::editSectionLink() +$skin: Skin rendering the UI +$title: Title being linked to +$section: Section to link to +$link: Default link +$result: Result (alter this to override the generated links) + +'EditSectionLinkForOther': Override the return value of Linker::editSectionLinkForOther() +$skin: Skin rendering the UI +$title: Title being linked to +$section: Section to link to +$hint: Anchor title/tooltip attributes +$link: Default link +$result: Result (alter this to override the generated links) + +'EmailConfirmed': When checking that the user's email address is "confirmed" +$user: User being checked +$confirmed: Whether or not the email address is confirmed +This runs before the other checks, such as anonymity and the real check; return +true to allow those checks to occur, and false if checking is done. + 'EmailUser': before sending email from one user to another $to: address of receiving user $from: address of sending user @@ -310,14 +469,180 @@ $subject: subject of the mail $text: text of the mail -'LogPageValidTypes': action being logged. -$type: array of strings - -'LogPageLogName': name of the logging page(s). -$typeText: array of strings +'FetchChangesList': When fetching the ChangesList derivative for a particular user +&$user: User the list is being fetched for +&$skin: Skin object to be used with the list +&$list: List object (defaults to NULL, change it to an object instance and return +false override the list derivative used) + +'FileUpload': When a file upload occurs +$file : Image object representing the file that was uploaded + +'GetInternalURL': modify fully-qualified URLs used for squid cache purging +$title: Title object of page +$url: string value as output (out parameter, can modify) +$query: query options passed to Title::getInternalURL() + +'GetLocalURL': modify local URLs as output into page links +$title: Title object of page +$url: string value as output (out parameter, can modify) +$query: query options passed to Title::getLocalURL() + +'GetFullURL': modify fully-qualified URLs used in redirects/export/offsite data +$title: Title object of page +$url: string value as output (out parameter, can modify) +$query: query options passed to Title::getFullURL() + +'ImageOpenShowImageInlineBefore': Call potential extension just before showing the image on an image page +$imagePage: ImagePage object ($this) +$output: $wgOut + +'InitPreferencesForm': called at the end of PreferencesForm's constructor +$form: the PreferencesForm +$request: the web request to initialized from + +'InternalParseBeforeLinks': during Parser's internalParse method before links but +after noinclude/includeonly/onlyinclude and other processing. +&$this: Parser object +&$text: string containing partially parsed text +&$this->mStripState: Parser's internal StripState object + +'isValidPassword': Override the result of User::isValidPassword() +$password: Desired password +&$result: Set this and return false to override the internal checks +$user: User the password is being validated for + +'LinksUpdateConstructed': At the end of LinksUpdate() is contruction. +&$linksUpdate: the LinkUpdate object + +'LoginAuthenticateAudit': a login attempt for a valid user account either succeeded or failed. + No return data is accepted; this hook is for auditing only. +$user: the User object being authenticated against +$password: the password being submitted and found wanting +$retval: a LoginForm class constant with authenticateUserData() return value (SUCCESS, WRONG_PASS, etc) + +'LogPageValidTypes': action being logged. DEPRECATED: Use $wgLogTypes +&$type: array of strings + +'LogPageLogName': name of the logging page(s). DEPRECATED: Use $wgLogNames +&$typeText: array of strings + +'LogPageLogHeader': strings used by wfMsg as a header. DEPRECATED: Use $wgLogHeaders +&$headerText: array of strings + +'LogPageActionText': strings used by wfMsg as a header. DEPRECATED: Use $wgLogActions +&$actionText: array of strings + +'MarkPatrolled': before an edit is marked patrolled +$rcid: ID of the revision to be marked patrolled +$user: the user (object) marking the revision as patrolled +$wcOnlySysopsCanPatrol: config setting indicating whether the user + needs to be a sysop in order to mark an edit patrolled + +'MarkPatrolledComplete': after an edit is marked patrolled +$rcid: ID of the revision marked as patrolled +$user: user (object) who marked the edit patrolled +$wcOnlySysopsCanPatrol: config setting indicating whether the user + must be a sysop to patrol the edit + +'MathAfterTexvc': after texvc is executed when rendering mathematics +$mathRenderer: instance of MathRenderer +$errmsg: error message, in HTML (string). Nonempty indicates failure + of rendering the formula + +'OutputPageBeforeHTML': a page has been processed by the parser and +the resulting HTML is about to be displayed. +$parserOutput: the parserOutput (object) that corresponds to the page +$text: the text that will be displayed, in HTML (string) + +'PageHistoryBeforeList': When a history page list is about to be constructed. +$article: the article that the history is loading for + +'PageHistoryLineEnding' : right before the end
  • is added to a history line +$row: the revision row for this line +$s: the string representing this parsed line + +'PageRenderingHash': alter the parser cache option hash key + A parser extension which depends on user options should install + this hook and append its values to the key. +$hash: reference to a hash key string which can be modified + +'ParserTestTables': alter the list of tables to duplicate when parser tests +are run. Use when page save hooks require the presence of custom tables +to ensure that tests continue to run properly. +&$tables: array of table names + +'PersonalUrls': Alter the user-specific navigation links (e.g. "my page, +my talk page, my contributions" etc). + +&$personal_urls: Array of link specifiers (see SkinTemplate.php) +&$title: Title object representing the current page + +'PingLimiter': Allows extensions to override the results of User::pingLimiter() +&$user : User performing the action +$action : Action being performed +&$result : Whether or not the action should be prevented +Change $result and return false to give a definitive answer, otherwise +the built-in rate limiting checks are used, if enabled. + +'PreferencesUserInformationPanel': Add HTML bits to user information list in preferences form +$form : PreferencesForm object +&$html : HTML to append to + +'RawPageViewBeforeOutput': Right before the text is blown out in action=raw +&$obj: RawPage object +&$text: The text that's going to be the output + +'RenderPreferencesForm': called at the end of PreferencesForm::mainPrefsForm +$form: the PreferencesForm +$out: output page to render to, probably $wgOut + +'ResetPreferences': called at the end of PreferencesForm::resetPrefs +$form: the PreferencesForm +$user: the User object to load preferences from + +'SavePreferences': called at the end of PreferencesForm::savePreferences; + returning false prevents the preferences from being saved. +$form: the PreferencesForm +$user: the User object to save preferences to +$message: change this to set an error message (ignored if the hook does notreturn fals) + +'SearchUpdate': Prior to search update completion +$id : Page id +$namespace : Page namespace +$title : Page title +$text : Current text being indexed + +'ShowRawCssJs': Customise the output of raw CSS and JavaScript in page views +$text: Text being shown +$title: Title of the custom script/stylesheet page +$output: Current OutputPage object + +'SiteNoticeBefore': Before the sitenotice/anonnotice is composed +&$siteNotice: HTML returned as the sitenotice +Return true to allow the normal method of notice selection/rendering to work, +or change the value of $siteNotice and return false to alter it. + +'SiteNoticeAfter': After the sitenotice/anonnotice is composed +&$siteNotice: HTML sitenotice +Alter the contents of $siteNotice to add to/alter the sitenotice/anonnotice. + +'SkinAfterBottomScripts': At the end of Skin::bottomScripts() +$skin: Skin object +&$text: bottomScripts Text +Append to $text to add additional text/scripts after the stock bottom scripts. + +'SkinTemplateContentActions': Alter the "content action" links in SkinTemplates +&$content_actions: Content actions +[See http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/examples/Content_action.php +for an example] + +'SkinTemplateOutputPageBeforeExec': Before SkinTemplate::outputPage() starts page output +&$sktemplate: SkinTemplate object +&$tpl: Template engine object -'LogPageLogHeader': strings used by wfMsg as a header. -$headerText: array of strings +'SpecialContributionsBeforeMainOutput': Before the form on Special:Contributions +$id: User identifier 'TitleMoveComplete': after moving an article (title) $old: old title @@ -339,15 +664,67 @@ $user: user that was watching $article: article object removed +'UnwatchArticleComplete': after a watch is removed from an article +$user: user that watched +$article: article object that was watched + +'UploadForm:initial': before the upload form is generated +$form: UploadForm object +You might set the member-variables $uploadFormTextTop and +$uploadFormTextAfterSummary to inject text (HTML) either before +or after the editform. + +'UploadForm:BeforeProcessing': at the beginning of processUpload() +$form: UploadForm object +Lets you poke at member variables like $mUploadDescription before the +file is saved. + +'UploadVerification': additional chances to reject an uploaded file +string $saveName: destination file name +string $tempName: filesystem path to the temporary file for checks +string &$error: output: HTML error to show if upload canceled by returning false + +'UploadComplete': Upon completion of a file upload +$image: Image object representing the file that was uploaded + +'UserCan': To interrupt/advise the "user can do X to Y article" check +$title: Title object being checked against +$user : Current user object +$action: Action being checked +$result: Pointer to result returned if hook returns false. If null is returned, + UserCan checks are continued by internal code + +'UserCreateForm': change to manipulate the login form +$template: SimpleTemplate instance for the form + 'UserLoginComplete': after a user has logged in $user: the user object that was created on login +'UserLoginForm': change to manipulate the login form +$template: SimpleTemplate instance for the form + 'UserLogout': before a user logs out $user: the user object that is about to be logged out 'UserLogoutComplete': after a user has logged out $user: the user object _after_ logout (won't have name, ID, etc.) + +'UserRights': After a user's group memberships are changed +$user : User object that was changed +$add : Array of strings corresponding to groups added +$remove: Array of strings corresponding to groups removed +'UserGetImplicitGroups': Called in User::getImplicitGroups() +&$groups: List of implicit (automatically-assigned) groups + +'UserGetRights': Called in User::getRights() +$user: User to get rights for +&$rights: Current rights + +'UserEffectiveGroups': Called in User::getEffectiveGroups() +$user: User to get groups for +&$groups: Current effective groups + 'WatchArticle': before a watch is added to an article $user: user that will watch $article: article object to be watched @@ -356,14 +733,6 @@ $user: user that watched $article: article object watched -'CategoryPageView': before viewing a categorypage in CategoryPage::view -$catpage: CategoryPage instance - -'SkinTemplateContentActions': after building the $content_action array right - before returning it, see content_action.php in - the extension module for a demonstration of how - to use this hook. -$content_actions: The array of content actions - -More hooks might not be available but undocumented. +More hooks might be available but undocumented, you can execute +./maintenance/findhooks.php to find hidden one. \ No newline at end of file diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/linkcache.txt mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/linkcache.txt --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/linkcache.txt 2005-08-17 15:48:56.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/linkcache.txt 2005-12-30 04:33:11.000000000 -0500 @@ -4,28 +4,15 @@ the information about whether or not the article exists in the database. This is used to mark up links when displaying a page. If the same link appears more than once on any page, -then it only has to be looked up once. +then it only has to be looked up once. In most cases, link +lookups are done in batches with the LinkBatch class, or the +equivalent in Parser::replaceLinkHolders(), so the link +cache is mostly useful for short snippets of parsed text +(such as the site notice), and for links in the navigation +areas of the skin. -In practice, what happens is that the global cache object -$wgLinkCache is consulted and updated every time the function -getArticleID() from Title is called. +The link cache was formerly used to track links used in a +document for the purposes of updating the link tables. This +application is now deprecated. -This has a side benefit that we take advantage of. We have -tables "links" and "brokenlinks" which we use to do things -like the Orphans page and Whatlinkshere page. It just so -happens that after we update a page, we display it--and as -we're displaying it, we look up all the links on that page, -causing them to be put into the cache. That information is -exactly what we need to update those two tables. So, we do -something tricky when we update pages: just after the update -and before we display, we clear the cache. Then we display -the updated page. Finally, we put a LinksUpdate object onto -the deferred updates list, which fetches its information from -the cache. - -There's a minor complication: displaying a page also looks up -a few things like the talk page link in the quick bar and the -date links. Since we don't want those in the link tables, we -must take care to suspend the cache while we look those up. -Skin.php does exactly that--see dateLink(), for example. diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/memcached.txt mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/memcached.txt --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/memcached.txt 2005-04-11 20:41:38.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/memcached.txt 2007-05-08 05:09:46.000000000 -0400 @@ -29,7 +29,6 @@ a network; storage can be distributed across multiple servers, and multiple web servers can use the same cache cluster. - ********************* W A R N I N G ! ! ! ! ! *********************** Memcached has no security or authentication. Please ensure that your server is appropriately firewalled, and that the port(s) used for @@ -54,8 +53,8 @@ In your LocalSettings.php file, set: - $wgUseMemCached = true; - $wgMemCachedServers = array( "127.0.0.1:11000" ); + $wgMainCacheType = CACHE_MEMCACHED; + $wgMemCachedServers = array( "127.0.0.1:11000" ); The wiki should then use memcached to cache various data. To use multiple servers (physically separate boxes or multiple caches @@ -66,10 +65,9 @@ $wgMemCachedServers = array( "127.0.0.1:11000", # one gig on this box - array("192.168.0.1:11000", 2) # two gigs on the other box + array("192.168.0.1:11000", 2 ) # two gigs on the other box ); - == PHP client for memcached == As of this writing, MediaWiki includes version 1.0.10 of the PHP @@ -129,4 +127,4 @@ stores: array of arrays, for the BlockCache class cleared by: BlockCache:clear() -... more to come ... +... more to come ... \ No newline at end of file diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/schema.txt mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/schema.txt --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/schema.txt 2005-05-02 04:07:37.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/schema.txt 2007-05-31 17:39:25.000000000 -0400 @@ -4,3 +4,6 @@ That file has been commented with details of the usage for each table and field. + +Historical information and some other notes are available at +http://www.mediawiki.org/wiki/Manual:Database_layout diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/title.txt mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/title.txt --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/docs/title.txt 2005-08-17 15:48:56.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/docs/title.txt 2007-06-18 11:30:39.000000000 -0400 @@ -10,15 +10,15 @@ immutable "value" class, so there are no mutator functions. To get a new instance, call one of the static factory -methods WikiTitle::newFromURL(), WikiTitle::newFromDBKey(), -or WikiTitle::newFromText(). Once instantiated, the +methods Title::newFromURL(), Title::newFromDBKey(), +or Title::newFromText(). Once instantiated, the other non-static accessor methods can be used, such as getText(), getDBKey(), getNamespace(), etc. -The prefix rules: a title consists of an optional Interwiki +The prefix rules: a title consists of an optional interwiki prefix (such as "m:" for meta or "de:" for German), followed by an optional namespace, followed by the remainder of the -title. Both Interwiki prefixes and namespace prefixes have +title. Both interwiki prefixes and namespace prefixes have the same rules: they contain only letters, digits, space, and underscore, must start with a letter, are case insensitive, and spaces and underscores are interchangeable. Prefixes end @@ -35,9 +35,14 @@ rules, it is possible to have articles with colons in their names. "E. Coli 0157:H7" is a valid title, as is "2001: A Space Odyssey", because "E. Coli 0157" and "2001" are not valid -interwikis or namespaces. Likewise, ":de:name" is a link to -the article "de:name"--even though "de" is a valid interwiki, -the initial colon stops all prefix matching. +interwikis or namespaces. + +It is not possible to have an article whose bare name includes +a namespace or interwiki prefix. + +An initial colon in a title listed in wiki text may however +suppress special handling for interlanguage links, image links, +and category links. Character mapping rules: Once prefixes have been stripped, the rest of the title processed this way: spaces and underscores are @@ -64,9 +69,8 @@ alone; it will use spaces only when attached to the text title. getArticleID() needs some explanation: for "internal" articles, -it should return the "cur_id" field if the article exists, else +it should return the "page_id" field if the article exists, else it returns 0. For all external articles it returns 0. All of the IDs for all instances of Title created during a request are -cached, so they can be looked up wuickly while rendering wiki +cached, so they can be looked up quickly while rendering wiki text with lots of internal links. - diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/extensions/README mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/extensions/README --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/extensions/README 2004-04-22 02:24:00.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/extensions/README 2007-04-13 13:31:57.000000000 -0400 @@ -1,3 +1,14 @@ -Some extensions (such as the hieroglyphic module WikiHiero) are -distributed separately. Drop them into this extensions directory -and enable as per the extension's directions. +Extensions (such as the hieroglyphic module WikiHiero) are distributed +separately. Drop them into this extensions directory and enable as +per the extension's directions. + +If you are a developer, you want to fetch the extension tree in another +directory and make a symbolic link: + + mediawiki/extensions$ ln -s ../../extensions-trunk/FooBarExt + +The extensions are available through svn at: + http://svn.wikimedia.org/svnroot/mediawiki/trunk/extensions/ + +You can find documentation and additional extensions on MediaWiki website: + http://www.mediawiki.org/wiki/Category:Extensions diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/images/README mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/images/README --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/images/README 2004-05-07 22:55:21.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/images/README 2006-06-26 12:52:04.000000000 -0400 @@ -1,5 +1,5 @@ If uploads are enabled in the wiki, files will be put in subdirectories under here. -Note to upgraders: as of MediaWiki 1.3, the images used in the user -interface have been moved to stylesheets/images. +Note to upgraders: as of MediaWiki 1.5, the images used in the user +interface have been moved to skins/common/images. diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/img_auth.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/img_auth.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/img_auth.php 2005-04-16 00:33:33.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/img_auth.php 2007-08-06 02:15:21.000000000 -0400 @@ -1,56 +1,89 @@ getNsText( NS_IMAGE ) . ":" . basename( $_SERVER['PATH_INFO'] ); +$realUpload = realpath( $wgUploadDirectory ); +wfDebugLog( 'img_auth', "\$path is {$path}" ); +wfDebugLog( 'img_auth', "\$filename is {$filename}" ); + +// Basic directory traversal check +if( substr( $filename, 0, strlen( $realUpload ) ) != $realUpload ) { + wfDebugLog( 'img_auth', 'Requested path not in upload directory' ); + wfForbidden(); +} -# Check if the filename is in the correct directory -if ( substr( $filename, 0, strlen( $realUploadDirectory ) ) != $realUploadDirectory ) { +// Extract the file name and chop off the size specifier +// (e.g. 120px-Foo.png => Foo.png) +$name = wfBaseName( $path ); +if( preg_match( '!\d+px-(.*)!i', $name, $m ) ) + $name = $m[1]; +wfDebugLog( 'img_auth', "\$name is {$name}" ); + +$title = Title::makeTitleSafe( NS_IMAGE, $name ); +if( !$title instanceof Title ) { + wfDebugLog( 'img_auth', "Unable to construct a valid Title from `{$name}`" ); wfForbidden(); } +$title = $title->getPrefixedText(); -if ( is_array( $wgWhitelistRead ) && !in_array( $imageName, $wgWhitelistRead ) && !$wgUser->getID() ) { +// Check the whitelist if needed +if( !$wgUser->getId() && ( !is_array( $wgWhitelistRead ) || !in_array( $title, $wgWhitelistRead ) ) ) { + wfDebugLog( 'img_auth', "Not logged in and `{$title}` not in whitelist." ); wfForbidden(); } if( !file_exists( $filename ) ) { + wfDebugLog( 'img_auth', "`{$filename}` does not exist" ); wfForbidden(); } if( is_dir( $filename ) ) { + wfDebugLog( 'img_auth', "`{$filename}` is a directory" ); wfForbidden(); } -# Write file +// Stream the requested file +wfDebugLog( 'img_auth', "Streaming `{$filename}`" ); wfStreamFile( $filename ); +wfLogProfilingData(); +/** + * Issue a standard HTTP 403 Forbidden header and a basic + * error message, then end the script + */ function wfForbidden() { header( 'HTTP/1.0 403 Forbidden' ); - print -" -

    Access denied

    -

    You need to log in to access files on this server

    -"; - exit; -} - -?> + header( 'Content-Type: text/html; charset=utf-8' ); + echo << + +

    Access Denied

    +

    You need to log in to access files on this server.

    + + +END; + wfLogProfilingData(); + exit(); +} \ No newline at end of file diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Article.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Article.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Article.php 2006-02-14 16:49:25.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Article.php 2007-09-03 00:19:53.000000000 -0400 @@ -1,55 +1,116 @@ mTitle =& $title; + $this->mOldId = $oldId; $this->clear(); } /** + * Tell the page view functions that this view was redirected + * from another page on the wiki. + * @param $from Title object. + */ + function setRedirectedFrom( $from ) { + $this->mRedirectedFrom = $from; + } + + /** + * @return mixed false, Title of in-wiki target, or string with URL + */ + function followRedirect() { + $text = $this->getContent(); + $rt = Title::newFromRedirect( $text ); + + # process if title object is valid and not special:userlogout + if( $rt ) { + if( $rt->getInterwiki() != '' ) { + if( $rt->isLocal() ) { + // Offsite wikis need an HTTP redirect. + // + // This can be hard to reverse and may produce loops, + // so they may be disabled in the site configuration. + + $source = $this->mTitle->getFullURL( 'redirect=no' ); + return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) ); + } + } else { + if( $rt->getNamespace() == NS_SPECIAL ) { + // Gotta handle redirects to special pages differently: + // Fill the HTTP response "Location" header and ignore + // the rest of the page we're on. + // + // This can be hard to reverse, so they may be disabled. + + if( $rt->isSpecial( 'Userlogout' ) ) { + // rolleyes + } else { + return $rt->getFullURL(); + } + } + return $rt; + } + } + + // No or invalid redirect + return false; + } + + /** * get the title object of the article - * @public */ function getTitle() { return $this->mTitle; @@ -64,199 +125,97 @@ $this->mContentLoaded = false; $this->mCurID = $this->mUser = $this->mCounter = -1; # Not loaded - $this->mRedirectedFrom = $this->mUserText = - $this->mTimestamp = $this->mComment = $this->mFileCache = ''; + $this->mRedirectedFrom = null; # Title object if set + $this->mUserText = + $this->mTimestamp = $this->mComment = ''; $this->mGoodAdjustment = $this->mTotalAdjustment = 0; $this->mTouched = '19700101000000'; $this->mForUpdate = false; $this->mIsRedirect = false; $this->mRevIdFetched = 0; + $this->mRedirectUrl = false; + $this->mLatest = false; } /** - * Note that getContent/loadContent may follow redirects if - * not told otherwise, and so may cause a change to mTitle. + * Note that getContent/loadContent do not follow redirects anymore. + * If you need to fetch redirectable content easily, try + * the shortcut in Article::followContent() + * FIXME + * @todo There are still side-effects in this! + * In general, you should use the Revision class, not Article, + * to fetch text for purposes other than page views. * - * @param $noredir * @return Return the text of this revision */ - function getContent( $noredir ) { - global $wgRequest, $wgUser, $wgOut; - - # Get variables from query string :P - $action = $wgRequest->getText( 'action', 'view' ); - $section = $wgRequest->getText( 'section' ); - $preload = $wgRequest->getText( 'preload' ); + function getContent() { + global $wgUser, $wgOut; - $fname = 'Article::getContent'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); if ( 0 == $this->getID() ) { - if ( 'edit' == $action ) { - wfProfileOut( $fname ); - - # If requested, preload some text. - $text=$this->getPreloadedText($preload); - - # We used to put MediaWiki:Newarticletext here if - # $text was empty at this point. - # This is now shown above the edit box instead. - return $text; - } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - return wfMsg( 'noarticletext' ); - } else { - $this->loadContent( $noredir ); - # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page - if ( $this->mTitle->getNamespace() == NS_USER_TALK && - $wgUser->isIP($this->mTitle->getText()) && - $action=='view' - ) { - wfProfileOut( $fname ); - return $this->mContent . "\n" .wfMsg('anontalkpagetext'); - } else { - if($action=='edit') { - if($section!='') { - if($section=='new') { - wfProfileOut( $fname ); - $text=$this->getPreloadedText($preload); - return $text; - } - # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML - # comments to be stripped as well) - $rv=$this->getSection($this->mContent,$section); - wfProfileOut( $fname ); - return $rv; - } - } - wfProfileOut( $fname ); - return $this->mContent; + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $ret = wfMsgWeirdKey ( $this->mTitle->getText() ) ; + } else { + $ret = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ); } - } - } - /** - This function accepts a title string as parameter - ($preload). If this string is non-empty, it attempts - to fetch the current revision text. - */ - function getPreloadedText($preload) { - if($preload) { - $preloadTitle=Title::newFromText($preload); - if(isset($preloadTitle) && $preloadTitle->userCanRead()) { - $rev=Revision::newFromTitle($preloadTitle); - if($rev) { - return $rev->getText(); - } - } + return "
    $ret
    "; + } else { + $this->loadContent(); + wfProfileOut( __METHOD__ ); + return $this->mContent; } - return ''; } /** * This function returns the text of a section, specified by a number ($section). - * A section is text under a heading like == Heading == or

    Heading

    , or + * A section is text under a heading like == Heading == or \Heading\, or * the first section before any such heading (section 0). * * If a section contains subsections, these are also returned. * - * @param string $text text to look in - * @param integer $section section number + * @param $text String: text to look in + * @param $section Integer: section number * @return string text of the requested section + * @deprecated */ function getSection($text,$section) { - - # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML - # comments to be stripped as well) - $striparray=array(); - $parser=new Parser(); - $parser->mOutputType=OT_WIKI; - $parser->mOptions = new ParserOptions(); - $striptext=$parser->strip($text, $striparray, true); - - # now that we can be sure that no pseudo-sections are in the source, - # split it up by section - $secs = - preg_split( - '/(^=+.+?=+|^.*?<\/h[1-6].*?' . '>)(?!\S)/mi', - $striptext, -1, - PREG_SPLIT_DELIM_CAPTURE); - if($section==0) { - $rv=$secs[0]; - } else { - $headline=$secs[$section*2-1]; - preg_match( '/^(=+).+?=+|^.*?<\/h[1-6].*?' . '>(?!\S)/mi',$headline,$matches); - $hlevel=$matches[1]; - - # translate wiki heading into level - if(strpos($hlevel,'=')!==false) { - $hlevel=strlen($hlevel); - } - - $rv=$headline. $secs[$section*2]; - $count=$section+1; - - $break=false; - while(!empty($secs[$count*2-1]) && !$break) { - - $subheadline=$secs[$count*2-1]; - preg_match( '/^(=+).+?=+|^.*?<\/h[1-6].*?' . '>(?!\S)/mi',$subheadline,$matches); - $subhlevel=$matches[1]; - if(strpos($subhlevel,'=')!==false) { - $subhlevel=strlen($subhlevel); - } - if($subhlevel > $hlevel) { - $rv.=$subheadline.$secs[$count*2]; - } - if($subhlevel <= $hlevel) { - $break=true; - } - $count++; - - } - } - # reinsert stripped tags - $rv=$parser->unstrip($rv,$striparray); - $rv=$parser->unstripNoWiki($rv,$striparray); - $rv=trim($rv); - return $rv; - + global $wgParser; + return $wgParser->getSection( $text, $section ); } /** - * Return an array of the columns of the "cur"-table + * @return int The oldid of the article that is to be shown, 0 for the + * current revision */ - function getContentFields() { - return $wgArticleContentFields = array( - 'old_text','old_flags', - 'rev_timestamp','rev_user', 'rev_user_text', 'rev_comment','page_counter', - 'page_namespace', 'page_title', 'page_restrictions','page_touched','page_is_redirect' ); + function getOldID() { + if ( is_null( $this->mOldId ) ) { + $this->mOldId = $this->getOldIDFromRequest(); + } + return $this->mOldId; } /** - * Return the oldid of the article that is to be shown. - * For requests with a "direction", this is not the oldid of the - * query + * Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect + * + * @return int The old id for the request */ - function getOldID() { - global $wgRequest, $wgOut; - static $lastid; - - if ( isset( $lastid ) ) { - return $lastid; - } - # Query variables :P + function getOldIDFromRequest() { + global $wgRequest; + $this->mRedirectUrl = false; $oldid = $wgRequest->getVal( 'oldid' ); if ( isset( $oldid ) ) { - $oldid = IntVal( $oldid ); + $oldid = intval( $oldid ); if ( $wgRequest->getVal( 'direction' ) == 'next' ) { $nextid = $this->mTitle->getNextRevisionID( $oldid ); if ( $nextid ) { $oldid = $nextid; } else { - $wgOut->redirect( $this->mTitle->getFullURL( 'redirect=no' ) ); + $this->mRedirectUrl = $this->mTitle->getFullURL( 'redirect=no' ); } } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) { $previd = $this->mTitle->getPreviousRevisionID( $oldid ); @@ -266,35 +225,29 @@ # TODO } } - $lastid = $oldid; + # unused: + # $lastid = $oldid; } - return @$oldid; # "@" to be able to return "unset" without PHP complaining - } + if ( !$oldid ) { + $oldid = 0; + } + return $oldid; + } /** - * Load the revision (including cur_text) into this object + * Load the revision (including text) into this object */ - function loadContent( $noredir = false ) { - global $wgOut, $wgRequest; - + function loadContent() { if ( $this->mContentLoaded ) return; # Query variables :P $oldid = $this->getOldID(); - $redirect = $wgRequest->getVal( 'redirect' ); - - $fname = 'Article::loadContent'; # Pre-fill content with error message so that if something # fails we'll have something telling us what we intended. - - $t = $this->mTitle->getPrefixedText(); - - $noredir = $noredir || ($wgRequest->getVal( 'redirect' ) == 'no') - || $wgRequest->getCheck( 'rdfrom' ); $this->mOldId = $oldid; - $this->fetchContent( $oldid, $noredir, true ); + $this->fetchContent( $oldid ); } @@ -302,11 +255,10 @@ * Fetch a page record with the given conditions * @param Database $dbr * @param array $conditions - * @access private + * @private */ - function pageData( &$dbr, $conditions ) { - return $dbr->selectRow( 'page', - array( + function pageData( $dbr, $conditions ) { + $fields = array( 'page_id', 'page_namespace', 'page_title', @@ -317,20 +269,35 @@ 'page_random', 'page_touched', 'page_latest', - 'page_len' ), + 'page_len', + ); + wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) ); + $row = $dbr->selectRow( + 'page', + $fields, $conditions, - 'Article::pageData' ); + __METHOD__ + ); + wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) ); + return $row ; } - function pageDataFromTitle( &$dbr, $title ) { + /** + * @param Database $dbr + * @param Title $title + */ + function pageDataFromTitle( $dbr, $title ) { return $this->pageData( $dbr, array( 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDBkey() ) ); } - function pageDataFromId( &$dbr, $id ) { - return $this->pageData( $dbr, array( - 'page_id' => IntVal( $id ) ) ); + /** + * @param Database $dbr + * @param int $id + */ + function pageDataFromId( $dbr, $id ) { + return $this->pageData( $dbr, array( 'page_id' => $id ) ); } /** @@ -338,33 +305,49 @@ * some source. * * @param object $data - * @access private + * @private */ - function loadPageData( $data ) { - $this->mTitle->loadRestrictions( $data->page_restrictions ); - $this->mTitle->mRestrictionsLoaded = true; - - $this->mCounter = $data->page_counter; - $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); - $this->mIsRedirect = $data->page_is_redirect; - $this->mLatest = $data->page_latest; + function loadPageData( $data = 'fromdb' ) { + if ( $data === 'fromdb' ) { + $dbr = $this->getDB(); + $data = $this->pageDataFromId( $dbr, $this->getId() ); + } + + $lc =& LinkCache::singleton(); + if ( $data ) { + $lc->addGoodLinkObj( $data->page_id, $this->mTitle ); + + $this->mTitle->mArticleID = $data->page_id; + + # Old-fashioned restrictions. + $this->mTitle->loadRestrictions( $data->page_restrictions ); + + $this->mCounter = $data->page_counter; + $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); + $this->mIsRedirect = $data->page_is_redirect; + $this->mLatest = $data->page_latest; + } else { + if ( is_object( $this->mTitle ) ) { + $lc->addBadLinkObj( $this->mTitle ); + } + $this->mTitle->mArticleID = 0; + } - $this->mDataLoaded = true; + $this->mDataLoaded = true; } /** * Get text of an article from database + * Does *NOT* follow redirects. * @param int $oldid 0 for whatever the latest revision is - * @param bool $noredir Set to false to follow redirects - * @param bool $globalTitle Set to true to change the global $wgTitle object when following redirects or other unexpected title changes * @return string */ - function fetchContent( $oldid = 0, $noredir = true, $globalTitle = false ) { + function fetchContent( $oldid = 0 ) { if ( $this->mContentLoaded ) { return $this->mContent; } - $dbr =& $this->getDB(); - $fname = 'Article::fetchContent'; + + $dbr = $this->getDB(); # Pre-fill content with error message so that if something # fails we'll have something telling us what we intended. @@ -372,21 +355,17 @@ if( $oldid ) { $t .= ',oldid='.$oldid; } - if( isset( $redirect ) ) { - $redirect = ($redirect == 'no') ? 'no' : 'yes'; - $t .= ',redirect='.$redirect; - } - $this->mContent = wfMsg( 'missingarticle', $t ); + $this->mContent = wfMsg( 'missingarticle', $t ) ; if( $oldid ) { $revision = Revision::newFromId( $oldid ); if( is_null( $revision ) ) { - wfDebug( "$fname failed to retrieve specified revision, id $oldid\n" ); + wfDebug( __METHOD__." failed to retrieve specified revision, id $oldid\n" ); return false; } $data = $this->pageDataFromId( $dbr, $revision->getPage() ); if( !$data ) { - wfDebug( "$fname failed to get page data linked to revision id $oldid\n" ); + wfDebug( __METHOD__." failed to get page data linked to revision id $oldid\n" ); return false; } $this->mTitle = Title::makeTitle( $data->page_namespace, $data->page_title ); @@ -395,66 +374,22 @@ if( !$this->mDataLoaded ) { $data = $this->pageDataFromTitle( $dbr, $this->mTitle ); if( !$data ) { - wfDebug( "$fname failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" ); + wfDebug( __METHOD__." failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" ); return false; } $this->loadPageData( $data ); } $revision = Revision::newFromId( $this->mLatest ); if( is_null( $revision ) ) { - wfDebug( "$fname failed to retrieve current page, rev_id $data->page_latest\n" ); + wfDebug( __METHOD__." failed to retrieve current page, rev_id {$data->page_latest}\n" ); return false; } } - # If we got a redirect, follow it (unless we've been told - # not to by either the function parameter or the query - if ( !$oldid && !$noredir ) { - $rt = Title::newFromRedirect( $revision->getText() ); - # process if title object is valid and not special:userlogout - if ( $rt && ! ( $rt->getNamespace() == NS_SPECIAL && $rt->getText() == 'Userlogout' ) ) { - # Gotta hand redirects to special pages differently: - # Fill the HTTP response "Location" header and ignore - # the rest of the page we're on. - global $wgDisableHardRedirects; - if( $globalTitle && !$wgDisableHardRedirects ) { - global $wgOut; - if ( $rt->getInterwiki() != '' && $rt->isLocal() ) { - $source = $this->mTitle->getFullURL( 'redirect=no' ); - $wgOut->redirect( $rt->getFullURL( 'rdfrom=' . urlencode( $source ) ) ) ; - return false; - } - if ( $rt->getNamespace() == NS_SPECIAL ) { - $wgOut->redirect( $rt->getFullURL() ); - return false; - } - } - if( $rt->getInterwiki() == '' ) { - $redirData = $this->pageDataFromTitle( $dbr, $rt ); - if( $redirData ) { - $redirRev = Revision::newFromId( $redirData->page_latest ); - if( !is_null( $redirRev ) ) { - $this->mRedirectedFrom = $this->mTitle->getPrefixedText(); - $this->mTitle = $rt; - $data = $redirData; - $this->loadPageData( $data ); - $revision = $redirRev; - } - } - } - } - } - - # if the title's different from expected, update... - if( $globalTitle ) { - global $wgTitle; - if( !$this->mTitle->equals( $wgTitle ) ) { - $wgTitle = $this->mTitle; - } - } - - # Back to the business at hand... - $this->mContent = $revision->getText(); + // FIXME: Horrible, horrible! This content-loading interface just plain sucks. + // We should instead work with the Revision object when we need it... + $this->mContent = $revision->userCan( Revision::DELETED_TEXT ) ? $revision->getRawText() : ""; + //$this->mContent = $revision->getText(); $this->mUser = $revision->getUser(); $this->mUserText = $revision->getUserText(); @@ -465,21 +400,15 @@ $this->mContentLoaded = true; $this->mRevision =& $revision; - return $this->mContent; - } + wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ) ; - /** - * Gets the article text without using so many damn globals - * Returns false on error - * - * @param integer $oldid - */ - function getContentWithoutUsingSoManyDamnGlobals( $oldid = 0, $noredir = false ) { - return $this->fetchContent( $oldid, $noredir, false ); + return $this->mContent; } /** * Read/write accessor to select FOR UPDATE + * + * @param $x Mixed: FIXME */ function forUpdate( $x = NULL ) { return wfSetVar( $this->mForUpdate, $x ); @@ -487,19 +416,19 @@ /** * Get the database which should be used for reads + * + * @return Database */ - function &getDB() { - #if ( $this->mForUpdate ) { - $ret =& wfGetDB( DB_MASTER ); - #} else { - # $ret =& wfGetDB( DB_SLAVE ); - #} - return $ret; + function getDB() { + return wfGetDB( DB_MASTER ); } /** * Get options for all SELECT statements - * Can pass an option array, to which the class-wide options will be appended + * + * @param $options Array: an optional options array which'll be appended to + * the default + * @return Array: options */ function getSelectOptions( $options = '' ) { if ( $this->mForUpdate ) { @@ -513,7 +442,7 @@ } /** - * Return the Article ID + * @return int Page ID */ function getID() { if( $this->mTitle ) { @@ -524,44 +453,51 @@ } /** - * Returns true if this article exists in the database. - * @return bool + * @return bool Whether or not the page exists in the database */ function exists() { return $this->getId() != 0; } /** - * Get the view count for this article + * @return int The view count for the page */ function getCount() { if ( -1 == $this->mCounter ) { $id = $this->getID(); - $dbr =& $this->getDB(); - $this->mCounter = $dbr->selectField( 'page', 'page_counter', array( 'page_id' => $id ), - 'Article::getCount', $this->getSelectOptions() ); + if ( $id == 0 ) { + $this->mCounter = 0; + } else { + $dbr = wfGetDB( DB_SLAVE ); + $this->mCounter = $dbr->selectField( 'page', 'page_counter', array( 'page_id' => $id ), + 'Article::getCount', $this->getSelectOptions() ); + } } return $this->mCounter; } /** - * Would the given text make this article a "good" article (i.e., - * suitable for including in the article count)? - * @param string $text Text to analyze - * @return integer 1 if it can be counted else 0 + * Determine whether a page would be suitable for being counted as an + * article in the site_stats table based on the title & its content + * + * @param $text String: text to analyze + * @return bool */ function isCountable( $text ) { global $wgUseCommaCount; - if ( NS_MAIN != $this->mTitle->getNamespace() ) { return 0; } - if ( $this->isRedirect( $text ) ) { return 0; } - $token = ($wgUseCommaCount ? ',' : '[[' ); - if ( false === strstr( $text, $token ) ) { return 0; } - return 1; + $token = $wgUseCommaCount ? ',' : '[['; + return + $this->mTitle->isContentPage() + && !$this->isRedirect( $text ) + && in_string( $token, $text ); } /** * Tests if the article text represents a redirect + * + * @param $text String: FIXME + * @return bool */ function isRedirect( $text = false ) { if ( $text === false ) { @@ -579,6 +515,10 @@ * @return bool */ function isCurrent() { + # If no oldid, this is the current version. + if ($this->getOldID() == 0) + return true; + return $this->exists() && isset( $this->mRevision ) && $this->mRevision->isCurrent(); @@ -590,8 +530,6 @@ * @private */ function loadLastEdit() { - global $wgOut; - if ( -1 != $this->mUser ) return; @@ -606,12 +544,16 @@ $this->mTimestamp = $this->mLastRevision->getTimestamp(); $this->mComment = $this->mLastRevision->getComment(); $this->mMinorEdit = $this->mLastRevision->isMinor(); + $this->mRevIdFetched = $this->mLastRevision->getID(); } } function getTimestamp() { - $this->loadLastEdit(); - return $this->mTimestamp; + // Check if the field has been filled by ParserCache::get() + if ( !$this->mTimestamp ) { + $this->loadLastEdit(); + } + return wfTimestamp(TS_MW, $this->mTimestamp); } function getUser() { @@ -639,18 +581,18 @@ return $this->mRevIdFetched; } + /** + * @todo Document, fixme $offset never used. + * @param $limit Integer: default 0. + * @param $offset Integer: default 0. + */ function getContributors($limit = 0, $offset = 0) { - $fname = 'Article::getContributors'; - # XXX: this is expensive; cache this info somewhere. - $title = $this->mTitle; $contribs = array(); - $dbr =& $this->getDB(); + $dbr = wfGetDB( DB_SLAVE ); $revTable = $dbr->tableName( 'revision' ); $userTable = $dbr->tableName( 'user' ); - $encDBkey = $dbr->addQuotes( $title->getDBkey() ); - $ns = $title->getNamespace(); $user = $this->getUser(); $pageId = $this->getId(); @@ -664,7 +606,7 @@ if ($limit > 0) { $sql .= ' LIMIT '.$limit; } $sql .= ' '. $this->getSelectOptions(); - $res = $dbr->query($sql, $fname); + $res = $dbr->query($sql, __METHOD__); while ( $line = $dbr->fetchObject( $res ) ) { $contribs[] = array($line->rev_user, $line->rev_user_text, $line->user_real_name); @@ -679,72 +621,149 @@ * the given title. */ function view() { - global $wgUser, $wgOut, $wgRequest, $wgOnlySysopsCanPatrol, $wgLang; - global $wgLinkCache, $IP, $wgEnableParserCache, $wgStylePath, $wgUseRCPatrol; - global $wgEnotif, $wgParser, $wgParserCache, $wgUseTrackbacks; + global $wgUser, $wgOut, $wgRequest, $wgContLang; + global $wgEnableParserCache, $wgStylePath, $wgUseRCPatrol, $wgParser; + global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies; $sk = $wgUser->getSkin(); - $fname = 'Article::view'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); + + $parserCache =& ParserCache::singleton(); + $ns = $this->mTitle->getNamespace(); # shortcut + # Get variables from query string $oldid = $this->getOldID(); + + # getOldID may want us to redirect somewhere else + if ( $this->mRedirectUrl ) { + $wgOut->redirect( $this->mRedirectUrl ); + wfProfileOut( __METHOD__ ); + return; + } + $diff = $wgRequest->getVal( 'diff' ); $rcid = $wgRequest->getVal( 'rcid' ); $rdfrom = $wgRequest->getVal( 'rdfrom' ); + $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); $wgOut->setArticleFlag( true ); - $wgOut->setRobotpolicy( 'index,follow' ); + + # Discourage indexing of printable versions, but encourage following + if( $wgOut->isPrintable() ) { + $policy = 'noindex,follow'; + } elseif ( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) { + $policy = $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()]; + } elseif( isset( $wgNamespaceRobotPolicies[$ns] ) ) { + # Honour customised robot policies for this namespace + $policy = $wgNamespaceRobotPolicies[$ns]; + } else { + # Default to encourage indexing and following links + $policy = 'index,follow'; + } + $wgOut->setRobotPolicy( $policy ); + # If we got diff and oldid in the query, we want to see a # diff page instead of the article. if ( !is_null( $diff ) ) { - require_once( 'DifferenceEngine.php' ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $de = new DifferenceEngine( $oldid, $diff, $rcid ); + $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid ); // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; - $de->showDiffPage(); + $de->showDiffPage( $diffOnly ); - if( $diff == 0 ) { + // Needed to get the page's current revision + $this->loadPageData(); + if( $diff == 0 || $diff == $this->mLatest ) { # Run view updates for current revision only $this->viewUpdates(); } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return; } if ( empty( $oldid ) && $this->checkTouched() ) { - $wgOut->setETag($wgParserCache->getETag($this, $wgUser)); + $wgOut->setETag($parserCache->getETag($this, $wgUser)); if( $wgOut->checkLastModified( $this->mTouched ) ){ - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return; } else if ( $this->tryFileCache() ) { # tell wgOut that output is taken care of $wgOut->disable(); $this->viewUpdates(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return; } } + # Should the parser cache be used? - $pcache = $wgEnableParserCache && - intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 && - $this->exists() && - empty( $oldid ); + $pcache = $wgEnableParserCache + && intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 + && $this->exists() + && empty( $oldid ) + && !$this->mTitle->isCssOrJsPage() + && !$this->mTitle->isCssJsSubpage(); wfDebug( 'Article::view using parser cache: ' . ($pcache ? 'yes' : 'no' ) . "\n" ); + if ( $wgUser->getOption( 'stubthreshold' ) ) { + wfIncrStats( 'pcache_miss_stub' ); + } + + $wasRedirected = false; + if ( isset( $this->mRedirectedFrom ) ) { + // This is an internally redirected page view. + // We'll need a backlink to the source page for navigation. + if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) { + $sk = $wgUser->getSkin(); + $redir = $sk->makeKnownLinkObj( $this->mRedirectedFrom, '', 'redirect=no' ); + $s = wfMsg( 'redirectedfrom', $redir ); + $wgOut->setSubtitle( $s ); + + // Set the fragment if one was specified in the redirect + if ( strval( $this->mTitle->getFragment() ) != '' ) { + $fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() ); + $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" ); + } + $wasRedirected = true; + } + } elseif ( !empty( $rdfrom ) ) { + // This is an externally redirected view, from some other wiki. + // If it was reported from a trusted site, supply a backlink. + global $wgRedirectSources; + if( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { + $sk = $wgUser->getSkin(); + $redir = $sk->makeExternalLink( $rdfrom, $rdfrom ); + $s = wfMsg( 'redirectedfrom', $redir ); + $wgOut->setSubtitle( $s ); + $wasRedirected = true; + } + } $outputDone = false; + wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$pcache ) ); if ( $pcache ) { if ( $wgOut->tryParserCache( $this, $wgUser ) ) { + // Ensure that UI elements requiring revision ID have + // the correct version information. + $wgOut->setRevisionId( $this->mLatest ); $outputDone = true; } } if ( !$outputDone ) { - $text = $this->getContent( false ); # May change mTitle by following a redirect + $text = $this->getContent(); + if ( $text === false ) { + # Failed to load, replace text with error message + $t = $this->mTitle->getPrefixedText(); + if( $oldid ) { + $t .= ',oldid='.$oldid; + $text = wfMsg( 'missingarticle', $t ); + } else { + $text = wfMsg( 'noarticletext', $t ); + } + } - # Another whitelist check in case oldid or redirects are altering the title + # Another whitelist check in case oldid is altering the title if ( !$this->mTitle->userCanRead() ) { $wgOut->loginToUse(); $wgOut->output(); @@ -754,65 +773,84 @@ # We're looking at an old revision if ( !empty( $oldid ) ) { - $this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid ); - $wgOut->setRobotpolicy( 'noindex,follow' ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + if( is_null( $this->mRevision ) ) { + // FIXME: This would be a nice place to load the 'no such page' text. + } else { + $this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid ); + if( $this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { + if( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) { + $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) ); + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + return; + } else { + $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) ); + // and we are allowed to see... + } + } + } + } - if ( '' != $this->mRedirectedFrom ) { - if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) { - $sk = $wgUser->getSkin(); - $redir = $sk->makeKnownLink( $this->mRedirectedFrom, '', 'redirect=no' ); - $s = wfMsg( 'redirectedfrom', $redir ); - $wgOut->setSubtitle( $s ); - # Can't cache redirects - $pcache = false; - } - } elseif ( !empty( $rdfrom ) ) { - global $wgRedirectSources; - if( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { - $sk = $wgUser->getSkin(); - $redir = $sk->makeExternalLink( $rdfrom, $rdfrom ); - $s = wfMsg( 'redirectedfrom', $redir ); - $wgOut->setSubtitle( $s ); + } + if( !$outputDone ) { + $wgOut->setRevisionId( $this->getRevIdFetched() ); + + // Pages containing custom CSS or JavaScript get special treatment + if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { + $wgOut->addHtml( wfMsgExt( 'clearyourcache', 'parse' ) ); + + // Give hooks a chance to customise the output + if( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) { + // Wrap the whole lot in a
     and don't parse
    +					$m = array();
    +					preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
    +					$wgOut->addHtml( "
    \n" );
    +					$wgOut->addHtml( htmlspecialchars( $this->mContent ) );
    +					$wgOut->addHtml( "\n
    \n" ); } + } - - # wrap user css and user js in pre and don't parse - # XXX: use $this->mTitle->usCssJsSubpage() when php is fixed/ a workaround is found - if ( - $this->mTitle->getNamespace() == NS_USER && - preg_match('/\\/[\\w]+\\.(css|js)$/', $this->mTitle->getDBkey()) - ) { - $wgOut->addWikiText( wfMsg('clearyourcache')); - $wgOut->addHTML( '
    '.htmlspecialchars($this->mContent)."\n
    " ); - } else if ( $rt = Title::newFromRedirect( $text ) ) { + + elseif ( $rt = Title::newFromRedirect( $text ) ) { # Display redirect - $imageUrl = $wgStylePath.'/common/images/redirect.png'; - $targetUrl = $rt->escapeLocalURL(); - $titleText = htmlspecialchars( $rt->getPrefixedText() ); - $link = $sk->makeLinkObj( $rt ); + $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr'; + $imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png'; + # Don't overwrite the subtitle if this was an old revision + if( !$wasRedirected && $this->isCurrent() ) { + $wgOut->setSubtitle( wfMsgHtml( 'redirectpagesub' ) ); + } + $link = $sk->makeLinkObj( $rt, $rt->getFullText() ); - $wgOut->addHTML( '#REDIRECT' . + $wgOut->addHTML( '#REDIRECT ' . ''.$link.'' ); $parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser)); - $catlinks = $parseout->getCategoryLinks(); - $wgOut->addCategoryLinks($catlinks); - $skin = $wgUser->getSkin(); + $wgOut->addParserOutputNoText( $parseout ); } else if ( $pcache ) { # Display content and save to parser cache - $wgOut->addPrimaryWikiText( $text, $this ); + $this->outputWikiText( $text ); } else { # Display content, don't attempt to save to parser cache - # Don't show section-edit links on old revisions... this way lies madness. if( !$this->isCurrent() ) { - $oldEditSectionSetting = $wgOut->mParserOptions->setEditSection( false ); + $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false ); + } + # Display content and don't save to parser cache + # With timing hack -- TS 2006-07-26 + $time = -wfTime(); + $this->outputWikiText( $text, false ); + $time += wfTime(); + + # Timing hack + if ( $time > 3 ) { + wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, + $this->mTitle->getPrefixedDBkey())); } - $wgOut->addWikiText( $text ); if( !$this->isCurrent() ) { - $wgOut->mParserOptions->setEditSection( $oldEditSectionSetting ); + $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); } + } } /* title may have been set from the cache */ @@ -821,18 +859,20 @@ $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); } + # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page + if( $ns == NS_USER_TALK && + User::isIP( $this->mTitle->getText() ) ) { + $wgOut->addWikiText( wfMsg('anontalkpagetext') ); + } + # If we have been passed an &rcid= parameter, we want to give the user a # chance to mark this new article as patrolled. - if ( $wgUseRCPatrol - && !is_null($rcid) - && $rcid != 0 - && $wgUser->isLoggedIn() - && ( $wgUser->isAllowed('patrol') || !$wgOnlySysopsCanPatrol ) ) - { + if ( $wgUseRCPatrol && !is_null( $rcid ) && $rcid != 0 && $wgUser->isAllowed( 'patrol' ) ) { $wgOut->addHTML( "' ); @@ -842,14 +882,8 @@ if ($wgUseTrackbacks) $this->addTrackbacks(); - # Put link titles into the link cache - $wgOut->transformBuffer(); - - # Add link titles as META keywords - $wgOut->addMetaTags() ; - $this->viewUpdates(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } function addTrackbacks() { @@ -868,10 +902,10 @@ $tbtext = ""; while ($o = $dbr->fetchObject($tbs)) { $rmvtxt = ""; - if ($wgUser->isSysop()) { + if ($wgUser->isAllowed( 'trackback' )) { $delurl = $this->mTitle->getFullURL("action=deletetrackback&tbid=" - . $o->tb_id . "&token=" . $wgUser->editToken()); - $rmvtxt = wfMsg('trackbackremove', $delurl); + . $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) ); + $rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) ); } $tbtext .= wfMsg(strlen($o->tb_ex) ? 'trackbackexcerpt' : 'trackback', $o->tb_title, @@ -892,7 +926,7 @@ } if ((!$wgUser->isAllowed('delete'))) { - $wgOut->sysopRequired(); + $wgOut->permissionRequired( 'delete' ); return; } @@ -915,6 +949,51 @@ } /** + * Handle action=purge + */ + function purge() { + global $wgUser, $wgRequest, $wgOut; + + if ( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) { + if( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { + $this->doPurge(); + } + } else { + $msg = $wgOut->parse( wfMsg( 'confirm_purge' ) ); + $action = $this->mTitle->escapeLocalURL( 'action=purge' ); + $button = htmlspecialchars( wfMsg( 'confirm_purge_button' ) ); + $msg = str_replace( '$1', + "
    \n" . + "\n" . + "
    \n", $msg ); + + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->addHTML( $msg ); + } + } + + /** + * Perform the actions of a page purging + */ + function doPurge() { + global $wgUseSquid; + // Invalidate the cache + $this->mTitle->invalidateCache(); + + if ( $wgUseSquid ) { + // Commit the transaction before the purge is sent + $dbw = wfGetDB( DB_MASTER ); + $dbw->immediateCommit(); + + // Send purge + $update = SquidUpdate::newSimplePurge( $this->mTitle ); + $update->doUpdate(); + } + $this->view(); + } + + /** * Insert a new empty page record for this article. * This *must* be followed up by creating a revision * and running $this->updateToLatest( $rev_id ); @@ -922,13 +1001,11 @@ * Best if all done inside a transaction. * * @param Database $dbw - * @param string $restrictions * @return int The newly created page_id key - * @access private + * @private */ - function insertOn( &$dbw, $restrictions = '' ) { - $fname = 'Article::insertOn'; - wfProfileIn( $fname ); + function insertOn( $dbw ) { + wfProfileIn( __METHOD__ ); $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' ); $dbw->insert( 'page', array( @@ -936,19 +1013,19 @@ 'page_namespace' => $this->mTitle->getNamespace(), 'page_title' => $this->mTitle->getDBkey(), 'page_counter' => 0, - 'page_restrictions' => $restrictions, + 'page_restrictions' => '', 'page_is_redirect' => 0, # Will set this shortly... 'page_is_new' => 1, 'page_random' => wfRandom(), 'page_touched' => $dbw->timestamp(), 'page_latest' => 0, # Fill this in shortly... 'page_len' => 0, # Fill this in shortly... - ), $fname ); + ), __METHOD__ ); $newid = $dbw->insertId(); $this->mTitle->resetArticleId( $newid ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $newid; } @@ -956,18 +1033,22 @@ * Update the page record to point to a newly saved revision. * * @param Database $dbw - * @param Revision $revision -- for ID number, and text used to set - length and redirect status fields - * @param int $lastRevision -- if given, will not overwrite the page field - * when different from the currently set value. - * Giving 0 indicates the new page flag should - * be set on. + * @param Revision $revision For ID number, and text used to set + length and redirect status fields + * @param int $lastRevision If given, will not overwrite the page field + * when different from the currently set value. + * Giving 0 indicates the new page flag should + * be set on. + * @param bool $lastRevIsRedirect If given, will optimize adding and + * removing rows in redirect table. * @return bool true on success, false on failure - * @access private + * @private */ - function updateRevisionOn( &$dbw, $revision, $lastRevision = null ) { - $fname = 'Article::updateToRevision'; - wfProfileIn( $fname ); + function updateRevisionOn( &$dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { + wfProfileIn( __METHOD__ ); + + $text = $revision->getText(); + $rt = Title::newFromRedirect( $text ); $conditions = array( 'page_id' => $this->getId() ); if( !is_null( $lastRevision ) ) { @@ -975,20 +1056,70 @@ $conditions['page_latest'] = $lastRevision; } - $text = $revision->getText(); $dbw->update( 'page', array( /* SET */ 'page_latest' => $revision->getId(), 'page_touched' => $dbw->timestamp(), 'page_is_new' => ($lastRevision === 0) ? 1 : 0, - 'page_is_redirect' => Article::isRedirect( $text ) ? 1 : 0, + 'page_is_redirect' => $rt !== NULL ? 1 : 0, 'page_len' => strlen( $text ), ), $conditions, - $fname ); + __METHOD__ ); + + $result = $dbw->affectedRows() != 0; + + if ($result) { + // FIXME: Should the result from updateRedirectOn() be returned instead? + $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); + } + + wfProfileOut( __METHOD__ ); + return $result; + } + + /** + * Add row to the redirect table if this is a redirect, remove otherwise. + * + * @param Database $dbw + * @param $redirectTitle a title object pointing to the redirect target, + * or NULL if this is not a redirect + * @param bool $lastRevIsRedirect If given, will optimize adding and + * removing rows in redirect table. + * @return bool true on success, false on failure + * @private + */ + function updateRedirectOn( &$dbw, $redirectTitle, $lastRevIsRedirect = null ) { + + // Always update redirects (target link might have changed) + // Update/Insert if we don't know if the last revision was a redirect or not + // Delete if changing from redirect to non-redirect + $isRedirect = !is_null($redirectTitle); + if ($isRedirect || is_null($lastRevIsRedirect) || $lastRevIsRedirect !== $isRedirect) { + + wfProfileIn( __METHOD__ ); + + if ($isRedirect) { + + // This title is a redirect, Add/Update row in the redirect table + $set = array( /* SET */ + 'rd_namespace' => $redirectTitle->getNamespace(), + 'rd_title' => $redirectTitle->getDBkey(), + 'rd_from' => $this->getId(), + ); + + $dbw->replace( 'redirect', array( 'rd_from' ), $set, __METHOD__ ); + } else { + // This is not a redirect, remove row from redirect table + $where = array( 'rd_from' => $this->getId() ); + $dbw->delete( 'redirect', $where, __METHOD__); + } + + wfProfileOut( __METHOD__ ); + return ( $dbw->affectedRows() != 0 ); + } - wfProfileOut( $fname ); - return ( $dbw->affectedRows() != 0 ); + return true; } /** @@ -999,451 +1130,447 @@ * @param Revision $revision */ function updateIfNewerOn( &$dbw, $revision ) { - $fname = 'Article::updateIfNewerOn'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $row = $dbw->selectRow( array( 'revision', 'page' ), - array( 'rev_id', 'rev_timestamp' ), + array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ), array( 'page_id' => $this->getId(), 'page_latest=rev_id' ), - $fname ); + __METHOD__ ); if( $row ) { - if( $row->rev_timestamp >= $revision->getTimestamp() ) { - wfProfileOut( $fname ); + if( wfTimestamp(TS_MW, $row->rev_timestamp) >= $revision->getTimestamp() ) { + wfProfileOut( __METHOD__ ); return false; } $prev = $row->rev_id; + $lastRevIsRedirect = (bool)$row->page_is_redirect; } else { # No or missing previous revision; mark the page as new $prev = 0; + $lastRevIsRedirect = null; } - $ret = $this->updateRevisionOn( $dbw, $revision, $prev ); - wfProfileOut( $fname ); + $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect ); + wfProfileOut( __METHOD__ ); return $ret; } /** - * Theoretically we could defer these whole insert and update - * functions for after display, but that's taking a big leap - * of faith, and we want to be able to report database - * errors at some point. - * @private + * @return string Complete article text, or null if error */ - function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false ) { - global $wgOut, $wgUser; - global $wgUseSquid, $wgDeferredUpdateList, $wgInternalServer; - - $fname = 'Article::insertNewArticle'; - wfProfileIn( $fname ); + function replaceSection($section, $text, $summary = '', $edittime = NULL) { + wfProfileIn( __METHOD__ ); - $this->mGoodAdjustment = $this->isCountable( $text ); - $this->mTotalAdjustment = 1; + if( $section == '' ) { + // Whole-page edit; let the text through unmolested. + } else { + if( is_null( $edittime ) ) { + $rev = Revision::newFromTitle( $this->mTitle ); + } else { + $dbw = wfGetDB( DB_MASTER ); + $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); + } + if( is_null( $rev ) ) { + wfDebug( "Article::replaceSection asked for bogus section (page: " . + $this->getId() . "; section: $section; edittime: $edittime)\n" ); + return null; + } + $oldtext = $rev->getText(); - $ns = $this->mTitle->getNamespace(); - $ttl = $this->mTitle->getDBkey(); + if( $section == 'new' ) { + # Inserting a new section + $subject = $summary ? wfMsgForContent('newsectionheaderdefaultlevel',$summary) . "\n\n" : ''; + $text = strlen( trim( $oldtext ) ) > 0 + ? "{$oldtext}\n\n{$subject}{$text}" + : "{$subject}{$text}"; + } else { + # Replacing an existing section; roll out the big guns + global $wgParser; + $text = $wgParser->replaceSection( $oldtext, $section, $text ); + } - # If this is a comment, add the summary as headline - if($comment && $summary!="") { - $text="== {$summary} ==\n\n".$text; } - $text = $this->preSaveTransform( $text ); - $isminor = ( $isminor && $wgUser->isLoggedIn() ) ? 1 : 0; - $now = wfTimestampNow(); - - $dbw =& wfGetDB( DB_MASTER ); - - # Add the page record; stake our claim on this title! - $newid = $this->insertOn( $dbw ); - - # Save the revision text... - $revision = new Revision( array( - 'page' => $newid, - 'comment' => $summary, - 'minor_edit' => $isminor, - 'text' => $text - ) ); - $revisionId = $revision->insertOn( $dbw ); - $this->mTitle->resetArticleID( $newid ); + wfProfileOut( __METHOD__ ); + return $text; + } - # Update the page record with revision data - $this->updateRevisionOn( $dbw, $revision, 0 ); + /** + * @deprecated use Article::doEdit() + */ + function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false ) { + $flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | + ( $isminor ? EDIT_MINOR : 0 ) | + ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ); - Article::onArticleCreate( $this->mTitle ); - if(!$suppressRC) { - RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, 'default', - '', strlen( $text ), $revisionId ); + # If this is a comment, add the summary as headline + if ( $comment && $summary != "" ) { + $text = wfMsgForContent('newsectionheaderdefaultlevel',$summary) . "\n\n".$text; } + $this->doEdit( $text, $summary, $flags ); + + $dbw = wfGetDB( DB_MASTER ); if ($watchthis) { - if(!$this->mTitle->userIsWatching()) $this->watch(); + if (!$this->mTitle->userIsWatching()) { + $dbw->begin(); + $this->doWatch(); + $dbw->commit(); + } } else { if ( $this->mTitle->userIsWatching() ) { - $this->unwatch(); + $dbw->begin(); + $this->doUnwatch(); + $dbw->commit(); } } + $this->doRedirect( $this->isRedirect( $text ) ); + } - # The talk page isn't in the regular link tables, so we need to update manually: - $talkns = $ns ^ 1; # talk -> normal; normal -> talk - $dbw->update( 'page', - array( 'page_touched' => $dbw->timestamp($now) ), - array( 'page_namespace' => $talkns, - 'page_title' => $ttl ), - $fname ); - - # standard deferred updates - $this->editUpdates( $text, $summary, $isminor, $now ); - - $oldid = 0; # new article - $this->showArticle( $text, wfMsg( 'newarticle' ), false, $isminor, $now, $summary, $oldid ); - wfProfileOut( $fname ); - } - - function getTextOfLastEditWithSectionReplacedOrAdded($section, $text, $summary = '', $edittime = NULL) { - $fname = 'Article::getTextOfLastEditWithSectionReplacedOrAdded'; - if ($section != '') { - if( is_null( $edittime ) ) { - $rev = Revision::newFromTitle( $this->mTitle ); - } else { - $dbw =& wfGetDB( DB_MASTER ); - $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); - } - $oldtext = $rev->getText(); + /** + * @deprecated use Article::doEdit() + */ + function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) { + $flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | + ( $minor ? EDIT_MINOR : 0 ) | + ( $forceBot ? EDIT_FORCE_BOT : 0 ); - if($section=='new') { - if($summary) $subject="== {$summary} ==\n\n"; - $text=$oldtext."\n\n".$subject.$text; + $good = $this->doEdit( $text, $summary, $flags ); + if ( $good ) { + $dbw = wfGetDB( DB_MASTER ); + if ($watchthis) { + if (!$this->mTitle->userIsWatching()) { + $dbw->begin(); + $this->doWatch(); + $dbw->commit(); + } } else { - - # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML - # comments to be stripped as well) - $striparray=array(); - $parser=new Parser(); - $parser->mOutputType=OT_WIKI; - $parser->mOptions = new ParserOptions(); - $oldtext=$parser->strip($oldtext, $striparray, true); - - # now that we can be sure that no pseudo-sections are in the source, - # split it up - # Unfortunately we can't simply do a preg_replace because that might - # replace the wrong section, so we have to use the section counter instead - $secs=preg_split('/(^=+.+?=+|^.*?<\/h[1-6].*?' . '>)(?!\S)/mi', - $oldtext,-1,PREG_SPLIT_DELIM_CAPTURE); - $secs[$section*2]=$text."\n\n"; // replace with edited - - # section 0 is top (intro) section - if($section!=0) { - - # headline of old section - we need to go through this section - # to determine if there are any subsections that now need to - # be erased, as the mother section has been replaced with - # the text of all subsections. - $headline=$secs[$section*2-1]; - preg_match( '/^(=+).+?=+|^.*?<\/h[1-6].*?' . '>(?!\S)/mi',$headline,$matches); - $hlevel=$matches[1]; - - # determine headline level for wikimarkup headings - if(strpos($hlevel,'=')!==false) { - $hlevel=strlen($hlevel); - } - - $secs[$section*2-1]=''; // erase old headline - $count=$section+1; - $break=false; - while(!empty($secs[$count*2-1]) && !$break) { - - $subheadline=$secs[$count*2-1]; - preg_match( - '/^(=+).+?=+|^.*?<\/h[1-6].*?' . '>(?!\S)/mi',$subheadline,$matches); - $subhlevel=$matches[1]; - if(strpos($subhlevel,'=')!==false) { - $subhlevel=strlen($subhlevel); - } - if($subhlevel > $hlevel) { - // erase old subsections - $secs[$count*2-1]=''; - $secs[$count*2]=''; - } - if($subhlevel <= $hlevel) { - $break=true; - } - $count++; - - } - + if ( $this->mTitle->userIsWatching() ) { + $dbw->begin(); + $this->doUnwatch(); + $dbw->commit(); } - $text=join('',$secs); - # reinsert the stuff that we stripped out earlier - $text=$parser->unstrip($text,$striparray); - $text=$parser->unstripNoWiki($text,$striparray); } + $extraq = ''; // Give extensions a chance to modify URL query on update + wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraq ) ); + + $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraq ); } - return $text; + return $good; } /** - * Change an existing article. Puts the previous version back into the old table, updates RC - * and all necessary caches, mostly via the deferred update array. + * Article::doEdit() + * + * Change an existing article or create a new article. Updates RC and all necessary caches, + * optionally via the deferred update array. + * + * $wgUser must be set before calling this function. * - * It is possible to call this function from a command-line script, but note that you should - * first set $wgUser, and clean up $wgDeferredUpdates after each edit. + * @param string $text New text + * @param string $summary Edit summary + * @param integer $flags bitfield: + * EDIT_NEW + * Article is known or assumed to be non-existent, create a new one + * EDIT_UPDATE + * Article is known or assumed to be pre-existing, update it + * EDIT_MINOR + * Mark this edit minor, if the user is allowed to do so + * EDIT_SUPPRESS_RC + * Do not log the change in recentchanges + * EDIT_FORCE_BOT + * Mark the edit a "bot" edit regardless of user rights + * EDIT_DEFER_UPDATES + * Defer some of the updates until the end of index.php + * EDIT_AUTOSUMMARY + * Fill in blank summaries with generated text where possible + * + * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected. + * If EDIT_UPDATE is specified and the article doesn't exist, the function will return false. If + * EDIT_NEW is specified and the article does exist, a duplicate key error will cause an exception + * to be thrown from the Database. These two conditions are also possible with auto-detection due + * to MediaWiki's performance-optimised locking strategy. + * + * @return bool success */ - function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) { - global $wgOut, $wgUser; - global $wgDBtransactions, $wgMwRedir; - global $wgUseSquid, $wgInternalServer, $wgPostCommitUpdateList, $wgUseFileCache; + function doEdit( $text, $summary, $flags = 0 ) { + global $wgUser, $wgDBtransactions; - $fname = 'Article::updateArticle'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $good = true; - $isminor = ( $minor && $wgUser->isLoggedIn() ); - if ( $this->isRedirect( $text ) ) { - # Remove all content but redirect - # This could be done by reconstructing the redirect from a title given by - # Title::newFromRedirect(), but then we wouldn't know which synonym the user - # wants to see - if ( preg_match( "/^((" . $wgMwRedir->getBaseRegex() . ')[^\\n]+)/i', $text, $m ) ) { - $redir = 1; - $text = $m[1] . "\n"; + if ( !($flags & EDIT_NEW) && !($flags & EDIT_UPDATE) ) { + $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); + if ( $aid ) { + $flags |= EDIT_UPDATE; + } else { + $flags |= EDIT_NEW; } } - else { $redir = 0; } - - $text = $this->preSaveTransform( $text ); - $dbw =& wfGetDB( DB_MASTER ); - $now = wfTimestampNow(); - # Update article, but only if changed. - - # It's important that we either rollback or complete, otherwise an attacker could - # overwrite cur entries by sending precisely timed user aborts. Random bored users - # could conceivably have the same effect, especially if cur is locked for long periods. - if( !$wgDBtransactions ) { - $userAbort = ignore_user_abort( true ); + if( !wfRunHooks( 'ArticleSave', array( &$this, &$wgUser, &$text, + &$summary, $flags & EDIT_MINOR, + null, null, &$flags ) ) ) + { + wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" ); + wfProfileOut( __METHOD__ ); + return false; } - $oldtext = $this->getContent( true ); + # Silently ignore EDIT_MINOR if not allowed + $isminor = ( $flags & EDIT_MINOR ) && $wgUser->isAllowed('minoredit'); + $bot = $wgUser->isAllowed( 'bot' ) || ( $flags & EDIT_FORCE_BOT ); + + $oldtext = $this->getContent(); $oldsize = strlen( $oldtext ); - $newsize = strlen( $text ); - $lastRevision = 0; - if ( 0 != strcmp( $text, $oldtext ) ) { - $this->mGoodAdjustment = $this->isCountable( $text ) - - $this->isCountable( $oldtext ); - $this->mTotalAdjustment = 0; - $now = wfTimestampNow(); + # Provide autosummaries if one is not provided. + if ($flags & EDIT_AUTOSUMMARY && $summary == '') + $summary = $this->getAutosummary( $oldtext, $text, $flags ); - $lastRevision = $dbw->selectField( - 'page', 'page_latest', array( 'page_id' => $this->getId() ) ); + $text = $this->preSaveTransform( $text ); + $newsize = strlen( $text ); - $revision = new Revision( array( - 'page' => $this->getId(), - 'comment' => $summary, - 'minor_edit' => $isminor, - 'text' => $text - ) ); - - $dbw->immediateCommit(); - $dbw->begin(); - $revisionId = $revision->insertOn( $dbw ); + $dbw = wfGetDB( DB_MASTER ); + $now = wfTimestampNow(); - # Update page - $ok = $this->updateRevisionOn( $dbw, $revision, $lastRevision ); + if ( $flags & EDIT_UPDATE ) { + # Update article, but only if changed. - if( !$ok ) { - /* Belated edit conflict! Run away!! */ - $good = false; - $dbw->rollback(); - } else { - # Update recentchanges and purge cache and whatnot - $bot = (int)($wgUser->isBot() || $forceBot); - RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $wgUser, $summary, - $lastRevision, $this->getTimestamp(), $bot, '', $oldsize, $newsize, - $revisionId ); - Article::onArticleEdit( $this->mTitle ); - $dbw->commit(); + # Make sure the revision is either completely inserted or not inserted at all + if( !$wgDBtransactions ) { + $userAbort = ignore_user_abort( true ); } - } - if( !$wgDBtransactions ) { - ignore_user_abort( $userAbort ); - } + $lastRevision = 0; + $revisionId = 0; - if ( $good ) { - if ($watchthis) { - if (!$this->mTitle->userIsWatching()) $this->watch(); - } else { - if ( $this->mTitle->userIsWatching() ) { - $this->unwatch(); - } - } - # standard deferred updates - $this->editUpdates( $text, $summary, $minor, $now ); + if ( 0 != strcmp( $text, $oldtext ) ) { + $this->mGoodAdjustment = (int)$this->isCountable( $text ) + - (int)$this->isCountable( $oldtext ); + $this->mTotalAdjustment = 0; + $lastRevision = $dbw->selectField( + 'page', 'page_latest', array( 'page_id' => $this->getId() ) ); - $urls = array(); - # Template namespace - # Purge all articles linking here - if ( $this->mTitle->getNamespace() == NS_TEMPLATE) { - $titles = $this->mTitle->getLinksTo(); - Title::touchArray( $titles ); - if ( $wgUseSquid ) { - foreach ( $titles as $title ) { - $urls[] = $title->getInternalURL(); + if ( !$lastRevision ) { + # Article gone missing + wfDebug( __METHOD__.": EDIT_UPDATE specified but article doesn't exist\n" ); + wfProfileOut( __METHOD__ ); + return false; + } + + $revision = new Revision( array( + 'page' => $this->getId(), + 'comment' => $summary, + 'minor_edit' => $isminor, + 'text' => $text + ) ); + + $dbw->begin(); + $revisionId = $revision->insertOn( $dbw ); + + # Update page + $ok = $this->updateRevisionOn( $dbw, $revision, $lastRevision ); + + if( !$ok ) { + /* Belated edit conflict! Run away!! */ + $good = false; + $dbw->rollback(); + } else { + # Update recentchanges + if( !( $flags & EDIT_SUPPRESS_RC ) ) { + $rcid = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $wgUser, $summary, + $lastRevision, $this->getTimestamp(), $bot, '', $oldsize, $newsize, + $revisionId ); + + # Mark as patrolled if the user can do so + if( $GLOBALS['wgUseRCPatrol'] && $wgUser->isAllowed( 'autopatrol' ) ) { + RecentChange::markPatrolled( $rcid ); + PatrolLog::record( $rcid, true ); } + } + $wgUser->incEditCount(); + $dbw->commit(); } + } else { + $revision = null; + // Keep the same revision ID, but do some updates on it + $revisionId = $this->getRevIdFetched(); + // Update page_touched, this is usually implicit in the page update + // Other cache updates are done in onArticleEdit() + $this->mTitle->invalidateCache(); } - # Squid updates - if ( $wgUseSquid ) { - $urls = array_merge( $urls, $this->mTitle->getSquidURLs() ); - $u = new SquidUpdate( $urls ); - array_push( $wgPostCommitUpdateList, $u ); + if( !$wgDBtransactions ) { + ignore_user_abort( $userAbort ); } - # File cache - if ( $wgUseFileCache ) { - $cm = new CacheManager($this->mTitle); - @unlink($cm->fileCacheName()); + if ( $good ) { + # Invalidate cache of this article and all pages using this article + # as a template. Partly deferred. + Article::onArticleEdit( $this->mTitle ); + + # Update links tables, site stats, etc. + $changed = ( strcmp( $oldtext, $text ) != 0 ); + $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed ); } + } else { + # Create new article - $this->showArticle( $text, wfMsg( 'updated' ), $sectionanchor, $isminor, $now, $summary, $lastRevision ); - } - wfProfileOut( $fname ); - return $good; - } + # Set statistics members + # We work out if it's countable after PST to avoid counter drift + # when articles are created with {{subst:}} + $this->mGoodAdjustment = (int)$this->isCountable( $text ); + $this->mTotalAdjustment = 1; - /** - * After we've either updated or inserted the article, update - * the link tables and redirect to the new page. - */ - function showArticle( $text, $subtitle , $sectionanchor = '', $me2, $now, $summary, $oldid ) { - global $wgUseDumbLinkUpdate, $wgAntiLockFlags, $wgOut, $wgUser, $wgLinkCache, $wgEnotif; - global $wgUseEnotif; + $dbw->begin(); + + # Add the page record; stake our claim on this title! + # This will fail with a database query exception if the article already exists + $newid = $this->insertOn( $dbw ); + + # Save the revision text... + $revision = new Revision( array( + 'page' => $newid, + 'comment' => $summary, + 'minor_edit' => $isminor, + 'text' => $text + ) ); + $revisionId = $revision->insertOn( $dbw ); - $fname = 'Article::showArticle'; - wfProfileIn( $fname ); + $this->mTitle->resetArticleID( $newid ); - $wgLinkCache = new LinkCache(); + # Update the page record with revision data + $this->updateRevisionOn( $dbw, $revision, 0 ); - if ( !$wgUseDumbLinkUpdate ) { - # Preload links to reduce lock time - if ( $wgAntiLockFlags & ALF_PRELOAD_LINKS ) { - $wgLinkCache->preFill( $this->mTitle ); - $wgLinkCache->clear(); + if( !( $flags & EDIT_SUPPRESS_RC ) ) { + $rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot, + '', strlen( $text ), $revisionId ); + # Mark as patrolled if the user can + if( $GLOBALS['wgUseRCPatrol'] && $wgUser->isAllowed( 'autopatrol' ) ) { + RecentChange::markPatrolled( $rcid ); + PatrolLog::record( $rcid, true ); + } } - } + $wgUser->incEditCount(); + $dbw->commit(); - # Parse the text and replace links with placeholders - $wgOut = new OutputPage(); + # Update links, etc. + $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, true ); - # Pass the current title along in case we're creating a wiki page - # which is different than the currently displayed one (e.g. image - # pages created on file uploads); otherwise, link updates will - # go wrong. - $wgOut->addWikiTextWithTitle( $text, $this->mTitle ); + # Clear caches + Article::onArticleCreate( $this->mTitle ); - if ( !$wgUseDumbLinkUpdate ) { - # Move the current links back to the second register - $wgLinkCache->swapRegisters(); + wfRunHooks( 'ArticleInsertComplete', array( &$this, &$wgUser, $text, $summary, + $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); + } - # Get old version of link table to allow incremental link updates - # Lock this data now since it is needed for an update - $wgLinkCache->forUpdate( true ); - $wgLinkCache->preFill( $this->mTitle ); + if ( $good && !( $flags & EDIT_DEFER_UPDATES ) ) { + wfDoUpdates(); + } - # Swap this old version back into its rightful place - $wgLinkCache->swapRegisters(); + if ( $good ) { + wfRunHooks( 'ArticleSaveComplete', array( &$this, &$wgUser, $text, $summary, + $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); } - if( $this->isRedirect( $text ) ) - $r = 'redirect=no'; - else - $r = ''; - $wgOut->redirect( $this->mTitle->getFullURL( $r ).$sectionanchor ); + wfProfileOut( __METHOD__ ); + return $good; + } - if ( $wgUseEnotif ) { - # this would be better as an extension hook - include_once( "UserMailer.php" ); - $wgEnotif = new EmailNotification (); - $wgEnotif->notifyOnPageChange( $this->mTitle, $now, $summary, $me2, $oldid ); + /** + * @deprecated wrapper for doRedirect + */ + function showArticle( $text, $subtitle , $sectionanchor = '', $me2, $now, $summary, $oldid ) { + $this->doRedirect( $this->isRedirect( $text ), $sectionanchor ); + } + + /** + * Output a redirect back to the article. + * This is typically used after an edit. + * + * @param boolean $noRedir Add redirect=no + * @param string $sectionAnchor section to redirect to, including "#" + */ + function doRedirect( $noRedir = false, $sectionAnchor = '', $extraq = '' ) { + global $wgOut; + if ( $noRedir ) { + $query = 'redirect=no'; + if( $extraq ) + $query .= "&$query"; + } else { + $query = $extraq; } - wfProfileOut( $fname ); + $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $sectionAnchor ); } /** * Mark this particular edit as patrolled */ function markpatrolled() { - global $wgOut, $wgRequest, $wgOnlySysopsCanPatrol, $wgUseRCPatrol, $wgUser; - $wgOut->setRobotpolicy( 'noindex,follow' ); + global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUser; + $wgOut->setRobotPolicy( 'noindex,nofollow' ); - if ( !$wgUseRCPatrol ) - { - $wgOut->errorpage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); + # Check RC patrol config. option + if( !$wgUseRCPatrol ) { + $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); return; } - if ( $wgUser->isAnon() ) - { - $wgOut->loginToUse(); + + # Check permissions + if( !$wgUser->isAllowed( 'patrol' ) ) { + $wgOut->permissionRequired( 'patrol' ); return; } - if ( $wgOnlySysopsCanPatrol && !$wgUser->isAllowed('patrol') ) - { - $wgOut->sysopRequired(); + + # If we haven't been given an rc_id value, we can't do anything + $rcid = $wgRequest->getVal( 'rcid' ); + if( !$rcid ) { + $wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); return; } - $rcid = $wgRequest->getVal( 'rcid' ); - if ( !is_null ( $rcid ) ) - { - RecentChange::markPatrolled( $rcid ); - $wgOut->setPagetitle( wfMsg( 'markedaspatrolled' ) ); - $wgOut->addWikiText( wfMsg( 'markedaspatrolledtext' ) ); - $rcTitle = Title::makeTitle( NS_SPECIAL, 'Recentchanges' ); - $wgOut->returnToMain( false, $rcTitle->getPrefixedText() ); - } - else - { - $wgOut->errorpage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); + # Handle the 'MarkPatrolled' hook + if( !wfRunHooks( 'MarkPatrolled', array( $rcid, &$wgUser, false ) ) ) { + return; } - } - - /** - * Validate function - */ - function validate() { - global $wgOut, $wgUser, $wgRequest, $wgUseValidation; - if ( !$wgUseValidation ) # Are we using article validation at all? - { - $wgOut->errorpage( "nosuchspecialpage", "nospecialpagetext" ); - return ; + $return = SpecialPage::getTitleFor( 'Recentchanges' ); + # If it's left up to us, check that the user is allowed to patrol this edit + # If the user has the "autopatrol" right, then we'll assume there are no + # other conditions stopping them doing so + if( !$wgUser->isAllowed( 'autopatrol' ) ) { + $rc = RecentChange::newFromId( $rcid ); + # Graceful error handling, as we've done before here... + # (If the recent change doesn't exist, then it doesn't matter whether + # the user is allowed to patrol it or not; nothing is going to happen + if( is_object( $rc ) && $wgUser->getName() == $rc->getAttribute( 'rc_user_text' ) ) { + # The user made this edit, and can't patrol it + # Tell them so, and then back off + $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) ); + $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrollederror-noautopatrol' ) ); + $wgOut->returnToMain( false, $return ); + return; + } } - $wgOut->setRobotpolicy( 'noindex,follow' ); - $revision = $wgRequest->getVal( 'revision' ); - - include_once ( "SpecialValidate.php" ) ; # The "Validation" class - - $v = new Validation ; - if ( $wgRequest->getVal ( "mode" , "" ) == "list" ) - $t = $v->showList ( $this ) ; - else if ( $wgRequest->getVal ( "mode" , "" ) == "details" ) - $t = $v->showDetails ( $this , $wgRequest->getVal( 'revision' ) ) ; - else - $t = $v->validatePageForm ( $this , $revision ) ; + # Mark the edit as patrolled + RecentChange::markPatrolled( $rcid ); + PatrolLog::record( $rcid ); + wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) ); - $wgOut->addHTML ( $t ) ; + # Inform the user + $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) ); + $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrolledtext' ) ); + $wgOut->returnToMain( false, $return ); } /** - * Add this page to $wgUser's watchlist + * User-interface handler for the "watch" action */ function watch() { @@ -1451,7 +1578,7 @@ global $wgUser, $wgOut; if ( $wgUser->isAnon() ) { - $wgOut->errorpage( 'watchnologin', 'watchnologintext' ); + $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' ); return; } if ( wfReadOnly() ) { @@ -1459,17 +1586,11 @@ return; } - if (wfRunHooks('WatchArticle', array(&$wgUser, &$this))) { - - $wgUser->addWatch( $this->mTitle ); - $wgUser->saveSettings(); - - wfRunHooks('WatchArticleComplete', array(&$wgUser, &$this)); - + if( $this->doWatch() ) { $wgOut->setPagetitle( wfMsg( 'addedwatch' ) ); - $wgOut->setRobotpolicy( 'noindex,follow' ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $link = $this->mTitle->getPrefixedText(); + $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); $text = wfMsg( 'addedwatchtext', $link ); $wgOut->addWikiText( $text ); } @@ -1478,15 +1599,33 @@ } /** - * Stop watching a page + * Add this page to $wgUser's watchlist + * @return bool true on successful watch operation */ + function doWatch() { + global $wgUser; + if( $wgUser->isAnon() ) { + return false; + } + + if (wfRunHooks('WatchArticle', array(&$wgUser, &$this))) { + $wgUser->addWatch( $this->mTitle ); + + return wfRunHooks('WatchArticleComplete', array(&$wgUser, &$this)); + } + + return false; + } + /** + * User interface handler for the "unwatch" action. + */ function unwatch() { global $wgUser, $wgOut; if ( $wgUser->isAnon() ) { - $wgOut->errorpage( 'watchnologin', 'watchnologintext' ); + $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' ); return; } if ( wfReadOnly() ) { @@ -1494,17 +1633,11 @@ return; } - if (wfRunHooks('UnwatchArticle', array(&$wgUser, &$this))) { - - $wgUser->removeWatch( $this->mTitle ); - $wgUser->saveSettings(); - - wfRunHooks('UnwatchArticleComplete', array(&$wgUser, &$this)); - + if( $this->doUnwatch() ) { $wgOut->setPagetitle( wfMsg( 'removedwatch' ) ); - $wgOut->setRobotpolicy( 'noindex,follow' ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $link = $this->mTitle->getPrefixedText(); + $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); $text = wfMsg( 'removedwatchtext', $link ); $wgOut->addWikiText( $text ); } @@ -1513,158 +1646,187 @@ } /** - * protect a page + * Stop watching a page + * @return bool true on successful unwatch */ - function protect( $limit = 'sysop' ) { - global $wgUser, $wgOut, $wgRequest; - - if ( ! $wgUser->isAllowed('protect') ) { - $wgOut->sysopRequired(); - return; + function doUnwatch() { + global $wgUser; + if( $wgUser->isAnon() ) { + return false; } - if ( wfReadOnly() ) { - $wgOut->readOnlyPage(); - return; + + if (wfRunHooks('UnwatchArticle', array(&$wgUser, &$this))) { + $wgUser->removeWatch( $this->mTitle ); + + return wfRunHooks('UnwatchArticleComplete', array(&$wgUser, &$this)); } + + return false; + } + + /** + * action=protect handler + */ + function protect() { + $form = new ProtectionForm( $this ); + $form->execute(); + } + + /** + * action=unprotect handler (alias) + */ + function unprotect() { + $this->protect(); + } + + /** + * Update the article's restriction field, and leave a log entry. + * + * @param array $limit set of restriction keys + * @param string $reason + * @return bool true on success + */ + function updateRestrictions( $limit = array(), $reason = '', $cascade = 0, $expiry = null ) { + global $wgUser, $wgRestrictionTypes, $wgContLang; + $id = $this->mTitle->getArticleID(); - if ( 0 == $id ) { - $wgOut->fatalError( wfMsg( 'badarticleerror' ) ); - return; + if( !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $id == 0 ) { + return false; } - $confirm = $wgRequest->wasPosted() && - $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ); - $moveonly = $wgRequest->getBool( 'wpMoveOnly' ); - $reason = $wgRequest->getText( 'wpReasonProtect' ); + if (!$cascade) { + $cascade = false; + } - if ( $confirm ) { - $dbw =& wfGetDB( DB_MASTER ); - $dbw->update( 'page', - array( /* SET */ - 'page_touched' => $dbw->timestamp(), - 'page_restrictions' => (string)$limit - ), array( /* WHERE */ - 'page_id' => $id - ), 'Article::protect' - ); + // Take this opportunity to purge out expired restrictions + Title::purgeExpiredRestrictions(); - $restrictions = "move=" . $limit; - if( !$moveonly ) { - $restrictions .= ":edit=" . $limit; - } - if (wfRunHooks('ArticleProtect', array(&$this, &$wgUser, $limit == 'sysop', $reason, $moveonly))) { + # FIXME: Same limitations as described in ProtectionForm.php (line 37); + # we expect a single selection, but the schema allows otherwise. + $current = array(); + foreach( $wgRestrictionTypes as $action ) + $current[$action] = implode( '', $this->mTitle->getRestrictions( $action ) ); - $dbw =& wfGetDB( DB_MASTER ); - $dbw->update( 'page', - array( /* SET */ - 'page_touched' => $dbw->timestamp(), - 'page_restrictions' => $restrictions - ), array( /* WHERE */ - 'page_id' => $id - ), 'Article::protect' - ); + $current = Article::flattenRestrictions( $current ); + $updated = Article::flattenRestrictions( $limit ); - wfRunHooks('ArticleProtectComplete', array(&$this, &$wgUser, $limit == 'sysop', $reason, $moveonly)); + $changed = ( $current != $updated ); + $changed = $changed || ($this->mTitle->areRestrictionsCascading() != $cascade); + $changed = $changed || ($this->mTitle->mRestrictionsExpiry != $expiry); + $protect = ( $updated != '' ); - $log = new LogPage( 'protect' ); - if ( $limit === '' ) { - $log->addEntry( 'unprotect', $this->mTitle, $reason ); - } else { - $log->addEntry( 'protect', $this->mTitle, $reason ); + # If nothing's changed, do nothing + if( $changed ) { + global $wgGroupPermissions; + if( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) { + + $dbw = wfGetDB( DB_MASTER ); + + $encodedExpiry = Block::encodeExpiry($expiry, $dbw ); + + $expiry_description = ''; + if ( $encodedExpiry != 'infinity' ) { + $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')'; } - $wgOut->redirect( $this->mTitle->getFullURL() ); - } - return; - } else { - return $this->confirmProtect( '', '', $limit ); - } - } - /** - * Output protection confirmation dialog - */ - function confirmProtect( $par, $reason, $limit = 'sysop' ) { - global $wgOut, $wgUser; + # Prepare a null revision to be added to the history + $modified = $current != '' && $protect; + if ( $protect ) { + $comment_type = $modified ? 'modifiedarticleprotection' : 'protectedarticle'; + } else { + $comment_type = 'unprotectedarticle'; + } + $comment = $wgContLang->ucfirst( wfMsgForContent( $comment_type, $this->mTitle->getPrefixedText() ) ); - wfDebug( "Article::confirmProtect\n" ); + foreach( $limit as $action => $restrictions ) { + # Check if the group level required to edit also can protect pages + # Otherwise, people who cannot normally protect can "protect" pages via transclusion + $cascade = ( $cascade && isset($wgGroupPermissions[$restrictions]['protect']) && $wgGroupPermissions[$restrictions]['protect'] ); + } + + $cascade_description = ''; + if ($cascade) { + $cascade_description = ' ['.wfMsg('protect-summary-cascade').']'; + } + + if( $reason ) + $comment .= ": $reason"; + if( $protect ) + $comment .= " [$updated]"; + if ( $expiry_description && $protect ) + $comment .= "$expiry_description"; + if ( $cascade ) + $comment .= "$cascade_description"; + + $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true ); + $nullRevId = $nullRevision->insertOn( $dbw ); + + # Update restrictions table + foreach( $limit as $action => $restrictions ) { + if ($restrictions != '' ) { + $dbw->replace( 'page_restrictions', array(array('pr_page', 'pr_type')), + array( 'pr_page' => $id, 'pr_type' => $action + , 'pr_level' => $restrictions, 'pr_cascade' => $cascade ? 1 : 0 + , 'pr_expiry' => $encodedExpiry ), __METHOD__ ); + } else { + $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, + 'pr_type' => $action ), __METHOD__ ); + } + } - $sub = htmlspecialchars( $this->mTitle->getPrefixedText() ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + # Update page record + $dbw->update( 'page', + array( /* SET */ + 'page_touched' => $dbw->timestamp(), + 'page_restrictions' => '', + 'page_latest' => $nullRevId + ), array( /* WHERE */ + 'page_id' => $id + ), 'Article::protect' + ); + wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) ); - $check = ''; - $protcom = ''; - $moveonly = ''; - - if ( $limit === '' ) { - $wgOut->setPageTitle( wfMsg( 'confirmunprotect' ) ); - $wgOut->setSubtitle( wfMsg( 'unprotectsub', $sub ) ); - $wgOut->addWikiText( wfMsg( 'confirmunprotecttext' ) ); - $protcom = htmlspecialchars( wfMsg( 'unprotectcomment' ) ); - $formaction = $this->mTitle->escapeLocalURL( 'action=unprotect' . $par ); - } else { - $wgOut->setPageTitle( wfMsg( 'confirmprotect' ) ); - $wgOut->setSubtitle( wfMsg( 'protectsub', $sub ) ); - $wgOut->addWikiText( wfMsg( 'confirmprotecttext' ) ); - $moveonly = wfMsg( 'protectmoveonly' ) ; // add it using addWikiText to prevent xss. bug:3991 - $protcom = htmlspecialchars( wfMsg( 'protectcomment' ) ); - $formaction = $this->mTitle->escapeLocalURL( 'action=protect' . $par ); - } + # Update the protection log + $log = new LogPage( 'protect' ); - $confirm = htmlspecialchars( wfMsg( 'confirm' ) ); - $token = htmlspecialchars( $wgUser->editToken() ); + if( $protect ) { + $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason . " [$updated]$cascade_description$expiry_description" ) ); + } else { + $log->addEntry( 'unprotect', $this->mTitle, $reason ); + } - $wgOut->addHTML( " -
    - - - - - " ); - if($moveonly != '') { - $wgOut->AddHTML( " - - - - " ); - } - $wgOut->addHTML( " - - - - -
    - - - -
    - - - -
      - -
    - -
    " ); + } # End hook + } # End "changed" check - $wgOut->returnToMain( false ); + return true; } /** - * Unprotect the pages + * Take an array of page restrictions and flatten it to a string + * suitable for insertion into the page_restrictions field. + * @param array $limit + * @return string + * @private */ - function unprotect() { - return $this->protect( '' ); + function flattenRestrictions( $limit ) { + if( !is_array( $limit ) ) { + throw new MWException( 'Article::flattenRestrictions given non-array restriction set' ); + } + $bits = array(); + ksort( $limit ); + foreach( $limit as $action => $restrictions ) { + if( $restrictions != '' ) { + $bits[] = "$action=$restrictions"; + } + } + return implode( ':', $bits ); } /* * UI entry point for page deletion */ function delete() { - global $wgUser, $wgOut, $wgMessageCache, $wgRequest; - $fname = 'Article::delete'; + global $wgUser, $wgOut, $wgRequest; $confirm = $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ); $reason = $wgRequest->getText( 'wpReason' ); @@ -1672,61 +1834,67 @@ # This code desperately needs to be totally rewritten # Check permissions - if( ( !$wgUser->isAllowed( 'delete' ) ) ) { - $wgOut->sysopRequired(); + if( $wgUser->isAllowed( 'delete' ) ) { + if( $wgUser->isBlocked( !$confirm ) ) { + $wgOut->blockedPage(); + return; + } + } else { + $wgOut->permissionRequired( 'delete' ); return; } + if( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } - # Better double-check that it hasn't been deleted yet! $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) ); - if( !$this->mTitle->exists() ) { - $wgOut->fatalError( wfMsg( 'cannotdelete' ) ); + + # Better double-check that it hasn't been deleted yet! + $dbw = wfGetDB( DB_MASTER ); + $conds = $this->mTitle->pageCond(); + $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); + if ( $latest === false ) { + $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); return; } if( $confirm ) { $this->doDelete( $reason ); + if( $wgRequest->getCheck( 'wpWatch' ) ) { + $this->doWatch(); + } elseif( $this->mTitle->userIsWatching() ) { + $this->doUnwatch(); + } return; } # determine whether this page has earlier revisions # and insert a warning if it does - # we select the text because it might be useful below - $dbr =& $this->getDB(); - $ns = $this->mTitle->getNamespace(); - $title = $this->mTitle->getDBkey(); - $revisions = $dbr->select( array( 'page', 'revision' ), - array( 'rev_id', 'rev_user_text' ), - array( - 'page_namespace' => $ns, - 'page_title' => $title, - 'rev_page = page_id' - ), $fname, $this->getSelectOptions( array( 'ORDER BY' => 'rev_timestamp DESC' ) ) - ); + $maxRevisions = 20; + $authors = $this->getLastNAuthors( $maxRevisions, $latest ); - if( $dbr->numRows( $revisions ) > 1 && !$confirm ) { + if( count( $authors ) > 1 && !$confirm ) { $skin=$wgUser->getSkin(); - $wgOut->addHTML(''.wfMsg('historywarning')); - $wgOut->addHTML( $skin->historyLink() .''); + $wgOut->addHTML( '' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '' ); } - # Fetch cur_text - $rev = Revision::newFromTitle( $this->mTitle ); - - # Fetch name(s) of contributors - $rev_name = ''; - $all_same_user = true; - while( $row = $dbr->fetchObject( $revisions ) ) { - if( $rev_name != '' && $rev_name != $row->rev_user_text ) { - $all_same_user = false; - } else { - $rev_name = $row->rev_user_text; + # If a single user is responsible for all revisions, find out who they are + if ( count( $authors ) == $maxRevisions ) { + // Query bailed out, too many revisions to find out if they're all the same + $authorOfAll = false; + } else { + $authorOfAll = reset( $authors ); + foreach ( $authors as $author ) { + if ( $authorOfAll != $author ) { + $authorOfAll = false; + break; + } } } + # Fetch article text + $rev = Revision::newFromTitle( $this->mTitle ); if( !is_null( $rev ) ) { # if this is a mini-text, we can paste part of it into the deletion reason @@ -1750,7 +1918,7 @@ $reason = wfMsgForContent( 'exblank' ); } - if( $length < 500 && $reason === '' ) { + if( $reason === '' ) { # comment field=255, let's grep the first 150 to have some user # space left global $wgContLang; @@ -1760,10 +1928,10 @@ $text = preg_replace( "/[\n\r]/", '', $text ); if( !$blanked ) { - if( !$all_same_user ) { + if( $authorOfAll === false ) { $reason = wfMsgForContent( 'excontent', $text ); } else { - $reason = wfMsgForContent( 'excontentauthor', $text, $rev_name ); + $reason = wfMsgForContent( 'excontentauthor', $text, $authorOfAll ); } } else { $reason = wfMsgForContent( 'exbeforeblank', $text ); @@ -1775,6 +1943,52 @@ } /** + * Get the last N authors + * @param int $num Number of revisions to get + * @param string $revLatest The latest rev_id, selected from the master (optional) + * @return array Array of authors, duplicates not removed + */ + function getLastNAuthors( $num, $revLatest = 0 ) { + wfProfileIn( __METHOD__ ); + + // First try the slave + // If that doesn't have the latest revision, try the master + $continue = 2; + $db = wfGetDB( DB_SLAVE ); + do { + $res = $db->select( array( 'page', 'revision' ), + array( 'rev_id', 'rev_user_text' ), + array( + 'page_namespace' => $this->mTitle->getNamespace(), + 'page_title' => $this->mTitle->getDBkey(), + 'rev_page = page_id' + ), __METHOD__, $this->getSelectOptions( array( + 'ORDER BY' => 'rev_timestamp DESC', + 'LIMIT' => $num + ) ) + ); + if ( !$res ) { + wfProfileOut( __METHOD__ ); + return array(); + } + $row = $db->fetchObject( $res ); + if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { + $db = wfGetDB( DB_MASTER ); + $continue--; + } else { + $continue = 0; + } + } while ( $continue ); + + $authors = array( $row->rev_user_text ); + while ( $row = $db->fetchObject( $res ) ) { + $authors[] = $row->rev_user_text; + } + wfProfileOut( __METHOD__ ); + return $authors; + } + + /** * Output deletion confirmation dialog */ function confirmDelete( $par, $reason ) { @@ -1789,9 +2003,10 @@ $formaction = $this->mTitle->escapeLocalURL( 'action=delete' . $par ); - $confirm = htmlspecialchars( wfMsg( 'confirm' ) ); + $confirm = htmlspecialchars( wfMsg( 'deletepage' ) ); $delcom = htmlspecialchars( wfMsg( 'deletecomment' ) ); $token = htmlspecialchars( $wgUser->editToken() ); + $watch = Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '2' ) ); $wgOut->addHTML( "
    @@ -1801,13 +2016,17 @@ - +   + $watch + + +   - + @@ -1815,6 +2034,22 @@
    \n" ); $wgOut->returnToMain( false ); + + $this->showLogExtract( $wgOut ); + } + + + /** + * Show relevant lines from the deletion log + */ + function showLogExtract( $out ) { + $out->addHtml( '

    ' . htmlspecialchars( LogPage::logName( 'delete' ) ) . '

    ' ); + $logViewer = new LogViewer( + new LogReader( + new FauxRequest( + array( 'page' => $this->mTitle->getPrefixedText(), + 'type' => 'delete' ) ) ) ); + $logViewer->showList( $out ); } @@ -1822,13 +2057,12 @@ * Perform a deletion and output success or failure messages */ function doDelete( $reason ) { - global $wgOut, $wgUser, $wgContLang; - $fname = 'Article::doDelete'; - wfDebug( $fname."\n" ); + global $wgOut, $wgUser; + wfDebug( __METHOD__."\n" ); if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) { if ( $this->doDeleteArticle( $reason ) ) { - $deleted = $this->mTitle->getPrefixedText(); + $deleted = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); @@ -1840,7 +2074,7 @@ $wgOut->returnToMain( false ); wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason)); } else { - $wgOut->fatalError( wfMsg( 'cannotdelete' ) ); + $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); } } } @@ -1851,14 +2085,12 @@ * Returns success */ function doDeleteArticle( $reason ) { - global $wgUser; - global $wgUseSquid, $wgDeferredUpdateList, $wgInternalServer, $wgPostCommitUpdateList; + global $wgUseSquid, $wgDeferredUpdateList; global $wgUseTrackbacks; - $fname = 'Article::doDeleteArticle'; - wfDebug( $fname."\n" ); + wfDebug( __METHOD__."\n" ); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $ns = $this->mTitle->getNamespace(); $t = $this->mTitle->getDBkey(); $id = $this->mTitle->getArticleID(); @@ -1867,27 +2099,9 @@ return false; } - $u = new SiteStatsUpdate( 0, 1, -$this->isCountable( $this->getContent( true ) ), -1 ); + $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -1 ); array_push( $wgDeferredUpdateList, $u ); - $linksTo = $this->mTitle->getLinksTo(); - - # Squid purging - if ( $wgUseSquid ) { - $urls = array( - $this->mTitle->getInternalURL(), - $this->mTitle->getInternalURL( 'history' ) - ); - - $u = SquidUpdate::newFromTitles( $linksTo, $urls ); - array_push( $wgPostCommitUpdateList, $u ); - - } - - # Client and file cache invalidation - Title::touchArray( $linksTo ); - - // For now, shunt the revision data into the archive table. // Text is *not* removed from the text table; bulk storage // is left intact to avoid breaking block-compression or @@ -1909,31 +2123,49 @@ 'ar_minor_edit' => 'rev_minor_edit', 'ar_rev_id' => 'rev_id', 'ar_text_id' => 'rev_text_id', + 'ar_text' => '\'\'', // Be explicit to appease + 'ar_flags' => '\'\'', // MySQL's "strict mode"... + 'ar_len' => 'rev_len', + 'ar_page_id' => 'page_id', ), array( 'page_id' => $id, 'page_id = rev_page' - ), $fname + ), __METHOD__ ); + # Delete restrictions for it + $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ ); + # Now that it's safely backed up, delete it - $dbw->delete( 'revision', array( 'rev_page' => $id ), $fname ); - $dbw->delete( 'page', array( 'page_id' => $id ), $fname); + $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__); - if ($wgUseTrackbacks) - $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), $fname ); + # If using cascading deletes, we can skip some explicit deletes + if ( !$dbw->cascadingDeletes() ) { - # Clean up recentchanges entries... - $dbw->delete( 'recentchanges', array( 'rc_namespace' => $ns, 'rc_title' => $t ), $fname ); + $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); - # Finally, clean up the link tables - $t = $this->mTitle->getPrefixedDBkey(); + if ($wgUseTrackbacks) + $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ ); - Article::onArticleDelete( $this->mTitle ); + # Delete outgoing links + $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) ); + $dbw->delete( 'imagelinks', array( 'il_from' => $id ) ); + $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) ); + $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) ); + $dbw->delete( 'externallinks', array( 'el_from' => $id ) ); + $dbw->delete( 'langlinks', array( 'll_from' => $id ) ); + $dbw->delete( 'redirect', array( 'rd_from' => $id ) ); + } - # Delete outgoing links - $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) ); - $dbw->delete( 'imagelinks', array( 'il_from' => $id ) ); - $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) ); + # If using cleanup triggers, we can skip some manual deletes + if ( !$dbw->cleanupTriggers() ) { + + # Clean up recentchanges entries... + $dbw->delete( 'recentchanges', array( 'rc_namespace' => $ns, 'rc_title' => $t ), __METHOD__ ); + } + + # Clear caches + Article::onArticleDelete( $this->mTitle ); # Log the deletion $log = new LogPage( 'delete' ); @@ -1946,108 +2178,178 @@ } /** - * Revert a modification - */ - function rollback() { - global $wgUser, $wgOut, $wgRequest; - $fname = 'Article::rollback'; - - if ( ! $wgUser->isAllowed('rollback') ) { - $wgOut->sysopRequired(); - return; + * Roll back the most recent consecutive set of edits to a page + * from the same user; fails if there are no eligible edits to + * roll back to, e.g. user is the sole contributor + * + * @param string $fromP - Name of the user whose edits to rollback. + * @param string $summary - Custom summary. Set to default summary if empty. + * @param string $token - Rollback token. + * @param bool $bot - If true, mark all reverted edits as bot. + * + * @param array $resultDetails contains result-specific dict of additional values + * ALREADY_ROLLED : 'current' (rev) + * SUCCESS : 'summary' (str), 'current' (rev), 'target' (rev) + * + * @return self::SUCCESS on succes, self::* on failure + */ + public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails ) { + global $wgUser, $wgUseRCPatrol; + $resultDetails = null; + + if( $wgUser->isAllowed( 'rollback' ) ) { + if( $wgUser->isBlocked() ) { + return self::BLOCKED; + } + } else { + return self::PERM_DENIED; } + if ( wfReadOnly() ) { - $wgOut->readOnlyPage( $this->getContent( true ) ); - return; - } - if( !$wgUser->matchEditToken( $wgRequest->getVal( 'token' ), - array( $this->mTitle->getPrefixedText(), - $wgRequest->getVal( 'from' ) ) ) ) { - $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); - $wgOut->addWikiText( wfMsg( 'sessionfailure' ) ); - return; + return self::READONLY; } - $dbw =& wfGetDB( DB_MASTER ); + if( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) + return self::BAD_TOKEN; - # Enhanced rollback, marks edits rc_bot=1 - $bot = $wgRequest->getBool( 'bot' ); + $dbw = wfGetDB( DB_MASTER ); - # Replace all this user's current edits with the next one down - $tt = $this->mTitle->getDBKey(); - $n = $this->mTitle->getNamespace(); - - # Get the last editor, lock table exclusively - $dbw->begin(); + # Get the last editor $current = Revision::newFromTitle( $this->mTitle ); if( is_null( $current ) ) { # Something wrong... no page? - $dbw->rollback(); - $wgOut->addHTML( wfMsg( 'notanarticle' ) ); - return; + return self::BAD_TITLE; } - $from = str_replace( '_', ' ', $wgRequest->getVal( 'from' ) ); + $from = str_replace( '_', ' ', $fromP ); if( $from != $current->getUserText() ) { - $wgOut->setPageTitle(wfmsg('rollbackfailed')); - $wgOut->addWikiText( wfMsg( 'alreadyrolled', - htmlspecialchars( $this->mTitle->getPrefixedText()), - htmlspecialchars( $from ), - htmlspecialchars( $current->getUserText() ) ) ); - if( $current->getComment() != '') { - $wgOut->addHTML( - wfMsg( 'editcomment', - htmlspecialchars( $current->getComment() ) ) ); - } - return; + $resultDetails = array( 'current' => $current ); + return self::ALREADY_ROLLED; } # Get the last edit not by this guy - $user = IntVal( $current->getUser() ); + $user = intval( $current->getUser() ); $user_text = $dbw->addQuotes( $current->getUserText() ); $s = $dbw->selectRow( 'revision', array( 'rev_id', 'rev_timestamp' ), array( 'rev_page' => $current->getPage(), "rev_user <> {$user} OR rev_user_text <> {$user_text}" - ), $fname, + ), __METHOD__, array( 'USE INDEX' => 'page_timestamp', 'ORDER BY' => 'rev_timestamp DESC' ) ); if( $s === false ) { # Something wrong - $dbw->rollback(); - $wgOut->setPageTitle(wfMsg('rollbackfailed')); - $wgOut->addHTML( wfMsg( 'cantrollback' ) ); - return; + return self::ONLY_AUTHOR; } - + + $set = array(); if ( $bot ) { # Mark all reverted edits as bot - $dbw->update( 'recentchanges', - array( /* SET */ - 'rc_bot' => 1 - ), array( /* WHERE */ - 'rc_cur_id' => $current->getPage(), - 'rc_user_text' => $current->getUserText(), - "rc_timestamp > '{$s->rev_timestamp}'", - ), $fname - ); + $set['rc_bot'] = 1; + } + if ( $wgUseRCPatrol ) { + # Mark all reverted edits as patrolled + $set['rc_patrolled'] = 1; + } + + if ( $set ) { + $dbw->update( 'recentchanges', $set, + array( /* WHERE */ + 'rc_cur_id' => $current->getPage(), + 'rc_user_text' => $current->getUserText(), + "rc_timestamp > '{$s->rev_timestamp}'", + ), __METHOD__ + ); } - # Save it! + # Get the edit summary $target = Revision::newFromId( $s->rev_id ); - $newcomment = wfMsgForContent( 'revertpage', $target->getUserText(), $from ); + if( empty( $summary ) ) + $summary = wfMsgForContent( 'revertpage', $target->getUserText(), $from ); - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $wgOut->addHTML( '

    ' . htmlspecialchars( $newcomment ) . "

    \n
    \n" ); + # Save + $flags = EDIT_UPDATE | EDIT_MINOR; + if( $bot ) + $flags |= EDIT_FORCE_BOT; + $this->doEdit( $target->getText(), $summary, $flags ); + + $resultDetails = array( + 'summary' => $summary, + 'current' => $current, + 'target' => $target, + ); + return self::SUCCESS; + } - $this->updateArticle( $target->getText(), $newcomment, 1, $this->mTitle->userIsWatching(), $bot ); - Article::onArticleEdit( $this->mTitle ); + /** + * User interface for rollback operations + */ + function rollback() { + global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol; + + $details = null; + $result = $this->doRollback( + $wgRequest->getVal( 'from' ), + $wgRequest->getText( 'summary' ), + $wgRequest->getVal( 'token' ), + $wgRequest->getBool( 'bot' ), + $details + ); + + switch( $result ) { + case self::BLOCKED: + $wgOut->blockedPage(); + break; + case self::PERM_DENIED: + $wgOut->permissionRequired( 'rollback' ); + break; + case self::READONLY: + $wgOut->readOnlyPage( $this->getContent() ); + break; + case self::BAD_TOKEN: + $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); + $wgOut->addWikiText( wfMsg( 'sessionfailure' ) ); + break; + case self::BAD_TITLE: + $wgOut->addHtml( wfMsg( 'notanarticle' ) ); + break; + case self::ALREADY_ROLLED: + $current = $details['current']; + $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); + $wgOut->addWikiText( + wfMsg( 'alreadyrolled', + htmlspecialchars( $this->mTitle->getPrefixedText() ), + htmlspecialchars( $wgRequest->getVal( 'from' ) ), + htmlspecialchars( $current->getUserText() ) + ) + ); + if( $current->getComment() != '' ) { + $wgOut->addHtml( wfMsg( 'editcomment', + $wgUser->getSkin()->formatComment( $current->getComment() ) ) ); + } + break; + case self::ONLY_AUTHOR: + $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); + $wgOut->addHtml( wfMsg( 'cantrollback' ) ); + break; + case self::SUCCESS: + $current = $details['current']; + $target = $details['target']; + $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() ) + . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() ); + $new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() ) + . $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() ); + $wgOut->addHtml( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) ); + $wgOut->returnToMain( false, $this->mTitle ); + break; + default: + throw new MWException( __METHOD__ . ": Unknown return value `{$result}`" ); + } - $dbw->commit(); - $wgOut->returnToMain( false ); } @@ -2056,7 +2358,7 @@ * @private */ function viewUpdates() { - global $wgDeferredUpdateList, $wgUseEnotif; + global $wgDeferredUpdateList; if ( 0 != $this->getID() ) { global $wgDisableCounters; @@ -2067,120 +2369,162 @@ } } - # Update newtalk status if user is reading their own - # talk page - - if (!wfRunHooks('UserClearNewTalkNotification', array(&$this))) - return; - + # Update newtalk / watchlist notification status global $wgUser; - if (wfRunHooks('ArticleEditUpdateNewTalk', array(&$this)) ) { - if ($this->mTitle->getNamespace() == NS_USER_TALK && - $this->mTitle->getText() == $wgUser->getName()) - { - - if ( $wgUseEnotif ) { - require_once( 'UserTalkUpdate.php' ); - $u = new UserTalkUpdate( 0, $this->mTitle->getNamespace(), - $this->mTitle->getDBkey(), false, false, false ); - } else { - $wgUser->setNewtalk(0); - $wgUser->saveNewtalk(); - } - } elseif ( $wgUseEnotif ) { - $wgUser->clearNotification( $this->mTitle ); - } - } + $wgUser->clearNotification( $this->mTitle ); } /** * Do standard deferred updates after page edit. - * Every 1000th edit, prune the recent changes table. + * Update links tables, site stats, search index and message cache. + * Every 100th edit, prune the recent changes table. + * * @private - * @param string $text - */ - function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange) { - global $wgDeferredUpdateList, $wgDBname, $wgMemc; - global $wgMessageCache, $wgUser, $wgUseEnotif; - - wfSeedRandom(); - if ( 0 == mt_rand( 0, 999 ) ) { - # Periodically flush old entries from the recentchanges table. - global $wgRCMaxAge; - $dbw =& wfGetDB( DB_MASTER ); - $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); - $recentchanges = $dbw->tableName( 'recentchanges' ); - $sql = "DELETE FROM $recentchanges WHERE rc_timestamp < '{$cutoff}'"; - //$dbw->query( $sql ); // HACK: disabled for now, slowness - - // re-enabled for commit of unrelated live changes -- TS - $dbw->query( $sql ); + * @param $text New text of the article + * @param $summary Edit summary + * @param $minoredit Minor edit + * @param $timestamp_of_pagechange Timestamp associated with the page change + * @param $newid rev_id value of the new revision + * @param $changed Whether or not the content actually changed + */ + function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) { + global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser; + + wfProfileIn( __METHOD__ ); + + # Parse the text + $options = new ParserOptions; + $options->setTidy(true); + $poutput = $wgParser->parse( $text, $this->mTitle, $options, true, true, $newid ); + + # Save it to the parser cache + $parserCache =& ParserCache::singleton(); + $parserCache->save( $poutput, $this, $wgUser ); + + # Update the links tables + $u = new LinksUpdate( $this->mTitle, $poutput ); + $u->doUpdate(); + + if( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { + if ( 0 == mt_rand( 0, 99 ) ) { + // Flush old entries from the `recentchanges` table; we do this on + // random requests so as to avoid an increase in writes for no good reason + global $wgRCMaxAge; + $dbw = wfGetDB( DB_MASTER ); + $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); + $recentchanges = $dbw->tableName( 'recentchanges' ); + $sql = "DELETE FROM $recentchanges WHERE rc_timestamp < '{$cutoff}'"; + $dbw->query( $sql ); + } } + $id = $this->getID(); $title = $this->mTitle->getPrefixedDBkey(); $shortTitle = $this->mTitle->getDBkey(); - if ( 0 != $id ) { - $u = new LinksUpdate( $id, $title ); - array_push( $wgDeferredUpdateList, $u ); - $u = new SiteStatsUpdate( 0, 1, $this->mGoodAdjustment, $this->mTotalAdjustment ); - array_push( $wgDeferredUpdateList, $u ); - $u = new SearchUpdate( $id, $title, $text ); - array_push( $wgDeferredUpdateList, $u ); - - # If this is another user's talk page, update newtalk - - if ($this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getName()) { - if ( $wgUseEnotif ) { - require_once( 'UserTalkUpdate.php' ); - $u = new UserTalkUpdate( 1, $this->mTitle->getNamespace(), $shortTitle, $summary, - $minoredit, $timestamp_of_pagechange); - } else { - $other = User::newFromName( $shortTitle ); - if( is_null( $other ) && User::isIP( $shortTitle ) ) { - // An anonymous user - $other = new User(); - $other->setName( $shortTitle ); - } - if( $other ) { - $other->setNewtalk(1); - $other->saveNewtalk(); - } + if ( 0 == $id ) { + wfProfileOut( __METHOD__ ); + return; + } + + $u = new SiteStatsUpdate( 0, 1, $this->mGoodAdjustment, $this->mTotalAdjustment ); + array_push( $wgDeferredUpdateList, $u ); + $u = new SearchUpdate( $id, $title, $text ); + array_push( $wgDeferredUpdateList, $u ); + + # If this is another user's talk page, update newtalk + # Don't do this if $changed = false otherwise some idiot can null-edit a + # load of user talk pages and piss people off, nor if it's a minor edit + # by a properly-flagged bot. + if( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed + && !($minoredit && $wgUser->isAllowed('nominornewtalk') ) ) { + if (wfRunHooks('ArticleEditUpdateNewTalk', array(&$this)) ) { + $other = User::newFromName( $shortTitle ); + if( is_null( $other ) && User::isIP( $shortTitle ) ) { + // An anonymous user + $other = new User(); + $other->setName( $shortTitle ); + } + if( $other ) { + $other->setNewtalk( true ); } } + } - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - $wgMessageCache->replace( $shortTitle, $text ); - } + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $wgMessageCache->replace( $shortTitle, $text ); } + + wfProfileOut( __METHOD__ ); + } + + /** + * Perform article updates on a special page creation. + * + * @param Revision $rev + * + * @todo This is a shitty interface function. Kill it and replace the + * other shitty functions like editUpdates and such so it's not needed + * anymore. + */ + function createUpdates( $rev ) { + $this->mGoodAdjustment = $this->isCountable( $rev->getText() ); + $this->mTotalAdjustment = 1; + $this->editUpdates( $rev->getText(), $rev->getComment(), + $rev->isMinor(), wfTimestamp(), $rev->getId(), true ); } /** * Generate the navigation links when browsing through an article revisions * It shows the information as: - * Revision as of ; view current revision - * <- Previous version | Next Version -> + * Revision as of \; view current revision + * \<- Previous version | Next Version -\> * - * @access private + * @private * @param string $oldid Revision ID of this article revision */ function setOldSubtitle( $oldid=0 ) { global $wgLang, $wgOut, $wgUser; + if ( !wfRunHooks( 'DisplayOldSubtitle', array(&$this, &$oldid) ) ) { + return; + } + + $revision = Revision::newFromId( $oldid ); + $current = ( $oldid == $this->mLatest ); $td = $wgLang->timeanddate( $this->mTimestamp, true ); $sk = $wgUser->getSkin(); $lnk = $current ? wfMsg( 'currentrevisionlink' ) : $lnk = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) ); + $curdiff = $current + ? wfMsg( 'diff' ) + : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=cur&oldid='.$oldid ); $prev = $this->mTitle->getPreviousRevisionID( $oldid ) ; $prevlink = $prev ? $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'previousrevision' ), 'direction=prev&oldid='.$oldid ) : wfMsg( 'previousrevision' ); + $prevdiff = $prev + ? $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=prev&oldid='.$oldid ) + : wfMsg( 'diff' ); $nextlink = $current ? wfMsg( 'nextrevision' ) : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'nextrevision' ), 'direction=next&oldid='.$oldid ); - $r = wfMsg( 'revisionasofwithlink', $td, $lnk, $prevlink, $nextlink ); + $nextdiff = $current + ? wfMsg( 'diff' ) + : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=next&oldid='.$oldid ); + + $userlinks = $sk->userLink( $revision->getUser(), $revision->getUserText() ) + . $sk->userToolLinks( $revision->getUser(), $revision->getUserText() ); + + $m = wfMsg( 'revision-info-current' ); + $infomsg = $current && !wfEmptyMsg( 'revision-info-current', $m ) && $m != '-' + ? 'revision-info-current' + : 'revision-info'; + + $r = "\n\t\t\t\t
    " . wfMsg( $infomsg, $td, $userlinks ) . "
    \n" . + "\n\t\t\t\t
    " . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "
    \n\t\t\t"; $wgOut->setSubtitle( $r ); } @@ -2205,24 +2549,23 @@ function tryFileCache() { static $called = false; if( $called ) { - wfDebug( " tryFileCache() -- called twice!?\n" ); + wfDebug( "Article::tryFileCache(): called twice!?\n" ); return; } $called = true; if($this->isFileCacheable()) { $touched = $this->mTouched; - $cache = new CacheManager( $this->mTitle ); + $cache = new HTMLFileCache( $this->mTitle ); if($cache->isFileCacheGood( $touched )) { - global $wgOut; - wfDebug( " tryFileCache() - about to load\n" ); + wfDebug( "Article::tryFileCache(): about to load file\n" ); $cache->loadFromFileCache(); return true; } else { - wfDebug( " tryFileCache() - starting buffer\n" ); + wfDebug( "Article::tryFileCache(): starting buffer\n" ); ob_start( array(&$cache, 'saveToFileCache' ) ); } } else { - wfDebug( " tryFileCache() - not cacheable\n" ); + wfDebug( "Article::tryFileCache(): not cacheable\n" ); } } @@ -2231,40 +2574,75 @@ * @return bool */ function isFileCacheable() { - global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest; - extract( $wgRequest->getValues( 'action', 'oldid', 'diff', 'redirect', 'printable' ) ); + global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest, $wgLang, $wgContLang; + $action = $wgRequest->getVal( 'action' ); + $oldid = $wgRequest->getVal( 'oldid' ); + $diff = $wgRequest->getVal( 'diff' ); + $redirect = $wgRequest->getVal( 'redirect' ); + $printable = $wgRequest->getVal( 'printable' ); + $page = $wgRequest->getVal( 'page' ); + + //check for non-standard user language; this covers uselang, + //and extensions for auto-detecting user language. + $ulang = $wgLang->getCode(); + $clang = $wgContLang->getCode(); + + $cacheable = $wgUseFileCache + && (!$wgShowIPinHeader) + && ($this->getID() != 0) + && ($wgUser->isAnon()) + && (!$wgUser->getNewtalk()) + && ($this->mTitle->getNamespace() != NS_SPECIAL ) + && (empty( $action ) || $action == 'view') + && (!isset($oldid)) + && (!isset($diff)) + && (!isset($redirect)) + && (!isset($printable)) + && !isset($page) + && (!$this->mRedirectedFrom) + && ($ulang === $clang); + + if ( $cacheable ) { + //extension may have reason to disable file caching on some pages. + $cacheable = wfRunHooks( 'IsFileCacheable', array( $this ) ); + } - return $wgUseFileCache - and (!$wgShowIPinHeader) - and ($this->getID() != 0) - and ($wgUser->isAnon()) - and (!$wgUser->getNewtalk()) - and ($this->mTitle->getNamespace() != NS_SPECIAL ) - and (empty( $action ) || $action == 'view') - and (!isset($oldid)) - and (!isset($diff)) - and (!isset($redirect)) - and (!isset($printable)) - and (!$this->mRedirectedFrom); + return $cacheable; } /** - * Loads cur_touched and returns a value indicating if it should be used + * Loads page_touched and returns a value indicating if it should be used * */ function checkTouched() { - $fname = 'Article::checkTouched'; if( !$this->mDataLoaded ) { - $dbr =& $this->getDB(); - $data = $this->pageDataFromId( $dbr, $this->getId() ); - if( $data ) { - $this->loadPageData( $data ); - } + $this->loadPageData(); } return !$this->mIsRedirect; } /** + * Get the page_touched field + */ + function getTouched() { + # Ensure that page data has been loaded + if( !$this->mDataLoaded ) { + $this->loadPageData(); + } + return $this->mTouched; + } + + /** + * Get the page_latest field + */ + function getLatest() { + if ( !$this->mDataLoaded ) { + $this->loadPageData(); + } + return $this->mLatest; + } + + /** * Edit an article without doing all that other stuff * The article must already exist; link tables etc * are not updated, caches are not flushed. @@ -2274,10 +2652,9 @@ * @param bool $minor whereas it's a minor modification */ function quickEdit( $text, $comment = '', $minor = 0 ) { - $fname = 'Article::quickEdit'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->begin(); $revision = new Revision( array( 'page' => $this->getId(), @@ -2285,11 +2662,11 @@ 'comment' => $comment, 'minor_edit' => $minor ? 1 : 0, ) ); - $revisionId = $revision->insertOn( $dbw ); + $revision->insertOn( $dbw ); $this->updateRevisionOn( $dbw, $revision ); $dbw->commit(); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /** @@ -2300,14 +2677,14 @@ */ function incViewCount( $id ) { $id = intval( $id ); - global $wgHitcounterUpdateFreq; + global $wgHitcounterUpdateFreq, $wgDBtype; - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $pageTable = $dbw->tableName( 'page' ); $hitcounterTable = $dbw->tableName( 'hitcounter' ); $acchitsTable = $dbw->tableName( 'acchits' ); - if( $wgHitcounterUpdateFreq <= 1 ){ // + if( $wgHitcounterUpdateFreq <= 1 ) { $dbw->query( "UPDATE $pageTable SET page_counter = page_counter + 1 WHERE page_id = $id" ); return; } @@ -2331,14 +2708,22 @@ wfProfileIn( 'Article::incViewCount-collect' ); $old_user_abort = ignore_user_abort( true ); - $dbw->query("LOCK TABLES $hitcounterTable WRITE"); - $dbw->query("CREATE TEMPORARY TABLE $acchitsTable TYPE=HEAP ". + if ($wgDBtype == 'mysql') + $dbw->query("LOCK TABLES $hitcounterTable WRITE"); + $tabletype = $wgDBtype == 'mysql' ? "ENGINE=HEAP " : ''; + $dbw->query("CREATE TEMPORARY TABLE $acchitsTable $tabletype AS ". "SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable ". 'GROUP BY hc_id'); $dbw->query("DELETE FROM $hitcounterTable"); - $dbw->query('UNLOCK TABLES'); - $dbw->query("UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n ". - 'WHERE page_id = hc_id'); + if ($wgDBtype == 'mysql') { + $dbw->query('UNLOCK TABLES'); + $dbw->query("UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n ". + 'WHERE page_id = hc_id'); + } + else { + $dbw->query("UPDATE $pageTable SET page_counter=page_counter + hc_n ". + "FROM $acchitsTable WHERE page_id = hc_id"); + } $dbw->query("DROP TABLE $acchitsTable"); ignore_user_abort( $old_user_abort ); @@ -2359,36 +2744,55 @@ * @param $title_obj a title object */ - function onArticleCreate($title_obj) { - global $wgUseSquid, $wgPostCommitUpdateList; - - $title_obj->touchLinks(); - $titles = $title_obj->getLinksTo(); - - # Purge squid - if ( $wgUseSquid ) { - $urls = $title_obj->getSquidURLs(); - foreach ( $titles as $linkTitle ) { - $urls[] = $linkTitle->getInternalURL(); - } - $u = new SquidUpdate( $urls ); - array_push( $wgPostCommitUpdateList, $u ); + static function onArticleCreate($title) { + # The talk page isn't in the regular link tables, so we need to update manually: + if ( $title->isTalkPage() ) { + $other = $title->getSubjectPage(); + } else { + $other = $title->getTalkPage(); } + $other->invalidateCache(); + $other->purgeSquid(); + + $title->touchLinks(); + $title->purgeSquid(); } - function onArticleDelete( $title ) { - global $wgMessageCache; - + static function onArticleDelete( $title ) { + global $wgUseFileCache, $wgMessageCache; + $title->touchLinks(); - + $title->purgeSquid(); + + # File cache + if ( $wgUseFileCache ) { + $cm = new HTMLFileCache( $title ); + @unlink( $cm->fileCacheName() ); + } + if( $title->getNamespace() == NS_MEDIAWIKI) { $wgMessageCache->replace( $title->getDBkey(), false ); } } - function onArticleEdit($title_obj) { - // This would be an appropriate place to purge caches. - // Why's this not in here now? + /** + * Purge caches on page update etc + */ + static function onArticleEdit( $title ) { + global $wgDeferredUpdateList, $wgUseFileCache; + + // Invalidate caches of articles which include this page + $update = new HTMLCacheUpdate( $title, 'templatelinks' ); + $wgDeferredUpdateList[] = $update; + + # Purge squid for this page only + $title->purgeSquid(); + + # Clear file cache + if ( $wgUseFileCache ) { + $cm = new HTMLFileCache( $title ); + @unlink( $cm->fileCacheName() ); + } } /**#@-*/ @@ -2397,28 +2801,37 @@ * Info about this page * Called for ?action=info when $wgAllowPageInfo is on. * - * @access public + * @public */ function info() { - global $wgLang, $wgOut, $wgAllowPageInfo; - $fname = 'Article::info'; + global $wgLang, $wgOut, $wgAllowPageInfo, $wgUser; if ( !$wgAllowPageInfo ) { - $wgOut->errorpage( 'nosuchaction', 'nosuchactiontext' ); + $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); return; } $page = $this->mTitle->getSubjectPage(); $wgOut->setPagetitle( $page->getPrefixedText() ); - $wgOut->setSubtitle( wfMsg( 'infosubtitle' )); + $wgOut->setPageTitleActionText( wfMsg( 'info_short' ) ); + $wgOut->setSubtitle( wfMsg( 'infosubtitle' ) ); - # first, see if the page exists at all. - $exists = $page->getArticleId() != 0; - if( !$exists ) { - $wgOut->addHTML( wfMsg('noarticletext') ); + if( !$this->mTitle->exists() ) { + $wgOut->addHtml( '
    ' ); + if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + // This doesn't quite make sense; the user is asking for + // information about the _page_, not the message... -- RC + $wgOut->addHtml( htmlspecialchars( wfMsgWeirdKey( $this->mTitle->getText() ) ) ); + } else { + $msg = $wgUser->isLoggedIn() + ? 'noarticletext' + : 'noarticletextanon'; + $wgOut->addHtml( wfMsgExt( $msg, 'parse' ) ); + } + $wgOut->addHtml( '
    ' ); } else { - $dbr =& $this->getDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $wl_clause = array( 'wl_title' => $page->getDBkey(), 'wl_namespace' => $page->getNamespace() ); @@ -2426,7 +2839,7 @@ 'watchlist', 'COUNT(*)', $wl_clause, - $fname, + __METHOD__, $this->getSelectOptions() ); $pageInfo = $this->pageCountInfo( $page ); @@ -2452,7 +2865,7 @@ * * @param Title $title * @return array - * @access private + * @private */ function pageCountInfo( $title ) { $id = $title->getArticleId(); @@ -2460,23 +2873,22 @@ return false; } - $dbr =& $this->getDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $rev_clause = array( 'rev_page' => $id ); - $fname = 'Article::pageCountInfo'; $edits = $dbr->selectField( 'revision', 'COUNT(rev_page)', $rev_clause, - $fname, + __METHOD__, $this->getSelectOptions() ); $authors = $dbr->selectField( 'revision', 'COUNT(DISTINCT rev_user_text)', $rev_clause, - $fname, + __METHOD__, $this->getSelectOptions() ); return array( 'edits' => $edits, 'authors' => $authors ); @@ -2484,31 +2896,185 @@ /** * Return a list of templates used by this article. - * Uses the links table to find the templates + * Uses the templatelinks table * - * @return array + * @return array Array of Title objects */ function getUsedTemplates() { $result = array(); $id = $this->mTitle->getArticleID(); + if( $id == 0 ) { + return array(); + } - $db =& wfGetDB( DB_SLAVE ); - $res = $db->select( array( 'pagelinks' ), - array( 'pl_title' ), - array( - 'pl_from' => $id, - 'pl_namespace' => NS_TEMPLATE ), + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'templatelinks' ), + array( 'tl_namespace', 'tl_title' ), + array( 'tl_from' => $id ), 'Article:getUsedTemplates' ); if ( false !== $res ) { - if ( $db->numRows( $res ) ) { - while ( $row = $db->fetchObject( $res ) ) { - $result[] = $row->pl_title; + if ( $dbr->numRows( $res ) ) { + while ( $row = $dbr->fetchObject( $res ) ) { + $result[] = Title::makeTitle( $row->tl_namespace, $row->tl_title ); } } } - $db->freeResult( $res ); + $dbr->freeResult( $res ); return $result; } -} -?> + /** + * Return an auto-generated summary if the text provided is a redirect. + * + * @param string $text The wikitext to check + * @return string '' or an appropriate summary + */ + public static function getRedirectAutosummary( $text ) { + $rt = Title::newFromRedirect( $text ); + if( is_object( $rt ) ) + return wfMsgForContent( 'autoredircomment', $rt->getFullText() ); + else + return ''; + } + + /** + * Return an auto-generated summary if the new text is much shorter than + * the old text. + * + * @param string $oldtext The previous text of the page + * @param string $text The submitted text of the page + * @return string An appropriate autosummary, or an empty string. + */ + public static function getBlankingAutosummary( $oldtext, $text ) { + if ($oldtext!='' && $text=='') { + return wfMsgForContent('autosumm-blank'); + } elseif (strlen($oldtext) > 10 * strlen($text) && strlen($text) < 500) { + #Removing more than 90% of the article + global $wgContLang; + $truncatedtext = $wgContLang->truncate($text, max(0, 200 - strlen(wfMsgForContent('autosumm-replace'))), '...'); + return wfMsgForContent('autosumm-replace', $truncatedtext); + } else { + return ''; + } + } + + /** + * Return an applicable autosummary if one exists for the given edit. + * @param string $oldtext The previous text of the page. + * @param string $newtext The submitted text of the page. + * @param bitmask $flags A bitmask of flags submitted for the edit. + * @return string An appropriate autosummary, or an empty string. + */ + public static function getAutosummary( $oldtext, $newtext, $flags ) { + + # This code is UGLY UGLY UGLY. + # Somebody PLEASE come up with a more elegant way to do it. + + #Redirect autosummaries + $summary = self::getRedirectAutosummary( $newtext ); + + if ($summary) + return $summary; + + #Blanking autosummaries + if (!($flags & EDIT_NEW)) + $summary = self::getBlankingAutosummary( $oldtext, $newtext ); + + if ($summary) + return $summary; + + #New page autosummaries + if ($flags & EDIT_NEW && strlen($newtext)) { + #If they're making a new article, give its text, truncated, in the summary. + global $wgContLang; + $truncatedtext = $wgContLang->truncate( + str_replace("\n", ' ', $newtext), + max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new') ) ), + '...' ); + $summary = wfMsgForContent( 'autosumm-new', $truncatedtext ); + } + + if ($summary) + return $summary; + + return $summary; + } + + /** + * Add the primary page-view wikitext to the output buffer + * Saves the text into the parser cache if possible. + * Updates templatelinks if it is out of date. + * + * @param string $text + * @param bool $cache + */ + public function outputWikiText( $text, $cache = true ) { + global $wgParser, $wgUser, $wgOut; + + $popts = $wgOut->parserOptions(); + $popts->setTidy(true); + $parserOutput = $wgParser->parse( $text, $this->mTitle, + $popts, true, true, $this->getRevIdFetched() ); + $popts->setTidy(false); + if ( $cache && $this && $parserOutput->getCacheTime() != -1 ) { + $parserCache =& ParserCache::singleton(); + $parserCache->save( $parserOutput, $this, $wgUser ); + } + + if ( !wfReadOnly() && $this->mTitle->areRestrictionsCascading() ) { + // templatelinks table may have become out of sync, + // especially if using variable-based transclusions. + // For paranoia, check if things have changed and if + // so apply updates to the database. This will ensure + // that cascaded protections apply as soon as the changes + // are visible. + + # Get templates from templatelinks + $id = $this->mTitle->getArticleID(); + + $tlTemplates = array(); + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'templatelinks' ), + array( 'tl_namespace', 'tl_title' ), + array( 'tl_from' => $id ), + 'Article:getUsedTemplates' ); + + global $wgContLang; + + if ( false !== $res ) { + if ( $dbr->numRows( $res ) ) { + while ( $row = $dbr->fetchObject( $res ) ) { + $tlTemplates[] = $wgContLang->getNsText( $row->tl_namespace ) . ':' . $row->tl_title ; + } + } + } + + # Get templates from parser output. + $poTemplates_allns = $parserOutput->getTemplates(); + + $poTemplates = array (); + foreach ( $poTemplates_allns as $ns_templates ) { + $poTemplates = array_merge( $poTemplates, $ns_templates ); + } + + # Get the diff + $templates_diff = array_diff( $poTemplates, $tlTemplates ); + + if ( count( $templates_diff ) > 0 ) { + # Whee, link updates time. + $u = new LinksUpdate( $this->mTitle, $parserOutput ); + + $dbw = wfGetDb( DB_MASTER ); + $dbw->begin(); + + $u->doUpdate(); + + $dbw->commit(); + } + } + + $wgOut->addParserOutput( $parserOutput ); + } + +} diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/AuthPlugin.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/AuthPlugin.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/AuthPlugin.php 2005-07-24 05:48:14.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/AuthPlugin.php 2007-08-04 12:36:25.000000000 -0400 @@ -1,23 +1,22 @@ # http://www.mediawiki.org/ -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** @@ -33,7 +32,6 @@ * This interface is new, and might change a bit before 1.4.0 final is * done... * - * @package MediaWiki */ class AuthPlugin { /** @@ -42,36 +40,36 @@ * you might need to munge it (for instance, for lowercase initial * letters). * - * @param string $username + * @param $username String: username. * @return bool - * @access public + * @public */ function userExists( $username ) { # Override this! return false; } - + /** * Check if a username+password pair is a valid login. * The name will be normalized to MediaWiki's requirements, so * you might need to munge it (for instance, for lowercase initial * letters). * - * @param string $username - * @param string $password + * @param $username String: username. + * @param $password String: user password. * @return bool - * @access public + * @public */ function authenticate( $username, $password ) { # Override this! return false; } - + /** * Modify options in the login template. * - * @param UserLoginTemplate $template - * @access public + * @param $template UserLoginTemplate object. + * @public */ function modifyUITemplate( &$template ) { # Override this! @@ -81,8 +79,8 @@ /** * Set the domain this plugin is supposed to use when authenticating. * - * @param string $domain - * @access public + * @param $domain String: authentication domain. + * @public */ function setDomain( $domain ) { $this->domain = $domain; @@ -91,9 +89,9 @@ /** * Check to see if the specific domain is a valid domain. * - * @param string $domain + * @param $domain String: authentication domain. * @return bool - * @access public + * @public */ function validDomain( $domain ) { # Override this! @@ -109,7 +107,7 @@ * forget the & on your function declaration. * * @param User $user - * @access public + * @public */ function updateUser( &$user ) { # Override this and do something @@ -129,21 +127,35 @@ * This is just a question, and shouldn't perform any actions. * * @return bool - * @access public + * @public */ function autoCreate() { return false; } - + + /** + * Can users change their passwords? + * + * @return bool + */ + function allowPasswordChange() { + return true; + } + /** * Set the given password in the authentication database. + * As a special case, the password may be set to null to request + * locking the password to an unusable value, with the expectation + * that it will be set later through a mail reset or other method. + * * Return true if successful. * - * @param string $password + * @param $user User object. + * @param $password String: password. * @return bool - * @access public + * @public */ - function setPassword( $password ) { + function setPassword( $user, $password ) { return true; } @@ -151,9 +163,9 @@ * Update user information in the external authentication database. * Return true if successful. * - * @param User $user + * @param $user User object. * @return bool - * @access public + * @public */ function updateExternalDB( $user ) { return true; @@ -163,7 +175,7 @@ * Check to see if external accounts can be created. * Return true if external accounts can be created. * @return bool - * @access public + * @public */ function canCreateAccounts() { return false; @@ -173,12 +185,14 @@ * Add a user to the external authentication database. * Return true if successful. * - * @param User $user + * @param User $user - only the name should be assumed valid at this point * @param string $password + * @param string $email + * @param string $realname * @return bool - * @access public + * @public */ - function addUser( $user, $password ) { + function addUser( $user, $password, $email='', $realname='' ) { return true; } @@ -190,12 +204,12 @@ * This is just a question, and shouldn't perform any actions. * * @return bool - * @access public + * @public */ function strict() { return false; } - + /** * When creating a user account, optionally fill in preferences and such. * For instance, you might pull the email address or real name from the @@ -204,13 +218,14 @@ * The User object is passed by reference so it can be modified; don't * forget the & on your function declaration. * - * @param User $user - * @access public + * @param $user User object. + * @param $autocreate bool True if user is being autocreated on login + * @public */ - function initUser( &$user ) { + function initUser( $user, $autocreate=false ) { # Override this to do something. } - + /** * If you want to munge the case of an account name before the final * check, now is your chance. @@ -220,4 +235,4 @@ } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/BagOStuff.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/BagOStuff.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/BagOStuff.php 2005-07-22 07:29:14.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/BagOStuff.php 2007-07-23 13:19:56.000000000 -0400 @@ -2,26 +2,25 @@ # # Copyright (C) 2003-2004 Brion Vibber # http://www.mediawiki.org/ -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** * - * @package MediaWiki */ - + /** * Simple generic object store * @@ -29,26 +28,26 @@ * the PHP memcached client. * * backends for local hash array and SQL table included: - * $bag = new HashBagOStuff(); - * $bag = new MysqlBagOStuff($tablename); # connect to db first + * + * $bag = new HashBagOStuff(); + * $bag = new MysqlBagOStuff($tablename); # connect to db first + * * - * @package MediaWiki - * @abstract */ class BagOStuff { var $debugmode; - - function BagOStuff() { + + function __construct() { $this->set_debug( false ); } - + function set_debug($bool) { $this->debugmode = $bool; } - + /* *** THE GUTS OF THE OPERATION *** */ /* Override these with functional things in subclasses */ - + function get($key) { /* stub */ return false; @@ -58,12 +57,12 @@ /* stub */ return false; } - + function delete($key, $time=0) { /* stub */ return false; } - + function lock($key, $timeout = 0) { /* stub */ return true; @@ -73,7 +72,7 @@ /* stub */ return true; } - + /* *** Emulated functions *** */ /* Better performance can likely be got with custom written versions */ function get_multi($keys) { @@ -82,19 +81,19 @@ $out[$key] = $this->get($key); return $out; } - + function set_multi($hash, $exptime=0) { foreach($hash as $key => $value) $this->set($key, $value, $exptime); } - + function add($key, $value, $exptime=0) { if( $this->get($key) == false ) { $this->set($key, $value, $exptime); return true; } } - + function add_multi($hash, $exptime=0) { foreach($hash as $key => $value) $this->add($key, $value, $exptime); @@ -104,12 +103,12 @@ foreach($keys as $key) $this->delete($key, $time); } - + function replace($key, $value, $exptime=0) { if( $this->get($key) !== false ) $this->set($key, $value, $exptime); } - + function incr($key, $value=1) { if ( !$this->lock($key) ) { return false; @@ -125,7 +124,7 @@ $this->unlock($key); return $n; } - + function decr($key, $value=1) { if ( !$this->lock($key) ) { return false; @@ -142,18 +141,28 @@ $this->unlock($key); return $m; } - + function _debug($text) { if($this->debugmode) wfDebug("BagOStuff debug: $text\n"); } + + /** + * Convert an optionally relative time to an absolute time + */ + static function convertExpiry( $exptime ) { + if(($exptime != 0) && ($exptime < 3600*24*30)) { + return time() + $exptime; + } else { + return $exptime; + } + } } /** * Functional versions! * @todo document - * @package MediaWiki */ class HashBagOStuff extends BagOStuff { /* @@ -162,11 +171,11 @@ persist between program runs. */ var $bag; - - function HashBagOStuff() { + + function __construct() { $this->bag = array(); } - + function _expire($key) { $et = $this->bag[$key][1]; if(($et == 0) || ($et > time())) @@ -174,7 +183,7 @@ $this->delete($key); return true; } - + function get($key) { if(!$this->bag[$key]) return false; @@ -182,13 +191,11 @@ return false; return $this->bag[$key][0]; } - + function set($key,$value,$exptime=0) { - if(($exptime != 0) && ($exptime < 3600*24*30)) - $exptime = time() + $exptime; - $this->bag[$key] = array( $value, $exptime ); + $this->bag[$key] = array( $value, BagOStuff::convertExpiry( $exptime ) ); } - + function delete($key,$time=0) { if(!$this->bag[$key]) return false; @@ -210,20 +217,19 @@ /** * @todo document * @abstract - * @package MediaWiki */ -class SqlBagOStuff extends BagOStuff { +abstract class SqlBagOStuff extends BagOStuff { var $table; var $lastexpireall = 0; - function SqlBagOStuff($tablename = 'objectcache') { + function __construct($tablename = 'objectcache') { $this->table = $tablename; } - + function get($key) { /* expire old entries if any */ $this->garbageCollect(); - + $res = $this->_query( "SELECT value,exptime FROM $0 WHERE keyname='$1'", $key); if(!$res) { @@ -232,40 +238,55 @@ } if($row=$this->_fetchobject($res)) { $this->_debug("get: retrieved data; exp time is " . $row->exptime); - return $this->_unserialize($row->value); + if ( $row->exptime != $this->_maxdatetime() && + wfTimestamp( TS_UNIX, $row->exptime ) < time() ) + { + $this->_debug("get: key has expired, deleting"); + $this->delete($key); + return false; + } + return $this->_unserialize($this->_blobdecode($row->value)); } else { $this->_debug('get: no matching rows'); } return false; } - + function set($key,$value,$exptime=0) { + if ( wfReadOnly() ) { + return false; + } $exptime = intval($exptime); if($exptime < 0) $exptime = 0; if($exptime == 0) { $exp = $this->_maxdatetime(); } else { - if($exptime < 3600*24*30) + if($exptime < 3.16e8) # ~10 years $exptime += time(); $exp = $this->_fromunixtime($exptime); } $this->delete( $key ); - $this->_query( - "INSERT INTO $0 (keyname,value,exptime) VALUES('$1','$2','$exp')", - $key, $this->_serialize($value)); + $this->_doinsert($this->getTableName(), array( + 'keyname' => $key, + 'value' => $this->_blobencode($this->_serialize($value)), + 'exptime' => $exp + )); return true; /* ? */ } - + function delete($key,$time=0) { + if ( wfReadOnly() ) { + return false; + } $this->_query( "DELETE FROM $0 WHERE keyname='$1'", $key ); return true; /* ? */ } - + function getTableName() { return $this->table; } - + function _query($sql) { $reps = func_get_args(); $reps[0] = $this->getTableName(); @@ -273,7 +294,7 @@ for($i=0;$i_strencode($reps[$i]), + $i > 0 ? $this->_strencode($reps[$i]) : $reps[$i], $sql); } $res = $this->_doquery($sql); @@ -282,38 +303,34 @@ } return $res; } - + function _strencode($str) { /* Protect strings in SQL */ return str_replace( "'", "''", $str ); } - - function _doquery($sql) { - die( 'abstract function SqlBagOStuff::_doquery() must be defined' ); + function _blobencode($str) { + return $str; } - - function _fetchrow($res) { - die( 'abstract function SqlBagOStuff::_fetchrow() must be defined' ); + function _blobdecode($str) { + return $str; } - + + abstract function _doinsert($table, $vals); + abstract function _doquery($sql); + function _freeresult($result) { /* stub */ return false; } - + function _dberror($result) { /* stub */ return 'unknown error'; } - - function _maxdatetime() { - die( 'abstract function SqlBagOStuff::_maxdatetime() must be defined' ); - } - - function _fromunixtime() { - die( 'abstract function SqlBagOStuff::_fromunixtime() must be defined' ); - } - + + abstract function _maxdatetime(); + abstract function _fromunixtime($ts); + function garbageCollect() { /* Ignore 99% of requests */ if ( !mt_rand( 0, 100 ) ) { @@ -325,18 +342,24 @@ } } } - + function expireall() { /* Remove any items that have expired */ + if ( wfReadOnly() ) { + return false; + } $now = $this->_fromunixtime( time() ); - $this->_query( "DELETE FROM $0 WHERE exptime<'$now'" ); + $this->_query( "DELETE FROM $0 WHERE exptime < '$now'" ); } - + function deleteall() { /* Clear *all* items from cache table */ + if ( wfReadOnly() ) { + return false; + } $this->_query( "DELETE FROM $0" ); } - + /** * Serialize an object and, if possible, compress the representation. * On typical message and page data, this can provide a 3X decrease @@ -353,7 +376,7 @@ return $serial; } } - + /** * Unserialize and, if necessary, decompress an object. * @param string $serial @@ -366,46 +389,68 @@ $serial = $decomp; } } - return unserialize( $serial ); + $ret = unserialize( $serial ); + return $ret; } } /** * @todo document - * @package MediaWiki */ class MediaWikiBagOStuff extends SqlBagOStuff { var $tableInitialised = false; function _doquery($sql) { - $dbw =& wfGetDB( DB_MASTER ); - return $dbw->query($sql, 'MediaWikiBagOStuff:_doquery'); + $dbw = wfGetDB( DB_MASTER ); + return $dbw->query($sql, 'MediaWikiBagOStuff::_doquery'); + } + function _doinsert($t, $v) { + $dbw = wfGetDB( DB_MASTER ); + return $dbw->insert($t, $v, 'MediaWikiBagOStuff::_doinsert', + array( 'IGNORE' ) ); } function _fetchobject($result) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->fetchObject($result); } function _freeresult($result) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->freeResult($result); } function _dberror($result) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->lastError(); } function _maxdatetime() { - return '9999-12-31 12:59:59'; + if ( time() > 0x7fffffff ) { + return $this->_fromunixtime( 1<<62 ); + } else { + return $this->_fromunixtime( 0x7fffffff ); + } } function _fromunixtime($ts) { - return gmdate( 'Y-m-d H:i:s', $ts ); + $dbw = wfGetDB(DB_MASTER); + return $dbw->timestamp($ts); } function _strencode($s) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); return $dbw->strencode($s); } + function _blobencode($s) { + $dbw = wfGetDB( DB_MASTER ); + return $dbw->encodeBlob($s); + } + function _blobdecode($s) { + $dbw = wfGetDB( DB_MASTER ); + return $dbw->decodeBlob($s); + } function getTableName() { if ( !$this->tableInitialised ) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); + /* This is actually a hack, we should be able + to use Language classes here... or not */ + if (!$dbw) + throw new MWException("Could not connect to database"); $this->table = $dbw->tableName( $this->table ); $this->tableInitialised = true; } @@ -414,19 +459,18 @@ } /** - * This is a wrapper for Turck MMCache's shared memory functions. - * - * You can store objects with mmcache_put() and mmcache_get(), but Turck seems - * to use a weird custom serializer that randomly segfaults. So we wrap calls + * This is a wrapper for Turck MMCache's shared memory functions. + * + * You can store objects with mmcache_put() and mmcache_get(), but Turck seems + * to use a weird custom serializer that randomly segfaults. So we wrap calls * with serialize()/unserialize(). - * + * * The thing I noticed about the Turck serialized data was that unlike ordinary - * serialize(), it contained the names of methods, and judging by the amount of - * binary data, perhaps even the bytecode of the methods themselves. It may be - * that Turck's serializer is faster, so a possible future extension would be + * serialize(), it contained the names of methods, and judging by the amount of + * binary data, perhaps even the bytecode of the methods themselves. It may be + * that Turck's serializer is faster, so a possible future extension would be * to use it for arrays but not for objects. * - * @package MediaWiki */ class TurckBagOStuff extends BagOStuff { function get($key) { @@ -441,7 +485,7 @@ mmcache_put( $key, serialize( $value ), $exptime ); return true; } - + function delete($key, $time=0) { mmcache_rm( $key ); return true; @@ -456,15 +500,39 @@ mmcache_unlock( $key ); return true; } -} +} + +/** + * This is a wrapper for APC's shared memory functions + * + */ +class APCBagOStuff extends BagOStuff { + function get($key) { + $val = apc_fetch($key); + if ( is_string( $val ) ) { + $val = unserialize( $val ); + } + return $val; + } + + function set($key, $value, $exptime=0) { + apc_store($key, serialize($value), $exptime); + return true; + } + + function delete($key, $time=0) { + apc_delete($key); + return true; + } +} + /** - * This is a wrapper for eAccelerator's shared memory functions. - * + * This is a wrapper for eAccelerator's shared memory functions. + * * This is basically identical to the Turck MMCache version, * mostly because eAccelerator is based on Turck MMCache. * - * @package MediaWiki */ class eAccelBagOStuff extends BagOStuff { function get($key) { @@ -479,7 +547,7 @@ eaccelerator_put( $key, serialize( $value ), $exptime ); return true; } - + function delete($key, $time=0) { eaccelerator_rm( $key ); return true; @@ -494,5 +562,187 @@ eaccelerator_unlock( $key ); return true; } -} -?> +} + +/** + * Wrapper for XCache object caching functions; identical interface + * to the APC wrapper + */ +class XCacheBagOStuff extends BagOStuff { + + /** + * Get a value from the XCache object cache + * + * @param string $key Cache key + * @return mixed + */ + public function get( $key ) { + $val = xcache_get( $key ); + if( is_string( $val ) ) + $val = unserialize( $val ); + return $val; + } + + /** + * Store a value in the XCache object cache + * + * @param string $key Cache key + * @param mixed $value Object to store + * @param int $expire Expiration time + * @return bool + */ + public function set( $key, $value, $expire = 0 ) { + xcache_set( $key, serialize( $value ), $expire ); + return true; + } + + /** + * Remove a value from the XCache object cache + * + * @param string $key Cache key + * @param int $time Not used in this implementation + * @return bool + */ + public function delete( $key, $time = 0 ) { + xcache_unset( $key ); + return true; + } + +} + +/** + * @todo document + */ +class DBABagOStuff extends BagOStuff { + var $mHandler, $mFile, $mReader, $mWriter, $mDisabled; + + function __construct( $handler = 'db3', $dir = false ) { + if ( $dir === false ) { + global $wgTmpDirectory; + $dir = $wgTmpDirectory; + } + $this->mFile = "$dir/mw-cache-" . wfWikiID(); + $this->mFile .= '.db'; + $this->mHandler = $handler; + } + + /** + * Encode value and expiry for storage + */ + function encode( $value, $expiry ) { + # Convert to absolute time + $expiry = BagOStuff::convertExpiry( $expiry ); + return sprintf( '%010u', intval( $expiry ) ) . ' ' . serialize( $value ); + } + + /** + * @return list containing value first and expiry second + */ + function decode( $blob ) { + if ( !is_string( $blob ) ) { + return array( null, 0 ); + } else { + return array( + unserialize( substr( $blob, 11 ) ), + intval( substr( $blob, 0, 10 ) ) + ); + } + } + + function getReader() { + if ( file_exists( $this->mFile ) ) { + $handle = dba_open( $this->mFile, 'rl', $this->mHandler ); + } else { + $handle = $this->getWriter(); + } + if ( !$handle ) { + wfDebug( "Unable to open DBA cache file {$this->mFile}\n" ); + } + return $handle; + } + + function getWriter() { + $handle = dba_open( $this->mFile, 'cl', $this->mHandler ); + if ( !$handle ) { + wfDebug( "Unable to open DBA cache file {$this->mFile}\n" ); + } + return $handle; + } + + function get( $key ) { + wfProfileIn( __METHOD__ ); + wfDebug( __METHOD__."($key)\n" ); + $handle = $this->getReader(); + if ( !$handle ) { + return null; + } + $val = dba_fetch( $key, $handle ); + list( $val, $expiry ) = $this->decode( $val ); + # Must close ASAP because locks are held + dba_close( $handle ); + + if ( !is_null( $val ) && $expiry && $expiry < time() ) { + # Key is expired, delete it + $handle = $this->getWriter(); + dba_delete( $key, $handle ); + dba_close( $handle ); + wfDebug( __METHOD__.": $key expired\n" ); + $val = null; + } + wfProfileOut( __METHOD__ ); + return $val; + } + + function set( $key, $value, $exptime=0 ) { + wfProfileIn( __METHOD__ ); + wfDebug( __METHOD__."($key)\n" ); + $blob = $this->encode( $value, $exptime ); + $handle = $this->getWriter(); + if ( !$handle ) { + return false; + } + $ret = dba_replace( $key, $blob, $handle ); + dba_close( $handle ); + wfProfileOut( __METHOD__ ); + return $ret; + } + + function delete( $key, $time = 0 ) { + wfProfileIn( __METHOD__ ); + wfDebug( __METHOD__."($key)\n" ); + $handle = $this->getWriter(); + if ( !$handle ) { + return false; + } + $ret = dba_delete( $key, $handle ); + dba_close( $handle ); + wfProfileOut( __METHOD__ ); + return $ret; + } + + function add( $key, $value, $exptime = 0 ) { + wfProfileIn( __METHOD__ ); + $blob = $this->encode( $value, $exptime ); + $handle = $this->getWriter(); + if ( !$handle ) { + return false; + } + $ret = dba_insert( $key, $blob, $handle ); + # Insert failed, check to see if it failed due to an expired key + if ( !$ret ) { + list( $value, $expiry ) = $this->decode( dba_fetch( $key, $handle ) ); + if ( $expiry < time() ) { + # Yes expired, delete and try again + dba_delete( $key, $handle ); + $ret = dba_insert( $key, $blob, $handle ); + # This time if it failed then it will be handled by the caller like any other race + } + } + + dba_close( $handle ); + wfProfileOut( __METHOD__ ); + return $ret; + } +} + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Block.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Block.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Block.php 2006-02-05 18:13:34.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Block.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,112 +1,190 @@ mId = 0; + # Expand valid IPv6 addresses + $address = IP::sanitizeIP( $address ); $this->mAddress = $address; $this->mUser = $user; $this->mBy = $by; $this->mReason = $reason; $this->mTimestamp = wfTimestamp(TS_MW,$timestamp); $this->mAuto = $auto; - if( empty( $expiry ) ) { - $this->mExpiry = $expiry; - } else { - $this->mExpiry = wfTimestamp( TS_MW, $expiry ); - } - + $this->mAnonOnly = $anonOnly; + $this->mCreateAccount = $createAccount; + $this->mExpiry = self::decodeExpiry( $expiry ); + $this->mEnableAutoblock = $enableAutoblock; + $this->mHideName = $hideName; + $this->mBlockEmail = $blockEmail; $this->mForUpdate = false; + $this->mFromMaster = false; + $this->mByName = false; $this->initialiseRange(); } - - /*static*/ function newFromDB( $address, $user = 0, $killExpired = true ) + + static function newFromDB( $address, $user = 0, $killExpired = true ) { - $ban = new Block(); - $ban->load( $address, $user, $killExpired ); - return $ban; + $block = new Block(); + $block->load( $address, $user, $killExpired ); + if ( $block->isValid() ) { + return $block; + } else { + return null; + } } - - function clear() + + static function newFromID( $id ) + { + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->resultObject( $dbr->select( 'ipblocks', '*', + array( 'ipb_id' => $id ), __METHOD__ ) ); + $block = new Block; + if ( $block->loadFromResult( $res ) ) { + return $block; + } else { + return null; + } + } + + function clear() { - $mAddress = $mReason = $mTimestamp = ''; - $mUser = $mBy = 0; + $this->mAddress = $this->mReason = $this->mTimestamp = ''; + $this->mId = $this->mAnonOnly = $this->mCreateAccount = + $this->mEnableAutoblock = $this->mAuto = $this->mUser = + $this->mBy = $this->mHideName = $this->mBlockEmail = 0; + $this->mByName = false; + } + + /** + * Get the DB object and set the reference parameter to the query options + */ + function &getDBOptions( &$options ) + { + global $wgAntiLockFlags; + if ( $this->mForUpdate || $this->mFromMaster ) { + $db = wfGetDB( DB_MASTER ); + if ( !$this->mForUpdate || ($wgAntiLockFlags & ALF_NO_BLOCK_LOCK) ) { + $options = array(); + } else { + $options = array( 'FOR UPDATE' ); + } + } else { + $db = wfGetDB( DB_SLAVE ); + $options = array(); + } + return $db; } /** * Get a ban from the DB, with either the given address or the given username + * + * @param string $address The IP address of the user, or blank to skip IP blocks + * @param integer $user The user ID, or zero for anonymous users + * @param bool $killExpired Whether to delete expired rows while loading + * */ - function load( $address = '', $user = 0, $killExpired = true ) + function load( $address = '', $user = 0, $killExpired = true ) { - global $wgDBmysql4, $wgAntiLockFlags; - $fname = 'Block::load'; wfDebug( "Block::load: '$address', '$user', $killExpired\n" ); - $ret = false; - $killed = false; - if ( $this->forUpdate() ) { - $db =& wfGetDB( DB_MASTER ); - if ( $wgAntiLockFlags & ALF_NO_BLOCK_LOCK ) { - $options = ''; + $options = array(); + $db =& $this->getDBOptions( $options ); + + if ( 0 == $user && $address == '' ) { + # Invalid user specification, not blocked + $this->clear(); + return false; + } + + # Try user block + if ( $user ) { + $res = $db->resultObject( $db->select( 'ipblocks', '*', array( 'ipb_user' => $user ), + __METHOD__, $options ) ); + if ( $this->loadFromResult( $res, $killExpired ) ) { + return true; + } + } + + # Try IP block + # TODO: improve performance by merging this query with the autoblock one + # Slightly tricky while handling killExpired as well + if ( $address ) { + $conds = array( 'ipb_address' => $address, 'ipb_auto' => 0 ); + $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); + if ( $this->loadFromResult( $res, $killExpired ) ) { + if ( $user && $this->mAnonOnly ) { + # Block is marked anon-only + # Whitelist this IP address against autoblocks and range blocks + $this->clear(); + return false; + } else { + return true; + } + } + } + + # Try range block + if ( $this->loadRange( $address, $killExpired, $user ) ) { + if ( $user && $this->mAnonOnly ) { + $this->clear(); + return false; } else { - $options = 'FOR UPDATE'; + return true; } - } else { - $db =& wfGetDB( DB_SLAVE ); - $options = ''; } - $ipblocks = $db->tableName( 'ipblocks' ); - if ( 0 == $user && $address=='' ) { - $sql = "SELECT * from $ipblocks $options"; - } elseif ($address=="") { - $sql = "SELECT * FROM $ipblocks WHERE ipb_user={$user} $options"; - } elseif ($user=="") { - $sql = "SELECT * FROM $ipblocks WHERE ipb_address='" . $db->strencode( $address ) . "' $options"; - } elseif ( $options=='' && $wgDBmysql4 ) { - # If there are no optiones (e.g. FOR UPDATE), use a UNION - # so that the query can make efficient use of indices - $sql = "SELECT * FROM $ipblocks WHERE ipb_address='" . $db->strencode( $address ) . - "' UNION SELECT * FROM $ipblocks WHERE ipb_user={$user}"; - } else { - # If there are options, a UNION can not be used, use one - # SELECT instead. Will do a full table scan. - $sql = "SELECT * FROM $ipblocks WHERE (ipb_address='" . $db->strencode( $address ) . - "' OR ipb_user={$user}) $options"; - } - - $res = $db->query( $sql, $fname ); - if ( 0 == $db->numRows( $res ) ) { - # User is not blocked - $this->clear(); - } else { + # Try autoblock + if ( $address ) { + $conds = array( 'ipb_address' => $address, 'ipb_auto' => 1 ); + if ( $user ) { + $conds['ipb_anon_only'] = 0; + } + $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); + if ( $this->loadFromResult( $res, $killExpired ) ) { + return true; + } + } + + # Give up + $this->clear(); + return false; + } + + /** + * Fill in member variables from a result wrapper + */ + function loadFromResult( ResultWrapper $res, $killExpired = true ) + { + $ret = false; + if ( 0 != $res->numRows() ) { # Get first block - $row = $db->fetchObject( $res ); + $row = $res->fetchObject(); $this->initFromRow( $row ); if ( $killExpired ) { @@ -114,29 +192,67 @@ do { $killed = $this->deleteIfExpired(); if ( $killed ) { - $row = $db->fetchObject( $res ); + $row = $res->fetchObject(); if ( $row ) { $this->initFromRow( $row ); } } } while ( $killed && $row ); - + # If there were any left after the killing finished, return true - if ( !$row ) { - $ret = false; - $this->clear(); - } else { + if ( $row ) { $ret = true; } } else { $ret = true; } } - $db->freeResult( $res ); + $res->free(); return $ret; } - - function initFromRow( $row ) + + /** + * Search the database for any range blocks matching the given address, and + * load the row if one is found. + */ + function loadRange( $address, $killExpired = true, $user = 0 ) + { + $iaddr = IP::toHex( $address ); + if ( $iaddr === false ) { + # Invalid address + return false; + } + + # Only scan ranges which start in this /16, this improves search speed + # Blocks should not cross a /16 boundary. + $range = substr( $iaddr, 0, 4 ); + + $options = array(); + $db =& $this->getDBOptions( $options ); + $conds = array( + "ipb_range_start LIKE '$range%'", + "ipb_range_start <= '$iaddr'", + "ipb_range_end >= '$iaddr'" + ); + + if ( $user ) { + $conds['ipb_anon_only'] = 0; + } + + $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); + $success = $this->loadFromResult( $res, $killExpired ); + return $success; + } + + /** + * Determine if a given integer IPv4 address is in a given CIDR network + * @deprecated Use IP::isInRange + */ + function isAddressInRange( $addr, $range ) { + return IP::isInRange( $addr, $range ); + } + + function initFromRow( $row ) { $this->mAddress = $row->ipb_address; $this->mReason = $row->ipb_reason; @@ -144,41 +260,43 @@ $this->mUser = $row->ipb_user; $this->mBy = $row->ipb_by; $this->mAuto = $row->ipb_auto; + $this->mAnonOnly = $row->ipb_anon_only; + $this->mCreateAccount = $row->ipb_create_account; + $this->mEnableAutoblock = $row->ipb_enable_autoblock; + $this->mBlockEmail = $row->ipb_block_email; + $this->mHideName = $row->ipb_deleted; $this->mId = $row->ipb_id; - $this->mExpiry = $row->ipb_expiry ? - wfTimestamp(TS_MW,$row->ipb_expiry) : - $row->ipb_expiry; - - $this->initialiseRange(); - } + $this->mExpiry = self::decodeExpiry( $row->ipb_expiry ); + if ( isset( $row->user_name ) ) { + $this->mByName = $row->user_name; + } else { + $this->mByName = false; + } + $this->mRangeStart = $row->ipb_range_start; + $this->mRangeEnd = $row->ipb_range_end; + } function initialiseRange() { + $this->mRangeStart = ''; + $this->mRangeEnd = ''; + if ( $this->mUser == 0 ) { - $rangeParts = explode( '/', $this->mAddress ); - if ( count( $rangeParts ) == 2 ) { - $this->mNetworkBits = $rangeParts[1]; - } else { - $this->mNetworkBits = 32; - } - $this->mIntegerAddr = ip2long( $rangeParts[0] ); - } else { - $this->mNetworkBits = false; - $this->mIntegerAddr = false; + list( $this->mRangeStart, $this->mRangeEnd ) = IP::parseRange( $this->mAddress ); } } - + /** * Callback with a Block object for every block * @return integer number of blocks; */ - /*static*/ function enumBlocks( $callback, $tag, $flags = 0 ) + /*static*/ function enumBlocks( $callback, $tag, $flags = 0 ) { global $wgAntiLockFlags; $block = new Block(); - if ( $flags & EB_FOR_UPDATE ) { - $db =& wfGetDB( DB_MASTER ); + if ( $flags & Block::EB_FOR_UPDATE ) { + $db = wfGetDB( DB_MASTER ); if ( $wgAntiLockFlags & ALF_NO_BLOCK_LOCK ) { $options = ''; } else { @@ -186,81 +304,240 @@ } $block->forUpdate( true ); } else { - $db =& wfGetDB( DB_SLAVE ); + $db = wfGetDB( DB_SLAVE ); $options = ''; - } - $ipblocks = $db->tableName( 'ipblocks' ); - - $sql = "SELECT * FROM $ipblocks ORDER BY ipb_timestamp DESC $options"; - $res = $db->query( $sql, 'Block::enumBans' ); + } + if ( $flags & Block::EB_RANGE_ONLY ) { + $cond = " AND ipb_range_start <> ''"; + } else { + $cond = ''; + } + + $now = wfTimestampNow(); + + list( $ipblocks, $user ) = $db->tableNamesN( 'ipblocks', 'user' ); + + $sql = "SELECT $ipblocks.*,user_name FROM $ipblocks,$user " . + "WHERE user_id=ipb_by $cond ORDER BY ipb_timestamp DESC $options"; + $res = $db->query( $sql, 'Block::enumBlocks' ); $num_rows = $db->numRows( $res ); while ( $row = $db->fetchObject( $res ) ) { $block->initFromRow( $row ); - if ( !( $flags & EB_KEEP_EXPIRED ) ) { - if ( !$block->deleteIfExpired() ) { - $callback( $block, $tag ); + if ( ( $flags & Block::EB_RANGE_ONLY ) && $block->mRangeStart == '' ) { + continue; + } + + if ( !( $flags & Block::EB_KEEP_EXPIRED ) ) { + if ( $block->mExpiry && $now > $block->mExpiry ) { + $block->delete(); + } else { + call_user_func( $callback, $block, $tag ); } } else { - $callback( $block, $tag ); + call_user_func( $callback, $block, $tag ); } } - wfFreeResult( $res ); + $db->freeResult( $res ); return $num_rows; } - function delete() + function delete() { - $fname = 'Block::delete'; if (wfReadOnly()) { - return; + return false; } - $dbw =& wfGetDB( DB_MASTER ); - - if ( $this->mAddress == '' ) { - $condition = array( 'ipb_id' => $this->mId ); - } else { - $condition = array( 'ipb_address' => $this->mAddress ); + if ( !$this->mId ) { + throw new MWException( "Block::delete() now requires that the mId member be filled\n" ); } - $dbw->delete( 'ipblocks', $condition, $fname ); - $this->clearCache(); + + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'ipblocks', array( 'ipb_id' => $this->mId ), __METHOD__ ); + return $dbw->affectedRows() > 0; } - function insert() + /** + * Insert a block into the block table. + *@return Whether or not the insertion was successful. + */ + function insert() { wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" ); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); + + # Unset ipb_anon_only for user blocks, makes no sense + if ( $this->mUser ) { + $this->mAnonOnly = 0; + } + + # Unset ipb_enable_autoblock for IP blocks, makes no sense + if ( !$this->mUser ) { + $this->mEnableAutoblock = 0; + $this->mBlockEmail = 0; //Same goes for email... + } + + # Don't collide with expired blocks + Block::purgeExpired(); + + $ipb_id = $dbw->nextSequenceValue('ipblocks_ipb_id_val'); $dbw->insert( 'ipblocks', array( + 'ipb_id' => $ipb_id, 'ipb_address' => $this->mAddress, 'ipb_user' => $this->mUser, 'ipb_by' => $this->mBy, 'ipb_reason' => $this->mReason, 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), 'ipb_auto' => $this->mAuto, - 'ipb_expiry' => $this->mExpiry ? - $dbw->timestamp($this->mExpiry) : - $this->mExpiry, - ), 'Block::insert' + 'ipb_anon_only' => $this->mAnonOnly, + 'ipb_create_account' => $this->mCreateAccount, + 'ipb_enable_autoblock' => $this->mEnableAutoblock, + 'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ), + 'ipb_range_start' => $this->mRangeStart, + 'ipb_range_end' => $this->mRangeEnd, + 'ipb_deleted' => $this->mHideName, + 'ipb_block_email' => $this->mBlockEmail + ), 'Block::insert', array( 'IGNORE' ) ); + $affected = $dbw->affectedRows(); + $dbw->commit(); + + if ($affected) + $this->doRetroactiveAutoblock(); - $this->clearCache(); + return $affected; } - function deleteIfExpired() + /** + * Retroactively autoblocks the last IP used by the user (if it is a user) + * blocked by this Block. + *@return Whether or not a retroactive autoblock was made. + */ + function doRetroactiveAutoblock() { + $dbr = wfGetDB( DB_SLAVE ); + #If autoblock is enabled, autoblock the LAST IP used + # - stolen shamelessly from CheckUser_body.php + + if ($this->mEnableAutoblock && $this->mUser) { + wfDebug("Doing retroactive autoblocks for " . $this->mAddress . "\n"); + + $row = $dbr->selectRow( 'recentchanges', array( 'rc_ip' ), array( 'rc_user_text' => $this->mAddress ), + __METHOD__ , array( 'ORDER BY' => 'rc_timestamp DESC' ) ); + + if ( !$row || !$row->rc_ip ) { + #No results, don't autoblock anything + wfDebug("No IP found to retroactively autoblock\n"); + } else { + #Limit is 1, so no loop needed. + $retroblockip = $row->rc_ip; + return $this->doAutoblock( $retroblockip, true ); + } + } + } + + /** + * Autoblocks the given IP, referring to this Block. + * @param string $autoblockip The IP to autoblock. + * @param bool $justInserted The main block was just inserted + * @return bool Whether or not an autoblock was inserted. + */ + function doAutoblock( $autoblockip, $justInserted = false ) { + # If autoblocks are disabled, go away. + if ( !$this->mEnableAutoblock ) { + return; + } + + # Check for presence on the autoblock whitelist + # TODO cache this? + $lines = explode( "\n", wfMsgForContentNoTrans( 'autoblock_whitelist' ) ); + + $ip = $autoblockip; + + wfDebug("Checking the autoblock whitelist..\n"); + + foreach( $lines as $line ) { + # List items only + if ( substr( $line, 0, 1 ) !== '*' ) { + continue; + } + + $wlEntry = substr($line, 1); + $wlEntry = trim($wlEntry); + + wfDebug("Checking $ip against $wlEntry..."); + + # Is the IP in this range? + if (IP::isInRange( $ip, $wlEntry )) { + wfDebug(" IP $ip matches $wlEntry, not autoblocking\n"); + #$autoblockip = null; # Don't autoblock a whitelisted IP. + return; #This /SHOULD/ introduce a dummy block - but + # I don't know a safe way to do so. -werdna + } else { + wfDebug( " No match\n" ); + } + } + + # It's okay to autoblock. Go ahead and create/insert the block. + + $ipblock = Block::newFromDB( $autoblockip ); + if ( $ipblock ) { + # If the user is already blocked. Then check if the autoblock would + # exceed the user block. If it would exceed, then do nothing, else + # prolong block time + if ($this->mExpiry && + ($this->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) { + return; + } + # Just update the timestamp + if ( !$justInserted ) { + $ipblock->updateTimestamp(); + } + return; + } else { + $ipblock = new Block; + } + + # Make a new block object with the desired properties + wfDebug( "Autoblocking {$this->mAddress}@" . $autoblockip . "\n" ); + $ipblock->mAddress = $autoblockip; + $ipblock->mUser = 0; + $ipblock->mBy = $this->mBy; + $ipblock->mReason = wfMsgForContent( 'autoblocker', $this->mAddress, $this->mReason ); + $ipblock->mTimestamp = wfTimestampNow(); + $ipblock->mAuto = 1; + $ipblock->mCreateAccount = $this->mCreateAccount; + # Continue suppressing the name if needed + $ipblock->mHideName = $this->mHideName; + + # If the user is already blocked with an expiry date, we don't + # want to pile on top of that! + if($this->mExpiry) { + $ipblock->mExpiry = min ( $this->mExpiry, Block::getAutoblockExpiry( $this->mTimestamp )); + } else { + $ipblock->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp ); + } + # Insert it + return $ipblock->insert(); + } + + function deleteIfExpired() { + $fname = 'Block::deleteIfExpired'; + wfProfileIn( $fname ); if ( $this->isExpired() ) { wfDebug( "Block::deleteIfExpired() -- deleting\n" ); $this->delete(); - return true; + $retVal = true; } else { wfDebug( "Block::deleteIfExpired() -- not expired\n" ); - return false; + $retVal = false; } + wfProfileOut( $fname ); + return $retVal; } - function isExpired() - { + function isExpired() + { wfDebug( "Block::isExpired() checking current " . wfTimestampNow() . " vs $this->mExpiry\n" ); if ( !$this->mExpiry ) { return false; @@ -269,71 +546,157 @@ } } - function isValid() + function isValid() { return $this->mAddress != ''; } - - function updateTimestamp() + + function updateTimestamp() { if ( $this->mAuto ) { $this->mTimestamp = wfTimestamp(); $this->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp ); - $dbw =& wfGetDB( DB_MASTER ); - $dbw->update( 'ipblocks', - array( /* SET */ + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'ipblocks', + array( /* SET */ 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), 'ipb_expiry' => $dbw->timestamp($this->mExpiry), ), array( /* WHERE */ 'ipb_address' => $this->mAddress - ), 'Block::updateTimestamp' + ), 'Block::updateTimestamp' ); - - $this->clearCache(); } } - /* private */ function clearCache() - { - global $wgBlockCache; - if ( is_object( $wgBlockCache ) ) { - $wgBlockCache->loadFromDB(); - } - } - + /* function getIntegerAddr() { return $this->mIntegerAddr; } - + function getNetworkBits() { return $this->mNetworkBits; + }*/ + + /** + * @return The blocker user ID. + */ + public function getBy() { + return $this->mBy; + } + + /** + * @return The blocker user name. + */ + function getByName() + { + if ( $this->mByName === false ) { + $this->mByName = User::whoIs( $this->mBy ); + } + return $this->mByName; } function forUpdate( $x = NULL ) { return wfSetVar( $this->mForUpdate, $x ); } - /* static */ function getAutoblockExpiry( $timestamp ) + function fromMaster( $x = NULL ) { + return wfSetVar( $this->mFromMaster, $x ); + } + + function getRedactedName() { + if ( $this->mAuto ) { + return '#' . $this->mId; + } else { + return $this->mAddress; + } + } + + /** + * Encode expiry for DB + */ + static function encodeExpiry( $expiry, $db ) { + if ( $expiry == '' || $expiry == Block::infinity() ) { + return Block::infinity(); + } else { + return $db->timestamp( $expiry ); + } + } + + /** + * Decode expiry which has come from the DB + */ + static function decodeExpiry( $expiry, $timestampType = TS_MW ) { + if ( $expiry == '' || $expiry == Block::infinity() ) { + return Block::infinity(); + } else { + return wfTimestamp( $timestampType, $expiry ); + } + } + + static function getAutoblockExpiry( $timestamp ) { global $wgAutoblockExpiry; return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry ); } - - /* static */ function normaliseRange( $range ) - { + + /** + * Gets rid of uneeded numbers in quad-dotted/octet IP strings + * For example, 127.111.113.151/24 -> 127.111.113.0/24 + */ + static function normaliseRange( $range ) { $parts = explode( '/', $range ); if ( count( $parts ) == 2 ) { - $shift = 32 - $parts[1]; - $ipint = ip2long( $parts[0] ); - $ipint = $ipint >> $shift << $shift; - $newip = long2ip( $ipint ); - $range = "$newip/{$parts[1]}"; + // IPv6 + if ( IP::isIPv6($range) && $parts[1] >= 64 && $parts[1] <= 128 ) { + $bits = $parts[1]; + $ipint = IP::toUnsigned6( $parts[0] ); + # Native 32 bit functions WONT work here!!! + # Convert to a padded binary number + $network = wfBaseConvert( $ipint, 10, 2, 128 ); + # Truncate the last (128-$bits) bits and replace them with zeros + $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT ); + # Convert back to an integer + $network = wfBaseConvert( $network, 2, 10 ); + # Reform octet address + $newip = IP::toOctet( $network ); + $range = "$newip/{$parts[1]}"; + } // IPv4 + else if ( IP::isIPv4($range) && $parts[1] >= 16 && $parts[1] <= 32 ) { + $shift = 32 - $parts[1]; + $ipint = IP::toUnsigned( $parts[0] ); + $ipint = $ipint >> $shift << $shift; + $newip = long2ip( $ipint ); + $range = "$newip/{$parts[1]}"; + } } return $range; } + /** + * Purge expired blocks from the ipblocks table + */ + static function purgeExpired() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'ipblocks', array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ ); + } + + static function infinity() { + # This is a special keyword for timestamps in PostgreSQL, and + # works with CHAR(14) as well because "i" sorts after all numbers. + return 'infinity'; + + /* + static $infinity; + if ( !isset( $infinity ) ) { + $dbr = wfGetDB( DB_SLAVE ); + $infinity = $dbr->bigTimestamp(); + } + return $infinity; + */ + } + } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/CategoryPage.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/CategoryPage.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/CategoryPage.php 2006-02-22 20:15:30.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/CategoryPage.php 2007-08-22 09:40:22.000000000 -0400 @@ -3,23 +3,23 @@ * Special handling for category description pages * Modelled after ImagePage.php * - * @package MediaWiki */ - -if( !defined( 'MEDIAWIKI' ) ) - die(); -global $wgCategoryMagicGallery; -if( $wgCategoryMagicGallery ) - /** */ - require_once('ImageGallery.php'); +if( !defined( 'MEDIAWIKI' ) ) + die( 1 ); /** - * @package MediaWiki */ class CategoryPage extends Article { - function view() { + global $wgRequest, $wgUser; + + $diff = $wgRequest->getVal( 'diff' ); + $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); + + if ( isset( $diff ) && $diffOnly ) + return Article::view(); + if(!wfRunHooks('CategoryPageView', array(&$this))) return; if ( NS_CATEGORY == $this->mTitle->getNamespace() ) { @@ -27,7 +27,7 @@ } Article::view(); - + # If the article we've just shown is in the "Image" namespace, # follow it with the history list and link list for the image # it describes. @@ -40,161 +40,262 @@ function openShowCategory() { # For overloading } - - # generate a list of subcategories and pages for a category - # depending on wfMsg("usenewcategorypage") it either calls the new - # or the old code. The new code will not work properly for some - # languages due to sorting issues, so they might want to turn it - # off. function closeShowCategory() { global $wgOut, $wgRequest; - $pageConditions = array(); $from = $wgRequest->getVal( 'from' ); $until = $wgRequest->getVal( 'until' ); - $wgOut->addHTML( $this->doCategoryMagic( $from, $until ) ); + + $viewer = new CategoryViewer( $this->mTitle, $from, $until ); + $wgOut->addHTML( $viewer->getHTML() ); } +} +class CategoryViewer { + var $title, $limit, $from, $until, + $articles, $articles_start_char, + $children, $children_start_char, + $showGallery, $gallery, + $skin; + + function __construct( $title, $from = '', $until = '' ) { + global $wgCategoryPagingLimit; + $this->title = $title; + $this->from = $from; + $this->until = $until; + $this->limit = $wgCategoryPagingLimit; + } + /** * Format the category data list. * * @param string $from -- return only sort keys from this item on * @param string $until -- don't return keys after this point. * @return string HTML output - * @access private + * @private */ - function doCategoryMagic( $from = '', $until = '' ) { - global $wgContLang,$wgUser, $wgCategoryMagicGallery, $wgCategoryPagingLimit; - $fname = 'CategoryPage::doCategoryMagic'; - wfProfileIn( $fname ); - - $articles = array(); - $articles_start_char = array(); - $children = array(); - $children_start_char = array(); - $data = array(); - if( $wgCategoryMagicGallery ) { - $ig = new ImageGallery(); - } - - $dbr =& wfGetDB( DB_SLAVE ); - if( $from != '' ) { - $pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $from ); - $flip = false; - } elseif( $until != '' ) { - $pageCondition = 'cl_sortkey < ' . $dbr->addQuotes( $until ); - $flip = true; + function getHTML() { + global $wgOut, $wgCategoryMagicGallery, $wgCategoryPagingLimit; + wfProfileIn( __METHOD__ ); + + $this->showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery; + + $this->clearCategoryState(); + $this->doCategoryQuery(); + $this->finaliseCategoryState(); + + $r = $this->getCategoryTop() . + $this->getSubcategorySection() . + $this->getPagesSection() . + $this->getImageSection() . + $this->getCategoryBottom(); + + // Give a proper message if category is empty + if ( $r == '' ) { + $r = wfMsgExt( 'category-empty', array( 'parse' ) ); + } + + wfProfileOut( __METHOD__ ); + return $r; + } + + function clearCategoryState() { + $this->articles = array(); + $this->articles_start_char = array(); + $this->children = array(); + $this->children_start_char = array(); + if( $this->showGallery ) { + $this->gallery = new ImageGallery(); + $this->gallery->setHideBadImages(); + } + } + + function getSkin() { + if ( !$this->skin ) { + global $wgUser; + $this->skin = $wgUser->getSkin(); + } + return $this->skin; + } + + /** + * Add a subcategory to the internal lists + */ + function addSubcategory( $title, $sortkey, $pageLength ) { + global $wgContLang; + // Subcategory; strip the 'Category' namespace from the link text. + $this->children[] = $this->getSkin()->makeKnownLinkObj( + $title, $wgContLang->convertHtml( $title->getText() ) ); + + $this->children_start_char[] = $this->getSubcategorySortChar( $title, $sortkey ); + } + + /** + * Get the character to be used for sorting subcategories. + * If there's a link from Category:A to Category:B, the sortkey of the resulting + * entry in the categorylinks table is Category:A, not A, which it SHOULD be. + * Workaround: If sortkey == "Category:".$title, than use $title for sorting, + * else use sortkey... + */ + function getSubcategorySortChar( $title, $sortkey ) { + global $wgContLang; + + if( $title->getPrefixedText() == $sortkey ) { + $firstChar = $wgContLang->firstChar( $title->getDBkey() ); } else { - $pageCondition = '1'; - $flip = false; + $firstChar = $wgContLang->firstChar( $sortkey ); + } + + return $wgContLang->convert( $firstChar ); + } + + /** + * Add a page in the image namespace + */ + function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) { + if ( $this->showGallery ) { + $image = new Image( $title ); + if( $this->flip ) { + $this->gallery->insert( $image ); + } else { + $this->gallery->add( $image ); + } + } else { + $this->addPage( $title, $sortkey, $pageLength, $isRedirect ); + } + } + + /** + * Add a miscellaneous page + */ + function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) { + global $wgContLang; + $this->articles[] = $isRedirect + ? '' . $this->getSkin()->makeKnownLinkObj( $title ) . '' + : $this->getSkin()->makeSizeLinkObj( $pageLength, $title ); + $this->articles_start_char[] = $wgContLang->convert( $wgContLang->firstChar( $sortkey ) ); + } + + function finaliseCategoryState() { + if( $this->flip ) { + $this->children = array_reverse( $this->children ); + $this->children_start_char = array_reverse( $this->children_start_char ); + $this->articles = array_reverse( $this->articles ); + $this->articles_start_char = array_reverse( $this->articles_start_char ); + } + } + + function doCategoryQuery() { + $dbr = wfGetDB( DB_SLAVE ); + if( $this->from != '' ) { + $pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $this->from ); + $this->flip = false; + } elseif( $this->until != '' ) { + $pageCondition = 'cl_sortkey < ' . $dbr->addQuotes( $this->until ); + $this->flip = true; + } else { + $pageCondition = '1 = 1'; + $this->flip = false; } - $limit = $wgCategoryPagingLimit; $res = $dbr->select( array( 'page', 'categorylinks' ), - array( 'page_title', 'page_namespace', 'page_len', 'cl_sortkey' ), + array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey' ), array( $pageCondition, 'cl_from = page_id', - 'cl_to' => $this->mTitle->getDBKey()), + 'cl_to' => $this->title->getDBKey()), #'page_is_redirect' => 0), #+ $pageCondition, - $fname, - array( 'ORDER BY' => $flip ? 'cl_sortkey DESC' : 'cl_sortkey', - 'LIMIT' => $limit + 1 ) ); - - $sk =& $wgUser->getSkin(); - $r = "
    \n"; + __METHOD__, + array( 'ORDER BY' => $this->flip ? 'cl_sortkey DESC' : 'cl_sortkey', + 'USE INDEX' => 'cl_sortkey', + 'LIMIT' => $this->limit + 1 ) ); + $count = 0; - $nextPage = null; + $this->nextPage = null; while( $x = $dbr->fetchObject ( $res ) ) { - if( ++$count > $limit ) { + if( ++$count > $this->limit ) { // We've reached the one extra which shows that there are // additional pages to be had. Stop here... - $nextPage = $x->cl_sortkey; + $this->nextPage = $x->cl_sortkey; break; } - + $title = Title::makeTitle( $x->page_namespace, $x->page_title ); - + if( $title->getNamespace() == NS_CATEGORY ) { - // Subcategory; strip the 'Category' namespace from the link text. - array_push( $children, $sk->makeKnownLinkObj( $title, $wgContLang->convertHtml( $title->getText() ) ) ); - - // If there's a link from Category:A to Category:B, the sortkey of the resulting - // entry in the categorylinks table is Category:A, not A, which it SHOULD be. - // Workaround: If sortkey == "Category:".$title, than use $title for sorting, - // else use sortkey... - $sortkey=''; - if( $title->getPrefixedText() == $x->cl_sortkey ) { - $sortkey=$wgContLang->firstChar( $x->page_title ); - } else { - $sortkey=$wgContLang->firstChar( $x->cl_sortkey ); - } - array_push( $children_start_char, $wgContLang->convert( $sortkey ) ) ; - } elseif( $wgCategoryMagicGallery && $title->getNamespace() == NS_IMAGE ) { - // Show thumbnails of categorized images, in a separate chunk - if( $flip ) { - $ig->insert( Image::newFromTitle( $title ) ); - } else { - $ig->add( Image::newFromTitle( $title ) ); - } + $this->addSubcategory( $title, $x->cl_sortkey, $x->page_len ); + } elseif( $this->showGallery && $title->getNamespace() == NS_IMAGE ) { + $this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect ); } else { - // Page in this category - array_push( $articles, $sk->makeSizeLinkObj( $x->page_len, $title, $wgContLang->convert( $title->getPrefixedText() ) ) ) ; - array_push( $articles_start_char, $wgContLang->convert( $wgContLang->firstChar( $x->cl_sortkey ) ) ); + $this->addPage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect ); } } $dbr->freeResult( $res ); + } - if( $flip ) { - $children = array_reverse( $children ); - $children_start_char = array_reverse( $children_start_char ); - $articles = array_reverse( $articles ); - $articles_start_char = array_reverse( $articles_start_char ); - } - - if( $until != '' ) { - $r .= $this->pagingLinks( $this->mTitle, $nextPage, $until, $limit ); - } elseif( $nextPage != '' || $from != '' ) { - $r .= $this->pagingLinks( $this->mTitle, $from, $nextPage, $limit ); - } - + function getCategoryTop() { + $r = ''; + if( $this->until != '' ) { + $r .= $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit ); + } elseif( $this->nextPage != '' || $this->from != '' ) { + $r .= $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit ); + } + return $r == '' + ? $r + : "
    \n" . $r; + } + + function getSubcategorySection() { # Don't show subcategories section if there are none. - if( count( $children ) > 0 ) { + $r = ''; + $c = count( $this->children ); + if( $c > 0 ) { # Showing subcategories + $r .= "
    \n"; $r .= '

    ' . wfMsg( 'subcategories' ) . "

    \n"; - $r .= $this->formatCount( $children, 'subcategorycount' ); - $r .= $this->formatList( $children, $children_start_char ); + $r .= wfMsgExt( 'subcategorycount', array( 'parse' ), $c ); + $r .= $this->formatList( $this->children, $this->children_start_char ); + $r .= "\n
    "; } + return $r; + } - # Showing articles in this category - $ti = htmlspecialchars( $this->mTitle->getText() ); - $r .= '

    ' . wfMsg( 'category_header', $ti ) . "

    \n"; - $r .= $this->formatCount( $articles, 'categoryarticlecount' ); - $r .= $this->formatList( $articles, $articles_start_char ); - - if( $wgCategoryMagicGallery && ! $ig->isEmpty() ) { - $r.= $ig->toHTML(); + function getPagesSection() { + $ti = htmlspecialchars( $this->title->getText() ); + # Don't show articles section if there are none. + $r = ''; + $c = count( $this->articles ); + if( $c > 0 ) { + $r = "
    \n"; + $r .= '

    ' . wfMsg( 'category_header', $ti ) . "

    \n"; + $r .= wfMsgExt( 'categoryarticlecount', array( 'parse' ), $c ); + $r .= $this->formatList( $this->articles, $this->articles_start_char ); + $r .= "\n
    "; } - - wfProfileOut( $fname ); return $r; } - /** - * @param array $articles - * @param string $message - * @return string - * @access private - */ - function formatCount( $articles, $message ) { - global $wgContLang; - $numart = count( $articles ); - if( $numart == 1 ) { - # Slightly different message to avoid silly plural - $message .= '1'; + function getImageSection() { + if( $this->showGallery && ! $this->gallery->isEmpty() ) { + return "
    \n" . + '

    ' . wfMsg( 'category-media-header', htmlspecialchars($this->title->getText()) ) . "

    \n" . + wfMsgExt( 'category-media-count', array( 'parse' ), $this->gallery->count() ) . + $this->gallery->toHTML() . "\n
    "; + } else { + return ''; + } + } + + function getCategoryBottom() { + if( $this->until != '' ) { + return $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit ); + } elseif( $this->nextPage != '' || $this->from != '' ) { + return $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit ); + } else { + return ''; } - return wfMsg( $message, $wgContLang->formatNum( $numart ) ); } + /** * Format a list of articles chunked by letter, either as a * bullet list or a columnar format, depending on the length. @@ -203,7 +304,7 @@ * @param array $articles_start_char * @param int $cutoff * @return string - * @access private + * @private */ function formatList( $articles, $articles_start_char, $cutoff = 6 ) { if ( count ( $articles ) > $cutoff ) { @@ -214,7 +315,7 @@ } return ''; } - + /** * Format a list of articles chunked by letter in a three-column * list, ordered vertically. @@ -222,7 +323,7 @@ * @param array $articles * @param array $articles_start_char * @return string - * @access private + * @private */ function columnList( $articles, $articles_start_char ) { // divide list into three equal chunks @@ -231,7 +332,7 @@ // get and display header $r = ''; - $prev_start_char = 'none'; + $prev_start_char = 'none'; // loop through the chunks for($startChunk = 0, $endChunk = $chunk, $chunkIndex = 0; @@ -258,8 +359,8 @@ } $cont_msg = ""; if ( $articles_start_char[$index] == $prev_start_char ) - $cont_msg = wfMsg('listingcontinuesabbrev'); - $r .= "

    {$articles_start_char[$index]}$cont_msg

    \n
      "; + $cont_msg = ' ' . wfMsgHtml( 'listingcontinuesabbrev' ); + $r .= "

      " . htmlspecialchars( $articles_start_char[$index] ) . "$cont_msg

      \n
        "; $prev_start_char = $articles_start_char[$index]; } @@ -275,22 +376,22 @@ $r .= '
    '; return $r; } - + /** * Format a list of articles chunked by letter in a bullet list. * @param array $articles * @param array $articles_start_char * @return string - * @access private + * @private */ function shortList( $articles, $articles_start_char ) { - $r = '

    '.$articles_start_char[0]."

    \n"; + $r = '

    ' . htmlspecialchars( $articles_start_char[0] ) . "

    \n"; $r .= '
    • '.$articles[0].'
    • '; for ($index = 1; $index < count($articles); $index++ ) { if ($articles_start_char[$index] != $articles_start_char[$index - 1]) { - $r .= "

    {$articles_start_char[$index]}

    \n
      "; + $r .= "

    " . htmlspecialchars( $articles_start_char[$index] ) . "

    \n
      "; } $r .= "
    • {$articles[$index]}
    • "; @@ -298,7 +399,7 @@ $r .= '
    '; return $r; } - + /** * @param Title $title * @param string $first @@ -306,13 +407,13 @@ * @param int $limit * @param array $query - additional query options to pass * @return string - * @access private + * @private */ function pagingLinks( $title, $first, $last, $limit, $query = array() ) { global $wgUser, $wgLang; - $sk =& $wgUser->getSkin(); + $sk = $this->getSkin(); $limitText = $wgLang->formatNum( $limit ); - + $prevLink = htmlspecialchars( wfMsg( 'prevn', $limitText ) ); if( $first != '' ) { $prevLink = $sk->makeLinkObj( $title, $prevLink, @@ -323,10 +424,10 @@ $nextLink = $sk->makeLinkObj( $title, $nextLink, wfArrayToCGI( $query + array( 'from' => $last ) ) ); } - + return "($prevLink) ($nextLink)"; } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ChangesList.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ChangesList.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ChangesList.php 2005-11-23 13:42:11.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ChangesList.php 2007-09-03 20:38:54.000000000 -0400 @@ -1,40 +1,90 @@ mAttribs = $rc->mAttribs; + $rc2->mExtra = $rc->mExtra; + return $rc2; + } +} ; /** - * @package MediaWiki + * Class to show various lists of changes: + * - what links here + * - related changes + * - recent changes */ class ChangesList { # Called by history lists and recent changes # /** @todo document */ - function ChangesList( &$skin ) { + function __construct( &$skin ) { $this->skin =& $skin; + $this->preCacheMessages(); + } + + /** + * Fetch an appropriate changes list class for the specified user + * Some users might want to use an enhanced list format, for instance + * + * @param $user User to fetch the list class for + * @return ChangesList derivative + */ + public static function newFromUser( &$user ) { + $sk = $user->getSkin(); + $list = NULL; + if( wfRunHooks( 'FetchChangesList', array( &$user, &$sk, &$list ) ) ) { + return $user->getOption( 'usenewrc' ) ? new EnhancedChangesList( $sk ) : new OldChangesList( $sk ); + } else { + return $list; + } + } + + /** + * As we use the same small set of messages in various methods and that + * they are called often, we call them once and save them in $this->message + */ + function preCacheMessages() { + // Precache various messages + if( !isset( $this->message ) ) { + foreach( explode(' ', 'cur diff hist minoreditletter newpageletter last '. + 'blocklink history boteditletter' ) as $msg ) { + $this->message[$msg] = wfMsgExt( $msg, array( 'escape') ); + } + } } + /** - * Returns the appropiate flags for new page, minor change and patrolling + * Returns the appropriate flags for new page, minor change and patrolling */ - function recentChangesFlags( $new, $minor, $patrolled, $nothing = ' ' ) { - $f = $new ? '' . htmlspecialchars( wfMsg( 'newpageletter' ) ) . '' + function recentChangesFlags( $new, $minor, $patrolled, $nothing = ' ', $bot = false ) { + $f = $new ? '' . $this->message['newpageletter'] . '' : $nothing; - $f .= $minor ? '' . htmlspecialchars( wfMsg( 'minoreditletter' ) ) . '' + $f .= $minor ? '' . $this->message['minoreditletter'] . '' : $nothing; + $f .= $bot ? '' . $this->message['boteditletter'] . '' : $nothing; $f .= $patrolled ? '!' : $nothing; return $f; - } /** * Returns text for the start of the tabular part of RC */ function beginRecentChangesList() { - $this->rc_cache = array() ; + $this->rc_cache = array(); $this->rcMoveIndex = 0; - $this->rcCacheIndex = 0 ; + $this->rcCacheIndex = 0; $this->lastdate = ''; $this->rclistOpen = false; return ''; @@ -42,509 +92,616 @@ /** * Returns text for the end of RC - * If enhanced RC is in use, returns pretty much all the text */ function endRecentChangesList() { - $s = $this->recentChangesBlock() ; if( $this->rclistOpen ) { - $s .= "\n"; - } - return $s; - } - - /** - * Enhanced RC ungrouped line - */ - function recentChangesBlockLine ( $rcObj ) { - global $wgStylePath, $wgContLang ; - - # Get rc_xxxx variables - extract( $rcObj->mAttribs ) ; - $curIdEq = 'curid='.$rc_cur_id; - - # Spacer image - $r = '' ; - - $r .= '' ; - $r .= '' ; - - if ( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { - $r .= '   '; + return "\n"; } else { - $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled ); + return ''; } + } - # Timestamp - $r .= ' '.$rcObj->timestamp.' ' ; - $r .= '' ; - - # Article link - $link = $rcObj->link ; - if ( $rcObj->watched ) $link = ''.$link.'' ; - $r .= $link ; + function insertMove( &$s, $rc ) { # Diff - $r .= ' (' ; - $r .= $rcObj->difflink ; - $r .= '; ' ; - + $s .= '(' . $this->message['diff'] . ') ('; # Hist - $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ); - - # User/talk - $r .= ') . . '.$rcObj->userlink ; - $r .= $rcObj->usertalklink ; - - # Comment - if ( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) { - $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() ); - } - - if ($rcObj->numberofWatchingusers > 0) { - $r .= wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($rcObj->numberofWatchingusers)); - } + $s .= $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), $this->message['hist'], 'action=history' ) . + ') . . '; - $r .= "
    \n" ; - return $r ; + # "[[x]] moved to [[y]]" + $msg = ( $rc->mAttribs['rc_type'] == RC_MOVE ) ? '1movedto2' : '1movedto2_redir'; + $s .= wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ), + $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) ); } - /** - * Enhanced RC group - */ - function recentChangesBlockGroup ( $block ) { - global $wgStylePath, $wgContLang ; - - $r = ''; + function insertDateHeader(&$s, $rc_timestamp) { + global $wgLang; - # Collate list of users - $isnew = false ; - $unpatrolled = false; - $userlinks = array () ; - foreach ( $block AS $rcObj ) { - $oldid = $rcObj->mAttribs['rc_last_oldid']; - $newid = $rcObj->mAttribs['rc_this_oldid']; - if ( $rcObj->mAttribs['rc_new'] ) { - $isnew = true ; - } - $u = $rcObj->userlink ; - if ( !isset ( $userlinks[$u] ) ) { - $userlinks[$u] = 0 ; - } - if ( $rcObj->unpatrolled ) { - $unpatrolled = true; + # Make date header if necessary + $date = $wgLang->date( $rc_timestamp, true, true ); + $s = ''; + if( $date != $this->lastdate ) { + if( '' != $this->lastdate ) { + $s .= "\n"; } - $userlinks[$u]++ ; - } - - # Sort the list and convert to text - krsort ( $userlinks ) ; - asort ( $userlinks ) ; - $users = array () ; - foreach ( $userlinks as $userlink => $count) { - $text = $userlink ; - if ( $count > 1 ) $text .= " ({$count}×)" ; - array_push ( $users , $text ) ; + $s .= '

    '.$date."

    \n
      "; + $this->lastdate = $date; + $this->rclistOpen = true; } - $users = ' ['.implode('; ',$users).']'; - - # Arrow - $rci = 'RCI'.$this->rcCacheIndex ; - $rcl = 'RCL'.$this->rcCacheIndex ; - $rcm = 'RCM'.$this->rcCacheIndex ; - $toggleLink = "javascript:toggleVisibility('$rci','$rcm','$rcl')" ; - $arrowdir = $wgContLang->isRTL() ? 'l' : 'r'; - $tl = '+' ; - $tl .= '' ; - $r .= $tl ; + } - # Main line + function insertLog(&$s, $title, $logtype) { + $logname = LogPage::logName( $logtype ); + $s .= '(' . $this->skin->makeKnownLinkObj($title, $logname ) . ')'; + } - $r .= '' ; - $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled ); - # Timestamp - $r .= ' '.$block[0]->timestamp.' ' ; - $r .= '' ; + function insertDiffHist(&$s, &$rc, $unpatrolled) { + # Diff link + if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) { + $diffLink = $this->message['diff']; + } else { + $rcidparam = $unpatrolled + ? array( 'rcid' => $rc->mAttribs['rc_id'] ) + : array(); + $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'], + wfArrayToCGI( array( + 'curid' => $rc->mAttribs['rc_cur_id'], + 'diff' => $rc->mAttribs['rc_this_oldid'], + 'oldid' => $rc->mAttribs['rc_last_oldid'] ), + $rcidparam ), + '', '', ' tabindex="'.$rc->counter.'"'); + } + $s .= '('.$diffLink.') ('; + + # History link + $s .= $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['hist'], + wfArrayToCGI( array( + 'curid' => $rc->mAttribs['rc_cur_id'], + 'action' => 'history' ) ) ); + $s .= ') . . '; + } + function insertArticleLink(&$s, &$rc, $unpatrolled, $watched) { # Article link - $link = $block[0]->link ; - if ( $block[0]->watched ) $link = ''.$link.'' ; - $r .= $link ; + # If it's a new article, there is no diff link, but if it hasn't been + # patrolled yet, we need to give users a way to do so + $params = ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW ) + ? 'rcid='.$rc->mAttribs['rc_id'] + : ''; + $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params ); + if( $watched ) + $articlelink = "{$articlelink}"; + global $wgContLang; + $articlelink .= $wgContLang->getDirMark(); - $curIdEq = 'curid=' . $block[0]->mAttribs['rc_cur_id']; - $currentRevision = $block[0]->mAttribs['rc_this_oldid']; - if ( $block[0]->mAttribs['rc_type'] != RC_LOG ) { - # Changes - $r .= ' ('.count($block).' ' ; - if ( $isnew ) $r .= wfMsg('changes'); - else $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle() , wfMsg('changes') , - $curIdEq."&diff=$currentRevision&oldid=$oldid" ) ; - $r .= '; ' ; - - # History - $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(), wfMsg( 'history' ), $curIdEq.'&action=history' ); - $r .= ')' ; - } - - $r .= $users ; - - if ($block[0]->numberofWatchingusers > 0) { - $r .= wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($block[0]->numberofWatchingusers)); - } - $r .= "
      \n" ; + $s .= ' '.$articlelink; + } - # Sub-entries - $r .= '\n" ; + } - $this->rcCacheIndex++ ; - return $r ; + /** + * Check whether to enable recent changes patrol features + * @return bool + */ + function usePatrol() { + global $wgUseRCPatrol, $wgUser; + return( $wgUseRCPatrol && ($wgUser->isAllowed('patrol') || $wgUser->isAllowed('patrolmarks')) ); } /** - * If enhanced RC is in use, this function takes the previously cached - * RC lines, arranges them, and outputs the HTML + * Returns the string which indicates the number of watching users */ - function recentChangesBlock () { - global $wgStylePath ; - if ( count ( $this->rc_cache ) == 0 ) return '' ; - $blockOut = ''; - foreach ( $this->rc_cache AS $secureName => $block ) { - if ( count ( $block ) < 2 ) { - $blockOut .= $this->recentChangesBlockLine ( array_shift ( $block ) ) ; - } else { - $blockOut .= $this->recentChangesBlockGroup ( $block ) ; + function numberofWatchingusers( $count ) { + global $wgLang; + static $cache = array(); + if ( $count > 0 ) { + if ( !isset( $cache[$count] ) ) { + $cache[$count] = wfMsgExt('number_of_watching_users_RCview', + array('parsemag', 'escape'), $wgLang->formatNum($count)); } + return $cache[$count]; + } else { + return ''; } - - return '
      '.$blockOut.'
      ' ; } +} + +/** + * Generate a list of changes using the good old system (no javascript) + */ +class OldChangesList extends ChangesList { /** - * Called in a loop over all displayed RC entries - * Either returns the line, or caches it for later use + * Format a line using the old system (aka without any javascript). */ function recentChangesLine( &$rc, $watched = false ) { - global $wgUser; - $usenew = $wgUser->getOption( 'usenewrc' ); - if ( $usenew ) - $line = $this->recentChangesLineNew ( $rc, $watched ) ; - else - $line = $this->recentChangesLineOld ( $rc, $watched ) ; - return $line ; - } + global $wgContLang, $wgRCShowChangedSize; - - function recentChangesLineOld( &$rc, $watched = false ) { - global $wgTitle, $wgLang, $wgContLang, $wgUser, $wgUseRCPatrol, - $wgOnlySysopsCanPatrol, $wgSysopUserBans; - - $fname = 'Skin::recentChangesLineOld'; + $fname = 'ChangesList::recentChangesLineOld'; wfProfileIn( $fname ); - static $message; - if( !isset( $message ) ) { - foreach( explode(' ', 'diff hist minoreditletter newpageletter blocklink' ) as $msg ) { - $message[$msg] = wfMsg( $msg ); - } - } - # Extract DB fields into local scope + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( $rc->mAttribs ); - $curIdEq = 'curid=' . $rc_cur_id; # Should patrol-related stuff be shown? - $unpatrolled = $wgUseRCPatrol && $wgUser->isLoggedIn() && - ( !$wgOnlySysopsCanPatrol || $wgUser->isAllowed('patrol') ) && $rc_patrolled == 0; + $unpatrolled = $this->usePatrol() && $rc_patrolled == 0; - # Make date header if necessary - $date = $wgLang->date( $rc_timestamp, true, true ); - $s = ''; - if ( $date != $this->lastdate ) { - if ( '' != $this->lastdate ) { $s .= "
    \n"; } - $s .= "

    {$date}

    \n
      "; - $this->lastdate = $date; - $this->rclistOpen = true; - } + $this->insertDateHeader($s,$rc_timestamp); $s .= '
    • '; - if ( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { - # Diff - $s .= '(' . $message['diff'] . ') ('; - # Hist - $s .= $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), $message['hist'], 'action=history' ) . - ') . . '; - - # "[[x]] moved to [[y]]" - $msg = ( $rc_type == RC_MOVE ) ? '1movedto2' : '1movedto2_redir'; - $s .= wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ), - $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) ); - } elseif( $rc_namespace == NS_SPECIAL && preg_match( '!^Log/(.*)$!', $rc_title, $matches ) ) { - # Log updates, etc - $logtype = $matches[1]; - $logname = LogPage::logName( $logtype ); - $s .= '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')'; - } else { - wfProfileIn("$fname-page"); - # Diff link - if ( $rc_type == RC_NEW || $rc_type == RC_LOG ) { - $diffLink = $message['diff']; + // moved pages + if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + $this->insertMove( $s, $rc ); + // log entries + } elseif ( $rc_namespace == NS_SPECIAL ) { + list( $specialName, $specialSubpage ) = SpecialPage::resolveAliasWithSubpage( $rc_title ); + if ( $specialName == 'Log' ) { + $this->insertLog( $s, $rc->getTitle(), $specialSubpage ); } else { - if ( $unpatrolled ) - $rcidparam = "&rcid={$rc_id}"; - else - $rcidparam = ""; - $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $message['diff'], - "{$curIdEq}&diff={$rc_this_oldid}&oldid={$rc_last_oldid}{$rcidparam}", - '', '', ' tabindex="'.$rc->counter.'"'); + wfDebug( "Unexpected special page in recentchanges\n" ); } - $s .= '('.$diffLink.') ('; - - # History link - $s .= $this->skin->makeKnownLinkObj( $rc->getTitle(), $message['hist'], $curIdEq.'&action=history' ); - $s .= ') . . '; - - # M, N and ! (minor, new and unpatrolled) - $s .= ' ' . $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $unpatrolled, '' ); + // all other stuff + } else { + wfProfileIn($fname.'-page'); - # Article link - # If it's a new article, there is no diff link, but if it hasn't been - # patrolled yet, we need to give users a way to do so - if ( $unpatrolled && $rc_type == RC_NEW ) - $articleLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" ); - else - $articleLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '' ); + $this->insertDiffHist($s, $rc, $unpatrolled); - if ( $watched ) { - $articleLink = ''.$articleLink.''; - } + # M, N, b and ! (minor, new, bot and unpatrolled) + $s .= ' ' . $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $unpatrolled, '', $rc_bot ); + $this->insertArticleLink($s, $rc, $unpatrolled, $watched); - $s .= ' '.$articleLink; - wfProfileOut("$fname-page"); + wfProfileOut($fname.'-page'); } - wfProfileIn( "$fname-rest" ); - # Timestamp - $s .= '; ' . $wgLang->time( $rc_timestamp, true, true ) . ' . . '; + wfProfileIn( $fname.'-rest' ); - # User link (or contributions for unregistered users) - if ( 0 == $rc_user ) { - $contribsPage =& Title::makeTitle( NS_SPECIAL, 'Contributions' ); - $userLink = $this->skin->makeKnownLinkObj( $contribsPage, - $rc_user_text, 'target=' . $rc_user_text ); - } else { - $userPage =& Title::makeTitle( NS_USER, $rc_user_text ); - $userLink = $this->skin->makeLinkObj( $userPage, htmlspecialchars( $rc_user_text ) ); - } - $s .= $userLink; + $this->insertTimestamp($s,$rc); - # User talk link - $talkname = $wgContLang->getNsText(NS_TALK); # use the shorter name - global $wgDisableAnonTalk; - if( 0 == $rc_user && $wgDisableAnonTalk ) { - $userTalkLink = ''; - } else { - $userTalkPage =& Title::makeTitle( NS_USER_TALK, $rc_user_text ); - $userTalkLink= $this->skin->makeLinkObj( $userTalkPage, htmlspecialchars( $talkname ) ); + if( $wgRCShowChangedSize ) { + $s .= ( $rc->getCharacterDifference() == '' ? '' : $rc->getCharacterDifference() . ' . . ' ); } - # Block link - $blockLink=''; - if ( ( $wgSysopUserBans || 0 == $rc_user ) && $wgUser->isAllowed('block') ) { - $blockLinkPage = Title::makeTitle( NS_SPECIAL, 'Blockip' ); - $blockLink = $this->skin->makeKnownLinkObj( $blockLinkPage, - htmlspecialchars( $message['blocklink'] ), 'ip=' . urlencode( $rc_user_text ) ); - } - if($blockLink) { - if($userTalkLink) $userTalkLink .= ' | '; - $userTalkLink .= $blockLink; - } - if($userTalkLink) $s.=' ('.$userTalkLink.')'; + $this->insertUserRelatedLinks($s,$rc); + $this->insertComment($s, $rc); - # Add comment - if ( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) { - $s .= $this->skin->commentBlock( $rc_comment, $rc->getTitle() ); - } - - if ($rc->numberofWatchingusers > 0) { - $s .= ' ' . wfMsg('number_of_watching_users_RCview', $wgContLang->formatNum($rc->numberofWatchingusers)); - } + $s .= rtrim(' ' . $this->numberofWatchingusers($rc->numberofWatchingusers)); $s .= "
    • \n"; - wfProfileOut( "$fname-rest" ); + wfProfileOut( $fname.'-rest' ); + wfProfileOut( $fname ); return $s; } +} - function recentChangesLineNew( &$baseRC, $watched = false ) { - global $wgTitle, $wgLang, $wgContLang, $wgUser, - $wgUseRCPatrol, $wgOnlySysopsCanPatrol, $wgSysopUserBans; - - static $message; - if( !isset( $message ) ) { - foreach( explode(' ', 'cur diff hist minoreditletter newpageletter last blocklink' ) as $msg ) { - $message[$msg] = wfMsg( $msg ); - } - } + +/** + * Generate a list of changes using an Enhanced system (use javascript). + */ +class EnhancedChangesList extends ChangesList { + /** + * Format a line for enhanced recentchange (aka with javascript and block of lines). + */ + function recentChangesLine( &$baseRC, $watched = false ) { + global $wgLang, $wgContLang; # Create a specialised object - $rc = RCCacheEntry::newFromParent( $baseRC ) ; + $rc = RCCacheEntry::newFromParent( $baseRC ); # Extract fields from DB into the function scope (rc_xxxx variables) + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( $rc->mAttribs ); $curIdEq = 'curid=' . $rc_cur_id; # If it's a new day, add the headline and flush the cache $date = $wgLang->date( $rc_timestamp, true); $ret = ''; - if ( $date != $this->lastdate ) { + if( $date != $this->lastdate ) { # Process current cache - $ret = $this->recentChangesBlock () ; - $this->rc_cache = array() ; + $ret = $this->recentChangesBlock(); + $this->rc_cache = array(); $ret .= "

      {$date}

      \n"; $this->lastdate = $date; } # Should patrol-related stuff be shown? - if ( $wgUseRCPatrol && $wgUser->isLoggedIn() && - ( !$wgOnlySysopsCanPatrol || $wgUser->isAllowed('patrol') )) { + if( $this->usePatrol() ) { $rc->unpatrolled = !$rc_patrolled; } else { $rc->unpatrolled = false; } # Make article link - if ( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { $msg = ( $rc_type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir"; $clink = wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ), $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) ); - } elseif( $rc_namespace == NS_SPECIAL && preg_match( '!^Log/(.*)$!', $rc_title, $matches ) ) { - # Log updates, etc - $logtype = $matches[1]; - $logname = LogPage::logName( $logtype ); - $clink = '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')'; - } elseif ( $rc->unpatrolled && $rc_type == RC_NEW ) { + } elseif( $rc_namespace == NS_SPECIAL ) { + list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $rc_title ); + if ( $specialName == 'Log' ) { + # Log updates, etc + $logname = LogPage::logName( $logtype ); + $clink = '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')'; + } else { + wfDebug( "Unexpected special page in recentchanges\n" ); + $clink = ''; + } + } elseif( $rc->unpatrolled && $rc_type == RC_NEW ) { # Unpatrolled new page, give rc_id in query $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" ); } else { - $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '' ) ; + $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '' ); } $time = $wgContLang->time( $rc_timestamp, true, true ); - $rc->watched = $watched ; - $rc->link = $clink ; + $rc->watched = $watched; + $rc->link = $clink; $rc->timestamp = $time; $rc->numberofWatchingusers = $baseRC->numberofWatchingusers; # Make "cur" and "diff" links - $titleObj = $rc->getTitle(); - if ( $rc->unpatrolled ) { + if( $rc->unpatrolled ) { $rcIdQuery = "&rcid={$rc_id}"; } else { $rcIdQuery = ''; } $querycur = $curIdEq."&diff=0&oldid=$rc_this_oldid"; - $querydiff = $curIdEq."&diff=$rc_this_oldid&oldid=$rc_last_oldid"; + $querydiff = $curIdEq."&diff=$rc_this_oldid&oldid=$rc_last_oldid$rcIdQuery"; $aprops = ' tabindex="'.$baseRC->counter.'"'; - $curLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $message['cur'], $querycur, '' ,'' , $aprops ); + $curLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['cur'], $querycur, '' ,'', $aprops ); if( $rc_type == RC_NEW || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { if( $rc_type != RC_NEW ) { - $curLink = $message['cur']; + $curLink = $this->message['cur']; } - $diffLink = $message['diff']; + $diffLink = $this->message['diff']; } else { - $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $message['diff'], $querydiff . $rcIdQuery, '' ,'' , $aprops ); + $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'], $querydiff, '' ,'', $aprops ); } # Make "last" link - if ( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { - $lastLink = $message['last']; + if( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + $lastLink = $this->message['last']; } else { - $lastLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $message['last'], + $lastLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['last'], $curIdEq.'&diff='.$rc_this_oldid.'&oldid='.$rc_last_oldid . $rcIdQuery ); } - # Make user link (or user contributions for unregistered users) - if ( $rc_user == 0 ) { - $contribsPage =& Title::makeTitle( NS_SPECIAL, 'Contributions' ); - $userLink = $this->skin->makeKnownLinkObj( $contribsPage, - $rc_user_text, 'target=' . $rc_user_text ); - } else { - $userPage =& Title::makeTitle( NS_USER, $rc_user_text ); - $userLink = $this->skin->makeLinkObj( $userPage, $rc_user_text ); - } + $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text ); - $rc->userlink = $userLink; $rc->lastlink = $lastLink; $rc->curlink = $curLink; $rc->difflink = $diffLink; - # Make user talk link - $talkname = $wgContLang->getNsText( NS_TALK ); # use the shorter name - $userTalkPage =& Title::makeTitle( NS_USER_TALK, $rc_user_text ); - $userTalkLink = $this->skin->makeLinkObj( $userTalkPage, $talkname ); - - global $wgDisableAnonTalk; - if ( ( $wgSysopUserBans || 0 == $rc_user ) && $wgUser->isAllowed('block') ) { - $blockPage =& Title::makeTitle( NS_SPECIAL, 'Blockip' ); - $blockLink = $this->skin->makeKnownLinkObj( $blockPage, - $message['blocklink'], 'ip='.$rc_user_text ); - if( $wgDisableAnonTalk ) - $rc->usertalklink = ' ('.$blockLink.')'; - else - $rc->usertalklink = ' ('.$userTalkLink.' | '.$blockLink.')'; - } else { - if( $wgDisableAnonTalk && ($rc_user == 0) ) - $rc->usertalklink = ''; - else - $rc->usertalklink = ' ('.$userTalkLink.')'; - } + $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text ); # Put accumulated information into the cache, for later display # Page moves go on their own line $title = $rc->getTitle(); $secureName = $title->getPrefixedDBkey(); - if ( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { # Use an @ character to prevent collision with page names $this->rc_cache['@@' . ($this->rcMoveIndex++)] = array($rc); } else { - if ( !isset ( $this->rc_cache[$secureName] ) ) $this->rc_cache[$secureName] = array() ; - array_push ( $this->rc_cache[$secureName] , $rc ) ; + if( !isset ( $this->rc_cache[$secureName] ) ) { + $this->rc_cache[$secureName] = array(); + } + array_push( $this->rc_cache[$secureName], $rc ); } return $ret; } + /** + * Enhanced RC group + */ + function recentChangesBlockGroup( $block ) { + global $wgLang, $wgContLang, $wgRCShowChangedSize; + $r = ''; + + # Collate list of users + $isnew = false; + $unpatrolled = false; + $userlinks = array(); + foreach( $block as $rcObj ) { + $oldid = $rcObj->mAttribs['rc_last_oldid']; + if( $rcObj->mAttribs['rc_new'] ) { + $isnew = true; + } + $u = $rcObj->userlink; + if( !isset( $userlinks[$u] ) ) { + $userlinks[$u] = 0; + } + if( $rcObj->unpatrolled ) { + $unpatrolled = true; + } + $bot = $rcObj->mAttribs['rc_bot']; + $userlinks[$u]++; + } + + # Sort the list and convert to text + krsort( $userlinks ); + asort( $userlinks ); + $users = array(); + foreach( $userlinks as $userlink => $count) { + $text = $userlink; + $text .= $wgContLang->getDirMark(); + if( $count > 1 ) { + $text .= ' ('.$count.'×)'; + } + array_push( $users, $text ); + } + + $users = ' ['.implode('; ',$users).']'; + + # Arrow + $rci = 'RCI'.$this->rcCacheIndex; + $rcl = 'RCL'.$this->rcCacheIndex; + $rcm = 'RCM'.$this->rcCacheIndex; + $toggleLink = "javascript:toggleVisibility('$rci','$rcm','$rcl')"; + $tl = '' . $this->sideArrow() . ''; + $tl .= ''; + $r .= $tl; + + # Main line + $r .= ''; + $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, ' ', $bot ); + + # Timestamp + $r .= ' '.$block[0]->timestamp.' '; + + # Article link + $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); + $r .= $wgContLang->getDirMark(); + + $curIdEq = 'curid=' . $block[0]->mAttribs['rc_cur_id']; + $currentRevision = $block[0]->mAttribs['rc_this_oldid']; + if( $block[0]->mAttribs['rc_type'] != RC_LOG ) { + # Changes + + $n = count($block); + static $nchanges = array(); + if ( !isset( $nchanges[$n] ) ) { + $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape'), + $wgLang->formatNum( $n ) ); + } + + $r .= ' ('; + + if( $isnew ) { + $r .= $nchanges[$n]; + } else { + $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(), + $nchanges[$n], $curIdEq."&diff=$currentRevision&oldid=$oldid" ); + } + + $r .= ') . . '; + + if( $wgRCShowChangedSize ) { + # Character difference + $chardiff = $rcObj->getCharacterDifference( $block[ count( $block ) - 1 ]->mAttribs['rc_old_len'], + $block[0]->mAttribs['rc_new_len'] ); + if( $chardiff == '' ) { + $r .= ' ('; + } else { + $r .= ' ' . $chardiff. ' . . '; + } + } + + # History + $r .= '(' . $this->skin->makeKnownLinkObj( $block[0]->getTitle(), + $this->message['history'], $curIdEq.'&action=history' ); + $r .= ')'; + } + + $r .= $users; + + $r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers); + $r .= "
      \n"; + + # Sub-entries + $r .= '\n"; + + $this->rcCacheIndex++; + return $r; + } + + function maybeWatchedLink( $link, $watched=false ) { + if( $watched ) { + // FIXME: css style might be more appropriate + return '' . $link . ''; + } else { + return $link; + } + } + + /** + * Generate HTML for an arrow or placeholder graphic + * @param string $dir one of '', 'd', 'l', 'r' + * @param string $alt text + * @return string HTML tag + * @access private + */ + function arrow( $dir, $alt='' ) { + global $wgStylePath; + $encUrl = htmlspecialchars( $wgStylePath . '/common/images/Arr_' . $dir . '.png' ); + $encAlt = htmlspecialchars( $alt ); + return "\"$encAlt\""; + } + + /** + * Generate HTML for a right- or left-facing arrow, + * depending on language direction. + * @return string HTML tag + * @access private + */ + function sideArrow() { + global $wgContLang; + $dir = $wgContLang->isRTL() ? 'l' : 'r'; + return $this->arrow( $dir, '+' ); + } + + /** + * Generate HTML for a down-facing arrow + * depending on language direction. + * @return string HTML tag + * @access private + */ + function downArrow() { + return $this->arrow( 'd', '-' ); + } + + /** + * Generate HTML for a spacer image + * @return string HTML tag + * @access private + */ + function spacerArrow() { + return $this->arrow( '', ' ' ); + } + + /** + * Enhanced RC ungrouped line. + * @return string a HTML formated line (generated using $r) + */ + function recentChangesBlockLine( $rcObj ) { + global $wgContLang, $wgRCShowChangedSize; + + # Get rc_xxxx variables + // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + extract( $rcObj->mAttribs ); + $curIdEq = 'curid='.$rc_cur_id; + + $r = ''; + + # Spacer image + $r .= $this->spacerArrow(); + + # Flag and Timestamp + $r .= ''; + + if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + $r .= '   '; + } else { + $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); + } + $r .= ' '.$rcObj->timestamp.' '; + + # Article link + $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched ); + + # Diff + $r .= ' ('. $rcObj->difflink .'; '; + + # Hist + $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ') . . '; + + # Character diff + if( $wgRCShowChangedSize ) { + $r .= ( $rcObj->getCharacterDifference() == '' ? '' : ' ' . $rcObj->getCharacterDifference() . ' . . ' ) ; + } + + # User/talk + $r .= $rcObj->userlink . $rcObj->usertalklink; + + # Comment + if( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) { + $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() ); + } + + $r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers); + + $r .= "
      \n"; + return $r; + } + + /** + * If enhanced RC is in use, this function takes the previously cached + * RC lines, arranges them, and outputs the HTML + */ + function recentChangesBlock() { + if( count ( $this->rc_cache ) == 0 ) { + return ''; + } + $blockOut = ''; + foreach( $this->rc_cache as $block ) { + if( count( $block ) < 2 ) { + $blockOut .= $this->recentChangesBlockLine( array_shift( $block ) ); + } else { + $blockOut .= $this->recentChangesBlockGroup( $block ); + } + } + + return '
      '.$blockOut.'
      '; + } + + /** + * Returns text for the end of RC + * If enhanced RC is in use, returns pretty much all the text + */ + function endRecentChangesList() { + return $this->recentChangesBlock() . parent::endRecentChangesList(); + } + } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Credits.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Credits.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Credits.php 2005-08-13 05:03:19.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Credits.php 2007-06-28 21:19:14.000000000 -0400 @@ -15,66 +15,65 @@ * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * * @author - * @package MediaWiki */ /** * This is largely cadged from PageHistory::history */ function showCreditsPage($article) { - global $wgOut; + global $wgOut; - $fname = 'showCreditsPage'; + $fname = 'showCreditsPage'; - wfProfileIn( $fname ); - - $wgOut->setPageTitle( $article->mTitle->getPrefixedText() ); - $wgOut->setSubtitle( wfMsg( 'creditspage' ) ); - $wgOut->setArticleFlag( false ); - $wgOut->setArticleRelated( true ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + wfProfileIn( $fname ); + + $wgOut->setPageTitle( $article->mTitle->getPrefixedText() ); + $wgOut->setSubtitle( wfMsg( 'creditspage' ) ); + $wgOut->setArticleFlag( false ); + $wgOut->setArticleRelated( true ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); - if( $article->mTitle->getArticleID() == 0 ) { + if( $article->mTitle->getArticleID() == 0 ) { $s = wfMsg( 'nocredits' ); - } else { + } else { $s = getCredits($article, -1); - } + } - $wgOut->addHTML( $s ); + $wgOut->addHTML( $s ); - wfProfileOut( $fname ); + wfProfileOut( $fname ); } function getCredits($article, $cnt, $showIfMax=true) { $fname = 'getCredits'; wfProfileIn( $fname ); - $s = ''; + $s = ''; - if (isset($cnt) && $cnt != 0) { + if (isset($cnt) && $cnt != 0) { $s = getAuthorCredits($article); if ($cnt > 1 || $cnt < 0) { $s .= ' ' . getContributorCredits($article, $cnt - 1, $showIfMax); } - } + } wfProfileOut( $fname ); - return $s; + return $s; } /** * */ function getAuthorCredits($article) { - global $wgLang, $wgAllowRealName; + global $wgLang, $wgAllowRealName; - $last_author = $article->getUser(); + $last_author = $article->getUser(); - if ($last_author == 0) { + if ($last_author == 0) { $author_credit = wfMsg('anonymous'); - } else { + } else { if($wgAllowRealName) { $real_name = User::whoIsReal($last_author); } $user_name = User::whoIs($last_author); @@ -83,15 +82,17 @@ } else { $author_credit = wfMsg('siteuser', creditLink($user_name)); } - } + } - $timestamp = $article->getTimestamp(); - if ($timestamp) { - $d = $wgLang->timeanddate($article->getTimestamp(), true); - } else { + $timestamp = $article->getTimestamp(); + if ($timestamp) { + $d = $wgLang->date($article->getTimestamp(), true); + $t = $wgLang->time($article->getTimestamp(), true); + } else { $d = ''; - } - return wfMsg('lastmodifiedby', $d, $author_credit); + $t = ''; + } + return wfMsg('lastmodifiedatby', $d, $t, $author_credit); } /** @@ -99,31 +100,31 @@ */ function getContributorCredits($article, $cnt, $showIfMax) { - global $wgLang, $wgAllowRealName; + global $wgLang, $wgAllowRealName; - $contributors = $article->getContributors(); + $contributors = $article->getContributors(); - $others_link = ''; + $others_link = ''; - # Hmm... too many to fit! + # Hmm... too many to fit! - if ($cnt > 0 && count($contributors) > $cnt) { + if ($cnt > 0 && count($contributors) > $cnt) { $others_link = creditOthersLink($article); if (!$showIfMax) { return wfMsg('othercontribs', $others_link); } else { $contributors = array_slice($contributors, 0, $cnt); } - } + } - $real_names = array(); - $user_names = array(); + $real_names = array(); + $user_names = array(); - $anon = ''; + $anon = ''; - # Sift for real versus user names + # Sift for real versus user names - foreach ($contributors as $user_parts) { + foreach ($contributors as $user_parts) { if ($user_parts[0] != 0) { if ($wgAllowRealName && !empty($user_parts[2])) { $real_names[] = creditLink($user_parts[1], $user_parts[2]); @@ -133,55 +134,55 @@ } else { $anon = wfMsg('anonymous'); } - } + } - # Two strings: real names, and user names + # Two strings: real names, and user names - $real = $wgLang->listToText($real_names); - $user = $wgLang->listToText($user_names); + $real = $wgLang->listToText($real_names); + $user = $wgLang->listToText($user_names); - # "ThisSite user(s) A, B and C" + # "ThisSite user(s) A, B and C" - if (!empty($user)) { - $user = wfMsg('siteusers', $user); - } + if (!empty($user)) { + $user = wfMsg('siteusers', $user); + } - # This is the big list, all mooshed together. We sift for blank strings + # This is the big list, all mooshed together. We sift for blank strings - $fulllist = array(); + $fulllist = array(); - foreach (array($real, $user, $anon, $others_link) as $s) { + foreach (array($real, $user, $anon, $others_link) as $s) { if (!empty($s)) { array_push($fulllist, $s); } - } + } - # Make the list into text... + # Make the list into text... - $creds = $wgLang->listToText($fulllist); + $creds = $wgLang->listToText($fulllist); - # "Based on work by ..." + # "Based on work by ..." - return (empty($creds)) ? '' : wfMsg('othercontribs', $creds); + return (empty($creds)) ? '' : wfMsg('othercontribs', $creds); } /** * */ function creditLink($user_name, $link_text = '') { - global $wgUser, $wgContLang; - $skin = $wgUser->getSkin(); - return $skin->makeLink($wgContLang->getNsText(NS_USER) . ':' . $user_name, - htmlspecialchars( (empty($link_text)) ? $user_name : $link_text )); + global $wgUser, $wgContLang; + $skin = $wgUser->getSkin(); + return $skin->makeLink($wgContLang->getNsText(NS_USER) . ':' . $user_name, + htmlspecialchars( (empty($link_text)) ? $user_name : $link_text )); } /** * */ function creditOthersLink($article) { - global $wgUser, $wgLang; - $skin = $wgUser->getSkin(); - return $skin->makeKnownLink($article->mTitle->getPrefixedText(), wfMsg('others'), 'action=credits'); + global $wgUser; + $skin = $wgUser->getSkin(); + return $skin->makeKnownLink($article->mTitle->getPrefixedText(), wfMsg('others'), 'action=credits'); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Database.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Database.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Database.php 2006-02-11 21:13:37.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Database.php 2007-09-02 14:03:10.000000000 -0400 @@ -1,21 +1,9 @@ mData = $data; + } + + function isLOB() { + return false; + } + + function data() { + return $this->mData; + } +}; + +/** + * Utility class. + * @addtogroup Database + */ +class MySQLField { + private $name, $tablename, $default, $max_length, $nullable, + $is_pk, $is_unique, $is_key, $type; + function __construct ($info) { + $this->name = $info->name; + $this->tablename = $info->table; + $this->default = $info->def; + $this->max_length = $info->max_length; + $this->nullable = !$info->not_null; + $this->is_pk = $info->primary_key; + $this->is_unique = $info->unique_key; + $this->is_multiple = $info->multiple_key; + $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple); + $this->type = $info->type; + } + + function name() { + return $this->name; + } + + function tableName() { + return $this->tableName; + } + + function defaultValue() { + return $this->default; + } + + function maxLength() { + return $this->max_length; + } + + function nullable() { + return $this->nullable; + } + + function isKey() { + return $this->is_key; + } + + function isMultipleKey() { + return $this->is_multiple; + } + + function type() { + return $this->type; + } +} + +/****************************************************************************** + * Error classes + *****************************************************************************/ + +/** + * Database error base class + * @addtogroup Database + */ +class DBError extends MWException { + public $db; + + /** + * Construct a database error + * @param Database $db The database object which threw the error + * @param string $error A simple error message to be used for debugging + */ + function __construct( Database &$db, $error ) { + $this->db =& $db; + parent::__construct( $error ); + } +} + +/** + * @addtogroup Database + */ +class DBConnectionError extends DBError { + public $error; + + function __construct( Database &$db, $error = 'unknown error' ) { + $msg = 'DB connection error'; + if ( trim( $error ) != '' ) { + $msg .= ": $error"; + } + $this->error = $error; + parent::__construct( $db, $msg ); + } + + function useOutputPage() { + // Not likely to work + return false; + } + + function useMessageCache() { + // Not likely to work + return false; + } + + function getText() { + return $this->getMessage() . "\n"; + } + + function getLogMessage() { + # Don't send to the exception log + return false; + } + + function getPageTitle() { + global $wgSitename; + return "$wgSitename has a problem"; + } + + function getHTML() { + global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding; + global $wgSitename, $wgServer, $wgMessageCache; + + # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky. + # Hard coding strings instead. + + $noconnect = "

      Sorry! This site is experiencing technical difficulties.

      Try waiting a few minutes and reloading.

      (Can't contact the database server: $1)

      "; + $mainpage = 'Main Page'; + $searchdisabled = <<$wgSitename search is disabled for performance reasons. You can search via Google in the meantime. +Note that their indexes of $wgSitename content may be out of date.

      ', +EOT; + + $googlesearch = " + +
      + +
      + +\"Google\" + + + + +
      WWW $wgServer
      + + +
      +
      +
      +"; + $cachederror = "The following is a cached copy of the requested page, and may not be up to date. "; + + # No database access + if ( is_object( $wgMessageCache ) ) { + $wgMessageCache->disable(); + } + + if ( trim( $this->error ) == '' ) { + $this->error = $this->db->getProperty('mServer'); + } + + $text = str_replace( '$1', $this->error, $noconnect ); + $text .= wfGetSiteNotice(); + + if($wgUseFileCache) { + if($wgTitle) { + $t =& $wgTitle; + } else { + if($title) { + $t = Title::newFromURL( $title ); + } elseif (@/**/$_REQUEST['search']) { + $search = $_REQUEST['search']; + return $searchdisabled . + str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ), + $wgInputEncoding ), $googlesearch ); + } else { + $t = Title::newFromText( $mainpage ); + } + } + + $cache = new HTMLFileCache( $t ); + if( $cache->isFileCached() ) { + // @todo, FIXME: $msg is not defined on the next line. + $msg = '

      '.$msg."
      \n" . + $cachederror . "

      \n"; + + $tag = '
      '; + $text = str_replace( + $tag, + $tag . $msg, + $cache->fetchPageText() ); + } + } + + return $text; + } +} + +/** + * @addtogroup Database + */ +class DBQueryError extends DBError { + public $error, $errno, $sql, $fname; + + function __construct( Database &$db, $error, $errno, $sql, $fname ) { + $message = "A database error has occurred\n" . + "Query: $sql\n" . + "Function: $fname\n" . + "Error: $errno $error\n"; + + parent::__construct( $db, $message ); + $this->error = $error; + $this->errno = $errno; + $this->sql = $sql; + $this->fname = $fname; + } + + function getText() { + if ( $this->useMessageCache() ) { + return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ), + htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n"; + } else { + return $this->getMessage(); + } + } + + function getSQL() { + global $wgShowSQLErrors; + if( !$wgShowSQLErrors ) { + return $this->msg( 'sqlhidden', 'SQL hidden' ); + } else { + return $this->sql; + } + } + + function getLogMessage() { + # Don't send to the exception log + return false; + } + + function getPageTitle() { + return $this->msg( 'databaseerror', 'Database error' ); + } + + function getHTML() { + if ( $this->useMessageCache() ) { + return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ), + htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ); + } else { + return nl2br( htmlspecialchars( $this->getMessage() ) ); + } + } +} + +/** + * @addtogroup Database + */ +class DBUnexpectedError extends DBError {} + +/******************************************************************************/ + /** * Database abstraction object - * @package MediaWiki + * @addtogroup Database */ class Database { #------------------------------------------------------------------------------ # Variables #------------------------------------------------------------------------------ - /**#@+ - * @access private - */ - var $mLastQuery = ''; - - var $mServer, $mUser, $mPassword, $mConn = null, $mDBname; - var $mOut, $mOpened = false; - - var $mFailFunction; - var $mTablePrefix; - var $mFlags; - var $mTrxLevel = 0; - var $mErrorCount = 0; - /**#@-*/ + + protected $mLastQuery = ''; + + protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname; + protected $mOut, $mOpened = false; + + protected $mFailFunction; + protected $mTablePrefix; + protected $mFlags; + protected $mTrxLevel = 0; + protected $mErrorCount = 0; + protected $mLBInfo = array(); #------------------------------------------------------------------------------ # Accessors #------------------------------------------------------------------------------ # These optionally set a variable and return the previous state - + /** * Fail function, takes a Database as a parameter * Set to false for default, 1 for ignore errors */ - function failFunction( $function = NULL ) { - return wfSetVar( $this->mFailFunction, $function ); + function failFunction( $function = NULL ) { + return wfSetVar( $this->mFailFunction, $function ); } - + /** * Output page, used for reporting errors * FALSE means discard output */ - function &setOutputPage( &$out ) { - $this->mOut =& $out; + function setOutputPage( $out ) { + $this->mOut = $out; } - + /** * Boolean, controls output of large amounts of debug information */ - function debug( $debug = NULL ) { - return wfSetBit( $this->mFlags, DBO_DEBUG, $debug ); + function debug( $debug = NULL ) { + return wfSetBit( $this->mFlags, DBO_DEBUG, $debug ); } - + /** * Turns buffering of SQL result sets on (true) or off (false). * Default is "on" and it should not be changed without good reasons. @@ -83,7 +351,7 @@ if ( is_null( $buffer ) ) { return !(bool)( $this->mFlags & DBO_NOBUFFER ); } else { - return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer ); + return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer ); } } @@ -91,28 +359,110 @@ * Turns on (false) or off (true) the automatic generation and sending * of a "we're sorry, but there has been a database error" page on * database errors. Default is on (false). When turned off, the - * code should use wfLastErrno() and wfLastError() to handle the + * code should use lastErrno() and lastError() to handle the * situation as appropriate. */ - function ignoreErrors( $ignoreErrors = NULL ) { - return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors ); + function ignoreErrors( $ignoreErrors = NULL ) { + return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors ); } - + /** * The current depth of nested transactions - * @param integer $level + * @param $level Integer: , default NULL. */ function trxLevel( $level = NULL ) { return wfSetVar( $this->mTrxLevel, $level ); } - /** + /** * Number of errors logged, only useful when errors are ignored */ function errorCount( $count = NULL ) { return wfSetVar( $this->mErrorCount, $count ); } + /** + * Properties passed down from the server info array of the load balancer + */ + function getLBInfo( $name = NULL ) { + if ( is_null( $name ) ) { + return $this->mLBInfo; + } else { + if ( array_key_exists( $name, $this->mLBInfo ) ) { + return $this->mLBInfo[$name]; + } else { + return NULL; + } + } + } + + function setLBInfo( $name, $value = NULL ) { + if ( is_null( $value ) ) { + $this->mLBInfo = $name; + } else { + $this->mLBInfo[$name] = $value; + } + } + + /** + * Returns true if this database supports (and uses) cascading deletes + */ + function cascadingDeletes() { + return false; + } + + /** + * Returns true if this database supports (and uses) triggers (e.g. on the page table) + */ + function cleanupTriggers() { + return false; + } + + /** + * Returns true if this database is strict about what can be put into an IP field. + * Specifically, it uses a NULL value instead of an empty string. + */ + function strictIPs() { + return false; + } + + /** + * Returns true if this database uses timestamps rather than integers + */ + function realTimestamps() { + return false; + } + + /** + * Returns true if this database does an implicit sort when doing GROUP BY + */ + function implicitGroupby() { + return true; + } + + /** + * Returns true if this database does an implicit order by when the column has an index + * For example: SELECT page_title FROM page LIMIT 1 + */ + function implicitOrderby() { + return true; + } + + /** + * Returns true if this database can do a native search on IP columns + * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32'; + */ + function searchableIPs() { + return false; + } + + /** + * Returns true if this database can use functional indexes + */ + function functionalIndexes() { + return false; + } + /**#@+ * Get function */ @@ -120,25 +470,42 @@ function isOpen() { return $this->mOpened; } /**#@-*/ + function setFlag( $flag ) { + $this->mFlags |= $flag; + } + + function clearFlag( $flag ) { + $this->mFlags &= ~$flag; + } + + function getFlag( $flag ) { + return !!($this->mFlags & $flag); + } + + /** + * General read-only accessor + */ + function getProperty( $name ) { + return $this->$name; + } + #------------------------------------------------------------------------------ # Other functions #------------------------------------------------------------------------------ - /**#@+ + /**@{{ + * Constructor. * @param string $server database server host * @param string $user database user name * @param string $password database user password * @param string $dbname database name - */ - - /** * @param failFunction * @param $flags - * @param string $tablePrefix Database table prefixes. By default use the prefix gave in LocalSettings.php + * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php */ - function Database( $server = false, $user = false, $password = false, $dbName = false, + function __construct( $server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) { - + global $wgOut, $wgDBprefix, $wgCommandLineMode; # Can't get a reference if it hasn't been set yet if ( !isset( $wgOut ) ) { @@ -148,7 +515,7 @@ $this->mFailFunction = $failFunction; $this->mFlags = $flags; - + if ( $this->mFlags & DBO_DEFAULT ) { if ( $wgCommandLineMode ) { $this->mFlags &= ~DBO_TRX; @@ -163,7 +530,7 @@ $this->mFlags |= DBO_PERSISTENT; $this->mFlags &= ~DBO_TRX; }*/ - + /** Get the default table prefix*/ if ( $tablePrefix == 'get from global' ) { $this->mTablePrefix = $wgDBprefix; @@ -175,65 +542,80 @@ $this->open( $server, $user, $password, $dbName ); } } - + /** * @static * @param failFunction * @param $flags */ - function newFromParams( $server, $user, $password, $dbName, - $failFunction = false, $flags = 0 ) + static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 ) { return new Database( $server, $user, $password, $dbName, $failFunction, $flags ); } - + /** * Usually aborts on failure * If the failFunction is set to a non-zero integer, returns success */ function open( $server, $user, $password, $dbName ) { + global $wguname; + wfProfileIn( __METHOD__ ); + # Test for missing mysql.so # First try to load it if (!@extension_loaded('mysql')) { @dl('mysql.so'); } + # Fail now # Otherwise we get a suppressed fatal error, which is very hard to track down if ( !function_exists( 'mysql_connect' ) ) { - die( "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" ); + throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" ); } - + $this->close(); $this->mServer = $server; $this->mUser = $user; $this->mPassword = $password; $this->mDBname = $dbName; - + $success = false; + + wfProfileIn("dbconnect-$server"); - if ( $this->mFlags & DBO_PERSISTENT ) { - @/**/$this->mConn = mysql_pconnect( $server, $user, $password ); - } else { - # Create a new connection... - if( version_compare( PHP_VERSION, '4.2.0', 'ge' ) ) { - @/**/$this->mConn = mysql_connect( $server, $user, $password, true ); + # LIVE PATCH by Tim, ask Domas for why: retry loop + $this->mConn = false; + $max = 3; + for ( $i = 0; $i < $max && !$this->mConn; $i++ ) { + if ( $i > 1 ) { + usleep( 1000 ); + } + if ( $this->mFlags & DBO_PERSISTENT ) { + @/**/$this->mConn = mysql_pconnect( $server, $user, $password ); } else { - # On PHP 4.1 the new_link parameter is not available. We cannot - # guarantee that we'll actually get a new connection, and this - # may cause some operations to fail possibly. - @/**/$this->mConn = mysql_connect( $server, $user, $password ); + # Create a new connection... + @/**/$this->mConn = mysql_connect( $server, $user, $password, true ); + } + if ($this->mConn === false) { + #$iplus = $i + 1; + #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); } } + + wfProfileOut("dbconnect-$server"); if ( $dbName != '' ) { if ( $this->mConn !== false ) { $success = @/**/mysql_select_db( $dbName, $this->mConn ); if ( !$success ) { - wfDebug( "Error selecting database \"$dbName\": " . $this->lastError() . "\n" ); + $error = "Error selecting database $dbName on server {$this->mServer} " . + "from client host {$wguname['nodename']}\n"; + wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n"); + wfDebug( $error ); } } else { wfDebug( "DB connection error\n" ); - wfDebug( "Server: $server, User: $user, Password: " . + wfDebug( "Server: $server, User: $user, Password: " . substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" ); $success = false; } @@ -241,23 +623,31 @@ # Delay USE query $success = (bool)$this->mConn; } - - if ( !$success ) { + + if ( $success ) { + $version = $this->getServerVersion(); + if ( version_compare( $version, '4.1' ) >= 0 ) { + // Tell the server we're communicating with it in UTF-8. + // This may engage various charset conversions. + global $wgDBmysql5; + if( $wgDBmysql5 ) { + $this->query( 'SET NAMES utf8', __METHOD__ ); + } + // Turn off strict mode + $this->query( "SET sql_mode = ''", __METHOD__ ); + } + + // Turn off strict mode if it is on + } else { $this->reportConnectionError(); } - - global $wgDBmysql5; - if( $wgDBmysql5 ) { - // Tell the server we're communicating with it in UTF-8. - // This may engage various charset conversions. - $this->query( 'SET NAMES utf8' ); - } - + $this->mOpened = $success; + wfProfileOut( __METHOD__ ); return $success; } - /**#@-*/ - + /**@}}*/ + /** * Closes a database connection. * if it is open : commits any open transactions @@ -276,48 +666,85 @@ return true; } } - + /** - * @access private - * @param string $msg error message ? + * @param string $error fallback error message, used if none is given by MySQL */ - function reportConnectionError() { + function reportConnectionError( $error = 'Unknown error' ) { + $myError = $this->lastError(); + if ( $myError ) { + $error = $myError; + } + if ( $this->mFailFunction ) { + # Legacy error handling method if ( !is_int( $this->mFailFunction ) ) { $ff = $this->mFailFunction; - $ff( $this, mysql_error() ); + $ff( $this, $error ); } } else { - wfEmergencyAbort( $this, $this->lastError() ); + # New method + wfLogDBError( "Connection error: $error\n" ); + throw new DBConnectionError( $this, $error ); } } - + /** - * Usually aborts on failure - * If errors are explicitly ignored, returns success + * Usually aborts on failure. If errors are explicitly ignored, returns success. + * + * @param $sql String: SQL query + * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST + * comment (you can use __METHOD__ or add some extra info) + * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors... + * maybe best to catch the exception instead? + * @return true for a successful write query, ResultWrapper object for a successful read query, + * or false on failure if $tempIgnore set + * @throws DBQueryError Thrown when the database returns an error of any kind */ - function query( $sql, $fname = '', $tempIgnore = false ) { - global $wgProfiling, $wgCommandLineMode; + public function query( $sql, $fname = '', $tempIgnore = false ) { + global $wgProfiling; if ( $wgProfiling ) { # generalizeSQL will probably cut down the query to reasonable # logging size most of the time. The substr is really just a sanity check. - $profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 ); - wfProfileIn( 'Database::query' ); - wfProfileIn( $profName ); + + # Who's been wasting my precious column space? -- TS + #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 ); + + if ( is_null( $this->getLBInfo( 'master' ) ) ) { + $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 ); + $totalProf = 'Database::query'; + } else { + $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 ); + $totalProf = 'Database::query-master'; + } + wfProfileIn( $totalProf ); + wfProfileIn( $queryProf ); } - + $this->mLastQuery = $sql; - + # Add a comment for easy SHOW PROCESSLIST interpretation - if ( $fname ) { - $commentedSql = "/* $fname */ $sql"; - } else { - $commentedSql = $sql; - } + #if ( $fname ) { + global $wgUser; + if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) { + $userName = $wgUser->getName(); + if ( strlen( $userName ) > 15 ) { + $userName = substr( $userName, 0, 15 ) . '...'; + } + $userName = str_replace( '/', '', $userName ); + } else { + $userName = ''; + } + $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1); + #} else { + # $commentedSql = $sql; + #} # If DBO_TRX is set, start a transaction - if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && $sql != 'BEGIN' ) { + if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && + $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' + ) { $this->begin(); } @@ -326,7 +753,7 @@ $sqlx = strtr( $sqlx, "\t\n", ' ' ); wfDebug( "SQL: $sqlx\n" ); } - + # Do the query and handle errors $ret = $this->doQuery( $commentedSql ); @@ -337,6 +764,11 @@ wfDebug( "Connection lost, reconnecting...\n" ); if ( $this->ping() ) { wfDebug( "Reconnected\n" ); + $sqlx = substr( $commentedSql, 0, 500 ); + $sqlx = strtr( $sqlx, "\t\n", ' ' ); + global $wgRequestTime; + $elapsed = round( microtime(true) - $wgRequestTime, 3 ); + wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" ); $ret = $this->doQuery( $commentedSql ); } else { wfDebug( "Failed\n" ); @@ -346,24 +778,26 @@ if ( false === $ret ) { $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore ); } - + if ( $wgProfiling ) { - wfProfileOut( $profName ); - wfProfileOut( 'Database::query' ); + wfProfileOut( $queryProf ); + wfProfileOut( $totalProf ); } - return $ret; + return $this->resultObject( $ret ); } - + /** * The DBMS-dependent part of query() - * @param string $sql SQL query. + * @param $sql String: SQL query. + * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure + * @access private */ - function doQuery( $sql ) { + /*private*/ function doQuery( $sql ) { if( $this->bufferResults() ) { $ret = mysql_query( $sql, $this->mConn ); } else { $ret = mysql_unbuffered_query( $sql, $this->mConn ); - } + } return $ret; } @@ -375,32 +809,20 @@ * @param bool $tempIgnore */ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - global $wgCommandLineMode, $wgFullyInitialised; + global $wgCommandLineMode; # Ignore errors during error handling to avoid infinite recursion $ignore = $this->ignoreErrors( true ); - $this->mErrorCount ++; + ++$this->mErrorCount; if( $ignore || $tempIgnore ) { - wfDebug("SQL ERROR (ignored): " . $error . "\n"); + wfDebug("SQL ERROR (ignored): $error\n"); + $this->ignoreErrors( $ignore ); } else { $sql1line = str_replace( "\n", "\\n", $sql ); wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n"); wfDebug("SQL ERROR: " . $error . "\n"); - if ( $wgCommandLineMode || !$this->mOut || empty( $wgFullyInitialised ) ) { - $message = "A database error has occurred\n" . - "Query: $sql\n" . - "Function: $fname\n" . - "Error: $errno $error\n"; - if ( !$wgCommandLineMode ) { - $message = nl2br( $message ); - } - wfDebugDieBacktrace( $message ); - } else { - // this calls wfAbruptExit() - $this->mOut->databaseError( $fname, $sql, $error, $errno ); - } + throw new DBQueryError( $this, $error, $errno, $sql, $fname ); } - $this->ignoreErrors( $ignore ); } @@ -419,11 +841,11 @@ the bits later. */ return array( 'query' => $sql, 'func' => $func ); } - + function freePrepared( $prepared ) { /* No-op for MySQL */ } - + /** * Execute a prepared query with the various arguments * @param string $prepared the prepared sql @@ -438,7 +860,7 @@ $sql = $this->fillPrepared( $prepared['query'], $args ); return $this->query( $sql, $prepared['func'] ); } - + /** * Prepare & execute an SQL statement, quoting and inserting arguments * in the appropriate places. @@ -456,7 +878,7 @@ $this->freePrepared( $prepared ); return $retval; } - + /** * For faking prepared SQL statements on DBs that don't support * it directly. @@ -465,21 +887,20 @@ * @return string executable SQL */ function fillPrepared( $preparedQuery, $args ) { - $n = 0; reset( $args ); $this->preparedArgs =& $args; return preg_replace_callback( '/(\\\\[?!&]|[?!&])/', array( &$this, 'fillPreparedArg' ), $preparedQuery ); } - + /** * preg_callback func for fillPrepared() * The arguments should be in $this->preparedArgs and must not be touched * while we're doing this. - * + * * @param array $matches * @return string - * @access private + * @private */ function fillPreparedArg( $matches ) { switch( $matches[1] ) { @@ -487,18 +908,18 @@ case '\\!': return '!'; case '\\&': return '&'; } - list( $n, $arg ) = each( $this->preparedArgs ); + list( /* $n */ , $arg ) = each( $this->preparedArgs ); switch( $matches[1] ) { case '?': return $this->addQuotes( $arg ); case '!': return $arg; case '&': # return $this->addQuotes( file_get_contents( $arg ) ); - wfDebugDieBacktrace( '& mode is not implemented. If it\'s really needed, uncomment the line above.' ); + throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' ); default: - wfDebugDieBacktrace( 'Received invalid match. This should never happen!' ); + throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' ); } } - + /**#@+ * @param mixed $res A SQL result */ @@ -506,56 +927,89 @@ * Free a result object */ function freeResult( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } if ( !@/**/mysql_free_result( $res ) ) { - wfDebugDieBacktrace( "Unable to free MySQL result\n" ); + throw new DBUnexpectedError( $this, "Unable to free MySQL result" ); } } - + /** - * Fetch the next row from the given result object, in object form + * Fetch the next row from the given result object, in object form. + * Fields can be retrieved with $row->fieldname, with fields acting like + * member variables. + * + * @param $res SQL result object as returned from Database::query(), etc. + * @return MySQL row object + * @throws DBUnexpectedError Thrown if the database returns an error */ function fetchObject( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } @/**/$row = mysql_fetch_object( $res ); - if( mysql_errno() ) { - wfDebugDieBacktrace( 'Error in fetchObject(): ' . htmlspecialchars( mysql_error() ) ); + if( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); } return $row; } /** - * Fetch the next row from the given result object - * Returns an array + * Fetch the next row from the given result object, in associative array + * form. Fields are retrieved with $row['fieldname']. + * + * @param $res SQL result object as returned from Database::query(), etc. + * @return MySQL row object + * @throws DBUnexpectedError Thrown if the database returns an error */ function fetchRow( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } @/**/$row = mysql_fetch_array( $res ); - if (mysql_errno() ) { - wfDebugDieBacktrace( 'Error in fetchRow(): ' . htmlspecialchars( mysql_error() ) ); + if ( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); } return $row; - } + } /** * Get the number of rows in a result object */ function numRows( $res ) { - @/**/$n = mysql_num_rows( $res ); - if( mysql_errno() ) { - wfDebugDieBacktrace( 'Error in numRows(): ' . htmlspecialchars( mysql_error() ) ); + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + @/**/$n = mysql_num_rows( $res ); + if( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) ); } return $n; } - + /** * Get the number of fields in a result object * See documentation for mysql_num_fields() */ - function numFields( $res ) { return mysql_num_fields( $res ); } + function numFields( $res ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return mysql_num_fields( $res ); + } /** * Get a field name in a result object - * See documentation for mysql_field_name() + * See documentation for mysql_field_name(): + * http://www.php.net/mysql_field_name */ - function fieldName( $res, $n ) { return mysql_field_name( $res, $n ); } + function fieldName( $res, $n ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return mysql_field_name( $res, $n ); + } /** * Get the inserted value of an auto-increment row @@ -568,30 +1022,35 @@ * $id = $dbw->insertId(); */ function insertId() { return mysql_insert_id( $this->mConn ); } - + /** * Change the position of the cursor in a result object * See mysql_data_seek() */ - function dataSeek( $res, $row ) { return mysql_data_seek( $res, $row ); } - + function dataSeek( $res, $row ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + return mysql_data_seek( $res, $row ); + } + /** * Get the last error number * See mysql_errno() */ - function lastErrno() { + function lastErrno() { if ( $this->mConn ) { - return mysql_errno( $this->mConn ); + return mysql_errno( $this->mConn ); } else { return mysql_errno(); } } - + /** * Get a description of the last error * See mysql_error() for more details */ - function lastError() { + function lastError() { if ( $this->mConn ) { # Even if it's non-zero, it can still be invalid wfSuppressWarnings(); @@ -607,7 +1066,7 @@ $error .= ' (' . $this->mServer . ')'; } return $error; - } + } /** * Get the number of rows affected by the last write query * See mysql_affected_rows() for more details @@ -620,7 +1079,7 @@ * Usually aborts on failure * If errors are explicitly ignored, returns success * - * This function exists for historical reasons, Database::update() has a more standard + * This function exists for historical reasons, Database::update() has a more standard * calling convention and feature set */ function set( $table, $var, $value, $cond, $fname = 'Database::set' ) @@ -630,7 +1089,7 @@ $this->strencode( $value ) . "' WHERE ($cond)"; return (bool)$this->query( $sql, $fname ); } - + /** * Simple SELECT wrapper, returns a single field, input must be encoded * Usually aborts on failure @@ -654,71 +1113,113 @@ return false; } } - + /** * Returns an optional USE INDEX clause to go after the table, and a * string to go at the end of the query * - * @access private + * @private * * @param array $options an associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return array */ function makeSelectOptions( $options ) { - $tailOpts = ''; + $preLimitTail = $postLimitTail = ''; + $startOpts = ''; - if ( isset( $options['GROUP BY'] ) ) { - $tailOpts .= " GROUP BY {$options['GROUP BY']}"; - } - if ( isset( $options['ORDER BY'] ) ) { - $tailOpts .= " ORDER BY {$options['ORDER BY']}"; - } - if ( isset( $options['LIMIT'] ) ) { - $tailOpts .= " LIMIT {$options['LIMIT']}"; + $noKeyOptions = array(); + foreach ( $options as $key => $option ) { + if ( is_numeric( $key ) ) { + $noKeyOptions[$option] = true; + } } - if ( is_numeric( array_search( 'FOR UPDATE', $options ) ) ) { - $tailOpts .= ' FOR UPDATE'; - } - - if ( is_numeric( array_search( 'LOCK IN SHARE MODE', $options ) ) ) { - $tailOpts .= ' LOCK IN SHARE MODE'; - } + if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; + if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}"; + if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; + + //if (isset($options['LIMIT'])) { + // $tailOpts .= $this->limitResult('', $options['LIMIT'], + // isset($options['OFFSET']) ? $options['OFFSET'] + // : false); + //} + + if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE'; + if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE'; + if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; + + # Various MySQL extensions + if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */'; + if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY'; + if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT'; + if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT'; + if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT'; + if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS'; + if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE'; + if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE'; - if ( isset( $options['USE INDEX'] ) ) { + if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) { $useIndex = $this->useIndexClause( $options['USE INDEX'] ); } else { $useIndex = ''; } - return array( $useIndex, $tailOpts ); + + return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); } /** * SELECT wrapper + * + * @param mixed $table Array or string, table name(s) (prefix auto-added) + * @param mixed $vars Array or string, field name(s) to be retrieved + * @param mixed $conds Array or string, condition(s) for WHERE + * @param string $fname Calling function name (use __METHOD__) for logs/profiling + * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), + * see Database::makeSelectOptions code for list of supported stuff + * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure */ function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() ) { if( is_array( $vars ) ) { $vars = implode( ',', $vars ); } + if( !is_array( $options ) ) { + $options = array( $options ); + } if( is_array( $table ) ) { - $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) ); + if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) ) + $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] ); + else + $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) ); } elseif ($table!='') { - $from = ' FROM ' .$this->tableName( $table ); + if ($table{0}==' ') { + $from = ' FROM ' . $table; + } else { + $from = ' FROM ' . $this->tableName( $table ); + } } else { $from = ''; } - list( $useIndex, $tailOpts ) = $this->makeSelectOptions( (array)$options ); - + list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options ); + if( !empty( $conds ) ) { if ( is_array( $conds ) ) { $conds = $this->makeList( $conds, LIST_AND ); } - $sql = "SELECT $vars $from $useIndex WHERE $conds $tailOpts"; + $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail"; } else { - $sql = "SELECT $vars $from $useIndex $tailOpts"; + $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail"; + } + + if (isset($options['LIMIT'])) + $sql = $this->limitResult($sql, $options['LIMIT'], + isset($options['OFFSET']) ? $options['OFFSET'] : false); + $sql = "$sql $postLimitTail"; + + if (isset($options['EXPLAIN'])) { + $sql = 'EXPLAIN ' . $sql; } return $this->query( $sql, $fname ); } @@ -726,9 +1227,9 @@ /** * Single row SELECT wrapper * Aborts or returns FALSE on error - * + * * $vars: the selected variables - * $conds: a condition map, terms are ANDed together. + * $conds: a condition map, terms are ANDed together. * Items with numeric keys are taken to be literal conditions * Takes an array of selected variables, and a condition map, which is ANDed * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" => @@ -740,15 +1241,45 @@ function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array() ) { $options['LIMIT'] = 1; $res = $this->select( $table, $vars, $conds, $fname, $options ); - if ( $res === false || !$this->numRows( $res ) ) { + if ( $res === false ) + return false; + if ( !$this->numRows($res) ) { + $this->freeResult($res); return false; } $obj = $this->fetchObject( $res ); $this->freeResult( $res ); return $obj; + + } + + /** + * Estimate rows in dataset + * Returns estimated count, based on EXPLAIN output + * Takes same arguments as Database::select() + */ + + function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) { + $options['EXPLAIN']=true; + $res = $this->select ($table, $vars, $conds, $fname, $options ); + if ( $res === false ) + return false; + if (!$this->numRows($res)) { + $this->freeResult($res); + return 0; + } + $rows=1; + + while( $plan = $this->fetchObject( $res ) ) { + $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero + } + + $this->freeResult($res); + return $rows; } + /** * Removes most variables from an SQL query and replaces them with X or N for numbers. * It's only slightly flawed. Don't use for anything important. @@ -756,26 +1287,26 @@ * @param string $sql A SQL Query * @static */ - function generalizeSQL( $sql ) { + static function generalizeSQL( $sql ) { # This does the same as the regexp below would do, but in such a way # as to avoid crashing php on some large strings. # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql); - + $sql = str_replace ( "\\\\", '', $sql); $sql = str_replace ( "\\'", '', $sql); $sql = str_replace ( "\\\"", '', $sql); $sql = preg_replace ("/'.*'/s", "'X'", $sql); $sql = preg_replace ('/".*"/s', "'X'", $sql); - + # All newlines, tabs, etc replaced by single space - $sql = preg_replace ( "/\s+/", ' ', $sql); - - # All numbers => N + $sql = preg_replace ( '/\s+/', ' ', $sql); + + # All numbers => N $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql); - + return $sql; } - + /** * Determines whether a field exists in a table * Usually aborts on failure @@ -787,9 +1318,9 @@ if ( !$res ) { return NULL; } - + $found = false; - + while ( $row = $this->fetchObject( $res ) ) { if ( $row->Field == $field ) { $found = true; @@ -798,7 +1329,7 @@ } return $found; } - + /** * Determines whether an index exists * Usually aborts on failure @@ -812,8 +1343,8 @@ return $info !== false; } } - - + + /** * Get information about an index into an object * Returns false if the index does not exist @@ -828,15 +1359,18 @@ if ( !$res ) { return NULL; } - + + $result = array(); while ( $row = $this->fetchObject( $res ) ) { if ( $row->Key_name == $index ) { - return $row; + $result[] = $row; } } - return false; + $this->freeResult($res); + + return empty($result) ? false : $result; } - + /** * Query whether a given table exists */ @@ -863,20 +1397,23 @@ function fieldInfo( $table, $field ) { $table = $this->tableName( $table ); $res = $this->query( "SELECT * FROM $table LIMIT 1" ); - $n = mysql_num_fields( $res ); + $n = mysql_num_fields( $res->result ); for( $i = 0; $i < $n; $i++ ) { - $meta = mysql_fetch_field( $res, $i ); + $meta = mysql_fetch_field( $res->result, $i ); if( $field == $meta->name ) { - return $meta; + return new MySQLField($meta); } } return false; } - + /** * mysql_field_type() wrapper */ function fieldType( $res, $index ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } return mysql_field_type( $res, $index ); } @@ -888,13 +1425,13 @@ if ( !$indexInfo ) { return NULL; } - return !$indexInfo->Non_unique; + return !$indexInfo[0]->Non_unique; } /** * INSERT wrapper, inserts an array into a table * - * $a may be a single associative array, or an array of these with numeric keys, for + * $a may be a single associative array, or an array of these with numeric keys, for * multi-row insert. * * Usually aborts on failure @@ -918,7 +1455,7 @@ $keys = array_keys( $a ); } - $sql = 'INSERT ' . implode( ' ', $options ) . + $sql = 'INSERT ' . implode( ' ', $options ) . " INTO $table (" . implode( ',', $keys ) . ') VALUES '; if ( $multi ) { @@ -940,32 +1477,33 @@ /** * Make UPDATE options for the Database::update function * - * @access private + * @private * @param array $options The options passed to Database::update * @return string */ function makeUpdateOptions( $options ) { if( !is_array( $options ) ) { - wfDebugDieBacktrace( 'makeUpdateOptions given non-array' ); + $options = array( $options ); } $opts = array(); if ( in_array( 'LOW_PRIORITY', $options ) ) $opts[] = $this->lowPriorityOption(); - if ( in_array( 'IGNORE', $options ) ) + if ( in_array( 'IGNORE', $options ) ) $opts[] = 'IGNORE'; return implode(' ', $opts); } - + /** * UPDATE wrapper, takes a condition array and a SET array * * @param string $table The table to UPDATE * @param array $values An array of values to SET - * @param array $conds An array of conditions (WHERE) + * @param array $conds An array of conditions (WHERE). Use '*' to update all rows. * @param string $fname The Class::Function calling this function * (for the log) * @param array $options An array of UPDATE options, can be one or * more of IGNORE, LOW_PRIORITY + * @return bool */ function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) { $table = $this->tableName( $table ); @@ -974,19 +1512,21 @@ if ( $conds != '*' ) { $sql .= " WHERE " . $this->makeList( $conds, LIST_AND ); } - $this->query( $sql, $fname ); + return $this->query( $sql, $fname ); } - + /** - * Makes a wfStrencoded list from an array - * $mode: LIST_COMMA - comma separated, no field names + * Makes an encoded list of strings from an array + * $mode: + * LIST_COMMA - comma separated, no field names * LIST_AND - ANDed WHERE clause (without the WHERE) + * LIST_OR - ORed WHERE clause (without the WHERE) * LIST_SET - comma separated with field names, like a SET clause - * LIST_NAMES - comma separated field names + * LIST_NAMES - comma separated field names */ function makeList( $a, $mode = LIST_COMMA ) { if ( !is_array( $a ) ) { - wfDebugDieBacktrace( 'Database::makeList called with incorrect parameters' ); + throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' ); } $first = true; @@ -995,18 +1535,29 @@ if ( !$first ) { if ( $mode == LIST_AND ) { $list .= ' AND '; + } elseif($mode == LIST_OR) { + $list .= ' OR '; } else { $list .= ','; } } else { $first = false; } - if ( $mode == LIST_AND && is_numeric( $field ) ) { + if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) { $list .= "($value)"; - } elseif ( $mode == LIST_AND && is_array ($value) ) { + } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) { + $list .= "$value"; + } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) { $list .= $field." IN (".$this->makeList($value).") "; + } elseif( is_null($value) ) { + if ( $mode == LIST_AND || $mode == LIST_OR ) { + $list .= "$field IS "; + } elseif ( $mode == LIST_SET ) { + $list .= "$field = "; + } + $list .= 'NULL'; } else { - if ( $mode == LIST_AND || $mode == LIST_SET ) { + if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) { $list .= "$field = "; } $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value ); @@ -1014,7 +1565,7 @@ } return $list; } - + /** * Change the current database */ @@ -1024,33 +1575,14 @@ } /** - * Starts a timer which will kill the DB thread after $timeout seconds - */ - function startTimer( $timeout ) { - global $IP; - if( function_exists( 'mysql_thread_id' ) ) { - # This will kill the query if it's still running after $timeout seconds. - $tid = mysql_thread_id( $this->mConn ); - exec( "php $IP/includes/killthread.php $timeout $tid &>/dev/null &" ); - } - } - - /** - * Stop a timer started by startTimer() - * Currently unimplemented. - * - */ - function stopTimer() { } - - /** * Format a table name ready for use in constructing an SQL query - * - * This does two important things: it quotes table names which as necessary, + * + * This does two important things: it quotes table names which as necessary, * and it adds a table prefix if there is one. - * - * All functions of this object which require a table name call this function + * + * All functions of this object which require a table name call this function * themselves. Pass the canonical name to such functions. This is only needed - * when calling query() directly. + * when calling query() directly. * * @param string $name database table name */ @@ -1058,7 +1590,7 @@ global $wgSharedDB; # Skip quoted literals if ( $name{0} != '`' ) { - if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) { + if ( $this->mTablePrefix !== '' && strpos( $name, '.' ) === false ) { $name = "{$this->mTablePrefix}$name"; } if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) { @@ -1067,7 +1599,7 @@ # Standard quoting $name = "`$name`"; } - } + } return $name; } @@ -1077,10 +1609,10 @@ * * Example: * extract($dbr->tableNames('user','watchlist')); - * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user + * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user * WHERE wl_user=user_id AND wl_user=$nameWithQuotes"; */ - function tableNames() { + public function tableNames() { $inArray = func_get_args(); $retVal = array(); foreach ( $inArray as $name ) { @@ -1090,12 +1622,45 @@ } /** + * Fetch a number of table names into an zero-indexed numerical array + * This is handy when you need to construct SQL for joins + * + * Example: + * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist'); + * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user + * WHERE wl_user=user_id AND wl_user=$nameWithQuotes"; + */ + public function tableNamesN() { + $inArray = func_get_args(); + $retVal = array(); + foreach ( $inArray as $name ) { + $retVal[] = $this->tableName( $name ); + } + return $retVal; + } + + /** + * @private + */ + function tableNamesWithUseIndex( $tables, $use_index ) { + $ret = array(); + + foreach ( $tables as $table ) + if ( @$use_index[$table] !== null ) + $ret[] = $this->tableName( $table ) . ' ' . $this->useIndexClause( implode( ',', (array)$use_index[$table] ) ); + else + $ret[] = $this->tableName( $table ); + + return implode( ',', $ret ); + } + + /** * Wrapper for addslashes() * @param string $s String to be slashed. * @return string slashed string. */ function strencode( $s ) { - return addslashes( $s ); + return mysql_real_escape_string( $s, $this->mConn ); } /** @@ -1111,9 +1676,18 @@ # _are_ strings such as article titles and string->number->string # conversion is not 1:1. return "'" . $this->strencode( $s ) . "'"; - } + } } - + + /** + * Escape string for safe LIKE usage + */ + function escapeLike( $s ) { + $s=$this->strencode( $s ); + $s=str_replace(array('%','_'),array('\%','\_'),$s); + return $s; + } + /** * Returns an appropriately quoted sequence value for inserting a new row. * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL @@ -1128,21 +1702,18 @@ * PostgreSQL doesn't have them and returns "" */ function useIndexClause( $index ) { - global $wgDBmysql4; - return $wgDBmysql4 - ? "FORCE INDEX ($index)" - : "USE INDEX ($index)"; + return "FORCE INDEX ($index)"; } /** * REPLACE query wrapper * PostgreSQL simulates this with a DELETE followed by INSERT * $row is the row to insert, an associative array - * $uniqueIndexes is an array of indexes. Each element may be either a + * $uniqueIndexes is an array of indexes. Each element may be either a * field name or an array of field names - * - * It may be more efficient to leave off unique indexes which are unlikely to collide. - * However if you do this, you run the risk of encountering errors which wouldn't have + * + * It may be more efficient to leave off unique indexes which are unlikely to collide. + * However if you do this, you run the risk of encountering errors which wouldn't have * occurred in MySQL * * @todo migrate comment to phodocumentor format @@ -1172,7 +1743,7 @@ * DELETE where the condition is a join * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects * - * For safety, an empty $conds will not delete everything. If you want to delete all rows where the + * For safety, an empty $conds will not delete everything. If you want to delete all rows where the * join condition matches, set $conds='*' * * DO NOT put the join condition in $conds @@ -1185,7 +1756,7 @@ */ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) { if ( !$conds ) { - wfDebugDieBacktrace( 'Database::deleteJoin() called with empty $conds' ); + throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' ); } $delTable = $this->tableName( $delTable ); @@ -1194,7 +1765,7 @@ if ( $conds != '*' ) { $sql .= ' AND ' . $this->makeList( $conds, LIST_AND ); } - + return $this->query( $sql, $fname ); } @@ -1208,7 +1779,8 @@ $row = $this->fetchObject( $res ); $this->freeResult( $res ); - if ( preg_match( "/\((.*)\)/", $row->Type, $m ) ) { + $m = array(); + if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) { $size = $m[1]; } else { $size = -1; @@ -1230,7 +1802,7 @@ */ function delete( $table, $conds, $fname = 'Database::delete' ) { if ( !$conds ) { - wfDebugDieBacktrace( 'Database::delete() called with no conditions' ); + throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' ); } $table = $this->tableName( $table ); $sql = "DELETE FROM $table"; @@ -1247,33 +1819,49 @@ * $conds may be "*" to copy the whole table * srcTable may be an array of tables. */ - function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect', - $options = array() ) + function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect', + $insertOptions = array(), $selectOptions = array() ) { $destTable = $this->tableName( $destTable ); - if ( is_array( $options ) ) { - $options = implode( ' ', $options ); + if ( is_array( $insertOptions ) ) { + $insertOptions = implode( ' ', $insertOptions ); + } + if( !is_array( $selectOptions ) ) { + $selectOptions = array( $selectOptions ); } + list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions ); if( is_array( $srcTable ) ) { $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) ); - } else { + } else { $srcTable = $this->tableName( $srcTable ); } - $sql = "INSERT $options INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . - ' SELECT ' . implode( ',', $varMap ) . - " FROM $srcTable"; + $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . + " SELECT $startOpts " . implode( ',', $varMap ) . + " FROM $srcTable $useIndex "; if ( $conds != '*' ) { $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); } + $sql .= " $tailOpts"; return $this->query( $sql, $fname ); } /** * Construct a LIMIT query with optional offset * This is used for query pages - */ - function limitResult($limit,$offset) { - return ' LIMIT '.(is_numeric($offset)?"{$offset},":"")."{$limit} "; + * $sql string SQL query we will append the limit too + * $limit integer the SQL limit + * $offset integer the SQL offset (default false) + */ + function limitResult($sql, $limit, $offset=false) { + if( !is_numeric($limit) ) { + throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" ); + } + return " $sql LIMIT " + . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" ) + . "{$limit} "; + } + function limitResultForUpdate($sql, $num) { + return $this->limitResult($sql, $num, 0); } /** @@ -1299,23 +1887,23 @@ /** * Perform a deadlock-prone transaction. * - * This function invokes a callback function to perform a set of write - * queries. If a deadlock occurs during the processing, the transaction + * This function invokes a callback function to perform a set of write + * queries. If a deadlock occurs during the processing, the transaction * will be rolled back and the callback function will be called again. * - * Usage: + * Usage: * $dbw->deadlockLoop( callback, ... ); * - * Extra arguments are passed through to the specified callback function. - * - * Returns whatever the callback function returned on its successful, - * iteration, or false on error, for example if the retry limit was + * Extra arguments are passed through to the specified callback function. + * + * Returns whatever the callback function returned on its successful, + * iteration, or false on error, for example if the retry limit was * reached. */ function deadlockLoop() { $myFname = 'Database::deadlockLoop'; - - $this->query( 'BEGIN', $myFname ); + + $this->begin(); $args = func_get_args(); $function = array_shift( $args ); $oldIgnore = $this->ignoreErrors( true ); @@ -1330,7 +1918,7 @@ $error = $this->lastError(); $errno = $this->lastErrno(); $sql = $this->lastQuery(); - + if ( $errno ) { if ( $this->wasDeadlock() ) { # Retry @@ -1361,11 +1949,11 @@ function masterPosWait( $file, $pos, $timeout ) { $fname = 'Database::masterPosWait'; wfProfileIn( $fname ); - - + + # Commit any open transactions $this->immediateCommit(); - + # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set $encFile = $this->strencode( $file ); $sql = "SELECT MASTER_POS_WAIT('$encFile', $pos, $timeout)"; @@ -1392,7 +1980,7 @@ return array( false, false ); } } - + /** * Get the position of the master from SHOW MASTER STATUS */ @@ -1407,26 +1995,19 @@ } /** - * Begin a transaction, or if a transaction has already started, continue it + * Begin a transaction, committing any previously open transaction */ function begin( $fname = 'Database::begin' ) { - if ( !$this->mTrxLevel ) { - $this->immediateBegin( $fname ); - } else { - $this->mTrxLevel++; - } + $this->query( 'BEGIN', $fname ); + $this->mTrxLevel = 1; } /** - * End a transaction, or decrement the nest level if transactions are nested + * End a transaction */ function commit( $fname = 'Database::commit' ) { - if ( $this->mTrxLevel ) { - $this->mTrxLevel--; - } - if ( !$this->mTrxLevel ) { - $this->immediateCommit( $fname ); - } + $this->query( 'COMMIT', $fname ); + $this->mTrxLevel = 0; } /** @@ -1439,18 +2020,18 @@ /** * Begin a transaction, committing any previously open transaction + * @deprecated use begin() */ function immediateBegin( $fname = 'Database::immediateBegin' ) { - $this->query( 'BEGIN', $fname ); - $this->mTrxLevel = 1; + $this->begin(); } - + /** * Commit transaction, if one is open + * @deprecated use commit() */ function immediateCommit( $fname = 'Database::immediateCommit' ) { - $this->query( 'COMMIT', $fname ); - $this->mTrxLevel = 0; + $this->commit(); } /** @@ -1459,7 +2040,7 @@ function timestamp( $ts=0 ) { return wfTimestamp(TS_MW,$ts); } - + /** * Local database timestamp format or null */ @@ -1470,13 +2051,18 @@ return $this->timestamp( $ts ); } } - + /** * @todo document */ function resultObject( $result ) { if( empty( $result ) ) { - return NULL; + return false; + } elseif ( $result instanceof ResultWrapper ) { + return $result; + } elseif ( $result === true ) { + // Successful write query + return $result; } else { return new ResultWrapper( $this, $result ); } @@ -1488,19 +2074,19 @@ function aggregateValue ($valuedata,$valuename='value') { return $valuename; } - + /** * @return string wikitext of a link to the server software's web site */ function getSoftwareLink() { return "[http://www.mysql.com/ MySQL]"; } - + /** * @return string Version information from the database */ function getServerVersion() { - return mysql_get_server_info(); + return mysql_get_server_info( $this->mConn ); } /** @@ -1514,20 +2100,32 @@ return true; } } - + /** * Get slave lag. * At the moment, this will only work if the DB user has the PROCESS privilege */ function getLag() { $res = $this->query( 'SHOW PROCESSLIST' ); - # Find slave SQL thread. Assumed to be the second one running, which is a bit - # dubious, but unfortunately there's no easy rigorous way - $slaveThreads = 0; + # Find slave SQL thread while ( $row = $this->fetchObject( $res ) ) { - if ( $row->User == 'system user' ) { - if ( ++$slaveThreads == 2 ) { - # This is it, return the time + /* This should work for most situations - when default db + * for thread is not specified, it had no events executed, + * and therefore it doesn't know yet how lagged it is. + * + * Relay log I/O thread does not select databases. + */ + if ( $row->User == 'system user' && + $row->State != 'Waiting for master to send event' && + $row->State != 'Connecting to master' && + $row->State != 'Queueing master event to the relay log' && + $row->State != 'Waiting for master update' && + $row->State != 'Requesting binlog dump' + ) { + # This is it, return the time (except -ve) + if ( $row->Time > 0x7fffffff ) { + return false; + } else { return $row->Time; } } @@ -1538,21 +2136,163 @@ /** * Get status information from SHOW STATUS in an associative array */ - function getStatus() { - $res = $this->query( 'SHOW STATUS' ); + function getStatus($which="%") { + $res = $this->query( "SHOW STATUS LIKE '{$which}'" ); $status = array(); while ( $row = $this->fetchObject( $res ) ) { $status[$row->Variable_name] = $row->Value; } return $status; } -} + + /** + * Return the maximum number of items allowed in a list, or 0 for unlimited. + */ + function maxListLen() { + return 0; + } + + function encodeBlob($b) { + return $b; + } + + function decodeBlob($b) { + return $b; + } + + /** + * Override database's default connection timeout. + * May be useful for very long batch queries such as + * full-wiki dumps, where a single query reads out + * over hours or days. + * @param int $timeout in seconds + */ + public function setTimeout( $timeout ) { + $this->query( "SET net_read_timeout=$timeout" ); + $this->query( "SET net_write_timeout=$timeout" ); + } + + /** + * Read and execute SQL commands from a file. + * Returns true on success, error string on failure + * @param string $filename File name to open + * @param callback $lineCallback Optional function called before reading each line + * @param callback $resultCallback Optional function called for each MySQL result + */ + function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) { + $fp = fopen( $filename, 'r' ); + if ( false === $fp ) { + return "Could not open \"{$filename}\".\n"; + } + $error = $this->sourceStream( $fp, $lineCallback, $resultCallback ); + fclose( $fp ); + return $error; + } + + /** + * Read and execute commands from an open file handle + * Returns true on success, error string on failure + * @param string $fp File handle + * @param callback $lineCallback Optional function called before reading each line + * @param callback $resultCallback Optional function called for each MySQL result + */ + function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) { + $cmd = ""; + $done = false; + $dollarquote = false; + + while ( ! feof( $fp ) ) { + if ( $lineCallback ) { + call_user_func( $lineCallback ); + } + $line = trim( fgets( $fp, 1024 ) ); + $sl = strlen( $line ) - 1; + + if ( $sl < 0 ) { continue; } + if ( '-' == $line{0} && '-' == $line{1} ) { continue; } + + ## Allow dollar quoting for function declarations + if (substr($line,0,4) == '$mw$') { + if ($dollarquote) { + $dollarquote = false; + $done = true; + } + else { + $dollarquote = true; + } + } + else if (!$dollarquote) { + if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) { + $done = true; + $line = substr( $line, 0, $sl ); + } + } + + if ( '' != $cmd ) { $cmd .= ' '; } + $cmd .= "$line\n"; + + if ( $done ) { + $cmd = str_replace(';;', ";", $cmd); + $cmd = $this->replaceVars( $cmd ); + $res = $this->query( $cmd, __METHOD__, true ); + if ( $resultCallback ) { + call_user_func( $resultCallback, $res ); + } + + if ( false === $res ) { + $err = $this->lastError(); + return "Query \"{$cmd}\" failed with error code \"$err\".\n"; + } + + $cmd = ''; + $done = false; + } + } + return true; + } + + + /** + * Replace variables in sourced SQL + */ + protected function replaceVars( $ins ) { + $varnames = array( + 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser', + 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword', + 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions', + ); + + // Ordinary variables + foreach ( $varnames as $var ) { + if( isset( $GLOBALS[$var] ) ) { + $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check? + $ins = str_replace( '{$' . $var . '}', $val, $ins ); + $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins ); + $ins = str_replace( '/*$' . $var . '*/', $val, $ins ); + } + } + + // Table prefixes + $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-z_]*)/', + array( &$this, 'tableNameCallback' ), $ins ); + return $ins; + } + + /** + * Table name callback + * @private + */ + protected function tableNameCallback( $matches ) { + return $this->tableName( $matches[1] ); + } + +} /** * Database abstraction object for mySQL * Inherit all methods and properties of Database::Database() * - * @package MediaWiki + * @addtogroup Database * @see Database */ class DatabaseMysql extends Database { @@ -1562,43 +2302,57 @@ /** * Result wrapper for grabbing data queried by someone else - * - * @package MediaWiki + * @addtogroup Database */ -class ResultWrapper { - var $db, $result; - +class ResultWrapper implements Iterator { + var $db, $result, $pos = 0, $currentRow = null; + /** - * @todo document + * Create a new result object from a result resource and a Database object */ function ResultWrapper( $database, $result ) { - $this->db =& $database; - $this->result =& $result; + $this->db = $database; + if ( $result instanceof ResultWrapper ) { + $this->result = $result->result; + } else { + $this->result = $result; + } } /** - * @todo document + * Get the number of rows in a result object */ function numRows() { return $this->db->numRows( $this->result ); } - + /** - * @todo document + * Fetch the next row from the given result object, in object form. + * Fields can be retrieved with $row->fieldname, with fields acting like + * member variables. + * + * @param $res SQL result object as returned from Database::query(), etc. + * @return MySQL row object + * @throws DBUnexpectedError Thrown if the database returns an error */ function fetchObject() { return $this->db->fetchObject( $this->result ); } - + /** - * @todo document + * Fetch the next row from the given result object, in associative array + * form. Fields are retrieved with $row['fieldname']. + * + * @param $res SQL result object as returned from Database::query(), etc. + * @return MySQL row object + * @throws DBUnexpectedError Thrown if the database returns an error */ - function &fetchRow() { + function fetchRow() { return $this->db->fetchRow( $this->result ); } - + /** - * @todo document + * Free a result object */ function free() { $this->db->freeResult( $this->result ); @@ -1606,107 +2360,48 @@ unset( $this->db ); } + /** + * Change the position of the cursor in a result object + * See mysql_data_seek() + */ function seek( $row ) { $this->db->dataSeek( $this->result, $row ); } -} - -#------------------------------------------------------------------------------ -# Global functions -#------------------------------------------------------------------------------ -/** - * Standard fail function, called by default when a connection cannot be - * established. - * Displays the file cache if possible - */ -function wfEmergencyAbort( &$conn, $error ) { - global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding, $wgOutputEncoding; - global $wgSitename, $wgServer, $wgMessageCache, $wgLogo; - - # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky. - # Hard coding strings instead. - - $noconnect = "

      $wgSitename has a problem

      Sorry! This site is experiencing technical difficulties.

      Try waiting a few minutes and reloading.

      (Can't contact the database server: $1)

      "; - $mainpage = 'Main Page'; - $searchdisabled = <<$wgSitename search is disabled for performance reasons. You can search via Google in the meantime. -Note that their indexes of $wgSitename content may be out of date.

      ', -EOT; + /********************* + * Iterator functions + * Note that using these in combination with the non-iterator functions + * above may cause rows to be skipped or repeated. + */ - $googlesearch = " - -
      - -
      - -\"Google\" - - - - -
      WWW $wgServer
      - - -
      -
      -
      -"; - $cachederror = "The following is a cached copy of the requested page, and may not be up to date. "; + function rewind() { + if ($this->numRows()) { + $this->db->dataSeek($this->result, 0); + } + $this->pos = 0; + $this->currentRow = null; + } + function current() { + if ( is_null( $this->currentRow ) ) { + $this->next(); + } + return $this->currentRow; + } - if( !headers_sent() ) { - header( 'HTTP/1.0 500 Internal Server Error' ); - header( 'Content-type: text/html; charset='.$wgOutputEncoding ); - /* Don't cache error pages! They cause no end of trouble... */ - header( 'Cache-control: none' ); - header( 'Pragma: nocache' ); + function key() { + return $this->pos; } - # No database access - if ( is_object( $wgMessageCache ) ) { - $wgMessageCache->disable(); + function next() { + $this->pos++; + $this->currentRow = $this->fetchObject(); + return $this->currentRow; } - - $msg = wfGetSiteNotice(); - if($msg == '') { - $msg = str_replace( '$1', htmlspecialchars( $error ), $noconnect ); - } - $text = $msg; - - if($wgUseFileCache) { - if($wgTitle) { - $t =& $wgTitle; - } else { - if($title) { - $t = Title::newFromURL( $title ); - } elseif (@/**/$_REQUEST['search']) { - $search = $_REQUEST['search']; - echo $searchdisabled; - echo str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ), - $wgInputEncoding ), $googlesearch ); - wfErrorExit(); - } else { - $t = Title::newFromText( $mainpage ); - } - } - $cache = new CacheManager( $t ); - if( $cache->isFileCached() ) { - $msg = '

      '.$msg."
      \n" . - $cachederror . "

      \n"; - - $tag = '
      '; - $text = str_replace( - $tag, - $tag . $msg, - $cache->fetchPageText() ); - } + function valid() { + return $this->current() !== false; } - - echo $text; - wfErrorExit(); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/DatabaseFunctions.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/DatabaseFunctions.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/DatabaseFunctions.php 2005-07-22 07:29:14.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/DatabaseFunctions.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,42 +1,39 @@ fatalError( wfMsgNoDB( 'wrong_wfQuery_params', $db, $sql ) ); + throw new FatalError( wfMsgNoDB( 'wrong_wfQuery_params', $db, $sql ) ); } - $c =& wfGetDB( $db ); + $c = wfGetDB( $db ); if ( $c !== false ) { return $c->query( $sql, $fname ); - } else { + } else { return false; } } /** * - * @param string $sql SQL query + * @param $sql String: SQL query * @param $dbi - * @param string $fname name of the php function calling - * @return array first row from the database + * @param $fname String: name of the php function calling + * @return Array: first row from the database */ function wfSingleQuery( $sql, $dbi, $fname = '' ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); $res = $db->query($sql, $fname ); $row = $db->fetchRow( $res ); $ret = $row[0]; @@ -44,15 +41,6 @@ return $ret; } -/* - * @todo document function - */ -function &wfGetDB( $db = DB_LAST, $groups = array() ) { - global $wgLoadBalancer; - $ret =& $wgLoadBalancer->getConnection( $db, true, $groups ); - return $ret; -} - /** * Turns on (false) or off (true) the automatic generation and sending * of a "we're sorry, but there has been a database error" page on @@ -65,7 +53,7 @@ * @return Returns the previous state. */ function wfIgnoreSQLErrors( $newstate, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->ignoreErrors( $newstate ); } else { @@ -74,21 +62,21 @@ } /**#@+ - * @param $res database result handler + * @param $res Database result handler * @param $dbi */ /** * Free a database result - * @return bool whether result is sucessful or not + * @return Bool: whether result is sucessful or not. */ -function wfFreeResult( $res, $dbi = DB_LAST ) -{ - $db =& wfGetDB( $dbi ); +function wfFreeResult( $res, $dbi = DB_LAST ) +{ + $db = wfGetDB( $dbi ); if ( $db !== false ) { - $db->freeResult( $res ); + $db->freeResult( $res ); return true; - } else { + } else { return false; } } @@ -97,11 +85,11 @@ * Get an object from a database result * @return object|false object we requested */ -function wfFetchObject( $res, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); +function wfFetchObject( $res, $dbi = DB_LAST ) { + $db = wfGetDB( $dbi ); if ( $db !== false ) { - return $db->fetchObject( $res, $dbi = DB_LAST ); - } else { + return $db->fetchObject( $res, $dbi = DB_LAST ); + } else { return false; } } @@ -111,10 +99,10 @@ * @return object|false row we requested */ function wfFetchRow( $res, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->fetchRow ( $res, $dbi = DB_LAST ); - } else { + } else { return false; } } @@ -123,11 +111,11 @@ * Get a number of rows from a database result * @return integer|false number of rows */ -function wfNumRows( $res, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); +function wfNumRows( $res, $dbi = DB_LAST ) { + $db = wfGetDB( $dbi ); if ( $db !== false ) { - return $db->numRows( $res, $dbi = DB_LAST ); - } else { + return $db->numRows( $res, $dbi = DB_LAST ); + } else { return false; } } @@ -136,26 +124,28 @@ * Get the number of fields from a database result * @return integer|false number of fields */ -function wfNumFields( $res, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); +function wfNumFields( $res, $dbi = DB_LAST ) { + $db = wfGetDB( $dbi ); if ( $db !== false ) { - return $db->numFields( $res ); - } else { + return $db->numFields( $res ); + } else { return false; } } /** * Return name of a field in a result - * @param integer $n id of the field + * @param $res Mixed: Ressource link see Database::fieldName() + * @param $n Integer: id of the field + * @param $dbi Default DB_LAST * @return string|false name of field */ -function wfFieldName( $res, $n, $dbi = DB_LAST ) -{ - $db =& wfGetDB( $dbi ); +function wfFieldName( $res, $n, $dbi = DB_LAST ) +{ + $db = wfGetDB( $dbi ); if ( $db !== false ) { - return $db->fieldName( $res, $n, $dbi = DB_LAST ); - } else { + return $db->fieldName( $res, $n, $dbi = DB_LAST ); + } else { return false; } } @@ -164,11 +154,11 @@ /** * @todo document function */ -function wfInsertId( $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); +function wfInsertId( $dbi = DB_LAST ) { + $db = wfGetDB( $dbi ); if ( $db !== false ) { - return $db->insertId(); - } else { + return $db->insertId(); + } else { return false; } } @@ -176,11 +166,11 @@ /** * @todo document function */ -function wfDataSeek( $res, $row, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); +function wfDataSeek( $res, $row, $dbi = DB_LAST ) { + $db = wfGetDB( $dbi ); if ( $db !== false ) { - return $db->dataSeek( $res, $row ); - } else { + return $db->dataSeek( $res, $row ); + } else { return false; } } @@ -188,11 +178,11 @@ /** * @todo document function */ -function wfLastErrno( $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); +function wfLastErrno( $dbi = DB_LAST ) { + $db = wfGetDB( $dbi ); if ( $db !== false ) { - return $db->lastErrno(); - } else { + return $db->lastErrno(); + } else { return false; } } @@ -200,11 +190,11 @@ /** * @todo document function */ -function wfLastError( $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); +function wfLastError( $dbi = DB_LAST ) { + $db = wfGetDB( $dbi ); if ( $db !== false ) { - return $db->lastError(); - } else { + return $db->lastError(); + } else { return false; } } @@ -212,11 +202,11 @@ /** * @todo document function */ -function wfAffectedRows( $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); +function wfAffectedRows( $dbi = DB_LAST ) { + $db = wfGetDB( $dbi ); if ( $db !== false ) { - return $db->affectedRows(); - } else { + return $db->affectedRows(); + } else { return false; } } @@ -225,94 +215,135 @@ * @todo document function */ function wfLastDBquery( $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->lastQuery(); - } else { + } else { return false; } } /** + * @see Database::Set() * @todo document function + * @param $table + * @param $var + * @param $value + * @param $cond + * @param $dbi Default DB_MASTER */ function wfSetSQL( $table, $var, $value, $cond, $dbi = DB_MASTER ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->set( $table, $var, $value, $cond ); - } else { + } else { return false; } } /** + * @see Database::selectField() * @todo document function + * @param $table + * @param $var + * @param $cond Default '' + * @param $dbi Default DB_LAST */ function wfGetSQL( $table, $var, $cond='', $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->selectField( $table, $var, $cond ); - } else { + } else { return false; } } /** + * @see Database::fieldExists() * @todo document function + * @param $table + * @param $field + * @param $dbi Default DB_LAST + * @return Result of Database::fieldExists() or false. */ function wfFieldExists( $table, $field, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->fieldExists( $table, $field ); - } else { + } else { return false; } } /** + * @see Database::indexExists() * @todo document function + * @param $table String + * @param $index + * @param $dbi Default DB_LAST + * @return Result of Database::indexExists() or false. */ function wfIndexExists( $table, $index, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->indexExists( $table, $index ); - } else { + } else { return false; } } /** + * @see Database::insert() * @todo document function + * @param $table String + * @param $array Array + * @param $fname String, default 'wfInsertArray'. + * @param $dbi Default DB_MASTER + * @return result of Database::insert() or false. */ function wfInsertArray( $table, $array, $fname = 'wfInsertArray', $dbi = DB_MASTER ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->insert( $table, $array, $fname ); - } else { + } else { return false; } } /** + * @see Database::getArray() * @todo document function + * @param $table String + * @param $vars + * @param $conds + * @param $fname String, default 'wfGetArray'. + * @param $dbi Default DB_LAST + * @return result of Database::getArray() or false. */ function wfGetArray( $table, $vars, $conds, $fname = 'wfGetArray', $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->getArray( $table, $vars, $conds, $fname ); - } else { + } else { return false; } } /** + * @see Database::update() + * @param $table String + * @param $values + * @param $conds + * @param $fname String, default 'wfUpdateArray' + * @param $dbi Default DB_MASTER + * @return Result of Database::update()) or false; * @todo document function */ function wfUpdateArray( $table, $values, $conds, $fname = 'wfUpdateArray', $dbi = DB_MASTER ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { $db->update( $table, $values, $conds, $fname ); return true; @@ -325,7 +356,7 @@ * @todo document function */ function wfTableName( $name, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->tableName( $name ); } else { @@ -337,7 +368,7 @@ * @todo document function */ function wfStrencode( $s, $dbi = DB_LAST ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->strencode( $s ); } else { @@ -349,7 +380,7 @@ * @todo document function */ function wfNextSequenceValue( $seqName, $dbi = DB_MASTER ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->nextSequenceValue( $seqName ); } else { @@ -361,11 +392,11 @@ * @todo document function */ function wfUseIndexClause( $index, $dbi = DB_SLAVE ) { - $db =& wfGetDB( $dbi ); + $db = wfGetDB( $dbi ); if ( $db !== false ) { return $db->useIndexClause( $index ); } else { return false; } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/DateFormatter.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/DateFormatter.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/DateFormatter.php 2005-07-05 17:22:06.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/DateFormatter.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,136 +1,134 @@ monthNames = $this->getMonthRegex(); for ( $i=1; $i<=12; $i++ ) { - $this->xMonths[strtolower( $wgContLang->getMonthName( $i ) )] = $i; - } - for ( $i=1; $i<=12; $i++ ) { - $this->xMonths[strtolower( $wgContLang->getMonthAbbreviation( $i ) )] = $i; - } - - # Attempt at UTF-8 support, untested at the moment - if ( $wgInputEncoding == 'UTF-8' ) { - $this->regexTrail = '(?![a-z])/iu'; - } else { - $this->regexTrail = '(?![a-z])/i'; + $this->xMonths[$wgContLang->lc( $wgContLang->getMonthName( $i ) )] = $i; + $this->xMonths[$wgContLang->lc( $wgContLang->getMonthAbbreviation( $i ) )] = $i; } + $this->regexTrail = '(?![a-z])/iu'; + # Partial regular expressions $this->prxDM = '\[\[(\d{1,2})[ _](' . $this->monthNames . ')]]'; $this->prxMD = '\[\[(' . $this->monthNames . ')[ _](\d{1,2})]]'; $this->prxY = '\[\[(\d{1,4}([ _]BC|))]]'; $this->prxISO1 = '\[\[(-?\d{4})]]-\[\[(\d{2})-(\d{2})]]'; $this->prxISO2 = '\[\[(-?\d{4})-(\d{2})-(\d{2})]]'; - + # Real regular expressions - $this->regexes[DF_DMY] = "/{$this->prxDM} *,? *{$this->prxY}{$this->regexTrail}"; - $this->regexes[DF_YDM] = "/{$this->prxY} *,? *{$this->prxDM}{$this->regexTrail}"; - $this->regexes[DF_MDY] = "/{$this->prxMD} *,? *{$this->prxY}{$this->regexTrail}"; - $this->regexes[DF_YMD] = "/{$this->prxY} *,? *{$this->prxMD}{$this->regexTrail}"; - $this->regexes[DF_DM] = "/{$this->prxDM}{$this->regexTrail}"; - $this->regexes[DF_MD] = "/{$this->prxMD}{$this->regexTrail}"; - $this->regexes[DF_ISO1] = "/{$this->prxISO1}{$this->regexTrail}"; - $this->regexes[DF_ISO2] = "/{$this->prxISO2}{$this->regexTrail}"; - + $this->regexes[self::DMY] = "/{$this->prxDM} *,? *{$this->prxY}{$this->regexTrail}"; + $this->regexes[self::YDM] = "/{$this->prxY} *,? *{$this->prxDM}{$this->regexTrail}"; + $this->regexes[self::MDY] = "/{$this->prxMD} *,? *{$this->prxY}{$this->regexTrail}"; + $this->regexes[self::YMD] = "/{$this->prxY} *,? *{$this->prxMD}{$this->regexTrail}"; + $this->regexes[self::DM] = "/{$this->prxDM}{$this->regexTrail}"; + $this->regexes[self::MD] = "/{$this->prxMD}{$this->regexTrail}"; + $this->regexes[self::ISO1] = "/{$this->prxISO1}{$this->regexTrail}"; + $this->regexes[self::ISO2] = "/{$this->prxISO2}{$this->regexTrail}"; + # Extraction keys # See the comments in replace() for the meaning of the letters - $this->keys[DF_DMY] = 'jFY'; - $this->keys[DF_YDM] = 'Y jF'; - $this->keys[DF_MDY] = 'FjY'; - $this->keys[DF_YMD] = 'Y Fj'; - $this->keys[DF_DM] = 'jF'; - $this->keys[DF_MD] = 'Fj'; - $this->keys[DF_ISO1] = 'ymd'; # y means ISO year - $this->keys[DF_ISO2] = 'ymd'; + $this->keys[self::DMY] = 'jFY'; + $this->keys[self::YDM] = 'Y jF'; + $this->keys[self::MDY] = 'FjY'; + $this->keys[self::YMD] = 'Y Fj'; + $this->keys[self::DM] = 'jF'; + $this->keys[self::MD] = 'Fj'; + $this->keys[self::ISO1] = 'ymd'; # y means ISO year + $this->keys[self::ISO2] = 'ymd'; # Target date formats - $this->targets[DF_DMY] = '[[F j|j F]] [[Y]]'; - $this->targets[DF_YDM] = '[[Y]], [[F j|j F]]'; - $this->targets[DF_MDY] = '[[F j]], [[Y]]'; - $this->targets[DF_YMD] = '[[Y]] [[F j]]'; - $this->targets[DF_DM] = '[[F j|j F]]'; - $this->targets[DF_MD] = '[[F j]]'; - $this->targets[DF_ISO1] = '[[Y|y]]-[[F j|m-d]]'; - $this->targets[DF_ISO2] = '[[y-m-d]]'; + $this->targets[self::DMY] = '[[F j|j F]] [[Y]]'; + $this->targets[self::YDM] = '[[Y]], [[F j|j F]]'; + $this->targets[self::MDY] = '[[F j]], [[Y]]'; + $this->targets[self::YMD] = '[[Y]] [[F j]]'; + $this->targets[self::DM] = '[[F j|j F]]'; + $this->targets[self::MD] = '[[F j]]'; + $this->targets[self::ISO1] = '[[Y|y]]-[[F j|m-d]]'; + $this->targets[self::ISO2] = '[[y-m-d]]'; # Rules # pref source target - $this->rules[DF_DMY][DF_MD] = DF_DM; - $this->rules[DF_ALL][DF_MD] = DF_MD; - $this->rules[DF_MDY][DF_DM] = DF_MD; - $this->rules[DF_ALL][DF_DM] = DF_DM; - $this->rules[DF_NONE][DF_ISO2] = DF_ISO1; + $this->rules[self::DMY][self::MD] = self::DM; + $this->rules[self::ALL][self::MD] = self::MD; + $this->rules[self::MDY][self::DM] = self::MD; + $this->rules[self::ALL][self::DM] = self::DM; + $this->rules[self::NONE][self::ISO2] = self::ISO1; + + $this->preferences = array( + 'default' => self::NONE, + 'dmy' => self::DMY, + 'mdy' => self::MDY, + 'ymd' => self::YMD, + 'ISO 8601' => self::ISO1, + ); } - + /** * @static */ function &getInstance() { - global $wgDBname, $wgMemc; + global $wgMemc; static $dateFormatter = false; if ( !$dateFormatter ) { - $dateFormatter = $wgMemc->get( "$wgDBname:dateformatter" ); + $dateFormatter = $wgMemc->get( wfMemcKey( 'dateformatter' ) ); if ( !$dateFormatter ) { $dateFormatter = new DateFormatter; - $wgMemc->set( "$wgDBname:dateformatter", $dateFormatter, 3600 ); + $wgMemc->set( wfMemcKey( 'dateformatter' ), $dateFormatter, 3600 ); } } return $dateFormatter; - } - + } + /** - * @param $preference - * @param $text + * @param string $preference User preference + * @param string $text Text to reformat */ function reformat( $preference, $text ) { - if ($preference == 'ISO 8601') $preference = 4; # The ISO 8601 option used to be 4 - for ( $i=1; $i<=DF_LAST; $i++ ) { + if ( isset( $this->preferences[$preference] ) ) { + $preference = $this->preferences[$preference]; + } else { + $preference = self::NONE; + } + for ( $i=1; $i<=self::LAST; $i++ ) { $this->mSource = $i; - if ( @$this->rules[$preference][$i] ) { + if ( isset ( $this->rules[$preference][$i] ) ) { # Specific rules $this->mTarget = $this->rules[$preference][$i]; - } elseif ( @$this->rules[DF_ALL][$i] ) { + } elseif ( isset ( $this->rules[self::ALL][$i] ) ) { # General rules - $this->mTarget = $this->rules[DF_ALL][$i]; + $this->mTarget = $this->rules[self::ALL][$i]; } elseif ( $preference ) { # User preference $this->mTarget = $preference; @@ -138,7 +136,7 @@ # Default $this->mTarget = $i; } - $text = preg_replace_callback( $this->regexes[$i], 'wfMainDateReplace', $text ); + $text = preg_replace_callback( $this->regexes[$i], array( &$this, 'replace' ), $text ); } return $text; } @@ -157,11 +155,11 @@ } $format = $this->targets[$this->mTarget]; - + # Construct new date $text = ''; $fail = false; - + for ( $p=0; $p < strlen( $format ); $p++ ) { $char = $format{$p}; switch ( $char ) { @@ -193,14 +191,14 @@ break; case 'j': # ordinary day of month if ( !isset($bits['j']) ) { - $text .= IntVal( $bits['d'] ); + $text .= intval( $bits['d'] ); } else { $text .= $bits['j']; } break; case 'F': # long month if ( !isset( $bits['F'] ) ) { - $m = IntVal($bits['m']); + $m = intval($bits['m']); if ( $m > 12 || $m < 1 ) { $fail = true; } else { @@ -227,7 +225,7 @@ } return $text; } - + /** * @todo document */ @@ -243,23 +241,25 @@ /** * Makes an ISO month, e.g. 02, from a month name - * @param string $monthName Month name + * @param $monthName String: month name * @return string ISO month name */ function makeIsoMonth( $monthName ) { - $n = $this->xMonths[strtolower( $monthName )]; + global $wgContLang; + + $n = $this->xMonths[$wgContLang->lc( $monthName )]; return sprintf( '%02d', $n ); } /** * @todo document - * @param string $year Year name + * @param $year String: Year name * @return string ISO year name */ function makeIsoYear( $year ) { # Assumes the year is in a nice format, as enforced by the regex if ( substr( $year, -2 ) == 'BC' ) { - $num = IntVal(substr( $year, 0, -3 )) - 1; + $num = intval(substr( $year, 0, -3 )) - 1; # PHP bug note: sprintf( "%04d", -1 ) fails poorly $text = sprintf( '-%04d', $num ); @@ -274,20 +274,12 @@ */ function makeNormalYear( $iso ) { if ( $iso{0} == '-' ) { - $text = (IntVal( substr( $iso, 1 ) ) + 1) . ' BC'; + $text = (intval( substr( $iso, 1 ) ) + 1) . ' BC'; } else { - $text = IntVal( $iso ); + $text = intval( $iso ); } return $text; } } -/** - * @todo document - */ -function wfMainDateReplace( $matches ) { - $df =& DateFormatter::getInstance(); - return $df->replace( $matches ); -} -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/DefaultSettings.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/DefaultSettings.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/DefaultSettings.php 2006-03-26 14:38:24.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/DefaultSettings.php 2007-09-10 17:36:51.000000000 -0400 @@ -1,7 +1,7 @@ Causes problems with HTML escaping, don't use + * % Enabled by default, minor problems with path to query rewrite rules, see below + * + Enabled by default, but doesn't work with path to query rewrite rules, corrupted by apache + * ? Enabled by default, but doesn't work with path to PATH_INFO rewrites + * + * All three of these punctuation problems can be avoided by using an alias, instead of a + * rewrite rule of either variety. + * + * The problem with % is that when using a path to query rewrite rule, URLs are + * double-unescaped: once by Apache's path conversion code, and again by PHP. So + * %253F, for example, becomes "?". Our code does not double-escape to compensate + * for this, indeed double escaping would break if the double-escaped title was + * passed in the query string rather than the path. This is a minor security issue + * because articles can be created such that they are hard to view or edit. + * + * In some rare cases you may wish to remove + for compatibility with old links. + * + * Theoretically 0x80-0x9F of ISO 8859-1 should be disallowed, but + * this breaks interlanguage links + */ +$wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+"; + + +/** + * The external URL protocols + */ +$wgUrlProtocols = array( + 'http://', + 'https://', + 'ftp://', + 'irc://', + 'gopher://', + 'telnet://', // Well if we're going to support the above.. -ævar + 'nntp://', // @bug 3808 RFC 1738 + 'worldwind://', + 'mailto:', + 'news:' +); /** internal name of virus scanner. This servers as a key to the $wgAntivirusSetup array. * Set this to NULL to disable virus scanning. If not null, every file uploaded will be scanned for viruses. @@ -161,34 +303,34 @@ * * @global array $wgAntivirusSetup */ -$wgAntivirusSetup= array( +$wgAntivirusSetup = array( #setup for clamav 'clamav' => array ( 'command' => "clamscan --no-summary ", - 'codemap'=> array ( - "0"=> AV_NO_VIRUS, #no virus - "1"=> AV_VIRUS_FOUND, #virus found - "52"=> AV_SCAN_ABORTED, #unsupported file format (probably imune) - "*"=> AV_SCAN_FAILED, #else scan failed + 'codemap' => array ( + "0" => AV_NO_VIRUS, # no virus + "1" => AV_VIRUS_FOUND, # virus found + "52" => AV_SCAN_ABORTED, # unsupported file format (probably imune) + "*" => AV_SCAN_FAILED, # else scan failed ), - 'messagepattern'=> '/.*?:(.*)/sim', + 'messagepattern' => '/.*?:(.*)/sim', ), #setup for f-prot 'f-prot' => array ( 'command' => "f-prot ", - 'codemap'=> array ( - "0"=> AV_NO_VIRUS, #no virus - "3"=> AV_VIRUS_FOUND, #virus found - "6"=> AV_VIRUS_FOUND, #virus found - "*"=> AV_SCAN_FAILED, #else scan failed + 'codemap' => array ( + "0" => AV_NO_VIRUS, # no virus + "3" => AV_VIRUS_FOUND, # virus found + "6" => AV_VIRUS_FOUND, # virus found + "*" => AV_SCAN_FAILED, # else scan failed ), - 'messagepattern'=> '/.*?Infection:(.*)$/m', + 'messagepattern' => '/.*?Infection:(.*)$/m', ), ); @@ -206,40 +348,38 @@ /** Sets the mime type definition file to use by MimeMagic.php. * @global string $wgMimeTypeFile */ -#$wgMimeTypeFile= "/etc/mime.types"; $wgMimeTypeFile= "includes/mime.types"; -#$wgMimeTypeFile= NULL; #use build in defaults only. +#$wgMimeTypeFile= "/etc/mime.types"; +#$wgMimeTypeFile= NULL; #use built-in defaults only. /** Sets the mime type info file to use by MimeMagic.php. * @global string $wgMimeInfoFile */ $wgMimeInfoFile= "includes/mime.info"; -#$wgMimeInfoFile= NULL; #use build in defaults only. +#$wgMimeInfoFile= NULL; #use built-in defaults only. /** Switch for loading the FileInfo extension by PECL at runtime. -* This should be used only if fileinfo is installed as a shared object / dynamic libary -* @global string $wgLoadFileinfoExtension + * This should be used only if fileinfo is installed as a shared object + * or a dynamic libary + * @global string $wgLoadFileinfoExtension */ $wgLoadFileinfoExtension= false; -/** Sets an external mime detector program. The command must print only the mime type to standard output. -* the name of the file to process will be appended to the command given here. -* If not set or NULL, mime_content_type will be used if available. +/** Sets an external mime detector program. The command must print only + * the mime type to standard output. + * The name of the file to process will be appended to the command given here. + * If not set or NULL, mime_content_type will be used if available. */ $wgMimeDetectorCommand= NULL; # use internal mime_content_type function, available since php 4.3.0 -#$wgMimeDetectorCommand= "file -bi" #use external mime detector (linux) +#$wgMimeDetectorCommand= "file -bi"; #use external mime detector (Linux) -/** Switch for trivial mime detection. Used by thumb.php to disable all fance things, -* because only a few types of images are needed and file extensions can be trusted. +/** Switch for trivial mime detection. Used by thumb.php to disable all fance + * things, because only a few types of images are needed and file extensions + * can be trusted. */ $wgTrivialMimeDetection= false; /** - * Produce hashed HTML article paths. Used internally, do not set. - */ -$wgMakeDumpLinks = false; - -/** * To set 'pretty' URL paths for actions other than * plain page views, add to this array. For instance: * 'edit' => "$wgScriptPath/edit/$1" @@ -257,6 +397,10 @@ * no file of the given name is found in the local repository (for [[Image:..]], * [[Media:..]] links). Thumbnails will also be looked for and generated in this * directory. + * + * Note that these configuration settings can now be defined on a per- + * repository basis for an arbitrary number of file repositories, using the + * $wgForeignFileRepos variable. */ $wgUseSharedUploads = false; /** Full path on the web server where shared uploads can be found */ @@ -271,11 +415,19 @@ $wgSharedUploadDBprefix = ''; /** Cache shared metadata in memcached. Don't do this if the commons wiki is in a different memcached domain */ $wgCacheSharedUploads = true; +/** Allow for upload to be copied from an URL. Requires Special:Upload?source=web */ +$wgAllowCopyUploads = false; +/** + * Max size for uploads, in bytes. Currently only works for uploads from URL + * via CURL (see $wgAllowCopyUploads). The only way to impose limits on + * normal uploads is currently to edit php.ini. + */ +$wgMaxUploadSize = 1024*1024*100; # 100MB /** * Point the upload navigation link to an external URL * Useful if you want to use a shared repository by default - * without disabling local uploads + * without disabling local uploads (use $wgEnableUploads = false for that) * e.g. $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload'; */ $wgUploadNavigationUrl = false; @@ -286,7 +438,7 @@ * apache servers don't have read/write access to the thumbnail path. * * Example: - * $wgThumbnailScriptPath = "{$wgScriptPath}/thumb.php"; + * $wgThumbnailScriptPath = "{$wgScriptPath}/thumb{$wgScriptExtension}"; */ $wgThumbnailScriptPath = false; $wgSharedThumbnailScriptPath = false; @@ -326,7 +478,7 @@ * Default to apache@SERVER_NAME * @global string $wgPasswordSender */ -$wgPasswordSender = 'Wikipedia Mail '; +$wgPasswordSender = 'MediaWiki Mail '; /** * dummy address which should be accepted during mail send action @@ -352,6 +504,12 @@ $wgEnableUserEmail = true; /** + * Minimum time, in hours, which must elapse between password reminder + * emails for a given account. This is to prevent abuse by mail flooding. + */ +$wgPasswordReminderResendTime = 24; + +/** * SMTP Mode * For using a direct (authenticated) SMTP server connection. * Default to false or fill an array : @@ -374,6 +532,8 @@ */ /** database host name or ip address */ $wgDBserver = 'localhost'; +/** database port number */ +$wgDBport = ''; /** name of the database */ $wgDBname = 'wikidb'; /** */ @@ -381,7 +541,6 @@ /** Database username */ $wgDBuser = 'wikiuser'; /** Database type - * "mysql" for working code and "PostgreSQL" for development/broken code */ $wgDBtype = "mysql"; /** Search type @@ -392,14 +551,15 @@ $wgSearchType = null; /** Table name prefix */ $wgDBprefix = ''; -/** Database schema - * on some databases this allows separate - * logical namespace for application data - */ -$wgDBschema = 'mediawiki'; +/** MySQL table options to use during installation or update */ +$wgDBTableOptions = 'TYPE=InnoDB'; + /**#@-*/ +/** Live high performance sites should disable this - some checks acquire giant mysql locks */ +$wgCheckDBSchema = true; + /** * Shared database for multiple wikis. Presently used for storing a user table @@ -416,7 +576,7 @@ # dbname: Default database name # user: DB user # password: DB password -# type: "mysql" or "pgsql" +# type: "mysql" or "postgres" # load: ratio of DB_SLAVE load, must be >=0, the sum of all loads must be >0 # groupLoads: array of load ratios, the key is the query group name. A query may belong # to several groups, the most specific group defined here is used. @@ -428,13 +588,19 @@ # DBO_IGNORE -- ignore errors (not useful in LocalSettings.php) # DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php) # +# max lag: (optional) Maximum replication lag before a slave will taken out of rotation +# max threads: (optional) Maximum number of running threads +# +# These and any other user-defined properties will be assigned to the mLBInfo member +# variable of the Database object. +# # Leave at false to use the single-server variables above $wgDBservers = false; /** How long to wait for a slave to catch up to the master */ $wgMasterWaitTimeout = 10; -/** File to log MySQL errors to */ +/** File to log database errors to */ $wgDBerrorLog = false; /** When to give an error message */ @@ -450,8 +616,9 @@ $wgDBminWordLen = 4; /** Set to true if using InnoDB tables */ $wgDBtransactions = false; -/** Set to true to use enhanced fulltext search */ -$wgDBmysql4 = false; +/** Set to true for compatibility with extensions that might be checking. + * MySQL 3.23.x is no longer supported. */ +$wgDBmysql4 = true; /** * Set to true to engage MySQL 4.1/5.0 charset-related features; @@ -474,11 +641,24 @@ /** * Other wikis on this site, can be administered from a single developer * account. - * Array, interwiki prefix => database name + * Array numeric key => database name */ $wgLocalDatabases = array(); /** + * For multi-wiki clusters with multiple master servers; if an alternate + * is listed for the requested database, a connection to it will be opened + * instead of to the current wiki's regular master server when cross-wiki + * data operations are done from here. + * + * Requires that the other server be accessible by network, with the same + * username/password as the primary. + * + * eg $wgAlternateMaster['enwiki'] = 'ariel'; + */ +$wgAlternateMaster = array(); + +/** * Object cache settings * See Defines.php for types */ @@ -486,6 +666,8 @@ $wgMessageCacheType = CACHE_ANYTHING; $wgParserCacheType = CACHE_ANYTHING; +$wgParserCacheExpireTime = 86400; + $wgSessionsInMemcached = false; $wgLinkCacheMemcached = false; # Not fully tested @@ -493,28 +675,51 @@ * Memcached-specific settings * See docs/memcached.txt */ +$wgUseMemCached = false; $wgMemCachedDebug = false; # Will be set to false in Setup.php, if the server isn't working $wgMemCachedServers = array( '127.0.0.1:11000' ); -$wgMemCachedDebug = false; +$wgMemCachedPersistent = false; +/** + * Directory for local copy of message cache, for use in addition to memcached + */ +$wgLocalMessageCache = false; +/** + * Defines format of local cache + * true - Serialized object + * false - PHP source file (Warning - security risk) + */ +$wgLocalMessageCacheSerialized = true; +/** + * Directory for compiled constant message array databases + * WARNING: turning anything on will just break things, aaaaaah!!!! + */ +$wgCachedMessageArrays = false; # Language settings # /** Site language code, should be one of ./languages/Language(.*).php */ $wgLanguageCode = 'en'; +/** + * Some languages need different word forms, usually for different cases. + * Used in Language::convertGrammar(). + */ +$wgGrammarForms = array(); +#$wgGrammarForms['en']['genitive']['car'] = 'car\'s'; + /** Treat language links as magic connectors, not inline links */ -$wgInterwikiMagic = true; +$wgInterwikiMagic = true; /** Hide interlanguage links from the sidebar */ $wgHideInterlanguageLinks = false; /** We speak UTF-8 all the time now, unless some oddities happen */ -$wgInputEncoding = 'UTF-8'; -$wgOutputEncoding = 'UTF-8'; -$wgEditEncoding = ''; +$wgInputEncoding = 'UTF-8'; +$wgOutputEncoding = 'UTF-8'; +$wgEditEncoding = ''; # Set this to eg 'ISO-8859-1' to perform character set # conversion when loading old revisions not marked with @@ -547,6 +752,15 @@ $wgJsMimeType = 'text/javascript'; $wgDocType = '-//W3C//DTD XHTML 1.0 Transitional//EN'; $wgDTD = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'; +$wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml'; + +# Permit other namespaces in addition to the w3.org default. +# Use the prefix for the key and the namespace for the value. For +# example: +# $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg'; +# Normally we wouldn't have to define this in the root +# element, but IE needs it there in some circumstances. +$wgXhtmlNamespaces = array(); /** Enable to allow rewriting dates in page text. * DOES NOT FORMAT CORRECTLY FOR MOST LANGUAGES */ @@ -561,19 +775,35 @@ */ $wgTranslateNumerals = true; - -# Translation using MediaWiki: namespace -# This will increase load times by 25-60% unless memcached is installed -# Interface messages will be loaded from the database. +/** + * Translation using MediaWiki: namespace. + * This will increase load times by 25-60% unless memcached is installed. + * Interface messages will be loaded from the database. + */ $wgUseDatabaseMessages = true; + +/** + * Expiry time for the message cache key + */ $wgMsgCacheExpiry = 86400; +/** + * Maximum entry size in the message cache, in bytes + */ +$wgMaxMsgCacheEntrySize = 10000; + # Whether to enable language variant conversion. $wgDisableLangConversion = false; -# Use article validation feature; turned off by default -$wgUseValidation = false; -$wgValidationForAnons = true ; +# Default variant code, if false, the default will be the language code +$wgDefaultLanguageVariant = false; + +/** + * Show a bar of language selection links in the user login and user + * registration forms; edit the "loginlanguagelinks" message to + * customise these + */ +$wgLoginLanguageSelector = false; # Whether to use zhdaemon to perform Chinese text processing # zhdaemon is under developement, so normally you don't want to @@ -597,6 +827,26 @@ $wgLocalInterwiki = 'w'; $wgInterwikiExpiry = 10800; # Expiry time for cache of interwiki table +/** Interwiki caching settings. + $wgInterwikiCache specifies path to constant database file + This cdb database is generated by dumpInterwiki from maintenance + and has such key formats: + dbname:key - a simple key (e.g. enwiki:meta) + _sitename:key - site-scope key (e.g. wiktionary:meta) + __global:key - global-scope key (e.g. __global:meta) + __sites:dbname - site mapping (e.g. __sites:enwiki) + Sites mapping just specifies site name, other keys provide + "local url" data layout. + $wgInterwikiScopes specify number of domains to check for messages: + 1 - Just wiki(db)-level + 2 - wiki and global levels + 3 - site levels + $wgInterwikiFallbackSite - if unable to resolve from cache +*/ +$wgInterwikiCache = false; +$wgInterwikiScopes = 3; +$wgInterwikiFallbackSite = 'wiki'; + /** * If local interwikis are set up which allow redirects, * set this regexp to restrict URLs which will be displayed @@ -614,11 +864,18 @@ $wgShowIPinHeader = true; # For non-logged in users $wgMaxNameChars = 255; # Maximum number of bytes in username +$wgMaxSigChars = 255; # Maximum number of Unicode characters in signature +$wgMaxArticleSize = 2048; # Maximum article size in kilobytes $wgExtraSubtitle = ''; $wgSiteSupportPage = ''; # A page where you users can receive donations -$wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR"; +/*** + * If this lock file exists, the wiki will be forced into read-only mode. + * Its contents will be shown to users as part of the read-only warning + * message. + */ +$wgReadOnlyFile = false; /// defaults to "{$wgUploadDirectory}/lock_yBgMBwiR"; /** * The debug log file should be not be publicly accessible if it is used, as it @@ -634,11 +891,15 @@ $wgDebugComments = false; $wgReadOnly = null; $wgLogQueries = false; + +/** + * Write SQL queries to the debug log + */ $wgDebugDumpSql = false; /** * Set to an array of log group keys to filenames. - * If set, wfDebugLog() output for that group will go to that file instead + * If set, wfDebugLog() output for that group will go to that file instead * of the regular $wgDebugLogFile. Useful for enabling selective logging * in production. */ @@ -651,15 +912,28 @@ */ $wgShowSQLErrors = false; -# Should [[Category:Dog]] on a page associate it with the -# category "Dog"? (a link to that category page will be -# added to the article, clicking it reveals a list of -# all articles in the category) -$wgUseCategoryMagic = true; +/** + * If true, some error messages will be colorized when running scripts on the + * command line; this can aid picking important things out when debugging. + * Ignored when running on Windows or when output is redirected to a file. + */ +$wgColorErrors = true; /** - * disable experimental dmoz-like category browsing. Output things like: - * Encyclopedia > Music > Style of Music > Jazz + * If set to true, uncaught exceptions will print a complete stack trace + * to output. This should only be used for debugging, as it may reveal + * private information in function parameters due to PHP's backtrace + * formatting. + */ +$wgShowExceptionDetails = false; + +/** + * Expose backend server host names through the API and various HTML comments + */ +$wgShowHostnames = false; + +/** + * Use experimental, DMOZ-like category browser */ $wgUseCategoryBrowser = false; @@ -674,6 +948,17 @@ $wgEnableParserCache = true; /** + * If on, the sidebar navigation links are cached for users with the + * current language set. This can save a touch of load on a busy site + * by shaving off extra message lookups. + * + * However it is also fragile: changing the site configuration, or + * having a variable $wgArticlePath, can produce broken links that + * don't update as expected. + */ +$wgEnableSidebarCache = false; + +/** * Under which condition should a page in the main namespace be counted * as a valid article? If $wgUseCommaCount is set to true, it will be * counted if it contains at least one comma. If it is set to false @@ -696,20 +981,28 @@ */ $wgHitcounterUpdateFreq = 1; -# User rights settings -# -# It's not 100% safe, there could be security hole using that one. Use at your -# own risks. - -$wgWhitelistRead = false; # Pages anonymous user may see, like: = array ( "Main Page", "Special:Userlogin", "Wikipedia:Help"); - -$wgAllowAnonymousMinor = false; # Allow anonymous users to mark changes as 'minor' - +# Basic user rights and block settings $wgSysopUserBans = true; # Allow sysops to ban logged-in users -$wgSysopRangeBans = true; # Allow sysops to ban IP ranges - -$wgAutoblockExpiry = 86400; # Number of seconds before autoblock entries expire +$wgSysopRangeBans = true; # Allow sysops to ban IP ranges +$wgAutoblockExpiry = 86400; # Number of seconds before autoblock entries expire $wgBlockAllowsUTEdit = false; # Blocks allow users to edit their own user talk page +$wgSysopEmailBans = true; # Allow sysops to ban users from accessing Emailuser + +# Pages anonymous user may see as an array, e.g.: +# array ( "Main Page", "Special:Userlogin", "Wikipedia:Help"); +# NOTE: This will only work if $wgGroupPermissions['*']['read'] +# is false -- see below. Otherwise, ALL pages are accessible, +# regardless of this setting. +# Also note that this will only protect _pages in the wiki_. +# Uploaded files will remain readable. Make your upload +# directory name unguessable, or use .htaccess to protect it. +$wgWhitelistRead = false; + +/** + * Should editors be required to have a validated e-mail + * address before being allowed to edit? + */ +$wgEmailConfirmToEdit=false; /** * Permission keys given to users in each group. @@ -718,36 +1011,81 @@ * combined with the permissions of all groups that a given user is listed * in in the user_groups table. * + * Note: Don't set $wgGroupPermissions = array(); unless you know what you're + * doing! This will wipe all permissions, and may mean that your users are + * unable to perform certain essential tasks or access new functionality + * when new permissions are introduced and default grants established. + * + * Functionality to make pages inaccessible has not been extensively tested + * for security. Use at your own risk! + * * This replaces wgWhitelistAccount and wgWhitelistEdit */ $wgGroupPermissions = array(); +// Implicit group for all visitors $wgGroupPermissions['*' ]['createaccount'] = true; $wgGroupPermissions['*' ]['read'] = true; $wgGroupPermissions['*' ]['edit'] = true; +$wgGroupPermissions['*' ]['createpage'] = true; +$wgGroupPermissions['*' ]['createtalk'] = true; +// Implicit group for all logged-in accounts $wgGroupPermissions['user' ]['move'] = true; $wgGroupPermissions['user' ]['read'] = true; $wgGroupPermissions['user' ]['edit'] = true; +$wgGroupPermissions['user' ]['createpage'] = true; +$wgGroupPermissions['user' ]['createtalk'] = true; $wgGroupPermissions['user' ]['upload'] = true; +$wgGroupPermissions['user' ]['reupload'] = true; +$wgGroupPermissions['user' ]['reupload-shared'] = true; +$wgGroupPermissions['user' ]['minoredit'] = true; +$wgGroupPermissions['user' ]['purge'] = true; // can use ?action=purge without clicking "ok" + +// Implicit group for accounts that pass $wgAutoConfirmAge +$wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true; + +// Implicit group for accounts with confirmed email addresses +// This has little use when email address confirmation is off +$wgGroupPermissions['emailconfirmed']['emailconfirmed'] = true; +// Users with bot privilege can have their edits hidden +// from various log pages by default $wgGroupPermissions['bot' ]['bot'] = true; +$wgGroupPermissions['bot' ]['autoconfirmed'] = true; +$wgGroupPermissions['bot' ]['nominornewtalk'] = true; +$wgGroupPermissions['bot' ]['autopatrol'] = true; +// Most extra permission abilities go to this group $wgGroupPermissions['sysop']['block'] = true; $wgGroupPermissions['sysop']['createaccount'] = true; $wgGroupPermissions['sysop']['delete'] = true; +$wgGroupPermissions['sysop']['deletedhistory'] = true; // can view deleted history entries, but not see or restore the text $wgGroupPermissions['sysop']['editinterface'] = true; $wgGroupPermissions['sysop']['import'] = true; $wgGroupPermissions['sysop']['importupload'] = true; $wgGroupPermissions['sysop']['move'] = true; $wgGroupPermissions['sysop']['patrol'] = true; +$wgGroupPermissions['sysop']['autopatrol'] = true; $wgGroupPermissions['sysop']['protect'] = true; +$wgGroupPermissions['sysop']['proxyunbannable'] = true; $wgGroupPermissions['sysop']['rollback'] = true; +$wgGroupPermissions['sysop']['trackback'] = true; $wgGroupPermissions['sysop']['upload'] = true; +$wgGroupPermissions['sysop']['reupload'] = true; +$wgGroupPermissions['sysop']['reupload-shared'] = true; +$wgGroupPermissions['sysop']['unwatchedpages'] = true; +$wgGroupPermissions['sysop']['autoconfirmed'] = true; +$wgGroupPermissions['sysop']['upload_by_url'] = true; +$wgGroupPermissions['sysop']['ipblock-exempt'] = true; +$wgGroupPermissions['sysop']['blockemail'] = true; +// Permission to change users' group assignments $wgGroupPermissions['bureaucrat']['userrights'] = true; -// Used by the Special:Renameuser extension -$wgGroupPermissions['bureaucrat']['renameuser'] = true; + +// Experimental permissions, not ready for production use +//$wgGroupPermissions['sysop']['deleterevision'] = true; +//$wgGroupPermissions['bureaucrat']['hiderevision'] = true; /** * The developer group is deprecated, but can be activated if need be @@ -757,7 +1095,75 @@ */ # $wgGroupPermissions['developer']['siteadmin'] = true; +/** + * Set of available actions that can be restricted via action=protect + * You probably shouldn't change this. + * Translated trough restriction-* messages. + */ +$wgRestrictionTypes = array( 'edit', 'move' ); + +/** + * Rights which can be required for each protection level (via action=protect) + * + * You can add a new protection level that requires a specific + * permission by manipulating this array. The ordering of elements + * dictates the order on the protection form's lists. + * + * '' will be ignored (i.e. unprotected) + * 'sysop' is quietly rewritten to 'protect' for backwards compatibility + */ +$wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ); + +/** + * Set the minimum permissions required to edit pages in each + * namespace. If you list more than one permission, a user must + * have all of them to edit pages in that namespace. + */ +$wgNamespaceProtection = array(); +$wgNamespaceProtection[ NS_MEDIAWIKI ] = array( 'editinterface' ); + +/** +* Pages in namespaces in this array can not be used as templates. +* Elements must be numeric namespace ids. +* Among other things, this may be useful to enforce read-restrictions +* which may otherwise be bypassed by using the template machanism. +*/ +$wgNonincludableNamespaces = array(); +/** + * Number of seconds an account is required to age before + * it's given the implicit 'autoconfirm' group membership. + * This can be used to limit privileges of new accounts. + * + * Accounts created by earlier versions of the software + * may not have a recorded creation date, and will always + * be considered to pass the age test. + * + * When left at 0, all registered accounts will pass. + */ +$wgAutoConfirmAge = 0; +//$wgAutoConfirmAge = 600; // ten minutes +//$wgAutoConfirmAge = 3600*24; // one day + +# Number of edits an account requires before it is autoconfirmed +# Passing both this AND the time requirement is needed +$wgAutoConfirmCount = 0; +//$wgAutoConfirmCount = 50; + +/** + * These settings can be used to give finer control over who can assign which + * groups at Special:Userrights. Example configuration: + * + * // Bureaucrat can add any group + * $wgAddGroups['bureaucrat'] = true; + * // Bureaucrats can only remove bots and sysops + * $wgRemoveGroups['bureaucrat'] = array( 'bot', 'sysop' ); + * // Sysops can make bots + * $wgAddGroups['sysop'] = array( 'bot' ); + * // Sysops can disable other sysops in an emergency, and disable bots + * $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' ); + */ +$wgAddGroups = $wgRemoveGroups = array(); # Proxy scanner settings # @@ -775,7 +1181,7 @@ /** Port we want to scan for a proxy */ $wgProxyPorts = array( 80, 81, 1080, 3128, 6588, 8000, 8080, 8888, 65506 ); /** Script used to scan */ -$wgProxyScriptPath = "$IP/proxy_check.php"; +$wgProxyScriptPath = "$IP/includes/proxy_check.php"; /** */ $wgProxyMemcExpiry = 86400; /** This should always be customised in LocalSettings.php */ @@ -797,9 +1203,19 @@ /** * Set this to current time to invalidate all prior cached pages. Affects both * client- and server-side caching. + * You can get the current date on your server by using the command: + * date +%Y%m%d%H%M%S */ $wgCacheEpoch = '20030516000000'; +/** + * Bump this number when changing the global style sheets and JavaScript. + * It should be appended in the query string of static CSS and JS includes, + * to ensure that client-side caches don't keep obsolete copies of global + * styles. + */ +$wgStyleVersion = '97'; + # Server-side caching: @@ -809,8 +1225,9 @@ * Must set $wgShowIPinHeader = false */ $wgUseFileCache = false; + /** Directory where the cached page will be saved */ -$wgFileCacheDirectory = "{$wgUploadDirectory}/cache"; +$wgFileCacheDirectory = false; /// defaults to "{$wgUploadDirectory}/cache"; /** * When using the file cache, we can store the cached HTML gzipped to save disk @@ -822,6 +1239,9 @@ */ $wgUseGzip = false; +/** Whether MediaWiki should send an ETag header */ +$wgUseETag = false; + # Email notification settings # @@ -843,11 +1263,40 @@ $wgEnotifMinorEdits = true; # UPO; false: "minor edits" on pages do not trigger notification mails. # # Attention: _every_ change on a user_talk page trigger a notification mail (if the user is not yet notified) +# Send a generic mail instead of a personalised mail for each user. This +# always uses UTC as the time zone, and doesn't include the username. +# +# For pages with many users watching, this can significantly reduce mail load. +# Has no effect when using sendmail rather than SMTP; + +$wgEnotifImpersonal = false; + +# Maximum number of users to mail at once when using impersonal mail. Should +# match the limit on your mail server. +$wgEnotifMaxRecips = 500; + +# Send mails via the job queue. +$wgEnotifUseJobQ = false; + +/** + * Array of usernames who will be sent a notification email for every change which occurs on a wiki + */ +$wgUsersNotifedOnAllChanges = array(); /** Show watching users in recent changes, watchlist and page history views */ $wgRCShowWatchingUsers = false; # UPO /** Show watching users in Page views */ $wgPageShowWatchingUsers = false; +/** Show the amount of changed characters in recent changes */ +$wgRCShowChangedSize = true; + +/** + * If the difference between the character counts of the text + * before and after the edit is below that value, the value will be + * highlighted on the RC page. + */ +$wgRCChangedSizeThreshold = -500; + /** * Show "Updated (since my last visit)" marker in RC view, watchlist and history * view for watched pages with new changes */ @@ -885,7 +1334,11 @@ /** * A list of proxy servers (ips if possible) to purge on changes don't specify - * ports here (80 is default) + * ports here (80 is default). When mediawiki is running behind a proxy, its + * address should be listed in $wgSquidServers otherwise mediawiki won't rely + * on the X-FORWARDED-FOR header to determine the user IP address and + * all users will appear to come from the proxy IP address. Don't use domain + * names here, only IP adresses. */ # $wgSquidServers = array('127.0.0.1'); $wgSquidServers = array(); @@ -898,6 +1351,7 @@ $wgHTCPPort = 4827; $wgHTCPMulticastTTL = 1; # $wgHTCPMulticastAddress = "224.0.0.85"; +$wgHTCPMulticastAddress = false; # Cookie settings: # @@ -907,21 +1361,46 @@ */ $wgCookieDomain = ''; $wgCookiePath = '/'; +$wgCookieSecure = ($wgProto == 'https'); $wgDisableCookieCheck = false; +/** Override to customise the session name */ +$wgSessionName = false; + /** Whether to allow inline image pointing to other websites */ -$wgAllowExternalImages = true; +$wgAllowExternalImages = false; + +/** If the above is false, you can specify an exception here. Image URLs + * that start with this string are then rendered, while all others are not. + * You can use this to set up a trusted, simple repository of images. + * + * Example: + * $wgAllowExternalImagesFrom = 'http://127.0.0.1/'; + */ +$wgAllowExternalImagesFrom = ''; /** Disable database-intensive features */ $wgMiserMode = false; /** Disable all query pages if miser mode is on, not just some */ $wgDisableQueryPages = false; -/** Generate a watchlist once every hour or so */ -$wgUseWatchlistCache = false; -/** The hour or so mentioned above */ -$wgWLCacheTimeout = 3600; -/** Minimum number of links to a page required before it appears on Special:Wantedpages */ +/** Number of rows to cache in 'querycache' table when miser mode is on */ +$wgQueryCacheLimit = 1000; +/** Number of links to a page required before it is deemed "wanted" */ $wgWantedPagesThreshold = 1; +/** Enable slow parser functions */ +$wgAllowSlowParserFunctions = false; + +/** + * Maps jobs to their handling classes; extensions + * can add to this to provide custom jobs + */ +$wgJobClasses = array( + 'refreshLinks' => 'RefreshLinksJob', + 'htmlCacheUpdate' => 'HTMLCacheUpdateJob', + 'html_cache_update' => 'HTMLCacheUpdateJob', // backwards-compatible + 'sendMail' => 'EmaillingJob', + 'enotifNotify' => 'EnotifNotifyJob', +); /** * To use inline TeX, you need to compile 'texvc' (in the 'math' subdirectory of @@ -936,19 +1415,25 @@ # # Profiling / debugging # +# You have to create a 'profiling' table in your database before using +# profiling see maintenance/archives/patch-profiling.sql . +# +# To enable profiling, edit StartProfiler.php -/** Enable for more detailed by-function times in debug log */ -$wgProfiling = false; /** Only record profiling info for pages that took longer than this */ $wgProfileLimit = 0.0; /** Don't put non-profiling info into log file */ $wgProfileOnly = false; /** Log sums from profiling into "profiling" table in db. */ $wgProfileToDatabase = false; -/** Only profile every n requests when profiling is turned on */ -$wgProfileSampleRate = 1; /** If true, print a raw call tree instead of per-function report */ $wgProfileCallTree = false; +/** Should application server host be put into profiling table */ +$wgProfilePerHost = false; + +/** Settings for UDP profiler */ +$wgUDPProfilerHost = '127.0.0.1'; +$wgUDPProfilerPort = '3811'; /** Detects non-matching wfProfileIn/wfProfileOut calls */ $wgDebugProfiling = false; @@ -957,8 +1442,13 @@ /** Lots of debugging output from SquidUpdate.php */ $wgDebugSquid = false; +/** Whereas to count the number of time an article is viewed. + * Does not work if pages are cached (for example with squid). + */ $wgDisableCounters = false; + $wgDisableTextSearch = false; +$wgDisableSearchContext = false; /** * If you've disabled search semi-permanently, this also disables updates to the * table. If you ever re-enable, be sure to rebuild the search table. @@ -969,6 +1459,13 @@ /** * Show EXIF data, on by default if available. * Requires PHP's EXIF extension: http://www.php.net/manual/en/ref.exif.php + * + * NOTE FOR WINDOWS USERS: + * To enable EXIF functions, add the folloing lines to the + * "Windows extensions" section of php.ini: + * + * extension=extensions/php_mbstring.dll + * extension=extensions/php_exif.dll */ $wgShowEXIF = function_exists( 'exif_read_data' ); @@ -991,8 +1488,8 @@ * ALF_PRELOAD_EXISTENCE * Preload cur_id during replaceLinkHolders * ALF_NO_LINK_LOCK - * Don't use locking reads when updating the link table. This is - * necessary for wikis with a high edit rate for performance + * Don't use locking reads when updating the link table. This is + * necessary for wikis with a high edit rate for performance * reasons, but may cause link table inconsistency * ALF_NO_BLOCK_LOCK * As for ALF_LINK_LOCK, this flag is a necessity for high-traffic @@ -1007,7 +1504,7 @@ $wgDiff3 = '/usr/bin/diff3'; /** - * We can also compress text in the old revisions table. If this is set on, old + * We can also compress text stored in the 'text' table. If this is set on, new * revisions will be compressed on page save if zlib support is available. Any * compressed revisions will be decompressed on load regardless of this setting * *but will not be readable at all* if zlib support is not available. @@ -1023,9 +1520,9 @@ /** Files with these extensions will never be allowed as uploads. */ $wgFileBlacklist = array( # HTML may contain cookie-stealing JavaScript and web bugs - 'html', 'htm', 'js', 'jsb', + 'html', 'htm', 'js', 'jsb', 'mhtml', 'mht', # PHP scripts may execute arbitrary code on the server - 'php', 'phtml', 'php3', 'php4', 'phps', + 'php', 'phtml', 'php3', 'php4', 'php5', 'phps', # Other types that may be interpreted by some servers 'shtml', 'jhtml', 'pl', 'py', 'cgi', # May contain harmful executables for Windows victims @@ -1054,7 +1551,7 @@ */ $wgStrictFileExtensions = true; -/** Warn if uploaded files are larger than this */ +/** Warn if uploaded files are larger than this (in bytes)*/ $wgUploadSizeWarning = 150 * 1024; /** For compatibility with old installations set to false */ @@ -1065,35 +1562,53 @@ */ $wgNamespacesWithSubpages = array( NS_TALK => true, - NS_USER => true, - NS_USER_TALK => true, - NS_PROJECT_TALK => true, - NS_IMAGE_TALK => true, - NS_MEDIAWIKI_TALK => true, - NS_TEMPLATE_TALK => true, - NS_HELP_TALK => true, - NS_CATEGORY_TALK => true - ); + NS_USER => true, + NS_USER_TALK => true, + NS_PROJECT_TALK => true, + NS_IMAGE_TALK => true, + NS_MEDIAWIKI_TALK => true, + NS_TEMPLATE_TALK => true, + NS_HELP_TALK => true, + NS_CATEGORY_TALK => true +); $wgNamespacesToBeSearchedDefault = array( NS_MAIN => true, ); -/** If set, a bold ugly notice will show up at the top of every page. */ +/** + * Site notice shown at the top of each page + * + * This message can contain wiki text, and can also be set through the + * MediaWiki:Sitenotice page. You can also provide a separate message for + * logged-out users using the MediaWiki:Anonnotice page. + */ $wgSiteNotice = ''; - # # Images settings # -/** dynamic server side image resizing ("Thumbnails") */ -$wgUseImageResize = false; +/** + * Plugins for media file type handling. + * Each entry in the array maps a MIME type to a class name + */ +$wgMediaHandlers = array( + 'image/jpeg' => 'BitmapHandler', + 'image/png' => 'BitmapHandler', + 'image/gif' => 'BitmapHandler', + 'image/x-ms-bmp' => 'BmpHandler', + 'image/svg+xml' => 'SvgHandler', + 'image/svg' => 'SvgHandler', + 'image/vnd.djvu' => 'DjVuHandler', +); + /** * Resizing can be done using PHP's internal image libraries or using - * ImageMagick. The later supports more file formats than PHP, which only - * supports PNG, GIF, JPG, XBM and WBMP. + * ImageMagick or another third-party converter, e.g. GraphicMagick. + * These support more file formats than PHP, which only supports PNG, + * GIF, JPG, XBM and WBMP. * * Use Image Magick instead of PHP builtin functions. */ @@ -1101,13 +1616,30 @@ /** The convert command shipped with ImageMagick */ $wgImageMagickConvertCommand = '/usr/bin/convert'; +/** Sharpening parameter to ImageMagick */ +$wgSharpenParameter = '0x0.4'; + +/** Reduction in linear dimensions below which sharpening will be enabled */ +$wgSharpenReductionThreshold = 0.85; + +/** + * Use another resizing converter, e.g. GraphicMagick + * %s will be replaced with the source path, %d with the destination + * %w and %h will be replaced with the width and height + * + * An example is provided for GraphicMagick + * Leave as false to skip this + */ +#$wgCustomConvertCommand = "gm convert %s -resize %wx%h %d" +$wgCustomConvertCommand = false; + # Scalable Vector Graphics (SVG) may be uploaded as images. # Since SVG support is not yet standard in browsers, it is # necessary to rasterize SVGs to PNG as a fallback format. # # An external program is required to perform this conversion: $wgSVGConverters = array( - 'ImageMagick' => '$path/convert -background white -geometry $width $input $output', + 'ImageMagick' => '$path/convert -background white -geometry $width $input PNG:$output', 'sodipodi' => '$path/sodipodi -z -w $width -f $input -e $output', 'inkscape' => '$path/inkscape -z -w $width -f $input -e $output', 'batik' => 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', @@ -1117,21 +1649,62 @@ $wgSVGConverter = 'ImageMagick'; /** If not in the executable PATH, specify */ $wgSVGConverterPath = ''; -/** Don't scale a SVG larger than this unless its native size is larger */ +/** Don't scale a SVG larger than this */ $wgSVGMaxSize = 1024; +/** + * Don't thumbnail an image if it will use too much working memory + * Default is 50 MB if decompressed to RGBA form, which corresponds to + * 12.5 million pixels or 3500x3500 + */ +$wgMaxImageArea = 1.25e7; +/** + * If rendered thumbnail files are older than this timestamp, they + * will be rerendered on demand as if the file didn't already exist. + * Update if there is some need to force thumbs and SVG rasterizations + * to rerender, such as fixes to rendering bugs. + */ +$wgThumbnailEpoch = '20030516000000'; + +/** + * If set, inline scaled images will still produce tags ready for + * output instead of showing an error message. + * + * This may be useful if errors are transitory, especially if the site + * is configured to automatically render thumbnails on request. + * + * On the other hand, it may obscure error conditions from debugging. + * Enable the debug log or the 'thumbnail' log group to make sure errors + * are logged to a file for review. + */ +$wgIgnoreImageErrors = false; + +/** + * Allow thumbnail rendering on page view. If this is false, a valid + * thumbnail URL is still output, but no file will be created at + * the target location. This may save some time if you have a + * thumb.php or 404 handler set up which is faster than the regular + * webserver(s). + */ +$wgGenerateThumbnailOnParse = true; + +/** Obsolete, always true, kept for compatibility with extensions */ +$wgUseImageResize = true; + /** Set $wgCommandLineMode if it's not set already, to avoid notices */ if( !isset( $wgCommandLineMode ) ) { $wgCommandLineMode = false; } +/** For colorized maintenance script output, is your terminal background dark ? */ +$wgCommandLineDarkBg = false; # # Recent changes settings # -/** Log IP addresses in the recentchanges table */ -$wgPutIPinRC = false; +/** Log IP addresses in the recentchanges table; can be accessed only by extensions (e.g. CheckUser) or a DB admin */ +$wgPutIPinRC = true; /** * Recentchanges items are periodically purged; entries older than this many @@ -1205,23 +1778,51 @@ */ $wgImportSources = array(); +/** + * Optional default target namespace for interwiki imports. + * Can use this to create an incoming "transwiki"-style queue. + * Set to numeric key, not the name. + * + * Users may override this in the Special:Import dialog. + */ +$wgImportTargetNamespace = null; + +/** + * If set to false, disables the full-history option on Special:Export. + * This is currently poorly optimized for long edit histories, so is + * disabled on Wikimedia's sites. + */ +$wgExportAllowHistory = true; + +/** + * If set nonzero, Special:Export requests for history of pages with + * more revisions than this will be rejected. On some big sites things + * could get bogged down by very very long pages. + */ +$wgExportMaxHistory = 0; + +$wgExportAllowListContributors = false ; /** Text matching this regular expression will be recognised as spam * See http://en.wikipedia.org/wiki/Regular_expression */ $wgSpamRegex = false; -/** Similarly if this function returns true */ +/** Similarly you can get a function to do the job. The function will be given + * the following args: + * - a Title object for the article the edit is made on + * - the text submitted in the textarea (wpTextbox1) + * - the section number. + * The return should be boolean indicating whether the edit matched some evilness: + * - true : block it + * - false : let it through + * + * For a complete example, have a look at the SpamBlacklist extension. + */ $wgFilterCallback = false; /** Go button goes straight to the edit screen if the article doesn't exist. */ $wgGoToEdit = false; -/** Allow limited user-specified HTML in wiki pages? - * It will be run through a whitelist for security. Set this to false if you - * want wiki pages to consist only of wiki markup. Note that replacements do not - * yet exist for all HTML constructs.*/ -$wgUserHtml = true; - /** Allow raw, unchecked HTML in ... sections. * THIS IS VERY DANGEROUS on a publically editable site, so USE wgGroupPermissions * TO RESTRICT EDITING to only those that you trust @@ -1230,8 +1831,7 @@ /** * $wgUseTidy: use tidy to make sure HTML output is sane. - * This should only be enabled if $wgUserHtml is true. - * tidy is a free tool that fixes broken HTML. + * Tidy is a free tool that fixes broken HTML. * See http://www.w3.org/People/Raggett/tidy/ * $wgTidyBin should be set to the path of the binary and * $wgTidyConf to the path of the configuration file. @@ -1244,8 +1844,9 @@ * 'extension=tidy.so' to php.ini. */ $wgUseTidy = false; +$wgAlwaysUseTidy = false; $wgTidyBin = 'tidy'; -$wgTidyConf = $IP.'/extensions/tidy/tidy.conf'; +$wgTidyConf = $IP.'/includes/tidy.conf'; $wgTidyOpts = ''; $wgTidyInternal = function_exists( 'tidy_load_config' ); @@ -1253,24 +1854,109 @@ $wgDefaultSkin = 'monobook'; /** - * Settings added to this array will override the language globals for the user - * preferences used by anonymous visitors and newly created accounts. (See names - * and sample values in languages/Language.php) + * Settings added to this array will override the default globals for the user + * preferences used by anonymous visitors and newly created accounts. * For instance, to disable section editing links: - * $wgDefaultUserOptions ['editsection'] = 0; + * $wgDefaultUserOptions ['editsection'] = 0; * */ -$wgDefaultUserOptions = array(); +$wgDefaultUserOptions = array( + 'quickbar' => 1, + 'underline' => 2, + 'cols' => 80, + 'rows' => 25, + 'searchlimit' => 20, + 'contextlines' => 5, + 'contextchars' => 50, + 'skin' => false, + 'math' => 1, + 'rcdays' => 7, + 'rclimit' => 50, + 'wllimit' => 250, + 'highlightbroken' => 1, + 'stubthreshold' => 0, + 'previewontop' => 1, + 'editsection' => 1, + 'editsectiononrightclick'=> 0, + 'showtoc' => 1, + 'showtoolbar' => 1, + 'date' => 'default', + 'imagesize' => 2, + 'thumbsize' => 2, + 'rememberpassword' => 0, + 'enotifwatchlistpages' => 0, + 'enotifusertalkpages' => 1, + 'enotifminoredits' => 0, + 'enotifrevealaddr' => 0, + 'shownumberswatching' => 1, + 'fancysig' => 0, + 'externaleditor' => 0, + 'externaldiff' => 0, + 'showjumplinks' => 1, + 'numberheadings' => 0, + 'uselivepreview' => 0, + 'watchlistdays' => 3.0, +); /** Whether or not to allow and use real name fields. Defaults to true. */ $wgAllowRealName = true; -/** Use XML parser? */ -$wgUseXMLparser = false ; +/***************************************************************************** + * Extensions + */ -/** Extensions */ -$wgSkinExtensionFunctions = array(); +/** + * A list of callback functions which are called once MediaWiki is fully initialised + */ $wgExtensionFunctions = array(); + +/** + * Extension functions for initialisation of skins. This is called somewhat earlier + * than $wgExtensionFunctions. + */ +$wgSkinExtensionFunctions = array(); + +/** + * Extension messages files + * Associative array mapping extension name to the filename where messages can be found. + * The file must create a variable called $messages. + * When the messages are needed, the extension should call wfLoadMessagesFile() + */ +$wgExtensionMessagesFiles = array(); + +/** + * Parser output hooks. + * This is an associative array where the key is an extension-defined tag + * (typically the extension name), and the value is a PHP callback. + * These will be called as an OutputPageParserOutput hook, if the relevant + * tag has been registered with the parser output object. + * + * Registration is done with $pout->addOutputHook( $tag, $data ). + * + * The callback has the form: + * function outputHook( $outputPage, $parserOutput, $data ) { ... } + */ +$wgParserOutputHooks = array(); + +/** + * List of valid skin names. + * The key should be the name in all lower case, the value should be a display name. + * The default skins will be added later, by Skin::getSkinNames(). Use + * Skin::getSkinNames() as an accessor if you wish to have access to the full list. + */ +$wgValidSkinNames = array(); + +/** + * Special page list. + * See the top of SpecialPage.php for documentation. + */ +$wgSpecialPages = array(); + +/** + * Array mapping class names to filenames, for autoloading. + */ +$wgAutoloadClasses = array(); + /** * An array of extension types and inside that their names, versions, authors * and urls, note that the version and url key can be omitted. @@ -1287,6 +1973,9 @@ * Where $type is 'specialpage', 'parserhook', or 'other'. */ $wgExtensionCredits = array(); +/* + * end extensions + ******************************************************************************/ /** * Allow user Javascript page? @@ -1317,8 +2006,8 @@ /** Maximum indent level of toc. */ $wgMaxTocLevel = 999; -/** Use external C++ diff engine (module wikidiff from the extensions package) */ -$wgUseExternalDiffEngine = false; +/** Name of the external diff engine to use */ +$wgExternalDiffEngine = false; /** Use RC Patrolling to check for vandalism */ $wgUseRCPatrol = true; @@ -1329,7 +2018,11 @@ /** _Minimum_ timeout for cached Recentchanges feed, in seconds. * A cached version will continue to be served out even if changes - * are made, until this many seconds runs out since the last render. */ + * are made, until this many seconds runs out since the last render. + * + * If set to 0, feed caching is disabled. Use this for debugging only; + * feed generation can be pretty slow with diffs. + */ $wgFeedCacheTimeout = 60; /** When generating Recentchanges RSS/Atom feed, diffs will not be generated for @@ -1357,9 +2050,28 @@ $wgExtraNamespaces = NULL; /** + * Namespace aliases + * These are alternate names for the primary localised namespace names, which + * are defined by $wgExtraNamespaces and the language file. If a page is + * requested with such a prefix, the request will be redirected to the primary + * name. + * + * Set this to a map from namespace names to IDs. + * Example: + * $wgNamespaceAliases = array( + * 'Wikipedian' => NS_USER, + * 'Help' => 100, + * ); + */ +$wgNamespaceAliases = array(); + +/** * Limit images on image description pages to a user-selectable limit. In order - * to reduce disk usage, limits can only be selected from a list. This is the - * list of settings the user can choose from: + * to reduce disk usage, limits can only be selected from a list. + * The user preference is saved as an array offset in the database, by default + * the offset is set with $wgDefaultUserOptions['imagesize']. Make sure you + * change it if you alter the array (see bug 8858). + * This is the list of settings the user can choose from: */ $wgImageLimits = array ( array(320,240), @@ -1384,13 +2096,20 @@ ); /** + * Adjust width of upright images when parameter 'upright' is used + * This allows a nicer look for upright images without the need to fix the width + * by hardcoded px in wiki sourcecode. + */ +$wgThumbUpright = 0.75; + +/** * On category pages, show thumbnail gallery for images belonging to that * category instead of listing them as articles. */ $wgCategoryMagicGallery = true; /** - * Paging limit for items in categories + * Paging limit for categories */ $wgCategoryPagingLimit = 200; @@ -1399,7 +2118,20 @@ * Contains a list of regexps : "/regexp/" matching problematic browsers */ $wgBrowserBlackList = array( - "/Mozilla\/4\.78 \[en\] \(X11; U; Linux/", + /** + * Netscape 2-4 detection + * The minor version may contain strings such as "Gold" or "SGoldC-SGI" + * Lots of non-netscape user agents have "compatible", so it's useful to check for that + * with a negative assertion. The [UIN] identifier specifies the level of security + * in a Netscape/Mozilla browser, checking for it rules out a number of fakers. + * The language string is unreliable, it is missing on NS4 Mac. + * + * Reference: http://www.psychedelix.com/agents/index.shtml + */ + '/^Mozilla\/2\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', + '/^Mozilla\/3\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', + '/^Mozilla\/4\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/', + /** * MSIE on Mac OS 9 is teh sux0r, converts þ to , ð to , Þ to and Ð to * @@ -1412,7 +2144,13 @@ * @link http://en.wikipedia.org/w/index.php?title=User%3A%C6var_Arnfj%F6r%F0_Bjarmason%2Ftestme&diff=12356041&oldid=12355864 * @link http://en.wikipedia.org/wiki/Template%3AOS9 */ - "/Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/" + '/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/', + + /** + * Google wireless transcoder, seems to eat a lot of chars alive + * http://it.wikipedia.org/w/index.php?title=Luciano_Ligabue&diff=prev&oldid=8857361 + */ + '/^Mozilla\/4\.0 \(compatible; MSIE 6.0; Windows NT 5.0; Google Wireless Transcoder;\)/' ); /** @@ -1430,21 +2168,21 @@ $wgLocaltimezone = null; /** - * Set an offset from UTC in hours to use for the default timezone setting + * Set an offset from UTC in minutes to use for the default timezone setting * for anonymous users and new user accounts. * * This setting is used for most date/time displays in the software, and is * overrideable in user preferences. It is *not* used for signature timestamps. * * You can set it to match the configured server timezone like this: - * $wgLocalTZoffset = date("Z") / 3600; + * $wgLocalTZoffset = date("Z") / 60; * * If your server is not configured for the timezone you want, you can set * this in conjunction with the signature timezone and override the TZ * environment variable like so: * $wgLocaltimezone="Europe/Berlin"; * putenv("TZ=$wgLocaltimezone"); - * $wgLocalTZoffset = date("Z") / 3600; + * $wgLocalTZoffset = date("Z") / 60; * * Leave at NULL to show times in universal time (UTC/GMT). */ @@ -1486,6 +2224,84 @@ $wgHooks = array(); /** + * The logging system has two levels: an event type, which describes the + * general category and can be viewed as a named subset of all logs; and + * an action, which is a specific kind of event that can exist in that + * log type. + */ +$wgLogTypes = array( '', + 'block', + 'protect', + 'rights', + 'delete', + 'upload', + 'move', + 'import', + 'patrol', +); + +/** + * Lists the message key string for each log type. The localized messages + * will be listed in the user interface. + * + * Extensions with custom log types may add to this array. + */ +$wgLogNames = array( + '' => 'all-logs-page', + 'block' => 'blocklogpage', + 'protect' => 'protectlogpage', + 'rights' => 'rightslog', + 'delete' => 'dellogpage', + 'upload' => 'uploadlogpage', + 'move' => 'movelogpage', + 'import' => 'importlogpage', + 'patrol' => 'patrol-log-page', +); + +/** + * Lists the message key string for descriptive text to be shown at the + * top of each log type. + * + * Extensions with custom log types may add to this array. + */ +$wgLogHeaders = array( + '' => 'alllogstext', + 'block' => 'blocklogtext', + 'protect' => 'protectlogtext', + 'rights' => 'rightslogtext', + 'delete' => 'dellogpagetext', + 'upload' => 'uploadlogpagetext', + 'move' => 'movelogpagetext', + 'import' => 'importlogpagetext', + 'patrol' => 'patrol-log-header', +); + +/** + * Lists the message key string for formatting individual events of each + * type and action when listed in the logs. + * + * Extensions with custom log types may add to this array. + */ +$wgLogActions = array( + 'block/block' => 'blocklogentry', + 'block/unblock' => 'unblocklogentry', + 'protect/protect' => 'protectedarticle', + 'protect/modify' => 'modifiedarticleprotection', + 'protect/unprotect' => 'unprotectedarticle', + 'rights/rights' => 'rightslogentry', + 'delete/delete' => 'deletedarticle', + 'delete/restore' => 'undeletedarticle', + 'delete/revision' => 'revdelete-logentry', + 'upload/upload' => 'uploadedimage', + 'upload/overwrite' => 'overwroteimage', + 'upload/revert' => 'uploadedimage', + 'move/move' => '1movedto2', + 'move/move_redir' => '1movedto2_redir', + 'import/upload' => 'import-logentry-upload', + 'import/interwiki' => 'import-logentry-interwiki', +); + +/** * Experimental preview feature to fetch rendered text * over an XMLHttpRequest from JavaScript instead of * forcing a submit and reload of the whole page. @@ -1521,6 +2337,31 @@ $wgNoFollowLinks = true; /** + * Namespaces in which $wgNoFollowLinks doesn't apply. + * See Language.php for a list of namespaces. + */ +$wgNoFollowNsExceptions = array(); + +/** + * Robot policies per namespaces. + * The default policy is 'index,follow', the array is made of namespace + * constants as defined in includes/Defines.php + * Example: + * $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' ); + */ +$wgNamespaceRobotPolicies = array(); + +/** + * Robot policies per article. + * These override the per-namespace robot policies. + * Must be in the form of an array where the key part is a properly + * canonicalised text form title and the value is a robot policy. + * Example: + * $wgArticleRobotPolicies = array( 'Main Page' => 'noindex' ); + */ +$wgArticleRobotPolicies = array(); + +/** * Specifies the minimal length of a user password. If set to * 0, empty passwords are allowed. */ @@ -1559,12 +2400,7 @@ * Use http.dnsbl.sorbs.net to check for open proxies */ $wgEnableSorbs = false; - -/** - * Use opm.blitzed.org to check for open proxies. - * Not yet actually used. - */ -$wgEnableOpm = false; +$wgSorbsUrl = 'http.dnsbl.sorbs.net.'; /** * Proxy whitelist, list of addresses that are assumed to be non-proxy despite what the other @@ -1597,6 +2433,12 @@ 'ip' => null, 'subnet' => null, ), + 'mailpassword' => array( + 'anon' => NULL, + ), + 'emailuser' => array( + 'user' => null, + ), ); /** @@ -1605,6 +2447,11 @@ $wgRateLimitLog = null; /** + * Array of groups which should never trigger the rate limiter + */ +$wgRateLimitsExcludedGroups = array( 'sysop', 'bureaucrat' ); + +/** * On Special:Unusedimages, consider images "used", if they are put * into a category. Default (false) is not to count those as used. */ @@ -1628,6 +2475,26 @@ $wgExternalServers = array(); /** + * The place to put new revisions, false to put them in the local text table. + * Part of a URL, e.g. DB://cluster1 + * + * Can be an array instead of a single string, to enable data distribution. Keys + * must be consecutive integers, starting at zero. Example: + * + * $wgDefaultExternalStore = array( 'DB://cluster1', 'DB://cluster2' ); + * + */ +$wgDefaultExternalStore = false; + +/** + * Revision text may be cached in $wgMemc to reduce load on external storage + * servers and object extraction overhead for frequently-loaded revisions. + * + * Set to 0 to disable, or number of seconds before cache expiry. + */ +$wgRevisionCacheExpiry = 0; + +/** * list of trusted media-types and mime types. * Use the MEDIATYPE_xxx constants to represent media types. * This list is used by Image::isSafeFile @@ -1641,9 +2508,9 @@ MEDIATYPE_BITMAP, //all bitmap formats MEDIATYPE_AUDIO, //all audio formats MEDIATYPE_VIDEO, //all plain video formats - "image/svg", //svg (only needed if inline rendering of svg is not supported) + "image/svg+xml", //svg (only needed if inline rendering of svg is not supported) "application/pdf", //PDF files - #"application/x-shockwafe-flash", //flash/shockwave movie + #"application/x-shockwave-flash", //flash/shockwave movie ); /** @@ -1665,6 +2532,10 @@ * Enable interwiki transcluding. Only when iw_trans=1. */ $wgEnableScaryTranscluding = false; +/** + * Expiry time for interwiki transclusion + */ +$wgTranscludeCacheExpiry = 3600; /** * Support blog-style "trackbacks" for articles. See @@ -1672,4 +2543,208 @@ */ $wgUseTrackbacks = false; -?> +/** + * Enable filtering of categories in Recentchanges + */ +$wgAllowCategorizedRecentChanges = false ; + +/** + * Number of jobs to perform per request. May be less than one in which case + * jobs are performed probabalistically. If this is zero, jobs will not be done + * during ordinary apache requests. In this case, maintenance/runJobs.php should + * be run periodically. + */ +$wgJobRunRate = 1; + +/** + * Number of rows to update per job + */ +$wgUpdateRowsPerJob = 500; + +/** + * Number of rows to update per query + */ +$wgUpdateRowsPerQuery = 10; + +/** + * Enable AJAX framework + */ +$wgUseAjax = true; + +/** + * Enable auto suggestion for the search bar + * Requires $wgUseAjax to be true too. + * Causes wfSajaxSearch to be added to $wgAjaxExportList + */ +$wgAjaxSearch = false; + +/** + * List of Ajax-callable functions. + * Extensions acting as Ajax callbacks must register here + */ +$wgAjaxExportList = array( ); + +/** + * Enable watching/unwatching pages using AJAX. + * Requires $wgUseAjax to be true too. + * Causes wfAjaxWatch to be added to $wgAjaxExportList + */ +$wgAjaxWatch = true; + +/** + * Enable AJAX check for file overwrite, pre-upload + */ +$wgAjaxUploadDestCheck = true; + +/** + * Enable previewing licences via AJAX + */ +$wgAjaxLicensePreview = true; + +/** + * Allow DISPLAYTITLE to change title display + */ +$wgAllowDisplayTitle = true; + +/** + * Array of usernames which may not be registered or logged in from + * Maintenance scripts can still use these + */ +$wgReservedUsernames = array( + 'MediaWiki default', // Default 'Main Page' and MediaWiki: message pages + 'Conversion script', // Used for the old Wikipedia software upgrade + 'Maintenance script', // Maintenance scripts which perform editing, image import script + 'Template namespace initialisation script', // Used in 1.2->1.3 upgrade +); + +/** + * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic browsers which can't + * perform basic stuff like MIME detection and which are vulnerable to further idiots uploading + * crap files as images. When this directive is on, will be allowed in files with + * an "image/svg+xml" MIME type. You should leave this disabled if your web server is misconfigured + * and doesn't send appropriate MIME types for SVG images. + */ +$wgAllowTitlesInSVG = false; + +/** + * Array of namespaces which can be deemed to contain valid "content", as far + * as the site statistics are concerned. Useful if additional namespaces also + * contain "content" which should be considered when generating a count of the + * number of articles in the wiki. + */ +$wgContentNamespaces = array( NS_MAIN ); + +/** + * Maximum amount of virtual memory available to shell processes under linux, in KB. + */ +$wgMaxShellMemory = 102400; + +/** + * Maximum file size created by shell processes under linux, in KB + * ImageMagick convert for example can be fairly hungry for scratch space + */ +$wgMaxShellFileSize = 102400; + +/** + * DJVU settings + * Path of the djvudump executable + * Enable this and $wgDjvuRenderer to enable djvu rendering + */ +# $wgDjvuDump = 'djvudump'; +$wgDjvuDump = null; + +/** + * Path of the ddjvu DJVU renderer + * Enable this and $wgDjvuDump to enable djvu rendering + */ +# $wgDjvuRenderer = 'ddjvu'; +$wgDjvuRenderer = null; + +/** + * Path of the djvutoxml executable + * This works like djvudump except much, much slower as of version 3.5. + * + * For now I recommend you use djvudump instead. The djvuxml output is + * probably more stable, so we'll switch back to it as soon as they fix + * the efficiency problem. + * http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583 + */ +# $wgDjvuToXML = 'djvutoxml'; +$wgDjvuToXML = null; + + +/** + * Shell command for the DJVU post processor + * Default: pnmtopng, since ddjvu generates ppm output + * Set this to false to output the ppm file directly. + */ +$wgDjvuPostProcessor = 'pnmtojpeg'; +/** + * File extension for the DJVU post processor output + */ +$wgDjvuOutputExtension = 'jpg'; + +/** + * Enable the MediaWiki API for convenient access to + * machine-readable data via api.php + * + * See http://www.mediawiki.org/wiki/API + */ +$wgEnableAPI = true; + +/** + * Allow the API to be used to perform write operations + * (page edits, rollback, etc.) when an authorised user + * accesses it + */ +$wgEnableWriteAPI = false; + +/** + * API module extensions + * Associative array mapping module name to class name. + * Extension modules may override the core modules. + */ +$wgAPIModules = array(); + +/** + * Parser test suite files to be run by parserTests.php when no specific + * filename is passed to it. + * + * Extensions may add their own tests to this array, or site-local tests + * may be added via LocalSettings.php + * + * Use full paths. + */ +$wgParserTestFiles = array( + "$IP/maintenance/parserTests.txt", +); + +/** + * Break out of framesets. This can be used to prevent external sites from + * framing your site with ads. + */ +$wgBreakFrames = false; + +/** + * Set this to an array of special page names to prevent + * maintenance/updateSpecialPages.php from updating those pages. + */ +$wgDisableQueryPageUpdate = false; + +/** + * Set this to false to disable cascading protection + */ +$wgEnableCascadingProtection = true; + +/** + * Disable output compression (enabled by default if zlib is available) + */ +$wgDisableOutputCompression = false; + +/** + * If lag is higher than $wgSlaveLagWarning, show a warning in some special + * pages (like watchlist). If the lag is higher than $wgSlaveLagCritical, + * show a more obvious warning. + */ +$wgSlaveLagWarning = 10; +$wgSlaveLagCritical = 30; diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Defines.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Defines.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Defines.php 2005-09-13 03:04:42.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Defines.php 2007-06-29 16:02:26.000000000 -0400 @@ -1,9 +1,13 @@ <?php /** * A few constants that might be needed during LocalSettings.php - * @package MediaWiki */ +/** + * Version constants for the benefit of extensions + */ +define( 'MW_SPECIALPAGE_VERSION', 2 ); + /**#@+ * Database related constants */ @@ -15,6 +19,17 @@ define( 'DBO_PERSISTENT', 32 ); /**#@-*/ +# Valid database indexes +# Operation-based indexes +define( 'DB_SLAVE', -1 ); # Read from the slave (or only server) +define( 'DB_MASTER', -2 ); # Write to master (or only server) +define( 'DB_LAST', -3 ); # Whatever database was used last + +# Obsolete aliases +define( 'DB_READ', -1 ); +define( 'DB_WRITE', -2 ); + + /**#@+ * Virtual namespaces; don't appear in the page database */ @@ -70,15 +85,28 @@ /**#@-*/ /** - * User rights management - * a big array of string defining a right, that's how they are saved in the - * database. - * @todo Is this necessary? - */ -$wgAvailableRights = array('read', 'edit', 'move', 'delete', 'undelete', -'protect', 'block', 'userrights', 'createaccount', 'upload', -'rollback', 'patrol', 'editinterface', 'siteadmin', 'bot', 'validate', -'import', 'importupload', 'renameuser' ); + * User rights list + * @deprecated + */ +$wgAvailableRights = array( + 'block', + 'bot', + 'createaccount', + 'delete', + 'edit', + 'editinterface', + 'import', + 'importupload', + 'move', + 'patrol', + 'protect', + 'read', + 'rollback', + 'siteadmin', + 'unwatchedpages', + 'upload', + 'userrights', +); /**#@+ * Cache type @@ -88,15 +116,15 @@ define( 'CACHE_DB', 1 ); // Store cache objects in the DB define( 'CACHE_MEMCACHED', 2 ); // MemCached, must specify servers in $wgMemCacheServers define( 'CACHE_ACCEL', 3 ); // eAccelerator or Turck, whichever is available +define( 'CACHE_DBA', 4 ); // Use PHP's DBA extension to store in a DBM-style database /**#@-*/ /**#@+ - * Media types. + * Media types. * This defines constants for the value returned by Image::getMediaType() */ - define( 'MEDIATYPE_UNKNOWN', 'UNKNOWN' ); // unknown format define( 'MEDIATYPE_BITMAP', 'BITMAP' ); // some bitmap image or image source (like psd, etc). Can't scale up. define( 'MEDIATYPE_DRAWING', 'DRAWING' ); // some vector drawing (SVG, WMF, PS, ...) or image source (oo-draw, etc). Can scale up. @@ -110,9 +138,8 @@ /**#@-*/ /**#@+ - * Antivirus result codes, for use in $wgAntivirusSetup. + * Antivirus result codes, for use in $wgAntivirusSetup. */ - define( 'AV_NO_VIRUS', 0 ); #scan ok, no virus found define( 'AV_VIRUS_FOUND', 1 ); #virus found! define( 'AV_SCAN_ABORTED', -1 ); #scan aborted, the file is probably imune @@ -133,11 +160,106 @@ * Date format selectors; used in user preference storage and by * Language::date() and co. */ -define( 'MW_DATE_DEFAULT', '0' ); +/*define( 'MW_DATE_DEFAULT', '0' ); define( 'MW_DATE_MDY', '1' ); define( 'MW_DATE_DMY', '2' ); define( 'MW_DATE_YMD', '3' ); +define( 'MW_DATE_ISO', 'ISO 8601' );*/ +define( 'MW_DATE_DEFAULT', 'default' ); +define( 'MW_DATE_MDY', 'mdy' ); +define( 'MW_DATE_DMY', 'dmy' ); +define( 'MW_DATE_YMD', 'ymd' ); define( 'MW_DATE_ISO', 'ISO 8601' ); /**#@-*/ -?> +/**#@+ + * RecentChange type identifiers + * This may be obsolete; log items are now used for moves? + */ +define( 'RC_EDIT', 0); +define( 'RC_NEW', 1); +define( 'RC_MOVE', 2); +define( 'RC_LOG', 3); +define( 'RC_MOVE_OVER_REDIRECT', 4); +/**#@-*/ + +/**#@+ + * Article edit flags + */ +define( 'EDIT_NEW', 1 ); +define( 'EDIT_UPDATE', 2 ); +define( 'EDIT_MINOR', 4 ); +define( 'EDIT_SUPPRESS_RC', 8 ); +define( 'EDIT_FORCE_BOT', 16 ); +define( 'EDIT_DEFER_UPDATES', 32 ); +define( 'EDIT_AUTOSUMMARY', 64 ); +/**#@-*/ + +/** + * Flags for Database::makeList() + * These are also available as Database class constants + */ +define( 'LIST_COMMA', 0 ); +define( 'LIST_AND', 1 ); +define( 'LIST_SET', 2 ); +define( 'LIST_NAMES', 3); +define( 'LIST_OR', 4); + +/** + * Unicode and normalisation related + */ +define( 'UNICODE_HANGUL_FIRST', 0xac00 ); +define( 'UNICODE_HANGUL_LAST', 0xd7a3 ); + +define( 'UNICODE_HANGUL_LBASE', 0x1100 ); +define( 'UNICODE_HANGUL_VBASE', 0x1161 ); +define( 'UNICODE_HANGUL_TBASE', 0x11a7 ); + +define( 'UNICODE_HANGUL_LCOUNT', 19 ); +define( 'UNICODE_HANGUL_VCOUNT', 21 ); +define( 'UNICODE_HANGUL_TCOUNT', 28 ); +define( 'UNICODE_HANGUL_NCOUNT', UNICODE_HANGUL_VCOUNT * UNICODE_HANGUL_TCOUNT ); + +define( 'UNICODE_HANGUL_LEND', UNICODE_HANGUL_LBASE + UNICODE_HANGUL_LCOUNT - 1 ); +define( 'UNICODE_HANGUL_VEND', UNICODE_HANGUL_VBASE + UNICODE_HANGUL_VCOUNT - 1 ); +define( 'UNICODE_HANGUL_TEND', UNICODE_HANGUL_TBASE + UNICODE_HANGUL_TCOUNT - 1 ); + +define( 'UNICODE_SURROGATE_FIRST', 0xd800 ); +define( 'UNICODE_SURROGATE_LAST', 0xdfff ); +define( 'UNICODE_MAX', 0x10ffff ); +define( 'UNICODE_REPLACEMENT', 0xfffd ); + + +define( 'UTF8_HANGUL_FIRST', "\xea\xb0\x80" /*codepointToUtf8( UNICODE_HANGUL_FIRST )*/ ); +define( 'UTF8_HANGUL_LAST', "\xed\x9e\xa3" /*codepointToUtf8( UNICODE_HANGUL_LAST )*/ ); + +define( 'UTF8_HANGUL_LBASE', "\xe1\x84\x80" /*codepointToUtf8( UNICODE_HANGUL_LBASE )*/ ); +define( 'UTF8_HANGUL_VBASE', "\xe1\x85\xa1" /*codepointToUtf8( UNICODE_HANGUL_VBASE )*/ ); +define( 'UTF8_HANGUL_TBASE', "\xe1\x86\xa7" /*codepointToUtf8( UNICODE_HANGUL_TBASE )*/ ); + +define( 'UTF8_HANGUL_LEND', "\xe1\x84\x92" /*codepointToUtf8( UNICODE_HANGUL_LEND )*/ ); +define( 'UTF8_HANGUL_VEND', "\xe1\x85\xb5" /*codepointToUtf8( UNICODE_HANGUL_VEND )*/ ); +define( 'UTF8_HANGUL_TEND', "\xe1\x87\x82" /*codepointToUtf8( UNICODE_HANGUL_TEND )*/ ); + +define( 'UTF8_SURROGATE_FIRST', "\xed\xa0\x80" /*codepointToUtf8( UNICODE_SURROGATE_FIRST )*/ ); +define( 'UTF8_SURROGATE_LAST', "\xed\xbf\xbf" /*codepointToUtf8( UNICODE_SURROGATE_LAST )*/ ); +define( 'UTF8_MAX', "\xf4\x8f\xbf\xbf" /*codepointToUtf8( UNICODE_MAX )*/ ); +define( 'UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/ ); +#define( 'UTF8_REPLACEMENT', '!' ); + +define( 'UTF8_OVERLONG_A', "\xc1\xbf" ); +define( 'UTF8_OVERLONG_B', "\xe0\x9f\xbf" ); +define( 'UTF8_OVERLONG_C', "\xf0\x8f\xbf\xbf" ); + +# These two ranges are illegal +define( 'UTF8_FDD0', "\xef\xb7\x90" /*codepointToUtf8( 0xfdd0 )*/ ); +define( 'UTF8_FDEF', "\xef\xb7\xaf" /*codepointToUtf8( 0xfdef )*/ ); +define( 'UTF8_FFFE', "\xef\xbf\xbe" /*codepointToUtf8( 0xfffe )*/ ); +define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ ); + +define( 'UTF8_HEAD', false ); +define( 'UTF8_TAIL', true ); + + + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/DifferenceEngine.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/DifferenceEngine.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/DifferenceEngine.php 2005-08-15 19:40:19.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/DifferenceEngine.php 2007-09-05 09:48:45.000000000 -0400 @@ -1,55 +1,62 @@ <?php /** * See diff.doc - * @package MediaWiki - * @subpackage DifferenceEngine + * @todo indicate where diff.doc can be found. + * @addtogroup DifferenceEngine */ -/** */ -require_once( 'Revision.php' ); +/** + * Constant to indicate diff cache compatibility. + * Bump this when changing the diff formatting in a way that + * fixes important bugs or such to force cached diff views to + * clear. + */ +define( 'MW_DIFF_VERSION', '1.11a' ); /** * @todo document - * @access public - * @package MediaWiki - * @subpackage DifferenceEngine + * @public + * @addtogroup DifferenceEngine */ class DifferenceEngine { /**#@+ - * @access private + * @private */ - var $mOldid, $mNewid; + var $mOldid, $mNewid, $mTitle; var $mOldtitle, $mNewtitle, $mPagetitle; var $mOldtext, $mNewtext; - var $mOldUser, $mNewUser; - var $mOldComment, $mNewComment; var $mOldPage, $mNewPage; var $mRcidMarkPatrolled; + var $mOldRev, $mNewRev; + var $mRevisionsLoaded = false; // Have the revisions been loaded + var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2? /**#@-*/ /** * Constructor - * @param integer $old Old ID we want to show and diff with. - * @param string $new Either 'prev' or 'next'. - * @param integer $rcid ??? (default 0) + * @param $titleObj Title object that the diff is associated with + * @param $old Integer: old ID we want to show and diff with. + * @param $new String: either 'prev' or 'next'. + * @param $rcid Integer: ??? FIXME (default 0) */ - function DifferenceEngine( $old, $new, $rcid = 0 ) { - global $wgTitle; + function DifferenceEngine( $titleObj = null, $old = 0, $new = 0, $rcid = 0 ) { + $this->mTitle = $titleObj; + wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n"); - if ( 'prev' == $new ) { + if ( 'prev' === $new ) { # Show diff between revision $old and the previous one. # Get previous one from DB. # $this->mNewid = intval($old); - $this->mOldid = $wgTitle->getPreviousRevisionID( $this->mNewid ); + $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid ); - } elseif ( 'next' == $new ) { + } elseif ( 'next' === $new ) { # Show diff between revision $old and the previous one. # Get previous one from DB. # $this->mOldid = intval($old); - $this->mNewid = $wgTitle->getNextRevisionID( $this->mOldid ); + $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid ); if ( false === $this->mNewid ) { # if no result, NewId points to the newest old revision. The only newer # revision is cur, which is "0". @@ -63,12 +70,11 @@ $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer } - function showDiffPage() { - global $wgUser, $wgTitle, $wgOut, $wgContLang, $wgOnlySysopsCanPatrol, - $wgUseExternalEditor, $wgUseRCPatrol; + function showDiffPage( $diffOnly = false ) { + global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol; $fname = 'DifferenceEngine::showDiffPage'; wfProfileIn( $fname ); - + # If external diffs are enabled both globally and for the user, # we'll use the application/x-external-editor interface to call # an external diff tool like kompare, kdiff3, etc. @@ -76,8 +82,8 @@ global $wgInputEncoding,$wgServer,$wgScript,$wgLang; $wgOut->disable(); header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding ); - $url1=$wgTitle->getFullURL("action=raw&oldid=".$this->mOldid); - $url2=$wgTitle->getFullURL("action=raw&oldid=".$this->mNewid); + $url1=$this->mTitle->getFullURL("action=raw&oldid=".$this->mOldid); + $url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid); $special=$wgLang->getNsText(NS_SPECIAL); $control=<<<CONTROL [Process] @@ -98,26 +104,32 @@ return; } + $wgOut->setArticleFlag( false ); + if ( ! $this->loadRevisionData() ) { + $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, {$this->mNewid})"; + $mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); + $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) ); + $wgOut->addWikitext( $mtext ); + wfProfileOut( $fname ); + return; + } + + wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) ); + + if ( $this->mNewRev->isCurrent() ) { + $wgOut->setArticleFlag( true ); + } + # mOldid is false if the difference engine is called with a "vague" query for # a diff between a version V and its previous version V' AND the version V # is the first version of that article. In that case, V' does not exist. if ( $this->mOldid === false ) { $this->showFirstRevision(); + $this->renderNewRevision(); // should we respect $diffOnly here or not? wfProfileOut( $fname ); return; } - $t = $wgTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " . - "{$this->mNewid})"; - $mtext = wfMsg( 'missingarticle', $t ); - - $wgOut->setArticleFlag( false ); - if ( ! $this->loadText() ) { - $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) ); - $wgOut->addHTML( $mtext ); - wfProfileOut( $fname ); - return; - } $wgOut->suppressQuickbar(); $oldTitle = $this->mOldPage->getPrefixedText(); @@ -128,7 +140,7 @@ $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle ); } $wgOut->setSubtitle( wfMsg( 'difference' ) ); - $wgOut->setRobotpolicy( 'noindex,follow' ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) { $wgOut->loginToUse(); @@ -138,63 +150,118 @@ } $sk = $wgUser->getSkin(); - $talk = $wgContLang->getNsText( NS_TALK ); - $contribs = wfMsg( 'contribslink' ); - - $this->mOldComment = $sk->formatComment($this->mOldComment); - $this->mNewComment = $sk->formatComment($this->mNewComment); - $oldUserLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mOldUser ), $this->mOldUser ); - $newUserLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mNewUser ), $this->mNewUser ); - $oldUTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mOldUser ), $talk ); - $newUTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mNewUser ), $talk ); - $oldContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $contribs, - 'target=' . urlencode($this->mOldUser) ); - $newContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $contribs, - 'target=' . urlencode($this->mNewUser) ); - if ( $this->newRev->isCurrent() && $wgUser->isAllowed('rollback') ) { - $rollback = '   <strong>[' . $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'rollbacklink' ), - 'action=rollback&from=' . urlencode($this->mNewUser) . - '&token=' . urlencode( $wgUser->editToken( array( $wgTitle->getPrefixedText(), $this->mNewUser ) ) ) ) . - ']</strong>'; + if ( $this->mNewRev->isCurrent() && $wgUser->isAllowed('rollback') ) { + $rollback = '   ' . $sk->generateRollback( $this->mNewRev ); } else { $rollback = ''; } - if ( $wgUseRCPatrol && $this->mRcidMarkPatrolled != 0 && $wgUser->isLoggedIn() && - ( $wgUser->isAllowed('rollback') || !$wgOnlySysopsCanPatrol ) ) - { - $patrol = ' [' . $sk->makeKnownLinkObj( $wgTitle, wfMsg( 'markaspatrolleddiff' ), - "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) . ']'; + + // Prepare a change patrol link, if applicable + if( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) { + // If we've been given an explicit change identifier, use it; saves time + if( $this->mRcidMarkPatrolled ) { + $rcid = $this->mRcidMarkPatrolled; + } else { + // Look for an unpatrolled change corresponding to this diff + $change = RecentChange::newFromConds( + array( + // Add redundant timestamp condition so we can use the + // existing index + 'rc_timestamp' => $this->mNewRev->getTimestamp(), + 'rc_this_oldid' => $this->mNewid, + 'rc_last_oldid' => $this->mOldid, + 'rc_patrolled' => 0, + ), + __METHOD__ + ); + if( $change instanceof RecentChange ) { + $rcid = $change->mAttribs['rc_id']; + } else { + // None found + $rcid = 0; + } + } + // Build the link + if( $rcid ) { + $patrol = ' [' . $sk->makeKnownLinkObj( + $this->mTitle, + wfMsgHtml( 'markaspatrolleddiff' ), + "action=markpatrolled&rcid={$rcid}" + ) . ']'; + } else { + $patrol = ''; + } } else { $patrol = ''; } - $prevlink = $sk->makeKnownLinkObj( $wgTitle, wfMsgHtml( 'previousdiff' ), + $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ), 'diff=prev&oldid='.$this->mOldid, '', '', 'id="differences-prevlink"' ); - if ( $this->newRev->isCurrent() ) { - $nextlink = ''; + if ( $this->mNewRev->isCurrent() ) { + $nextlink = ' '; } else { - $nextlink = $sk->makeKnownLinkObj( $wgTitle, wfMsgHtml( 'nextdiff' ), + $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' ); } - $oldHeader = "<strong>{$this->mOldtitle}</strong><br />$oldUserLink " . - "($oldUTLink | $oldContribs)<br />" . $this->mOldComment . - '<br />' . $prevlink; - $newHeader = "<strong>{$this->mNewtitle}</strong><br />$newUserLink " . - "($newUTLink | $newContribs) $rollback<br />" . $this->mNewComment . - '<br />' . $nextlink . $patrol; + $oldminor = ''; + $newminor = ''; + + if ($this->mOldRev->mMinorEdit == 1) { + $oldminor = wfElement( 'span', array( 'class' => 'minor' ), + wfMsg( 'minoreditletter') ) . ' '; + } + + if ($this->mNewRev->mMinorEdit == 1) { + $newminor = wfElement( 'span', array( 'class' => 'minor' ), + wfMsg( 'minoreditletter') ) . ' '; + } + + $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $this->mOldtitle . '</strong></div>' . + '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev ) . "</div>" . + '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly ) . "</div>" . + '<div id="mw-diff-otitle4">' . $prevlink . '</div>'; + $newHeader = '<div id="mw-diff-ntitle1"><strong>' .$this->mNewtitle . '</strong></div>' . + '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev ) . " $rollback</div>" . + '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly ) . "</div>" . + '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>'; + + $this->showDiff( $oldHeader, $newHeader ); + + if ( !$diffOnly ) + $this->renderNewRevision(); + + wfProfileOut( $fname ); + } + + /** + * Show the new revision of the page. + */ + function renderNewRevision() { + global $wgOut; + $fname = 'DifferenceEngine::renderNewRevision'; + wfProfileIn( $fname ); - DifferenceEngine::showDiff( $this->mOldtext, $this->mNewtext, - $oldHeader, $newHeader ); $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); + #add deleted rev tag if needed + if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) ); + } - if( !$this->newRev->isCurrent() ) { - $oldEditSectionSetting = $wgOut->mParserOptions->setEditSection( false ); + if( !$this->mNewRev->isCurrent() ) { + $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false ); } - $wgOut->addWikiText( $this->mNewtext ); - if( !$this->newRev->isCurrent() ) { - $wgOut->mParserOptions->setEditSection( $oldEditSectionSetting ); + + $this->loadNewText(); + if( is_object( $this->mNewRev ) ) { + $wgOut->setRevisionId( $this->mNewRev->getId() ); + } + + $wgOut->addWikiTextTidy( $this->mNewtext ); + + if( !$this->mNewRev->isCurrent() ) { + $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); } wfProfileOut( $fname ); @@ -205,29 +272,29 @@ * contrast to normal "old revision" display style. */ function showFirstRevision() { - global $wgOut, $wgTitle, $wgUser, $wgLang; + global $wgOut, $wgUser; $fname = 'DifferenceEngine::showFirstRevision'; wfProfileIn( $fname ); - - $this->mOldid = $this->mNewid; # hack to make loadText() work. - # Get article text from the DB # - if ( ! $this->loadText() ) { - $t = $wgTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " . + if ( ! $this->loadNewText() ) { + $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " . "{$this->mNewid})"; - $mtext = wfMsg( 'missingarticle', $t ); + $mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" ); $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) ); - $wgOut->addHTML( $mtext ); + $wgOut->addWikitext( $mtext ); wfProfileOut( $fname ); return; } + if ( $this->mNewRev->isCurrent() ) { + $wgOut->setArticleFlag( true ); + } # Check if user is allowed to look at this page. If not, bail out. # - if ( !( $this->mOldPage->userCanRead() ) ) { + if ( !( $this->mTitle->userCanRead() ) ) { $wgOut->loginToUse(); $wgOut->output(); wfProfileOut( $fname ); @@ -238,127 +305,406 @@ # $sk = $wgUser->getSkin(); - $uTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mOldUser ), $wgLang->getNsText( NS_TALK ) ); - $userLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mOldUser ), $this->mOldUser ); - $contribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), wfMsg( 'contribslink' ), - 'target=' . urlencode($this->mOldUser) ); - $nextlink = $sk->makeKnownLinkObj( $wgTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' ); - $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />$userLink " . - "($uTLink | $contribs)<br />" . $this->mOldComment . - '<br />' . $nextlink. "</div>\n"; + $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' ); + $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" . + $sk->revUserTools( $this->mNewRev ) . "<br />" . + $sk->revComment( $this->mNewRev ) . "<br />" . + $nextlink . "</div>\n"; $wgOut->addHTML( $header ); $wgOut->setSubtitle( wfMsg( 'difference' ) ); - $wgOut->setRobotpolicy( 'noindex,follow' ); - - - # Show current revision - # - $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" ); - $wgOut->addWikiText( $this->mNewtext ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); wfProfileOut( $fname ); } - function showDiff( $otext, $ntext, $otitle, $ntitle ) { - global $wgOut; - $wgOut->addHTML( DifferenceEngine::getDiff( $otext, $ntext, $otitle, $ntitle ) ); + /** + * Get the diff text, send it to $wgOut + * Returns false if the diff could not be generated, otherwise returns true + */ + function showDiff( $otitle, $ntitle ) { + global $wgOut, $wgRequest; + $diff = $this->getDiff( $otitle, $ntitle, $wgRequest->getVal( 'action' ) == 'purge' ); + if ( $diff === false ) { + $wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ) ); + return false; + } else { + $this->showDiffStyle(); + $wgOut->addHTML( $diff ); + return true; + } } - function getDiff( $otext, $ntext, $otitle, $ntitle ) { - global $wgUseExternalDiffEngine, $wgContLang; - $out = " - <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'> - <tr> - <td colspan='2' width='50%' align='center' class='diff-otitle'>{$otitle}</td> - <td colspan='2' width='50%' align='center' class='diff-ntitle'>{$ntitle}</td> - </tr> - "; - $otext = $wgContLang->segmentForDiff($otext); - $ntext = $wgContLang->segmentForDiff($ntext); - $difftext=''; - if ( $wgUseExternalDiffEngine ) { + /** + * Add style sheets and supporting JS for diff display. + */ + function showDiffStyle() { + global $wgStylePath, $wgStyleVersion, $wgOut; + $wgOut->addStyle( 'common/diff.css' ); + + // JS is needed to detect old versions of Mozilla to work around an annoyance bug. + $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" ); + } + + /** + * Get complete diff table, including header + * + * @param Title $otitle Old title + * @param Title $ntitle New title + * @param bool $skipCache Skip the diff cache for this request? + * @return mixed + */ + function getDiff( $otitle, $ntitle, $skipCache = false ) { + $body = $this->getDiffBody( $skipCache ); + if ( $body === false ) { + return false; + } else { + $multi = $this->getMultiNotice(); + return $this->addHeader( $body, $otitle, $ntitle, $multi ); + } + } + + /** + * Get the diff table body, without header + * + * @param bool $skipCache Skip cache for this request? + * @return mixed + */ + function getDiffBody( $skipCache = false ) { + global $wgMemc; + $fname = 'DifferenceEngine::getDiffBody'; + wfProfileIn( $fname ); + + // Cacheable? + $key = false; + if ( $this->mOldid && $this->mNewid && !$skipCache ) { + // Try cache + $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid ); + $difftext = $wgMemc->get( $key ); + if ( $difftext ) { + wfIncrStats( 'diff_cache_hit' ); + $difftext = $this->localiseLineNumbers( $difftext ); + $difftext .= "\n<!-- diff cache key $key -->\n"; + wfProfileOut( $fname ); + return $difftext; + } + } + + #loadtext is permission safe, this just clears out the diff + if ( !$this->loadText() ) { + wfProfileOut( $fname ); + return false; + } else if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) { + return ''; + } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + return ''; + } + + $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); + + // Save to cache for 7 days + if ( $key !== false && $difftext !== false ) { + wfIncrStats( 'diff_cache_miss' ); + $wgMemc->set( $key, $difftext, 7*86400 ); + } else { + wfIncrStats( 'diff_uncacheable' ); + } + // Replace line numbers with the text in the user's language + if ( $difftext !== false ) { + $difftext = $this->localiseLineNumbers( $difftext ); + } + wfProfileOut( $fname ); + return $difftext; + } + + /** + * Generate a diff, no caching + * $otext and $ntext must be already segmented + */ + function generateDiffBody( $otext, $ntext ) { + global $wgExternalDiffEngine, $wgContLang; + $fname = 'DifferenceEngine::generateDiffBody'; + + $otext = str_replace( "\r\n", "\n", $otext ); + $ntext = str_replace( "\r\n", "\n", $ntext ); + + if ( $wgExternalDiffEngine == 'wikidiff' ) { # For historical reasons, external diff engine expects # input text to be HTML-escaped already - $otext = str_replace( "\r\n", "\n", htmlspecialchars ( $otext ) ); - $ntext = str_replace( "\r\n", "\n", htmlspecialchars ( $ntext ) ); + $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) ); + $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) ); if( !function_exists( 'wikidiff_do_diff' ) ) { dl('php_wikidiff.so'); } - $difftext = wikidiff_do_diff( $otext, $ntext, 2 ); - } else { - $ota = explode( "\n", str_replace( "\r\n", "\n", $otext ) ); - $nta = explode( "\n", str_replace( "\r\n", "\n", $ntext ) ); - $diffs =& new Diff( $ota, $nta ); - $formatter =& new TableDiffFormatter(); - $difftext = $formatter->format( $diffs ); + return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ); + } + + if ( $wgExternalDiffEngine == 'wikidiff2' ) { + # Better external diff engine, the 2 may some day be dropped + # This one does the escaping and segmenting itself + if ( !function_exists( 'wikidiff2_do_diff' ) ) { + wfProfileIn( "$fname-dl" ); + @dl('php_wikidiff2.so'); + wfProfileOut( "$fname-dl" ); + } + if ( function_exists( 'wikidiff2_do_diff' ) ) { + wfProfileIn( 'wikidiff2_do_diff' ); + $text = wikidiff2_do_diff( $otext, $ntext, 2 ); + wfProfileOut( 'wikidiff2_do_diff' ); + return $text; + } + } + if ( $wgExternalDiffEngine !== false ) { + # Diff via the shell + global $wgTmpDirectory; + $tempName1 = tempnam( $wgTmpDirectory, 'diff_' ); + $tempName2 = tempnam( $wgTmpDirectory, 'diff_' ); + + $tempFile1 = fopen( $tempName1, "w" ); + if ( !$tempFile1 ) { + wfProfileOut( $fname ); + return false; + } + $tempFile2 = fopen( $tempName2, "w" ); + if ( !$tempFile2 ) { + wfProfileOut( $fname ); + return false; + } + fwrite( $tempFile1, $otext ); + fwrite( $tempFile2, $ntext ); + fclose( $tempFile1 ); + fclose( $tempFile2 ); + $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 ); + wfProfileIn( "$fname-shellexec" ); + $difftext = wfShellExec( $cmd ); + wfProfileOut( "$fname-shellexec" ); + unlink( $tempName1 ); + unlink( $tempName2 ); + return $difftext; + } + + # Native PHP diff + $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); + $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); + $diffs = new Diff( $ota, $nta ); + $formatter = new TableDiffFormatter(); + return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ); + } + + + /** + * Replace line numbers with the text in the user's language + */ + function localiseLineNumbers( $text ) { + return preg_replace_callback( '/<!--LINE (\d+)-->/', + array( &$this, 'localiseLineNumbersCb' ), $text ); + } + + function localiseLineNumbersCb( $matches ) { + global $wgLang; + return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) ); + } + + + /** + * If there are revisions between the ones being compared, return a note saying so. + */ + function getMultiNotice() { + if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) ) + return ''; + + if( !$this->mOldPage->equals( $this->mNewPage ) ) { + // Comparing two different pages? Count would be meaningless. + return ''; + } + + $oldid = $this->mOldRev->getId(); + $newid = $this->mNewRev->getId(); + if ( $oldid > $newid ) { + $tmp = $oldid; $oldid = $newid; $newid = $tmp; + } + + $n = $this->mTitle->countRevisionsBetween( $oldid, $newid ); + if ( !$n ) + return ''; + + return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n ); + } + + + /** + * Add the header to a diff body + */ + function addHeader( $diff, $otitle, $ntitle, $multi = '' ) { + global $wgOut; + + if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) { + $otitle = '<span class="history-deleted">'.$otitle.'</span>'; + } + if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { + $ntitle = '<span class="history-deleted">'.$ntitle.'</span>'; } - $difftext = $wgContLang->unsegmentForDiff($difftext); - $out .= $difftext."</table>\n"; - return $out; + $header = " + <table class='diff'> + <col class='diff-marker' /> + <col class='diff-content' /> + <col class='diff-marker' /> + <col class='diff-content' /> + <tr> + <td colspan='2' class='diff-otitle'>{$otitle}</td> + <td colspan='2' class='diff-ntitle'>{$ntitle}</td> + </tr> + "; + + if ( $multi != '' ) + $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>"; + + return $header . $diff . "</table>"; + } + + /** + * Use specified text instead of loading from the database + */ + function setText( $oldText, $newText ) { + $this->mOldtext = $oldText; + $this->mNewtext = $newText; + $this->mTextLoaded = 2; } /** - * Load the text of the articles to compare. If newid is 0, then compare + * Load revision metadata for the specified articles. If newid is 0, then compare * the old article in oldid to the current article; if oldid is 0, then * compare the current article to the immediately previous one (ignoring the * value of newid). - */ - function loadText() { - global $wgTitle, $wgOut, $wgLang; - $fname = 'DifferenceEngine::loadText'; - - $dbr =& wfGetDB( DB_SLAVE ); - if( $this->mNewid ) { - $this->newRev = Revision::newFromId( $this->mNewid ); + * + * If oldid is false, leave the corresponding revision object set + * to false. This is impossible via ordinary user input, and is provided for + * API convenience. + */ + function loadRevisionData() { + global $wgLang; + if ( $this->mRevisionsLoaded ) { + return true; } else { - $this->newRev = Revision::newFromTitle( $wgTitle ); + // Whether it succeeds or fails, we don't want to try again + $this->mRevisionsLoaded = true; } - if( is_null( $this->newRev ) ) { + + // Load the new revision object + $this->mNewRev = $this->mNewid + ? Revision::newFromId( $this->mNewid ) + : Revision::newFromTitle( $this->mTitle ); + if( !$this->mNewRev instanceof Revision ) return false; - } - if( $this->newRev->isCurrent() ) { - $wgOut->setArticleFlag( true ); - $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) ); - $this->mNewPage = $wgTitle; + // Update the new revision ID in case it was 0 (makes life easier doing UI stuff) + $this->mNewid = $this->mNewRev->getId(); + + // Set assorted variables + $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true ); + $this->mNewPage = $this->mNewRev->getTitle(); + if( $this->mNewRev->isCurrent() ) { $newLink = $this->mNewPage->escapeLocalUrl(); - $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"; + $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) ); + $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' ); + + $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)" + . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; + } else { - $this->mNewPage = $this->newRev->getTitle(); - $newLink = $this->mNewPage->escapeLocalUrl ('oldid=' . $this->mNewid ); - $t = $wgLang->timeanddate( $this->newRev->getTimestamp(), true ); - $this->mPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) ); - $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"; + $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); + $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid ); + $this->mPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $timestamp ) ); + + $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>" + . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; } - + + // Load the old revision object + $this->mOldRev = false; if( $this->mOldid ) { - $this->oldRev = Revision::newFromId( $this->mOldid ); - } else { - $this->oldRev = $this->newRev->getPrevious(); - $this->mOldid = $this->oldRev->getId(); - } - if( is_null( $this->oldRev ) ) { + $this->mOldRev = Revision::newFromId( $this->mOldid ); + } elseif ( $this->mOldid === 0 ) { + $rev = $this->mNewRev->getPrevious(); + if( $rev ) { + $this->mOldid = $rev->getId(); + $this->mOldRev = $rev; + } else { + // No previous revision; mark to show as first-version only. + $this->mOldid = false; + $this->mOldRev = false; + } + }/* elseif ( $this->mOldid === false ) leave mOldRev false; */ + + if( is_null( $this->mOldRev ) ) { return false; } + + if ( $this->mOldRev ) { + $this->mOldPage = $this->mOldRev->getTitle(); + + $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true ); + $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid ); + $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid ); + $this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) ) + . "</a> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)"; - $this->mOldPage = $this->oldRev->getTitle(); + // Add an "undo" link + $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid); + $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)"; + } - $t = $wgLang->timeanddate( $this->oldRev->getTimestamp(), true ); - $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid ); - $this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) ) . '</a>'; - - $this->mNewUser = $this->newRev->getUserText(); - $this->mNewComment = $this->newRev->getComment(); - $this->mNewtext = $this->newRev->getText(); - - $this->mOldUser = $this->oldRev->getUserText(); - $this->mOldComment = $this->oldRev->getComment(); - $this->mOldtext = $this->oldRev->getText(); + return true; + } + + /** + * Load the text of the revisions, as well as revision data. + */ + function loadText() { + if ( $this->mTextLoaded == 2 ) { + return true; + } else { + // Whether it succeeds or fails, we don't want to try again + $this->mTextLoaded = 2; + } + + if ( !$this->loadRevisionData() ) { + return false; + } + if ( $this->mOldRev ) { + // FIXME: permission tests + $this->mOldtext = $this->mOldRev->revText(); + if ( $this->mOldtext === false ) { + return false; + } + } + if ( $this->mNewRev ) { + $this->mNewtext = $this->mNewRev->revText(); + if ( $this->mNewtext === false ) { + return false; + } + } + return true; + } + /** + * Load the text of the new revision, not the old one + */ + function loadNewText() { + if ( $this->mTextLoaded >= 1 ) { + return true; + } else { + $this->mTextLoaded = 1; + } + if ( !$this->loadRevisionData() ) { + return false; + } + $this->mNewtext = $this->mNewRev->getText(); return true; } + + } // A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3) @@ -371,9 +717,8 @@ /** * @todo document - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * @private + * @addtogroup DifferenceEngine */ class _DiffOp { var $type; @@ -395,9 +740,8 @@ /** * @todo document - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * @private + * @addtogroup DifferenceEngine */ class _DiffOp_Copy extends _DiffOp { var $type = 'copy'; @@ -416,9 +760,8 @@ /** * @todo document - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * @private + * @addtogroup DifferenceEngine */ class _DiffOp_Delete extends _DiffOp { var $type = 'delete'; @@ -435,9 +778,8 @@ /** * @todo document - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * @private + * @addtogroup DifferenceEngine */ class _DiffOp_Add extends _DiffOp { var $type = 'add'; @@ -454,9 +796,8 @@ /** * @todo document - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * @private + * @addtogroup DifferenceEngine */ class _DiffOp_Change extends _DiffOp { var $type = 'change'; @@ -489,17 +830,20 @@ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations) * are my own. * - * @author Geoffrey T. Dairiki - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * Line length limits for robustness added by Tim Starling, 2005-08-31 + * + * @author Geoffrey T. Dairiki, Tim Starling + * @private + * @addtogroup DifferenceEngine */ class _DiffEngine { + const MAX_XREF_LENGTH = 10000; + function diff ($from_lines, $to_lines) { $fname = '_DiffEngine::diff'; wfProfileIn( $fname ); - + $n_from = sizeof($from_lines); $n_to = sizeof($to_lines); @@ -512,32 +856,34 @@ // Skip leading common lines. for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) { - if ($from_lines[$skip] != $to_lines[$skip]) + if ($from_lines[$skip] !== $to_lines[$skip]) break; $this->xchanged[$skip] = $this->ychanged[$skip] = false; } // Skip trailing common lines. $xi = $n_from; $yi = $n_to; for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) { - if ($from_lines[$xi] != $to_lines[$yi]) + if ($from_lines[$xi] !== $to_lines[$yi]) break; $this->xchanged[$xi] = $this->ychanged[$yi] = false; } // Ignore lines which do not exist in both files. - for ($xi = $skip; $xi < $n_from - $endskip; $xi++) - $xhash[$from_lines[$xi]] = 1; + for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { + $xhash[$this->_line_hash($from_lines[$xi])] = 1; + } + for ($yi = $skip; $yi < $n_to - $endskip; $yi++) { $line = $to_lines[$yi]; - if ( ($this->ychanged[$yi] = empty($xhash[$line])) ) + if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) ) continue; - $yhash[$line] = 1; + $yhash[$this->_line_hash($line)] = 1; $this->yv[] = $line; $this->yind[] = $yi; } for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { $line = $from_lines[$xi]; - if ( ($this->xchanged[$xi] = empty($yhash[$line])) ) + if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) ) continue; $this->xv[] = $line; $this->xind[] = $xi; @@ -587,6 +933,17 @@ return $edits; } + /** + * Returns the whole line if it's small enough, or the MD5 hash otherwise + */ + function _line_hash( $line ) { + if ( strlen( $line ) > self::MAX_XREF_LENGTH ) { + return md5( $line ); + } else { + return $line; + } + } + /* Divide the Largest Common Subsequence (LCS) of the sequences * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally @@ -623,12 +980,12 @@ else for ($i = $ylim - 1; $i >= $yoff; $i--) $ymatches[$this->yv[$i]][] = $i; - + $this->lcs = 0; $this->seq[0]= $yoff - 1; $this->in_seq = array(); $ymids[0] = array(); - + $numer = $xlim - $xoff + $nchunks - 1; $x = $xoff; for ($chunk = 0; $chunk < $nchunks; $chunk++) { @@ -636,7 +993,7 @@ if ($chunk > 0) for ($i = 0; $i <= $this->lcs; $i++) $ymids[$i][$chunk-1] = $this->seq[$i]; - + $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks); for ( ; $x < $x1; $x++) { $line = $flip ? $this->yv[$x] : $this->xv[$x]; @@ -651,7 +1008,7 @@ $ymids[$k] = $ymids[$k-1]; break; } - while (list ($junk, $y) = each($matches)) { + while (list ( /* $junk */, $y) = each($matches)) { if ($y > $this->seq[$k-1]) { USE_ASSERTS && assert($y < $this->seq[$k]); // Optimization: this is a common case: @@ -668,7 +1025,7 @@ } wfProfileOut( "$fname-chunk" ); } - + $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff); $ymid = $ymids[$this->lcs]; for ($n = 0; $n < $nchunks - 1; $n++) { @@ -677,7 +1034,7 @@ $seps[] = $flip ? array($y1, $x1) : array($x1, $y1); } $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim); - + wfProfileOut( $fname ); return array($this->lcs, $seps); } @@ -685,7 +1042,7 @@ function _lcs_pos ($ypos) { $fname = '_DiffEngine::_lcs_pos'; wfProfileIn( $fname ); - + $end = $this->lcs; if ($end == 0 || $ypos > $this->seq[$end]) { $this->seq[++$this->lcs] = $ypos; @@ -702,9 +1059,9 @@ else $end = $mid; } - + USE_ASSERTS && assert($ypos != $this->seq[$end]); - + $this->in_seq[$this->seq[$end]] = false; $this->seq[$end] = $ypos; $this->in_seq[$ypos] = 1; @@ -726,7 +1083,7 @@ function _compareseq ($xoff, $xlim, $yoff, $ylim) { $fname = '_DiffEngine::_compareseq'; wfProfileIn( $fname ); - + // Slide down the bottom initial diagonal. while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) { @@ -788,11 +1145,11 @@ wfProfileIn( $fname ); $i = 0; $j = 0; - + USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)'); $len = sizeof($lines); $other_len = sizeof($other_changed); - + while (1) { /* * Scan forwards to find beginning of another run of changes. @@ -807,30 +1164,30 @@ */ while ($j < $other_len && $other_changed[$j]) $j++; - + while ($i < $len && ! $changed[$i]) { USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]'); $i++; $j++; while ($j < $other_len && $other_changed[$j]) $j++; } - + if ($i == $len) break; - + $start = $i; - + // Find the end of this run of changes. while (++$i < $len && $changed[$i]) continue; - + do { /* * Record the length of this run of changes, so that * we can later determine whether the run has grown. */ $runlength = $i - $start; - + /* * Move the changed region back, so long as the * previous unchanged line matches the last changed one. @@ -846,14 +1203,14 @@ continue; USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]'); } - + /* * Set CORRESPONDING to the end of the changed run, at the last * point where it corresponds to a changed run in the other file. * CORRESPONDING == LEN means no such point has been found. */ $corresponding = $j < $other_len ? $i : $len; - + /* * Move the changed region forward, so long as the * first changed line matches the following unchanged one. @@ -866,7 +1223,7 @@ $changed[$i++] = 1; while ($i < $len && $changed[$i]) $i++; - + USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]'); $j++; if ($j < $other_len && $other_changed[$j]) { @@ -876,7 +1233,7 @@ } } } while ($runlength != $i - $start); - + /* * If possible, move the fully-merged run of changes * back to a corresponding run in the other file. @@ -897,9 +1254,8 @@ /** * Class representing a 'diff' between two sequences of strings. * @todo document - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * @private + * @addtogroup DifferenceEngine */ class Diff { @@ -1037,11 +1393,9 @@ } /** - * FIXME: bad name. - * @todo document - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * @todo document, bad name. + * @private + * @addtogroup DifferenceEngine */ class MappedDiff extends Diff { @@ -1072,7 +1426,7 @@ $mapped_from_lines, $mapped_to_lines) { $fname = 'MappedDiff::MappedDiff'; wfProfileIn( $fname ); - + assert(sizeof($from_lines) == sizeof($mapped_from_lines)); assert(sizeof($to_lines) == sizeof($mapped_to_lines)); @@ -1103,9 +1457,8 @@ * It is intended that this class be customized via inheritance, * to obtain fancier outputs. * @todo document - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * @private + * @addtogroup DifferenceEngine */ class DiffFormatter { @@ -1270,9 +1623,8 @@ /** * @todo document - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * @private + * @addtogroup DifferenceEngine */ class _HWLDF_WordAccumulator { function _HWLDF_WordAccumulator () { @@ -1284,9 +1636,12 @@ function _flushGroup ($new_tag) { if ($this->_group !== '') { - if ($this->_tag == 'mark') - $this->_line .= '<span class="diffchange">' . - htmlspecialchars ( $this->_group ) . '</span>'; + if ($this->_tag == 'ins') + $this->_line .= '<ins class="diffchange">' . + htmlspecialchars ( $this->_group ) . '</ins>'; + elseif ($this->_tag == 'del') + $this->_line .= '<del class="diffchange">' . + htmlspecialchars ( $this->_group ) . '</del>'; else $this->_line .= htmlspecialchars ( $this->_group ); } @@ -1329,20 +1684,20 @@ /** * @todo document - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * @private + * @addtogroup DifferenceEngine */ class WordLevelDiff extends MappedDiff { + const MAX_LINE_LENGTH = 10000; + function WordLevelDiff ($orig_lines, $closing_lines) { $fname = 'WordLevelDiff::WordLevelDiff'; wfProfileIn( $fname ); - + list ($orig_words, $orig_stripped) = $this->_split($orig_lines); list ($closing_words, $closing_stripped) = $this->_split($closing_lines); - $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped); wfProfileOut( $fname ); @@ -1351,14 +1706,34 @@ function _split($lines) { $fname = 'WordLevelDiff::_split'; wfProfileIn( $fname ); - if (!preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', - implode("\n", $lines), - $m)) { - wfProfileOut( $fname ); - return array(array(''), array('')); + + $words = array(); + $stripped = array(); + $first = true; + foreach ( $lines as $line ) { + # If the line is too long, just pretend the entire line is one big word + # This prevents resource exhaustion problems + if ( $first ) { + $first = false; + } else { + $words[] = "\n"; + $stripped[] = "\n"; + } + if ( strlen( $line ) > self::MAX_LINE_LENGTH ) { + $words[] = $line; + $stripped[] = $line; + } else { + $m = array(); + if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', + $line, $m)) + { + $words = array_merge( $words, $m[0] ); + $stripped = array_merge( $stripped, $m[1] ); + } + } } wfProfileOut( $fname ); - return array($m[0], $m[1]); + return array($words, $stripped); } function orig () { @@ -1370,7 +1745,7 @@ if ($edit->type == 'copy') $orig->addWords($edit->orig); elseif ($edit->orig) - $orig->addWords($edit->orig, 'mark'); + $orig->addWords($edit->orig, 'del'); } $lines = $orig->getLines(); wfProfileOut( $fname ); @@ -1386,7 +1761,7 @@ if ($edit->type == 'copy') $closing->addWords($edit->closing); elseif ($edit->closing) - $closing->addWords($edit->closing, 'mark'); + $closing->addWords($edit->closing, 'ins'); } $lines = $closing->getLines(); wfProfileOut( $fname ); @@ -1397,9 +1772,8 @@ /** * Wikipedia Table style diff formatter. * @todo document - * @access private - * @package MediaWiki - * @subpackage DifferenceEngine + * @private + * @addtogroup DifferenceEngine */ class TableDiffFormatter extends DiffFormatter { @@ -1409,16 +1783,12 @@ } function _block_header( $xbeg, $xlen, $ybeg, $ylen ) { - $l1 = wfMsg( 'lineno', $xbeg ); - $l2 = wfMsg( 'lineno', $ybeg ); - - $r = '<tr><td colspan="2" align="left"><strong>'.$l1."</strong></td>\n" . - '<td colspan="2" align="left"><strong>'.$l2."</strong></td></tr>\n"; + $r = '<tr><td colspan="2" class="diff-lineno"><!--LINE '.$xbeg."--></td>\n" . + '<td colspan="2" class="diff-lineno"><!--LINE '.$ybeg."--></td></tr>\n"; return $r; } function _start_block( $header ) { - global $wgOut; echo $header; } @@ -1430,17 +1800,25 @@ # HTML-escape parameter before calling this function addedLine( $line ) { - return "<td>+</td><td class='diff-addedline'>{$line}</td>"; + return $this->wrapLine( '+', 'diff-addedline', $line ); } # HTML-escape parameter before calling this function deletedLine( $line ) { - return "<td>-</td><td class='diff-deletedline'>{$line}</td>"; + return $this->wrapLine( '-', 'diff-deletedline', $line ); } # HTML-escape parameter before calling this function contextLine( $line ) { - return "<td> </td><td class='diff-context'>{$line}</td>"; + return $this->wrapLine( ' ', 'diff-context', $line ); + } + + private function wrapLine( $marker, $class, $line ) { + if( $line !== '' ) { + // The <div> wrapper is needed for 'overflow: auto' style to scroll properly + $line = "<div>$line</div>"; + } + return "<td class='diff-marker'>$marker</td><td class='$class'>$line</td>"; } function emptyLine() { @@ -1472,7 +1850,7 @@ function _changed( $orig, $closing ) { $fname = 'TableDiffFormatter::_changed'; wfProfileIn( $fname ); - + $diff = new WordLevelDiff( $orig, $closing ); $del = $diff->orig(); $add = $diff->closing(); @@ -1493,4 +1871,5 @@ } } -?> + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/EditPage.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/EditPage.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/EditPage.php 2006-01-07 08:38:54.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/EditPage.php 2007-09-03 14:13:19.000000000 -0400 @@ -1,30 +1,51 @@ <?php /** - * Contain the EditPage class - * @package MediaWiki + * Contains the EditPage class */ /** - * Splitting edit page/HTML interface from Article... + * The edit page/HTML interface (split from Article) * The actual database and text munging is still in Article, * but it should get easier to call those from alternate * interfaces. - * - * @package MediaWiki */ - class EditPage { var $mArticle; var $mTitle; var $mMetaData = ''; - + var $isConflict = false; + var $isCssJsSubpage = false; + var $deletedSinceEdit = false; + var $formtype; + var $firsttime; + var $lastDelete; + var $mTokenOk = false; + var $mTokenOkExceptSuffix = false; + var $mTriedSave = false; + var $tooBig = false; + var $kblength = false; + var $missingComment = false; + var $missingSummary = false; + var $allowBlankSummary = false; + var $autoSumm = ''; + var $hookError = ''; + var $mPreviewTemplates; + # Form values var $save = false, $preview = false, $diff = false; - var $minoredit = false, $watchthis = false; + var $minoredit = false, $watchthis = false, $recreate = false; var $textbox1 = '', $textbox2 = '', $summary = ''; - var $edittime = '', $section = ''; - var $oldid = 0; - + var $edittime = '', $section = '', $starttime = ''; + var $oldid = 0, $editintro = '', $scrolltop = null; + + # Placeholders for text injection by hooks (must be HTML) + # extensions should take care to _append_ to the present value + public $editFormPageTop; // Before even the preview + public $editFormTextTop; + public $editFormTextAfterWarn; + public $editFormTextAfterTools; + public $editFormTextBottom; + /** * @todo document * @param $article @@ -33,6 +54,136 @@ $this->mArticle =& $article; global $wgTitle; $this->mTitle =& $wgTitle; + + # Placeholders for text injection by hooks (empty per default) + $this->editFormPageTop = + $this->editFormTextTop = + $this->editFormTextAfterWarn = + $this->editFormTextAfterTools = + $this->editFormTextBottom = ""; + } + + /** + * Fetch initial editing page content. + */ + private function getContent( $def_text = '' ) { + global $wgOut, $wgRequest, $wgParser; + + # Get variables from query string :P + $section = $wgRequest->getVal( 'section' ); + $preload = $wgRequest->getVal( 'preload' ); + $undoafter = $wgRequest->getVal( 'undoafter' ); + $undo = $wgRequest->getVal( 'undo' ); + + wfProfileIn( __METHOD__ ); + + $text = ''; + if( !$this->mTitle->exists() ) { + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + # If this is a system message, get the default text. + $text = wfMsgWeirdKey ( $this->mTitle->getText() ) ; + } else { + # If requested, preload some text. + $text = $this->getPreloadedText( $preload ); + } + # We used to put MediaWiki:Newarticletext here if + # $text was empty at this point. + # This is now shown above the edit box instead. + } else { + // FIXME: may be better to use Revision class directly + // But don't mess with it just yet. Article knows how to + // fetch the page record from the high-priority server, + // which is needed to guarantee we don't pick up lagged + // information. + + $text = $this->mArticle->getContent(); + + if ($undo > 0 && $undoafter > 0 && $undo < $undoafter) { + # If they got undoafter and undo round the wrong way, switch them + list( $undo, $undoafter ) = array( $undoafter, $undo ); + } + + if ( $undo > 0 && $undo > $undoafter ) { + # Undoing a specific edit overrides section editing; section-editing + # doesn't work with undoing. + if ( $undoafter ) { + $undorev = Revision::newFromId($undo); + $oldrev = Revision::newFromId($undoafter); + } else { + $undorev = Revision::newFromId($undo); + $oldrev = $undorev ? $undorev->getPrevious() : null; + } + + #Sanity check, make sure it's the right page. + # Otherwise, $text will be left as-is. + if ( !is_null($undorev) && !is_null($oldrev) && $undorev->getPage()==$oldrev->getPage() && $undorev->getPage()==$this->mArticle->getID() ) { + $undorev_text = $undorev->getText(); + $oldrev_text = $oldrev->getText(); + $currev_text = $text; + + #No use doing a merge if it's just a straight revert. + if ( $currev_text != $undorev_text ) { + $result = wfMerge($undorev_text, $oldrev_text, $currev_text, $text); + } else { + $text = $oldrev_text; + $result = true; + } + } else { + // Failed basic sanity checks. + // Older revisions may have been removed since the link + // was created, or we may simply have got bogus input. + $result = false; + } + + if( $result ) { + # Inform the user of our success and set an automatic edit summary + $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) ); + $firstrev = $oldrev->getNext(); + # If we just undid one rev, use an autosummary + if ( $firstrev->mId == $undo ) { + $this->summary = wfMsgForContent('undo-summary', $undo, $undorev->getUserText()); + } + $this->formtype = 'diff'; + } else { + # Warn the user that something went wrong + $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-failure' ) ); + } + } else if( $section != '' ) { + if( $section == 'new' ) { + $text = $this->getPreloadedText( $preload ); + } else { + $text = $wgParser->getSection( $text, $section, $def_text ); + } + } + } + + wfProfileOut( __METHOD__ ); + return $text; + } + + /** + * Get the contents of a page from its title and remove includeonly tags + * + * @param $preload String: the title of the page. + * @return string The contents of the page. + */ + private function getPreloadedText($preload) { + if ( $preload === '' ) + return ''; + else { + $preloadTitle = Title::newFromText( $preload ); + if ( isset( $preloadTitle ) && $preloadTitle->userCanRead() ) { + $rev=Revision::newFromTitle($preloadTitle); + if ( is_object( $rev ) ) { + $text = $rev->getText(); + // TODO FIXME: AAAAAAAAAAA, this shouldn't be implementing + // its own mini-parser! -ævar + $text = preg_replace( '~</?includeonly>~', '', $text ); + return $text; + } else + return ''; + } + } } /** @@ -46,7 +197,7 @@ if ( !$wgUseMetadataEdit ) return ; if ( $wgMetadataWhitelist == '' ) return ; $s = '' ; - $t = $this->mArticle->getContent ( true ) ; + $t = $this->getContent(); # MISSING : <nowiki> filtering @@ -83,7 +234,7 @@ $sat = array () ; # stand-alone-templates; must be lowercase $wl_title = Title::newFromText ( $wgMetadataWhitelist ) ; $wl_article = new Article ( $wl_title ) ; - $wl = explode ( "\n" , $wl_article->getContent(true) ) ; + $wl = explode ( "\n" , $wl_article->getContent() ) ; foreach ( $wl AS $x ) { $isentry = false ; @@ -97,7 +248,7 @@ { $sat[] = strtolower ( $x ) ; } - + } # Templates, but only some @@ -130,83 +281,196 @@ $this->mMetaData = $s ; } + function submit() { + $this->edit(); + } + /** - * This is the function that gets called for "action=edit". + * This is the function that gets called for "action=edit". It + * sets up various member variables, then passes execution to + * another function, usually showEditForm() + * + * The edit form is self-submitting, so that when things like + * preview and edit conflicts occur, we get the same form back + * with the extra stuff added. Only when the final submission + * is made and all is well do we actually save and redirect to + * the newly-edited page. */ function edit() { - global $wgOut, $wgUser, $wgRequest; + global $wgOut, $wgUser, $wgRequest, $wgTitle; + + if ( ! wfRunHooks( 'AlternateEdit', array( &$this ) ) ) + return; + + $fname = 'EditPage::edit'; + wfProfileIn( $fname ); + wfDebug( "$fname: enter\n" ); + // this is not an article $wgOut->setArticleFlag(false); $this->importFormData( $wgRequest ); - + $this->firsttime = false; + if( $this->live ) { $this->livePreview(); + wfProfileOut( $fname ); return; } - if ( ! $this->mTitle->userCanEdit() ) { - $wgOut->readOnlyPage( $this->mArticle->getContent( true ), true ); - return; + $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser); + if( !$this->mTitle->exists() ) + $permErrors += $this->mTitle->getUserPermissionsErrors( 'create', $wgUser); + + # Ignore some permissions errors. + $remove = array(); + foreach( $permErrors as $error ) { + if ($this->preview || $this->diff && + ($error[0] == 'blockedtext' || $error[0] == 'autoblockedtext')) + { + // Don't worry about blocks when previewing/diffing + $remove[] = $error; + } + + if ($error[0] == 'readonlytext') + { + if ($this->edit) { + $this->formtype = 'preview'; + } elseif ($this->save || $this->preview || $this->diff) { + $remove[] = $error; + } + } } - if ( !$this->preview && !$this->diff && $wgUser->isBlockedFrom( $this->mTitle, !$this->save ) ) { - # When previewing, don't check blocked state - will get caught at save time. - # Also, check when starting edition is done against slave to improve performance. - $this->blockedIPpage(); + # array_diff returns elements in $permErrors that are not in $remove. + $permErrors = array_diff( $permErrors, $remove ); + + if ( !empty($permErrors) ) + { + wfDebug( "$fname: User can't edit\n" ); + $wgOut->readOnlyPage( $this->getContent(), true, $permErrors ); + wfProfileOut( $fname ); return; + } else { + if ( $this->save ) { + $this->formtype = 'save'; + } else if ( $this->preview ) { + $this->formtype = 'preview'; + } else if ( $this->diff ) { + $this->formtype = 'diff'; + } else { # First time through + $this->firsttime = true; + if( $this->previewOnOpen() ) { + $this->formtype = 'preview'; + } else { + $this->extractMetaDataFromArticle () ; + $this->formtype = 'initial'; + } + } } - if ( !$wgUser->isAllowed('edit') ) { - if ( $wgUser->isAnon() ) { - $this->userNotLoggedInPage(); - return; - } else { - $wgOut->readOnlyPage( $this->mArticle->getContent( true ), true ); - return; + + wfProfileIn( "$fname-business-end" ); + + $this->isConflict = false; + // css / js subpages of user pages get a special treatment + $this->isCssJsSubpage = $wgTitle->isCssJsSubpage(); + $this->isValidCssJsSubpage = $wgTitle->isValidCssJsSubpage(); + + /* Notice that we can't use isDeleted, because it returns true if article is ever deleted + * no matter it's current state + */ + $this->deletedSinceEdit = false; + if ( $this->edittime != '' ) { + /* Note that we rely on logging table, which hasn't been always there, + * but that doesn't matter, because this only applies to brand new + * deletes. This is done on every preview and save request. Move it further down + * to only perform it on saves + */ + if ( $this->mTitle->isDeleted() ) { + $this->lastDelete = $this->getLastDelete(); + if ( !is_null($this->lastDelete) ) { + $deletetime = $this->lastDelete->log_timestamp; + if ( ($deletetime - $this->starttime) > 0 ) { + $this->deletedSinceEdit = true; + } + } } } - if ( wfReadOnly() ) { - if( $this->save || $this->preview ) { - $this->editForm( 'preview' ); - } else if ( $this->diff ) { - $this->editForm( 'diff' ); - } else { - $wgOut->readOnlyPage( $this->mArticle->getContent( true ) ); + + # Show applicable editing introductions + if( $this->formtype == 'initial' || $this->firsttime ) + $this->showIntro(); + + if( $this->mTitle->isTalkPage() ) { + $wgOut->addWikiText( wfMsg( 'talkpagetext' ) ); + } + + # Attempt submission here. This will check for edit conflicts, + # and redundantly check for locked database, blocked IPs, etc. + # that edit() already checked just in case someone tries to sneak + # in the back door with a hand-edited submission URL. + + if ( 'save' == $this->formtype ) { + if ( !$this->attemptSave() ) { + wfProfileOut( "$fname-business-end" ); + wfProfileOut( $fname ); + return; } - return; } - if ( $this->save ) { - $this->editForm( 'save' ); - } else if ( $this->preview ) { - $this->editForm( 'preview' ); - } else if ( $this->diff ) { - $this->editForm( 'diff' ); - } else { # First time through - if( $this->previewOnOpen() ) { - $this->editForm( 'preview', true ); - } else { - $this->extractMetaDataFromArticle () ; - $this->editForm( 'initial', true ); + + # First time through: get contents, set time for conflict + # checking, etc. + if ( 'initial' == $this->formtype || $this->firsttime ) { + if ($this->initialiseForm() === false) { + $this->noSuchSectionPage(); + wfProfileOut( "$fname-business-end" ); + wfProfileOut( $fname ); + return; } + if( !$this->mTitle->getArticleId() ) + wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) ); } + + $this->showEditForm(); + wfProfileOut( "$fname-business-end" ); + wfProfileOut( $fname ); } - + /** - * Return true if this page should be previewed when the edit form - * is initially opened. + * Should we show a preview when the edit form is first shown? + * * @return bool - * @access private */ - function previewOnOpen() { - global $wgUser; - return $wgUser->getOption( 'previewonfirst' ) || - ( $this->mTitle->getNamespace() == NS_CATEGORY && - !$this->mTitle->exists() ); + private function previewOnOpen() { + global $wgRequest, $wgUser; + if( $wgRequest->getVal( 'preview' ) == 'yes' ) { + // Explicit override from request + return true; + } elseif( $wgRequest->getVal( 'preview' ) == 'no' ) { + // Explicit override from request + return false; + } elseif( $this->section == 'new' ) { + // Nothing *to* preview for new sections + return false; + } elseif( ( $wgRequest->getVal( 'preload' ) !== '' || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) { + // Standard preference behaviour + return true; + } elseif( !$this->mTitle->exists() && $this->mTitle->getNamespace() == NS_CATEGORY ) { + // Categories are special + return true; + } else { + return false; + } } /** * @todo document + * @param $request */ function importFormData( &$request ) { + global $wgLang, $wgUser; + $fname = 'EditPage::importFormData'; + wfProfileIn( $fname ); + if( $request->wasPosted() ) { # These fields need to be checked for encoding. # Also remove trailing whitespace, but don't remove _initial_ @@ -214,23 +478,41 @@ $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' ); $this->textbox2 = $this->safeUnicodeInput( $request, 'wpTextbox2' ); $this->mMetaData = rtrim( $request->getText( 'metadata' ) ); - $this->summary = $request->getText( 'wpSummary' ); - + # Truncate for whole multibyte characters. +5 bytes for ellipsis + $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 ); + $this->edittime = $request->getVal( 'wpEdittime' ); + $this->starttime = $request->getVal( 'wpStarttime' ); + + $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' ); + if( is_null( $this->edittime ) ) { # If the form is incomplete, force to preview. + wfDebug( "$fname: Form data appears to be incomplete\n" ); + wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" ); $this->preview = true; } else { - if( $this->tokenOk( $request ) ) { + /* Fallback for live preview */ + $this->preview = $request->getCheck( 'wpPreview' ) || $request->getCheck( 'wpLivePreview' ); + $this->diff = $request->getCheck( 'wpDiff' ); + + // Remember whether a save was requested, so we can indicate + // if we forced preview due to session failure. + $this->mTriedSave = !$this->preview; + + if ( $this->tokenOk( $request ) ) { # Some browsers will not report any submit button # if the user hits enter in the comment box. # The unmarked state will be assumed to be a save, # if the form seems otherwise complete. - $this->preview = $request->getCheck( 'wpPreview' ); - $this->diff = $request->getCheck( 'wpDiff' ); + wfDebug( "$fname: Passed token check.\n" ); + } else if ( $this->diff ) { + # Failed token check, but only requested "Show Changes". + wfDebug( "$fname: Failed token check; Show Changes requested.\n" ); } else { # Page might be a hack attempt posted from # an external site. Preview instead of saving. + wfDebug( "$fname: Failed token check; forcing preview\n" ); $this->preview = true; } } @@ -238,272 +520,421 @@ if( !preg_match( '/^\d{14}$/', $this->edittime )) { $this->edittime = null; } - + + if( !preg_match( '/^\d{14}$/', $this->starttime )) { + $this->starttime = null; + } + + $this->recreate = $request->getCheck( 'wpRecreate' ); + $this->minoredit = $request->getCheck( 'wpMinoredit' ); $this->watchthis = $request->getCheck( 'wpWatchthis' ); + + # Don't force edit summaries when a user is editing their own user or talk page + if( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) && $this->mTitle->getText() == $wgUser->getName() ) { + $this->allowBlankSummary = true; + } else { + $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' ); + } + + $this->autoSumm = $request->getText( 'wpAutoSummary' ); } else { # Not a posted form? Start with nothing. + wfDebug( "$fname: Not a posted form.\n" ); $this->textbox1 = ''; $this->textbox2 = ''; $this->mMetaData = ''; $this->summary = ''; $this->edittime = ''; + $this->starttime = wfTimestampNow(); + $this->edit = false; $this->preview = false; $this->save = false; - $this->diff = false; + $this->diff = false; $this->minoredit = false; $this->watchthis = false; + $this->recreate = false; } $this->oldid = $request->getInt( 'oldid' ); # Section edit can come from either the form or a link $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); - + $this->live = $request->getCheck( 'live' ); + $this->editintro = $request->getText( 'editintro' ); + + wfProfileOut( $fname ); } /** * Make sure the form isn't faking a user's credentials. * - * @param WebRequest $request + * @param $request WebRequest * @return bool - * @access private + * @private */ function tokenOk( &$request ) { global $wgUser; - if( $wgUser->isAnon() ) { - # Anonymous users may not have a session - # open. Don't tokenize. - return true; - } else { - return $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) ); - } - } - - function submit() { - $this->edit(); + $token = $request->getVal( 'wpEditToken' ); + $this->mTokenOk = $wgUser->matchEditToken( $token ); + $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token ); + return $this->mTokenOk; } /** - * The edit form is self-submitting, so that when things like - * preview and edit conflicts occur, we get the same form back - * with the extra stuff added. Only when the final submission - * is made and all is well do we actually save and redirect to - * the newly-edited page. - * - * @param string $formtype Type of form either : save, initial, diff or preview - * @param bool $firsttime True to load form data from db + * Show all applicable editing introductions */ - function editForm( $formtype, $firsttime = false ) { + private function showIntro() { global $wgOut, $wgUser; - global $wgLang, $wgContLang, $wgParser, $wgTitle; - global $wgAllowAnonymousMinor, $wgRequest; - global $wgSpamRegex, $wgFilterCallback; - - $sk = $wgUser->getSkin(); - $isConflict = false; - // css / js subpages of user pages get a special treatment - $isCssJsSubpage = $wgTitle->isCssJsSubpage(); - - if(!$this->mTitle->getArticleID()) { # new article - $editintro = $wgRequest->getText( 'editintro' ); - $addstandardintro=true; - if($editintro) { - $introtitle=Title::newFromText($editintro); - if(isset($introtitle) && $introtitle->userCanRead()) { - $rev=Revision::newFromTitle($introtitle); - if($rev) { - $wgOut->addWikiText($rev->getText()); - $addstandardintro=false; - } - } + if( !$this->showCustomIntro() && !$this->mTitle->exists() ) { + if( $wgUser->isLoggedIn() ) { + $wgOut->addWikiText( wfMsg( 'newarticletext' ) ); + } else { + $wgOut->addWikiText( wfMsg( 'newarticletextanon' ) ); } - if($addstandardintro) { - $wgOut->addWikiText(wfmsg('newarticletext')); + $this->showDeletionLog( $wgOut ); + } + } + + /** + * Attempt to show a custom editing introduction, if supplied + * + * @return bool + */ + private function showCustomIntro() { + if( $this->editintro ) { + $title = Title::newFromText( $this->editintro ); + if( $title instanceof Title && $title->exists() && $title->userCanRead() ) { + global $wgOut; + $revision = Revision::newFromTitle( $title ); + $wgOut->addSecondaryWikiText( $revision->getText() ); + return true; + } else { + return false; } + } else { + return false; } + } - if( $this->mTitle->isTalkPage() ) { - $wgOut->addWikiText(wfmsg('talkpagetext')); + /** + * Attempt submission + * @return bool false if output is done, true if the rest of the form should be displayed + */ + function attemptSave() { + global $wgSpamRegex, $wgFilterCallback, $wgUser, $wgOut; + global $wgMaxArticleSize; + + $fname = 'EditPage::attemptSave'; + wfProfileIn( $fname ); + wfProfileIn( "$fname-checks" ); + + if( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) ) + { + wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" ); + return false; } - # Attempt submission here. This will check for edit conflicts, - # and redundantly check for locked database, blocked IPs, etc. - # that edit() already checked just in case someone tries to sneak - # in the back door with a hand-edited submission URL. + # Reintegrate metadata + if ( $this->mMetaData != '' ) $this->textbox1 .= "\n" . $this->mMetaData ; + $this->mMetaData = '' ; - if ( 'save' == $formtype ) { - # Reintegrate metadata - if ( $this->mMetaData != '' ) $this->textbox1 .= "\n" . $this->mMetaData ; - $this->mMetaData = '' ; - - # Check for spam - if ( $wgSpamRegex && preg_match( $wgSpamRegex, $this->textbox1, $matches ) ) { - $this->spamPage ( $matches[0] ); - return; - } - if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section ) ) { - # Error messages or other handling should be performed by the filter function - return; - } - if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { - # Check block state against master, thus 'false'. - $this->blockedIPpage(); - return; - } + # Check for spam + $matches = array(); + if ( $wgSpamRegex && preg_match( $wgSpamRegex, $this->textbox1, $matches ) ) { + $this->spamPage ( $matches[0] ); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section ) ) { + # Error messages or other handling should be performed by the filter function + wfProfileOut( $fname ); + wfProfileOut( "$fname-checks" ); + return false; + } + if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError ) ) ) { + # Error messages etc. could be handled within the hook... + wfProfileOut( $fname ); + wfProfileOut( "$fname-checks" ); + return false; + } elseif( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + wfProfileOut( "$fname-checks " ); + wfProfileOut( $fname ); + return true; + } + if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { + # Check block state against master, thus 'false'. + $this->blockedPage(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + $this->kblength = (int)(strlen( $this->textbox1 ) / 1024); + if ( $this->kblength > $wgMaxArticleSize ) { + // Error will be displayed by showEditForm() + $this->tooBig = true; + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return true; + } - if ( !$wgUser->isAllowed('edit') ) { - if ( $wgUser->isAnon() ) { + if ( !$wgUser->isAllowed('edit') ) { + if ( $wgUser->isAnon() ) { $this->userNotLoggedInPage(); - return; - } - else { - $wgOut->readOnlyPage(); - return; - } + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; } - - if ( wfReadOnly() ) { + else { $wgOut->readOnlyPage(); - return; + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; } - if ( $wgUser->pingLimiter() ) { - $wgOut->rateLimited(); + } + + if ( wfReadOnly() ) { + $wgOut->readOnlyPage(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + if ( $wgUser->pingLimiter() ) { + $wgOut->rateLimited(); + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return false; + } + + # If the article has been deleted while editing, don't save it without + # confirmation + if ( $this->deletedSinceEdit && !$this->recreate ) { + wfProfileOut( "$fname-checks" ); + wfProfileOut( $fname ); + return true; + } + + wfProfileOut( "$fname-checks" ); + + # If article is new, insert it. + $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); + if ( 0 == $aid ) { + + // Late check for create permission, just in case *PARANOIA* + if ( !$this->mTitle->userCan( 'create' ) ) { + wfDebug( "$fname: no create permission\n" ); + $this->noCreatePermission(); + wfProfileOut( $fname ); return; } - # If article is new, insert it. - $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); - if ( 0 == $aid ) { - # Don't save a new article if it's blank. - if ( ( '' == $this->textbox1 ) || - ( wfMsg( 'newarticletext' ) == $this->textbox1 ) ) { + # Don't save a new article if it's blank. + if ( ( '' == $this->textbox1 ) ) { $wgOut->redirect( $this->mTitle->getFullURL() ); - return; - } - if (wfRunHooks('ArticleSave', array(&$this->mArticle, &$wgUser, &$this->textbox1, - &$this->summary, &$this->minoredit, &$this->watchthis, NULL))) - { - - $isComment=($this->section=='new'); - $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, - $this->minoredit, $this->watchthis, false, $isComment); - wfRunHooks('ArticleSaveComplete', array(&$this->mArticle, &$wgUser, $this->textbox1, - $this->summary, $this->minoredit, - $this->watchthis, NULL)); - } - return; + wfProfileOut( $fname ); + return false; } - # Article exists. Check for edit conflict. + $isComment=($this->section=='new'); + $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, + $this->minoredit, $this->watchthis, false, $isComment); - $this->mArticle->clear(); # Force reload of dates, etc. - $this->mArticle->forUpdate( true ); # Lock the article + wfProfileOut( $fname ); + return false; + } - if( ( $this->section != 'new' ) && - ($this->mArticle->getTimestamp() != $this->edittime ) ) { - $isConflict = true; - } - $userid = $wgUser->getID(); - - if ( $isConflict) { - wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" . - $this->mArticle->getTimestamp() . "'\n" ); - $text = $this->mArticle->getTextOfLastEditWithSectionReplacedOrAdded( - $this->section, $this->textbox1, $this->summary, $this->edittime); - } - else { - wfDebug( "EditPage::editForm getting section '$this->section'\n" ); - $text = $this->mArticle->getTextOfLastEditWithSectionReplacedOrAdded( - $this->section, $this->textbox1, $this->summary); - } - # Suppress edit conflict with self - - if ( ( 0 != $userid ) && ( $this->mArticle->getUser() == $userid ) ) { - wfDebug( "Suppressing edit conflict, same user.\n" ); - $isConflict = false; - } else { - # switch from section editing to normal editing in edit conflict - if($isConflict) { - # Attempt merge - if( $this->mergeChangesInto( $text ) ){ - // Successful merge! Maybe we should tell the user the good news? - $isConflict = false; - wfDebug( "Suppressing edit conflict, successful merge.\n" ); - } else { - $this->section = ''; - $this->textbox1 = $text; - wfDebug( "Keeping edit conflict, failed merge.\n" ); - } + # Article exists. Check for edit conflict. + + $this->mArticle->clear(); # Force reload of dates, etc. + $this->mArticle->forUpdate( true ); # Lock the article + + wfDebug("timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n"); + + if( $this->mArticle->getTimestamp() != $this->edittime ) { + $this->isConflict = true; + if( $this->section == 'new' ) { + if( $this->mArticle->getUserText() == $wgUser->getName() && + $this->mArticle->getComment() == $this->summary ) { + // Probably a duplicate submission of a new comment. + // This can happen when squid resends a request after + // a timeout but the first one actually went through. + wfDebug( "EditPage::editForm duplicate new section submission; trigger edit conflict!\n" ); + } else { + // New comment; suppress conflict. + $this->isConflict = false; + wfDebug( "EditPage::editForm conflict suppressed; new section\n" ); } } - if ( ! $isConflict ) { - # All's well - $sectionanchor = ''; - if( $this->section == 'new' ) { - if( $this->summary != '' ) { - $sectionanchor = $this->sectionAnchor( $this->summary ); - } - } elseif( $this->section != '' ) { - # Try to get a section anchor from the section source, redirect to edited section if header found - # XXX: might be better to integrate this into Article::getTextOfLastEditWithSectionReplacedOrAdded - # for duplicate heading checking and maybe parsing - $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); - # we can't deal with anchors, includes, html etc in the header for now, - # headline would need to be parsed to improve this - #if($hasmatch and strlen($matches[2]) > 0 and !preg_match( "/[\\['{<>]/", $matches[2])) { - if($hasmatch and strlen($matches[2]) > 0) { - $sectionanchor = $this->sectionAnchor( $matches[2] ); - } - } - - // Save errors may fall down to the edit form, but we've now - // merged the section into full text. Clear the section field - // so that later submission of conflict forms won't try to - // replace that into a duplicated mess. - $this->textbox1 = $text; - $this->section = ''; - - if (wfRunHooks('ArticleSave', array(&$this->mArticle, &$wgUser, &$text, - &$this->summary, &$this->minoredit, - &$this->watchthis, &$sectionanchor))) - { - # update the article here - if($this->mArticle->updateArticle( $text, $this->summary, $this->minoredit, - $this->watchthis, '', $sectionanchor )) - { - wfRunHooks('ArticleSaveComplete', array(&$this->mArticle, &$wgUser, $text, - $this->summary, $this->minoredit, - $this->watchthis, $sectionanchor)); - return; - } else { - $isConflict = true; - } + } + $userid = $wgUser->getID(); + + if ( $this->isConflict) { + wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" . + $this->mArticle->getTimestamp() . "')\n" ); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime); + } + else { + wfDebug( "EditPage::editForm getting section '$this->section'\n" ); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary); + } + if( is_null( $text ) ) { + wfDebug( "EditPage::editForm activating conflict; section replace failed.\n" ); + $this->isConflict = true; + $text = $this->textbox1; + } + + # Suppress edit conflict with self, except for section edits where merging is required. + if ( ( $this->section == '' ) && ( 0 != $userid ) && ( $this->mArticle->getUser() == $userid ) ) { + wfDebug( "EditPage::editForm Suppressing edit conflict, same user.\n" ); + $this->isConflict = false; + } else { + # switch from section editing to normal editing in edit conflict + if($this->isConflict) { + # Attempt merge + if( $this->mergeChangesInto( $text ) ){ + // Successful merge! Maybe we should tell the user the good news? + $this->isConflict = false; + wfDebug( "EditPage::editForm Suppressing edit conflict, successful merge.\n" ); + } else { + $this->section = ''; + $this->textbox1 = $text; + wfDebug( "EditPage::editForm Keeping edit conflict, failed merge.\n" ); } } } - # First time through: get contents, set time for conflict - # checking, etc. - if ( 'initial' == $formtype || $firsttime ) { - $this->edittime = $this->mArticle->getTimestamp(); - $this->textbox1 = $this->mArticle->getContent( true ); - $this->summary = ''; - $this->proxyCheck(); - } + if ( $this->isConflict ) { + wfProfileOut( $fname ); + return true; + } + + $oldtext = $this->mArticle->getContent(); + + # Handle the user preference to force summaries here, but not for null edits + if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary') + && 0 != strcmp($oldtext, $text) && !Article::getRedirectAutosummary( $text )) { + if( md5( $this->summary ) == $this->autoSumm ) { + $this->missingSummary = true; + wfProfileOut( $fname ); + return( true ); + } + } + + #And a similar thing for new sections + if( $this->section == 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary' ) ) { + if (trim($this->summary) == '') { + $this->missingSummary = true; + wfProfileOut( $fname ); + return( true ); + } + } + + # All's well + wfProfileIn( "$fname-sectionanchor" ); + $sectionanchor = ''; + if( $this->section == 'new' ) { + if ( $this->textbox1 == '' ) { + $this->missingComment = true; + return true; + } + if( $this->summary != '' ) { + $sectionanchor = $this->sectionAnchor( $this->summary ); + # This is a new section, so create a link to the new section + # in the revision summary. + $this->summary = wfMsgForContent('newsectionsummary') . + " [[{$this->mTitle->getPrefixedText()}#{$this->summary}|{$this->summary}]]"; + } + } elseif( $this->section != '' ) { + # Try to get a section anchor from the section source, redirect to edited section if header found + # XXX: might be better to integrate this into Article::replaceSection + # for duplicate heading checking and maybe parsing + $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); + # we can't deal with anchors, includes, html etc in the header for now, + # headline would need to be parsed to improve this + if($hasmatch and strlen($matches[2]) > 0) { + $sectionanchor = $this->sectionAnchor( $matches[2] ); + } + } + wfProfileOut( "$fname-sectionanchor" ); + + // Save errors may fall down to the edit form, but we've now + // merged the section into full text. Clear the section field + // so that later submission of conflict forms won't try to + // replace that into a duplicated mess. + $this->textbox1 = $text; + $this->section = ''; + + // Check for length errors again now that the section is merged in + $this->kblength = (int)(strlen( $text ) / 1024); + if ( $this->kblength > $wgMaxArticleSize ) { + $this->tooBig = true; + wfProfileOut( $fname ); + return true; + } + + # update the article here + if( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit, + $this->watchthis, '', $sectionanchor ) ) { + wfProfileOut( $fname ); + return false; + } else { + $this->isConflict = true; + } + wfProfileOut( $fname ); + return true; + } + + /** + * Initialise form fields in the object + * Called on the first invocation, e.g. when a user clicks an edit link + */ + function initialiseForm() { + $this->edittime = $this->mArticle->getTimestamp(); + $this->summary = ''; + $this->textbox1 = $this->getContent(false); + if ($this->textbox1 === false) return false; + + if ( !$this->mArticle->exists() && $this->mArticle->mTitle->getNamespace() == NS_MEDIAWIKI ) + $this->textbox1 = wfMsgWeirdKey( $this->mArticle->mTitle->getText() ); + wfProxyCheck(); + return true; + } + + /** + * Send the edit form and related headers to $wgOut + * @param $formCallback Optional callable that takes an OutputPage + * parameter; will be called during form output + * near the top, for captchas and the like. + */ + function showEditForm( $formCallback=null ) { + global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize; + + $fname = 'EditPage::showEditForm'; + wfProfileIn( $fname ); + + $sk = $wgUser->getSkin(); + + wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) ) ; + $wgOut->setRobotpolicy( 'noindex,nofollow' ); # Enabled article-related sidebar, toplinks, etc. $wgOut->setArticleRelated( true ); - if ( $isConflict ) { + if ( $this->formtype == 'preview' ) { + $wgOut->setPageTitleActionText( wfMsg( 'preview' ) ); + } + + if ( $this->isConflict ) { $s = wfMsg( 'editconflict', $this->mTitle->getPrefixedText() ); $wgOut->setPageTitle( $s ); $wgOut->addWikiText( wfMsg( 'explainconflict' ) ); $this->textbox2 = $this->textbox1; - $this->textbox1 = $this->mArticle->getContent( true ); + $this->textbox1 = $this->getContent(); $this->edittime = $this->mArticle->getTimestamp(); } else { @@ -512,46 +943,112 @@ $s = wfMsg('editingcomment', $this->mTitle->getPrefixedText() ); } else { $s = wfMsg('editingsection', $this->mTitle->getPrefixedText() ); - if( !$this->preview && !$this->diff ) { + $matches = array(); + if( !$this->summary && !$this->preview && !$this->diff ) { preg_match( "/^(=+)(.+)\\1/mi", $this->textbox1, $matches ); if( !empty( $matches[2] ) ) { $this->summary = "/* ". trim($matches[2])." */ "; } - } + } } } else { $s = wfMsg( 'editing', $this->mTitle->getPrefixedText() ); } $wgOut->setPageTitle( $s ); + + if ( $this->missingComment ) { + $wgOut->addWikiText( wfMsg( 'missingcommenttext' ) ); + } + + if( $this->missingSummary && $this->section != 'new' ) { + $wgOut->addWikiText( wfMsg( 'missingsummary' ) ); + } + + if( $this->missingSummary && $this->section == 'new' ) { + $wgOut->addWikiText( wfMsg( 'missingcommentheader' ) ); + } + + if( !$this->hookError == '' ) { + $wgOut->addWikiText( $this->hookError ); + } + if ( !$this->checkUnicodeCompliantBrowser() ) { $wgOut->addWikiText( wfMsg( 'nonunicodebrowser') ); } - if ( isset( $this->mArticle ) - && isset( $this->mArticle->mRevision ) - && !$this->mArticle->mRevision->isCurrent() ) { - $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() ); - $wgOut->addWikiText( wfMsg( 'editingold' ) ); + if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) { + // Let sysop know that this will make private content public if saved + if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { + $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) ); + } + if( !$this->mArticle->mRevision->isCurrent() ) { + $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() ); + $wgOut->addWikiText( wfMsg( 'editingold' ) ); + } } } if( wfReadOnly() ) { $wgOut->addWikiText( wfMsg( 'readonlywarning' ) ); - } else if ( $isCssJsSubpage and 'preview' != $formtype) { - $wgOut->addWikiText( wfMsg( 'usercssjsyoucanpreview' )); + } elseif( $wgUser->isAnon() && $this->formtype != 'preview' ) { + $wgOut->addWikiText( wfMsg( 'anoneditwarning' ) ); + } else { + if( $this->isCssJsSubpage && $this->formtype != 'preview' ) { + # Check the skin exists + if( $this->isValidCssJsSubpage ) { + $wgOut->addWikiText( wfMsg( 'usercssjsyoucanpreview' ) ); + } else { + $wgOut->addWikiText( wfMsg( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) ); + } + } + } + + if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + # Show a warning if editing an interface message + $wgOut->addWikiText( wfMsg( 'editinginterface' ) ); + } elseif( $this->mTitle->isProtected( 'edit' ) ) { + # Is the title semi-protected? + if( $this->mTitle->isSemiProtected() ) { + $notice = wfMsg( 'semiprotectedpagewarning' ); + if( wfEmptyMsg( 'semiprotectedpagewarning', $notice ) || $notice == '-' ) + $notice = ''; + } else { + # Then it must be protected based on static groups (regular) + $notice = wfMsg( 'protectedpagewarning' ); + } + $wgOut->addWikiText( $notice ); + } + if ( $this->mTitle->isCascadeProtected() ) { + # Is this page under cascading protection from some source pages? + list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources(); + if ( count($cascadeSources) > 0 ) { + # Explain, and list the titles responsible + $notice = wfMsgExt( 'cascadeprotectedwarning', array('parsemag'), count($cascadeSources) ) . "\n"; + foreach( $cascadeSources as $page ) { + $notice .= '* [[:' . $page->getPrefixedText() . "]]\n"; + } + } + $wgOut->addWikiText( $notice ); } - if( $this->mTitle->isProtected('edit') ) { - $wgOut->addWikiText( wfMsg( 'protectedpagewarning' ) ); + + if ( $this->kblength === false ) { + $this->kblength = (int)(strlen( $this->textbox1 ) / 1024); + } + if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) { + $wgOut->addWikiText( wfMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgMaxArticleSize ) ); + } elseif( $this->kblength > 29 ) { + $wgOut->addWikiText( wfMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) ) ); } - $kblength = (int)(strlen( $this->textbox1 ) / 1024); - if( $kblength > 29 ) { - $wgOut->addWikiText( wfMsg( 'longpagewarning', $wgLang->formatNum( $kblength ) ) ); + #need to parse the preview early so that we know which templates are used, + #otherwise users with "show preview after edit box" will get a blank list + if ( $this->formtype == 'preview' ) { + $previewOutput = $this->getPreviewText(); } - $rows = $wgUser->getOption( 'rows' ); - $cols = $wgUser->getOption( 'cols' ); + $rows = $wgUser->getIntOption( 'rows' ); + $cols = $wgUser->getIntOption( 'cols' ); $ew = $wgUser->getOption( 'editwidth' ); if ( $ew ) $ew = " style=\"width:100%\""; @@ -563,15 +1060,10 @@ $summary = wfMsg('summary'); $subject = wfMsg('subject'); - $minor = wfMsg('minoredit'); - $watchthis = wfMsg ('watchthis'); - $save = wfMsg('savearticle'); - $prev = wfMsg('showpreview'); - $diff = wfMsg('showdiff'); $cancel = $sk->makeKnownLink( $this->mTitle->getPrefixedText(), - wfMsg('cancel') ); - $edithelpurl = $sk->makeInternalOrExternalUrl( wfMsg( 'edithelppage' )); + wfMsgExt('cancel', array('parseinline')) ); + $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' )); $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'. htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '. htmlspecialchars( wfMsg( 'newwindow' ) ); @@ -582,7 +1074,7 @@ '[[' . wfMsgForContent( 'copyrightpage' ) . ']]', $wgRightsText ) . "\n</div>"; - if( $wgUser->getOption('showtoolbar') and !$isCssJsSubpage ) { + if( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) { # prepare toolbar for edit buttons $toolbar = $this->getEditToolbar(); } else { @@ -591,346 +1083,477 @@ // activate checkboxes if user wants them to be always active if( !$this->preview && !$this->diff ) { - if( $wgUser->getOption( 'watchdefault' ) ) $this->watchthis = true; - if( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; - - // activate checkbox also if user is already watching the page, - // require wpWatchthis to be unset so that second condition is not - // checked unnecessarily - if( !$this->watchthis && $this->mTitle->userIsWatching() ) $this->watchthis = true; - } - - $minoredithtml = ''; + # Sort out the "watch" checkbox + if( $wgUser->getOption( 'watchdefault' ) ) { + # Watch all edits + $this->watchthis = true; + } elseif( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) { + # Watch creations + $this->watchthis = true; + } elseif( $this->mTitle->userIsWatching() ) { + # Already watched + $this->watchthis = true; + } - if ( $wgUser->isLoggedIn() || $wgAllowAnonymousMinor ) { - $minoredithtml = - "<input tabindex='3' type='checkbox' value='1' name='wpMinoredit'".($this->minoredit?" checked='checked'":""). - " accesskey='".wfMsg('accesskey-minoredit')."' id='wpMinoredit' />". - "<label for='wpMinoredit' title='".wfMsg('tooltip-minoredit')."'>{$minor}</label>"; + if( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; } - $watchhtml = ''; + $wgOut->addHTML( $this->editFormPageTop ); - if ( $wgUser->isLoggedIn() ) { - $watchhtml = "<input tabindex='4' type='checkbox' name='wpWatchthis'".($this->watchthis?" checked='checked'":""). - " accesskey='".wfMsg('accesskey-watch')."' id='wpWatchthis' />". - "<label for='wpWatchthis' title='".wfMsg('tooltip-watch')."'>{$watchthis}</label>"; - } + if ( $wgUser->getOption( 'previewontop' ) ) { - $checkboxhtml = $minoredithtml . $watchhtml . '<br />'; - - $wgOut->addHTML( '<div id="wikiPreview">' ); - if ( 'preview' == $formtype) { - $previewOutput = $this->getPreviewText( $isConflict, $isCssJsSubpage ); - if ( $wgUser->getOption('previewontop' ) ) { - $wgOut->addHTML( $previewOutput ); - if($this->mTitle->getNamespace() == NS_CATEGORY) { - $this->mArticle->closeShowCategory(); - } - $wgOut->addHTML( "<br style=\"clear:both;\" />\n" ); + if ( 'preview' == $this->formtype ) { + $this->showPreview( $previewOutput ); + } else { + $wgOut->addHTML( '<div id="wikiPreview"></div>' ); } - } - $wgOut->addHTML( '</div>' ); - if ( 'diff' == $formtype ) { - if ( $wgUser->getOption('previewontop' ) ) { - $wgOut->addHTML( $this->getDiff() ); + + if ( 'diff' == $this->formtype ) { + $this->showDiff(); } } + $wgOut->addHTML( $this->editFormTextTop ); + # if this is a comment, show a subject line at the top, which is also the edit summary. # Otherwise, show a summary field at the bottom $summarytext = htmlspecialchars( $wgContLang->recodeForEdit( $this->summary ) ); # FIXME - if( $this->section == 'new' ) { - $commentsubject="{$subject}: <input tabindex='1' type='text' value=\"$summarytext\" name=\"wpSummary\" maxlength='200' size='60' /><br />"; - $editsummary = ''; - } else { - $commentsubject = ''; - $editsummary="{$summary}: <input tabindex='2' type='text' value=\"$summarytext\" name=\"wpSummary\" maxlength='200' size='60' /><br />"; - } + if( $this->section == 'new' ) { + $commentsubject="<span id='wpSummaryLabel'><label for='wpSummary'>{$subject}:</label></span>\n<div class='editOptions'>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />"; + $editsummary = ''; + $subjectpreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('subject-preview').':'.$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : ''; + $summarypreview = ''; + } else { + $commentsubject = ''; + $editsummary="<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}:</label></span>\n<div class='editOptions'>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />"; + $summarypreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('summary-preview').':'.$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : ''; + $subjectpreview = ''; + } + # Set focus to the edit box on load, except on preview or diff, where it would interfere with the display if( !$this->preview && !$this->diff ) { - # Don't select the edit box on preview; this interferes with seeing what's going on. $wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' ); } - # Prepare a list of templates used by this page - $templates = ''; - $articleTemplates = $this->mArticle->getUsedTemplates(); - if ( count( $articleTemplates ) > 0 ) { - $templates = '<br />'. wfMsg( 'templatesused' ) . '<ul>'; - foreach ( $articleTemplates as $tpl ) { - if ( $titleObj = Title::makeTitle( NS_TEMPLATE, $tpl ) ) { - $templates .= '<li>' . $sk->makeLinkObj( $titleObj ) . '</li>'; - } - } - $templates .= '</ul>'; - } - - global $wgLivePreview, $wgStylePath; - /** - * Live Preview lets us fetch rendered preview page content and - * add it to the page without refreshing the whole page. - * Set up the button for it; if not supported by the browser - * it will fall through to the normal form submission method. - */ - if( $wgLivePreview ) { - global $wgJsMimeType; - $wgOut->addHTML( '<script type="'.$wgJsMimeType.'" src="' . - htmlspecialchars( $wgStylePath . '/common/preview.js' ) . - '"></script>' . "\n" ); - $liveAction = $wgTitle->getLocalUrl( 'action=submit&wpPreview=true&live=true' ); - $liveOnclick = 'onclick="return !livePreview('. - 'getElementById(\'wikiPreview\'),' . - 'editform.wpTextbox1.value,' . - htmlspecialchars( '"' . $liveAction . '"' ) . ')"'; - } else { - $liveOnclick = ''; - } - + $templates = ($this->preview || $this->section != '') ? $this->mPreviewTemplates : $this->mArticle->getUsedTemplates(); + $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != ''); + global $wgUseMetadataEdit ; - if ( $wgUseMetadataEdit ) - { + if ( $wgUseMetadataEdit ) { $metadata = $this->mMetaData ; $metadata = htmlspecialchars( $wgContLang->recodeForEdit( $metadata ) ) ; - $helppage = Title::newFromText ( wfmsg("metadata_page") ) ; - $top = str_replace ( "$1" , $helppage->getInternalURL() , wfmsg("metadata") ) ; + $top = wfMsgWikiHtml( 'metadata_help' ); $metadata = $top . "<textarea name='metadata' rows='3' cols='{$cols}'{$ew}>{$metadata}</textarea>" ; } else $metadata = "" ; + $hidden = ''; + $recreate = ''; + if ($this->deletedSinceEdit) { + if ( 'save' != $this->formtype ) { + $wgOut->addWikiText( wfMsg('deletedwhileediting')); + } else { + // Hide the toolbar and edit area, use can click preview to get it back + // Add an confirmation checkbox and explanation. + $toolbar = ''; + $hidden = 'type="hidden" style="display:none;"'; + $recreate = $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment )); + $recreate .= + "<br /><input tabindex='1' type='checkbox' value='1' name='wpRecreate' id='wpRecreate' />". + "<label for='wpRecreate' title='".wfMsg('tooltip-recreate')."'>". wfMsg('recreate')."</label>"; + } + } + + $tabindex = 2; + + $checkboxes = self::getCheckboxes( $tabindex, $sk, + array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) ); + + $checkboxhtml = implode( $checkboxes, "\n" ); + + $buttons = $this->getEditButtons( $tabindex ); + $buttonshtml = implode( $buttons, "\n" ); + $safemodehtml = $this->checkUnicodeCompliantBrowser() - ? "" - : "<input type='hidden' name=\"safemode\" value='1' />\n"; + ? '' : Xml::hidden( 'safemode', '1' ); $wgOut->addHTML( <<<END {$toolbar} -<form id="editform" name="editform" method="post" action="$action" -enctype="multipart/form-data"> +<form id="editform" name="editform" method="post" action="$action" enctype="multipart/form-data"> +END +); + + if( is_callable( $formCallback ) ) { + call_user_func_array( $formCallback, array( &$wgOut ) ); + } + + wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) ); + + // Put these up at the top to ensure they aren't lost on early form submission + $wgOut->addHTML( " +<input type='hidden' value=\"" . htmlspecialchars( $this->section ) . "\" name=\"wpSection\" /> +<input type='hidden' value=\"{$this->starttime}\" name=\"wpStarttime\" />\n +<input type='hidden' value=\"{$this->edittime}\" name=\"wpEdittime\" />\n +<input type='hidden' value=\"{$this->scrolltop}\" name=\"wpScrolltop\" id=\"wpScrolltop\" />\n" ); + + $wgOut->addHTML( <<<END +$recreate {$commentsubject} -<textarea tabindex='1' accesskey="," name="wpTextbox1" rows='{$rows}' -cols='{$cols}'{$ew}> +{$subjectpreview} +<textarea tabindex='1' accesskey="," name="wpTextbox1" id="wpTextbox1" rows='{$rows}' +cols='{$cols}'{$ew} $hidden> END . htmlspecialchars( $this->safeUnicodeOutput( $this->textbox1 ) ) . " </textarea> + " ); + + $wgOut->addWikiText( $copywarn ); + $wgOut->addHTML( $this->editFormTextAfterWarn ); + $wgOut->addHTML( " {$metadata} -<br />{$editsummary} +{$editsummary} +{$summarypreview} {$checkboxhtml} {$safemodehtml} -<input tabindex='5' id='wpSave' type='submit' value=\"{$save}\" name=\"wpSave\" accesskey=\"".wfMsg('accesskey-save')."\"". -" title=\"".wfMsg('tooltip-save')."\"/> -<input tabindex='6' id='wpPreview' type='submit' $liveOnclick value=\"{$prev}\" name=\"wpPreview\" accesskey=\"".wfMsg('accesskey-preview')."\"". -" title=\"".wfMsg('tooltip-preview')."\"/> -<input tabindex='7' id='wpDiff' type='submit' value=\"{$diff}\" name=\"wpDiff\" accesskey=\"".wfMsg('accesskey-diff')."\"". -" title=\"".wfMsg('tooltip-diff')."\"/> -<em>{$cancel}</em> | <em>{$edithelp}</em>{$templates}" ); - $wgOut->addWikiText( $copywarn ); +"); + + $wgOut->addHTML( +"<div class='editButtons'> +{$buttonshtml} + <span class='editHelp'>{$cancel} | {$edithelp}</span> +</div><!-- editButtons --> +</div><!-- editOptions -->"); + + $wgOut->addHtml( '<div class="mw-editTools">' ); + $wgOut->addWikiText( wfMsgForContent( 'edittools' ) ); + $wgOut->addHtml( '</div>' ); + + $wgOut->addHTML( $this->editFormTextAfterTools ); + $wgOut->addHTML( " -<input type='hidden' value=\"" . htmlspecialchars( $this->section ) . "\" name=\"wpSection\" /> -<input type='hidden' value=\"{$this->edittime}\" name=\"wpEdittime\" />\n" ); +<div class='templatesUsed'> +{$formattedtemplates} +</div> +" ); - if ( $wgUser->isLoggedIn() ) { - /** - * To make it harder for someone to slip a user a page - * which submits an edit form to the wiki without their - * knowledge, a random token is associated with the login - * session. If it's not passed back with the submission, - * we won't save the page, or render user JavaScript and - * CSS previews. - */ - $token = htmlspecialchars( $wgUser->editToken() ); - $wgOut->addHTML( " -<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" ); - } - - - if ( $isConflict ) { - require_once( "DifferenceEngine.php" ); + /** + * To make it harder for someone to slip a user a page + * which submits an edit form to the wiki without their + * knowledge, a random token is associated with the login + * session. If it's not passed back with the submission, + * we won't save the page, or render user JavaScript and + * CSS previews. + * + * For anon editors, who may not have a session, we just + * include the constant suffix to prevent editing from + * broken text-mangling proxies. + */ + $token = htmlspecialchars( $wgUser->editToken() ); + $wgOut->addHTML( "\n<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" ); + + + # If a blank edit summary was previously provided, and the appropriate + # user preference is active, pass a hidden tag here. This will stop the + # user being bounced back more than once in the event that a summary + # is not required. + if( $this->missingSummary ) { + $wgOut->addHTML( "<input type=\"hidden\" name=\"wpIgnoreBlankSummary\" value=\"1\" />\n" ); + } + + # For a bit more sophisticated detection of blank summaries, hash the + # automatic one and pass that in a hidden field. + $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); + $wgOut->addHtml( wfHidden( 'wpAutoSummary', $autosumm ) ); + + if ( $this->isConflict ) { $wgOut->addWikiText( '==' . wfMsg( "yourdiff" ) . '==' ); - DifferenceEngine::showDiff( $this->textbox2, $this->textbox1, - wfMsg( "yourtext" ), wfMsg( "storedversion" ) ); + + $de = new DifferenceEngine( $this->mTitle ); + $de->setText( $this->textbox2, $this->textbox1 ); + $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) ); $wgOut->addWikiText( '==' . wfMsg( "yourtext" ) . '==' ); $wgOut->addHTML( "<textarea tabindex=6 id='wpTextbox2' name=\"wpTextbox2\" rows='{$rows}' cols='{$cols}' wrap='virtual'>" -. htmlspecialchars( $this->safeUnicodeOutput( $this->textbox2 ) ) . -" -</textarea>" ); + . htmlspecialchars( $this->safeUnicodeOutput( $this->textbox2 ) ) . "\n</textarea>" ); } + $wgOut->addHTML( $this->editFormTextBottom ); $wgOut->addHTML( "</form>\n" ); - if ( $formtype == 'preview' && !$wgUser->getOption( 'previewontop' ) ) { - $wgOut->addHTML( '<div id="wikiPreview">' . $previewOutput . '</div>' ); + if ( !$wgUser->getOption( 'previewontop' ) ) { + + if ( $this->formtype == 'preview') { + $this->showPreview( $previewOutput ); + } else { + $wgOut->addHTML( '<div id="wikiPreview"></div>' ); + } + + if ( $this->formtype == 'diff') { + $this->showDiff(); + } + + } + + wfProfileOut( $fname ); + } + + /** + * Append preview output to $wgOut. + * Includes category rendering if this is a category page. + * + * @param string $text The HTML to be output for the preview. + */ + private function showPreview( $text ) { + global $wgOut; + + $wgOut->addHTML( '<div id="wikiPreview">' ); + if($this->mTitle->getNamespace() == NS_CATEGORY) { + $this->mArticle->openShowCategory(); } - if ( $formtype == 'diff' && !$wgUser->getOption( 'previewontop' ) ) { - #$wgOut->addHTML( '<div id="wikiPreview">' . $difftext . '</div>' ); - $wgOut->addHTML( $this->getDiff() ); + wfRunHooks( 'OutputPageBeforeHTML',array( &$wgOut, &$text ) ); + $wgOut->addHTML( $text ); + if($this->mTitle->getNamespace() == NS_CATEGORY) { + $this->mArticle->closeShowCategory(); } + $wgOut->addHTML( '</div>' ); + } + + /** + * Live Preview lets us fetch rendered preview page content and + * add it to the page without refreshing the whole page. + * If not supported by the browser it will fall through to the normal form + * submission method. + * + * This function outputs a script tag to support live preview, and + * returns an onclick handler which should be added to the attributes + * of the preview button + */ + function doLivePreviewScript() { + global $wgStylePath, $wgJsMimeType, $wgStyleVersion, $wgOut, $wgTitle; + $wgOut->addHTML( '<script type="'.$wgJsMimeType.'" src="' . + htmlspecialchars( "$wgStylePath/common/preview.js?$wgStyleVersion" ) . + '"></script>' . "\n" ); + $liveAction = $wgTitle->getLocalUrl( 'action=submit&wpPreview=true&live=true' ); + return "return !livePreview(" . + "getElementById('wikiPreview')," . + "editform.wpTextbox1.value," . + '"' . $liveAction . '"' . ")"; + } + + function getLastDelete() { + $dbr = wfGetDB( DB_SLAVE ); + $fname = 'EditPage::getLastDelete'; + $res = $dbr->select( + array( 'logging', 'user' ), + array( 'log_type', + 'log_action', + 'log_timestamp', + 'log_user', + 'log_namespace', + 'log_title', + 'log_comment', + 'log_params', + 'user_name', ), + array( 'log_namespace' => $this->mTitle->getNamespace(), + 'log_title' => $this->mTitle->getDBkey(), + 'log_type' => 'delete', + 'log_action' => 'delete', + 'user_id=log_user' ), + $fname, + array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) ); + + if($dbr->numRows($res) == 1) { + while ( $x = $dbr->fetchObject ( $res ) ) + $data = $x; + $dbr->freeResult ( $res ) ; + } else { + $data = null; + } + return $data; } /** * @todo document */ - function getPreviewText( $isConflict, $isCssJsSubpage ) { - global $wgOut, $wgUser, $wgTitle, $wgParser, $wgAllowDiffPreview, $wgEnableDiffPreviewPreference; + function getPreviewText() { + global $wgOut, $wgUser, $wgTitle, $wgParser; + + $fname = 'EditPage::getPreviewText'; + wfProfileIn( $fname ); + + if ( $this->mTriedSave && !$this->mTokenOk ) { + if ( $this->mTokenOkExceptSuffix ) { + $msg = 'token_suffix_mismatch'; + } else { + $msg = 'session_fail_preview'; + } + } else { + $msg = 'previewnote'; + } $previewhead = '<h2>' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>\n" . - "<p class='previewnote'>" . htmlspecialchars( wfMsg( 'previewnote' ) ) . "</p>\n"; - if ( $isConflict ) { - $previewhead.='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . - "</h2>\n"; + "<div class='previewnote'>" . $wgOut->parse( wfMsg( $msg ) ) . "</div>\n"; + if ( $this->isConflict ) { + $previewhead.='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n"; } $parserOptions = ParserOptions::newFromUser( $wgUser ); $parserOptions->setEditSection( false ); + global $wgRawHtml; + if( $wgRawHtml && !$this->mTokenOk ) { + // Could be an offsite preview attempt. This is very unsafe if + // HTML is enabled, as it could be an attack. + return $wgOut->parse( "<div class='previewnote'>" . + wfMsg( 'session_fail_preview_html' ) . "</div>" ); + } + # don't parse user css/js, show message about preview # XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here - if ( $isCssJsSubpage ) { + if ( $this->isCssJsSubpage ) { if(preg_match("/\\.css$/", $wgTitle->getText() ) ) { $previewtext = wfMsg('usercsspreview'); } else if(preg_match("/\\.js$/", $wgTitle->getText() ) ) { $previewtext = wfMsg('userjspreview'); } + $parserOptions->setTidy(true); $parserOutput = $wgParser->parse( $previewtext , $wgTitle, $parserOptions ); $wgOut->addHTML( $parserOutput->mText ); + wfProfileOut( $fname ); return $previewhead; } else { - # if user want to see preview when he edit an article - if( $wgUser->getOption('previewonfirst') and ($this->textbox1 == '')) { - $this->textbox1 = $this->mArticle->getContent(true); - } - $toparse = $this->textbox1; - + # If we're adding a comment, we need to show the # summary as the headline if($this->section=="new" && $this->summary!="") { $toparse="== {$this->summary} ==\n\n".$toparse; } - + if ( $this->mMetaData != "" ) $toparse .= "\n" . $this->mMetaData ; - + $parserOptions->setTidy(true); $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ) ."\n\n", - $wgTitle, $parserOptions ); - - $previewHTML = $parserOutput->mText; + $wgTitle, $parserOptions ); + + $previewHTML = $parserOutput->getText(); + $wgOut->addParserOutputNoText( $parserOutput ); - $wgOut->addCategoryLinks($parserOutput->getCategoryLinks()); - $wgOut->addLanguageLinks($parserOutput->getLanguageLinks()); + # ParserOutput might have altered the page title, so reset it + $wgOut->setPageTitle( wfMsg( 'editing', $this->mTitle->getPrefixedText() ) ); + + foreach ( $parserOutput->getTemplates() as $ns => $template) + foreach ( array_keys( $template ) as $dbk) + $this->mPreviewTemplates[] = Title::makeTitle($ns, $dbk); + + wfProfileOut( $fname ); return $previewhead . $previewHTML; } } - + /** - * @todo document + * Call the stock "user is blocked" page */ - function blockedIPpage() { - global $wgOut, $wgUser, $wgContLang, $wgIP; - - $wgOut->setPageTitle( wfMsg( 'blockedtitle' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $wgOut->setArticleRelated( false ); + function blockedPage() { + global $wgOut, $wgUser; + $wgOut->blockedPage( false ); # Standard block notice on the top, don't 'return' - $id = $wgUser->blockedBy(); - $reason = $wgUser->blockedFor(); - $ip = $wgIP; - - if ( is_numeric( $id ) ) { - $name = User::whoIs( $id ); + # If the user made changes, preserve them when showing the markup + # (This happens when a user is blocked during edit, for instance) + $first = $this->firsttime || ( !$this->save && $this->textbox1 == '' ); + if( $first ) { + $source = $this->mTitle->exists() ? $this->getContent() : false; } else { - $name = $id; + $source = $this->textbox1; } - $link = '[[' . $wgContLang->getNsText( NS_USER ) . - ":{$name}|{$name}]]"; - $wgOut->addWikiText( wfMsg( 'blockedtext', $link, $reason, $ip, $name ) ); - $wgOut->returnToMain( false ); + # Spit out the source or the user's modified version + if( $source !== false ) { + $rows = $wgUser->getOption( 'rows' ); + $cols = $wgUser->getOption( 'cols' ); + $attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' ); + $wgOut->addHtml( '<hr />' ); + $wgOut->addWikiText( wfMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() ) ); + $wgOut->addHtml( wfOpenElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . wfCloseElement( 'textarea' ) ); + } } /** - * @todo document + * Produce the stock "please login to edit pages" page */ function userNotLoggedInPage() { - global $wgOut; + global $wgUser, $wgOut; + $skin = $wgUser->getSkin(); + + $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); + $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $this->mTitle->getPrefixedUrl() ); $wgOut->setPageTitle( wfMsg( 'whitelistedittitle' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - $wgOut->addWikiText( wfMsg( 'whitelistedittext' ) ); - $wgOut->returnToMain( false ); + $wgOut->addHtml( wfMsgWikiHtml( 'whitelistedittext', $loginLink ) ); + $wgOut->returnToMain( false, $this->mTitle->getPrefixedUrl() ); } /** - * @todo document + * Creates a basic error page which informs the user that + * they have to validate their email address before being + * allowed to edit. */ - function spamPage ( $match = false ) - { + function userNotConfirmedPage() { global $wgOut; - $wgOut->setPageTitle( wfMsg( 'spamprotectiontitle' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + + $wgOut->setPageTitle( wfMsg( 'confirmedittitle' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - $wgOut->addWikiText( wfMsg( 'spamprotectiontext' ) ); - if ( $match ) { - $wgOut->addWikiText( wfMsg( 'spamprotectionmatch', "<nowiki>{$match}</nowiki>" ) ); - } + $wgOut->addWikiText( wfMsg( 'confirmedittext' ) ); $wgOut->returnToMain( false ); } /** - * Forks processes to scan the originating IP for an open proxy server - * MemCached can be used to skip IPs that have already been scanned + * Creates a basic error page which informs the user that + * they have attempted to edit a nonexistant section. */ - function proxyCheck() { - global $wgBlockOpenProxies, $wgProxyPorts, $wgProxyScriptPath; - global $wgIP, $wgUseMemCached, $wgMemc, $wgDBname, $wgProxyMemcExpiry; - - if ( !$wgBlockOpenProxies ) { - return; - } - - # Get MemCached key - $skip = false; - if ( $wgUseMemCached ) { - $mcKey = $wgDBname.':proxy:ip:'.$wgIP; - $mcValue = $wgMemc->get( $mcKey ); - if ( $mcValue ) { - $skip = true; - } - } + function noSuchSectionPage() { + global $wgOut; + + $wgOut->setPageTitle( wfMsg( 'nosuchsectiontitle' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->setArticleRelated( false ); - # Fork the processes - if ( !$skip ) { - $title = Title::makeTitle( NS_SPECIAL, 'Blockme' ); - $iphash = md5( $wgIP . $wgProxyKey ); - $url = $title->getFullURL( 'ip='.$iphash ); + $wgOut->addWikiText( wfMsg( 'nosuchsectiontext', $this->section ) ); + $wgOut->returnToMain( false, $this->mTitle->getPrefixedUrl() ); + } - foreach ( $wgProxyPorts as $port ) { - $params = implode( ' ', array( - escapeshellarg( $wgProxyScriptPath ), - escapeshellarg( $wgIP ), - escapeshellarg( $port ), - escapeshellarg( $url ) - )); - exec( "php $params &>/dev/null &" ); - } - # Set MemCached key - if ( $wgUseMemCached ) { - $wgMemc->set( $mcKey, 1, $wgProxyMemcExpiry ); - } - } + /** + * Produce the stock "your edit contains spam" page + * + * @param $match Text which triggered one or more filters + */ + function spamPage( $match = false ) { + global $wgOut; + + $wgOut->setPageTitle( wfMsg( 'spamprotectiontitle' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->setArticleRelated( false ); + + $wgOut->addWikiText( wfMsg( 'spamprotectiontext' ) ); + if ( $match ) + $wgOut->addWikiText( wfMsg( 'spamprotectionmatch', "<nowiki>{$match}</nowiki>" ) ); + + $wgOut->returnToMain( false ); } /** - * @access private + * @private * @todo document */ function mergeChangesInto( &$editText ){ $fname = 'EditPage::mergeChangesInto'; wfProfileIn( $fname ); - $db =& wfGetDB( DB_MASTER ); - + $db = wfGetDB( DB_MASTER ); + // This is the revision the editor started from $baseRevision = Revision::loadFromTimestamp( $db, $this->mArticle->mTitle, $this->edittime ); @@ -948,7 +1571,8 @@ return false; } $currentText = $currentRevision->getText(); - + + $result = ''; if( wfMerge( $baseText, $editText, $currentText, $result ) ){ $editText = $result; wfProfileOut( $fname ); @@ -964,7 +1588,7 @@ * mangle UTF-8 data on form submission. Returns true if Unicode * should make it through, false if it's known to be a problem. * @return bool - * @access private + * @private */ function checkUnicodeCompliantBrowser() { global $wgBrowserBlackList; @@ -985,11 +1609,11 @@ * Format an anchor fragment as it would appear for a given section name * @param string $text * @return string - * @access private + * @private */ function sectionAnchor( $text ) { $headline = Sanitizer::decodeCharReferences( $text ); - # strip out HTML + # strip out HTML $headline = preg_replace( '/<.*?' . '>/', '', $headline ); $headline = trim( $headline ); $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) ); @@ -1009,7 +1633,7 @@ * The necessary JavaScript code can be found in style/wikibits.js. */ function getEditToolbar() { - global $wgStylePath, $wgContLang, $wgMimeType, $wgJsMimeType; + global $wgStylePath, $wgContLang, $wgJsMimeType; /** * toolarray an array of arrays which each include the filename of @@ -1022,90 +1646,102 @@ * can figure out a way to make them work in IE. However, we should make * sure these keys are not defined on the edit page. */ - $toolarray=array( - array( 'image'=>'button_bold.png', - 'open' => "\'\'\'", - 'close' => "\'\'\'", - 'sample'=> wfMsg('bold_sample'), - 'tip' => wfMsg('bold_tip'), - 'key' => 'B' - ), - array( 'image'=>'button_italic.png', - 'open' => "\'\'", - 'close' => "\'\'", - 'sample'=> wfMsg('italic_sample'), - 'tip' => wfMsg('italic_tip'), - 'key' => 'I' - ), - array( 'image'=>'button_link.png', - 'open' => '[[', - 'close' => ']]', - 'sample'=> wfMsg('link_sample'), - 'tip' => wfMsg('link_tip'), - 'key' => 'L' - ), - array( 'image'=>'button_extlink.png', - 'open' => '[', - 'close' => ']', - 'sample'=> wfMsg('extlink_sample'), - 'tip' => wfMsg('extlink_tip'), - 'key' => 'X' - ), - array( 'image'=>'button_headline.png', - 'open' => "\\n== ", - 'close' => " ==\\n", - 'sample'=> wfMsg('headline_sample'), - 'tip' => wfMsg('headline_tip'), - 'key' => 'H' - ), - array( 'image'=>'button_image.png', - 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).":", - 'close' => ']]', - 'sample'=> wfMsg('image_sample'), - 'tip' => wfMsg('image_tip'), - 'key' => 'D' - ), - array( 'image' =>'button_media.png', - 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':', - 'close' => ']]', - 'sample'=> wfMsg('media_sample'), - 'tip' => wfMsg('media_tip'), - 'key' => 'M' - ), - array( 'image' =>'button_math.png', - 'open' => "\\<math\\>", - 'close' => "\\</math\\>", - 'sample'=> wfMsg('math_sample'), - 'tip' => wfMsg('math_tip'), - 'key' => 'C' - ), - array( 'image' =>'button_nowiki.png', - 'open' => "\\<nowiki\\>", - 'close' => "\\</nowiki\\>", - 'sample'=> wfMsg('nowiki_sample'), - 'tip' => wfMsg('nowiki_tip'), - 'key' => 'N' - ), - array( 'image' =>'button_sig.png', - 'open' => '--~~~~', - 'close' => '', - 'sample'=> '', - 'tip' => wfMsg('sig_tip'), - 'key' => 'Y' - ), - array( 'image' =>'button_hr.png', - 'open' => "\\n----\\n", - 'close' => '', - 'sample'=> '', - 'tip' => wfMsg('hr_tip'), - 'key' => 'R' - ) + $toolarray = array( + array( 'image' => 'button_bold.png', + 'id' => 'mw-editbutton-bold', + 'open' => '\\\'\\\'\\\'', + 'close' => '\\\'\\\'\\\'', + 'sample'=> wfMsg('bold_sample'), + 'tip' => wfMsg('bold_tip'), + 'key' => 'B' + ), + array( 'image' => 'button_italic.png', + 'id' => 'mw-editbutton-italic', + 'open' => '\\\'\\\'', + 'close' => '\\\'\\\'', + 'sample'=> wfMsg('italic_sample'), + 'tip' => wfMsg('italic_tip'), + 'key' => 'I' + ), + array( 'image' => 'button_link.png', + 'id' => 'mw-editbutton-link', + 'open' => '[[', + 'close' => ']]', + 'sample'=> wfMsg('link_sample'), + 'tip' => wfMsg('link_tip'), + 'key' => 'L' + ), + array( 'image' => 'button_extlink.png', + 'id' => 'mw-editbutton-extlink', + 'open' => '[', + 'close' => ']', + 'sample'=> wfMsg('extlink_sample'), + 'tip' => wfMsg('extlink_tip'), + 'key' => 'X' + ), + array( 'image' => 'button_headline.png', + 'id' => 'mw-editbutton-headline', + 'open' => "\\n== ", + 'close' => " ==\\n", + 'sample'=> wfMsg('headline_sample'), + 'tip' => wfMsg('headline_tip'), + 'key' => 'H' + ), + array( 'image' => 'button_image.png', + 'id' => 'mw-editbutton-image', + 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).":", + 'close' => ']]', + 'sample'=> wfMsg('image_sample'), + 'tip' => wfMsg('image_tip'), + 'key' => 'D' + ), + array( 'image' => 'button_media.png', + 'id' => 'mw-editbutton-media', + 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':', + 'close' => ']]', + 'sample'=> wfMsg('media_sample'), + 'tip' => wfMsg('media_tip'), + 'key' => 'M' + ), + array( 'image' => 'button_math.png', + 'id' => 'mw-editbutton-math', + 'open' => "<math>", + 'close' => "<\\/math>", + 'sample'=> wfMsg('math_sample'), + 'tip' => wfMsg('math_tip'), + 'key' => 'C' + ), + array( 'image' => 'button_nowiki.png', + 'id' => 'mw-editbutton-nowiki', + 'open' => "<nowiki>", + 'close' => "<\\/nowiki>", + 'sample'=> wfMsg('nowiki_sample'), + 'tip' => wfMsg('nowiki_tip'), + 'key' => 'N' + ), + array( 'image' => 'button_sig.png', + 'id' => 'mw-editbutton-signature', + 'open' => '--~~~~', + 'close' => '', + 'sample'=> '', + 'tip' => wfMsg('sig_tip'), + 'key' => 'Y' + ), + array( 'image' => 'button_hr.png', + 'id' => 'mw-editbutton-hr', + 'open' => "\\n----\\n", + 'close' => '', + 'sample'=> '', + 'tip' => wfMsg('hr_tip'), + 'key' => 'R' + ) ); - $toolbar ="<script type='$wgJsMimeType'>\n/*<![CDATA[*/\n"; + $toolbar = "<div id='toolbar'>\n"; + $toolbar.="<script type='$wgJsMimeType'>\n/*<![CDATA[*/\n"; - $toolbar.="document.writeln(\"<div id='toolbar'>\");\n"; foreach($toolarray as $tool) { + $cssId = $tool['id']; $image=$wgStylePath.'/common/images/'.$tool['image']; $open=$tool['open']; $close=$tool['close']; @@ -1119,17 +1755,135 @@ #$key = $tool["key"]; - $toolbar.="addButton('$image','$tip','$open','$close','$sample');\n"; + $toolbar.="addButton('$image','$tip','$open','$close','$sample','$cssId');\n"; } - $toolbar.="addInfobox('" . wfEscapeJsString( wfMsg( "infobox" ) ) . - "','" . wfEscapeJsString( wfMsg( "infobox_alert" ) ) . "');\n"; - $toolbar.="document.writeln(\"</div>\");\n"; - $toolbar.="/*]]>*/\n</script>"; + $toolbar.="\n</div>"; return $toolbar; } - + + /** + * Returns an array of html code of the following checkboxes: + * minor and watch + * + * @param $tabindex Current tabindex + * @param $skin Skin object + * @param $checked Array of checkbox => bool, where bool indicates the checked + * status of the checkbox + * + * @return array + */ + public static function getCheckboxes( &$tabindex, $skin, $checked ) { + global $wgUser; + + $checkboxes = array(); + + $checkboxes['minor'] = ''; + $minorLabel = wfMsgExt('minoredit', array('parseinline')); + if ( $wgUser->isAllowed('minoredit') ) { + $attribs = array( + 'tabindex' => ++$tabindex, + 'accesskey' => wfMsg( 'accesskey-minoredit' ), + 'id' => 'wpMinoredit', + ); + $checkboxes['minor'] = + Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) . + " <label for='wpMinoredit'".$skin->tooltipAndAccesskey('minoredit').">{$minorLabel}</label>"; + } + + $watchLabel = wfMsgExt('watchthis', array('parseinline')); + $checkboxes['watch'] = ''; + if ( $wgUser->isLoggedIn() ) { + $attribs = array( + 'tabindex' => ++$tabindex, + 'accesskey' => wfMsg( 'accesskey-watch' ), + 'id' => 'wpWatchthis', + ); + $checkboxes['watch'] = + Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) . + " <label for='wpWatchthis'".$skin->tooltipAndAccesskey('watch').">{$watchLabel}</label>"; + } + return $checkboxes; + } + + /** + * Returns an array of html code of the following buttons: + * save, diff, preview and live + * + * @param $tabindex Current tabindex + * + * @return array + */ + public function getEditButtons(&$tabindex) { + global $wgLivePreview, $wgUser; + + $buttons = array(); + + $temp = array( + 'id' => 'wpSave', + 'name' => 'wpSave', + 'type' => 'submit', + 'tabindex' => ++$tabindex, + 'value' => wfMsg('savearticle'), + 'accesskey' => wfMsg('accesskey-save'), + 'title' => wfMsg( 'tooltip-save' ).' ['.wfMsg( 'accesskey-save' ).']', + ); + $buttons['save'] = wfElement('input', $temp, ''); + + ++$tabindex; // use the same for preview and live preview + if ( $wgLivePreview && $wgUser->getOption( 'uselivepreview' ) ) { + $temp = array( + 'id' => 'wpPreview', + 'name' => 'wpPreview', + 'type' => 'submit', + 'tabindex' => $tabindex, + 'value' => wfMsg('showpreview'), + 'accesskey' => '', + 'title' => wfMsg( 'tooltip-preview' ).' ['.wfMsg( 'accesskey-preview' ).']', + 'style' => 'display: none;', + ); + $buttons['preview'] = wfElement('input', $temp, ''); + + $temp = array( + 'id' => 'wpLivePreview', + 'name' => 'wpLivePreview', + 'type' => 'submit', + 'tabindex' => $tabindex, + 'value' => wfMsg('showlivepreview'), + 'accesskey' => wfMsg('accesskey-preview'), + 'title' => '', + 'onclick' => $this->doLivePreviewScript(), + ); + $buttons['live'] = wfElement('input', $temp, ''); + } else { + $temp = array( + 'id' => 'wpPreview', + 'name' => 'wpPreview', + 'type' => 'submit', + 'tabindex' => $tabindex, + 'value' => wfMsg('showpreview'), + 'accesskey' => wfMsg('accesskey-preview'), + 'title' => wfMsg( 'tooltip-preview' ).' ['.wfMsg( 'accesskey-preview' ).']', + ); + $buttons['preview'] = wfElement('input', $temp, ''); + $buttons['live'] = ''; + } + + $temp = array( + 'id' => 'wpDiff', + 'name' => 'wpDiff', + 'type' => 'submit', + 'tabindex' => ++$tabindex, + 'value' => wfMsg('showdiff'), + 'accesskey' => wfMsg('accesskey-diff'), + 'title' => wfMsg( 'tooltip-diff' ).' ['.wfMsg( 'accesskey-diff' ).']', + ); + $buttons['diff'] = wfElement('input', $temp, ''); + + return $buttons; + } + /** * Output preview text only. This can be sucked into the edit page * via JavaScript, and saves the server time rendering the skin as @@ -1138,17 +1892,23 @@ * failure, etc). * * @todo This doesn't include category or interlanguage links. - * Would need to enhance it a bit, maybe wrap them in XML - * or something... that might also require more skin + * Would need to enhance it a bit, <s>maybe wrap them in XML + * or something...</s> that might also require more skin * initialization, so check whether that's a problem. */ function livePreview() { global $wgOut; $wgOut->disable(); - header( 'Content-type: text/xml' ); + header( 'Content-type: text/xml; charset=utf-8' ); header( 'Cache-control: no-cache' ); - # FIXME - echo $this->getPreviewText( false, false ); + + $s = + '<?xml version="1.0" encoding="UTF-8" ?>' . "\n" . + Xml::openElement( 'livepreview' ) . + Xml::element( 'preview', null, $this->getPreviewText() ) . + Xml::element( 'br', array( 'style' => 'clear: both;' ) ) . + Xml::closeElement( 'livepreview' ); + echo $s; } @@ -1158,21 +1918,25 @@ * * If this is a section edit, we'll replace the section as for final * save and then make a comparison. - * - * @return string HTML */ - function getDiff() { - require_once( 'DifferenceEngine.php' ); + function showDiff() { $oldtext = $this->mArticle->fetchContent(); - $newtext = $this->mArticle->getTextOfLastEditWithSectionReplacedOrAdded( + $newtext = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); - $oldtitle = wfMsg( 'currentrev' ); - $newtitle = wfMsg( 'yourtext' ); - if ( $oldtext != wfMsg( 'noarticletext' ) || $newtext != '' ) { - $difftext = DifferenceEngine::getDiff( $oldtext, $newtext, $oldtitle, $newtitle ); + $newtext = $this->mArticle->preSaveTransform( $newtext ); + $oldtitle = wfMsgExt( 'currentrev', array('parseinline') ); + $newtitle = wfMsgExt( 'yourtext', array('parseinline') ); + if ( $oldtext !== false || $newtext != '' ) { + $de = new DifferenceEngine( $this->mTitle ); + $de->setText( $oldtext, $newtext ); + $difftext = $de->getDiff( $oldtitle, $newtitle ); + $de->showDiffStyle(); + } else { + $difftext = ''; } - - return '<div id="wikiDiff">' . $difftext . '</div>'; + + global $wgOut; + $wgOut->addHtml( '<div id="wikiDiff">' . $difftext . '</div>' ); } /** @@ -1182,7 +1946,7 @@ * @param WebRequest $request * @param string $field * @return string - * @access private + * @private */ function safeUnicodeInput( $request, $field ) { $text = rtrim( $request->getText( $field ) ); @@ -1190,14 +1954,14 @@ ? $this->unmakesafe( $text ) : $text; } - + /** * Filter an output field through a Unicode armoring process if it is * going to an old browser with known broken Unicode editing issues. * * @param string $text * @return string - * @access private + * @private */ function safeUnicodeOutput( $text ) { global $wgContLang; @@ -1206,7 +1970,7 @@ ? $codedText : $this->makesafe( $codedText ); } - + /** * A number of web browsers are known to corrupt non-ASCII characters * in a UTF-8 text editing environment. To protect against this, @@ -1218,12 +1982,12 @@ * * @param string $invalue * @return string - * @access private + * @private */ function makesafe( $invalue ) { // Armor existing references for reversability. $invalue = strtr( $invalue, array( "&#x" => "�" ) ); - + $bytesleft = 0; $result = ""; $working = 0; @@ -1252,7 +2016,7 @@ } return $result; } - + /** * Reverse the previously applied transliteration of non-ASCII characters * back to UTF-8. Used to protect data from corruption by broken web browsers @@ -1260,7 +2024,7 @@ * * @param string $invalue * @return string - * @access private + * @private */ function unmakesafe( $invalue ) { $result = ""; @@ -1272,11 +2036,11 @@ $hexstring .= $invalue{$i}; $i++; } while( ctype_xdigit( $invalue{$i} ) && ( $i < strlen( $invalue ) ) ); - + // Do some sanity checks. These aren't needed for reversability, - // but should help keep the breakage down if the editor + // but should help keep the breakage down if the editor // breaks one of the entities whilst editing. - if ((substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6)) { + if ((substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6)) { $codepoint = hexdec($hexstring); $result .= codepointToUtf8( $codepoint ); } else { @@ -1289,8 +2053,38 @@ // reverse the transform that we made for reversability reasons. return strtr( $result, array( "�" => "&#x" ) ); } - + function noCreatePermission() { + global $wgOut; + $wgOut->setPageTitle( wfMsg( 'nocreatetitle' ) ); + $wgOut->addWikiText( wfMsg( 'nocreatetext' ) ); + } + + /** + * If there are rows in the deletion log for this page, show them, + * along with a nice little note for the user + * + * @param OutputPage $out + */ + private function showDeletionLog( $out ) { + $title = $this->mArticle->getTitle(); + $reader = new LogReader( + new FauxRequest( + array( + 'page' => $title->getPrefixedText(), + 'type' => 'delete', + ) + ) + ); + if( $reader->hasRows() ) { + $out->addHtml( '<div id="mw-recreate-deleted-warn">' ); + $out->addWikiText( wfMsg( 'recreate-deleted-warn' ) ); + $viewer = new LogViewer( $reader ); + $viewer->showList( $out ); + $out->addHtml( '</div>' ); + } + } + } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Exif.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Exif.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Exif.php 2005-07-22 16:57:15.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Exif.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,8 +1,6 @@ <?php -if ( !defined( 'MEDIAWIKI' ) ) die(); /** - * @package MediaWiki - * @subpackage Metadata + * @addtogroup Media * * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason @@ -10,50 +8,47 @@ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or + * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * - * @link http://exif.org/Exif2-2.PDF The Exif 2.2 specification - * @bug 1555, 1947 + * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification */ -/**#@+ - * Exif tag type definition - */ -define('MW_EXIF_BYTE', 1); # An 8-bit unsigned integer. -define('MW_EXIF_ASCII', 2); # An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL. -define('MW_EXIF_SHORT', 3); # A 16-bit (2-byte) unsigned integer. -define('MW_EXIF_LONG', 4); # A 32-bit (4-byte) unsigned integer. -define('MW_EXIF_RATIONAL', 5); # Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator -define('MW_EXIF_UNDEFINED', 7); # An 8-bit byte that can take any value depending on the field definition -define('MW_EXIF_SLONG', 9); # A 32-bit (4-byte) signed integer (2's complement notation), -define('MW_EXIF_SRATIONAL', 10); # Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. -/**#@-*/ - - /** - * @package MediaWiki - * @subpackage Metadata + * @todo document (e.g. one-sentence class-overview description) + * @addtogroup Media */ class Exif { + //@{ + /* @var array + * @private + */ + /**#@+ - * @var array - * @access private + * Exif tag type definition */ - + const BYTE = 1; # An 8-bit (1-byte) unsigned integer. + const ASCII = 2; # An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL. + const SHORT = 3; # A 16-bit (2-byte) unsigned integer. + const LONG = 4; # A 32-bit (4-byte) unsigned integer. + const RATIONAL = 5; # Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator + const UNDEFINED = 7; # An 8-bit byte that can take any value depending on the field definition + const SLONG = 9; # A 32-bit (4-byte) signed integer (2's complement notation), + const SRATIONAL = 10; # Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. + /** * Exif tags grouped by category, the tagname itself is the key and the type - * is the value, in the case of more than one possible value type they are + * is the value, in the case of more than one possible value type they are * seperated by commas. */ var $mExifTags; @@ -79,15 +74,37 @@ * Filtered and formatted Exif data, see FormatExif::getFormattedData() */ var $mFormattedExifData; - - /**#@-*/ + + //@} + + //@{ + /* @var string + * @private + */ + + /** + * The file being processed + */ + var $file; + + /** + * The basename of the file being processed + */ + var $basename; + + /** + * The private log to log to, e.g. 'exif' + */ + var $log = false; + + //@} /** * Constructor * - * @param string $file + * @param $file String: filename. */ - function Exif( $file ) { + function __construct( $file ) { /** * Page numbers here refer to pages in the EXIF 2.2 standard * @@ -98,191 +115,205 @@ 'tiff' => array( # Tags relating to image structure 'structure' => array( - 'ImageWidth' => MW_EXIF_SHORT.','.MW_EXIF_LONG, # Image width - 'ImageLength' => MW_EXIF_SHORT.','.MW_EXIF_LONG, # Image height - 'BitsPerSample' => MW_EXIF_SHORT, # Number of bits per component + 'ImageWidth' => Exif::SHORT.','.Exif::LONG, # Image width + 'ImageLength' => Exif::SHORT.','.Exif::LONG, # Image height + 'BitsPerSample' => Exif::SHORT, # Number of bits per component # "When a primary image is JPEG compressed, this designation is not" # "necessary and is omitted." (p23) - 'Compression' => MW_EXIF_SHORT, # Compression scheme #p23 - 'PhotometricInterpretation' => MW_EXIF_SHORT, # Pixel composition #p23 - 'Orientation' => MW_EXIF_SHORT, # Orientation of image #p24 - 'SamplesPerPixel' => MW_EXIF_SHORT, # Number of components - 'PlanarConfiguration' => MW_EXIF_SHORT, # Image data arrangement #p24 - 'YCbCrSubSampling' => MW_EXIF_SHORT, # Subsampling ratio of Y to C #p24 - 'YCbCrPositioning' => MW_EXIF_SHORT, # Y and C positioning #p24-25 - 'XResolution' => MW_EXIF_RATIONAL, # Image resolution in width direction - 'YResolution' => MW_EXIF_RATIONAL, # Image resolution in height direction - 'ResolutionUnit' => MW_EXIF_SHORT, # Unit of X and Y resolution #(p26) + 'Compression' => Exif::SHORT, # Compression scheme #p23 + 'PhotometricInterpretation' => Exif::SHORT, # Pixel composition #p23 + 'Orientation' => Exif::SHORT, # Orientation of image #p24 + 'SamplesPerPixel' => Exif::SHORT, # Number of components + 'PlanarConfiguration' => Exif::SHORT, # Image data arrangement #p24 + 'YCbCrSubSampling' => Exif::SHORT, # Subsampling ratio of Y to C #p24 + 'YCbCrPositioning' => Exif::SHORT, # Y and C positioning #p24-25 + 'XResolution' => Exif::RATIONAL, # Image resolution in width direction + 'YResolution' => Exif::RATIONAL, # Image resolution in height direction + 'ResolutionUnit' => Exif::SHORT, # Unit of X and Y resolution #(p26) ), - + # Tags relating to recording offset 'offset' => array( - 'StripOffsets' => MW_EXIF_SHORT.','.MW_EXIF_LONG, # Image data location - 'RowsPerStrip' => MW_EXIF_SHORT.','.MW_EXIF_LONG, # Number of rows per strip - 'StripByteCounts' => MW_EXIF_SHORT.','.MW_EXIF_LONG, # Bytes per compressed strip - 'JPEGInterchangeFormat' => MW_EXIF_SHORT.','.MW_EXIF_LONG, # Offset to JPEG SOI - 'JPEGInterchangeFormatLength' => MW_EXIF_SHORT.','.MW_EXIF_LONG, # Bytes of JPEG data + 'StripOffsets' => Exif::SHORT.','.Exif::LONG, # Image data location + 'RowsPerStrip' => Exif::SHORT.','.Exif::LONG, # Number of rows per strip + 'StripByteCounts' => Exif::SHORT.','.Exif::LONG, # Bytes per compressed strip + 'JPEGInterchangeFormat' => Exif::SHORT.','.Exif::LONG, # Offset to JPEG SOI + 'JPEGInterchangeFormatLength' => Exif::SHORT.','.Exif::LONG, # Bytes of JPEG data ), - + # Tags relating to image data characteristics 'characteristics' => array( - 'TransferFunction' => MW_EXIF_SHORT, # Transfer function - 'WhitePoint' => MW_EXIF_RATIONAL, # White point chromaticity - 'PrimaryChromaticities' => MW_EXIF_RATIONAL, # Chromaticities of primarities - 'YCbCrCoefficients' => MW_EXIF_RATIONAL, # Color space transformation matrix coefficients #p27 - 'ReferenceBlackWhite' => MW_EXIF_RATIONAL # Pair of black and white reference values + 'TransferFunction' => Exif::SHORT, # Transfer function + 'WhitePoint' => Exif::RATIONAL, # White point chromaticity + 'PrimaryChromaticities' => Exif::RATIONAL, # Chromaticities of primarities + 'YCbCrCoefficients' => Exif::RATIONAL, # Color space transformation matrix coefficients #p27 + 'ReferenceBlackWhite' => Exif::RATIONAL # Pair of black and white reference values ), - + # Other tags 'other' => array( - 'DateTime' => MW_EXIF_ASCII, # File change date and time - 'ImageDescription' => MW_EXIF_ASCII, # Image title - 'Make' => MW_EXIF_ASCII, # Image input equipment manufacturer - 'Model' => MW_EXIF_ASCII, # Image input equipment model - 'Software' => MW_EXIF_ASCII, # Software used - 'Artist' => MW_EXIF_ASCII, # Person who created the image - 'Copyright' => MW_EXIF_ASCII, # Copyright holder + 'DateTime' => Exif::ASCII, # File change date and time + 'ImageDescription' => Exif::ASCII, # Image title + 'Make' => Exif::ASCII, # Image input equipment manufacturer + 'Model' => Exif::ASCII, # Image input equipment model + 'Software' => Exif::ASCII, # Software used + 'Artist' => Exif::ASCII, # Person who created the image + 'Copyright' => Exif::ASCII, # Copyright holder ), ), - + # Exif IFD Attribute Information (p30-31) 'exif' => array( # Tags relating to version 'version' => array( # TODO: NOTE: Nonexistence of this field is taken to mean nonconformance # to the EXIF 2.1 AND 2.2 standards - 'ExifVersion' => MW_EXIF_UNDEFINED, # Exif version - 'FlashpixVersion' => MW_EXIF_UNDEFINED, # Supported Flashpix version #p32 + 'ExifVersion' => Exif::UNDEFINED, # Exif version + 'FlashpixVersion' => Exif::UNDEFINED, # Supported Flashpix version #p32 ), - + # Tags relating to Image Data Characteristics 'characteristics' => array( - 'ColorSpace' => MW_EXIF_SHORT, # Color space information #p32 + 'ColorSpace' => Exif::SHORT, # Color space information #p32 ), - + # Tags relating to image configuration 'configuration' => array( - 'ComponentsConfiguration' => MW_EXIF_UNDEFINED, # Meaning of each component #p33 - 'CompressedBitsPerPixel' => MW_EXIF_RATIONAL, # Image compression mode - 'PixelYDimension' => MW_EXIF_SHORT.','.MW_EXIF_LONG, # Valid image width - 'PixelXDimension' => MW_EXIF_SHORT.','.MW_EXIF_LONG, # Valind image height + 'ComponentsConfiguration' => Exif::UNDEFINED, # Meaning of each component #p33 + 'CompressedBitsPerPixel' => Exif::RATIONAL, # Image compression mode + 'PixelYDimension' => Exif::SHORT.','.Exif::LONG, # Valid image width + 'PixelXDimension' => Exif::SHORT.','.Exif::LONG, # Valind image height ), - + # Tags relating to related user information 'user' => array( - 'MakerNote' => MW_EXIF_UNDEFINED, # Manufacturer notes - 'UserComment' => MW_EXIF_UNDEFINED, # User comments #p34 + 'MakerNote' => Exif::UNDEFINED, # Manufacturer notes + 'UserComment' => Exif::UNDEFINED, # User comments #p34 ), - + # Tags relating to related file information 'related' => array( - 'RelatedSoundFile' => MW_EXIF_ASCII, # Related audio file + 'RelatedSoundFile' => Exif::ASCII, # Related audio file ), - + # Tags relating to date and time 'dateandtime' => array( - 'DateTimeOriginal' => MW_EXIF_ASCII, # Date and time of original data generation #p36 - 'DateTimeDigitized' => MW_EXIF_ASCII, # Date and time of original data generation - 'SubSecTime' => MW_EXIF_ASCII, # DateTime subseconds - 'SubSecTimeOriginal' => MW_EXIF_ASCII, # DateTimeOriginal subseconds - 'SubSecTimeDigitized' => MW_EXIF_ASCII, # DateTimeDigitized subseconds + 'DateTimeOriginal' => Exif::ASCII, # Date and time of original data generation #p36 + 'DateTimeDigitized' => Exif::ASCII, # Date and time of original data generation + 'SubSecTime' => Exif::ASCII, # DateTime subseconds + 'SubSecTimeOriginal' => Exif::ASCII, # DateTimeOriginal subseconds + 'SubSecTimeDigitized' => Exif::ASCII, # DateTimeDigitized subseconds ), - + # Tags relating to picture-taking conditions (p31) 'conditions' => array( - 'ExposureTime' => MW_EXIF_RATIONAL, # Exposure time - 'FNumber' => MW_EXIF_RATIONAL, # F Number - 'ExposureProgram' => MW_EXIF_SHORT, # Exposure Program #p38 - 'SpectralSensitivity' => MW_EXIF_ASCII, # Spectral sensitivity - 'ISOSpeedRatings' => MW_EXIF_SHORT, # ISO speed rating - 'OECF' => MW_EXIF_UNDEFINED, # Optoelectronic conversion factor - 'ShutterSpeedValue' => MW_EXIF_SRATIONAL, # Shutter speed - 'ApertureValue' => MW_EXIF_RATIONAL, # Aperture - 'BrightnessValue' => MW_EXIF_SRATIONAL, # Brightness - 'ExposureBiasValue' => MW_EXIF_SRATIONAL, # Exposure bias - 'MaxApertureValue' => MW_EXIF_RATIONAL, # Maximum land aperture - 'SubjectDistance' => MW_EXIF_RATIONAL, # Subject distance - 'MeteringMode' => MW_EXIF_SHORT, # Metering mode #p40 - 'LightSource' => MW_EXIF_SHORT, # Light source #p40-41 - 'Flash' => MW_EXIF_SHORT, # Flash #p41-42 - 'FocalLength' => MW_EXIF_RATIONAL, # Lens focal length - 'SubjectArea' => MW_EXIF_SHORT, # Subject area - 'FlashEnergy' => MW_EXIF_RATIONAL, # Flash energy - 'SpatialFrequencyResponse' => MW_EXIF_UNDEFINED, # Spatial frequency response - 'FocalPlaneXResolution' => MW_EXIF_RATIONAL, # Focal plane X resolution - 'FocalPlaneYResolution' => MW_EXIF_RATIONAL, # Focal plane Y resolution - 'FocalPlaneResolutionUnit' => MW_EXIF_SHORT, # Focal plane resolution unit #p46 - 'SubjectLocation' => MW_EXIF_SHORT, # Subject location - 'ExposureIndex' => MW_EXIF_RATIONAL, # Exposure index - 'SensingMethod' => MW_EXIF_SHORT, # Sensing method #p46 - 'FileSource' => MW_EXIF_UNDEFINED, # File source #p47 - 'SceneType' => MW_EXIF_UNDEFINED, # Scene type #p47 - 'CFAPattern' => MW_EXIF_UNDEFINED, # CFA pattern - 'CustomRendered' => MW_EXIF_SHORT, # Custom image processing #p48 - 'ExposureMode' => MW_EXIF_SHORT, # Exposure mode #p48 - 'WhiteBalance' => MW_EXIF_SHORT, # White Balance #p49 - 'DigitalZoomRatio' => MW_EXIF_RATIONAL, # Digital zoom ration - 'FocalLengthIn35mmFilm' => MW_EXIF_SHORT, # Focal length in 35 mm film - 'SceneCaptureType' => MW_EXIF_SHORT, # Scene capture type #p49 - 'GainControl' => MW_EXIF_RATIONAL, # Scene control #p49-50 - 'Contrast' => MW_EXIF_SHORT, # Contrast #p50 - 'Saturation' => MW_EXIF_SHORT, # Saturation #p50 - 'Sharpness' => MW_EXIF_SHORT, # Sharpness #p50 - 'DeviceSettingDescription' => MW_EXIF_UNDEFINED, # Desice settings description - 'SubjectDistanceRange' => MW_EXIF_SHORT, # Subject distance range #p51 + 'ExposureTime' => Exif::RATIONAL, # Exposure time + 'FNumber' => Exif::RATIONAL, # F Number + 'ExposureProgram' => Exif::SHORT, # Exposure Program #p38 + 'SpectralSensitivity' => Exif::ASCII, # Spectral sensitivity + 'ISOSpeedRatings' => Exif::SHORT, # ISO speed rating + 'OECF' => Exif::UNDEFINED, # Optoelectronic conversion factor + 'ShutterSpeedValue' => Exif::SRATIONAL, # Shutter speed + 'ApertureValue' => Exif::RATIONAL, # Aperture + 'BrightnessValue' => Exif::SRATIONAL, # Brightness + 'ExposureBiasValue' => Exif::SRATIONAL, # Exposure bias + 'MaxApertureValue' => Exif::RATIONAL, # Maximum land aperture + 'SubjectDistance' => Exif::RATIONAL, # Subject distance + 'MeteringMode' => Exif::SHORT, # Metering mode #p40 + 'LightSource' => Exif::SHORT, # Light source #p40-41 + 'Flash' => Exif::SHORT, # Flash #p41-42 + 'FocalLength' => Exif::RATIONAL, # Lens focal length + 'SubjectArea' => Exif::SHORT, # Subject area + 'FlashEnergy' => Exif::RATIONAL, # Flash energy + 'SpatialFrequencyResponse' => Exif::UNDEFINED, # Spatial frequency response + 'FocalPlaneXResolution' => Exif::RATIONAL, # Focal plane X resolution + 'FocalPlaneYResolution' => Exif::RATIONAL, # Focal plane Y resolution + 'FocalPlaneResolutionUnit' => Exif::SHORT, # Focal plane resolution unit #p46 + 'SubjectLocation' => Exif::SHORT, # Subject location + 'ExposureIndex' => Exif::RATIONAL, # Exposure index + 'SensingMethod' => Exif::SHORT, # Sensing method #p46 + 'FileSource' => Exif::UNDEFINED, # File source #p47 + 'SceneType' => Exif::UNDEFINED, # Scene type #p47 + 'CFAPattern' => Exif::UNDEFINED, # CFA pattern + 'CustomRendered' => Exif::SHORT, # Custom image processing #p48 + 'ExposureMode' => Exif::SHORT, # Exposure mode #p48 + 'WhiteBalance' => Exif::SHORT, # White Balance #p49 + 'DigitalZoomRatio' => Exif::RATIONAL, # Digital zoom ration + 'FocalLengthIn35mmFilm' => Exif::SHORT, # Focal length in 35 mm film + 'SceneCaptureType' => Exif::SHORT, # Scene capture type #p49 + 'GainControl' => Exif::RATIONAL, # Scene control #p49-50 + 'Contrast' => Exif::SHORT, # Contrast #p50 + 'Saturation' => Exif::SHORT, # Saturation #p50 + 'Sharpness' => Exif::SHORT, # Sharpness #p50 + 'DeviceSettingDescription' => Exif::UNDEFINED, # Desice settings description + 'SubjectDistanceRange' => Exif::SHORT, # Subject distance range #p51 ), - + 'other' => array( - 'ImageUniqueID' => MW_EXIF_ASCII, # Unique image ID + 'ImageUniqueID' => Exif::ASCII, # Unique image ID ), ), - + # GPS Attribute Information (p52) 'gps' => array( - 'GPSVersionID' => MW_EXIF_BYTE, # GPS tag version - 'GPSLatitudeRef' => MW_EXIF_ASCII, # North or South Latitude #p52-53 - 'GPSLatitude' => MW_EXIF_RATIONAL, # Latitude - 'GPSLongitudeRef' => MW_EXIF_ASCII, # East or West Longitude #p53 - 'GPSLongitude' => MW_EXIF_RATIONAL, # Longitude - 'GPSAltitudeRef' => MW_EXIF_BYTE, # Altitude reference - 'GPSAltitude' => MW_EXIF_RATIONAL, # Altitude - 'GPSTimeStamp' => MW_EXIF_RATIONAL, # GPS time (atomic clock) - 'GPSSatellites' => MW_EXIF_ASCII, # Satellites used for measurement - 'GPSStatus' => MW_EXIF_ASCII, # Receiver status #p54 - 'GPSMeasureMode' => MW_EXIF_ASCII, # Measurement mode #p54-55 - 'GPSDOP' => MW_EXIF_RATIONAL, # Measurement precision - 'GPSSpeedRef' => MW_EXIF_ASCII, # Speed unit #p55 - 'GPSSpeed' => MW_EXIF_RATIONAL, # Speed of GPS receiver - 'GPSTrackRef' => MW_EXIF_ASCII, # Reference for direction of movement #p55 - 'GPSTrack' => MW_EXIF_RATIONAL, # Direction of movement - 'GPSImgDirectionRef' => MW_EXIF_ASCII, # Reference for direction of image #p56 - 'GPSImgDirection' => MW_EXIF_RATIONAL, # Direction of image - 'GPSMapDatum' => MW_EXIF_ASCII, # Geodetic survey data used - 'GPSDestLatitudeRef' => MW_EXIF_ASCII, # Reference for latitude of destination #p56 - 'GPSDestLatitude' => MW_EXIF_RATIONAL, # Latitude destination - 'GPSDestLongitudeRef' => MW_EXIF_ASCII, # Reference for longitude of destination #p57 - 'GPSDestLongitude' => MW_EXIF_RATIONAL, # Longitude of destination - 'GPSDestBearingRef' => MW_EXIF_ASCII, # Reference for bearing of destination #p57 - 'GPSDestBearing' => MW_EXIF_RATIONAL, # Bearing of destination - 'GPSDestDistanceRef' => MW_EXIF_ASCII, # Reference for distance to destination #p57-58 - 'GPSDestDistance' => MW_EXIF_RATIONAL, # Distance to destination - 'GPSProcessingMethod' => MW_EXIF_UNDEFINED, # Name of GPS processing method - 'GPSAreaInformation' => MW_EXIF_UNDEFINED, # Name of GPS area - 'GPSDateStamp' => MW_EXIF_ASCII, # GPS date - 'GPSDifferential' => MW_EXIF_SHORT, # GPS differential correction + 'GPSVersionID' => Exif::BYTE, # GPS tag version + 'GPSLatitudeRef' => Exif::ASCII, # North or South Latitude #p52-53 + 'GPSLatitude' => Exif::RATIONAL, # Latitude + 'GPSLongitudeRef' => Exif::ASCII, # East or West Longitude #p53 + 'GPSLongitude' => Exif::RATIONAL, # Longitude + 'GPSAltitudeRef' => Exif::BYTE, # Altitude reference + 'GPSAltitude' => Exif::RATIONAL, # Altitude + 'GPSTimeStamp' => Exif::RATIONAL, # GPS time (atomic clock) + 'GPSSatellites' => Exif::ASCII, # Satellites used for measurement + 'GPSStatus' => Exif::ASCII, # Receiver status #p54 + 'GPSMeasureMode' => Exif::ASCII, # Measurement mode #p54-55 + 'GPSDOP' => Exif::RATIONAL, # Measurement precision + 'GPSSpeedRef' => Exif::ASCII, # Speed unit #p55 + 'GPSSpeed' => Exif::RATIONAL, # Speed of GPS receiver + 'GPSTrackRef' => Exif::ASCII, # Reference for direction of movement #p55 + 'GPSTrack' => Exif::RATIONAL, # Direction of movement + 'GPSImgDirectionRef' => Exif::ASCII, # Reference for direction of image #p56 + 'GPSImgDirection' => Exif::RATIONAL, # Direction of image + 'GPSMapDatum' => Exif::ASCII, # Geodetic survey data used + 'GPSDestLatitudeRef' => Exif::ASCII, # Reference for latitude of destination #p56 + 'GPSDestLatitude' => Exif::RATIONAL, # Latitude destination + 'GPSDestLongitudeRef' => Exif::ASCII, # Reference for longitude of destination #p57 + 'GPSDestLongitude' => Exif::RATIONAL, # Longitude of destination + 'GPSDestBearingRef' => Exif::ASCII, # Reference for bearing of destination #p57 + 'GPSDestBearing' => Exif::RATIONAL, # Bearing of destination + 'GPSDestDistanceRef' => Exif::ASCII, # Reference for distance to destination #p57-58 + 'GPSDestDistance' => Exif::RATIONAL, # Distance to destination + 'GPSProcessingMethod' => Exif::UNDEFINED, # Name of GPS processing method + 'GPSAreaInformation' => Exif::UNDEFINED, # Name of GPS area + 'GPSDateStamp' => Exif::ASCII, # GPS date + 'GPSDifferential' => Exif::SHORT, # GPS differential correction ), ); + $this->file = $file; + $this->basename = wfBaseName( $this->file ); + $this->makeFlatExifTags(); + + $this->debugFile( $this->basename, __FUNCTION__, true ); wfSuppressWarnings(); - $this->mRawExifData = exif_read_data( $file ); + $data = exif_read_data( $this->file ); wfRestoreWarnings(); + /** + * exif_read_data() will return false on invalid input, such as + * when somebody uploads a file called something.jpeg + * containing random gibberish. + */ + $this->mRawExifData = $data ? $data : array(); + $this->makeFilteredData(); $this->makeFormattedData(); + + $this->debugFile( __FUNCTION__, false ); } - + /**#@+ - * @access private + * @private */ /** * Generate a flat list of the exif tags @@ -290,7 +321,7 @@ function makeFlatExifTags() { $this->extractTags( $this->mExifTags ); } - + /** * A recursing extractor function used by makeFlatExifTags() * @@ -306,13 +337,13 @@ } } } - + /** * Make $this->mFilteredExifData */ function makeFilteredData() { $this->mFilteredExifData = $this->mRawExifData; - + foreach( $this->mFilteredExifData as $k => $v ) { if ( !in_array( $k, array_keys( $this->mFlatExifTags ) ) ) { $this->debug( $v, __FUNCTION__, "'$k' is not a valid Exif tag" ); @@ -328,7 +359,10 @@ } } - function makeFormattedData( $data = null ) { + /** + * @todo document + */ + function makeFormattedData( ) { $format = new FormatExif( $this->getFilteredData() ); $this->mFormattedExifData = $format->getFormattedData(); } @@ -358,7 +392,7 @@ return $this->mFormattedExifData; } /**#@-*/ - + /** * The version of the output format * @@ -371,20 +405,20 @@ * * @return int */ - function version() { + public static function version() { return 1; // We don't need no bloddy constants! } /**#@+ * Validates if a tag value is of the type it should be according to the Exif spec * - * @access private + * @private * - * @param mixed $in The input value to check + * @param $in Mixed: the input value to check * @return bool */ function isByte( $in ) { - if ( sprintf('%d', $in) == $in && $in >= 0 && $in <= 255 ) { + if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 255 ) { $this->debug( $in, __FUNCTION__, true ); return true; } else { @@ -392,23 +426,27 @@ return false; } } - + function isASCII( $in ) { + if ( is_array( $in ) ) { + return false; + } + if ( preg_match( "/[^\x0a\x20-\x7e]/", $in ) ) { $this->debug( $in, __FUNCTION__, 'found a character not in our whitelist' ); return false; } - - if ( preg_match( "/^\s*$/", $in ) ) { + + if ( preg_match( '/^\s*$/', $in ) ) { $this->debug( $in, __FUNCTION__, 'input consisted solely of whitespace' ); return false; } - + return true; } function isShort( $in ) { - if ( sprintf('%d', $in) == $in && $in >= 0 && $in <= 65536 ) { + if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 65536 ) { $this->debug( $in, __FUNCTION__, true ); return true; } else { @@ -418,7 +456,7 @@ } function isLong( $in ) { - if ( sprintf('%d', $in) == $in && $in >= 0 && $in <= 4294967296 ) { + if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 4294967296 ) { $this->debug( $in, __FUNCTION__, true ); return true; } else { @@ -426,9 +464,10 @@ return false; } } - + function isRational( $in ) { - if ( @preg_match( "/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/", $in, $m ) ) { # Avoid division by zero + $m = array(); + if ( !is_array( $in ) && @preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero return $this->isLong( $m[1] ) && $this->isLong( $m[2] ); } else { $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' ); @@ -437,7 +476,7 @@ } function isUndefined( $in ) { - if ( preg_match( "/^\d{4}$/", $in ) ) { // Allow ExifVersion and FlashpixVersion + if ( !is_array( $in ) && preg_match( '/^\d{4}$/', $in ) ) { // Allow ExifVersion and FlashpixVersion $this->debug( $in, __FUNCTION__, true ); return true; } else { @@ -457,7 +496,8 @@ } function isSrational( $in ) { - if ( preg_match( "/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/", $in, $m ) ) { # Avoid division by zero + $m = array(); + if ( !is_array( $in ) && preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero return $this->isSlong( $m[0] ) && $this->isSlong( $m[1] ); } else { $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' ); @@ -469,41 +509,41 @@ /** * Validates if a tag has a legal value according to the Exif spec * - * @access private + * @private * - * @param string $tag The tag to check - * @param mixed $val The value of the tag + * @param $tag String: the tag to check. + * @param $val Mixed: the value of the tag. * @return bool */ function validate( $tag, $val ) { $debug = "tag is '$tag'"; - // Fucks up if not typecast + // Does not work if not typecast switch( (string)$this->mFlatExifTags[$tag] ) { - case (string)MW_EXIF_BYTE: + case (string)Exif::BYTE: $this->debug( $val, __FUNCTION__, $debug ); return $this->isByte( $val ); - case (string)MW_EXIF_ASCII: + case (string)Exif::ASCII: $this->debug( $val, __FUNCTION__, $debug ); return $this->isASCII( $val ); - case (string)MW_EXIF_SHORT: + case (string)Exif::SHORT: $this->debug( $val, __FUNCTION__, $debug ); return $this->isShort( $val ); - case (string)MW_EXIF_LONG: + case (string)Exif::LONG: $this->debug( $val, __FUNCTION__, $debug ); return $this->isLong( $val ); - case (string)MW_EXIF_RATIONAL: + case (string)Exif::RATIONAL: $this->debug( $val, __FUNCTION__, $debug ); return $this->isRational( $val ); - case (string)MW_EXIF_UNDEFINED: + case (string)Exif::UNDEFINED: $this->debug( $val, __FUNCTION__, $debug ); return $this->isUndefined( $val ); - case (string)MW_EXIF_SLONG: + case (string)Exif::SLONG: $this->debug( $val, __FUNCTION__, $debug ); return $this->isSlong( $val ); - case (string)MW_EXIF_SRATIONAL: + case (string)Exif::SRATIONAL: $this->debug( $val, __FUNCTION__, $debug ); return $this->isSrational( $val ); - case (string)MW_EXIF_SHORT.','.MW_EXIF_LONG: + case (string)Exif::SHORT.','.Exif::LONG: $this->debug( $val, __FUNCTION__, $debug ); return $this->isShort( $val ) || $this->isLong( $val ); default: @@ -513,55 +553,78 @@ } /** - * Conviniance function for debugging output + * Convenience function for debugging output * - * @access private + * @private * - * @param mixed $in - * @param string $fname - * @param mixed $action - */ - function debug( $in, $fname, $action = null ) { + * @param $in Mixed: + * @param $fname String: + * @param $action Mixed: , default NULL. + */ + function debug( $in, $fname, $action = NULL ) { + if ( !$this->log ) { + return; + } $type = gettype( $in ); $class = ucfirst( __CLASS__ ); if ( $type === 'array' ) - $in = print_r( $in, true ); - + $in = print_r( $in, true ); + if ( $action === true ) - wfDebug( "$class::$fname: accepted: '$in' (type: $type)\n"); - elseif ( $action === false ) - wfDebug( "$class::$fname: rejected: '$in' (type: $type)\n"); + wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)\n"); + elseif ( $action === false ) + wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)\n"); elseif ( $action === null ) - wfDebug( "$class::$fname: input was: '$in' (type: $type)\n"); + wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)\n"); else - wfDebug( "$class::$fname: $action (type: $type; content: '$in')\n"); + wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')\n"); + } + + /** + * Convenience function for debugging output + * + * @private + * + * @param $fname String: the name of the function calling this function + * @param $io Boolean: Specify whether we're beginning or ending + */ + function debugFile( $fname, $io ) { + if ( !$this->log ) { + return; + } + $class = ucfirst( __CLASS__ ); + if ( $io ) { + wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'\n" ); + } else { + wfDebugLog( $this->log, "$class::$fname: end processing: '{$this->basename}'\n" ); + } } } /** - * @package MediaWiki - * @subpackage Metadata + * @todo document (e.g. one-sentence class-overview description) + * @addtogroup Media */ class FormatExif { /** * The Exif data to format * * @var array - * @access private + * @private */ var $mExif; - + /** * Constructor * - * @param array $exif The Exif data to format ( as returned by + * @param $exif Array: the Exif data to format ( as returned by * Exif::getFilteredData() ) */ function FormatExif( $exif ) { $this->mExif = $exif; } - + /** * Numbers given by Exif user agents are often magical, that is they * should be replaced by a detailed explanation depending on their @@ -572,12 +635,12 @@ */ function getFormattedData() { global $wgLang; - + $tags =& $this->mExif; $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3; unset( $tags['ResolutionUnit'] ); - + foreach( $tags as $tag => $val ) { switch( $tag ) { case 'Compression': @@ -590,7 +653,7 @@ break; } break; - + case 'PhotometricInterpretation': switch( $val ) { case 2: case 6: @@ -601,7 +664,7 @@ break; } break; - + case 'Orientation': switch( $val ) { case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: @@ -612,7 +675,7 @@ break; } break; - + case 'PlanarConfiguration': switch( $val ) { case 1: case 2: @@ -623,10 +686,10 @@ break; } break; - + // TODO: YCbCrSubSampling // TODO: YCbCrPositioning - + case 'XResolution': case 'YResolution': switch( $resolutionunit ) { @@ -641,12 +704,12 @@ break; } break; - + // TODO: YCbCrCoefficients #p27 (see annex E) case 'ExifVersion': case 'FlashpixVersion': $tags[$tag] = "$val"/100; break; - + case 'ColorSpace': switch( $val ) { case 1: case 'FFFF.H': @@ -657,7 +720,7 @@ break; } break; - + case 'ComponentsConfiguration': switch( $val ) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: @@ -668,13 +731,17 @@ break; } break; - + case 'DateTime': case 'DateTimeOriginal': case 'DateTimeDigitized': - $tags[$tag] = $wgLang->timeanddate( wfTimestamp(TS_MW, $val) ); + if( $val == '0000:00:00 00:00:00' ) { + $tags[$tag] = wfMsg('exif-unknowndate'); + } elseif( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/', $val ) ) { + $tags[$tag] = $wgLang->timeanddate( wfTimestamp(TS_MW, $val) ); + } break; - + case 'ExposureProgram': switch( $val ) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: @@ -689,7 +756,7 @@ case 'SubjectDistance': $tags[$tag] = $this->msg( $tag, '', $this->formatNum( $val ) ); break; - + case 'MeteringMode': switch( $val ) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 255: @@ -700,7 +767,7 @@ break; } break; - + case 'LightSource': switch( $val ) { case 0: case 1: case 2: case 3: case 4: case 9: case 10: case 11: @@ -713,7 +780,7 @@ break; } break; - + // TODO: Flash case 'FocalPlaneResolutionUnit': switch( $val ) { @@ -725,7 +792,7 @@ break; } break; - + case 'SensingMethod': switch( $val ) { case 1: case 2: case 3: case 4: case 5: case 7: case 8: @@ -736,7 +803,7 @@ break; } break; - + case 'FileSource': switch( $val ) { case 3: @@ -747,7 +814,7 @@ break; } break; - + case 'SceneType': switch( $val ) { case 1: @@ -758,7 +825,7 @@ break; } break; - + case 'CustomRendered': switch( $val ) { case 0: case 1: @@ -769,7 +836,7 @@ break; } break; - + case 'ExposureMode': switch( $val ) { case 0: case 1: case 2: @@ -780,7 +847,7 @@ break; } break; - + case 'WhiteBalance': switch( $val ) { case 0: case 1: @@ -791,7 +858,7 @@ break; } break; - + case 'SceneCaptureType': switch( $val ) { case 0: case 1: case 2: case 3: @@ -802,7 +869,7 @@ break; } break; - + case 'GainControl': switch( $val ) { case 0: case 1: case 2: case 3: case 4: @@ -813,7 +880,7 @@ break; } break; - + case 'Contrast': switch( $val ) { case 0: case 1: case 2: @@ -824,7 +891,7 @@ break; } break; - + case 'Saturation': switch( $val ) { case 0: case 1: case 2: @@ -835,7 +902,7 @@ break; } break; - + case 'Sharpness': switch( $val ) { case 0: case 1: case 2: @@ -846,7 +913,7 @@ break; } break; - + case 'SubjectDistanceRange': switch( $val ) { case 0: case 1: case 2: case 3: @@ -857,7 +924,7 @@ break; } break; - + case 'GPSLatitudeRef': case 'GPSDestLatitudeRef': switch( $val ) { @@ -869,7 +936,7 @@ break; } break; - + case 'GPSLongitudeRef': case 'GPSDestLongitudeRef': switch( $val ) { @@ -881,7 +948,7 @@ break; } break; - + case 'GPSStatus': switch( $val ) { case 'A': case 'V': @@ -892,7 +959,7 @@ break; } break; - + case 'GPSMeasureMode': switch( $val ) { case 2: case 3: @@ -903,7 +970,7 @@ break; } break; - + case 'GPSSpeedRef': case 'GPSDestDistanceRef': switch( $val ) { @@ -915,7 +982,7 @@ break; } break; - + case 'GPSTrackRef': case 'GPSImgDirectionRef': case 'GPSDestBearingRef': @@ -928,11 +995,11 @@ break; } break; - + case 'GPSDateStamp': $tags[$tag] = $wgLang->date( substr( $val, 0, 4 ) . substr( $val, 5, 2 ) . substr( $val, 8, 2 ) . '000000' ); break; - + // This is not in the Exif standard, just a special // case for our purposes which enables wikis to wikify // the make, model and software name to link to their articles. @@ -941,6 +1008,23 @@ case 'Software': $tags[$tag] = $this->msg( $tag, '', $val ); break; + + case 'ExposureTime': + // Show the pretty fraction as well as decimal version + $tags[$tag] = wfMsg( 'exif-exposuretime-format', + $this->formatFraction( $val ), $this->formatNum( $val ) ); + break; + + case 'FNumber': + $tags[$tag] = wfMsg( 'exif-fnumber-format', + $this->formatNum( $val ) ); + break; + + case 'FocalLength': + $tags[$tag] = wfMsg( 'exif-focallength-format', + $this->formatNum( $val ) ); + break; + default: $tags[$tag] = $this->formatNum( $val ); break; @@ -951,34 +1035,100 @@ } /** - * Conviniance function for getFormattedData() + * Convenience function for getFormattedData() * - * @access private + * @private * - * @param string $tag The tag name to pass on - * @param string $val The value of the tag - * @param string $arg An argument to pass ($1) + * @param $tag String: the tag name to pass on + * @param $val String: the value of the tag + * @param $arg String: an argument to pass ($1) * @return string A wfMsg of "exif-$tag-$val" in lower case */ function msg( $tag, $val, $arg = null ) { + global $wgContLang; + if ($val === '') $val = 'value'; - return wfMsg( strtolower( "exif-$tag-$val" ), $arg ); + return wfMsg( $wgContLang->lc( "exif-$tag-$val" ), $arg ); } /** * Format a number, convert numbers from fractions into floating point * numbers * - * @access private + * @private * - * @param mixed $num The value to format + * @param $num Mixed: the value to format * @return mixed A floating point number or whatever we were fed */ function formatNum( $num ) { + $m = array(); if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) ) return $m[2] != 0 ? $m[1] / $m[2] : $num; else return $num; } + + /** + * Format a rational number, reducing fractions + * + * @private + * + * @param $num Mixed: the value to format + * @return mixed A floating point number or whatever we were fed + */ + function formatFraction( $num ) { + $m = array(); + if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) ) { + $numerator = intval( $m[1] ); + $denominator = intval( $m[2] ); + $gcd = $this->gcd( $numerator, $denominator ); + if( $gcd != 0 ) { + // 0 shouldn't happen! ;) + return $numerator / $gcd . '/' . $denominator / $gcd; + } + } + return $this->formatNum( $num ); + } + + /** + * Calculate the greatest common divisor of two integers. + * + * @param $a Integer: FIXME + * @param $b Integer: FIXME + * @return int + * @private + */ + function gcd( $a, $b ) { + /* + // http://en.wikipedia.org/wiki/Euclidean_algorithm + // Recursive form would be: + if( $b == 0 ) + return $a; + else + return gcd( $b, $a % $b ); + */ + while( $b != 0 ) { + $remainder = $a % $b; + + // tail recursion... + $a = $b; + $b = $remainder; + } + return $a; + } } + +/** + * MW 1.6 compatibility + */ +define( 'MW_EXIF_BYTE', Exif::BYTE ); +define( 'MW_EXIF_ASCII', Exif::ASCII ); +define( 'MW_EXIF_SHORT', Exif::SHORT ); +define( 'MW_EXIF_LONG', Exif::LONG ); +define( 'MW_EXIF_RATIONAL', Exif::RATIONAL ); +define( 'MW_EXIF_UNDEFINED', Exif::UNDEFINED ); +define( 'MW_EXIF_SLONG', Exif::SLONG ); +define( 'MW_EXIF_SRATIONAL', Exif::SRATIONAL ); + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ExternalEdit.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ExternalEdit.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ExternalEdit.php 2005-06-28 01:02:09.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ExternalEdit.php 2007-06-28 21:19:14.000000000 -0400 @@ -3,12 +3,10 @@ * License: Public domain * * @author Erik Moeller <moeller@scireview.de> - * @package MediaWiki */ /** - * - * @package MediaWiki + * * * Support for external editors to modify both text and files * in external applications. It works as follows: MediaWiki @@ -19,37 +17,36 @@ * and save the modified data back to the server. * */ - + class ExternalEdit { - function ExternalEdit ( $article, $mode ) { + function __construct( $article, $mode ) { global $wgInputEncoding; $this->mArticle =& $article; $this->mTitle =& $article->mTitle; $this->mCharset = $wgInputEncoding; $this->mMode = $mode; } - + function edit() { - global $wgUser, $wgOut, $wgScript, $wgScriptPath, $wgServer, - $wgLang; + global $wgOut, $wgScript, $wgScriptPath, $wgServer, $wgLang; $wgOut->disable(); $name=$this->mTitle->getText(); $pos=strrpos($name,".")+1; header ( "Content-type: application/x-external-editor; charset=".$this->mCharset ); - + # $type can be "Edit text", "Edit file" or "Diff text" at the moment # See the protocol specifications at [[m:Help:External editors/Tech]] for # details. - if(!isset($this->mMode)) { - $type="Edit text"; + if(!isset($this->mMode)) { + $type="Edit text"; $url=$this->mTitle->getFullURL("action=edit&internaledit=true"); - # *.wiki file extension is used by some editors for syntax + # *.wiki file extension is used by some editors for syntax # highlighting, so we follow that convention - $extension="wiki"; + $extension="wiki"; } elseif($this->mMode=="file") { - $type="Edit file"; - $image = Image::newFromTitle( $this->mTitle ); + $type="Edit file"; + $image = wfLocalFile( $this->mTitle ); $img_url = $image->getURL(); if(strpos($img_url,"://")) { $url = $img_url; @@ -58,7 +55,7 @@ } $extension=substr($name, $pos); } - $special=$wgLang->getNsText(NS_SPECIAL); + $special=$wgLang->getNsText(NS_SPECIAL); $control = <<<CONTROL [Process] Type=$type @@ -75,4 +72,4 @@ echo $control; } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ExternalStore.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ExternalStore.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ExternalStore.php 2005-04-18 13:21:28.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ExternalStore.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,44 +1,68 @@ <?php /** - * - * @package MediaWiki + * * * Constructor class for data kept in external repositories * - * External repositories might be populated by maintenance/async - * scripts, thus partial moving of data may be possible, as well + * External repositories might be populated by maintenance/async + * scripts, thus partial moving of data may be possible, as well * as possibility to have any storage format (i.e. for archives) * */ - + class ExternalStore { /* Fetch data from given URL */ function fetchFromURL($url) { global $wgExternalStores; - if (!$wgExternalStores) + if (!$wgExternalStores) return false; @list($proto,$path)=explode('://',$url,2); /* Bad URL */ - if ($path=="") + if ($path=="") + return false; + + $store =& ExternalStore::getStoreObject( $proto ); + if ( $store === false ) + return false; + return $store->fetchFromURL($url); + } + + /** + * Get an external store object of the given type + */ + function &getStoreObject( $proto ) { + global $wgExternalStores; + if (!$wgExternalStores) return false; /* Protocol not enabled */ if (!in_array( $proto, $wgExternalStores )) return false; $class='ExternalStore'.ucfirst($proto); - /* Preloaded modules might exist, especially ones serving multiple protocols */ + /* Any custom modules should be added to $wgAutoLoadClasses for on-demand loading */ if (!class_exists($class)) { - if (!include_once($class.'.php')) - return false; + return false; } $store=new $class(); - return $store->fetchFromURL($url); + return $store; } - /* XXX: may require other methods, for store, delete, - * whatever, for initial ext storage + /** + * Store a data item to an external store, identified by a partial URL + * The protocol part is used to identify the class, the rest is passed to the + * class itself as a parameter. + * Returns the URL of the stored data item, or false on error */ + function insert( $url, $data ) { + list( $proto, $params ) = explode( '://', $url, 2 ); + $store =& ExternalStore::getStoreObject( $proto ); + if ( $store === false ) { + return false; + } else { + return $store->store( $params, $data ); + } + } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ExternalStoreDB.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ExternalStoreDB.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ExternalStoreDB.php 2005-08-15 09:22:01.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ExternalStoreDB.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,47 +1,71 @@ <?php /** - * - * @package MediaWiki + * * * DB accessable external objects * */ -require_once( 'LoadBalancer.php' ); -/** @package MediaWiki */ + +/** + * External database storage will use one (or more) separate connection pools + * from what the main wiki uses. If we load many revisions, such as when doing + * bulk backups or maintenance, we want to keep them around over the lifetime + * of the script. + * + * Associative array of LoadBalancer objects, indexed by cluster name. + */ +global $wgExternalLoadBalancers; +$wgExternalLoadBalancers = array(); + +/** + * One-step cache variable to hold base blobs; operations that + * pull multiple revisions may often pull multiple times from + * the same blob. By keeping the last-used one open, we avoid + * redundant unserialization and decompression overhead. + */ +global $wgExternalBlobCache; +$wgExternalBlobCache = array(); class ExternalStoreDB { - var $loadBalancers = array(); - - /** - * Fetch data from given URL - * @param string $url An url - */ + /** @todo Document.*/ function &getLoadBalancer( $cluster ) { - global $wgExternalServers; - if ( !array_key_exists( $cluster, $this->loadBalancers ) ) { - $this->loadBalancers[$cluster] = LoadBalancer::newFromParams( $wgExternalServers[$cluster] ); + global $wgExternalServers, $wgExternalLoadBalancers; + if ( !array_key_exists( $cluster, $wgExternalLoadBalancers ) ) { + $wgExternalLoadBalancers[$cluster] = LoadBalancer::newFromParams( $wgExternalServers[$cluster] ); } - return $this->loadBalancers[$cluster]; + $wgExternalLoadBalancers[$cluster]->allowLagged(true); + return $wgExternalLoadBalancers[$cluster]; } - + + /** @todo Document.*/ function &getSlave( $cluster ) { $lb =& $this->getLoadBalancer( $cluster ); return $lb->getConnection( DB_SLAVE ); } + /** @todo Document.*/ function &getMaster( $cluster ) { $lb =& $this->getLoadBalancer( $cluster ); return $lb->getConnection( DB_MASTER ); - } - + } + + /** @todo Document.*/ + function getTable( &$db ) { + $table = $db->getLBInfo( 'blobs table' ); + if ( is_null( $table ) ) { + $table = 'blobs'; + } + return $table; + } + + /** + * Fetch data from given URL + * @param string $url An url of the form DB://cluster/id or DB://cluster/id/itemid for concatened storage. + */ function fetchFromURL($url) { - global $wgExternalServers; - # - # URLs have the form DB://cluster/id or DB://cluster/id/itemid for concatenated storage - # $path = explode( '/', $url ); $cluster = $path[2]; $id = $path[3]; @@ -51,33 +75,73 @@ $itemID = false; } - $dbr =& $this->getSlave( $cluster ); - $ret = $dbr->selectField( 'blobs', 'blob_text', array( 'blob_id' => $id ) ); + $ret =& $this->fetchBlob( $cluster, $id, $itemID ); - if ( $itemID !== false ) { - # Unserialise object and get item - $obj = unserialize( $ret ); - $ret = $obj->getItem( $itemID ); + if ( $itemID !== false && $ret !== false ) { + return $ret->getItem( $itemID ); } return $ret; } /** + * Fetch a blob item out of the database; a cache of the last-loaded + * blob will be kept so that multiple loads out of a multi-item blob + * can avoid redundant database access and decompression. + * @param $cluster + * @param $id + * @param $itemID + * @return mixed + * @private + */ + function &fetchBlob( $cluster, $id, $itemID ) { + global $wgExternalBlobCache; + $cacheID = ( $itemID === false ) ? "$cluster/$id" : "$cluster/$id/"; + if( isset( $wgExternalBlobCache[$cacheID] ) ) { + wfDebug( "ExternalStoreDB::fetchBlob cache hit on $cacheID\n" ); + return $wgExternalBlobCache[$cacheID]; + } + + wfDebug( "ExternalStoreDB::fetchBlob cache miss on $cacheID\n" ); + + $dbr =& $this->getSlave( $cluster ); + $ret = $dbr->selectField( $this->getTable( $dbr ), 'blob_text', array( 'blob_id' => $id ) ); + if ( $ret === false ) { + wfDebugLog( 'ExternalStoreDB', "ExternalStoreDB::fetchBlob master fallback on $cacheID\n" ); + // Try the master + $dbw =& $this->getMaster( $cluster ); + $ret = $dbw->selectField( $this->getTable( $dbw ), 'blob_text', array( 'blob_id' => $id ) ); + if( $ret === false) { + wfDebugLog( 'ExternalStoreDB', "ExternalStoreDB::fetchBlob master failed to find $cacheID\n" ); + } + } + if( $itemID !== false && $ret !== false ) { + // Unserialise object; caller extracts item + $ret = unserialize( $ret ); + } + + $wgExternalBlobCache = array( $cacheID => &$ret ); + return $ret; + } + + /** * Insert a data item into a given cluster * - * @param string $cluster The cluster name - * @param string $data The data item + * @param $cluster String: the cluster name + * @param $data String: the data item * @return string URL */ function store( $cluster, $data ) { - global $wgExternalServers; $fname = 'ExternalStoreDB::store'; $dbw =& $this->getMaster( $cluster ); $id = $dbw->nextSequenceValue( 'blob_blob_id_seq' ); - $dbw->insert( 'blobs', array( 'blob_id' => $id, 'blob_text' => $data ), $fname ); - return "DB://$cluster/" . $dbw->insertId(); + $dbw->insert( $this->getTable( $dbw ), array( 'blob_id' => $id, 'blob_text' => $data ), $fname ); + $id = $dbw->insertId(); + if ( $dbw->getFlag( DBO_TRX ) ) { + $dbw->immediateCommit(); + } + return "DB://$cluster/$id"; } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ExternalStoreHttp.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ExternalStoreHttp.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ExternalStoreHttp.php 2005-04-18 13:21:28.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ExternalStoreHttp.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,23 +1,22 @@ <?php /** - * - * @package MediaWiki + * * * Example class for HTTP accessable external objects * */ - + class ExternalStoreHttp { /* Fetch data from given URL */ function fetchFromURL($url) { - ini_set( "allow_url_fopen", true ); - $ret = file_get_contents( $url ); - ini_set( "allow_url_fopen", false ); + ini_set( "allow_url_fopen", true ); + $ret = file_get_contents( $url ); + ini_set( "allow_url_fopen", false ); return $ret; } - /* XXX: may require other methods, for store, delete, - * whatever, for initial ext storage + /* XXX: may require other methods, for store, delete, + * whatever, for initial ext storage */ } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Feed.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Feed.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Feed.php 2005-11-16 03:50:52.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Feed.php 2007-04-24 02:53:31.000000000 -0400 @@ -1,39 +1,36 @@ <?php -# Basic support for outputting syndication feeds in RSS, other formats -# + # Copyright (C) 2004 Brion Vibber <brion@pobox.com> # http://www.mediawiki.org/ -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** + * Basic support for outputting syndication feeds in RSS, other formats. * Contain a feed class as well as classes to build rss / atom ... feeds * Available feeds are defined in Defines.php - * @package MediaWiki */ - /** - * @todo document - * @package MediaWiki + * A base class for basic support for outputting syndication feeds in RSS and other formats. */ class FeedItem { /**#@+ * @var string - * @access private + * @private */ var $Title = 'Wiki'; var $Description = ''; @@ -41,11 +38,11 @@ var $Date = ''; var $Author = ''; /**#@-*/ - + /**#@+ * @todo document */ - function FeedItem( $Title, $Description, $Url, $Date = '', $Author = '', $Comments = '' ) { + function __construct( $Title, $Description, $Url, $Date = '', $Author = '', $Comments = '' ) { $this->Title = $Title; $this->Description = $Description; $this->Url = $Url; @@ -53,7 +50,7 @@ $this->Author = $Author; $this->Comments = $Comments; } - + /** * @static */ @@ -62,7 +59,7 @@ $string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $string ); return htmlspecialchars( $string ); } - + function getTitle() { return $this->xmlEncode( $this->Title ); } function getUrl() { return $this->xmlEncode( $this->Url ); } function getDescription() { return $this->xmlEncode( $this->Description ); } @@ -77,22 +74,21 @@ } /** - * @todo document - * @package MediaWiki + * @todo document (needs one-sentence top-level class description). */ class ChannelFeed extends FeedItem { /**#@+ * Abstract function, override! * @abstract */ - + /** * Generate Header of the feed */ function outHeader() { # print "<feed>"; } - + /** * Generate an item * @param $item @@ -100,7 +96,7 @@ function outItem( $item ) { # print "<item>...</item>"; } - + /** * Generate Footer of the feed */ @@ -108,7 +104,7 @@ # print "</feed>"; } /**#@-*/ - + /** * Setup and send HTTP headers. Don't send any content; * content might end up being cached and re-sent with @@ -117,24 +113,24 @@ * This should be called from the outHeader() method, * but can also be called separately. * - * @access public + * @public */ function httpHeaders() { global $wgOut; - + # We take over from $wgOut, excepting its cache header info $wgOut->disable(); $mimetype = $this->contentType(); header( "Content-type: $mimetype; charset=UTF-8" ); $wgOut->sendCacheControl(); - + } - + /** * Return an internet media type to be sent in the headers. * * @return string - * @access private + * @private */ function contentType() { global $wgRequest; @@ -142,26 +138,24 @@ $allowedctypes = array('application/xml','text/xml','application/rss+xml','application/atom+xml'); return (in_array($ctype, $allowedctypes) ? $ctype : 'application/xml'); } - + /** * Output the initial XML headers with a stylesheet for legibility * if someone finds it in a browser. - * @access private + * @private */ function outXmlHeader() { - global $wgServer, $wgStylePath; - + global $wgServer, $wgStylePath, $wgStyleVersion; + $this->httpHeaders(); - echo '<?xml version="1.0" encoding="utf-8"?' . ">\n"; + echo '<?xml version="1.0" encoding="utf-8"?>' . "\n"; echo '<?xml-stylesheet type="text/css" href="' . - htmlspecialchars( "$wgServer$wgStylePath/common/feed.css" ) . '"?' . ">\n"; + htmlspecialchars( "$wgServer$wgStylePath/common/feed.css?$wgStyleVersion" ) . '"?' . ">\n"; } } /** * Generate a RSS feed - * @todo document - * @package MediaWiki */ class RSSFeed extends ChannelFeed { @@ -173,13 +167,13 @@ function formatTime( $ts ) { return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) ); } - + /** * Ouput an RSS 2.0 header */ function outHeader() { global $wgVersion; - + $this->outXmlHeader(); ?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"> <channel> @@ -191,7 +185,7 @@ <lastBuildDate><?php print $this->formatTime( wfTimestampNow() ) ?></lastBuildDate> <?php } - + /** * Output an RSS 2.0 item * @param FeedItem item to be output @@ -221,8 +215,6 @@ /** * Generate an Atom feed - * @todo document - * @package MediaWiki */ class AtomFeed extends ChannelFeed { /** @@ -234,48 +226,73 @@ } /** - * @todo document + * Outputs a basic header for Atom 1.0 feeds. */ function outHeader() { - global $wgVersion, $wgOut; - + global $wgVersion; + $this->outXmlHeader(); - ?><feed version="0.3" xmlns="http://purl.org/atom/ns#" xml:lang="<?php print $this->getLanguage() ?>"> + ?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="<?php print $this->getLanguage() ?>"> + <id><?php print $this->getFeedId() ?></id> <title><?php print $this->getTitle() ?> + - formatTime( wfTimestampNow() ) ?>Z - getDescription() ?> + formatTime( wfTimestampNow() ) ?>Z + getDescription() ?> MediaWiki - + getSelfUrl(); + } + + /** + * Atom 1.0 requests a self-reference to the feed. + * @return string + * @private + */ + function getSelfUrl() { + global $wgRequest; + return htmlspecialchars( $wgRequest->getFullRequestURL() ); + } + + /** + * Output a given item. + * @param $item */ function outItem( $item ) { global $wgMimeType; ?> + getUrl() ?> <?php print $item->getTitle() ?> getDate() ) { ?> - formatTime( $item->getDate() ) ?>Z - formatTime( $item->getDate() ) ?> - formatTime( $item->getDate() ) ?>Z - - getDescription() ?> - getAuthor() ) { ?>getAuthor() ?> - foobar + formatTime( $item->getDate() ) ?>Z + + + getDescription() ?> + getAuthor() ) { ?>getAuthor() ?> getComments() ) { ?>getComments() ?> */ } - + /** - * @todo document + * Outputs the footer for Atom 1.0 feed (basicly '\'). */ function outFooter() {?> = 3 ) { - $end = func_get_arg( 2 ); - return join( '', array_slice( $ar[0], $start, $end ) ); - } else { - return join( '', array_slice( $ar[0], $start ) ); + $end = func_get_arg( 2 ); + return join( '', array_slice( $ar[0], $start, $end ) ); + } else { + return join( '', array_slice( $ar[0], $start ) ); + } + } +} + +if ( !function_exists( 'mb_strlen' ) ) { + /** + * Fallback implementation of mb_strlen, hardcoded to UTF-8. + * @param string $str + * @param string $enc optional encoding; ignored + * @return int + */ + function mb_strlen( $str, $enc="" ) { + $counts = count_chars( $str ); + $total = 0; + + // Count ASCII bytes + for( $i = 0; $i < 0x80; $i++ ) { + $total += $counts[$i]; + } + + // Count multibyte sequence heads + for( $i = 0xc0; $i < 0xff; $i++ ) { + $total += $counts[$i]; } + return $total; } } -if( !function_exists( 'floatval' ) ) { +if ( !function_exists( 'array_diff_key' ) ) { /** - * First defined in PHP 4.2.0 - * @param mixed $var; - * @return float + * Exists in PHP 5.1.0+ + * Not quite compatible, two-argument version only + * Null values will cause problems due to this use of isset() */ - function floatval( $var ) { - return (float)$var; + function array_diff_key( $left, $right ) { + $result = $left; + foreach ( $left as $key => $unused ) { + if ( isset( $right[$key] ) ) { + unset( $result[$key] ); + } + } + return $result; } } + +/** + * Wrapper for clone(), for compatibility with PHP4-friendly extensions. + * PHP 5 won't let you declare a 'clone' function, even conditionally, + * so it has to be a wrapper with a different name. + */ +function wfClone( $object ) { + return clone( $object ); +} + /** * Where as we got a random seed - * @var bool $wgTotalViews */ $wgRandomSeeded = false; /** * Seed Mersenne Twister - * Only necessary in PHP < 4.2.0 - * - * @return bool + * No-op for compatibility; only necessary in PHP < 4.2.0 */ function wfSeedRandom() { - global $wgRandomSeeded; - - if ( ! $wgRandomSeeded && version_compare( phpversion(), '4.2.0' ) < 0 ) { - $seed = hexdec(substr(md5(microtime()),-8)) & 0x7fffffff; - mt_srand( $seed ); - $wgRandomSeeded = true; - } + /* No-op */ } /** @@ -122,7 +134,7 @@ function wfRandom() { # The maximum random value is "only" 2^31-1, so get two random # values to reduce the chance of dupes - $max = mt_getrandmax(); + $max = mt_getrandmax() + 1; $rand = number_format( (mt_rand() * $max + mt_rand()) / $max / $max, 12, '.', '' ); return $rand; @@ -132,7 +144,7 @@ * We want / and : to be included as literal characters in our title URLs. * %2F in the page titles seems to fatally break for some reason. * - * @param string $s + * @param $s String: * @return string */ function wfUrlencode ( $s ) { @@ -153,69 +165,97 @@ * $wgDebugRawPage - if false, 'action=raw' hits will not result in debug output. * $wgDebugComments - if on, some debug items may appear in comments in the HTML output. * - * @param string $text - * @param bool $logonly Set true to avoid appearing in HTML when $wgDebugComments is set + * @param $text String + * @param $logonly Bool: set true to avoid appearing in HTML when $wgDebugComments is set */ function wfDebug( $text, $logonly = false ) { global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage; + static $recursion = 0; # Check for raw action using $_GET not $wgRequest, since the latter might not be initialised yet if ( isset( $_GET['action'] ) && $_GET['action'] == 'raw' && !$wgDebugRawPage ) { return; } - if ( isset( $wgOut ) && $wgDebugComments && !$logonly ) { + if ( $wgDebugComments && !$logonly ) { + if ( !isset( $wgOut ) ) { + return; + } + if ( !StubObject::isRealObject( $wgOut ) ) { + if ( $recursion ) { + return; + } + $recursion++; + $wgOut->_unstub(); + $recursion--; + } $wgOut->debug( $text ); } if ( '' != $wgDebugLogFile && !$wgProfileOnly ) { # Strip unprintables; they can switch terminal modes when binary data # gets dumped, which is pretty annoying. $text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $text ); - @error_log( $text, 3, $wgDebugLogFile ); + wfErrorLog( $text, $wgDebugLogFile ); } } /** * Send a line to a supplementary debug log file, if configured, or main debug log if not. * $wgDebugLogGroups[$logGroup] should be set to a filename to send to a separate log. - * @param string $logGroup - * @param string $text + * + * @param $logGroup String + * @param $text String + * @param $public Bool: whether to log the event in the public log if no private + * log file is specified, (default true) */ -function wfDebugLog( $logGroup, $text ) { - global $wgDebugLogGroups, $wgDBname; +function wfDebugLog( $logGroup, $text, $public = true ) { + global $wgDebugLogGroups; if( $text{strlen( $text ) - 1} != "\n" ) $text .= "\n"; if( isset( $wgDebugLogGroups[$logGroup] ) ) { - @error_log( "$wgDBname: $text", 3, $wgDebugLogGroups[$logGroup] ); - } else { + $time = wfTimestamp( TS_DB ); + $wiki = wfWikiID(); + wfErrorLog( "$time $wiki: $text", $wgDebugLogGroups[$logGroup] ); + } else if ( $public === true ) { wfDebug( $text, true ); } } /** * Log for database errors - * @param string $text Database error message. + * @param $text String: database error message. */ function wfLogDBError( $text ) { - global $wgDBerrorLog; + global $wgDBerrorLog, $wgDBname; if ( $wgDBerrorLog ) { - $text = date('D M j G:i:s T Y') . "\t".$text; - error_log( $text, 3, $wgDBerrorLog ); + $host = trim(`hostname`); + $text = date('D M j G:i:s T Y') . "\t$host\t$wgDBname\t$text"; + wfErrorLog( $text, $wgDBerrorLog ); + } +} + +/** + * Log to a file without getting "file size exceeded" signals + */ +function wfErrorLog( $text, $file ) { + wfSuppressWarnings(); + $exists = file_exists( $file ); + $size = $exists ? filesize( $file ) : false; + if ( !$exists || ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) ) { + error_log( $text, 3, $file ); } + wfRestoreWarnings(); } /** * @todo document */ -function logProfilingData() { +function wfLogProfilingData() { global $wgRequestTime, $wgDebugLogFile, $wgDebugRawPage, $wgRequest; - global $wgProfiling, $wgProfileStack, $wgProfileLimit, $wgUser; - $now = wfTime(); - - list( $usec, $sec ) = explode( ' ', $wgRequestTime ); - $start = (float)$sec + (float)$usec; - $elapsed = $now - $start; + global $wgProfiling, $wgUser; if ( $wgProfiling ) { - $prof = wfGetProfilingOutput( $start, $elapsed ); + $now = wfTime(); + $elapsed = $now - $wgRequestTime; + $prof = wfGetProfilingOutput( $wgRequestTime, $elapsed ); $forward = ''; if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) $forward = ' forwarded for ' . $_SERVER['HTTP_X_FORWARDED_FOR']; @@ -225,13 +265,14 @@ $forward .= ' from ' . $_SERVER['HTTP_FROM']; if( $forward ) $forward = "\t(proxied via {$_SERVER['REMOTE_ADDR']}{$forward})"; - if( $wgUser->isAnon() ) + // Don't unstub $wgUser at this late stage just for statistics purposes + if( StubObject::isRealObject($wgUser) && $wgUser->isAnon() ) $forward .= ' anon'; $log = sprintf( "%s\t%04.3f\t%s\n", gmdate( 'YmdHis' ), $elapsed, - urldecode( $_SERVER['REQUEST_URI'] . $forward ) ); + urldecode( $wgRequest->getRequestURL() . $forward ) ); if ( '' != $wgDebugLogFile && ( $wgRequest->getVal('action') != 'raw' || $wgDebugRawPage ) ) { - error_log( $log . $prof, 3, $wgDebugLogFile ); + wfErrorLog( $log . $prof, $wgDebugLogFile ); } } } @@ -251,21 +292,33 @@ if ( '' == $wgReadOnlyFile ) { return false; } - // Set $wgReadOnly for faster access next time if ( is_file( $wgReadOnlyFile ) ) { $wgReadOnly = file_get_contents( $wgReadOnlyFile ); } else { $wgReadOnly = false; } - return $wgReadOnly; + return (bool)$wgReadOnly; } /** - * Get a message from anywhere, for the current user language + * Get a message from anywhere, for the current user language. + * + * Use wfMsgForContent() instead if the message should NOT + * change depending on the user preferences. + * + * Note that the message may contain HTML, and is therefore + * not safe for insertion anywhere. Some functions such as + * addWikiText will do the escaping for you. Use wfMsgHtml() + * if you need an escaped message. * - * @param string + * @param $key String: lookup key for the message, usually + * defined in languages/Language.php + * + * This function also takes extra optional parameters (not + * shown in the function definition), which can by used to + * insert variable text into the predefined message. */ function wfMsg( $key ) { $args = func_get_args(); @@ -274,7 +327,35 @@ } /** + * Same as above except doesn't transform the message + */ +function wfMsgNoTrans( $key ) { + $args = func_get_args(); + array_shift( $args ); + return wfMsgReal( $key, $args, true, false, false ); +} + +/** * Get a message from anywhere, for the current global language + * set with $wgLanguageCode. + * + * Use this if the message should NOT change dependent on the + * language set in the user's preferences. This is the case for + * most text written into logs, as well as link targets (such as + * the name of the copyright policy page). Link titles, on the + * other hand, should be shown in the UI language. + * + * Note that MediaWiki allows users to change the user interface + * language in their preferences, but a single installation + * typically only contains content in one language. + * + * Be wary of this distinction: If you use wfMsg() where you should + * use wfMsgForContent(), a user of the software may have to + * customize over 70 messages in order to, e.g., fix a link in every + * possible language. + * + * @param $key String: lookup key for the message, usually + * defined in languages/Language.php */ function wfMsgForContent( $key ) { global $wgForceUIMsgAsContentMsg; @@ -288,6 +369,20 @@ } /** + * Same as above except doesn't transform the message + */ +function wfMsgForContentNoTrans( $key ) { + global $wgForceUIMsgAsContentMsg; + $args = func_get_args(); + array_shift( $args ); + $forcontent = true; + if( is_array( $wgForceUIMsgAsContentMsg ) && + in_array( $key, $wgForceUIMsgAsContentMsg ) ) + $forcontent = false; + return wfMsgReal( $key, $args, true, $forcontent, false ); +} + +/** * Get a message from the language file, for the UI elements */ function wfMsgNoDB( $key ) { @@ -313,31 +408,62 @@ /** * Really get a message + * @param $key String: key to get. + * @param $args + * @param $useDB Boolean + * @param $transform Boolean: Whether or not to transform the message. + * @param $forContent Boolean + * @return String: the requested message. */ -function wfMsgReal( $key, $args, $useDB, $forContent=false ) { +function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform = true ) { $fname = 'wfMsgReal'; wfProfileIn( $fname ); - - $message = wfMsgGetKey( $key, $useDB, $forContent ); + $message = wfMsgGetKey( $key, $useDB, $forContent, $transform ); $message = wfMsgReplaceArgs( $message, $args ); wfProfileOut( $fname ); return $message; } /** + * This function provides the message source for messages to be edited which are *not* stored in the database. + * @param $key String: + */ +function wfMsgWeirdKey ( $key ) { + $source = wfMsgGetKey( $key, false, true, false ); + if ( wfEmptyMsg( $key, $source ) ) + return ""; + else + return $source; +} + +/** * Fetch a message string value, but don't replace any keys yet. * @param string $key * @param bool $useDB * @param bool $forContent * @return string - * @access private + * @private */ -function wfMsgGetKey( $key, $useDB, $forContent = false ) { - global $wgParser, $wgMsgParserOptions; - global $wgContLang, $wgLanguageCode; - global $wgMessageCache, $wgLang; - +function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) { + global $wgParser, $wgContLang, $wgMessageCache, $wgLang; + + /* btw, is all that code in wfMsgGetKey() that check + * if the message cache exists of not really necessary, or is + * it just paranoia? + * Vyznev: it's probably not necessary + * I think I wrote it in an attempt to report DB + * connection errors properly + * but eventually we gave up on using the + * message cache for that and just hard-coded the strings + * it may have other uses, it's not mere paranoia + */ + + if ( is_object( $wgMessageCache ) ) + $transstat = $wgMessageCache->getTransform(); + if( is_object( $wgMessageCache ) ) { + if ( ! $transform ) + $wgMessageCache->disableTransform(); $message = $wgMessageCache->get( $key, $useDB, $forContent ); } else { if( $forContent ) { @@ -346,20 +472,26 @@ $lang = &$wgLang; } + # MessageCache::get() does this already, Language::getMessage() doesn't + # ISSUE: Should we try to handle "message/lang" here too? + $key = str_replace( ' ' , '_' , $wgContLang->lcfirst( $key ) ); + wfSuppressWarnings(); - if( is_object( $lang ) ) { $message = $lang->getMessage( $key ); } else { - $message = ''; + $message = false; } wfRestoreWarnings(); - if(!$message) - $message = Language::getMessage($key); - if(strstr($message, '{{' ) !== false) { - $message = $wgParser->transformMsg($message, $wgMsgParserOptions); + + if ( $transform && strstr( $message, '{{' ) !== false ) { + $message = $wgParser->transformMsg($message, $wgMessageCache->getParserOptions() ); } } + + if ( is_object( $wgMessageCache ) && ! $transform ) + $wgMessageCache->setTransform( $transstat ); + return $message; } @@ -369,19 +501,27 @@ * @param string $message * @param array $args * @return string - * @access private + * @private */ function wfMsgReplaceArgs( $message, $args ) { - static $replacementKeys = array( '$1', '$2', '$3', '$4', '$5', '$6', '$7', '$8', '$9' ); - # Fix windows line-endings # Some messages are split with explode("\n", $msg) $message = str_replace( "\r", '', $message ); - # Replace arguments - if( count( $args ) ) { - $message = str_replace( $replacementKeys, $args, $message ); + // Replace arguments + if ( count( $args ) ) { + if ( is_array( $args[0] ) ) { + foreach ( $args[0] as $key => $val ) { + $message = str_replace( '$' . $key, $val, $message ); + } + } else { + foreach( $args as $n => $param ) { + $replacementKeys['$' . ($n + 1)] = $param; + } + $message = strtr( $message, $replacementKeys ); + } } + return $message; } @@ -403,19 +543,93 @@ } /** + * Return an HTML version of message + * Parameter replacements, if any, are done *after* parsing the wiki-text message, + * so parameters may contain HTML (eg links or form controls). Be sure + * to pre-escape them if you really do want plaintext, or just wrap + * the whole thing in htmlspecialchars(). + * + * @param string $key + * @param string ... parameters + * @return string + */ +function wfMsgWikiHtml( $key ) { + global $wgOut; + $args = func_get_args(); + array_shift( $args ); + return wfMsgReplaceArgs( $wgOut->parse( wfMsgGetKey( $key, true ), /* can't be set to false */ true ), $args ); +} + +/** + * Returns message in the requested format + * @param string $key Key of the message + * @param array $options Processing rules: + * parse: parses wikitext to html + * parseinline: parses wikitext to html and removes the surrounding p's added by parser or tidy + * escape: filters message trough htmlspecialchars + * replaceafter: parameters are substituted after parsing or escaping + * parsemag: transform the message using magic phrases + */ +function wfMsgExt( $key, $options ) { + global $wgOut, $wgParser; + + $args = func_get_args(); + array_shift( $args ); + array_shift( $args ); + + if( !is_array($options) ) { + $options = array($options); + } + + $string = wfMsgGetKey( $key, true, false, false ); + + if( !in_array('replaceafter', $options) ) { + $string = wfMsgReplaceArgs( $string, $args ); + } + + if( in_array('parse', $options) ) { + $string = $wgOut->parse( $string, true, true ); + } elseif ( in_array('parseinline', $options) ) { + $string = $wgOut->parse( $string, true, true ); + $m = array(); + if( preg_match( '/^

      (.*)\n?<\/p>$/sU', $string, $m ) ) { + $string = $m[1]; + } + } elseif ( in_array('parsemag', $options) ) { + global $wgMessageCache; + if ( isset( $wgMessageCache ) ) { + $string = $wgMessageCache->transform( $string ); + } + } + + if ( in_array('escape', $options) ) { + $string = htmlspecialchars ( $string ); + } + + if( in_array('replaceafter', $options) ) { + $string = wfMsgReplaceArgs( $string, $args ); + } + + return $string; +} + + +/** * Just like exit() but makes a note of it. * Commits open transactions except if the error parameter is set + * + * @deprecated Please return control to the caller or throw an exception */ function wfAbruptExit( $error = false ){ global $wgLoadBalancer; static $called = false; if ( $called ){ - exit(); + exit( -1 ); } $called = true; - if( function_exists( 'debug_backtrace' ) ){ // PHP >= 4.3 - $bt = debug_backtrace(); + $bt = wfDebugBacktrace(); + if( $bt ) { for($i = 0; $i < count($bt) ; $i++){ $file = isset($bt[$i]['file']) ? $bt[$i]['file'] : "unknown"; $line = isset($bt[$i]['line']) ? $bt[$i]['line'] : "unknown"; @@ -424,53 +638,109 @@ } else { wfDebug('WARNING: Abrupt exit\n'); } + + wfLogProfilingData(); + if ( !$error ) { $wgLoadBalancer->closeAll(); } - exit(); + exit( -1 ); } /** - * @todo document + * @deprecated Please return control the caller or throw an exception */ function wfErrorExit() { wfAbruptExit( true ); } /** - * Die with a backtrace - * This is meant as a debugging aid to track down where bad data comes from. - * Shouldn't be used in production code except maybe in "shouldn't happen" areas. + * Print a simple message and die, returning nonzero to the shell if any. + * Plain die() fails to return nonzero to the shell if you pass a string. + * @param string $msg + */ +function wfDie( $msg='' ) { + echo $msg; + die( 1 ); +} + +/** + * Throw a debugging exception. This function previously once exited the process, + * but now throws an exception instead, with similar results. * * @param string $msg Message shown when dieing. */ function wfDebugDieBacktrace( $msg = '' ) { - global $wgCommandLineMode; + throw new MWException( $msg ); +} - $backtrace = wfBacktrace(); - if ( $backtrace !== false ) { - if ( $wgCommandLineMode ) { - $msg .= "\nBacktrace:\n$backtrace"; - } else { - $msg .= "\n

      Backtrace:

      \n$backtrace"; - } - } - echo $msg; - die( -1 ); +/** + * Fetch server name for use in error reporting etc. + * Use real server name if available, so we know which machine + * in a server farm generated the current page. + * @return string + */ +function wfHostname() { + if ( function_exists( 'posix_uname' ) ) { + // This function not present on Windows + $uname = @posix_uname(); + } else { + $uname = false; + } + if( is_array( $uname ) && isset( $uname['nodename'] ) ) { + return $uname['nodename']; + } else { + # This may be a virtual server. + return $_SERVER['SERVER_NAME']; + } +} + + /** + * Returns a HTML comment with the elapsed time since request. + * This method has no side effects. + * @return string + */ + function wfReportTime() { + global $wgRequestTime, $wgShowHostnames; + + $now = wfTime(); + $elapsed = $now - $wgRequestTime; + + return $wgShowHostnames + ? sprintf( "", wfHostname(), $elapsed ) + : sprintf( "", $elapsed ); + } + +/** + * Safety wrapper for debug_backtrace(). + * + * With Zend Optimizer 3.2.0 loaded, this causes segfaults under somewhat + * murky circumstances, which may be triggered in part by stub objects + * or other fancy talkin'. + * + * Will return an empty array if Zend Optimizer is detected, otherwise + * the output from debug_backtrace() (trimmed). + * + * @return array of backtrace information + */ +function wfDebugBacktrace() { + if( extension_loaded( 'Zend Optimizer' ) ) { + wfDebug( "Zend Optimizer detected; skipping debug_backtrace for safety.\n" ); + return array(); + } else { + return array_slice( debug_backtrace(), 1 ); + } } function wfBacktrace() { global $wgCommandLineMode; - if ( !function_exists( 'debug_backtrace' ) ) { - return false; - } - + if ( $wgCommandLineMode ) { $msg = ''; } else { $msg = "
        \n"; } - $backtrace = debug_backtrace(); + $backtrace = wfDebugBacktrace(); foreach( $backtrace as $call ) { if( isset( $call['file'] ) ) { $f = explode( DIRECTORY_SEPARATOR, $call['file'] ); @@ -515,7 +785,7 @@ */ function wfShowingResults( $offset, $limit ) { global $wgLang; - return wfMsg( 'showingresults', $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ) ); + return wfMsgExt( 'showingresults', array( 'parseinline' ), $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ) ); } /** @@ -523,18 +793,18 @@ */ function wfShowingResultsNum( $offset, $limit, $num ) { global $wgLang; - return wfMsg( 'showingresultsnum', $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ), $wgLang->formatNum( $num ) ); + return wfMsgExt( 'showingresultsnum', array( 'parseinline' ), $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ), $wgLang->formatNum( $num ) ); } /** * @todo document */ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { - global $wgUser, $wgLang; + global $wgLang; $fmtLimit = $wgLang->formatNum( $limit ); $prev = wfMsg( 'prevn', $fmtLimit ); $next = wfMsg( 'nextn', $fmtLimit ); - + if( is_object( $link ) ) { $title =& $link; } else { @@ -543,14 +813,13 @@ return false; } } - - $sk = $wgUser->getSkin(); + if ( 0 != $offset ) { $po = $offset - $limit; if ( $po < 0 ) { $po = 0; } $q = "limit={$limit}&offset={$po}"; if ( '' != $query ) { $q .= '&'.$query; } - $plink = '{$prev}"; + $plink = '{$prev}"; } else { $plink = $prev; } $no = $offset + $limit; @@ -560,7 +829,7 @@ if ( $atend ) { $nlink = $next; } else { - $nlink = '{$next}"; + $nlink = '{$next}"; } $nums = wfNumLink( $offset, 20, $title, $query ) . ' | ' . wfNumLink( $offset, 50, $title, $query ) . ' | ' . @@ -575,13 +844,13 @@ * @todo document */ function wfNumLink( $offset, $limit, &$title, $query = '' ) { - global $wgUser, $wgLang; + global $wgLang; if ( '' == $query ) { $q = ''; } else { $q = $query.'&'; } $q .= 'limit='.$limit.'&offset='.$offset; $fmtLimit = $wgLang->formatNum( $limit ); - $s = '{$fmtLimit}"; + $s = '{$fmtLimit}"; return $s; } @@ -595,6 +864,7 @@ global $wgUseGzip; if( $wgUseGzip ) { # FIXME: we may want to blacklist some broken browsers + $m = array(); if( preg_match( '/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/', $_SERVER['HTTP_ACCEPT_ENCODING'], @@ -608,7 +878,13 @@ } /** - * Yay, more global functions! + * Obtain the offset and limit values from the request string; + * used in special pages + * + * @param $deflimit Default limit if none supplied + * @param $optionname Name of a user preference to check against + * @return array + * */ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) { global $wgRequest; @@ -626,9 +902,9 @@ * @param string $text Text to be escaped */ function wfEscapeWikiText( $text ) { - $text = str_replace( - array( '[', '|', '\'', 'ISBN ' , '://' , "\n=", '{{' ), - array( '[', '|', ''', 'ISBN ', '://' , "\n=", '{{' ), + $text = str_replace( + array( '[', '|', '\'', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), + array( '[', '|', ''', 'ISBN ', 'RFC ', '://', "\n=", '{{' ), htmlspecialchars($text) ); return $text; } @@ -654,46 +930,13 @@ return $out; } -/** - * Returns an escaped string suitable for inclusion in a string literal - * for JavaScript source code. - * Illegal control characters are assumed not to be present. - * - * @param string $string - * @return string - */ -function wfEscapeJsString( $string ) { - // See ECMA 262 section 7.8.4 for string literal format - $pairs = array( - "\\" => "\\\\", - "\"" => "\\\"", - '\'' => '\\\'', - "\n" => "\\n", - "\r" => "\\r", - - # To avoid closing the element or CDATA section - "<" => "\\x3c", - ">" => "\\x3e", - ); - return strtr( $string, $pairs ); -} /** * @todo document * @return float */ function wfTime() { - $st = explode( ' ', microtime() ); - return (float)$st[0] + (float)$st[1]; -} - -/** - * Changes the first character to an HTML entity - */ -function wfHtmlEscapeFirst( $text ) { - $ord = ord($text); - $newText = substr($text, 1); - return "&#$ord;$newText"; + return microtime(true); } /** @@ -747,6 +990,26 @@ } /** + * Append a query string to an existing URL, which may or may not already + * have query string parameters already. If so, they will be combined. + * + * @param string $url + * @param string $query + * @return string + */ +function wfAppendQuery( $url, $query ) { + if( $query != '' ) { + if( false === strpos( $url, '?' ) ) { + $url .= '?'; + } else { + $url .= '&'; + } + $url .= $query; + } + return $url; +} + +/** * This is obsolete, use SquidUpdate::purge() * @deprecated */ @@ -756,7 +1019,7 @@ /** * Windows-compatible version of escapeshellarg() - * Windows doesn't recognise single-quotes in the shell, but the escapeshellarg() + * Windows doesn't recognise single-quotes in the shell, but the escapeshellarg() * function puts single quotes in regardless of OS */ function wfEscapeShellArg( ) { @@ -769,9 +1032,31 @@ } else { $first = false; } - + if ( wfIsWindows() ) { - $retVal .= '"' . str_replace( '"','\"', $arg ) . '"'; + // Escaping for an MSVC-style command line parser + // Ref: http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html + // Double the backslashes before any double quotes. Escape the double quotes. + $tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE ); + $arg = ''; + $delim = false; + foreach ( $tokens as $token ) { + if ( $delim ) { + $arg .= str_replace( '\\', '\\\\', substr( $token, 0, -1 ) ) . '\\"'; + } else { + $arg .= $token; + } + $delim = !$delim; + } + // Double the backslashes before the end of the string, because + // we will soon add a quote + $m = array(); + if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) ) { + $arg = $m[1] . str_replace( '\\', '\\\\', $m[2] ); + } + + // Add surrounding quotes + $retVal .= '"' . $arg . '"'; } else { $retVal .= escapeshellarg( $arg ); } @@ -862,17 +1147,82 @@ header( "Status: $code $label" ); $wgOut->sendCacheControl(); - header( 'Content-type: text/html' ); - print "" . - htmlspecialchars( $label ) . - "

        " . + header( 'Content-type: text/html; charset=utf-8' ); + print "". + "" . + htmlspecialchars( $label ) . + "

        " . htmlspecialchars( $label ) . "

        " . - htmlspecialchars( $desc ) . + nl2br( htmlspecialchars( $desc ) ) . "

        \n"; } /** + * Clear away any user-level output buffers, discarding contents. + * + * Suitable for 'starting afresh', for instance when streaming + * relatively large amounts of data without buffering, or wanting to + * output image files without ob_gzhandler's compression. + * + * The optional $resetGzipEncoding parameter controls suppression of + * the Content-Encoding header sent by ob_gzhandler; by default it + * is left. See comments for wfClearOutputBuffers() for why it would + * be used. + * + * Note that some PHP configuration options may add output buffer + * layers which cannot be removed; these are left in place. + * + * @param bool $resetGzipEncoding + */ +function wfResetOutputBuffers( $resetGzipEncoding=true ) { + if( $resetGzipEncoding ) { + // Suppress Content-Encoding and Content-Length + // headers from 1.10+s wfOutputHandler + global $wgDisableOutputCompression; + $wgDisableOutputCompression = true; + } + while( $status = ob_get_status() ) { + if( $status['type'] == 0 /* PHP_OUTPUT_HANDLER_INTERNAL */ ) { + // Probably from zlib.output_compression or other + // PHP-internal setting which can't be removed. + // + // Give up, and hope the result doesn't break + // output behavior. + break; + } + if( !ob_end_clean() ) { + // Could not remove output buffer handler; abort now + // to avoid getting in some kind of infinite loop. + break; + } + if( $resetGzipEncoding ) { + if( $status['name'] == 'ob_gzhandler' ) { + // Reset the 'Content-Encoding' field set by this handler + // so we can start fresh. + header( 'Content-Encoding:' ); + } + } + } +} + +/** + * More legible than passing a 'false' parameter to wfResetOutputBuffers(): + * + * Clear away output buffers, but keep the Content-Encoding header + * produced by ob_gzhandler, if any. + * + * This should be used for HTTP 304 responses, where you need to + * preserve the Content-Encoding header of the real result, but + * also need to suppress the output of ob_gzhandler to keep to spec + * and avoid breaking Firefox in rare cases where the headers and + * body are broken over two packets. + */ +function wfClearOutputBuffers() { + wfResetOutputBuffers( false ); +} + +/** * Converts an Accept-* header into an array mapping string values to quality * factors */ @@ -889,6 +1239,7 @@ foreach( $parts as $part ) { # FIXME: doesn't deal with params like 'text/html; level=1' @list( $value, $qpart ) = explode( ';', $part ); + $match = array(); if( !isset( $qpart ) ) { $prefs[$value] = 1; } elseif( preg_match( '/q\s*=\s*(\d*\.\d+)/', $qpart, $match ) ) { @@ -909,7 +1260,7 @@ * @param string $type * @param array $avail * @return string - * @access private + * @private */ function mimeTypeMatch( $type, $avail ) { if( array_key_exists($type, $avail) ) { @@ -979,7 +1330,7 @@ * Array lookup * Returns an array where the values in the first array are replaced by the * values in the second array with the corresponding keys - * + * * @return array */ function wfArrayLookup( $a, $b ) { @@ -1004,7 +1355,7 @@ if ( $end ) { if ( $suppressCount ) { - $suppressCount --; + --$suppressCount; if ( !$suppressCount ) { error_reporting( $originalLevel ); } @@ -1013,7 +1364,7 @@ if ( !$suppressCount ) { $originalLevel = error_reporting( E_ALL & ~( E_WARNING | E_NOTICE ) ); } - $suppressCount++; + ++$suppressCount; } } @@ -1026,7 +1377,7 @@ # Autodetect, convert and provide timestamps of various types -/** +/** * Unix time - the number of seconds since 1970-01-01 00:00:00 UTC */ define('TS_UNIX', 0); @@ -1047,47 +1398,80 @@ define('TS_RFC2822', 3); /** + * ISO 8601 format with no timezone: 1986-02-09T20:00:00Z + * + * This is used by Special:Export + */ +define('TS_ISO_8601', 4); + +/** * An Exif timestamp (YYYY:MM:DD HH:MM:SS) * - * @link http://exif.org/Exif2-2.PDF The Exif 2.2 spec, see page 28 for the + * @see http://exif.org/Exif2-2.PDF The Exif 2.2 spec, see page 28 for the * DateTime tag and page 36 for the DateTimeOriginal and * DateTimeDigitized tags. */ -define('TS_EXIF', 4); +define('TS_EXIF', 5); + +/** + * Oracle format time. + */ +define('TS_ORACLE', 6); +/** + * Postgres format time. + */ +define('TS_POSTGRES', 7); /** * @param mixed $outputtype A timestamp in one of the supported formats, the * function will autodetect which format is supplied - and act accordingly. + * and act accordingly. * @return string Time in the format specified in $outputtype */ function wfTimestamp($outputtype=TS_UNIX,$ts=0) { $uts = 0; + $da = array(); if ($ts==0) { $uts=time(); - } elseif (preg_match("/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/",$ts,$da)) { + } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D',$ts,$da)) { # TS_DB $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], (int)$da[2],(int)$da[3],(int)$da[1]); - } elseif (preg_match("/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/",$ts,$da)) { + } elseif (preg_match('/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D',$ts,$da)) { # TS_EXIF $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], (int)$da[2],(int)$da[3],(int)$da[1]); - } elseif (preg_match("/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/",$ts,$da)) { + } elseif (preg_match('/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D',$ts,$da)) { # TS_MW $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], (int)$da[2],(int)$da[3],(int)$da[1]); - } elseif (preg_match("/^(\d{1,13})$/",$ts,$datearray)) { + } elseif (preg_match('/^(\d{1,13})$/D',$ts,$da)) { # TS_UNIX - $uts=$ts; + $uts = $ts; + } elseif (preg_match('/^(\d{1,2})-(...)-(\d\d(\d\d)?) (\d\d)\.(\d\d)\.(\d\d)/', $ts, $da)) { + # TS_ORACLE + $uts = strtotime(preg_replace('/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3", + str_replace("+00:00", "UTC", $ts))); + } elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/', $ts, $da)) { + # TS_ISO_8601 + $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], + (int)$da[2],(int)$da[3],(int)$da[1]); + } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)[\+\- ](\d\d)$/',$ts,$da)) { + # TS_POSTGRES + $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], + (int)$da[2],(int)$da[3],(int)$da[1]); + } elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/',$ts,$da)) { + # TS_POSTGRES + $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6], + (int)$da[2],(int)$da[3],(int)$da[1]); } else { # Bogus value; fall back to the epoch... wfDebug("wfTimestamp() fed bogus time value: $outputtype; $ts\n"); $uts = 0; } - + switch($outputtype) { case TS_UNIX: return $uts; @@ -1095,13 +1479,19 @@ return gmdate( 'YmdHis', $uts ); case TS_DB: return gmdate( 'Y-m-d H:i:s', $uts ); + case TS_ISO_8601: + return gmdate( 'Y-m-d\TH:i:s\Z', $uts ); // This shouldn't ever be used, but is included for completeness case TS_EXIF: return gmdate( 'Y:m:d H:i:s', $uts ); case TS_RFC2822: return gmdate( 'D, d M Y H:i:s', $uts ) . ' GMT'; + case TS_ORACLE: + return gmdate( 'd-M-y h.i.s A', $uts) . ' +00:00'; + case TS_POSTGRES: + return gmdate( 'Y-m-d H:i:s', $uts) . ' GMT'; default: - wfDebugDieBacktrace( 'wfTimestamp() called with illegal output type.'); + throw new MWException( 'wfTimestamp() called with illegal output type.'); } } @@ -1121,17 +1511,17 @@ } /** - * Check where as the operating system is Windows + * Check if the operating system is Windows * - * @return bool True if it's windows, False otherwise. + * @return bool True if it's Windows, False otherwise. */ -function wfIsWindows() { - if (substr(php_uname(), 0, 7) == 'Windows') { - return true; - } else { - return false; - } -} +function wfIsWindows() { + if (substr(php_uname(), 0, 7) == 'Windows') { + return true; + } else { + return false; + } +} /** * Swap two variables @@ -1142,143 +1532,112 @@ $y = $z; } -function wfGetSiteNotice() { - global $wgSiteNotice, $wgTitle, $wgOut; - $fname = 'wfGetSiteNotice'; +function wfGetCachedNotice( $name ) { + global $wgOut, $parserMemc; + $fname = 'wfGetCachedNotice'; wfProfileIn( $fname ); - - $notice = wfMsg( 'sitenotice' ); - if( $notice == '<sitenotice>' || $notice == '-' ) { - $notice = ''; - } - if( $notice == '' ) { - # We may also need to override a message with eg downtime info - # FIXME: make this work! + + $needParse = false; + + if( $name === 'default' ) { + // special case + global $wgSiteNotice; $notice = $wgSiteNotice; + if( empty( $notice ) ) { + wfProfileOut( $fname ); + return false; + } + } else { + $notice = wfMsgForContentNoTrans( $name ); + if( wfEmptyMsg( $name, $notice ) || $notice == '-' ) { + wfProfileOut( $fname ); + return( false ); + } } - if($notice != '-' && $notice != '') { - $specialparser = new Parser(); - $parserOutput = $specialparser->parse( $notice, $wgTitle, $wgOut->mParserOptions, false ); - $notice = $parserOutput->getText(); - } - wfProfileOut( $fname ); - return $notice; -} - -/** - * Format an XML element with given attributes and, optionally, text content. - * Element and attribute names are assumed to be ready for literal inclusion. - * Strings are assumed to not contain XML-illegal characters; special - * characters (<, >, &) are escaped but illegals are not touched. - * - * @param string $element - * @param array $attribs Name=>value pairs. Values will be escaped. - * @param string $contents NULL to make an open tag only; '' for a contentless closed tag (default) - * @return string - */ -function wfElement( $element, $attribs = null, $contents = '') { - $out = '<' . $element; - if( !is_null( $attribs ) ) { - foreach( $attribs as $name => $val ) { - $out .= ' ' . $name . '="' . htmlspecialchars( $val ) . '"'; + + $cachedNotice = $parserMemc->get( wfMemcKey( $name ) ); + if( is_array( $cachedNotice ) ) { + if( md5( $notice ) == $cachedNotice['hash'] ) { + $notice = $cachedNotice['html']; + } else { + $needParse = true; } - } - if( is_null( $contents ) ) { - $out .= '>'; } else { - if( $contents == '' ) { - $out .= ' />'; + $needParse = true; + } + + if( $needParse ) { + if( is_object( $wgOut ) ) { + $parsed = $wgOut->parse( $notice ); + $parserMemc->set( wfMemcKey( $name ), array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 ); + $notice = $parsed; } else { - $out .= '>'; - $out .= htmlspecialchars( $contents ); - $out .= ""; + wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available' ); + $notice = ''; } } - return $out; + + wfProfileOut( $fname ); + return $notice; } -/** - * Format an XML element as with wfElement(), but run text through the - * UtfNormal::cleanUp() validator first to ensure that no invalid UTF-8 - * is passed. - * - * @param string $element - * @param array $attribs Name=>value pairs. Values will be escaped. - * @param string $contents NULL to make an open tag only; '' for a contentless closed tag (default) - * @return string - */ -function wfElementClean( $element, $attribs = array(), $contents = '') { - if( $attribs ) { - $attribs = array_map( array( 'UtfNormal', 'cleanUp' ), $attribs ); - } - if( $contents ) { - $contents = UtfNormal::cleanUp( $contents ); +function wfGetNamespaceNotice() { + global $wgTitle; + + # Paranoia + if ( !isset( $wgTitle ) || !is_object( $wgTitle ) ) + return ""; + + $fname = 'wfGetNamespaceNotice'; + wfProfileIn( $fname ); + + $key = "namespacenotice-" . $wgTitle->getNsText(); + $namespaceNotice = wfGetCachedNotice( $key ); + if ( $namespaceNotice && substr ( $namespaceNotice , 0 ,7 ) != "

        <" ) { + $namespaceNotice = '

        ' . $namespaceNotice . "
        "; + } else { + $namespaceNotice = ""; } - return wfElement( $element, $attribs, $contents ); + + wfProfileOut( $fname ); + return $namespaceNotice; } -/** - * Create a namespace selector - * - * @param mixed $selected The namespace which should be selected, default '' - * @param string $allnamespaces Value of a special item denoting all namespaces. Null to not include (default) - * @return Html string containing the namespace selector - */ -function &HTMLnamespaceselector($selected = '', $allnamespaces = null) { - global $wgContLang; - $s = "\n"; - return $s; -} -/** Global singleton instance of MimeMagic. This is initialized on demand, -* please always use the wfGetMimeMagic() function to get the instance. -* -* @private -*/ -$wgMimeMagic= NULL; + wfRunHooks( 'SiteNoticeAfter', array( &$siteNotice ) ); + wfProfileOut( $fname ); + return $siteNotice; +} -/** Factory functions for the global MimeMagic object. -* This function always returns the same singleton instance of MimeMagic. -* That objects will be instantiated on the first call to this function. -* If needed, the MimeMagic.php file is automatically included by this function. -* @return MimeMagic the global MimeMagic objects. -*/ +/** + * BC wrapper for MimeMagic::singleton() + * @deprecated + */ function &wfGetMimeMagic() { - global $wgMimeMagic; - - if (!is_null($wgMimeMagic)) { - return $wgMimeMagic; - } - - if (!class_exists("MimeMagic")) { - #include on demand - require_once("MimeMagic.php"); - } - - $wgMimeMagic= new MimeMagic(); - - return $wgMimeMagic; + return MimeMagic::singleton(); } - /** * Tries to get the system directory for temporary files. * The TMPDIR, TMP, and TEMP environment variables are checked in sequence, @@ -1303,31 +1662,24 @@ /** * Make directory, and make all parent directories if they don't exist */ -function wfMkdirParents( $fullDir, $mode ) { - $parts = explode( '/', $fullDir ); - $path = ''; - $success = false; - foreach ( $parts as $dir ) { - $path .= $dir . '/'; - if ( !is_dir( $path ) ) { - if ( !mkdir( $path, $mode ) ) { - return false; - } - } - } - return true; +function wfMkdirParents( $fullDir, $mode = 0777 ) { + if( strval( $fullDir ) === '' ) + return true; + if( file_exists( $fullDir ) ) + return true; + return mkdir( str_replace( '/', DIRECTORY_SEPARATOR, $fullDir ), $mode, true ); } /** * Increment a statistics counter */ -function wfIncrStats( $key ) { - global $wgDBname, $wgMemc; - $key = "$wgDBname:stats:$key"; - if ( is_null( $wgMemc->incr( $key ) ) ) { - $wgMemc->add( $key, 1 ); - } -} + function wfIncrStats( $key ) { + global $wgMemc; + $key = wfMemcKey( 'stats', $key ); + if ( is_null( $wgMemc->incr( $key ) ) ) { + $wgMemc->add( $key, 1 ); + } + } /** * @param mixed $nr The number to format @@ -1362,7 +1714,7 @@ */ function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) { if ( is_null( $changed ) ) { - wfDebugDieBacktrace('GlobalFunctions::wfAppendToArrayIfNotDefault got null'); + throw new MWException('GlobalFunctions::wfAppendToArrayIfNotDefault got null'); } if ( $default[$key] !== $value ) { $changed[$key] = $value; @@ -1378,7 +1730,594 @@ * @param $wfMsgOut The output of wfMsg*() * @return bool */ -function wfNoMsg( $msg, $wfMsgOut ) { - return $wfMsgOut === "<$msg>"; +function wfEmptyMsg( $msg, $wfMsgOut ) { + return $wfMsgOut === htmlspecialchars( "<$msg>" ); +} + +/** + * Find out whether or not a mixed variable exists in a string + * + * @param mixed needle + * @param string haystack + * @return bool + */ +function in_string( $needle, $str ) { + return strpos( $str, $needle ) !== false; +} + +function wfSpecialList( $page, $details ) { + global $wgContLang; + $details = $details ? ' ' . $wgContLang->getDirMark() . "($details)" : ""; + return $page . $details; +} + +/** + * Returns a regular expression of url protocols + * + * @return string + */ +function wfUrlProtocols() { + global $wgUrlProtocols; + + // Support old-style $wgUrlProtocols strings, for backwards compatibility + // with LocalSettings files from 1.5 + if ( is_array( $wgUrlProtocols ) ) { + $protocols = array(); + foreach ($wgUrlProtocols as $protocol) + $protocols[] = preg_quote( $protocol, '/' ); + + return implode( '|', $protocols ); + } else { + return $wgUrlProtocols; + } +} + +/** + * Execute a shell command, with time and memory limits mirrored from the PHP + * configuration if supported. + * @param $cmd Command line, properly escaped for shell. + * @param &$retval optional, will receive the program's exit code. + * (non-zero is usually failure) + * @return collected stdout as a string (trailing newlines stripped) + */ +function wfShellExec( $cmd, &$retval=null ) { + global $IP, $wgMaxShellMemory, $wgMaxShellFileSize; + + if( ini_get( 'safe_mode' ) ) { + wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); + $retval = 1; + return "Unable to run external programs in safe mode."; + } + + if ( php_uname( 's' ) == 'Linux' ) { + $time = intval( ini_get( 'max_execution_time' ) ); + $mem = intval( $wgMaxShellMemory ); + $filesize = intval( $wgMaxShellFileSize ); + + if ( $time > 0 && $mem > 0 ) { + $script = "$IP/bin/ulimit4.sh"; + if ( is_executable( $script ) ) { + $cmd = escapeshellarg( $script ) . " $time $mem $filesize " . escapeshellarg( $cmd ); + } + } + } elseif ( php_uname( 's' ) == 'Windows NT' ) { + # This is a hack to work around PHP's flawed invocation of cmd.exe + # http://news.php.net/php.internals/21796 + $cmd = '"' . $cmd . '"'; + } + wfDebug( "wfShellExec: $cmd\n" ); + + $output = array(); + $retval = 1; // error by default? + exec( $cmd, $output, $retval ); // returns the last line of output. + return implode( "\n", $output ); + +} + +/** + * This function works like "use VERSION" in Perl, the program will die with a + * backtrace if the current version of PHP is less than the version provided + * + * This is useful for extensions which due to their nature are not kept in sync + * with releases, and might depend on other versions of PHP than the main code + * + * Note: PHP might die due to parsing errors in some cases before it ever + * manages to call this function, such is life + * + * @see perldoc -f use + * + * @param mixed $version The version to check, can be a string, an integer, or + * a float + */ +function wfUsePHP( $req_ver ) { + $php_ver = PHP_VERSION; + + if ( version_compare( $php_ver, (string)$req_ver, '<' ) ) + throw new MWException( "PHP $req_ver required--this is only $php_ver" ); +} + +/** + * This function works like "use VERSION" in Perl except it checks the version + * of MediaWiki, the program will die with a backtrace if the current version + * of MediaWiki is less than the version provided. + * + * This is useful for extensions which due to their nature are not kept in sync + * with releases + * + * @see perldoc -f use + * + * @param mixed $version The version to check, can be a string, an integer, or + * a float + */ +function wfUseMW( $req_ver ) { + global $wgVersion; + + if ( version_compare( $wgVersion, (string)$req_ver, '<' ) ) + throw new MWException( "MediaWiki $req_ver required--this is only $wgVersion" ); +} + +/** + * @deprecated use StringUtils::escapeRegexReplacement + */ +function wfRegexReplacement( $string ) { + return StringUtils::escapeRegexReplacement( $string ); +} + +/** + * Return the final portion of a pathname. + * Reimplemented because PHP5's basename() is buggy with multibyte text. + * http://bugs.php.net/bug.php?id=33898 + * + * PHP's basename() only considers '\' a pathchar on Windows and Netware. + * We'll consider it so always, as we don't want \s in our Unix paths either. + * + * @param string $path + * @param string $suffix to remove if present + * @return string + */ +function wfBaseName( $path, $suffix='' ) { + $encSuffix = ($suffix == '') + ? '' + : ( '(?:' . preg_quote( $suffix, '#' ) . ')?' ); + $matches = array(); + if( preg_match( "#([^/\\\\]*?){$encSuffix}[/\\\\]*$#", $path, $matches ) ) { + return $matches[1]; + } else { + return ''; + } +} + +/** + * Generate a relative path name to the given file. + * May explode on non-matching case-insensitive paths, + * funky symlinks, etc. + * + * @param string $path Absolute destination path including target filename + * @param string $from Absolute source path, directory only + * @return string + */ +function wfRelativePath( $path, $from ) { + // Normalize mixed input on Windows... + $path = str_replace( '/', DIRECTORY_SEPARATOR, $path ); + $from = str_replace( '/', DIRECTORY_SEPARATOR, $from ); + + $pieces = explode( DIRECTORY_SEPARATOR, dirname( $path ) ); + $against = explode( DIRECTORY_SEPARATOR, $from ); + + // Trim off common prefix + while( count( $pieces ) && count( $against ) + && $pieces[0] == $against[0] ) { + array_shift( $pieces ); + array_shift( $against ); + } + + // relative dots to bump us to the parent + while( count( $against ) ) { + array_unshift( $pieces, '..' ); + array_shift( $against ); + } + + array_push( $pieces, wfBaseName( $path ) ); + + return implode( DIRECTORY_SEPARATOR, $pieces ); +} + +/** + * Make a URL index, appropriate for the el_index field of externallinks. + */ +function wfMakeUrlIndex( $url ) { + global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php + $bits = parse_url( $url ); + wfSuppressWarnings(); + wfRestoreWarnings(); + if ( !$bits ) { + return false; + } + // most of the protocols are followed by ://, but mailto: and sometimes news: not, check for it + $delimiter = ''; + if ( in_array( $bits['scheme'] . '://' , $wgUrlProtocols ) ) { + $delimiter = '://'; + } elseif ( in_array( $bits['scheme'] .':' , $wgUrlProtocols ) ) { + $delimiter = ':'; + // parse_url detects for news: and mailto: the host part of an url as path + // We have to correct this wrong detection + if ( isset ( $bits['path'] ) ) { + $bits['host'] = $bits['path']; + $bits['path'] = ''; + } + } else { + return false; + } + + // Reverse the labels in the hostname, convert to lower case + // For emails reverse domainpart only + if ( $bits['scheme'] == 'mailto' ) { + $mailparts = explode( '@', $bits['host'] ); + $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); + $reversedHost = $domainpart . '@' . $mailparts[0]; + } else { + $reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); + } + // Add an extra dot to the end + if ( substr( $reversedHost, -1, 1 ) !== '.' ) { + $reversedHost .= '.'; + } + // Reconstruct the pseudo-URL + $prot = $bits['scheme']; + $index = "$prot$delimiter$reversedHost"; + // Leave out user and password. Add the port, path, query and fragment + if ( isset( $bits['port'] ) ) $index .= ':' . $bits['port']; + if ( isset( $bits['path'] ) ) { + $index .= $bits['path']; + } else { + $index .= '/'; + } + if ( isset( $bits['query'] ) ) $index .= '?' . $bits['query']; + if ( isset( $bits['fragment'] ) ) $index .= '#' . $bits['fragment']; + return $index; +} + +/** + * Do any deferred updates and clear the list + * TODO: This could be in Wiki.php if that class made any sense at all + */ +function wfDoUpdates() +{ + global $wgPostCommitUpdateList, $wgDeferredUpdateList; + foreach ( $wgDeferredUpdateList as $update ) { + $update->doUpdate(); + } + foreach ( $wgPostCommitUpdateList as $update ) { + $update->doUpdate(); + } + $wgDeferredUpdateList = array(); + $wgPostCommitUpdateList = array(); +} + +/** + * @deprecated use StringUtils::explodeMarkup + */ +function wfExplodeMarkup( $separator, $text ) { + return StringUtils::explodeMarkup( $separator, $text ); +} + +/** + * Convert an arbitrarily-long digit string from one numeric base + * to another, optionally zero-padding to a minimum column width. + * + * Supports base 2 through 36; digit values 10-36 are represented + * as lowercase letters a-z. Input is case-insensitive. + * + * @param $input string of digits + * @param $sourceBase int 2-36 + * @param $destBase int 2-36 + * @param $pad int 1 or greater + * @param $lowercase bool + * @return string or false on invalid input + */ +function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1, $lowercase=true ) { + $input = strval( $input ); + if( $sourceBase < 2 || + $sourceBase > 36 || + $destBase < 2 || + $destBase > 36 || + $pad < 1 || + $sourceBase != intval( $sourceBase ) || + $destBase != intval( $destBase ) || + $pad != intval( $pad ) || + !is_string( $input ) || + $input == '' ) { + return false; + } + $digitChars = ( $lowercase ) ? '0123456789abcdefghijklmnopqrstuvwxyz' : '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $inDigits = array(); + $outChars = ''; + + // Decode and validate input string + $input = strtolower( $input ); + for( $i = 0; $i < strlen( $input ); $i++ ) { + $n = strpos( $digitChars, $input{$i} ); + if( $n === false || $n > $sourceBase ) { + return false; + } + $inDigits[] = $n; + } + + // Iterate over the input, modulo-ing out an output digit + // at a time until input is gone. + while( count( $inDigits ) ) { + $work = 0; + $workDigits = array(); + + // Long division... + foreach( $inDigits as $digit ) { + $work *= $sourceBase; + $work += $digit; + + if( $work < $destBase ) { + // Gonna need to pull another digit. + if( count( $workDigits ) ) { + // Avoid zero-padding; this lets us find + // the end of the input very easily when + // length drops to zero. + $workDigits[] = 0; + } + } else { + // Finally! Actual division! + $workDigits[] = intval( $work / $destBase ); + + // Isn't it annoying that most programming languages + // don't have a single divide-and-remainder operator, + // even though the CPU implements it that way? + $work = $work % $destBase; + } + } + + // All that division leaves us with a remainder, + // which is conveniently our next output digit. + $outChars .= $digitChars[$work]; + + // And we continue! + $inDigits = $workDigits; + } + + while( strlen( $outChars ) < $pad ) { + $outChars .= '0'; + } + + return strrev( $outChars ); +} + +/** + * Create an object with a given name and an array of construct parameters + * @param string $name + * @param array $p parameters + */ +function wfCreateObject( $name, $p ){ + $p = array_values( $p ); + switch ( count( $p ) ) { + case 0: + return new $name; + case 1: + return new $name( $p[0] ); + case 2: + return new $name( $p[0], $p[1] ); + case 3: + return new $name( $p[0], $p[1], $p[2] ); + case 4: + return new $name( $p[0], $p[1], $p[2], $p[3] ); + case 5: + return new $name( $p[0], $p[1], $p[2], $p[3], $p[4] ); + case 6: + return new $name( $p[0], $p[1], $p[2], $p[3], $p[4], $p[5] ); + default: + throw new MWException( "Too many arguments to construtor in wfCreateObject" ); + } +} + +/** + * Aliases for modularized functions + */ +function wfGetHTTP( $url, $timeout = 'default' ) { + return Http::get( $url, $timeout ); +} +function wfIsLocalURL( $url ) { + return Http::isLocalURL( $url ); +} + +/** + * Initialise php session + */ +function wfSetupSession() { + global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain, $wgCookieSecure; + if( $wgSessionsInMemcached ) { + require_once( 'MemcachedSessions.php' ); + } elseif( 'files' != ini_get( 'session.save_handler' ) ) { + # If it's left on 'user' or another setting from another + # application, it will end up failing. Try to recover. + ini_set ( 'session.save_handler', 'files' ); + } + session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure); + session_cache_limiter( 'private, must-revalidate' ); + @session_start(); +} + +/** + * Get an object from the precompiled serialized directory + * + * @return mixed The variable on success, false on failure + */ +function wfGetPrecompiledData( $name ) { + global $IP; + + $file = "$IP/serialized/$name"; + if ( file_exists( $file ) ) { + $blob = file_get_contents( $file ); + if ( $blob ) { + return unserialize( $blob ); + } + } + return false; +} + +function wfGetCaller( $level = 2 ) { + $backtrace = wfDebugBacktrace(); + if ( isset( $backtrace[$level] ) ) { + if ( isset( $backtrace[$level]['class'] ) ) { + $caller = $backtrace[$level]['class'] . '::' . $backtrace[$level]['function']; + } else { + $caller = $backtrace[$level]['function']; + } + } else { + $caller = 'unknown'; + } + return $caller; +} + +/** Return a string consisting all callers in stack, somewhat useful sometimes for profiling specific points */ +function wfGetAllCallers() { + return implode('/', array_map( + create_function('$frame',' + return isset( $frame["class"] )? + $frame["class"]."::".$frame["function"]: + $frame["function"]; + '), + array_reverse(wfDebugBacktrace()))); +} + +/** + * Get a cache key + */ +function wfMemcKey( /*... */ ) { + global $wgDBprefix, $wgDBname; + $args = func_get_args(); + if ( $wgDBprefix ) { + $key = "$wgDBname-$wgDBprefix:" . implode( ':', $args ); + } else { + $key = $wgDBname . ':' . implode( ':', $args ); + } + return $key; +} + +/** + * Get a cache key for a foreign DB + */ +function wfForeignMemcKey( $db, $prefix /*, ... */ ) { + $args = array_slice( func_get_args(), 2 ); + if ( $prefix ) { + $key = "$db-$prefix:" . implode( ':', $args ); + } else { + $key = $db . ':' . implode( ':', $args ); + } + return $key; +} + +/** + * Get an ASCII string identifying this wiki + * This is used as a prefix in memcached keys + */ +function wfWikiID() { + global $wgDBprefix, $wgDBname; + if ( $wgDBprefix ) { + return "$wgDBname-$wgDBprefix"; + } else { + return $wgDBname; + } +} + +/* + * Get a Database object + * @param integer $db Index of the connection to get. May be DB_MASTER for the + * master (for write queries), DB_SLAVE for potentially lagged + * read queries, or an integer >= 0 for a particular server. + * + * @param mixed $groups Query groups. An array of group names that this query + * belongs to. May contain a single string if the query is only + * in one group. + */ +function &wfGetDB( $db = DB_LAST, $groups = array() ) { + global $wgLoadBalancer; + $ret = $wgLoadBalancer->getConnection( $db, true, $groups ); + return $ret; +} + +/** + * Find a file. + * Shortcut for RepoGroup::singleton()->findFile() + * @param mixed $title Title object or string. May be interwiki. + * @param mixed $time Requested time for an archived image, or false for the + * current version. An image object will be returned which + * existed at or before the specified time. + * @return File, or false if the file does not exist + */ +function wfFindFile( $title, $time = false ) { + return RepoGroup::singleton()->findFile( $title, $time ); +} + +/** + * Get an object referring to a locally registered file. + * Returns a valid placeholder object if the file does not exist. + */ +function wfLocalFile( $title ) { + return RepoGroup::singleton()->getLocalRepo()->newFile( $title ); +} + +/** + * Should low-performance queries be disabled? + * + * @return bool + */ +function wfQueriesMustScale() { + global $wgMiserMode; + return $wgMiserMode + || ( SiteStats::pages() > 100000 + && SiteStats::edits() > 1000000 + && SiteStats::users() > 10000 ); +} + +/** + * Get the path to a specified script file, respecting file + * extensions; this is a wrapper around $wgScriptExtension etc. + * + * @param string $script Script filename, sans extension + * @return string + */ +function wfScript( $script = 'index' ) { + global $wgScriptPath, $wgScriptExtension; + return "{$wgScriptPath}/{$script}{$wgScriptExtension}"; } -?> + +/** + * Convenience function converts boolean values into "true" + * or "false" (string) values + * + * @param bool $value + * @return string + */ +function wfBoolToStr( $value ) { + return $value ? 'true' : 'false'; +} + +/** + * Load an extension messages file + */ +function wfLoadExtensionMessages( $extensionName ) { + global $wgExtensionMessagesFiles, $wgMessageCache; + if ( !empty( $wgExtensionMessagesFiles[$extensionName] ) ) { + $wgMessageCache->loadMessagesFile( $wgExtensionMessagesFiles[$extensionName] ); + // Prevent double-loading + $wgExtensionMessagesFiles[$extensionName] = false; + } +} + +/** + * Get a platform-independent path to the null file, e.g. + * /dev/null + * + * @return string + */ +function wfGetNull() { + return wfIsWindows() + ? 'NUL' + : '/dev/null'; +} \ No newline at end of file diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/HTMLForm.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/HTMLForm.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/HTMLForm.php 2005-06-09 05:49:10.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/HTMLForm.php 2007-08-07 15:52:24.000000000 -0400 @@ -1,14 +1,11 @@ mRequest->wasPosted() && !is_null( $this->mRequest->getVal( $varname ) ) ) { @@ -45,11 +42,11 @@ "
      \n"; } - /** - * @access private - * @param string $varname Name of the textbox. - * @param string $value Optional value (default empty) - * @param integer $size Optional size of the textbox (default 20) + /** + * @private + * @param $varname String: name of the textbox. + * @param $value String: optional value (default empty) + * @param $size Integer: optional size of the textbox (default 20) */ function textbox( $varname, $value='', $size=20 ) { if ( $this->mRequest->wasPosted() ) { @@ -60,10 +57,10 @@ "
      \n"; } - /** - * @access private - * @param string $varname Name of the radiobox. - * @param array $fields Various fields. + /** + * @private + * @param $varname String: name of the radiobox. + * @param $fields Array: Various fields. */ function radiobox( $varname, $fields ) { foreach ( $fields as $value => $checked ) { @@ -71,35 +68,35 @@ ( $checked ? ' checked="checked"' : '' ) . " />" . wfMsg( $this->mName.'-'.$varname.'-'.$value ) . "\n"; } - return $this->fieldset( $this->mName.'-'.$varname, $s ); + return $this->fieldset( $varname, $s ); } - - /** - * @access private - * @param string $varname Name of the textareabox. - * @param string $value Optional value (default empty) - * @param integer $size Optional size of the textarea (default 20) + + /** + * @private + * @param $varname String: name of the textareabox. + * @param $value String: optional value (default empty) + * @param $size Integer: optional size of the textarea (default 20) */ function textareabox ( $varname, $value='', $size=20 ) { if ( $this->mRequest->wasPosted() ) { $value = $this->mRequest->getText( $varname, $value ); - } + } $value = htmlspecialchars( $value ); return '
      \n"; } - /** - * @access private - * @param string $varname Name of the arraybox. - * @param integer $size Optional size of the textarea (default 20) + /** + * @private + * @param $varname String: name of the arraybox. + * @param $size Integer: Optional size of the textarea (default 20) */ function arraybox( $varname , $size=20 ) { $s = ''; if ( $this->mRequest->wasPosted() ) { $arr = $this->mRequest->getArray( $varname ); if ( is_array( $arr ) ) { - foreach ( $_POST[$varname] as $index=>$element ) { + foreach ( $_POST[$varname] as $element ) { $s .= htmlspecialchars( $element )."\n"; } } @@ -108,68 +105,3 @@ "\n"; } } // end class - - -// functions used by SpecialUserrights.php and SpecialGroups.php - -/** Build a select with all defined groups - * @param string $selectname Name of this element. Name of form is automaticly prefixed. - * @param array $selected Array of element selected when posted. Only multiples will show them. - * @param boolean $multiple A multiple elements select. - * @param integer $size Number of elements to be shown ignored for non-multiple (default 6). - * @param boolean $reverse If true, multiple select will hide selected elements (default false). -*/ -function HTMLSelectGroups($selectname, $selectmsg, $selected=array(), $multiple=false, $size=6, $reverse=false) { - $groups = User::getAllGroups(); - $out = htmlspecialchars( wfMsg( $selectmsg ) ); - - if( $multiple ) { - $attribs = array( - 'name' => $selectname . '[]', - 'multiple'=> 'multiple', - 'size' => $size ); - } else { - $attribs = array( 'name' => $selectname ); - } - $out .= wfElement( 'select', $attribs, null ); - - foreach( $groups as $group ) { - $attribs = array( 'value' => $group ); - if( $multiple ) { - // for multiple will only show the things we want - if( !in_array( $group, $selected ) xor $reverse ) { - continue; - } - } else { - if( in_array( $group, $selected ) ) { - $attribs['selected'] = 'selected'; - } - } - $out .= wfElement( 'option', $attribs, User::getGroupName( $group ) ) . "\n"; - } - - $out .= "\n"; - return $out; -} - -/** Build a select with all existent rights - * @param array $selected Names(?) of user rights that should be selected. - * @return string HTML select. - */ -function HTMLSelectRights($selected='') { - global $wgAvailableRights; - $out = '\n"; - return $out; -} -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/HistoryBlob.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/HistoryBlob.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/HistoryBlob.php 2005-08-21 09:27:06.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/HistoryBlob.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,82 +1,86 @@ uncompress(); $this->mItems['meta'] = $metaData; } /** @todo document */ - function getMeta() { + public function getMeta() { $this->uncompress(); return $this->mItems['meta']; } /** @todo document */ - function addItem( $text ) { + public function addItem( $text ) { $this->uncompress(); $hash = md5( $text ); $this->mItems[$hash] = $text; @@ -87,7 +91,7 @@ } /** @todo document */ - function getItem( $hash ) { + public function getItem( $hash ) { $this->uncompress(); if ( array_key_exists( $hash, $this->mItems ) ) { return $this->mItems[$hash]; @@ -97,13 +101,29 @@ } /** @todo document */ - function removeItem( $hash ) { + public function setText( $text ) { + $this->uncompress(); + $stub = $this->addItem( $text ); + $this->mDefaultHash = $stub->mHash; + } + + /** @todo document */ + public function getText() { + $this->uncompress(); + return $this->getItem( $this->mDefaultHash ); + } + + # HistoryBlob implemented. + + + /** @todo document */ + public function removeItem( $hash ) { $this->mSize -= strlen( $this->mItems[$hash] ); unset( $this->mItems[$hash] ); } /** @todo document */ - function compress() { + public function compress() { if ( !$this->mCompressed ) { $this->mItems = gzdeflate( serialize( $this->mItems ) ); $this->mCompressed = true; @@ -111,25 +131,13 @@ } /** @todo document */ - function uncompress() { + public function uncompress() { if ( $this->mCompressed ) { $this->mItems = unserialize( gzinflate( $this->mItems ) ); $this->mCompressed = false; } } - /** @todo document */ - function getText() { - $this->uncompress(); - return $this->getItem( $this->mDefaultHash ); - } - - /** @todo document */ - function setText( $text ) { - $this->uncompress(); - $stub = $this->addItem( $text ); - $this->mDefaultHash = $stub->mHash; - } /** @todo document */ function __sleep() { @@ -145,7 +153,7 @@ /** * Determines if this object is happy */ - function isHappy( $maxFactor, $factorThreshold ) { + public function isHappy( $maxFactor, $factorThreshold ) { if ( count( $this->mItems ) == 0 ) { return true; } @@ -179,16 +187,16 @@ /** - * @package MediaWiki + * @todo document (needs one-sentence top-level class description + some function descriptions). */ class HistoryBlobStub { - var $mOldId, $mHash,$mRef; + var $mOldId, $mHash, $mRef; /** @todo document */ function HistoryBlobStub( $hash = '', $oldid = 0 ) { $this->mHash = $hash; } - + /** * Sets the location (old_id) of the main object to which this object * points @@ -213,31 +221,31 @@ /** @todo document */ function getText() { + $fname = 'HistoryBlobStub::getText'; global $wgBlobCache; if( isset( $wgBlobCache[$this->mOldId] ) ) { $obj = $wgBlobCache[$this->mOldId]; } else { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'text', array( 'old_flags', 'old_text' ), array( 'old_id' => $this->mOldId ) ); if( !$row ) { return false; } $flags = explode( ',', $row->old_flags ); if( in_array( 'external', $flags ) ) { - $url=$row->old_text; - @list($proto,$path)=explode('://',$url,2); - if ($path=="") { - wfProfileOut( $fname ); - return false; - } - require_once('ExternalStore.php'); - $row->old_text=ExternalStore::fetchFromUrl($url); + $url=$row->old_text; + @list( /* $proto */ ,$path)=explode('://',$url,2); + if ($path=="") { + wfProfileOut( $fname ); + return false; + } + $row->old_text=ExternalStore::fetchFromUrl($url); } if( !in_array( 'object', $flags ) ) { return false; } - + if( in_array( 'gzip', $flags ) ) { // This shouldn't happen, but a bug in the compress script // may at times gzip-compress a HistoryBlob object row. @@ -245,12 +253,12 @@ } else { $obj = unserialize( $row->old_text ); } - + if( !is_object( $obj ) ) { // Correct for old double-serialization bug. $obj = unserialize( $obj ); } - + // Save this item for reference; if pulling many // items in a row we'll likely use it again. $obj->uncompress(); @@ -273,8 +281,6 @@ * * Serialized HistoryBlobCurStub objects will be inserted into the text table * on conversion if $wgFastSchemaUpgrades is set to true. - * - * @package MediaWiki */ class HistoryBlobCurStub { var $mCurId; @@ -283,7 +289,7 @@ function HistoryBlobCurStub( $curid = 0 ) { $this->mCurId = $curid; } - + /** * Sets the location (cur_id) of the main object to which this object * points @@ -294,7 +300,7 @@ /** @todo document */ function getText() { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'cur', array( 'cur_text' ), array( 'cur_id' => $this->mCurId ) ); if( !$row ) { return false; @@ -304,4 +310,4 @@ } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Hooks.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Hooks.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Hooks.php 2005-10-12 02:33:20.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Hooks.php 2007-07-20 11:15:09.000000000 -0400 @@ -15,112 +15,129 @@ * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * * @author Evan Prodromou - * @package MediaWiki * @see hooks.txt */ -if (defined('MEDIAWIKI')) { - - /** - * Because programmers assign to $wgHooks, we need to be very - * careful about its contents. So, there's a lot more error-checking - * in here than would normally be necessary. - */ - - function wfRunHooks($event, $args = null) { - - global $wgHooks; - if (!is_array($wgHooks)) { - wfDebugDieBacktrace("Global hooks array is not an array!\n"); - return false; - } +/** + * Because programmers assign to $wgHooks, we need to be very + * careful about its contents. So, there's a lot more error-checking + * in here than would normally be necessary. + */ +function wfRunHooks($event, $args = null) { - if (!array_key_exists($event, $wgHooks)) { - return true; - } - - if (!is_array($wgHooks[$event])) { - wfDebugDieBacktrace("Hooks array for event '$event' is not an array!\n"); - return false; - } - - foreach ($wgHooks[$event] as $hook) { - - $object = NULL; - $method = NULL; - $func = NULL; - $data = NULL; - $have_data = false; - - /* $hook can be: a function, an object, an array of $function and $data, - * an array of just a function, an array of object and method, or an - * array of object, method, and data. - */ - - if (is_array($hook)) { - if (count($hook) < 1) { - wfDebugDieBacktrace("Empty array in hooks for " . $event . "\n"); - } else if (is_object($hook[0])) { - $object = $hook[0]; - if (count($hook) < 2) { - $method = "on" . $event; - } else { - $method = $hook[1]; - if (count($hook) > 2) { - $data = $hook[2]; - $have_data = true; - } - } - } else if (is_string($hook[0])) { - $func = $hook[0]; - if (count($hook) > 1) { - $data = $hook[1]; + global $wgHooks; + + if (!is_array($wgHooks)) { + throw new MWException("Global hooks array is not an array!\n"); + return false; + } + + if (!array_key_exists($event, $wgHooks)) { + return true; + } + + if (!is_array($wgHooks[$event])) { + throw new MWException("Hooks array for event '$event' is not an array!\n"); + return false; + } + + foreach ($wgHooks[$event] as $index => $hook) { + + $object = NULL; + $method = NULL; + $func = NULL; + $data = NULL; + $have_data = false; + + /* $hook can be: a function, an object, an array of $function and $data, + * an array of just a function, an array of object and method, or an + * array of object, method, and data. + */ + + if (is_array($hook)) { + if (count($hook) < 1) { + throw new MWException("Empty array in hooks for " . $event . "\n"); + } else if (is_object($hook[0])) { + $object = $wgHooks[$event][$index][0]; + if (count($hook) < 2) { + $method = "on" . $event; + } else { + $method = $hook[1]; + if (count($hook) > 2) { + $data = $hook[2]; $have_data = true; } - } else { - wfDebugDieBacktrace("Unknown datatype in hooks for " . $event . "\n"); } - } else if (is_string($hook)) { # functions look like strings, too - $func = $hook; - } else if (is_object($hook)) { - $object = $hook; - $method = "on" . $event; - } else { - wfDebugDieBacktrace("Unknown datatype in hooks for " . $event . "\n"); - } - - /* We put the first data element on, if needed. */ - - if ($have_data) { - $hook_args = array_merge(array($data), $args); + } else if (is_string($hook[0])) { + $func = $hook[0]; + if (count($hook) > 1) { + $data = $hook[1]; + $have_data = true; + } } else { - $hook_args = $args; + var_dump( $wgHooks ); + throw new MWException("Unknown datatype in hooks for " . $event . "\n"); } - - /* Call the hook. */ - - if ($object) { - $retval = call_user_func_array(array($object, $method), $hook_args); + } else if (is_string($hook)) { # functions look like strings, too + $func = $hook; + } else if (is_object($hook)) { + $object = $wgHooks[$event][$index]; + $method = "on" . $event; + } else { + throw new MWException("Unknown datatype in hooks for " . $event . "\n"); + } + + /* We put the first data element on, if needed. */ + + if ($have_data) { + $hook_args = array_merge(array($data), $args); + } else { + $hook_args = $args; + } + + if ( isset( $object ) ) { + $func = get_class( $object ) . '::' . $method; + $callback = array( $object, $method ); + } elseif ( false !== ( $pos = strpos( $func, '::' ) ) ) { + $callback = array( substr( $func, 0, $pos ), substr( $func, $pos + 2 ) ); + } else { + $callback = $func; + } + + /* Call the hook. */ + wfProfileIn( $func ); + $retval = call_user_func_array( $callback, $hook_args ); + wfProfileOut( $func ); + + /* String return is an error; false return means stop processing. */ + + if (is_string($retval)) { + global $wgOut; + $wgOut->showFatalError($retval); + return false; + } elseif( $retval === null ) { + if( is_array( $callback ) ) { + if( is_object( $callback[0] ) ) { + $prettyClass = get_class( $callback[0] ); + } else { + $prettyClass = strval( $callback[0] ); + } + $prettyFunc = $prettyClass . '::' . strval( $callback[1] ); } else { - $retval = call_user_func_array($func, $hook_args); - } - - /* String return is an error; false return means stop processing. */ - - if (is_string($retval)) { - global $wgOut; - $wgOut->fatalError($retval); - return false; - } else if (!$retval) { - return false; + $prettyFunc = strval( $callback ); } + throw new MWException( "Detected bug in an extension! " . + "Hook $prettyFunc failed to return a value; " . + "should return true to continue hook processing or false to abort." ); + } else if (!$retval) { + return false; } - - return true; } -} /* if defined(MEDIAWIKI) */ -?> + + return true; +} + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/HttpFunctions.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/HttpFunctions.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/HttpFunctions.php 2005-07-05 17:22:08.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/HttpFunctions.php 2007-08-31 10:53:17.000000000 -0400 @@ -1,69 +1,121 @@ getFullURL() ); + } - if ( $timeout == 'default' ) { - $timeout = $wgHTTPTimeout; + ob_start(); + curl_exec( $c ); + $text = ob_get_contents(); + ob_end_clean(); + + # Don't return the text of error messages, return false on error + if ( curl_getinfo( $c, CURLINFO_HTTP_CODE ) != 200 ) { + $text = false; + } + # Don't return truncated output + if ( curl_errno( $c ) != CURLE_OK ) { + $text = false; + } + curl_close( $c ); + } else { + # Otherwise use file_get_contents... + # This may take 3 minutes to time out, and doesn't have local fetch capabilities + + global $wgVersion; + $headers = array( "User-Agent: MediaWiki/$wgVersion" ); + if( strcasecmp( $method, 'post' ) == 0 ) { + // Required for HTTP 1.0 POSTs + $headers[] = "Content-Length: 0"; + } + $opts = array( + 'http' => array( + 'method' => $method, + 'header' => implode( "\r\n", $headers ) ) ); + $ctx = stream_context_create($opts); + + $url_fopen = ini_set( 'allow_url_fopen', 1 ); + $text = file_get_contents( $url, false, $ctx ); + ini_set( 'allow_url_fopen', $url_fopen ); } - curl_setopt( $c, CURLOPT_TIMEOUT, $timeout ); - ob_start(); - curl_exec( $c ); - $text = ob_get_contents(); - ob_end_clean(); - curl_close( $c ); - } else { - # Otherwise use file_get_contents, or its compatibility function from GlobalFunctions.php - # This may take 3 minutes to time out, and doesn't have local fetch capabilities - $url_fopen = ini_set( 'allow_url_fopen', 1 ); - $text = file_get_contents( $url ); - ini_set( 'allow_url_fopen', $url_fopen ); + return $text; } - return $text; -} -/** - * Check if the URL can be served by localhost - */ -function wfIsLocalURL( $url ) { - global $wgConf; - // Extract host part - if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) { - $host = $matches[1]; - // Split up dotwise - $domainParts = explode( '.', $host ); - // Check if this domain or any superdomain is listed in $wgConf as a local virtual host - $domainParts = array_reverse( $domainParts ); - for ( $i = 0; $i < count( $domainParts ); $i++ ) { - $domainPart = $domainParts[$i]; - if ( $i == 0 ) { - $domain = $domainPart; - } else { - $domain = $domainPart . '.' . $domain; - } - if ( $wgConf->isLocalVHost( $domain ) ) { - return true; + /** + * Check if the URL can be served by localhost + */ + static function isLocalURL( $url ) { + global $wgCommandLineMode, $wgConf; + if ( $wgCommandLineMode ) { + return false; + } + + // Extract host part + $matches = array(); + if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) { + $host = $matches[1]; + // Split up dotwise + $domainParts = explode( '.', $host ); + // Check if this domain or any superdomain is listed in $wgConf as a local virtual host + $domainParts = array_reverse( $domainParts ); + for ( $i = 0; $i < count( $domainParts ); $i++ ) { + $domainPart = $domainParts[$i]; + if ( $i == 0 ) { + $domain = $domainPart; + } else { + $domain = $domainPart . '.' . $domain; + } + if ( $wgConf->isLocalVHost( $domain ) ) { + return true; + } } } + return false; } - return false; } -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ImageGallery.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ImageGallery.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ImageGallery.php 2005-04-16 00:33:34.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ImageGallery.php 2007-08-30 22:51:23.000000000 -0400 @@ -1,52 +1,172 @@ mImages = array(); $this->mShowBytes = true; $this->mShowFilename = true; + $this->mParser = false; + $this->mHideBadImages = false; + } + + /** + * Register a parser object + */ + function setParser( $parser ) { + $this->mParser = $parser; + } + + /** + * Set bad image flag + */ + function setHideBadImages( $flag = true ) { + $this->mHideBadImages = $flag; + } + + /** + * Set the caption (as plain text) + * + * @param $caption Caption + */ + function setCaption( $caption ) { + $this->mCaption = htmlspecialchars( $caption ); + } + + /** + * Set the caption (as HTML) + * + * @param $caption Caption + */ + public function setCaptionHtml( $caption ) { + $this->mCaption = $caption; + } + + /** + * Set how many images will be displayed per row. + * + * @param int $num > 0; invalid numbers will be rejected + */ + public function setPerRow( $num ) { + if ($num > 0) { + $this->mPerRow = (int)$num; + } + } + + /** + * Set how wide each image will be, in pixels. + * + * @param int $num > 0; invalid numbers will be ignored + */ + public function setWidths( $num ) { + if ($num > 0) { + $this->mWidths = (int)$num; + } + } + + /** + * Set how high each image will be, in pixels. + * + * @param int $num > 0; invalid numbers will be ignored + */ + public function setHeights( $num ) { + if ($num > 0) { + $this->mHeights = (int)$num; + } + } + + /** + * Instruct the class to use a specific skin for rendering + * + * @param $skin Skin object + */ + function useSkin( $skin ) { + $this->mSkin = $skin; + } + + /** + * Return the skin that should be used + * + * @return Skin object + */ + function getSkin() { + if( !$this->mSkin ) { + global $wgUser; + $skin = $wgUser->getSkin(); + } else { + $skin = $this->mSkin; + } + return $skin; } /** * Add an image to the gallery. * - * @param Image $image Image object that is added to the gallery - * @param string $html Additional HTML text to be shown. The name and size of the image are always shown. + * @param $title Title object of the image that is added to the gallery + * @param $html String: additional HTML text to be shown. The name and size of the image are always shown. */ - function add( $image, $html='' ) { - $this->mImages[] = array( &$image, $html ); + function add( $title, $html='' ) { + if ( $title instanceof File ) { + // Old calling convention + $title = $title->getTitle(); + } + $this->mImages[] = array( $title, $html ); + wfDebug( "ImageGallery::add " . $title->getText() . "\n" ); } /** * Add an image at the beginning of the gallery. * - * @param Image $image Image object that is added to the gallery - * @param string $html Additional HTML text to be shown. The name and size of the image are always shown. + * @param $title Title object of the image that is added to the gallery + * @param $html String: Additional HTML text to be shown. The name and size of the image are always shown. */ - function insert( $image, $html='' ) { - array_unshift( $this->mImages, array( &$image, $html ) ); + function insert( $title, $html='' ) { + if ( $title instanceof File ) { + // Old calling convention + $title = $title->getTitle(); + } + array_unshift( $this->mImages, array( &$title, $html ) ); } @@ -60,8 +180,8 @@ /** * Enable/Disable showing of the file size of an image in the gallery. * Enabled by default. - * - * @param boolean $f set to false to disable + * + * @param $f Boolean: set to false to disable. */ function setShowBytes( $f ) { $this->mShowBytes = ( $f == true); @@ -70,16 +190,29 @@ /** * Enable/Disable showing of the filename of an image in the gallery. * Enabled by default. - * - * @param boolean $f set to false to disable + * + * @param $f Boolean: set to false to disable. */ function setShowFilename( $f ) { $this->mShowFilename = ( $f == true); } + + /** + * Set arbitrary attributes to go on the HTML gallery output element. + * Should be suitable for a <table> element. + * + * Note -- if taking from user input, you should probably run through + * Sanitizer::validateAttributes() first. + * + * @param array of HTML attribute pairs + */ + function setAttributes( $attribs ) { + $this->mAttribs = $attribs; + } /** * Return a HTML representation of the image gallery - * + * * For each image in the gallery, display * - a thumbnail * - the image name @@ -88,73 +221,134 @@ * */ function toHTML() { - global $wgLang, $wgContLang, $wgUser; + global $wgLang; + + $sk = $this->getSkin(); - $sk = $wgUser->getSkin(); + $attribs = Sanitizer::mergeAttributes( + array( + 'class' => 'gallery', + 'cellspacing' => '0', + 'cellpadding' => '0' ), + $this->mAttribs ); + $s = Xml::openElement( 'table', $attribs ); + if( $this->mCaption ) + $s .= "\n\t{$this->mCaption}"; - $s = ''; + $params = array( 'width' => $this->mWidths, 'height' => $this->mHeights ); $i = 0; foreach ( $this->mImages as $pair ) { - $img =& $pair[0]; + $nt = $pair[0]; $text = $pair[1]; - - $name = $img->getName(); - $nt = $img->getTitle(); - - // Not an image. Just print the name and skip. - if ( $nt->getNamespace() != NS_IMAGE ) { - $s .= '' . (($i%4==3) ? "\n" : ''); - $i++; - - continue; + + # Give extensions a chance to select the file revision for us + $time = false; + wfRunHooks( 'BeforeGalleryFindFile', array( &$this, &$nt, &$time ) ); + + $img = wfFindFile( $nt, $time ); + + if( $nt->getNamespace() != NS_IMAGE || !$img ) { + # We're dealing with a non-image, spit out the name and be done with it. + $thumbhtml = "\n\t\t\t".'
      ' + . htmlspecialchars( $nt->getText() ) . '
      '; + } elseif( $this->mHideBadImages && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() ) ) { + # The image is blacklisted, just show it as a text link. + $thumbhtml = "\n\t\t\t".'
      ' + . $sk->makeKnownLinkObj( $nt, htmlspecialchars( $nt->getText() ) ) . '
      '; + } elseif( !( $thumb = $img->transform( $params ) ) ) { + # Error generating thumbnail. + $thumbhtml = "\n\t\t\t".'
      ' + . htmlspecialchars( $img->getLastError() ) . '
      '; + } else { + $vpad = floor( ( 1.25*$this->mHeights - $thumb->height ) /2 ) - 2; + + $thumbhtml = "\n\t\t\t". + '
      ' + # Auto-margin centering for block-level elements. Needed now that we have video + # handlers since they may emit block-level elements as opposed to simple tags. + # ref http://css-discuss.incutio.com/?page=CenteringBlockElement + . '
      ' + . $thumb->toHtml( array( 'desc-link' => true ) ) . '
      '; + + // Call parser transform hook + if ( $this->mParser && $img->getHandler() ) { + $img->getHandler()->parserTransformHook( $this->mParser, $img ); + } } //TODO //$ul = $sk->makeLink( $wgContLang->getNsText( Namespace::getUser() ) . ":{$ut}", $ut ); if( $this->mShowBytes ) { - if( $img->exists() ) { - $nb = wfMsg( 'nbytes', $wgLang->formatNum( $img->getSize() ) ); + if( $img ) { + $nb = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'), + $wgLang->formatNum( $img->getSize() ) ); } else { - $nb = wfMsg( 'filemissing' ); + $nb = wfMsgHtml( 'filemissing' ); } - $nb = htmlspecialchars( $nb ) . "
      \n"; + $nb = "$nb
      \n"; } else { $nb = ''; } - + $textlink = $this->mShowFilename ? $sk->makeKnownLinkObj( $nt, htmlspecialchars( $wgLang->truncate( $nt->getText(), 20, '...' ) ) ) . "
      \n" : '' ; - $s .= ($i%4==0) ? '' : ''; - $thumb = $img->getThumbnail( 120, 120 ); - $vpad = floor( ( 150 - $thumb->height ) /2 ) - 2; - $s .= '\n"; - $s .= ($i%4==3) ? '' : ''; - $i++; + # http://bugzilla.wikimedia.org/show_bug.cgi?id=1765 -Ævar + + if ( $i % $this->mPerRow == 0 ) { + $s .= "\n\t"; + } + $s .= + "\n\t\t" . '"; + if ( $i % $this->mPerRow == $this->mPerRow - 1 ) { + $s .= "\n\t"; + } + ++$i; } - if( $i %4 != 0 ) { - $s .= "\n"; + if( $i % $this->mPerRow != 0 ) { + $s .= "\n\t"; } - $s .= ''; + $s .= "\n"; return $s; } -} //class - + /** + * @return int Number of images in the gallery + */ + public function count() { + return count( $this->mImages ); + } + + /** + * Set the contextual title + * + * @param Title $title Contextual title + */ + public function setContextTitle( $title ) { + $this->contextTitle = $title; + } + + /** + * Get the contextual title, if applicable + * + * @return mixed Title or false + */ + public function getContextTitle() { + return is_object( $this->contextTitle ) && $this->contextTitle instanceof Title + ? $this->contextTitle + : false; + } +} //class -} // defined( 'MEDIAWIKI' ) -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ImagePage.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ImagePage.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ImagePage.php 2005-11-16 03:28:13.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ImagePage.php 2007-08-30 22:51:23.000000000 -0400 @@ -1,76 +1,98 @@ img = wfFindFile( $this->mTitle ); + if ( !$this->img ) { + $this->img = wfLocalFile( $this->mTitle ); + } + $this->repo = $this->img->repo; + } + + /** + * Handler for action=render + * Include body text only; none of the image extras + */ function render() { global $wgOut; - $wgOut->setArticleBodyOnly(true); - $wgOut->addWikitext($this->getContent(true)); + $wgOut->setArticleBodyOnly( true ); + $wgOut->addSecondaryWikitext( $this->getContent() ); } function view() { - global $wgUseExternalEditor, $wgOut, $wgShowEXIF; + global $wgOut, $wgShowEXIF, $wgRequest, $wgUser; - $this->img = new Image( $this->mTitle ); + $diff = $wgRequest->getVal( 'diff' ); + $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); - if( $this->mTitle->getNamespace() == NS_IMAGE ) { - if ($wgShowEXIF && $this->img->exists()) { - $exif = $this->img->getExifData(); - $showmeta = count($exif) ? true : false; - } else { - $exif = false; - $showmeta = false; - } + if ( $this->mTitle->getNamespace() != NS_IMAGE || ( isset( $diff ) && $diffOnly ) ) + return Article::view(); - if ($this->img->exists()) - $wgOut->addHTML($this->showTOC($showmeta)); + if ($wgShowEXIF && $this->img->exists()) { + // FIXME: bad interface, see note on MediaHandler::formatMetadata(). + $formattedMetadata = $this->img->formatMetadata(); + $showmeta = $formattedMetadata !== false; + } else { + $showmeta = false; + } - $this->openShowImage(); - if ($exif) - $wgOut->addWikiText($this->makeMetadataTable($exif)); - - # No need to display noarticletext, we use our own message, output in openShowImage() - if( $this->getID() ) { - Article::view(); - } else { - # Just need to set the right headers - $wgOut->setArticleFlag( true ); - $wgOut->setRobotpolicy( 'index,follow' ); - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $wgOut->addMetaTags(); - $this->viewUpdates(); - } + if ($this->img->exists()) + $wgOut->addHTML($this->showTOC($showmeta)); - if ($this->mExtraDescription) { - $fol = wfMsg('shareddescriptionfollows'); - if ($fol != '-') - $wgOut->addWikiText(wfMsg('shareddescriptionfollows')); - $wgOut->addHTML($this->mExtraDescription); - } + $this->openShowImage(); - $this->closeShowImage(); - $this->imageHistory(); - $this->imageLinks(); - } else { + # No need to display noarticletext, we use our own message, output in openShowImage() + if ( $this->getID() ) { Article::view(); + } else { + # Just need to set the right headers + $wgOut->setArticleFlag( true ); + $wgOut->setRobotpolicy( 'index,follow' ); + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + $this->viewUpdates(); + } + + # Show shared description, if needed + if ( $this->mExtraDescription ) { + $fol = wfMsg( 'shareddescriptionfollows' ); + if( $fol != '-' && !wfEmptyMsg( 'shareddescriptionfollows', $fol ) ) { + $wgOut->addWikiText( $fol ); + } + $wgOut->addHTML( '
      ' . $this->mExtraDescription . '
      ' ); + } + + $this->closeShowImage(); + $this->imageHistory(); + $this->imageLinks(); + + if ( $showmeta ) { + global $wgStylePath, $wgStyleVersion; + $expand = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-expand' ) ) ); + $collapse = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-collapse' ) ) ); + $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ). "\n" ); + $wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) ); + $wgOut->addHTML( + "\n" . + "\n" ); } } @@ -85,10 +107,10 @@ function showTOC( $metadata ) { global $wgLang; $r = ''; return $r; } @@ -96,53 +118,59 @@ /** * Make a table with metadata to be shown in the output page. * + * FIXME: bad interface, see note on MediaHandler::formatMetadata(). + * * @access private * * @param array $exif The array containing the EXIF data * @return string */ - function makeMetadataTable( $exif ) { - $r = "{| class=metadata align=right width=250px\n"; - $r .= '|+ id=metadata | '. htmlspecialchars( wfMsg( 'metadata' ) ) . "\n"; - foreach( $exif as $k => $v ) { - $tag = strtolower( $k ); - $r .= "! class=$tag |" . wfMsg( "exif-$tag" ) . "\n"; - $r .= "| class=$tag |" . htmlspecialchars( $v ) . "\n"; - $r .= "|-\n"; + function makeMetadataTable( $metadata ) { + $r = wfMsg( 'metadata-help' ) . "\n\n"; + $r .= "{| id=mw_metadata class=mw_metadata\n"; + foreach ( $metadata as $type => $stuff ) { + foreach ( $stuff as $v ) { + $class = Sanitizer::escapeId( $v['id'] ); + if( $type == 'collapsed' ) { + $class .= ' collapsable'; + } + $r .= "|- class=\"$class\"\n"; + $r .= "!| {$v['name']}\n"; + $r .= "|| {$v['value']}\n"; + } } - return substr($r, 0, -3) . '|}'; + $r .= '|}'; + return $r; } /** * Overloading Article's getContent method. - * Omit noarticletext if sharedupload - * - * @param $noredir If true, do not follow redirects + * + * Omit noarticletext if sharedupload; text will be fetched from the + * shared upload server if possible. */ - function getContent( $noredir ) - { - if ( $this->img && $this->img->fromSharedDirectory && 0 == $this->getID() ) { + function getContent() { + if( $this->img && !$this->img->isLocal() && 0 == $this->getID() ) { return ''; } - return Article::getContent( $noredir ); + return Article::getContent(); } - function openShowImage() - { - global $wgOut, $wgUser, $wgImageLimits, $wgRequest, - $wgUseImageResize, $wgRepositoryBaseUrl, - $wgUseExternalEditor, $wgServer, $wgFetchCommonsDescriptions; - $full_url = $this->img->getURL(); - $anchoropen = ''; - $anchorclose = ''; + function openShowImage() { + global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang; - if( $wgUser->getOption( 'imagesize' ) == '' ) { - $sizeSel = User::getDefaultOption( 'imagesize' ); - } else { - $sizeSel = IntVal( $wgUser->getOption( 'imagesize' ) ); - } + $full_url = $this->img->getURL(); + $linkAttribs = false; + $sizeSel = intval( $wgUser->getOption( 'imagesize') ); if( !isset( $wgImageLimits[$sizeSel] ) ) { $sizeSel = User::getDefaultOption( 'imagesize' ); + + // The user offset might still be incorrect, specially if + // $wgImageLimits got changed (see bug #8858). + if( !isset( $wgImageLimits[$sizeSel] ) ) { + // Default to the first offset in $wgImageLimits + $sizeSel = 0; + } } $max = $wgImageLimits[$sizeSel]; $maxWidth = $max[0]; @@ -151,143 +179,221 @@ if ( $this->img->exists() ) { # image - $width = $this->img->getWidth(); - $height = $this->img->getHeight(); + $page = $wgRequest->getIntOrNull( 'page' ); + if ( is_null( $page ) ) { + $params = array(); + $page = 1; + } else { + $params = array( 'page' => $page ); + } + $width_orig = $this->img->getWidth(); + $width = $width_orig; + $height_orig = $this->img->getHeight(); + $height = $height_orig; + $mime = $this->img->getMimeType(); $showLink = false; + $linkAttribs = array( 'href' => $full_url ); + $longDesc = $this->img->getLongDesc(); + + wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this , &$wgOut ) ) ; - if ( $this->img->allowInlineDisplay() and $width and $height) { + if ( $this->img->allowInlineDisplay() ) { # image # "Download high res version" link below the image - $msg = wfMsg('showbigimage', $width, $height, intval( $this->img->getSize()/1024 ) ); - if ( $width > $maxWidth ) { - $height = floor( $height * $maxWidth / $width ); - $width = $maxWidth; + #$msgsize = wfMsgHtml('file-info-size', $width_orig, $height_orig, $sk->formatSize( $this->img->getSize() ), $mime ); + # We'll show a thumbnail of this image + if ( $width > $maxWidth || $height > $maxHeight ) { + # Calculate the thumbnail size. + # First case, the limiting factor is the width, not the height. + if ( $width / $height >= $maxWidth / $maxHeight ) { + $height = round( $height * $maxWidth / $width); + $width = $maxWidth; + # Note that $height <= $maxHeight now. + } else { + $newwidth = floor( $width * $maxHeight / $height); + $height = round( $height * $newwidth / $width ); + $width = $newwidth; + # Note that $height <= $maxHeight now, but might not be identical + # because of rounding. + } + $msgbig = wfMsgHtml( 'show-big-image' ); + $msgsmall = wfMsgExt( 'show-big-image-thumb', + array( 'parseinline' ), $width, $height ); + } else { + # Image is small enough to show full size on image page + $msgbig = htmlspecialchars( $this->img->getName() ); + $msgsmall = wfMsgExt( 'file-nohires', array( 'parseinline' ) ); + } + + $params['width'] = $width; + $thumbnail = $this->img->transform( $params ); + + $anchorclose = "
      "; + if( $this->img->mustRender() ) { + $showLink = true; + } else { + $anchorclose .= + $msgsmall . + '
      ' . Xml::tags( 'a', $linkAttribs, $msgbig ) . ' ' . $longDesc; } - if ( $height > $maxHeight ) { - $width = floor( $width * $maxHeight / $height ); - $height = $maxHeight; + + if ( $this->img->isMultipage() ) { + $wgOut->addHTML( '
      ' ); } - if ( $width != $this->img->getWidth() || $height != $this->img->getHeight() ) { - if( $wgUseImageResize ) { - $thumbnail = $this->img->getThumbnail( $width ); - if ( $thumbnail == null ) { - $url = $this->img->getViewURL(); - } else { - $url = $thumbnail->getURL(); - } + + if ( $thumbnail ) { + $options = array( + 'alt' => $this->img->getTitle()->getPrefixedText(), + 'file-link' => true, + ); + $wgOut->addHTML( '' ); + } + + if ( $this->img->isMultipage() ) { + $count = $this->img->pageCount(); + + if ( $page > 1 ) { + $label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false ); + $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page-1) ); + $thumb1 = $sk->makeThumbLinkObj( $this->mTitle, $this->img, $link, $label, 'none', + array( 'page' => $page - 1 ) ); } else { - # No resize ability? Show the full image, but scale - # it down in the browser so it fits on the page. - $url = $this->img->getViewURL(); + $thumb1 = ''; } - $anchoropen = ""; - $anchorclose = "
      "; - if( $this->img->mustRender() ) { - $showLink = true; + + if ( $page < $count ) { + $label = wfMsg( 'imgmultipagenext' ); + $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page+1) ); + $thumb2 = $sk->makeThumbLinkObj( $this->mTitle, $this->img, $link, $label, 'none', + array( 'page' => $page + 1 ) ); } else { - $anchorclose .= "\n$anchoropen{$msg}"; + $thumb2 = ''; } - } else { - $url = $this->img->getViewURL(); - $showLink = true; + + global $wgScript; + $select = '
      ' . + Xml::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ); + $select .= $wgOut->parse( wfMsg( 'imgmultigotopre' ), false ) . + ' ' . $wgOut->parse( wfMsg( 'imgmultigotopost' ), false ) . + '
      '; + + $wgOut->addHTML( '
      ' . + "$select
      $thumb1\n$thumb2
      " ); } - $wgOut->addHTML( '' ); } else { #if direct link is allowed but it's not a renderable image, show an icon. if ($this->img->isSafeFile()) { $icon= $this->img->iconThumb(); - $wgOut->addHTML( '' ); + $wgOut->addHTML( '' ); } - + $showLink = true; } if ($showLink) { $filename = wfEscapeWikiText( $this->img->getName() ); - $info = wfMsg( 'fileinfo', - ceil($this->img->getSize()/1024.0), - $this->img->getMimeType() ); - + + global $wgContLang; + $dirmark = $wgContLang->getDirMark(); if (!$this->img->isSafeFile()) { $warning = wfMsg( 'mediawarning' ); - $wgOut->addWikiText( <<addWikiText( << -[[Media:$filename|$filename]] - ($info) +[[Media:$filename|$filename]]$dirmark + $longDesc
      $warning
      -END +EOT ); } else { - $wgOut->addWikiText( <<addWikiText( << -[[Media:$filename|$filename]] ($info) +[[Media:$filename|$filename]]$dirmark $longDesc -END +EOT ); } } - if($this->img->fromSharedDirectory) { + if(!$this->img->isLocal()) { $this->printSharedImageText(); } } else { # Image does not exist - $wgOut->addWikiText( wfMsg( 'noimage', $this->getUploadUrl() ) ); + + $title = SpecialPage::getTitleFor( 'Upload' ); + $link = $sk->makeKnownLinkObj($title, wfMsgHtml('noimage-linktext'), + 'wpDestFile=' . urlencode( $this->img->getName() ) ); + $wgOut->addHTML( wfMsgWikiHtml( 'noimage', $link ) ); } } function printSharedImageText() { - global $wgRepositoryBaseUrl, $wgFetchCommonsDescriptions, $wgOut; + global $wgOut, $wgUser; - $url = $wgRepositoryBaseUrl . urlencode($this->mTitle->getDBkey()); - $sharedtext = "
      " . wfMsg("sharedupload"); - if ($wgRepositoryBaseUrl && !$wgFetchCommonsDescriptions) { - $sharedtext .= " " . wfMsg("shareduploadwiki", $url); - } - $sharedtext .= "
      "; - $wgOut->addWikiText($sharedtext); - - if ($wgRepositoryBaseUrl && $wgFetchCommonsDescriptions) { - require_once("HttpFunctions.php"); - $ur = ini_set('allow_url_fopen', true); - $text = wfGetHTTP($url . '?action=render'); - ini_set('allow_url_fopen', $ur); - if ($text) - $this->mExtraDescription = $text; + $descUrl = $this->img->getDescriptionUrl(); + $descText = $this->img->getDescriptionText(); + $s = "
      " . wfMsgWikiHtml("sharedupload"); + if ( $descUrl && !$descText) { + $sk = $wgUser->getSkin(); + $link = $sk->makeExternalLink( $descUrl, wfMsg('shareduploadwiki-linktext') ); + $s .= " " . wfMsgWikiHtml('shareduploadwiki', $link); + } + $s .= "
      "; + $wgOut->addHTML($s); + + if ( $descText ) { + $this->mExtraDescription = $descText; } } function getUploadUrl() { global $wgServer; - $uploadTitle = Title::makeTitle( NS_SPECIAL, 'Upload' ); + $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); return $wgServer . $uploadTitle->getLocalUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) ); } - - function uploadLinksBox() - { + /** + * Print out the various links at the bottom of the image page, e.g. reupload, + * external editing (and instructions link) etc. + */ + function uploadLinksBox() { global $wgUser, $wgOut; - if ($this->img->fromSharedDirectory) + if( !$this->img->isLocal() ) return; $sk = $wgUser->getSkin(); - $wgOut->addHTML( '
      • ' ); - $wgOut->addWikiText( '
        '. wfMsg( 'uploadnewversion', $this->getUploadUrl() ) .'
        ' ); - $wgOut->addHTML( '
      • ' ); - $wgOut->addHTML( $sk->makeKnownLinkObj( $this->mTitle, - wfMsg( 'edit-externally' ), "action=edit&externaledit=true&mode=file" ) ); - $wgOut->addWikiText( '
        ' . wfMsg('edit-externally-help') . '
        ' ); - $wgOut->addHTML( '
      ' ); + + $wgOut->addHtml( '
        ' ); + + # "Upload a new version of this file" link + if( UploadForm::userCanReUpload($wgUser,$this->img->name) ) { + $ulink = $sk->makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) ); + $wgOut->addHtml( "
      • " ); + } + + # External editing link + $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' ); + $wgOut->addHtml( '
      • ' . $elink . '
        ' . wfMsgWikiHtml( 'edit-externally-help' ) . '
      • ' ); + + $wgOut->addHtml( '
      ' ); } function closeShowImage() @@ -309,21 +415,31 @@ $line = $this->img->nextHistoryLine(); if ( $line ) { - $list =& new ImageHistoryList( $sk ); + $list = new ImageHistoryList( $sk, $this->img ); + $file = $this->repo->newFileFromRow( $line ); + $dims = $file->getDimensionsString(); $s = $list->beginImageHistoryList() . - $list->imageHistoryLine( true, $line->img_timestamp, + $list->imageHistoryLine( true, wfTimestamp(TS_MW, $line->img_timestamp), $this->mTitle->getDBkey(), $line->img_user, - $line->img_user_text, $line->img_size, $line->img_description ); + $line->img_user_text, $line->img_size, $line->img_description, + $dims + ); while ( $line = $this->img->nextHistoryLine() ) { - $s .= $list->imageHistoryLine( false, $line->img_timestamp, - $line->oi_archive_name, $line->img_user, - $line->img_user_text, $line->img_size, $line->img_description ); + $file = $this->repo->newFileFromRow( $line ); + $dims = $file->getDimensionsString(); + $s .= $list->imageHistoryLine( false, $line->oi_timestamp, + $line->oi_archive_name, $line->oi_user, + $line->oi_user_text, $line->oi_size, $line->oi_description, + $dims + ); } $s .= $list->endImageHistoryList(); } else { $s=''; } $wgOut->addHTML( $s ); + $this->img->resetHistory(); // free db resources + # Exist check because we don't want to show this on pages where an image # doesn't exist along with the noimage message, that would suck. -ævar if( $wgUseExternalEditor && $this->img->exists() ) { @@ -336,15 +452,15 @@ { global $wgUser, $wgOut; - $wgOut->addHTML( '\n" ); + $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'filelinks' ), wfMsg( 'imagelinks' ) ) . "\n" ); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $page = $dbr->tableName( 'page' ); $imagelinks = $dbr->tableName( 'imagelinks' ); $sql = "SELECT page_namespace,page_title FROM $imagelinks,$page WHERE il_to=" . - $dbr->addQuotes( $this->mTitle->getDBkey() ) . " AND il_from=page_id" - . " LIMIT 500"; # quickie emergency brake + $dbr->addQuotes( $this->mTitle->getDBkey() ) . " AND il_from=page_id"; + $sql = $dbr->limitResult($sql, 500, 0); $res = $dbr->query( $sql, "ImagePage::imageLinks" ); if ( 0 == $dbr->numRows( $res ) ) { @@ -362,332 +478,157 @@ $wgOut->addHTML( "
    \n" ); } - function delete() - { - global $wgUser, $wgOut, $wgRequest; - - $confirm = $wgRequest->wasPosted(); - $image = $wgRequest->getVal( 'image' ); - $oldimage = $wgRequest->getVal( 'oldimage' ); - - # Only sysops can delete images. Previously ordinary users could delete - # old revisions, but this is no longer the case. - if ( !$wgUser->isAllowed('delete') ) { - $wgOut->sysopRequired(); - return; - } - if ( $wgUser->isBlocked() ) { - return $this->blockedIPpage(); - } - if ( wfReadOnly() ) { - $wgOut->readOnlyPage(); - return; - } - - # Better double-check that it hasn't been deleted yet! - $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) ); - if ( ( !is_null( $image ) ) - && ( '' == trim( $image ) ) ) { - $wgOut->fatalError( wfMsg( 'cannotdelete' ) ); - return; - } - - $this->img = new Image( $this->mTitle ); - - # Deleting old images doesn't require confirmation - if ( !is_null( $oldimage ) || $confirm ) { - if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) { - $this->doDelete(); - } else { - $wgOut->fatalError( wfMsg( 'sessionfailure' ) ); - } + /** + * Delete the file, or an earlier version of it + */ + public function delete() { + if( !$this->img->exists() || !$this->img->isLocal() ) { + // Standard article deletion + Article::delete(); return; } - - if ( !is_null( $image ) ) { - $q = '&image=' . urlencode( $image ); - } else if ( !is_null( $oldimage ) ) { - $q = '&oldimage=' . urlencode( $oldimage ); - } else { - $q = ''; - } - return $this->confirmDelete( $q, $wgRequest->getText( 'wpReason' ) ); + $deleter = new FileDeleteForm( $this->img ); + $deleter->execute(); } - function doDelete() - { - global $wgOut, $wgUser, $wgContLang, $wgRequest; - global $wgUseSquid, $wgInternalServer, $wgPostCommitUpdateList; - $fname = 'ImagePage::doDelete'; - - $reason = $wgRequest->getVal( 'wpReason' ); - $oldimage = $wgRequest->getVal( 'oldimage' ); - - $dbw =& wfGetDB( DB_MASTER ); - - if ( !is_null( $oldimage ) ) { - if ( strlen( $oldimage ) < 16 ) { - $wgOut->unexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); - return; - } - if ( strstr( $oldimage, "/" ) || strstr( $oldimage, "\\" ) ) { - $wgOut->unexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); - return; - } - - # Invalidate description page cache - $this->mTitle->invalidateCache(); - - # Squid purging - if ( $wgUseSquid ) { - $urlArr = Array( - $wgInternalServer.wfImageArchiveUrl( $oldimage ), - $wgInternalServer.$this->mTitle->getFullURL() - ); - wfPurgeSquidServers($urlArr); - } - $this->doDeleteOldImage( $oldimage ); - $dbw->delete( 'oldimage', array( 'oi_archive_name' => $oldimage ) ); - $deleted = $oldimage; - } else { - $image = $this->mTitle->getDBkey(); - $dest = wfImageDir( $image ); - $archive = wfImageDir( $image ); - - # Delete the image file if it exists; due to sync problems - # or manual trimming sometimes the file will be missing. - $targetFile = "{$dest}/{$image}"; - if( file_exists( $targetFile ) && ! @unlink( $targetFile ) ) { - # If the deletion operation actually failed, bug out: - $wgOut->fileDeleteError( $targetFile ); - return; - } - $dbw->delete( 'image', array( 'img_name' => $image ) ); - $res = $dbw->select( 'oldimage', array( 'oi_archive_name' ), array( 'oi_name' => $image ) ); - - # Purge archive URLs from the squid - $urlArr = Array(); - while ( $s = $dbw->fetchObject( $res ) ) { - $this->doDeleteOldImage( $s->oi_archive_name ); - $urlArr[] = $wgInternalServer.wfImageArchiveUrl( $s->oi_archive_name ); - } - - # And also the HTML of all pages using this image - $linksTo = $this->img->getLinksTo(); - if ( $wgUseSquid ) { - $u = SquidUpdate::newFromTitles( $linksTo, $urlArr ); - array_push( $wgPostCommitUpdateList, $u ); - } - - $dbw->delete( 'oldimage', array( 'oi_name' => $image ) ); - - # Image itself is now gone, and database is cleaned. - # Now we remove the image description page. - - $article = new Article( $this->mTitle ); - $article->doDeleteArticle( $reason ); # ignore errors - - # Invalidate parser cache and client cache for pages using this image - # This is left until relatively late to reduce lock time - Title::touchArray( $linksTo ); - - /* Delete thumbnails and refresh image metadata cache */ - $this->img->purgeCache(); - - - $deleted = $image; - } - - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - - $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]'; - $text = wfMsg( 'deletedtext', $deleted, $loglink ); - - $wgOut->addWikiText( $text ); - - $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() ); + /** + * Revert the file to an earlier version + */ + public function revert() { + $reverter = new FileRevertForm( $this->img ); + $reverter->execute(); } - - function doDeleteOldImage( $oldimage ) - { - global $wgOut; - - $name = substr( $oldimage, 15 ); - $archive = wfImageArchiveDir( $name ); - - # Delete the image if it exists. Sometimes the file will be missing - # due to manual intervention or weird sync problems; treat that - # condition gracefully and continue to delete the database entry. - # Also some records may end up with an empty oi_archive_name field - # if the original file was missing when a new upload was made; - # don't try to delete the directory then! - # - $targetFile = "{$archive}/{$oldimage}"; - if( $oldimage != '' && file_exists( $targetFile ) && !@unlink( $targetFile ) ) { - # If we actually have a file and can't delete it, throw an error. - $wgOut->fileDeleteError( "{$archive}/{$oldimage}" ); + + /** + * Override handling of action=purge + */ + function doPurge() { + if( $this->img->exists() ) { + wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" ); + $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ); + $update->doUpdate(); + $this->img->upgradeRow(); + $this->img->purgeCache(); } else { - # Log the deletion - $log = new LogPage( 'delete' ); - $log->addEntry( 'delete', $this->mTitle, wfMsg('deletedrevision',$oldimage) ); - } - } - - function revert() - { - global $wgOut, $wgRequest, $wgUser; - global $wgUseSquid, $wgInternalServer, $wgDeferredUpdateList; - - $oldimage = $wgRequest->getText( 'oldimage' ); - if ( strlen( $oldimage ) < 16 ) { - $wgOut->unexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); - return; + wfDebug( "ImagePage::doPurge no image\n" ); } - if ( strstr( $oldimage, "/" ) || strstr( $oldimage, "\\" ) ) { - $wgOut->unexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); - return; - } - - if ( wfReadOnly() ) { - $wgOut->readOnlyPage(); - return; - } - if( $wgUser->isAnon() ) { - $wgOut->errorpage( 'uploadnologin', 'uploadnologintext' ); - return; - } - if ( ! $this->mTitle->userCanEdit() ) { - $wgOut->sysopRequired(); - return; - } - if ( $wgUser->isBlocked() ) { - return $this->blockedIPpage(); - } - if( !$wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) { - $wgOut->errorpage( 'internalerror', 'sessionfailure' ); - return; - } - $name = substr( $oldimage, 15 ); - - $dest = wfImageDir( $name ); - $archive = wfImageArchiveDir( $name ); - $curfile = "{$dest}/{$name}"; - - if ( ! is_file( $curfile ) ) { - $wgOut->fileNotFoundError( htmlspecialchars( $curfile ) ); - return; - } - $oldver = wfTimestampNow() . "!{$name}"; - - $dbr =& wfGetDB( DB_SLAVE ); - $size = $dbr->selectField( 'oldimage', 'oi_size', array( 'oi_archive_name' => $oldimage ) ); - - if ( ! rename( $curfile, "${archive}/{$oldver}" ) ) { - $wgOut->fileRenameError( $curfile, "${archive}/{$oldver}" ); - return; - } - if ( ! copy( "{$archive}/{$oldimage}", $curfile ) ) { - $wgOut->fileCopyError( "${archive}/{$oldimage}", $curfile ); - } - - # Record upload and update metadata cache - $img = Image::newFromName( $name ); - $img->recordUpload( $oldver, wfMsg( "reverted" ) ); - - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $wgOut->addHTML( wfMsg( 'imagereverted' ) ); - - $descTitle = $img->getTitle(); - $wgOut->returnToMain( false, $descTitle->getPrefixedText() ); + parent::doPurge(); } - function blockedIPpage() { - require_once( 'EditPage.php' ); - $edit = new EditPage( $this ); - return $edit->blockedIPpage(); + /** + * Display an error with a wikitext description + */ + function showError( $description ) { + global $wgOut; + $wgOut->setPageTitle( wfMsg( "internalerror" ) ); + $wgOut->setRobotpolicy( "noindex,nofollow" ); + $wgOut->setArticleRelated( false ); + $wgOut->enableClientCache( false ); + $wgOut->addWikiText( $description ); } } /** - * @todo document - * @package MediaWiki + * Builds the image revision log shown on image pages + * + * @addtogroup Media */ class ImageHistoryList { - function ImageHistoryList( &$skin ) { - $this->skin =& $skin; - } - function beginImageHistoryList() { - $s = "\n

    " . wfMsg( 'imghistory' ) . "

    \n" . - "

    " . wfMsg( 'imghistlegend' ) . "

    \n".'
      '; - return $s; - } + protected $img, $skin, $title, $repo; - function endImageHistoryList() { - $s = "
    \n"; - return $s; - } + public function __construct( $skin, $img ) { + $this->skin = $skin; + $this->img = $img; + $this->title = $img->getTitle(); + } + + public function beginImageHistoryList() { + global $wgOut, $wgUser; + return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) ) + . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) ) + . Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n" + . '' + . ( $this->img->isLocal() && $wgUser->isAllowed( 'delete' ) ? '' : '' ) + . '' . wfMsgHtml( 'filehist-datetime' ) . '' + . '' . wfMsgHtml( 'filehist-user' ) . '' + . '' . wfMsgHtml( 'filehist-dimensions' ) . '' + . '' . wfMsgHtml( 'filehist-filesize' ) . '' + . '' . wfMsgHtml( 'filehist-comment' ) . '' + . "\n"; + } + + public function endImageHistoryList() { + return "\n"; + } + + public function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $dims ) { + global $wgUser, $wgLang, $wgContLang; + $local = $this->img->isLocal(); + $row = ''; + + // Deletion link + if( $local && $wgUser->isAllowed( 'delete' ) ) { + $row .= ''; + $q = array(); + $q[] = 'action=delete'; + if( !$iscur ) + $q[] = 'oldimage=' . urlencode( $img ); + $row .= '(' . $this->skin->makeKnownLinkObj( + $this->title, + wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ), + implode( '&', $q ) + ) . ')'; + $row .= ''; + } + + // Reversion link/current indicator + $row .= ''; + if( $iscur ) { + $row .= '(' . wfMsgHtml( 'filehist-current' ) . ')'; + } elseif( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) { + $q = array(); + $q[] = 'action=revert'; + $q[] = 'oldimage=' . urlencode( $img ); + $q[] = 'wpEditToken=' . urlencode( $wgUser->editToken( $img ) ); + $row .= '(' . $this->skin->makeKnownLinkObj( + $this->title, + wfMsgHtml( 'filehist-revert' ), + implode( '&', $q ) + ) . ')'; + } + $row .= ''; + + // Date/time and image link + $row .= ''; + $url = $iscur ? $this->img->getUrl() : $this->img->getArchiveUrl( $img ); + $row .= Xml::element( + 'a', + array( 'href' => $url ), + $wgLang->timeAndDate( $timestamp, true ) + ); + $row .= ''; + + // Uploading user + $row .= ''; + if( $local ) { + $row .= $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext ); + } else { + $row .= htmlspecialchars( $usertext ); + } + $row .= ''; - function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description ) { - global $wgUser, $wgLang, $wgContLang, $wgTitle; + // Image dimensions + $row .= '' . htmlspecialchars( $dims ) . ''; - $datetime = $wgLang->timeanddate( $timestamp, true ); - $del = wfMsg( 'deleteimg' ); - $delall = wfMsg( 'deleteimgcompletely' ); - $cur = wfMsg( 'cur' ); + // File size + $row .= '' . $this->skin->formatSize( $size ) . ''; - if ( $iscur ) { - $url = Image::imageUrl( $img ); - $rlink = $cur; - if ( $wgUser->isAllowed('delete') ) { - $link = $wgTitle->escapeLocalURL( 'image=' . $wgTitle->getPartialURL() . - '&action=delete' ); - $style = $this->skin->getInternalLinkAttributes( $link, $delall ); + // Comment + $row .= '' . $this->skin->formatComment( $description, $this->title ) . ''; - $dlink = ''.$delall.''; - } else { - $dlink = $del; - } - } else { - $url = htmlspecialchars( wfImageArchiveUrl( $img ) ); - if( $wgUser->getID() != 0 && $wgTitle->userCanEdit() ) { - $token = urlencode( $wgUser->editToken( $img ) ); - $rlink = $this->skin->makeKnownLinkObj( $wgTitle, - wfMsg( 'revertimg' ), 'action=revert&oldimage=' . - urlencode( $img ) . "&wpEditToken=$token" ); - $dlink = $this->skin->makeKnownLinkObj( $wgTitle, - $del, 'action=delete&oldimage=' . urlencode( $img ) . - "&wpEditToken=$token" ); - } else { - # Having live active links for non-logged in users - # means that bots and spiders crawling our site can - # inadvertently change content. Baaaad idea. - $rlink = wfMsg( 'revertimg' ); - $dlink = $del; - } - } - if ( 0 == $user ) { - $userlink = $usertext; - } else { - $userlink = $this->skin->makeLinkObj( - Title::makeTitle( NS_USER, $usertext ), - $usertext ); - } - $nbytes = wfMsg( 'nbytes', $size ); - $style = $this->skin->getInternalLinkAttributes( $url, $datetime ); - - $s = "
  • ({$dlink}) ({$rlink}) {$datetime}" - . " . . {$userlink} ({$nbytes})"; - - $s .= $this->skin->commentBlock( $description, $wgTitle ); - $s .= "
  • \n"; - return $s; + return "{$row}\n"; } } - - -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/LinkCache.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/LinkCache.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/LinkCache.php 2005-06-26 04:25:18.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/LinkCache.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,60 +1,47 @@ mActive = true; - $this->mPreFilled = false; + + function __construct() { $this->mForUpdate = false; $this->mPageLinks = array(); $this->mGoodLinks = array(); $this->mBadLinks = array(); - $this->mImageLinks = array(); - $this->mCategoryLinks = array(); - $this->mOldGoodLinks = array(); - $this->mOldBadLinks = array(); - $this->mOldPageLinks = array(); + } + + /* private */ function getKey( $title ) { + return wfMemcKey( 'lc', 'title', $title ); } /** * General accessor to get/set whether SELECT FOR UPDATE should be used */ - function forUpdate( $update = NULL ) { + function forUpdate( $update = NULL ) { return wfSetVar( $this->mForUpdate, $update ); } - + function getGoodLinkID( $title ) { if ( array_key_exists( $title, $this->mGoodLinks ) ) { return $this->mGoodLinks[$title]; @@ -64,60 +51,43 @@ } function isBadLink( $title ) { - return array_key_exists( $title, $this->mBadLinks ); + return array_key_exists( $title, $this->mBadLinks ); } function addGoodLinkObj( $id, $title ) { - if ( $this->mActive ) { - $dbkey = $title->getPrefixedDbKey(); - $this->mGoodLinks[$dbkey] = $id; - $this->mPageLinks[$dbkey] = $title; - } + $dbkey = $title->getPrefixedDbKey(); + $this->mGoodLinks[$dbkey] = $id; + $this->mPageLinks[$dbkey] = $title; } function addBadLinkObj( $title ) { $dbkey = $title->getPrefixedDbKey(); - if ( $this->mActive && ( ! $this->isBadLink( $dbkey ) ) ) { + if ( ! $this->isBadLink( $dbkey ) ) { $this->mBadLinks[$dbkey] = 1; $this->mPageLinks[$dbkey] = $title; } } - function addImageLink( $title ) { - if ( $this->mActive ) { $this->mImageLinks[$title] = 1; } - } - - function addImageLinkObj( $nt ) { - if ( $this->mActive ) { $this->mImageLinks[$nt->getDBkey()] = 1; } - } - - function addCategoryLink( $title, $sortkey ) { - if ( $this->mActive ) { $this->mCategoryLinks[$title] = $sortkey; } - } - - function addCategoryLinkObj( &$nt, $sortkey ) { - $this->addCategoryLink( $nt->getDBkey(), $sortkey ); - } - function clearBadLink( $title ) { unset( $this->mBadLinks[$title] ); $this->clearLink( $title ); } - + function clearLink( $title ) { global $wgMemc, $wgLinkCacheMemcached; if( $wgLinkCacheMemcached ) $wgMemc->delete( $this->getKey( $title ) ); } - function suspend() { $this->mActive = false; } - function resume() { $this->mActive = true; } function getPageLinks() { return $this->mPageLinks; } function getGoodLinks() { return $this->mGoodLinks; } function getBadLinks() { return array_keys( $this->mBadLinks ); } - function getImageLinks() { return $this->mImageLinks; } - function getCategoryLinks() { return $this->mCategoryLinks; } + /** + * Add a title to the link cache, return the page_id or zero if non-existent + * @param $title String: title to add + * @return integer + */ function addLink( $title ) { $nt = Title::newFromDBkey( $title ); if( $nt ) { @@ -126,47 +96,61 @@ return 0; } } - + + /** + * Add a title to the link cache, return the page_id or zero if non-existent + * @param $nt Title to add. + * @return integer + */ function addLinkObj( &$nt ) { global $wgMemc, $wgLinkCacheMemcached, $wgAntiLockFlags; $title = $nt->getPrefixedDBkey(); - if ( $this->isBadLink( $title ) ) { return 0; } + if ( $this->isBadLink( $title ) ) { return 0; } $id = $this->getGoodLinkID( $title ); if ( 0 != $id ) { return $id; } $fname = 'LinkCache::addLinkObj'; + global $wgProfiling, $wgProfiler; + if ( $wgProfiling && isset( $wgProfiler ) ) { + $fname .= ' (' . $wgProfiler->getCurrentSection() . ')'; + } + wfProfileIn( $fname ); $ns = $nt->getNamespace(); $t = $nt->getDBkey(); - if ( '' == $title ) { + if ( '' == $title ) { wfProfileOut( $fname ); - return 0; + return 0; } - + $id = NULL; if( $wgLinkCacheMemcached ) $id = $wgMemc->get( $key = $this->getKey( $title ) ); if( ! is_integer( $id ) ) { if ( $this->mForUpdate ) { - $db =& wfGetDB( DB_MASTER ); + $db = wfGetDB( DB_MASTER ); if ( !( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) ) { $options = array( 'FOR UPDATE' ); + } else { + $options = array(); } } else { - $db =& wfGetDB( DB_SLAVE ); + $db = wfGetDB( DB_SLAVE ); $options = array(); } - $id = $db->selectField( 'page', 'page_id', array( 'page_namespace' => $ns, 'page_title' => $t ), $fname, $options ); + $id = $db->selectField( 'page', 'page_id', + array( 'page_namespace' => $ns, 'page_title' => $t ), + $fname, $options ); if ( !$id ) { $id = 0; } if( $wgLinkCacheMemcached ) $wgMemc->add( $key, $id, 3600*24 ); } - + if( 0 == $id ) { $this->addBadLinkObj( $nt ); } else { @@ -177,281 +161,12 @@ } /** - * Bulk-check the pagelinks and page arrays for existence info. - * @param Title $fromtitle - */ - function preFill( &$fromtitle ) { - global $wgAntiLockFlags; - $fname = 'LinkCache::preFill'; - wfProfileIn( $fname ); - - $this->suspend(); - $id = $fromtitle->getArticleID(); - $this->resume(); - - if( $id == 0 ) { - wfDebug( "$fname - got id 0 for title '" . $fromtitle->getPrefixedDBkey() . "'\n" ); - wfProfileOut( $fname ); - return; - } - - if ( $this->mForUpdate ) { - $db =& wfGetDB( DB_MASTER ); - if ( !( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) ) { - $options = 'FOR UPDATE'; - } else { - $options = ''; - } - } else { - $db =& wfGetDB( DB_SLAVE ); - $options = ''; - } - - $page = $db->tableName( 'page' ); - $pagelinks = $db->tableName( 'pagelinks' ); - - $sql = "SELECT page_id,pl_namespace,pl_title - FROM $pagelinks - LEFT JOIN $page - ON pl_namespace=page_namespace AND pl_title=page_title - WHERE pl_from=$id $options"; - $res = $db->query( $sql, $fname ); - while( $s = $db->fetchObject( $res ) ) { - $title = Title::makeTitle( $s->pl_namespace, $s->pl_title ); - if( $s->page_id ) { - $this->addGoodLinkObj( $s->page_id, $title ); - } else { - $this->addBadLinkObj( $title ); - } - } - $this->mPreFilled = true; - - wfProfileOut( $fname ); - } - - function getGoodAdditions() { - return array_diff( $this->mGoodLinks, $this->mOldGoodLinks ); - } - - function getBadAdditions() { - #wfDebug( "mOldBadLinks: " . implode( ', ', array_keys( $this->mOldBadLinks ) ) . "\n" ); - #wfDebug( "mBadLinks: " . implode( ', ', array_keys( $this->mBadLinks ) ) . "\n" ); - return array_values( array_diff( array_keys( $this->mBadLinks ), array_keys( $this->mOldBadLinks ) ) ); - } - - function getImageAdditions() { - return array_diff_assoc( $this->mImageLinks, $this->mOldImageLinks ); - } - - function getGoodDeletions() { - return array_diff( $this->mOldGoodLinks, $this->mGoodLinks ); - } - - function getBadDeletions() { - return array_values( array_diff( array_keys( $this->mOldBadLinks ), array_keys( $this->mBadLinks ) )); - } - - function getImageDeletions() { - return array_diff_assoc( $this->mOldImageLinks, $this->mImageLinks ); - } - - function getPageAdditions() { - $set = array_diff( array_keys( $this->mPageLinks ), array_keys( $this->mOldPageLinks ) ); - $out = array(); - foreach( $set as $key ) { - $out[$key] = $this->mPageLinks[$key]; - } - return $out; - } - - function getPageDeletions() { - $set = array_diff( array_keys( $this->mOldPageLinks ), array_keys( $this->mPageLinks ) ); - $out = array(); - foreach( $set as $key ) { - $out[$key] = $this->mOldPageLinks[$key]; - } - return $out; - } - - /** - * Parameters: - * @param $which is one of the LINKCACHE_xxx constants - * @param $del,$add are the incremental update arrays which will be filled. - * - * @return Returns whether or not it's worth doing the incremental version. - * - * For example, if [[List of mathematical topics]] was blanked, - * it would take a long, long time to do incrementally. - */ - function incrementalSetup( $which, &$del, &$add ) { - if ( ! $this->mPreFilled ) { - return false; - } - - switch ( $which ) { - case LINKCACHE_GOOD: - $old =& $this->mOldGoodLinks; - $cur =& $this->mGoodLinks; - $del = $this->getGoodDeletions(); - $add = $this->getGoodAdditions(); - break; - case LINKCACHE_BAD: - $old =& $this->mOldBadLinks; - $cur =& $this->mBadLinks; - $del = $this->getBadDeletions(); - $add = $this->getBadAdditions(); - break; - case LINKCACHE_PAGE: - $old =& $this->mOldPageLinks; - $cur =& $this->mPageLinks; - $del = $this->getPageDeletions(); - $add = $this->getPageAdditions(); - break; - default: # LINKCACHE_IMAGE - return false; - } - - return true; - } - - /** - * Clears cache + * Clears cache */ function clear() { $this->mPageLinks = array(); $this->mGoodLinks = array(); $this->mBadLinks = array(); - $this->mImageLinks = array(); - $this->mCategoryLinks = array(); - $this->mOldGoodLinks = array(); - $this->mOldBadLinks = array(); - $this->mOldPageLinks = array(); - } - - /** - * Swaps old and current link registers - */ - function swapRegisters() { - swap( $this->mGoodLinks, $this->mOldGoodLinks ); - swap( $this->mBadLinks, $this->mOldBadLinks ); - swap( $this->mImageLinks, $this->mOldImageLinks ); - swap( $this->mPageLinks, $this->mOldPageLinks ); - } -} - -/** - * Class representing a list of titles - * The execute() method checks them all for existence and adds them to a LinkCache object - + - * @package MediaWiki - * @subpackage Cache - */ -class LinkBatch { - /** - * 2-d array, first index namespace, second index dbkey, value arbitrary - */ - var $data = array(); - - function LinkBatch( $arr = array() ) { - foreach( $arr as $item ) { - $this->addObj( $item ); - } - } - - function addObj( $title ) { - $this->add( $title->getNamespace(), $title->getDBkey() ); - } - - function add( $ns, $dbkey ) { - if ( $ns < 0 ) { - return; - } - if ( !array_key_exists( $ns, $this->data ) ) { - $this->data[$ns] = array(); - } - - $this->data[$ns][$dbkey] = 1; - } - - function execute( &$cache ) { - $fname = 'LinkBatch::execute'; - $namespaces = array(); - - if ( !count( $this->data ) ) { - return; - } - - wfProfileIn( $fname ); - - // Construct query - // This is very similar to Parser::replaceLinkHolders - $dbr = wfGetDB( DB_SLAVE ); - $page = $dbr->tableName( 'page' ); - $sql = "SELECT page_id, page_namespace, page_title FROM $page WHERE " - . $this->constructSet( 'page', $dbr ); - - // Do query - $res = $dbr->query( $sql, $fname ); - - // Process results - // For each returned entry, add it to the list of good links, and remove it from $remaining - - $remaining = $this->data; - while ( $row = $dbr->fetchObject( $res ) ) { - $title = Title::makeTitle( $row->page_namespace, $row->page_title ); - $cache->addGoodLinkObj( $row->page_id, $title ); - unset( $remaining[$row->page_namespace][$row->page_title] ); - } - $dbr->freeResult( $res ); - - // The remaining links in $data are bad links, register them as such - foreach ( $remaining as $ns => $dbkeys ) { - foreach ( $dbkeys as $dbkey => $nothing ) { - $title = Title::makeTitle( $ns, $dbkey ); - $cache->addBadLinkObj( $title ); - } - } - - wfProfileOut( $fname ); - } - - /** - * Construct a WHERE clause which will match all the given titles. - * Give the appropriate table's field name prefix ('page', 'pl', etc). - * - * @param string $prefix - * @return string - * @access public - */ - function constructSet( $prefix, $db ) { - $first = true; - $sql = ''; - foreach ( $this->data as $ns => $dbkeys ) { - if ( !count( $dbkeys ) ) { - continue; - } - - if ( $first ) { - $first = false; - } else { - $sql .= ' OR '; - } - $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title IN ("; - - $firstTitle = true; - foreach( $dbkeys as $dbkey => $nothing ) { - if ( $firstTitle ) { - $firstTitle = false; - } else { - $sql .= ','; - } - $sql .= $db->addQuotes( $dbkey ); - } - - $sql .= '))'; - } - return $sql; } } -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Linker.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Linker.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Linker.php 2006-01-19 01:24:56.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Linker.php 2007-08-31 10:01:27.000000000 -0400 @@ -4,20 +4,21 @@ * These functions are used for primarily page content: * links, embedded images, table of contents. Links are * also used in the skin. - * @package MediaWiki - */ - -/** * For the moment, Skin is a descendent class of Linker. * In the future, it should probably be further split * so that ever other bit of the wiki doesn't have to * go loading up Skin to get at it. * - * @package MediaWiki + * @addtogroup Skins */ class Linker { - function Linker() {} + /** + * Flags for userToolLinks() + */ + const TOOL_LINKS_NOBLOCK = 1; + + function __construct() {} /** * @deprecated @@ -28,15 +29,23 @@ /** @todo document */ function getExternalLinkAttributes( $link, $text, $class='' ) { + $link = htmlspecialchars( $link ); + + $r = ($class != '') ? " class=\"$class\"" : " class=\"external\""; + + $r .= " title=\"{$link}\""; + return $r; + } + + function getInterwikiLinkAttributes( $link, $text, $class='' ) { global $wgContLang; - $same = ($link == $text); $link = urldecode( $link ); $link = $wgContLang->checkTitleEncoding( $link ); - $link = preg_replace( '/[\\x00-\\x1f_]/', ' ', $link ); + $link = preg_replace( '/[\\x00-\\x1f]/', ' ', $link ); $link = htmlspecialchars( $link ); - $r = ($class != '') ? " class='$class'" : " class='external'"; + $r = ($class != '') ? " class=\"$class\"" : " class=\"external\""; $r .= " title=\"{$link}\""; return $r; @@ -61,7 +70,9 @@ } /** - * @param bool $broken + * @param $nt Title object. + * @param $text String: FIXME + * @param $broken Boolean: FIXME, default 'false'. */ function getInternalLinkAttributesObj( &$nt, $text, $broken = false ) { if( $broken == 'stub' ) { @@ -77,14 +88,21 @@ } /** - * Note: This function MUST call getArticleID() on the link, - * otherwise the cache won't get updated properly. See LINKCACHE.DOC. + * This function is a shortcut to makeLinkObj(Title::newFromText($title),...). Do not call + * it if you already have a title object handy. See makeLinkObj for further documentation. + * + * @param $title String: the text of the title + * @param $text String: link text + * @param $query String: optional query part + * @param $trail String: optional trail. Alphabetic characters at the start of this string will + * be included in the link text. Other characters will be appended after + * the end of the link. */ function makeLink( $title, $text = '', $query = '', $trail = '' ) { wfProfileIn( 'Linker::makeLink' ); $nt = Title::newFromText( $title ); if ($nt) { - $result = $this->makeLinkObj( Title::newFromText( $title ), $text, $query, $trail ); + $result = $this->makeLinkObj( $nt, $text, $query, $trail ); } else { wfDebug( 'Invalid title passed to Linker::makeLink(): "'.$title."\"\n" ); $result = $text == "" ? $title : $text; @@ -94,7 +112,17 @@ return $result; } - /** @todo document */ + /** + * This function is a shortcut to makeKnownLinkObj(Title::newFromText($title),...). Do not call + * it if you already have a title object handy. See makeKnownLinkObj for further documentation. + * + * @param $title String: the text of the title + * @param $text String: link text + * @param $query String: optional query part + * @param $trail String: optional trail. Alphabetic characters at the start of this string will + * be included in the link text. Other characters will be appended after + * the end of the link. + */ function makeKnownLink( $title, $text = '', $query = '', $trail = '', $prefix = '',$aprops = '') { $nt = Title::newFromText( $title ); if ($nt) { @@ -105,7 +133,17 @@ } } - /** @todo document */ + /** + * This function is a shortcut to makeBrokenLinkObj(Title::newFromText($title),...). Do not call + * it if you already have a title object handy. See makeBrokenLinkObj for further documentation. + * + * @param string $title The text of the title + * @param string $text Link text + * @param string $query Optional query part + * @param string $trail Optional trail. Alphabetic characters at the start of this string will + * be included in the link text. Other characters will be appended after + * the end of the link. + */ function makeBrokenLink( $title, $text = '', $query = '', $trail = '' ) { $nt = Title::newFromText( $title ); if ($nt) { @@ -116,7 +154,17 @@ } } - /** @todo document */ + /** + * This function is a shortcut to makeStubLinkObj(Title::newFromText($title),...). Do not call + * it if you already have a title object handy. See makeStubLinkObj for further documentation. + * + * @param $title String: the text of the title + * @param $text String: link text + * @param $query String: optional query part + * @param $trail String: optional trail. Alphabetic characters at the start of this string will + * be included in the link text. Other characters will be appended after + * the end of the link. + */ function makeStubLink( $title, $text = '', $query = '', $trail = '' ) { $nt = Title::newFromText( $title ); if ($nt) { @@ -128,7 +176,18 @@ } /** - * Pass a title object, not a title string + * Make a link for a title which may or may not be in the database. If you need to + * call this lots of times, pre-fill the link cache with a LinkBatch, otherwise each + * call to this will result in a DB query. + * + * @param $nt Title: the title object to make the link from, e.g. from + * Title::newFromText. + * @param $text String: link text + * @param $query String: optional query part + * @param $trail String: optional trail. Alphabetic characters at the start of this string will + * be included in the link text. Other characters will be appended after + * the end of the link. + * @param $prefix String: optional prefix. As trail, only before instead of after. */ function makeLinkObj( $nt, $text= '', $query = '', $trail = '', $prefix = '' ) { global $wgUser; @@ -137,44 +196,27 @@ # Fail gracefully if ( ! is_object($nt) ) { - # wfDebugDieBacktrace(); + # throw new MWException(); wfProfileOut( $fname ); return "{$prefix}{$text}{$trail}"; } - $ns = $nt->getNamespace(); - $dbkey = $nt->getDBkey(); if ( $nt->isExternal() ) { $u = $nt->getFullURL(); $link = $nt->getPrefixedURL(); if ( '' == $text ) { $text = $nt->getPrefixedText(); } - $style = $this->getExternalLinkAttributes( $link, $text, 'extiw' ); + $style = $this->getInterwikiLinkAttributes( $link, $text, 'extiw' ); $inside = ''; if ( '' != $trail ) { + $m = array(); if ( preg_match( '/^([a-z]+)(.*)$$/sD', $trail, $m ) ) { $inside = $m[1]; $trail = $m[2]; } } - - # Check for anchors, normalize the anchor - - $parts = explode( '#', $u, 2 ); - if ( count( $parts ) == 2 ) { - $anchor = urlencode( Sanitizer::decodeCharReferences( str_replace(' ', '_', $parts[1] ) ) ); - $replacearray = array( - '%3A' => ':', - '%' => '.' - ); - $u = $parts[0] . '#' . - str_replace( array_keys( $replacearray ), - array_values( $replacearray ), - $anchor ); - } - $t = "{$text}{$inside}"; - + wfProfileOut( $fname ); return $t; } elseif ( $nt->isAlwaysKnown() ) { @@ -182,32 +224,39 @@ $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); } else { wfProfileIn( $fname.'-immediate' ); + + # Handles links to special pages wich do not exist in the database: + if( $nt->getNamespace() == NS_SPECIAL ) { + if( SpecialPage::exists( $nt->getDbKey() ) ) { + $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); + } else { + $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix ); + } + wfProfileOut( $fname.'-immediate' ); + wfProfileOut( $fname ); + return $retVal; + } + # Work out link colour immediately $aid = $nt->getArticleID() ; if ( 0 == $aid ) { $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix ); } else { - $threshold = $wgUser->getOption('stubthreshold') ; - if ( $threshold > 0 ) { - $dbr =& wfGetDB( DB_SLAVE ); - $s = $dbr->selectRow( - array( 'page' ), - array( 'page_len', - 'page_namespace', - 'page_is_redirect' ), - array( 'page_id' => $aid ), $fname ) ; - if ( $s !== false ) { - $size = $s->page_len; - if ( $s->page_is_redirect OR $s->page_namespace != NS_MAIN ) { - $size = $threshold*2 ; # Really big - } - } else { - $size = $threshold*2 ; # Really big + $stub = false; + if ( $nt->isContentPage() ) { + $threshold = $wgUser->getOption('stubthreshold'); + if ( $threshold > 0 ) { + $dbr = wfGetDB( DB_SLAVE ); + $s = $dbr->selectRow( + array( 'page' ), + array( 'page_len', + 'page_is_redirect' ), + array( 'page_id' => $aid ), $fname ) ; + $stub = ( $s !== false && !$s->page_is_redirect && + $s->page_len < $threshold ); } - } else { - $size = 1 ; } - if ( $size < $threshold ) { + if ( $stub ) { $retVal = $this->makeStubLinkObj( $nt, $text, $query, $trail, $prefix ); } else { $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); @@ -220,10 +269,20 @@ } /** - * Pass a title object, not a title string + * Make a link for a title which definitely exists. This is faster than makeLinkObj because + * it doesn't have to do a database query. It's also valid for interwiki titles and special + * pages. + * + * @param $nt Title object of target page + * @param $text String: text to replace the title + * @param $query String: link target + * @param $trail String: text after link + * @param $prefix String: text before link text + * @param $aprops String: extra attributes to the a-element + * @param $style String: style to apply - if empty, use getInternalLinkAttributesObj instead + * @return the a-element */ - function makeKnownLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '' ) { - global $wgTitle; + function makeKnownLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) { $fname = 'Linker::makeKnownLinkObj'; wfProfileIn( $fname ); @@ -232,27 +291,26 @@ wfProfileOut( $fname ); return $text; } - + $u = $nt->escapeLocalURL( $query ); - if ( '' != $nt->getFragment() ) { + if ( $nt->getFragment() != '' ) { if( $nt->getPrefixedDbkey() == '' ) { $u = ''; if ( '' == $text ) { $text = htmlspecialchars( $nt->getFragment() ); } } - $anchor = urlencode( Sanitizer::decodeCharReferences( str_replace( ' ', '_', $nt->getFragment() ) ) ); - $replacearray = array( - '%3A' => ':', - '%' => '.' - ); - $u .= '#' . str_replace(array_keys($replacearray),array_values($replacearray),$anchor); + $u .= $nt->getFragmentForURL(); } - if ( '' == $text ) { + if ( $text == '' ) { $text = htmlspecialchars( $nt->getPrefixedText() ); } - $style = $this->getInternalLinkAttributesObj( $nt, $text ); - + if ( $style == '' ) { + $style = $this->getInternalLinkAttributesObj( $nt, $text ); + } + + if ( $aprops !== '' ) $aprops = ' ' . $aprops; + list( $inside, $trail ) = Linker::splitTrail( $trail ); $r = "{$prefix}{$text}{$inside}{$trail}"; wfProfileOut( $fname ); @@ -260,19 +318,28 @@ } /** - * Pass a title object, not a title string + * Make a red link to the edit page of a given title. + * + * @param $title String: The text of the title + * @param $text String: Link text + * @param $query String: Optional query part + * @param $trail String: Optional trail. Alphabetic characters at the start of this string will + * be included in the link text. Other characters will be appended after + * the end of the link. */ function makeBrokenLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { # Fail gracefully if ( ! isset($nt) ) { - # wfDebugDieBacktrace(); + # throw new MWException(); return "{$prefix}{$text}{$trail}"; } $fname = 'Linker::makeBrokenLinkObj'; wfProfileIn( $fname ); - if ( '' == $query ) { + if( $nt->getNamespace() == NS_SPECIAL ) { + $q = $query; + } else if ( '' == $query ) { $q = 'action=edit'; } else { $q = 'action=edit&'.$query; @@ -283,7 +350,7 @@ $text = htmlspecialchars( $nt->getPrefixedText() ); } $style = $this->getInternalLinkAttributesObj( $nt, $text, "yes" ); - + list( $inside, $trail ) = Linker::splitTrail( $trail ); $s = "{$prefix}{$text}{$inside}{$trail}"; @@ -292,38 +359,35 @@ } /** - * Pass a title object, not a title string + * Make a brown link to a short article. + * + * @param $title String: the text of the title + * @param $text String: link text + * @param $query String: optional query part + * @param $trail String: optional trail. Alphabetic characters at the start of this string will + * be included in the link text. Other characters will be appended after + * the end of the link. */ function makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { - $link = $nt->getPrefixedURL(); - - $u = $nt->escapeLocalURL( $query ); - - if ( '' == $text ) { - $text = htmlspecialchars( $nt->getPrefixedText() ); - } $style = $this->getInternalLinkAttributesObj( $nt, $text, 'stub' ); - - list( $inside, $trail ) = Linker::splitTrail( $trail ); - $s = "{$prefix}{$text}{$inside}{$trail}"; - return $s; + return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix, '', $style ); } /** * Generate either a normal exists-style link or a stub link, depending * on the given page size. * - * @param int $size - * @param Title $nt - * @param string $text - * @param string $query - * @param string $trail - * @param string $prefix + * @param $size Integer + * @param $nt Title object. + * @param $text String + * @param $query String + * @param $trail String + * @param $prefix String * @return string HTML of link */ function makeSizeLinkObj( $size, $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { global $wgUser; - $threshold = IntVal( $wgUser->getOption( 'stubthreshold' ) ); + $threshold = intval( $wgUser->getOption( 'stubthreshold' ) ); if( $size < $threshold ) { return $this->makeStubLinkObj( $nt, $text, $query, $trail, $prefix ); } else { @@ -331,14 +395,17 @@ } } - /** @todo document */ + /** + * Make appropriate markup for a link to the current article. This is currently rendered + * as the bold link text. The calling sequence is the same as the other make*LinkObj functions, + * despite $query not being used. + */ function makeSelfLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { - $u = $nt->escapeLocalURL( $query ); if ( '' == $text ) { $text = htmlspecialchars( $nt->getPrefixedText() ); } list( $inside, $trail ) = Linker::splitTrail( $trail ); - return "{$prefix}{$text}{$inside}{$trail}"; + return "{$prefix}{$text}{$inside}{$trail}"; } /** @todo document */ @@ -366,30 +433,118 @@ return $s; } - /** @todo document */ - function makeImageLinkObj( $nt, $label, $alt, $align = '', $width = false, $height = false, $framed = false, - $thumb = false, $manual_thumb = '' ) + /** + * Creates the HTML source for images + * @deprecated use makeImageLink2 + * + * @param object $title + * @param string $label label text + * @param string $alt alt text + * @param string $align horizontal alignment: none, left, center, right) + * @param array $handlerParams Parameters to be passed to the media handler + * @param boolean $framed shows image in original size in a frame + * @param boolean $thumb shows image as thumbnail in a frame + * @param string $manualthumb image name for the manual thumbnail + * @param string $valign vertical alignment: baseline, sub, super, top, text-top, middle, bottom, text-bottom + * @return string + */ + function makeImageLinkObj( $title, $label, $alt, $align = '', $handlerParams = array(), $framed = false, + $thumb = false, $manualthumb = '', $valign = '', $time = false ) { - global $wgContLang, $wgUser, $wgThumbLimits; - - $img = new Image( $nt ); - if ( !$img->allowInlineDisplay() ) { - return $this->makeKnownLinkObj( $nt ); + $frameParams = array( 'alt' => $alt, 'caption' => $label ); + if ( $align ) { + $frameParams['align'] = $align; + } + if ( $framed ) { + $frameParams['framed'] = true; + } + if ( $thumb ) { + $frameParams['thumbnail'] = true; + } + if ( $manualthumb ) { + $frameParams['manualthumb'] = $manualthumb; + } + if ( $valign ) { + $frameParams['valign'] = $valign; + } + $file = wfFindFile( $title, $time ); + return $this->makeImageLink2( $title, $file, $frameParams, $handlerParams ); + } + + /** + * Make an image link + * @param Title $title Title object + * @param File $file File object, or false if it doesn't exist + * + * @param array $frameParams Associative array of parameters external to the media handler. + * Boolean parameters are indicated by presence or absence, the value is arbitrary and + * will often be false. + * thumbnail If present, downscale and frame + * manualthumb Image name to use as a thumbnail, instead of automatic scaling + * framed Shows image in original size in a frame + * frameless Downscale but don't frame + * upright If present, tweak default sizes for portrait orientation + * upright_factor Fudge factor for "upright" tweak (default 0.75) + * border If present, show a border around the image + * align Horizontal alignment (left, right, center, none) + * valign Vertical alignment (baseline, sub, super, top, text-top, middle, + * bottom, text-bottom) + * alt Alternate text for image (i.e. alt attribute). Plain text. + * caption HTML for image caption. + * + * @param array $handlerParams Associative array of media handler parameters, to be passed + * to transform(). Typical keys are "width" and "page". + */ + function makeImageLink2( Title $title, $file, $frameParams = array(), $handlerParams = array() ) { + global $wgContLang, $wgUser, $wgThumbLimits, $wgThumbUpright; + if ( $file && !$file->allowInlineDisplay() ) { + wfDebug( __METHOD__.': '.$title->getPrefixedDBkey()." does not allow inline display\n" ); + return $this->makeKnownLinkObj( $title ); } - $url = $img->getViewURL(); + // Shortcuts + $fp =& $frameParams; + $hp =& $handlerParams; + + // Clean up parameters + $page = isset( $hp['page'] ) ? $hp['page'] : false; + if ( !isset( $fp['align'] ) ) $fp['align'] = ''; + if ( !isset( $fp['alt'] ) ) $fp['alt'] = ''; + $prefix = $postfix = ''; - - wfDebug( "makeImageLinkObj: '$width'x'$height'\n" ); - - if ( 'center' == $align ) + + if ( 'center' == $fp['align'] ) { $prefix = '
    '; $postfix = '
    '; - $align = 'none'; + $fp['align'] = 'none'; } + if ( $file && !isset( $hp['width'] ) ) { + $hp['width'] = $file->getWidth( $page ); - if ( $thumb || $framed ) { + if( isset( $fp['thumbnail'] ) || isset( $fp['framed'] ) || isset( $fp['frameless'] ) || !$hp['width'] ) { + $wopt = $wgUser->getOption( 'thumbsize' ); + + if( !isset( $wgThumbLimits[$wopt] ) ) { + $wopt = User::getDefaultOption( 'thumbsize' ); + } + + // Reduce width for upright images when parameter 'upright' is used + if ( isset( $fp['upright'] ) && $fp['upright'] == 0 ) { + $fp['upright'] = $wgThumbUpright; + } + // Use width which is smaller: real image width or user preference width + // For caching health: If width scaled down due to upright parameter, round to full __0 pixel to avoid the creation of a lot of odd thumbs + $prefWidth = isset( $fp['upright'] ) ? + round( $wgThumbLimits[$wopt] * $fp['upright'], -1 ) : + $wgThumbLimits[$wopt]; + if ( $hp['width'] <= 0 || $prefWidth < $hp['width'] ) { + $hp['width'] = $prefWidth; + } + } + } + + if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) { # Create a thumbnail. Alignment depends on language # writing direction, # right aligned for left-to-right- @@ -398,201 +553,184 @@ # # If thumbnail width has not been provided, it is set # to the default user option as specified in Language*.php - if ( $align == '' ) { - $align = $wgContLang->isRTL() ? 'left' : 'right'; + if ( $fp['align'] == '' ) { + $fp['align'] = $wgContLang->isRTL() ? 'left' : 'right'; } + return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp ).$postfix; + } - - if ( $width === false ) { - $wopt = $wgUser->getOption( 'thumbsize' ); - - if( !isset( $wgThumbLimits[$wopt] ) ) { - $wopt = User::getDefaultOption( 'thumbsize' ); - } - - $width = min( $img->getWidth(), $wgThumbLimits[$wopt] ); - } - - return $prefix.$this->makeThumbLinkObj( $img, $label, $alt, $align, $width, $height, $framed, $manual_thumb ).$postfix; - - } elseif ( $width ) { - - # Create a resized image, without the additional thumbnail - # features - - if ( $height !== false && ( $img->getHeight() * $width / $img->getWidth() > $height ) ) { - $width = $img->getWidth() * $height / $img->getHeight(); - } - if ( $manual_thumb == '') { - $thumb = $img->getThumbnail( $width ); - if ( $thumb ) { - if( $width > $thumb->width ) { - // Requested a display size larger than the actual image; - // fake it up! - $height = floor($thumb->height * $width / $thumb->width); - wfDebug( "makeImageLinkObj: client-size height set to '$height'\n" ); - } else { - $height = $thumb->height; - wfDebug( "makeImageLinkObj: thumb height set to '$height'\n" ); - } - $url = $thumb->getUrl(); - } - } + if ( $file && $hp['width'] ) { + # Create a resized image, without the additional thumbnail features + $thumb = $file->transform( $hp ); } else { - $width = $img->width; - $height = $img->height; + $thumb = false; } - wfDebug( "makeImageLinkObj2: '$width'x'$height'\n" ); - $u = $nt->escapeLocalURL(); - if ( $url == '' ) { - $s = $this->makeBrokenImageLinkObj( $img->getTitle() ); - //$s .= "
    {$alt}
    {$url}
    \n"; + if ( !$thumb ) { + $s = $this->makeBrokenImageLinkObj( $title ); } else { - $s = '' . - ''.$alt.''; + $s = $thumb->toHtml( array( + 'desc-link' => true, + 'alt' => $fp['alt'], + 'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false , + 'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false ) ); } - if ( '' != $align ) { - $s = "
    {$s}
    "; + if ( '' != $fp['align'] ) { + $s = "
    {$s}
    "; } return str_replace("\n", ' ',$prefix.$s.$postfix); } /** * Make HTML for a thumbnail including image, border and caption - * $img is an Image object + * @param Title $title + * @param File $file File object or false if it doesn't exist */ - function makeThumbLinkObj( $img, $label = '', $alt, $align = 'right', $boxwidth = 180, $boxheight=false, $framed=false , $manual_thumb = "" ) { + function makeThumbLinkObj( Title $title, $file, $label = '', $alt, $align = 'right', $params = array(), $framed=false , $manualthumb = "" ) { + $frameParams = array( + 'alt' => $alt, + 'caption' => $label, + 'align' => $align + ); + if ( $framed ) $frameParams['framed'] = true; + if ( $manualthumb ) $frameParams['manualthumb'] = $manualthumb; + return $this->makeThumbLink2( $title, $file, $frameParams, $params ); + } + + function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array() ) { global $wgStylePath, $wgContLang; - $url = $img->getViewURL(); + $exists = $file && $file->exists(); - $width = $height = 0; - if ( $img->exists() ) - { - $width = $img->getWidth(); - $height = $img->getHeight(); - } - if ( 0 == $width || 0 == $height ) - { - $width = $height = 200; - } - if ( $boxwidth == 0 ) - { - $boxwidth = 200; + # Shortcuts + $fp =& $frameParams; + $hp =& $handlerParams; + + $page = isset( $hp['page'] ) ? $hp['page'] : false; + if ( !isset( $fp['align'] ) ) $fp['align'] = 'right'; + if ( !isset( $fp['alt'] ) ) $fp['alt'] = ''; + if ( !isset( $fp['caption'] ) ) $fp['caption'] = ''; + + if ( empty( $hp['width'] ) ) { + // Reduce width for upright images when parameter 'upright' is used + $hp['width'] = isset( $fp['upright'] ) ? 130 : 180; } - if ( $framed ) - { - // Use image dimensions, don't scale - $boxwidth = $width; - $oboxwidth = $boxwidth + 2; - $boxheight = $height; - $thumbUrl = $url; - } else { - $h = round( $height/($width/$boxwidth) ); - $oboxwidth = $boxwidth + 2; - if ( ( ! $boxheight === false ) && ( $h > $boxheight ) ) - { - $boxwidth *= $boxheight/$h; + $thumb = false; + + if ( !$exists ) { + $outerWidth = $hp['width'] + 2; + } else { + if ( isset( $fp['manualthumb'] ) ) { + # Use manually specified thumbnail + $manual_title = Title::makeTitleSafe( NS_IMAGE, $fp['manualthumb'] ); + if( $manual_title ) { + $manual_img = wfFindFile( $manual_title ); + if ( $manual_img ) { + $thumb = $manual_img->getUnscaledThumb(); + } else { + $exists = false; + } + } + } elseif ( isset( $fp['framed'] ) ) { + // Use image dimensions, don't scale + $thumb = $file->getUnscaledThumb( $page ); } else { - $boxheight = $h; + # Do not present an image bigger than the source, for bitmap-style images + # This is a hack to maintain compatibility with arbitrary pre-1.10 behaviour + $srcWidth = $file->getWidth( $page ); + if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) { + $hp['width'] = $srcWidth; + } + $thumb = $file->transform( $hp ); } - if ( '' == $manual_thumb ) $thumbUrl = $img->createThumb( $boxwidth ); - } - if ( $manual_thumb != '' ) # Use manually specified thumbnail - { - $manual_title = Title::makeTitleSafe( NS_IMAGE, $manual_thumb ); #new Title ( $manual_thumb ) ; - $manual_img = new Image( $manual_title ); - $thumbUrl = $manual_img->getViewURL(); - if ( $manual_img->exists() ) - { - $width = $manual_img->getWidth(); - $height = $manual_img->getHeight(); - $boxwidth = $width ; - $boxheight = $height ; - $oboxwidth = $boxwidth + 2 ; + if ( $thumb ) { + $outerWidth = $thumb->getWidth() + 2; + } else { + $outerWidth = $hp['width'] + 2; } } - $u = $img->getEscapeLocalURL(); + $query = $page ? 'page=' . urlencode( $page ) : ''; + $url = $title->getLocalURL( $query ); $more = htmlspecialchars( wfMsg( 'thumbnail-more' ) ); $magnifyalign = $wgContLang->isRTL() ? 'left' : 'right'; $textalign = $wgContLang->isRTL() ? ' style="text-align:right"' : ''; - $s = "
    "; - if ( $thumbUrl == '' ) { - $s .= $this->makeBrokenImageLinkObj( $img->getTitle() ); + $s = "
    "; + if( !$exists ) { + $s .= $this->makeBrokenImageLinkObj( $title ); + $zoomicon = ''; + } elseif ( !$thumb ) { + $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) ); $zoomicon = ''; } else { - $s .= ''. - ''.$alt.''; - if ( $framed ) { + $s .= $thumb->toHtml( array( + 'alt' => $fp['alt'], + 'img-class' => 'thumbimage', + 'desc-link' => true ) ); + if ( isset( $fp['framed'] ) ) { $zoomicon=""; } else { $zoomicon = ''; + 'width="15" height="11" alt="" />
    '; } } - $s .= '
    '.$zoomicon.$label."
    "; + $s .= '
    '.$zoomicon.$fp['caption']."
    "; return str_replace("\n", ' ', $s); } - + /** - * Pass a title object, not a title string + * Make a "broken" link to an image + * + * @param Title $title Image title + * @param string $text Link label + * @param string $query Query string + * @param string $trail Link trail + * @param string $prefix Link prefix + * @return string */ - function makeBrokenImageLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { - # Fail gracefully - if ( ! isset($nt) ) { - # wfDebugDieBacktrace(); + public function makeBrokenImageLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) { + global $wgEnableUploads; + if( $title instanceof Title ) { + wfProfileIn( __METHOD__ ); + if( $wgEnableUploads ) { + $upload = SpecialPage::getTitleFor( 'Upload' ); + if( $text == '' ) + $text = htmlspecialchars( $title->getPrefixedText() ); + $q = 'wpDestFile=' . $title->getPartialUrl(); + if( $query != '' ) + $q .= '&' . $query; + list( $inside, $trail ) = self::splitTrail( $trail ); + $style = $this->getInternalLinkAttributesObj( $title, $text, 'yes' ); + wfProfileOut( __METHOD__ ); + return '' . $prefix . $text . $inside . '' . $trail; + } else { + wfProfileOut( __METHOD__ ); + return $this->makeKnownLinkObj( $title, $text, $query, $trail, $prefix ); + } + } else { return "{$prefix}{$text}{$trail}"; } - - $fname = 'Linker::makeBrokenImageLinkObj'; - wfProfileIn( $fname ); - - $q = 'wpDestFile=' . urlencode( $nt->getDBkey() ); - if ( '' != $query ) { - $q .= "&$query"; - } - $uploadTitle = Title::makeTitle( NS_SPECIAL, 'Upload' ); - $url = $uploadTitle->escapeLocalURL( $q ); - - if ( '' == $text ) { - $text = htmlspecialchars( $nt->getPrefixedText() ); - } - $style = $this->getInternalLinkAttributesObj( $nt, $text, "yes" ); - list( $inside, $trail ) = Linker::splitTrail( $trail ); - $s = "{$prefix}{$text}{$inside}{$trail}"; - - wfProfileOut( $fname ); - return $s; } - - /** @todo document */ - function makeMediaLink( $name, $url, $alt = '' ) { + + /** @deprecated use Linker::makeMediaLinkObj() */ + function makeMediaLink( $name, $unused = '', $text = '' ) { $nt = Title::makeTitleSafe( NS_IMAGE, $name ); - return $this->makeMediaLinkObj( $nt, $alt ); + return $this->makeMediaLinkObj( $nt, $text ); } /** * Create a direct link to a given uploaded file. * - * @param Title $title - * @param string $text pre-sanitized HTML - * @param bool $nourl Mask absolute URLs, so the parser doesn't - * linkify them (it is currently not context-aware) + * @param $title Title object. + * @param $text String: pre-sanitized HTML * @return string HTML * - * @access public + * @public * @todo Handle invalid or missing images better. */ function makeMediaLinkObj( $title, $text = '' ) { @@ -600,14 +738,13 @@ ### HOTFIX. Instead of breaking, return empty string. return $text; } else { - $name = $title->getDBKey(); - $img = new Image( $title ); - if( $img->exists() ) { + $img = wfFindFile( $title ); + if( $img ) { $url = $img->getURL(); $class = 'internal'; } else { - $upload = Title::makeTitle( NS_SPECIAL, 'Upload' ); - $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $img->getName() ) ); + $upload = SpecialPage::getTitleFor( 'Upload' ); + $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $title->getDbKey() ) ); $class = 'new'; } $alt = htmlspecialchars( $title->getText() ); @@ -615,7 +752,7 @@ $text = $alt; } $u = htmlspecialchars( $url ); - return "{$text}"; + return "{$text}"; } } @@ -630,10 +767,10 @@ } /** @todo document */ - function makeExternalLink( $url, $text, $escape = true, $linktype = '' ) { + function makeExternalLink( $url, $text, $escape = true, $linktype = '', $ns = null ) { $style = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype ); - global $wgNoFollowLinks; - if( $wgNoFollowLinks ) { + global $wgNoFollowLinks, $wgNoFollowNsExceptions; + if( $wgNoFollowLinks && !(isset($ns) && in_array($ns, $wgNoFollowNsExceptions)) ) { $style .= ' rel="nofollow"'; } $url = htmlspecialchars( $url ); @@ -644,44 +781,201 @@ } /** + * Make user link (or user contributions for unregistered users) + * @param $userId Integer: user id in database. + * @param $userText String: user name in database + * @return string HTML fragment + * @private + */ + function userLink( $userId, $userText ) { + $encName = htmlspecialchars( $userText ); + if( $userId == 0 ) { + $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText ); + return $this->makeKnownLinkObj( $contribsPage, + $encName); + } else { + $userPage = Title::makeTitle( NS_USER, $userText ); + return $this->makeLinkObj( $userPage, $encName ); + } + } + + /** + * Generate standard user tool links (talk, contributions, block link, etc.) + * + * @param int $userId User identifier + * @param string $userText User name or IP address + * @param bool $redContribsWhenNoEdits Should the contributions link be red if the user has no edits? + * @param int $flags Customisation flags (e.g. self::TOOL_LINKS_NOBLOCK) + * @return string + */ + public function userToolLinks( $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0 ) { + global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans; + $talkable = !( $wgDisableAnonTalk && 0 == $userId ); + $blockable = ( $wgSysopUserBans || 0 == $userId ) && !$flags & self::TOOL_LINKS_NOBLOCK; + + $items = array(); + if( $talkable ) { + $items[] = $this->userTalkLink( $userId, $userText ); + } + if( $userId ) { + // check if the user has an edit + if( $redContribsWhenNoEdits && User::edits( $userId ) == 0 ) { + $style = " class='new'"; + } else { + $style = ''; + } + $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText ); + + $items[] = $this->makeKnownLinkObj( $contribsPage, wfMsgHtml( 'contribslink' ), '', '', '', '', $style ); + } + if( $blockable && $wgUser->isAllowed( 'block' ) ) { + $items[] = $this->blockLink( $userId, $userText ); + } + + if( $items ) { + return ' (' . implode( ' | ', $items ) . ')'; + } else { + return ''; + } + } + + /** + * Alias for userToolLinks( $userId, $userText, true ); + */ + public function userToolLinksRedContribs( $userId, $userText ) { + return $this->userToolLinks( $userId, $userText, true ); + } + + + /** + * @param $userId Integer: user id in database. + * @param $userText String: user name in database. + * @return string HTML fragment with user talk link + * @private + */ + function userTalkLink( $userId, $userText ) { + $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText ); + $userTalkLink = $this->makeLinkObj( $userTalkPage, wfMsgHtml( 'talkpagelinktext' ) ); + return $userTalkLink; + } + + /** + * @param $userId Integer: userid + * @param $userText String: user name in database. + * @return string HTML fragment with block link + * @private + */ + function blockLink( $userId, $userText ) { + $blockPage = SpecialPage::getTitleFor( 'Blockip', $userText ); + $blockLink = $this->makeKnownLinkObj( $blockPage, + wfMsgHtml( 'blocklink' ) ); + return $blockLink; + } + + /** + * Generate a user link if the current user is allowed to view it + * @param $rev Revision object. + * @return string HTML + */ + function revUserLink( $rev ) { + if( $rev->userCan( Revision::DELETED_USER ) ) { + $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ); + } else { + $link = wfMsgHtml( 'rev-deleted-user' ); + } + if( $rev->isDeleted( Revision::DELETED_USER ) ) { + return '' . $link . ''; + } + return $link; + } + + /** + * Generate a user tool link cluster if the current user is allowed to view it + * @param $rev Revision object. + * @return string HTML + */ + function revUserTools( $rev ) { + if( $rev->userCan( Revision::DELETED_USER ) ) { + $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ) . + ' ' . + $this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() ); + } else { + $link = wfMsgHtml( 'rev-deleted-user' ); + } + if( $rev->isDeleted( Revision::DELETED_USER ) ) { + return '' . $link . ''; + } + return $link; + } + + /** * This function is called by all recent changes variants, by the page history, * and by the user contributions list. It is responsible for formatting edit * comments. It escapes any HTML in the comment, but adds some CSS to format * auto-generated comments (from section editing) and formats [[wikilinks]]. * - * The &$title parameter must be a title OBJECT. It is used to generate a - * direct link to the section in the autocomment. * @author Erik Moeller * * Note: there's not always a title to pass to this function. * Since you can't set a default parameter for a reference, I've turned it * temporarily to a value pass. Should be adjusted further. --brion + * + * @param string $comment + * @param mixed $title Title object (to generate link to the section in autocomment) or null + * @param bool $local Whether section links should refer to local page */ - function formatComment($comment, $title = NULL) { - $fname = 'Linker::formatComment'; - wfProfileIn( $fname ); - - global $wgContLang; + function formatComment($comment, $title = NULL, $local = false) { + wfProfileIn( __METHOD__ ); + + # Sanitize text a bit: $comment = str_replace( "\n", " ", $comment ); $comment = htmlspecialchars( $comment ); - # The pattern for autogen comments is / * foo * /, which makes for - # some nasty regex. - # We look for all comments, match any text before and after the comment, - # add a separator where needed and format the comment itself with CSS - while (preg_match('/(.*)\/\*\s*(.*?)\s*\*\/(.*)/', $comment,$match)) { + # Render autocomments and make links: + $comment = $this->formatAutoComments( $comment, $title, $local ); + $comment = $this->formatLinksInComment( $comment ); + + wfProfileOut( __METHOD__ ); + return $comment; + } + + /** + * The pattern for autogen comments is / * foo * /, which makes for + * some nasty regex. + * We look for all comments, match any text before and after the comment, + * add a separator where needed and format the comment itself with CSS + * Called by Linker::formatComment. + * + * @param $comment Comment text + * @param $title An optional title object used to links to sections + * + * @todo Document the $local parameter. + */ + private function formatAutocomments( $comment, $title = NULL, $local = false ) { + $match = array(); + while (preg_match('!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment,$match)) { $pre=$match[1]; $auto=$match[2]; $post=$match[3]; $link=''; - if($title) { - $section=$auto; + if( $title ) { + $section = $auto; - # This is hackish but should work in most cases. - $section=str_replace('[[','',$section); - $section=str_replace(']]','',$section); - $title->mFragment=$section; - $link=$this->makeKnownLinkObj($title,wfMsg('sectionlink')); + # Generate a valid anchor name from the section title. + # Hackish, but should generally work - we strip wiki + # syntax, including the magic [[: that is used to + # "link rather than show" in case of images and + # interlanguage links. + $section = str_replace( '[[:', '', $section ); + $section = str_replace( '[[', '', $section ); + $section = str_replace( ']]', '', $section ); + if ( $local ) { + $sectionTitle = Title::newFromText( '#' . $section); + } else { + $sectionTitle = wfClone( $title ); + $sectionTitle->mFragment = $section; + } + $link = $this->makeKnownLinkObj( $sectionTitle, wfMsg( 'sectionlink' ) ); } $sep='-'; $auto=$link.$auto; @@ -691,58 +985,96 @@ $comment=$pre.$auto.$post; } - # format regular and media links - all other wiki formatting - # is ignored - $medians = $wgContLang->getNsText( NS_MEDIA ) . ':'; - while(preg_match('/\[\[(.*?)(\|(.*?))*\]\](.*)$/',$comment,$match)) { + return $comment; + } + + /** + * Formats wiki links and media links in text; all other wiki formatting + * is ignored + * + * @param string $comment Text to format links in + * @return string + */ + public function formatLinksInComment( $comment ) { + global $wgContLang; + + $medians = '(?:' . preg_quote( Namespace::getCanonicalName( NS_MEDIA ), '/' ) . '|'; + $medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):'; + + $match = array(); + while(preg_match('/\[\[:?(.*?)(\|(.*?))*\]\](.*)$/',$comment,$match)) { # Handle link renaming [[foo|text]] will show link as "text" if( "" != $match[3] ) { $text = $match[3]; } else { $text = $match[1]; } + $submatch = array(); if( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) { # Media link; trail not supported. $linkRegexp = '/\[\[(.*?)\]\]/'; $thelink = $this->makeMediaLink( $submatch[1], "", $text ); } else { # Other kind of link - if( preg_match( wfMsgForContent( "linktrail" ), $match[4], $submatch ) ) { + if( preg_match( $wgContLang->linkTrail(), $match[4], $submatch ) ) { $trail = $submatch[1]; } else { $trail = ""; } $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/'; - if ($match[1][0] == ':') + if (isset($match[1][0]) && $match[1][0] == ':') $match[1] = substr($match[1], 1); $thelink = $this->makeLink( $match[1], $text, "", $trail ); } - # Quote backreferences, then run preg_replace - $thelink = strtr( $thelink, array( "\\" => "\\\\", '$' => "\\$" ) ); - $comment = preg_replace( $linkRegexp, $thelink, $comment, 1 ); + $comment = preg_replace( $linkRegexp, StringUtils::escapeRegexReplacement( $thelink ), $comment, 1 ); } - wfProfileOut( $fname ); + return $comment; } - + /** * Wrap a comment in standard punctuation and formatting if * it's non-empty, otherwise return empty string. * * @param string $comment - * @param Title $title + * @param mixed $title Title object (to generate link to section in autocomment) or null + * @param bool $local Whether section links should refer to local page + * * @return string - * @access public */ - function commentBlock( $comment, $title = NULL ) { + function commentBlock( $comment, $title = NULL, $local = false ) { + // '*' used to be the comment inserted by the software way back + // in antiquity in case none was provided, here for backwards + // compatability, acc. to brion -ævar if( $comment == '' || $comment == '*' ) { return ''; } else { - $formatted = $this->formatComment( $comment, $title ); - return " ($formatted)"; + $formatted = $this->formatComment( $comment, $title, $local ); + return " ($formatted)"; } } + /** + * Wrap and format the given revision's comment block, if the current + * user is allowed to view it. + * + * @param Revision $rev + * @param bool $local Whether section links should refer to local page + * @return string HTML + */ + function revComment( Revision $rev, $local = false ) { + if( $rev->userCan( Revision::DELETED_COMMENT ) ) { + $block = $this->commentBlock( $rev->getRawComment(), $rev->getTitle(), $local ); + } else { + $block = " " . + wfMsgHtml( 'rev-deleted-comment' ) . ""; + } + if( $rev->isDeleted( Revision::DELETED_COMMENT ) ) { + return " $block"; + } + return $block; + } + /** @todo document */ function tocIndent() { return "\n
      "; @@ -757,7 +1089,7 @@ * parameter level defines if we are on an indentation level */ function tocLine( $anchor, $tocline, $tocnumber, $level ) { - return "\n
    • ' . $tocnumber . ' ' . $tocline . ''; @@ -771,63 +1103,108 @@ /** @todo document */ function tocList($toc) { global $wgJsMimeType; - return "
      " - . "

      " . wfMsgForContent('toc') . "

      \n" - . $toc - . "\n
      \n" - . '\n"; + $title = wfMsgHtml('toc') ; + return + '
      ' + . '

      ' . $title . "

      \n" + . $toc + # no trailing newline, script should not be wrapped in a + # paragraph + . "\n
      " + . '\n"; } - /** @todo document */ - function editSectionLinkForOther( $title, $section ) { - global $wgRequest; - global $wgContLang; - - $title = Title::newFromText($title); - $editurl = '§ion='.$section; - $url = $this->makeKnownLink($title->getPrefixedText(),wfMsg('editsection'),'action=edit'.$editurl); + /** + * Used to generate section edit links that point to "other" pages + * (sections that are really part of included pages). + * + * @param $title Title string. + * @param $section Integer: section number. + */ + public function editSectionLinkForOther( $title, $section ) { + $title = Title::newFromText( $title ); + return $this->doEditSectionLink( $title, $section, '', 'EditSectionLinkForOther' ); + } - if( $wgContLang->isRTL() ) { - $farside = 'left'; - $nearside = 'right'; - } else { - $farside = 'right'; - $nearside = 'left'; + /** + * @param $nt Title object. + * @param $section Integer: section number. + * @param $hint Link String: title, or default if omitted or empty + */ + public function editSectionLink( Title $nt, $section, $hint='' ) { + if( $hint != '' ) { + $hint = wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) ); + $hint = " title=\"$hint\""; } - return "
      [".$url."]
      "; - + return $this->doEditSectionLink( $nt, $section, $hint, 'EditSectionLink' ); } - /** @todo document */ - function editSectionLink( $nt, $section ) { + /** + * Implement editSectionLink and editSectionLinkForOther. + * + * @param $nt Title object + * @param $section Integer, section number + * @param $hint String, for HTML title attribute + * @param $hook String, name of hook to run + * @return String, HTML to use for edit link + */ + protected function doEditSectionLink( Title $nt, $section, $hint, $hook ) { global $wgContLang; - $editurl = '§ion='.$section; - $url = $this->makeKnownLink($nt->getPrefixedText(),wfMsg('editsection'),'action=edit'.$editurl); - - if( $wgContLang->isRTL() ) { - $farside = 'left'; - $nearside = 'right'; + $url = $this->makeKnownLinkObj( + $nt, + wfMsg('editsection'), + 'action=edit'.$editurl, + '', '', '', $hint + ); + $result = null; + + // The two hooks have slightly different interfaces . . . + if( $hook == 'EditSectionLink' ) { + wfRunHooks( $hook, array( &$this, $nt, $section, $hint, $url, &$result ) ); + } elseif( $hook == 'EditSectionLinkForOther' ) { + wfRunHooks( $hook, array( &$this, $nt, $section, $url, &$result ) ); + } + + // For reverse compatibility, add the brackets *after* the hook is run, + // and even add them to hook-provided text. + if( is_null( $result ) ) { + $result = wfMsg( 'editsection-brackets', $url ); } else { - $farside = 'right'; - $nearside = 'left'; + $result = wfMsg( 'editsection-brackets', $result ); } - return "
      [".$url."]
      "; + return "$result"; } - /** + /** + * Create a headline for content + * + * @param int $level The level of the headline (1-6) + * @param string $attribs Any attributes for the headline, starting with a space and ending with '>' + * This *must* be at least '>' for no attribs + * @param string $anchor The anchor to give the headline (the bit after the #) + * @param string $text The text of the header + * @param string $link HTML to add for the section edit link + * + * @return string HTML headline + */ + public function makeHeadline( $level, $attribs, $anchor, $text, $link ) { + return "$text"; + } + + /** * Split a link trail, return the "inside" portion and the remainder of the trail * as a two-element array - * + * * @static */ - function splitTrail( $trail ) { + static function splitTrail( $trail ) { static $regex = false; if ( $regex === false ) { global $wgContLang; @@ -835,6 +1212,7 @@ } $inside = ''; if ( '' != $trail ) { + $m = array(); if ( preg_match( $regex, $trail, $m ) ) { $inside = $m[1]; $trail = $m[2]; @@ -843,5 +1221,159 @@ return array( $inside, $trail ); } + /** + * Generate a rollback link for a given revision. Currently it's the + * caller's responsibility to ensure that the revision is the top one. If + * it's not, of course, the user will get an error message. + * + * If the calling page is called with the parameter &bot=1, all rollback + * links also get that parameter. It causes the edit itself and the rollback + * to be marked as "bot" edits. Bot edits are hidden by default from recent + * changes, so this allows sysops to combat a busy vandal without bothering + * other users. + * + * @param Revision $rev + */ + function generateRollback( $rev ) { + return '[' + . $this->buildRollbackLink( $rev ) + . ']'; + } + + /** + * Build a raw rollback link, useful for collections of "tool" links + * + * @param Revision $rev + * @return string + */ + public function buildRollbackLink( $rev ) { + global $wgRequest, $wgUser; + $title = $rev->getTitle(); + $extra = $wgRequest->getBool( 'bot' ) ? '&bot=1' : ''; + $extra .= '&token=' . urlencode( $wgUser->editToken( array( $title->getPrefixedText(), + $rev->getUserText() ) ) ); + return $this->makeKnownLinkObj( + $title, + wfMsgHtml( 'rollbacklink' ), + 'action=rollback&from=' . urlencode( $rev->getUserText() ) . $extra + ); + } + + /** + * Returns HTML for the "templates used on this page" list. + * + * @param array $templates Array of templates from Article::getUsedTemplate + * or similar + * @param bool $preview Whether this is for a preview + * @param bool $section Whether this is for a section edit + * @return string HTML output + */ + public function formatTemplates( $templates, $preview = false, $section = false) { + global $wgUser; + wfProfileIn( __METHOD__ ); + + $sk = $wgUser->getSkin(); + + $outText = ''; + if ( count( $templates ) > 0 ) { + # Do a batch existence check + $batch = new LinkBatch; + foreach( $templates as $title ) { + $batch->addObj( $title ); + } + $batch->execute(); + + # Construct the HTML + $outText = '
      '; + if ( $preview ) { + $outText .= wfMsgExt( 'templatesusedpreview', array( 'parse' ) ); + } elseif ( $section ) { + $outText .= wfMsgExt( 'templatesusedsection', array( 'parse' ) ); + } else { + $outText .= wfMsgExt( 'templatesused', array( 'parse' ) ); + } + $outText .= '
        '; + + foreach ( $templates as $titleObj ) { + $r = $titleObj->getRestrictions( 'edit' ); + if ( in_array( 'sysop', $r ) ) { + $protected = wfMsgExt( 'template-protected', array( 'parseinline' ) ); + } elseif ( in_array( 'autoconfirmed', $r ) ) { + $protected = wfMsgExt( 'template-semiprotected', array( 'parseinline' ) ); + } else { + $protected = ''; + } + $outText .= '
      • ' . $sk->makeLinkObj( $titleObj ) . ' ' . $protected . '
      • '; + } + $outText .= '
      '; + } + wfProfileOut( __METHOD__ ); + return $outText; + } + + /** + * Format a size in bytes for output, using an appropriate + * unit (B, KB, MB or GB) according to the magnitude in question + * + * @param $size Size to format + * @return string + */ + public function formatSize( $size ) { + global $wgLang; + return htmlspecialchars( $wgLang->formatSize( $size ) ); + } + + /** + * Given the id of an interface element, constructs the appropriate title + * and accesskey attributes from the system messages. (Note, this is usu- + * ally the id but isn't always, because sometimes the accesskey needs to + * go on a different element than the id, for reverse-compatibility, etc.) + * + * @param string $name Id of the element, minus prefixes. + * @return string title and accesskey attributes, ready to drop in an + * element (e.g., ' title="This does something [x]" accesskey="x"'). + */ + public function tooltipAndAccesskey($name) { + $out = ''; + + $tooltip = wfMsg('tooltip-'.$name); + if (!wfEmptyMsg('tooltip-'.$name, $tooltip) && $tooltip != '-') { + // Compatibility: formerly some tooltips had [alt-.] hardcoded + $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip ); + $out .= ' title="'.htmlspecialchars($tooltip); + } + $accesskey = wfMsg('accesskey-'.$name); + if ($accesskey && $accesskey != '-' && !wfEmptyMsg('accesskey-'.$name, $accesskey)) { + if ($out) $out .= " [$accesskey]\" accesskey=\"$accesskey\""; + else $out .= " title=\"[$accesskey]\" accesskey=\"$accesskey\""; + } elseif ($out) { + $out .= '"'; + } + return $out; + } + + /** + * Given the id of an interface element, constructs the appropriate title + * attribute from the system messages. (Note, this is usually the id but + * isn't always, because sometimes the accesskey needs to go on a different + * element than the id, for reverse-compatibility, etc.) + * + * @param string $name Id of the element, minus prefixes. + * @return string title attribute, ready to drop in an element + * (e.g., ' title="This does something"'). + */ + public function tooltip($name) { + $out = ''; + + $tooltip = wfMsg('tooltip-'.$name); + if (!wfEmptyMsg('tooltip-'.$name, $tooltip) && $tooltip != '-') { + $out = ' title="'.htmlspecialchars($tooltip).'"'; + } + + return $out; + } } -?> + + + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/LinksUpdate.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/LinksUpdate.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/LinksUpdate.php 2005-05-31 05:08:19.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/LinksUpdate.php 2007-07-18 04:49:24.000000000 -0400 @@ -1,255 +1,599 @@ mId = $id; + function LinksUpdate( $title, $parserOutput, $recursive = true ) { + global $wgAntiLockFlags; + + if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) { + $this->mOptions = array(); + } else { + $this->mOptions = array( 'FOR UPDATE' ); + } + $this->mDb = wfGetDB( DB_MASTER ); + + if ( !is_object( $title ) ) { + throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " . + "Please see Article::editUpdates() for an invocation example.\n" ); + } $this->mTitle = $title; + $this->mId = $title->getArticleID(); + + $this->mLinks = $parserOutput->getLinks(); + $this->mImages = $parserOutput->getImages(); + $this->mTemplates = $parserOutput->getTemplates(); + $this->mExternals = $parserOutput->getExternalLinks(); + $this->mCategories = $parserOutput->getCategories(); + + # Convert the format of the interlanguage links + # I didn't want to change it in the ParserOutput, because that array is passed all + # the way back to the skin, so either a skin API break would be required, or an + # inefficient back-conversion. + $ill = $parserOutput->getLanguageLinks(); + $this->mInterlangs = array(); + foreach ( $ill as $link ) { + list( $key, $title ) = explode( ':', $link, 2 ); + $this->mInterlangs[$key] = $title; + } + + $this->mRecursive = $recursive; + + wfRunHooks( 'LinksUpdateConstructed', array( &$this ) ); } /** * Update link tables with outgoing links from an updated article - * Relies on the 'link cache' to be filled out. */ - function doUpdate() { - global $wgUseDumbLinkUpdate, $wgLinkCache, $wgDBtransactions; - global $wgUseCategoryMagic; - + global $wgUseDumbLinkUpdate; if ( $wgUseDumbLinkUpdate ) { $this->doDumbUpdate(); - return; + } else { + $this->doIncrementalUpdate(); } + } - $fname = 'LinksUpdate::doUpdate'; + function doIncrementalUpdate() { + $fname = 'LinksUpdate::doIncrementalUpdate'; wfProfileIn( $fname ); + + # Page links + $existing = $this->getExistingLinks(); + $this->incrTableUpdate( 'pagelinks', 'pl', $this->getLinkDeletions( $existing ), + $this->getLinkInsertions( $existing ) ); - $del = array(); - $add = array(); + # Image links + $existing = $this->getExistingImages(); + $this->incrTableUpdate( 'imagelinks', 'il', $this->getImageDeletions( $existing ), + $this->getImageInsertions( $existing ) ); + + # Invalidate all image description pages which had links added or removed + $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing ); + $this->invalidateImageDescriptions( $imageUpdates ); + + # External links + $existing = $this->getExistingExternals(); + $this->incrTableUpdate( 'externallinks', 'el', $this->getExternalDeletions( $existing ), + $this->getExternalInsertions( $existing ) ); + + # Language links + $existing = $this->getExistingInterlangs(); + $this->incrTableUpdate( 'langlinks', 'll', $this->getInterlangDeletions( $existing ), + $this->getInterlangInsertions( $existing ) ); + + # Template links + $existing = $this->getExistingTemplates(); + $this->incrTableUpdate( 'templatelinks', 'tl', $this->getTemplateDeletions( $existing ), + $this->getTemplateInsertions( $existing ) ); - $dbw =& wfGetDB( DB_MASTER ); - $pagelinks = $dbw->tableName( 'pagelinks' ); - $imagelinks = $dbw->tableName( 'imagelinks' ); - $categorylinks = $dbw->tableName( 'categorylinks' ); - - #------------------------------------------------------------------------------ - # Good links - - if ( $wgLinkCache->incrementalSetup( LINKCACHE_PAGE, $del, $add ) ) { - # Delete where necessary - if ( count( $del ) ) { - $batch = new LinkBatch( $del ); - $set = $batch->constructSet( 'pl', $dbw ); - if ( $set ) { - $sql = "DELETE FROM $pagelinks WHERE pl_from={$this->mId} AND ($set)"; - $dbw->query( $sql, $fname ); + # Category links + $existing = $this->getExistingCategories(); + $this->incrTableUpdate( 'categorylinks', 'cl', $this->getCategoryDeletions( $existing ), + $this->getCategoryInsertions( $existing ) ); + + # Invalidate all categories which were added, deleted or changed (set symmetric difference) + $categoryUpdates = array_diff_assoc( $existing, $this->mCategories ) + array_diff_assoc( $this->mCategories, $existing ); + $this->invalidateCategories( $categoryUpdates ); + + # Refresh links of all pages including this page + # This will be in a separate transaction + if ( $this->mRecursive ) { + $this->queueRecursiveJobs(); + } + + wfProfileOut( $fname ); + } + + /** + * Link update which clears the previous entries and inserts new ones + * May be slower or faster depending on level of lock contention and write speed of DB + * Also useful where link table corruption needs to be repaired, e.g. in refreshLinks.php + */ + function doDumbUpdate() { + $fname = 'LinksUpdate::doDumbUpdate'; + wfProfileIn( $fname ); + + # Refresh category pages and image description pages + $existing = $this->getExistingCategories(); + $categoryUpdates = array_diff_assoc( $existing, $this->mCategories ) + array_diff_assoc( $this->mCategories, $existing ); + $existing = $this->getExistingImages(); + $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing ); + + $this->dumbTableUpdate( 'pagelinks', $this->getLinkInsertions(), 'pl_from' ); + $this->dumbTableUpdate( 'imagelinks', $this->getImageInsertions(), 'il_from' ); + $this->dumbTableUpdate( 'categorylinks', $this->getCategoryInsertions(), 'cl_from' ); + $this->dumbTableUpdate( 'templatelinks', $this->getTemplateInsertions(), 'tl_from' ); + $this->dumbTableUpdate( 'externallinks', $this->getExternalInsertions(), 'el_from' ); + $this->dumbTableUpdate( 'langlinks', $this->getInterlangInsertions(), 'll_from' ); + + # Update the cache of all the category pages and image description pages which were changed + $this->invalidateCategories( $categoryUpdates ); + $this->invalidateImageDescriptions( $imageUpdates ); + + # Refresh links of all pages including this page + # This will be in a separate transaction + if ( $this->mRecursive ) { + $this->queueRecursiveJobs(); + } + + wfProfileOut( $fname ); + } + + function queueRecursiveJobs() { + wfProfileIn( __METHOD__ ); + + $batchSize = 100; + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'templatelinks', 'page' ), + array( 'page_namespace', 'page_title' ), + array( + 'page_id=tl_from', + 'tl_namespace' => $this->mTitle->getNamespace(), + 'tl_title' => $this->mTitle->getDBkey() + ), __METHOD__ + ); + + $done = false; + while ( !$done ) { + $jobs = array(); + for ( $i = 0; $i < $batchSize; $i++ ) { + $row = $dbr->fetchObject( $res ); + if ( !$row ) { + $done = true; + break; } + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $jobs[] = new RefreshLinksJob( $title, '' ); } - } else { - # Delete everything - $dbw->delete( 'pagelinks', array( 'pl_from' => $this->mId ), $fname ); - - # Get the addition list - $add = $wgLinkCache->getPageLinks(); - } - - # Do the insertion - if( 0 != count( $add ) ) { - $arr = array(); - foreach( $add as $lt => $target ) { - array_push( $arr, array( - 'pl_from' => $this->mId, - 'pl_namespace' => $target->getNamespace(), - 'pl_title' => $target->getDbKey() ) ); - } - - # The link cache was constructed without FOR UPDATE, so there may be collisions - # Ignoring for now, I'm not sure if that causes problems or not, but I'm fairly - # sure it's better than without IGNORE - $dbw->insert( 'pagelinks', $arr, $fname, array( 'IGNORE' ) ); + Job::batchInsert( $jobs ); } - - #------------------------------------------------------------------------------ - # Image links - $dbw->delete('imagelinks',array('il_from'=>$this->mId),$fname); + $dbr->freeResult( $res ); + wfProfileOut( __METHOD__ ); + } + + /** + * Invalidate the cache of a list of pages from a single namespace + * + * @param integer $namespace + * @param array $dbkeys + */ + function invalidatePages( $namespace, $dbkeys ) { + $fname = 'LinksUpdate::invalidatePages'; - # Get addition list - $add = $wgLinkCache->getImageLinks(); + if ( !count( $dbkeys ) ) { + return; + } - # Do the insertion - $sql = ''; - $image = NS_IMAGE; - if ( 0 != count ( $add ) ) { - $arr = array(); - foreach ($add as $iname => $val ) { - $nt = Title::makeTitle( $image, $iname ); - if( !$nt ) continue; - $nt->invalidateCache(); - array_push( $arr, array( - 'il_from' => $this->mId, - 'il_to' => $iname ) ); - } - $dbw->insert('imagelinks', $arr, $fname, array('IGNORE')); + /** + * Determine which pages need to be updated + * This is necessary to prevent the job queue from smashing the DB with + * large numbers of concurrent invalidations of the same page + */ + $now = $this->mDb->timestamp(); + $ids = array(); + $res = $this->mDb->select( 'page', array( 'page_id' ), + array( + 'page_namespace' => $namespace, + 'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')', + 'page_touched < ' . $this->mDb->addQuotes( $now ) + ), $fname + ); + while ( $row = $this->mDb->fetchObject( $res ) ) { + $ids[] = $row->page_id; + } + if ( !count( $ids ) ) { + return; } + + /** + * Do the update + * We still need the page_touched condition, in case the row has changed since + * the non-locking select above. + */ + $this->mDb->update( 'page', array( 'page_touched' => $now ), + array( + 'page_id IN (' . $this->mDb->makeList( $ids ) . ')', + 'page_touched < ' . $this->mDb->addQuotes( $now ) + ), $fname + ); + } - #------------------------------------------------------------------------------ - # Category links - if( $wgUseCategoryMagic ) { - global $messageMemc, $wgDBname; - - # Get addition list - $add = $wgLinkCache->getCategoryLinks(); - - # select existing catlinks for this page - $res = $dbw->select( 'categorylinks', - array( 'cl_to', 'cl_sortkey' ), - array( 'cl_from' => $this->mId ), - $fname, - 'FOR UPDATE' ); - - $del = array(); - if( 0 != $dbw->numRows( $res ) ) { - while( $row = $dbw->fetchObject( $res ) ) { - if( !isset( $add[$row->cl_to] ) || $add[$row->cl_to] != $row->cl_sortkey ) { - // in the db, but no longer in the page - // or sortkey has changed -> delete - $del[] = $row->cl_to; - } else { - // remove already existing category memberships - // from the add array - unset( $add[$row->cl_to] ); - } - } + function invalidateCategories( $cats ) { + $this->invalidatePages( NS_CATEGORY, array_keys( $cats ) ); + } + + function invalidateImageDescriptions( $images ) { + $this->invalidatePages( NS_IMAGE, array_keys( $images ) ); + } + + function dumbTableUpdate( $table, $insertions, $fromField ) { + $fname = 'LinksUpdate::dumbTableUpdate'; + $this->mDb->delete( $table, array( $fromField => $this->mId ), $fname ); + if ( count( $insertions ) ) { + # The link array was constructed without FOR UPDATE, so there may be collisions + # This may cause minor link table inconsistencies, which is better than + # crippling the site with lock contention. + $this->mDb->insert( $table, $insertions, $fname, array( 'IGNORE' ) ); + } + } + + /** + * Make a WHERE clause from a 2-d NS/dbkey array + * + * @param array $arr 2-d array indexed by namespace and DB key + * @param string $prefix Field name prefix, without the underscore + */ + function makeWhereFrom2d( &$arr, $prefix ) { + $lb = new LinkBatch; + $lb->setArray( $arr ); + return $lb->constructSet( $prefix, $this->mDb ); + } + + /** + * Update a table by doing a delete query then an insert query + * @private + */ + function incrTableUpdate( $table, $prefix, $deletions, $insertions ) { + $fname = 'LinksUpdate::incrTableUpdate'; + $where = array( "{$prefix}_from" => $this->mId ); + if ( $table == 'pagelinks' || $table == 'templatelinks' ) { + $clause = $this->makeWhereFrom2d( $deletions, $prefix ); + if ( $clause ) { + $where[] = $clause; + } else { + $where = false; } - - // delete any removed categorylinks - if( count( $del ) > 0) { - // delete old ones - $dbw->delete( 'categorylinks', - array( - 'cl_from' => $this->mId, - 'cl_to' => $del ), - $fname ); - foreach( $del as $cname ){ - $nt = Title::makeTitle( NS_CATEGORY, $cname ); - $nt->invalidateCache(); - // update the timestamp which indicates when the last article - // was added or removed to/from this article - $key = $wgDBname . ':Category:' . md5( $nt->getDBkey() ) . ':adddeltimestamp'; - $messageMemc->set( $key , wfTimestamp( TS_MW ), 24*3600 ); - } + } else { + if ( $table == 'langlinks' ) { + $toField = 'll_lang'; + } else { + $toField = $prefix . '_to'; } - - // add any new category memberships - if( count( $add ) > 0 ) { - $arr = array(); - foreach( $add as $cname => $sortkey ) { - $nt = Title::makeTitle( NS_CATEGORY, $cname ); - $nt->invalidateCache(); - // update the timestamp which indicates when the last article - // was added or removed to/from this article - $key = $wgDBname . ':Category:' . md5( $nt->getDBkey() ) . ':adddeltimestamp'; - $messageMemc->set( $key , wfTimestamp( TS_MW ), 24*3600 ); - array_push( $arr, array( - 'cl_from' => $this->mId, - 'cl_to' => $cname, - 'cl_sortkey' => $sortkey ) ); - } - // do the actual sql insertion - $dbw->insert( 'categorylinks', $arr, $fname, array( 'IGNORE' ) ); + if ( count( $deletions ) ) { + $where[] = "$toField IN (" . $this->mDb->makeList( array_keys( $deletions ) ) . ')'; + } else { + $where = false; } } - - wfProfileOut( $fname ); + if ( $where ) { + $this->mDb->delete( $table, $where, $fname ); + } + if ( count( $insertions ) ) { + $this->mDb->insert( $table, $insertions, $fname, 'IGNORE' ); + } } + /** - * Link update which clears the previous entries and inserts new ones - * May be slower or faster depending on level of lock contention and write speed of DB - * Also useful where link table corruption needs to be repaired, e.g. in refreshLinks.php + * Get an array of pagelinks insertions for passing to the DB + * Skips the titles specified by the 2-D array $existing + * @private */ - function doDumbUpdate() { - global $wgLinkCache, $wgDBtransactions, $wgUseCategoryMagic; - $fname = 'LinksUpdate::doDumbUpdate'; - wfProfileIn( $fname ); - - - $dbw =& wfGetDB( DB_MASTER ); - $pagelinks = $dbw->tableName( 'pagelinks' ); - $imagelinks = $dbw->tableName( 'imagelinks' ); - $categorylinks = $dbw->tableName( 'categorylinks' ); - - $dbw->delete('pagelinks', array('pl_from'=>$this->mId),$fname); - - $a = $wgLinkCache->getPageLinks(); - if ( 0 != count( $a ) ) { - $arr = array(); - foreach( $a as $lt => $target ) { - array_push( $arr, array( + function getLinkInsertions( $existing = array() ) { + $arr = array(); + foreach( $this->mLinks as $ns => $dbkeys ) { + # array_diff_key() was introduced in PHP 5.1, there is a compatibility function + # in GlobalFunctions.php + $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys; + foreach ( $diffs as $dbk => $id ) { + $arr[] = array( 'pl_from' => $this->mId, - 'pl_namespace' => $target->getNamespace(), - 'pl_title' => $target->getDBkey() ) ); + 'pl_namespace' => $ns, + 'pl_title' => $dbk + ); + } + } + return $arr; + } + + /** + * Get an array of template insertions. Like getLinkInsertions() + * @private + */ + function getTemplateInsertions( $existing = array() ) { + $arr = array(); + foreach( $this->mTemplates as $ns => $dbkeys ) { + $diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys; + foreach ( $diffs as $dbk => $id ) { + $arr[] = array( + 'tl_from' => $this->mId, + 'tl_namespace' => $ns, + 'tl_title' => $dbk + ); } - $dbw->insert( 'pagelinks', $arr, $fname, array( 'IGNORE' ) ); } + return $arr; + } - $dbw->delete('imagelinks', array('il_from'=>$this->mId),$fname); + /** + * Get an array of image insertions + * Skips the names specified in $existing + * @private + */ + function getImageInsertions( $existing = array() ) { + $arr = array(); + $diffs = array_diff_key( $this->mImages, $existing ); + foreach( $diffs as $iname => $dummy ) { + $arr[] = array( + 'il_from' => $this->mId, + 'il_to' => $iname + ); + } + return $arr; + } - $a = $wgLinkCache->getImageLinks(); - $sql = ''; - if ( 0 != count ( $a ) ) { - $arr = array(); - foreach( $a as $iname => $val ) - array_push( $arr, array( - 'il_from' => $this->mId, - 'il_to' => $iname ) ); - $dbw->insert( 'imagelinks', $arr, $fname, array( 'IGNORE' ) ); - } - - if( $wgUseCategoryMagic ) { - $dbw->delete('categorylinks', array('cl_from'=>$this->mId),$fname); - - # Get addition list - $add = $wgLinkCache->getCategoryLinks(); - - # Do the insertion - $sql = ''; - if ( 0 != count ( $add ) ) { - $arr = array(); - foreach( $add as $cname => $sortkey ) { - # FIXME: Change all this to avoid unnecessary duplication - $nt = Title::makeTitle( NS_CATEGORY, $cname ); - if( !$nt ) continue; - $nt->invalidateCache(); - array_push( $arr, array( - 'cl_from' => $this->mId, - 'cl_to' => $cname, - 'cl_sortkey' => $sortkey ) ); - } - $dbw->insert( 'categorylinks', $arr, $fname, array( 'IGNORE' ) ); + /** + * Get an array of externallinks insertions. Skips the names specified in $existing + * @private + */ + function getExternalInsertions( $existing = array() ) { + $arr = array(); + $diffs = array_diff_key( $this->mExternals, $existing ); + foreach( $diffs as $url => $dummy ) { + $arr[] = array( + 'el_from' => $this->mId, + 'el_to' => $url, + 'el_index' => wfMakeUrlIndex( $url ), + ); + } + return $arr; + } + + /** + * Get an array of category insertions + * @param array $existing Array mapping existing category names to sort keys. If both + * match a link in $this, the link will be omitted from the output + * @private + */ + function getCategoryInsertions( $existing = array() ) { + $diffs = array_diff_assoc( $this->mCategories, $existing ); + $arr = array(); + foreach ( $diffs as $name => $sortkey ) { + $arr[] = array( + 'cl_from' => $this->mId, + 'cl_to' => $name, + 'cl_sortkey' => $sortkey, + 'cl_timestamp' => $this->mDb->timestamp() + ); + } + return $arr; + } + + /** + * Get an array of interlanguage link insertions + * @param array $existing Array mapping existing language codes to titles + * @private + */ + function getInterlangInsertions( $existing = array() ) { + $diffs = array_diff_assoc( $this->mInterlangs, $existing ); + $arr = array(); + foreach( $diffs as $lang => $title ) { + $arr[] = array( + 'll_from' => $this->mId, + 'll_lang' => $lang, + 'll_title' => $title + ); + } + return $arr; + } + + /** + * Given an array of existing links, returns those links which are not in $this + * and thus should be deleted. + * @private + */ + function getLinkDeletions( $existing ) { + $del = array(); + foreach ( $existing as $ns => $dbkeys ) { + if ( isset( $this->mLinks[$ns] ) ) { + $del[$ns] = array_diff_key( $existing[$ns], $this->mLinks[$ns] ); + } else { + $del[$ns] = $existing[$ns]; } } - wfProfileOut( $fname ); + return $del; + } + + /** + * Given an array of existing templates, returns those templates which are not in $this + * and thus should be deleted. + * @private + */ + function getTemplateDeletions( $existing ) { + $del = array(); + foreach ( $existing as $ns => $dbkeys ) { + if ( isset( $this->mTemplates[$ns] ) ) { + $del[$ns] = array_diff_key( $existing[$ns], $this->mTemplates[$ns] ); + } else { + $del[$ns] = $existing[$ns]; + } + } + return $del; + } + + /** + * Given an array of existing images, returns those images which are not in $this + * and thus should be deleted. + * @private + */ + function getImageDeletions( $existing ) { + return array_diff_key( $existing, $this->mImages ); + } + + /** + * Given an array of existing external links, returns those links which are not + * in $this and thus should be deleted. + * @private + */ + function getExternalDeletions( $existing ) { + return array_diff_key( $existing, $this->mExternals ); + } + + /** + * Given an array of existing categories, returns those categories which are not in $this + * and thus should be deleted. + * @private + */ + function getCategoryDeletions( $existing ) { + return array_diff_assoc( $existing, $this->mCategories ); + } + + /** + * Given an array of existing interlanguage links, returns those links which are not + * in $this and thus should be deleted. + * @private + */ + function getInterlangDeletions( $existing ) { + return array_diff_assoc( $existing, $this->mInterlangs ); + } + + /** + * Get an array of existing links, as a 2-D array + * @private + */ + function getExistingLinks() { + $fname = 'LinksUpdate::getExistingLinks'; + $res = $this->mDb->select( 'pagelinks', array( 'pl_namespace', 'pl_title' ), + array( 'pl_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + if ( !isset( $arr[$row->pl_namespace] ) ) { + $arr[$row->pl_namespace] = array(); + } + $arr[$row->pl_namespace][$row->pl_title] = 1; + } + $this->mDb->freeResult( $res ); + return $arr; + } + + /** + * Get an array of existing templates, as a 2-D array + * @private + */ + function getExistingTemplates() { + $fname = 'LinksUpdate::getExistingTemplates'; + $res = $this->mDb->select( 'templatelinks', array( 'tl_namespace', 'tl_title' ), + array( 'tl_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + if ( !isset( $arr[$row->tl_namespace] ) ) { + $arr[$row->tl_namespace] = array(); + } + $arr[$row->tl_namespace][$row->tl_title] = 1; + } + $this->mDb->freeResult( $res ); + return $arr; + } + + /** + * Get an array of existing images, image names in the keys + * @private + */ + function getExistingImages() { + $fname = 'LinksUpdate::getExistingImages'; + $res = $this->mDb->select( 'imagelinks', array( 'il_to' ), + array( 'il_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + $arr[$row->il_to] = 1; + } + $this->mDb->freeResult( $res ); + return $arr; + } + + /** + * Get an array of existing external links, URLs in the keys + * @private + */ + function getExistingExternals() { + $fname = 'LinksUpdate::getExistingExternals'; + $res = $this->mDb->select( 'externallinks', array( 'el_to' ), + array( 'el_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + $arr[$row->el_to] = 1; + } + $this->mDb->freeResult( $res ); + return $arr; + } + + /** + * Get an array of existing categories, with the name in the key and sort key in the value. + * @private + */ + function getExistingCategories() { + $fname = 'LinksUpdate::getExistingCategories'; + $res = $this->mDb->select( 'categorylinks', array( 'cl_to', 'cl_sortkey' ), + array( 'cl_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + $arr[$row->cl_to] = $row->cl_sortkey; + } + $this->mDb->freeResult( $res ); + return $arr; + } + + /** + * Get an array of existing interlanguage links, with the language code in the key and the + * title in the value. + * @private + */ + function getExistingInterlangs() { + $fname = 'LinksUpdate::getExistingInterlangs'; + $res = $this->mDb->select( 'langlinks', array( 'll_lang', 'll_title' ), + array( 'll_from' => $this->mId ), $fname, $this->mOptions ); + $arr = array(); + while ( $row = $this->mDb->fetchObject( $res ) ) { + $arr[$row->ll_lang] = $row->ll_title; + } + return $arr; } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/LoadBalancer.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/LoadBalancer.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/LoadBalancer.php 2005-08-17 16:09:53.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/LoadBalancer.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,56 +1,28 @@ mServers = array(); - $this->mConnections = array(); - $this->mFailFunction = false; - $this->mReadIndex = -1; - $this->mForce = -1; - $this->mLastIndex = -1; - $this->mErrorConnection = false; - } + /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error'; - function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 ) - { - $lb = new LoadBalancer; - $lb->initialise( $servers, $failFunction, $waitTimeout ); - return $lb; - } + /** + * Scale polling time so that under overload conditions, the database server + * receives a SHOW STATUS query at an average interval of this many microseconds + */ + const AVG_STATUS_POLL = 2000; - function initialise( $servers, $failFunction = false, $waitTimeout = 10 ) + function __construct( $servers, $failFunction = false, $waitTimeout = 10, $waitForMasterNow = false ) { $this->mServers = $servers; $this->mFailFunction = $failFunction; @@ -58,12 +30,14 @@ $this->mWriteIndex = -1; $this->mForce = -1; $this->mConnections = array(); - $this->mLastIndex = 1; + $this->mLastIndex = -1; $this->mLoads = array(); $this->mWaitForFile = false; $this->mWaitForPos = false; $this->mWaitTimeout = $waitTimeout; $this->mLaggedSlaveMode = false; + $this->mErrorConnection = false; + $this->mAllowLag = false; foreach( $servers as $i => $server ) { $this->mLoads[$i] = $server['load']; @@ -75,9 +49,17 @@ $this->mGroupLoads[$group][$i] = $ratio; } } - } + } + if ( $waitForMasterNow ) { + $this->loadMasterPos(); + } } - + + static function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 ) + { + return new LoadBalancer( $servers, $failFunction, $waitTimeout ); + } + /** * Given an array of non-normalised probabilities, this function will select * an element and return the appropriate key @@ -88,21 +70,17 @@ return false; } - $sum = 0; - foreach ( $weights as $w ) { - $sum += $w; - } - + $sum = array_sum( $weights ); if ( $sum == 0 ) { # No loads on any of them - # Just pick one at random - foreach ( $weights as $i => $w ) { - $weights[$i] = 1; - } + # In previous versions, this triggered an unweighted random selection, + # but this feature has been removed as of April 2006 to allow for strict + # separation of query groups. + return false; } $max = mt_getrandmax(); $rand = mt_rand(0, $max) / $max * $sum; - + $sum = 0; foreach ( $weights as $i => $w ) { $sum += $w; @@ -117,12 +95,13 @@ # Unset excessively lagged servers $lags = $this->getLagTimes(); foreach ( $lags as $i => $lag ) { - if ( isset( $this->mServers[$i]['max lag'] ) && $lag > $this->mServers[$i]['max lag'] ) { + if ( $i != 0 && isset( $this->mServers[$i]['max lag'] ) && + ( $lag === false || $lag > $this->mServers[$i]['max lag'] ) ) + { unset( $loads[$i] ); } } - # Find out if all the slaves with non-zero load are lagged $sum = 0; foreach ( $loads as $load ) { @@ -131,9 +110,9 @@ if ( $sum == 0 ) { # No appropriate DB servers except maybe the master and some slaves with zero load # Do NOT use the master - # Instead, this function will return false, triggering read-only mode, + # Instead, this function will return false, triggering read-only mode, # and a lagged slave will be used instead. - unset ( $loads[0] ); + return false; } if ( count( $loads ) == 0 ) { @@ -148,14 +127,13 @@ /** * Get the index of the reader connection, which may be a slave - * This takes into account load ratios and lag times. It should + * This takes into account load ratios and lag times. It should * always return a consistent index during a given invocation * * Side effect: opens connections to databases */ - function getReaderIndex() - { - global $wgMaxLag, $wgReadOnly, $wgDBClusterTimeout; + function getReaderIndex() { + global $wgReadOnly, $wgDBClusterTimeout; $fname = 'LoadBalancer::getReaderIndex'; wfProfileIn( $fname ); @@ -163,6 +141,9 @@ $i = false; if ( $this->mForce >= 0 ) { $i = $this->mForce; + } elseif ( count( $this->mServers ) == 1 ) { + # Skip the load balancing if there's only one server + $i = 0; } else { if ( $this->mReadIndex >= 0 ) { $i = $this->mReadIndex; @@ -173,38 +154,43 @@ $done = false; $totalElapsed = 0; do { - if ( $wgReadOnly ) { + if ( $wgReadOnly or $this->mAllowLagged ) { $i = $this->pickRandom( $loads ); } else { $i = $this->getRandomNonLagged( $loads ); if ( $i === false && count( $loads ) != 0 ) { # All slaves lagged. Switch to read-only mode - $wgReadOnly = wfMsgNoDB( 'readonly_lag' ); + $wgReadOnly = wfMsgNoDBForContent( 'readonly_lag' ); $i = $this->pickRandom( $loads ); } } $serverIndex = $i; if ( $i !== false ) { - wfDebugLog( 'connect', "Using reader #$i: {$this->mServers[$i]['host']}...\n" ); + wfDebugLog( 'connect', "$fname: Using reader #$i: {$this->mServers[$i]['host']}...\n" ); $this->openConnection( $i ); - + if ( !$this->isOpen( $i ) ) { - wfDebug( "Failed\n" ); + wfDebug( "$fname: Failed\n" ); unset( $loads[$i] ); $sleepTime = 0; } else { - $status = $this->mConnections[$i]->getStatus(); - if ( isset( $this->mServers[$i]['max threads'] ) && - $status['Threads_running'] > $this->mServers[$i]['max threads'] ) - { - # Slave is lagged, wait for a while - $sleepTime = 5000 * $status['Threads_connected']; + if ( isset( $this->mServers[$i]['max threads'] ) ) { + $status = $this->mConnections[$i]->getStatus("Thread%"); + if ( $status['Threads_running'] > $this->mServers[$i]['max threads'] ) { + # Too much load, back off and wait for a while. + # The sleep time is scaled by the number of threads connected, + # to produce a roughly constant global poll rate. + $sleepTime = self::AVG_STATUS_POLL * $status['Threads_connected']; # If we reach the timeout and exit the loop, don't use it $i = false; - } else { + } else { $done = true; $sleepTime = 0; + } + } else { + $done = true; + $sleepTime = 0; } } } else { @@ -212,13 +198,18 @@ } if ( $sleepTime ) { $totalElapsed += $sleepTime; - $x = "{$this->mServers[$serverIndex]['host']} $sleepTime [$serverIndex]"; + $x = "{$this->mServers[$serverIndex]['host']} [$serverIndex]"; wfProfileIn( "$fname-sleep $x" ); usleep( $sleepTime ); wfProfileOut( "$fname-sleep $x" ); } } while ( count( $loads ) && !$done && $totalElapsed / 1e6 < $wgDBClusterTimeout ); + if ( $totalElapsed / 1e6 >= $wgDBClusterTimeout ) { + $this->mErrorConnection = false; + $this->mLastError = 'All servers busy'; + } + if ( $i !== false && $this->isOpen( $i ) ) { # Wait for the session master pos for a short time if ( $this->mWaitForFile ) { @@ -237,7 +228,7 @@ wfProfileOut( $fname ); return $i; } - + /** * Get a random server to use in a query group */ @@ -250,7 +241,7 @@ wfDebug( "Query group $group => $i\n" ); return $i; } - + /** * Set the master wait position * If a DB_SLAVE connection has been opened already, waits @@ -274,7 +265,7 @@ $this->mServers[$i]['slave pos'] = $this->mConnections[$i]->getSlavePos(); $this->mLaggedSlaveMode = true; } - } + } } wfProfileOut( $fname ); } @@ -284,7 +275,7 @@ */ function doWait( $index ) { global $wgMemc; - + $retVal = false; # Debugging hacks @@ -319,28 +310,40 @@ } } return $retVal; - } + } /** * Get a connection by index */ function &getConnection( $i, $fail = true, $groups = array() ) { + global $wgDBtype; $fname = 'LoadBalancer::getConnection'; wfProfileIn( $fname ); - + + # Query groups - $groupIndex = false; - foreach ( $groups as $group ) { - $groupIndex = $this->getGroupIndex( $group ); + if ( !is_array( $groups ) ) { + $groupIndex = $this->getGroupIndex( $groups, $i ); if ( $groupIndex !== false ) { $i = $groupIndex; - break; } + } else { + foreach ( $groups as $group ) { + $groupIndex = $this->getGroupIndex( $group, $i ); + if ( $groupIndex !== false ) { + $i = $groupIndex; + break; + } + } + } + + # For now, only go through all this for mysql databases + if ($wgDBtype != 'mysql') { + $i = $this->getWriterIndex(); } - # Operation-based index - if ( $i == DB_SLAVE ) { + elseif ( $i == DB_SLAVE ) { $i = $this->getReaderIndex(); } elseif ( $i == DB_MASTER ) { $i = $this->getWriterIndex(); @@ -359,7 +362,7 @@ } # Now we have an explicit index into the servers array $this->openConnection( $i, $fail ); - + wfProfileOut( $fname ); return $this->mConnections[$i]; } @@ -368,7 +371,7 @@ * Open a connection to the server given by the specified index * Index must be an actual index into the array * Returns success - * @private + * @access private */ function openConnection( $i, $fail = false ) { $fname = 'LoadBalancer::openConnection'; @@ -378,6 +381,7 @@ if ( !$this->isOpen( $i ) ) { $this->mConnections[$i] = $this->reallyOpenConnection( $this->mServers[$i] ); } + if ( !$this->isOpen( $i ) ) { wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" ); if ( $fail ) { @@ -394,80 +398,102 @@ /** * Test if the specified index represents an open connection - * @private + * @access private */ function isOpen( $index ) { if( !is_integer( $index ) ) { return false; } - if ( array_key_exists( $index, $this->mConnections ) && is_object( $this->mConnections[$index] ) && - $this->mConnections[$index]->isOpen() ) + if ( array_key_exists( $index, $this->mConnections ) && is_object( $this->mConnections[$index] ) && + $this->mConnections[$index]->isOpen() ) { return true; } else { return false; } } - + /** * Really opens a connection - * @private + * @access private */ function reallyOpenConnection( &$server ) { if( !is_array( $server ) ) { - wfDebugDieBacktrace( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' ); + throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' ); } - + extract( $server ); # Get class for this database type $class = 'Database' . ucfirst( $type ); - if ( !class_exists( $class ) ) { - require_once( "$class.php" ); - } # Create object - return new $class( $host, $user, $password, $dbname, 1, $flags ); + $db = new $class( $host, $user, $password, $dbname, 1, $flags ); + $db->setLBInfo( $server ); + return $db; } - + function reportConnectionError( &$conn ) { $fname = 'LoadBalancer::reportConnectionError'; wfProfileIn( $fname ); # Prevent infinite recursion - + static $reporting = false; if ( !$reporting ) { $reporting = true; if ( !is_object( $conn ) ) { + // No last connection, probably due to all servers being too busy $conn = new Database; - } - if ( $this->mFailFunction ) { - $conn->failFunction( $this->mFailFunction ); + if ( $this->mFailFunction ) { + $conn->failFunction( $this->mFailFunction ); + $conn->reportConnectionError( $this->mLastError ); + } else { + // If all servers were busy, mLastError will contain something sensible + throw new DBConnectionError( $conn, $this->mLastError ); + } } else { - $conn->failFunction( false ); + if ( $this->mFailFunction ) { + $conn->failFunction( $this->mFailFunction ); + } else { + $conn->failFunction( false ); + } + $server = $conn->getProperty( 'mServer' ); + $conn->reportConnectionError( "{$this->mLastError} ({$server})" ); } - $conn->reportConnectionError(); $reporting = false; } wfProfileOut( $fname ); } - - function getWriterIndex() - { + + function getWriterIndex() { return 0; } - function force( $i ) - { + /** + * Force subsequent calls to getConnection(DB_SLAVE) to return the + * given index. Set to -1 to restore the original load balancing + * behaviour. I thought this was a good idea when I originally + * wrote this class, but it has never been used. + */ + function force( $i ) { $this->mForce = $i; } - function haveIndex( $i ) - { + /** + * Returns true if the specified index is a valid server index + */ + function haveIndex( $i ) { return array_key_exists( $i, $this->mServers ); } /** + * Returns true if the specified index is valid and has non-zero load + */ + function isNonZeroLoad( $i ) { + return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0; + } + + /** * Get the number of defined servers (not the number of open connections) */ function getServerCount() { @@ -478,9 +504,8 @@ * Save master pos to the session and to memcached, if the session exists */ function saveMasterPos() { - global $wgSessionStarted; - if ( $wgSessionStarted && count( $this->mServers ) > 1 ) { - # If this entire request was served from a slave without opening a connection to the + if ( session_id() != '' && count( $this->mServers ) > 1 ) { + # If this entire request was served from a slave without opening a connection to the # master (however unlikely that may be), then we can fetch the position from the slave. if ( empty( $this->mConnections[0] ) ) { $conn =& $this->getConnection( DB_SLAVE ); @@ -536,6 +561,13 @@ return $this->mLaggedSlaveMode; } + /* Disables/enables lag checks */ + function allowLagged($mode=null) { + if ($mode===null) + return $this->mAllowLagged; + $this->mAllowLagged=$mode; + } + function pingAll() { $success = true; foreach ( $this->mConnections as $i => $conn ) { @@ -566,44 +598,52 @@ } return array( $host, $maxLag ); } - + /** * Get lag time for each DB * Results are cached for a short time in memcached */ function getLagTimes() { + wfProfileIn( __METHOD__ ); $expiry = 5; $requestRate = 10; global $wgMemc; - $times = $wgMemc->get( 'lag_times' ); + $times = $wgMemc->get( wfMemcKey( 'lag_times' ) ); if ( $times ) { # Randomly recache with probability rising over $expiry $elapsed = time() - $times['timestamp']; $chance = max( 0, ( $expiry - $elapsed ) * $requestRate ); if ( mt_rand( 0, $chance ) != 0 ) { unset( $times['timestamp'] ); + wfProfileOut( __METHOD__ ); return $times; } + wfIncrStats( 'lag_cache_miss_expired' ); + } else { + wfIncrStats( 'lag_cache_miss_absent' ); } # Cache key missing or expired $times = array(); foreach ( $this->mServers as $i => $conn ) { - if ( $this->openConnection( $i ) ) { + if ($i==0) { # Master + $times[$i] = 0; + } elseif ( $this->openConnection( $i ) ) { $times[$i] = $this->mConnections[$i]->getLag(); } } # Add a timestamp key so we know when it was cached $times['timestamp'] = time(); - $wgMemc->set( 'lag_times', $times, $expiry ); + $wgMemc->set( wfMemcKey( 'lag_times' ), $times, $expiry ); # But don't give the timestamp to the caller unset($times['timestamp']); + wfProfileOut( __METHOD__ ); return $times; } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/LogPage.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/LogPage.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/LogPage.php 2005-07-22 14:55:58.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/LogPage.php 2007-08-06 03:14:29.000000000 -0400 @@ -2,26 +2,25 @@ # # Copyright (C) 2002, 2004 Brion Vibber # http://www.mediawiki.org/ -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** * Contain log classes * - * @package MediaWiki */ /** @@ -29,171 +28,185 @@ * The logs are now kept in a table which is easier to manage and trim * than ever-growing wiki pages. * - * @package MediaWiki */ class LogPage { - /* private */ var $type, $action, $comment, $params, $target; - var $updateRecentChanges = true; + /* @access private */ + var $type, $action, $comment, $params, $target; + /* @acess public */ + var $updateRecentChanges; /** * Constructor * * @param string $type One of '', 'block', 'protect', 'rights', 'delete', * 'upload', 'move' + * @param bool $rc Whether to update recent changes as well as the logging table */ - function LogPage( $type ) { + function __construct( $type, $rc = true ) { $this->type = $type; + $this->updateRecentChanges = $rc; } function saveContent() { - if( wfReadOnly() ) return; + if( wfReadOnly() ) return false; global $wgUser; $fname = 'LogPage::saveContent'; - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $uid = $wgUser->getID(); + $log_id = $dbw->nextSequenceValue( 'log_log_id_seq' ); $this->timestamp = $now = wfTimestampNow(); - $dbw->insert( 'logging', - array( - 'log_type' => $this->type, - 'log_action' => $this->action, - 'log_timestamp' => $dbw->timestamp( $now ), - 'log_user' => $uid, - 'log_namespace' => $this->target->getNamespace(), - 'log_title' => $this->target->getDBkey(), - 'log_comment' => $this->comment, - 'log_params' => $this->params - ), $fname + $data = array( + 'log_type' => $this->type, + 'log_action' => $this->action, + 'log_timestamp' => $dbw->timestamp( $now ), + 'log_user' => $uid, + 'log_namespace' => $this->target->getNamespace(), + 'log_title' => $this->target->getDBkey(), + 'log_comment' => $this->comment, + 'log_params' => $this->params ); - + + # log_id doesn't exist on Wikimedia servers yet, and it's a tricky + # schema update to do. Hack it for now to ignore the field on MySQL. + if ( !is_null( $log_id ) ) { + $data['log_id'] = $log_id; + } + $dbw->insert( 'logging', $data, $fname ); + # And update recentchanges if ( $this->updateRecentChanges ) { - $titleObj = Title::makeTitle( NS_SPECIAL, 'Log/' . $this->type ); - $rcComment = $this->actionText; - if( '' != $this->comment ) { - if ($rcComment == '') - $rcComment = $this->comment; - else - $rcComment .= ': ' . $this->comment; - } - - RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment ); + $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); + $rcComment = $this->getRcComment(); + RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment, '', + $this->type, $this->action, $this->target, $this->comment, $this->params ); } return true; } - /** - * @static - */ - function validTypes() { - static $types = array( '', 'block', 'protect', 'rights', 'delete', 'upload', 'move' ); - wfRunHooks( 'LogPageValidTypes', array( &$types) ); - return $types; + public function getRcComment() { + $rcComment = $this->actionText; + if( '' != $this->comment ) { + if ($rcComment == '') + $rcComment = $this->comment; + else + $rcComment .= ': ' . $this->comment; + } + return $rcComment; } - + /** * @static */ - function validActions( $type ) { - static $actions = array( - '' => NULL, - 'block' => array( 'block', 'unblock' ), - 'protect' => array( 'protect', 'unprotect' ), - 'rights' => array( 'rights' ), - 'delete' => array( 'delete', 'restore' ), - 'upload' => array( 'upload' ), - 'move' => array( 'move' ) - ); - return $actions[$type]; + public static function validTypes() { + global $wgLogTypes; + return $wgLogTypes; } - + /** * @static */ - function isLogType( $type ) { + public static function isLogType( $type ) { return in_array( $type, LogPage::validTypes() ); } - + /** * @static */ - function logName( $type ) { - static $typeText = array( - '' => 'log', - 'block' => 'blocklogpage', - 'protect' => 'protectlogpage', - 'rights' => 'bureaucratlog', - 'delete' => 'dellogpage', - 'upload' => 'uploadlogpage', - 'move' => 'movelogpage' - ); - wfRunHooks( 'LogPageLogName', array( &$typeText) ); - - return str_replace( '_', ' ', wfMsg( $typeText[$type] ) ); + public static function logName( $type ) { + global $wgLogNames; + + if( isset( $wgLogNames[$type] ) ) { + return str_replace( '_', ' ', wfMsg( $wgLogNames[$type] ) ); + } else { + // Bogus log types? Perhaps an extension was removed. + return $type; + } } - + /** + * @todo handle missing log types * @static */ - function logHeader( $type ) { - static $headerText = array( - '' => 'alllogstext', - 'block' => 'blocklogtext', - 'protect' => 'protectlogtext', - 'rights' => 'rightslogtext', - 'delete' => 'dellogpagetext', - 'upload' => 'uploadlogpagetext', - 'move' => 'movelogpagetext' - ); - wfRunHooks( 'LogPageLogHeader', array( &$headerText ) ); - - return wfMsg( $headerText[$type] ); + static function logHeader( $type ) { + global $wgLogHeaders; + return wfMsg( $wgLogHeaders[$type] ); } - + /** * @static */ - function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false ) { - static $actions = array( - 'block/block' => 'blocklogentry', - 'block/unblock' => 'unblocklogentry', - 'protect/protect' => 'protectedarticle', - 'protect/unprotect' => 'unprotectedarticle', - 'rights/rights' => 'bureaucratlogentry', - 'rights/addgroup' => 'addgrouplogentry', - 'rights/rngroup' => 'renamegrouplogentry', - 'rights/chgroup' => 'changegrouplogentry', - 'delete/delete' => 'deletedarticle', - 'delete/restore' => 'undeletedarticle', - 'upload/upload' => 'uploadedimage', - 'upload/revert' => 'uploadedimage', - 'move/move' => '1movedto2', - 'move/move_redir' => '1movedto2_redir' - ); + static function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false, $translate=false ) { + global $wgLang, $wgContLang, $wgLogActions; + $key = "$type/$action"; - if( isset( $actions[$key] ) ) { + + if( $key == 'patrol/patrol' ) + return PatrolLog::makeActionText( $title, $params, $skin ); + + if( isset( $wgLogActions[$key] ) ) { if( is_null( $title ) ) { - $rv=wfMsgForContent( $actions[$key] ); + $rv=wfMsg( $wgLogActions[$key] ); } else { if( $skin ) { - if ( $type == 'move' ) { - $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' ); - // Change $param[0] into a link to the title specified in $param[0] - $movedTo = Title::newFromText( $params[0] ); - $params[0] = $skin->makeLinkObj( $movedTo, $params[0] ); - } else { - $titleLink = $skin->makeLinkObj( $title ); + + switch( $type ) { + case 'move': + $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' ); + $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) ); + break; + case 'block': + if( substr( $title->getText(), 0, 1 ) == '#' ) { + $titleLink = $title->getText(); + } else { + // TODO: Store the user identifier in the parameters + // to make this faster for future log entries + $id = User::idFromName( $title->getText() ); + $titleLink = $skin->userLink( $id, $title->getText() ) + . $skin->userToolLinks( $id, $title->getText(), false, Linker::TOOL_LINKS_NOBLOCK ); + } + break; + case 'rights': + $text = $wgContLang->ucfirst( $title->getText() ); + $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) ); + break; + default: + $titleLink = $skin->makeLinkObj( $title ); } + } else { $titleLink = $title->getPrefixedText(); } + if( $key == 'rights/rights' ) { + if ($skin) { + $rightsnone = wfMsg( 'rightsnone' ); + } else { + $rightsnone = wfMsgForContent( 'rightsnone' ); + } + if( !isset( $params[0] ) || trim( $params[0] ) == '' ) + $params[0] = $rightsnone; + if( !isset( $params[1] ) || trim( $params[1] ) == '' ) + $params[1] = $rightsnone; + } if( count( $params ) == 0 ) { - $rv = wfMsgForContent( $actions[$key], $titleLink ); + if ( $skin ) { + $rv = wfMsg( $wgLogActions[$key], $titleLink ); + } else { + $rv = wfMsgForContent( $wgLogActions[$key], $titleLink ); + } } else { array_unshift( $params, $titleLink ); - $rv = wfMsgReal( $actions[$key], $params, true, true ); + if ( $key == 'block/block' ) { + if ( $translate ) { + $params[1] = $wgLang->translateBlockExpiry( $params[1] ); + } + $params[2] = isset( $params[2] ) + ? self::formatBlockFlags( $params[2] ) + : ''; + } + $rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin ); } } } else { @@ -214,26 +227,26 @@ * @param string $comment Description associated * @param array $params Parameters passed later to wfMsg.* functions */ - function addEntry( $action, &$target, $comment, $params = array() ) { + function addEntry( $action, $target, $comment, $params = array() ) { if ( !is_array( $params ) ) { $params = array( $params ); } - + $this->action = $action; - $this->target =& $target; + $this->target = $target; $this->comment = $comment; $this->params = LogPage::makeParamBlob( $params ); - + $this->actionText = LogPage::actionText( $this->type, $action, $target, NULL, $params ); return $this->saveContent(); } - /** + /** * Create a blob from a parameter array * @static */ - function makeParamBlob( $params ) { + static function makeParamBlob( $params ) { return implode( "\n", $params ); } @@ -241,13 +254,48 @@ * Extract a parameter array from a blob * @static */ - function extractParams( $blob ) { + static function extractParams( $blob ) { if ( $blob === '' ) { return array(); } else { return explode( "\n", $blob ); } } + + /** + * Convert a comma-delimited list of block log flags + * into a more readable (and translated) form + * + * @param $flags Flags to format + * @return string + */ + public static function formatBlockFlags( $flags ) { + $flags = explode( ',', trim( $flags ) ); + if( count( $flags ) > 0 ) { + for( $i = 0; $i < count( $flags ); $i++ ) + $flags[$i] = self::formatBlockFlag( $flags[$i] ); + return '(' . implode( ', ', $flags ) . ')'; + } else { + return ''; + } + } + + /** + * Translate a block log flag if possible + * + * @param $flag Flag to translate + * @return string + */ + public static function formatBlockFlag( $flag ) { + static $messages = array(); + if( !isset( $messages[$flag] ) ) { + $k = 'block-log-flags-' . $flag; + $msg = wfMsg( $k ); + $messages[$flag] = htmlspecialchars( wfEmptyMsg( $k, $msg ) ? $flag : $msg ); + } + return $messages[$flag]; + } + } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/MagicWord.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/MagicWord.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/MagicWord.php 2005-06-25 23:23:22.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/MagicWord.php 2007-08-20 23:57:54.000000000 -0400 @@ -1,108 +1,112 @@ match( $text ) ) - * - * Possible future improvements: + * if (MagicWord::get( 'redirect' )->match( $text ) ) + * + * Possible future improvements: * * Simultaneous searching for a number of magic words - * * $wgMagicWords in shared memory + * * MagicWord::$mObjects in shared memory * - * Please avoid reading the data out of one of these objects and then writing + * Please avoid reading the data out of one of these objects and then writing * special case code. If possible, add another match()-like function here. * - * @package MediaWiki + * To add magic words in an extension, use the LanguageGetMagic hook. For + * magic words which are also Parser variables, add a MagicWordwgVariableIDs + * hook. Use string keys. + * */ class MagicWord { /**#@+ - * @access private + * @private */ var $mId, $mSynonyms, $mCaseSensitive, $mRegex; var $mRegexStart, $mBaseRegex, $mVariableRegex; - var $mModified; + var $mModified, $mFound; + + static public $mVariableIDsInitialised = false; + static public $mVariableIDs = array( + 'currentmonth', + 'currentmonthname', + 'currentmonthnamegen', + 'currentmonthabbrev', + 'currentday', + 'currentday2', + 'currentdayname', + 'currentyear', + 'currenttime', + 'currenthour', + 'localmonth', + 'localmonthname', + 'localmonthnamegen', + 'localmonthabbrev', + 'localday', + 'localday2', + 'localdayname', + 'localyear', + 'localtime', + 'localhour', + 'numberofarticles', + 'numberoffiles', + 'numberofedits', + 'sitename', + 'server', + 'servername', + 'scriptpath', + 'pagename', + 'pagenamee', + 'fullpagename', + 'fullpagenamee', + 'namespace', + 'namespacee', + 'currentweek', + 'currentdow', + 'localweek', + 'localdow', + 'revisionid', + 'revisionday', + 'revisionday2', + 'revisionmonth', + 'revisionyear', + 'revisiontimestamp', + 'subpagename', + 'subpagenamee', + 'displaytitle', + 'talkspace', + 'talkspacee', + 'subjectspace', + 'subjectspacee', + 'talkpagename', + 'talkpagenamee', + 'subjectpagename', + 'subjectpagenamee', + 'numberofusers', + 'newsectionlink', + 'numberofpages', + 'currentversion', + 'basepagename', + 'basepagenamee', + 'urlencode', + 'currenttimestamp', + 'localtimestamp', + 'directionmark', + 'language', + 'contentlanguage', + 'pagesinnamespace', + 'numberofadmins', + 'defaultsort', + ); + + static public $mObjects = array(); + /**#@-*/ - function MagicWord($id = 0, $syn = '', $cs = false) { + function __construct($id = 0, $syn = '', $cs = false) { $this->mId = $id; $this->mSynonyms = (array)$syn; $this->mCaseSensitive = $cs; @@ -117,27 +121,46 @@ * Factory: creates an object representing an ID * @static */ - function &get( $id ) { - global $wgMagicWords; - - if ( !is_array( $wgMagicWords ) ) { - wfDebugDieBacktrace( "Incorrect initialisation order, \$wgMagicWords does not exist\n" ); - } - if (!array_key_exists( $id, $wgMagicWords ) ) { + static function &get( $id ) { + if (!array_key_exists( $id, self::$mObjects ) ) { $mw = new MagicWord(); $mw->load( $id ); - $wgMagicWords[$id] = $mw; + self::$mObjects[$id] = $mw; + } + return self::$mObjects[$id]; + } + + /** + * Get an array of parser variable IDs + */ + static function getVariableIDs() { + if ( !self::$mVariableIDsInitialised ) { + # Deprecated constant definition hook, available for extensions that need it + $magicWords = array(); + wfRunHooks( 'MagicWordMagicWords', array( &$magicWords ) ); + foreach ( $magicWords as $word ) { + define( $word, $word ); + } + + # Get variable IDs + wfRunHooks( 'MagicWordwgVariableIDs', array( &self::$mVariableIDs ) ); + self::$mVariableIDsInitialised = true; } - return $wgMagicWords[$id]; + return self::$mVariableIDs; } - + # Initialises this object with an ID function load( $id ) { - global $wgContLang; + global $wgContLang; $this->mId = $id; $wgContLang->getMagic( $this ); + if ( !$this->mSynonyms ) { + $this->mSynonyms = array( 'dkjsagfjsgashfajsh' ); + #throw new MWException( "Error: invalid magic word '$id'" ); + wfDebugLog( 'exception', "Error: invalid magic word '$id'\n" ); + } } - + /** * Preliminary initialisation * @private @@ -147,16 +170,21 @@ # This was used for matching "$1" variables, but different uses of the feature will have # different restrictions, which should be checked *after* the MagicWord has been matched, # not here. - IMSoP - $escSyn = array_map( 'preg_quote', $this->mSynonyms ); + + $escSyn = array(); + foreach ( $this->mSynonyms as $synonym ) + // In case a magic word contains /, like that's going to happen;) + $escSyn[] = preg_quote( $synonym, '/' ); $this->mBaseRegex = implode( '|', $escSyn ); - $case = $this->mCaseSensitive ? '' : 'i'; + + $case = $this->mCaseSensitive ? '' : 'iu'; $this->mRegex = "/{$this->mBaseRegex}/{$case}"; $this->mRegexStart = "/^(?:{$this->mBaseRegex})/{$case}"; $this->mVariableRegex = str_replace( "\\$1", "(.*?)", $this->mRegex ); - $this->mVariableStartToEndRegex = str_replace( "\\$1", "(.*?)", + $this->mVariableStartToEndRegex = str_replace( "\\$1", "(.*?)", "/^(?:{$this->mBaseRegex})$/{$case}" ); } - + /** * Gets a regex representing matching the word */ @@ -168,6 +196,18 @@ } /** + * Gets the regexp case modifier to use, i.e. i or nothing, to be used if + * one is using MagicWord::getBaseRegex(), otherwise it'll be included in + * the complete expression + */ + function getRegexCase() { + if ( $this->mRegex === '' ) + $this->initRegex(); + + return $this->mCaseSensitive ? '' : 'iu'; + } + + /** * Gets a regex matching the word, if it is at the string start */ function getRegexStart() { @@ -186,7 +226,7 @@ } return $this->mBaseRegex; } - + /** * Returns true if the text contains the word * @return bool @@ -210,16 +250,20 @@ * is one. */ function matchVariableStartToEnd( $text ) { + $matches = array(); $matchcount = preg_match( $this->getVariableStartToEndRegex(), $text, $matches ); if ( $matchcount == 0 ) { return NULL; - } elseif ( count($matches) == 1 ) { - return $matches[0]; } else { - # multiple matched parts (variable match); some will be empty because of synonyms - # the variable will be the second non-empty one so remove any blank elements and re-sort the indices + # multiple matched parts (variable match); some will be empty because of + # synonyms. The variable will be the second non-empty one so remove any + # blank elements and re-sort the indices. + # See also bug 6526 + $matches = array_values(array_filter($matches)); - return $matches[1]; + + if ( count($matches) == 1 ) { return $matches[0]; } + else { return $matches[1]; } } } @@ -229,25 +273,31 @@ * input string, removing all instances of the word */ function matchAndRemove( &$text ) { - global $wgMagicFound; - $wgMagicFound = false; - $text = preg_replace_callback( $this->getRegex(), 'pregRemoveAndRecord', $text ); - return $wgMagicFound; + $this->mFound = false; + $text = preg_replace_callback( $this->getRegex(), array( &$this, 'pregRemoveAndRecord' ), $text ); + return $this->mFound; } function matchStartAndRemove( &$text ) { - global $wgMagicFound; - $wgMagicFound = false; - $text = preg_replace_callback( $this->getRegexStart(), 'pregRemoveAndRecord', $text ); - return $wgMagicFound; - } + $this->mFound = false; + $text = preg_replace_callback( $this->getRegexStart(), array( &$this, 'pregRemoveAndRecord' ), $text ); + return $this->mFound; + } + /** + * Used in matchAndRemove() + * @private + **/ + function pregRemoveAndRecord( ) { + $this->mFound = true; + return ''; + } /** * Replaces the word with something else */ - function replace( $replacement, $subject ) { - $res = preg_replace( $this->getRegex(), $replacement, $subject ); + function replace( $replacement, $subject, $limit=-1 ) { + $res = preg_replace( $this->getRegex(), StringUtils::escapeRegexReplacement( $replacement ), $subject, $limit ); $this->mModified = !($res === $subject); return $res; } @@ -258,7 +308,6 @@ * Input word must contain $1 */ function substituteCallback( $text, $callback ) { - $regex = $this->getVariableRegex(); $res = preg_replace_callback( $this->getVariableRegex(), $callback, $text ); $this->mModified = !($res === $text); return $res; @@ -270,7 +319,7 @@ function getVariableRegex() { if ( $this->mVariableRegex == '' ) { $this->initRegex(); - } + } return $this->mVariableRegex; } @@ -280,7 +329,7 @@ function getVariableStartToEndRegex() { if ( $this->mVariableStartToEndRegex == '' ) { $this->initRegex(); - } + } return $this->mVariableStartToEndRegex; } @@ -291,8 +340,12 @@ return $this->mSynonyms[$i]; } + function getSynonyms() { + return $this->mSynonyms; + } + /** - * Returns true if the last call to replace() or substituteCallback() + * Returns true if the last call to replace() or substituteCallback() * returned a modified text, otherwise false. */ function getWasModified(){ @@ -304,7 +357,7 @@ * This method uses the php feature to do several replacements at the same time, * thereby gaining some efficiency. The result is placed in the out variable * $result. The return value is true if something was replaced. - * @static + * @static **/ function replaceMultiple( $magicarr, $subject, &$result ){ $search = array(); @@ -324,20 +377,190 @@ * lookup in a list of magic words */ function addToArray( &$array, $value ) { + global $wgContLang; foreach ( $this->mSynonyms as $syn ) { - $array[$syn] = $value; + $array[$wgContLang->lc($syn)] = $value; } } + + function isCaseSensitive() { + return $this->mCaseSensitive; + } + + function getId() { + return $this->mId; + } } /** - * Used in matchAndRemove() - * @private - **/ -function pregRemoveAndRecord( $match ) { - global $wgMagicFound; - $wgMagicFound = true; - return ''; -} + * Class for handling an array of magic words + */ +class MagicWordArray { + var $names = array(); + var $hash; + var $baseRegex, $regex; + + function __construct( $names = array() ) { + $this->names = $names; + } -?> + /** + * Add a magic word by name + */ + public function add( $name ) { + global $wgContLang; + $this->names[] = $name; + $this->hash = $this->baseRegex = $this->regex = null; + } + + /** + * Add a number of magic words by name + */ + public function addArray( $names ) { + $this->names = array_merge( $this->names, array_values( $names ) ); + $this->hash = $this->baseRegex = $this->regex = null; + } + + /** + * Get a 2-d hashtable for this array + */ + function getHash() { + if ( is_null( $this->hash ) ) { + global $wgContLang; + $this->hash = array( 0 => array(), 1 => array() ); + foreach ( $this->names as $name ) { + $magic = MagicWord::get( $name ); + $case = intval( $magic->isCaseSensitive() ); + foreach ( $magic->getSynonyms() as $syn ) { + if ( !$case ) { + $syn = $wgContLang->lc( $syn ); + } + $this->hash[$case][$syn] = $name; + } + } + } + return $this->hash; + } + + /** + * Get the base regex + */ + function getBaseRegex() { + if ( is_null( $this->baseRegex ) ) { + $this->baseRegex = array( 0 => '', 1 => '' ); + foreach ( $this->names as $name ) { + $magic = MagicWord::get( $name ); + $case = intval( $magic->isCaseSensitive() ); + foreach ( $magic->getSynonyms() as $i => $syn ) { + $group = "(?P<{$i}_{$name}>" . preg_quote( $syn, '/' ) . ')'; + if ( $this->baseRegex[$case] === '' ) { + $this->baseRegex[$case] = $group; + } else { + $this->baseRegex[$case] .= '|' . $group; + } + } + } + } + return $this->baseRegex; + } + + /** + * Get an unanchored regex + */ + function getRegex() { + if ( is_null( $this->regex ) ) { + $base = $this->getBaseRegex(); + $this->regex = array( '', '' ); + if ( $this->baseRegex[0] !== '' ) { + $this->regex[0] = "/{$base[0]}/iuS"; + } + if ( $this->baseRegex[1] !== '' ) { + $this->regex[1] = "/{$base[1]}/S"; + } + } + return $this->regex; + } + + /** + * Get a regex for matching variables + */ + function getVariableRegex() { + return str_replace( "\\$1", "(.*?)", $this->getRegex() ); + } + + /** + * Get an anchored regex for matching variables + */ + function getVariableStartToEndRegex() { + $base = $this->getBaseRegex(); + $newRegex = array( '', '' ); + if ( $base[0] !== '' ) { + $newRegex[0] = str_replace( "\\$1", "(.*?)", "/^(?:{$base[0]})$/iuS" ); + } + if ( $base[1] !== '' ) { + $newRegex[1] = str_replace( "\\$1", "(.*?)", "/^(?:{$base[1]})$/S" ); + } + return $newRegex; + } + + /** + * Parse a match array from preg_match + */ + function parseMatch( $m ) { + reset( $m ); + while ( list( $key, $value ) = each( $m ) ) { + if ( $key === 0 || $value === '' ) { + continue; + } + $parts = explode( '_', $key, 2 ); + if ( count( $parts ) != 2 ) { + // This shouldn't happen + // continue; + throw new MWException( __METHOD__ . ': bad parameter name' ); + } + list( /* $synIndex */, $magicName ) = $parts; + $paramValue = next( $m ); + return array( $magicName, $paramValue ); + } + // This shouldn't happen either + throw new MWException( __METHOD__.': parameter not found' ); + return array( false, false ); + } + + /** + * Match some text, with parameter capture + * Returns an array with the magic word name in the first element and the + * parameter in the second element. + * Both elements are false if there was no match. + */ + public function matchVariableStartToEnd( $text ) { + global $wgContLang; + $regexes = $this->getVariableStartToEndRegex(); + foreach ( $regexes as $regex ) { + if ( $regex !== '' ) { + $m = false; + if ( preg_match( $regex, $text, $m ) ) { + return $this->parseMatch( $m ); + } + } + } + return array( false, false ); + } + + /** + * Match some text, without parameter capture + * Returns the magic word name, or false if there was no capture + */ + public function matchStartToEnd( $text ) { + $hash = $this->getHash(); + if ( isset( $hash[1][$text] ) ) { + return $hash[1][$text]; + } + global $wgContLang; + $lc = $wgContLang->lc( $text ); + if ( isset( $hash[0][$lc] ) ) { + return $hash[0][$lc]; + } + return false; + } +} diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Math.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Math.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Math.php 2005-06-18 01:09:27.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Math.php 2007-07-13 13:25:06.000000000 -0400 @@ -1,17 +1,15 @@ parsing - * @package MediaWiki */ /** * Takes LaTeX fragments, sends them to a helper program (texvc) for rendering * to rasterized PNG and HTML and MathML approximations. An appropriate * rendering form is picked and returned. - * + * * by Tomasz Wegrzanowski, with additions by Brion Vibber (2003, 2004) * - * @package MediaWiki */ class MathRenderer { var $mode = MW_MATH_MODERN; @@ -21,34 +19,31 @@ var $html = ''; var $mathml = ''; var $conservativeness = 0; - - function MathRenderer( $tex ) { + + function __construct( $tex, $params=array() ) { $this->tex = $tex; + $this->params = $params; } - + function setOutputMode( $mode ) { $this->mode = $mode; } function render() { - global $wgMathDirectory, $wgTmpDirectory, $wgInputEncoding; + global $wgTmpDirectory, $wgInputEncoding; global $wgTexvc; $fname = 'MathRenderer::render'; - + if( $this->mode == MW_MATH_SOURCE ) { # No need to render or parse anything more! return ('$ '.htmlspecialchars( $this->tex ).' $'); } - + if( $this->tex == '' ) { + return; # bug 8372 + } + if( !$this->_recall() ) { # Ensure that the temp and output directories are available before continuing... - if( !file_exists( $wgMathDirectory ) ) { - if( !@mkdir( $wgMathDirectory ) ) { - return $this->_error( 'math_bad_output' ); - } - } elseif( !is_dir( $wgMathDirectory ) || !is_writable( $wgMathDirectory ) ) { - return $this->_error( 'math_bad_output' ); - } if( !file_exists( $wgTmpDirectory ) ) { if( !@mkdir( $wgTmpDirectory ) ) { return $this->_error( 'math_bad_tmpdir' ); @@ -56,51 +51,54 @@ } elseif( !is_dir( $wgTmpDirectory ) || !is_writable( $wgTmpDirectory ) ) { return $this->_error( 'math_bad_tmpdir' ); } - + if( function_exists( 'is_executable' ) && !is_executable( $wgTexvc ) ) { return $this->_error( 'math_notexvc' ); } - $cmd = $wgTexvc . ' ' . + $cmd = $wgTexvc . ' ' . + escapeshellarg( $wgTmpDirectory ).' '. escapeshellarg( $wgTmpDirectory ).' '. - escapeshellarg( $wgMathDirectory ).' '. escapeshellarg( $this->tex ).' '. escapeshellarg( $wgInputEncoding ); - + if ( wfIsWindows() ) { # Invoke it within cygwin sh, because texvc expects sh features in its default shell $cmd = 'sh -c ' . wfEscapeShellArg( $cmd ); - } + } wfDebug( "TeX: $cmd\n" ); $contents = `$cmd`; wfDebug( "TeX output:\n $contents\n---\n" ); - + if (strlen($contents) == 0) { return $this->_error( 'math_unknown_error' ); } - + $retval = substr ($contents, 0, 1); + $errmsg = ''; if (($retval == 'C') || ($retval == 'M') || ($retval == 'L')) { - if ($retval == 'C') + if ($retval == 'C') { $this->conservativeness = 2; - else if ($retval == 'M') + } else if ($retval == 'M') { $this->conservativeness = 1; - else + } else { $this->conservativeness = 0; + } $outdata = substr ($contents, 33); - + $i = strpos($outdata, "\000"); - + $this->html = substr($outdata, 0, $i); $this->mathml = substr($outdata, $i+1); } else if (($retval == 'c') || ($retval == 'm') || ($retval == 'l')) { $this->html = substr ($contents, 33); - if ($retval == 'c') + if ($retval == 'c') { $this->conservativeness = 2; - else if ($retval == 'm') + } else if ($retval == 'm') { $this->conservativeness = 1; - else + } else { $this->conservativeness = 0; + } $this->mathml = NULL; } else if ($retval == 'X') { $this->html = NULL; @@ -113,79 +111,120 @@ } else { $errbit = htmlspecialchars( substr($contents, 1) ); switch( $retval ) { - case 'E': return $this->_error( 'math_lexing_error', $errbit ); - case 'S': return $this->_error( 'math_syntax_error', $errbit ); - case 'F': return $this->_error( 'math_unknown_function', $errbit ); - default: return $this->_error( 'math_unknown_error', $errbit ); + case 'E': $errmsg = $this->_error( 'math_lexing_error', $errbit ); + case 'S': $errmsg = $this->_error( 'math_syntax_error', $errbit ); + case 'F': $errmsg = $this->_error( 'math_unknown_function', $errbit ); + default: $errmsg = $this->_error( 'math_unknown_error', $errbit ); } } - - $this->hash = substr ($contents, 1, 32); + + if ( !$errmsg ) { + $this->hash = substr ($contents, 1, 32); + } + + wfRunHooks( 'MathAfterTexvc', array( &$this, &$errmsg ) ); + + if ( $errmsg ) { + return $errmsg; + } + if (!preg_match("/^[a-f0-9]{32}$/", $this->hash)) { return $this->_error( 'math_unknown_error' ); } - - if( !file_exists( "$wgMathDirectory/{$this->hash}.png" ) ) { + + if( !file_exists( "$wgTmpDirectory/{$this->hash}.png" ) ) { return $this->_error( 'math_image_error' ); } - + + $hashpath = $this->_getHashPath(); + if( !file_exists( $hashpath ) ) { + if( !@wfMkdirParents( $hashpath, 0755 ) ) { + return $this->_error( 'math_bad_output' ); + } + } elseif( !is_dir( $hashpath ) || !is_writable( $hashpath ) ) { + return $this->_error( 'math_bad_output' ); + } + + if( !rename( "$wgTmpDirectory/{$this->hash}.png", "$hashpath/{$this->hash}.png" ) ) { + return $this->_error( 'math_output_error' ); + } + # Now save it back to the DB: if ( !wfReadOnly() ) { $outmd5_sql = pack('H32', $this->hash); - + $md5_sql = pack('H32', $this->md5); # Binary packed, not hex - - $dbw =& wfGetDB( DB_MASTER ); + + $dbw = wfGetDB( DB_MASTER ); $dbw->replace( 'math', array( 'math_inputhash' ), - array( - 'math_inputhash' => $md5_sql, - 'math_outputhash' => $outmd5_sql, + array( + 'math_inputhash' => $dbw->encodeBlob($md5_sql), + 'math_outputhash' => $dbw->encodeBlob($outmd5_sql), 'math_html_conservativeness' => $this->conservativeness, 'math_html' => $this->html, 'math_mathml' => $this->mathml, - ), $fname, array( 'IGNORE' ) + ), $fname, array( 'IGNORE' ) ); } - + } - + return $this->_doRender(); } - + function _error( $msg, $append = '' ) { $mf = htmlspecialchars( wfMsg( 'math_failure' ) ); - $munk = htmlspecialchars( wfMsg( 'math_unknown_error' ) ); $errmsg = htmlspecialchars( wfMsg( $msg ) ); - $source = htmlspecialchars($this->tex); + $source = htmlspecialchars( str_replace( "\n", ' ', $this->tex ) ); return "$mf ($errmsg$append): $source\n"; } - + function _recall() { global $wgMathDirectory; $fname = 'MathRenderer::_recall'; $this->md5 = md5( $this->tex ); - $dbr =& wfGetDB( DB_SLAVE ); - $rpage = $dbr->selectRow( 'math', + $dbr = wfGetDB( DB_SLAVE ); + $rpage = $dbr->selectRow( 'math', array( 'math_outputhash','math_html_conservativeness','math_html','math_mathml' ), - array( 'math_inputhash' => pack("H32", $this->md5)), # Binary packed, not hex + array( 'math_inputhash' => $dbr->encodeBlob(pack("H32", $this->md5))), # Binary packed, not hex $fname ); if( $rpage !== false ) { # Tailing 0x20s can get dropped by the database, add it back on if necessary: - $xhash = unpack( 'H32md5', $rpage->math_outputhash . " " ); + $xhash = unpack( 'H32md5', $dbr->decodeBlob($rpage->math_outputhash) . " " ); $this->hash = $xhash ['md5']; - + $this->conservativeness = $rpage->math_html_conservativeness; $this->html = $rpage->math_html; $this->mathml = $rpage->math_mathml; - - if( file_exists( "$wgMathDirectory/{$this->hash}.png" ) ) { + + if( file_exists( $this->_getHashPath() . "/{$this->hash}.png" ) ) { return true; } + + if( file_exists( $wgMathDirectory . "/{$this->hash}.png" ) ) { + $hashpath = $this->_getHashPath(); + + if( !file_exists( $hashpath ) ) { + if( !@wfMkdirParents( $hashpath, 0755 ) ) { + return false; + } + } elseif( !is_dir( $hashpath ) || !is_writable( $hashpath ) ) { + return false; + } + if ( function_exists( "link" ) ) { + return link ( $wgMathDirectory . "/{$this->hash}.png", + $hashpath . "/{$this->hash}.png" ); + } else { + return rename ( $wgMathDirectory . "/{$this->hash}.png", + $hashpath . "/{$this->hash}.png" ); + } + } + } - + # Missing from the database and/or the render cache return false; } @@ -195,31 +234,60 @@ */ function _doRender() { if( $this->mode == MW_MATH_MATHML && $this->mathml != '' ) { - return "{$this->mathml}"; + return Xml::tags( 'math', + $this->_attribs( 'math', + array( 'xmlns' => 'http://www.w3.org/1998/Math/MathML' ) ), + $this->mathml ); } if (($this->mode == MW_MATH_PNG) || ($this->html == '') || (($this->mode == MW_MATH_SIMPLE) && ($this->conservativeness != 2)) || (($this->mode == MW_MATH_MODERN || $this->mode == MW_MATH_MATHML) && ($this->conservativeness == 0))) { return $this->_linkToMathImage(); } else { - return ''.$this->html.''; + return Xml::tags( 'span', + $this->_attribs( 'span', + array( 'class' => 'texhtml' ) ), + $this->html ); } } + + function _attribs( $tag, $defaults=array(), $overrides=array() ) { + $attribs = Sanitizer::validateTagAttributes( $this->params, $tag ); + $attribs = Sanitizer::mergeAttributes( $defaults, $attribs ); + $attribs = Sanitizer::mergeAttributes( $attribs, $overrides ); + return $attribs; + } function _linkToMathImage() { global $wgMathPath; - $url = htmlspecialchars( "$wgMathPath/{$this->hash}.png" ); - $alt = trim(str_replace("\n", ' ', htmlspecialchars( $this->tex ))); - return "\"$alt\""; + $url = "$wgMathPath/" . substr($this->hash, 0, 1) + .'/'. substr($this->hash, 1, 1) .'/'. substr($this->hash, 2, 1) + . "/{$this->hash}.png"; + + return Xml::element( 'img', + $this->_attribs( + 'img', + array( + 'class' => 'tex', + 'alt' => $this->tex ), + array( + 'src' => $url ) ) ); } -} + function _getHashPath() { + global $wgMathDirectory; + $path = $wgMathDirectory .'/'. substr($this->hash, 0, 1) + .'/'. substr($this->hash, 1, 1) + .'/'. substr($this->hash, 2, 1); + wfDebug( "TeX: getHashPath, hash is: $this->hash, path is: $path\n" ); + return $path; + } -function renderMath( $tex ) { - global $wgUser; - $math = new MathRenderer( $tex ); - $math->setOutputMode( $wgUser->getOption('math')); - return $math->render(); + public static function renderMath( $tex, $params=array() ) { + global $wgUser; + $math = new MathRenderer( $tex, $params ); + $math->setOutputMode( $wgUser->getOption('math')); + return $math->render(); + } } -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/MemcachedSessions.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/MemcachedSessions.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/MemcachedSessions.php 2004-09-03 18:59:59.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/MemcachedSessions.php 2007-06-28 21:19:14.000000000 -0400 @@ -6,15 +6,13 @@ * be necessary to change the cookie settings to work across hostnames. * See: http://www.php.net/manual/en/function.session-set-save-handler.php * - * @package MediaWiki */ /** * @todo document */ function memsess_key( $id ) { - global $wgDBname; - return "$wgDBname:session:$id"; + return wfMemcKey( 'session', $id ); } /** @@ -71,4 +69,4 @@ session_set_save_handler( 'memsess_open', 'memsess_close', 'memsess_read', 'memsess_write', 'memsess_destroy', 'memsess_gc' ); -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/MessageCache.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/MessageCache.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/MessageCache.php 2006-02-11 06:38:26.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/MessageCache.php 2007-09-03 22:48:34.000000000 -0400 @@ -1,37 +1,32 @@ mUseCache = !is_null( $memCached ); $this->mMemc = &$memCached; @@ -41,13 +36,7 @@ $this->mMemcKey = $memcPrefix.':messages'; $this->mKeys = false; # initialised on demand $this->mInitialised = true; - - wfProfileIn( $fname.'-parseropt' ); - $this->mParserOptions = ParserOptions::newFromUser( $u=NULL ); - wfProfileOut( $fname.'-parseropt' ); - wfProfileIn( $fname.'-parser' ); - $this->mParser = new Parser; - wfProfileOut( $fname.'-parser' ); + $this->mParser = null; # When we first get asked for a message, # then we'll fill up the cache. If we @@ -55,7 +44,133 @@ # some extra milliseconds $this->mDeferred = true; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); + } + + function getParserOptions() { + if ( !$this->mParserOptions ) { + $this->mParserOptions = new ParserOptions; + } + return $this->mParserOptions; + } + + /** + * Try to load the cache from a local file + */ + function loadFromLocal( $hash ) { + global $wgLocalMessageCache; + + $this->mCache = false; + if ( $wgLocalMessageCache === false ) { + return; + } + + $filename = "$wgLocalMessageCache/messages-" . wfWikiID(); + + wfSuppressWarnings(); + $file = fopen( $filename, 'r' ); + wfRestoreWarnings(); + if ( !$file ) { + return; + } + + // Check to see if the file has the hash specified + $localHash = fread( $file, 32 ); + if ( $hash == $localHash ) { + // All good, get the rest of it + $serialized = fread( $file, 10000000 ); + $this->setCache( unserialize( $serialized ) ); + } + fclose( $file ); + } + + /** + * Save the cache to a local file + */ + function saveToLocal( $serialized, $hash ) { + global $wgLocalMessageCache; + + if ( $wgLocalMessageCache === false ) { + return; + } + + $filename = "$wgLocalMessageCache/messages-" . wfWikiID(); + $oldUmask = umask( 0 ); + wfMkdirParents( $wgLocalMessageCache, 0777 ); + umask( $oldUmask ); + + $file = fopen( $filename, 'w' ); + if ( !$file ) { + wfDebug( "Unable to open local cache file for writing\n" ); + return; + } + + fwrite( $file, $hash . $serialized ); + fclose( $file ); + @chmod( $filename, 0666 ); + } + + function loadFromScript( $hash ) { + global $wgLocalMessageCache; + if ( $wgLocalMessageCache === false ) { + return; + } + + $filename = "$wgLocalMessageCache/messages-" . wfWikiID(); + + wfSuppressWarnings(); + $file = fopen( $filename, 'r' ); + wfRestoreWarnings(); + if ( !$file ) { + return; + } + $localHash=substr(fread($file,40),8); + fclose($file); + if ($hash!=$localHash) { + return; + } + require("$wgLocalMessageCache/messages-" . wfWikiID()); + $this->setCache( $this->mCache); + } + + function saveToScript($array, $hash) { + global $wgLocalMessageCache; + if ( $wgLocalMessageCache === false ) { + return; + } + + $filename = "$wgLocalMessageCache/messages-" . wfWikiID(); + $oldUmask = umask( 0 ); + wfMkdirParents( $wgLocalMessageCache, 0777 ); + umask( $oldUmask ); + $file = fopen( $filename.'.tmp', 'w'); + fwrite($file,"mCache = array("); + + foreach ($array as $key => $message) { + fwrite($file, "'". $this->escapeForScript($key). + "' => '" . $this->escapeForScript($message). + "',\n"); + } + fwrite($file,");\n?>"); + fclose($file); + rename($filename.'.tmp',$filename); + } + + function escapeForScript($string) { + $string = str_replace( '\\', '\\\\', $string ); + $string = str_replace( '\'', '\\\'', $string ); + return $string; + } + + /** + * Set the cache to $cache, if it is valid. Otherwise set the cache to false. + */ + function setCache( $cache ) { + if ( isset( $cache['VERSION'] ) && $cache['VERSION'] == MSG_CACHE_VERSION ) { + $this->mCache = $cache; + } else { + $this->mCache = false; + } } /** @@ -64,7 +179,7 @@ * Returns false for a reportable error, true otherwise */ function load() { - global $wgAllMessagesEn; + global $wgLocalMessageCache, $wgLocalMessageCacheSerialized; if ( $this->mDisable ) { static $shownDisabled = false; @@ -74,59 +189,104 @@ } return true; } + if ( !$this->mUseCache ) { + $this->mDeferred = false; + return true; + } + $fname = 'MessageCache::load'; wfProfileIn( $fname ); $success = true; - if ( $this->mUseCache ) { - wfProfileIn( $fname.'-fromcache' ); - $this->mCache = $this->mMemc->get( $this->mMemcKey ); - wfProfileOut( $fname.'-fromcache' ); + $this->mCache = false; - # If there's nothing in memcached, load all the messages from the database - if ( !$this->mCache ) { - wfDebug( "MessageCache::load(): loading all messages\n" ); - $this->lock(); - # Other threads don't need to load the messages if another thread is doing it. - $success = $this->mMemc->add( $this->mMemcKey.'-status', "loading", MSG_LOAD_TIMEOUT ); - if ( $success ) { - wfProfileIn( $fname.'-load' ); - $this->loadFromDB(); - wfProfileOut( $fname.'-load' ); - # Save in memcached - # Keep trying if it fails, this is kind of important - wfProfileIn( $fname.'-save' ); - for ($i=0; $i<20 && - !$this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry ); - $i++ ) { - usleep(mt_rand(500000,1500000)); + # Try local cache + wfProfileIn( $fname.'-fromlocal' ); + $hash = $this->mMemc->get( "{$this->mMemcKey}-hash" ); + if ( $hash ) { + if ($wgLocalMessageCacheSerialized) { + $this->loadFromLocal( $hash ); + } else { + $this->loadFromScript( $hash ); + } + if ( $this->mCache ) { + wfDebug( "MessageCache::load(): got from local cache\n" ); + } + } + wfProfileOut( $fname.'-fromlocal' ); + + # Try memcached + if ( !$this->mCache ) { + wfProfileIn( $fname.'-fromcache' ); + $this->setCache( $this->mMemc->get( $this->mMemcKey ) ); + if ( $this->mCache ) { + wfDebug( "MessageCache::load(): got from global cache\n" ); + # Save to local cache + if ( $wgLocalMessageCache !== false ) { + $serialized = serialize( $this->mCache ); + if ( !$hash ) { + $hash = md5( $serialized ); + $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry ); } - wfProfileOut( $fname.'-save' ); - if ( $i == 20 ) { - $this->mMemc->set( $this->mMemcKey.'-status', 'error', 60*5 ); - wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" ); + if ($wgLocalMessageCacheSerialized) { + $this->saveToLocal( $serialized,$hash ); + } else { + $this->saveToScript( $this->mCache, $hash ); } } - $this->unlock(); } + wfProfileOut( $fname.'-fromcache' ); + } + + + # If there's nothing in memcached, load all the messages from the database + if ( !$this->mCache ) { + wfDebug( "MessageCache::load(): cache is empty\n" ); + $this->lock(); + # Other threads don't need to load the messages if another thread is doing it. + $success = $this->mMemc->add( $this->mMemcKey.'-status', "loading", MSG_LOAD_TIMEOUT ); + if ( $success ) { + wfProfileIn( $fname.'-load' ); + wfDebug( "MessageCache::load(): loading all messages from DB\n" ); + $this->loadFromDB(); + wfProfileOut( $fname.'-load' ); + + # Save in memcached + # Keep trying if it fails, this is kind of important + wfProfileIn( $fname.'-save' ); + for ($i=0; $i<20 && + !$this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry ); + $i++ ) { + usleep(mt_rand(500000,1500000)); + } - if ( !is_array( $this->mCache ) ) { - wfDebug( "MessageCache::load(): individual message mode\n" ); - # If it is 'loading' or 'error', switch to individual message mode, otherwise disable - # Causing too much DB load, disabling -- TS - $this->mDisable = true; - /* - if ( $this->mCache == "loading" ) { - $this->mUseCache = false; - } elseif ( $this->mCache == "error" ) { - $this->mUseCache = false; - $success = false; + # Save to local cache + if ( $wgLocalMessageCache !== false ) { + $serialized = serialize( $this->mCache ); + $hash = md5( $serialized ); + $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry ); + if ($wgLocalMessageCacheSerialized) { + $this->saveToLocal( $serialized,$hash ); + } else { + $this->saveToScript( $this->mCache, $hash ); + } + } + + wfProfileOut( $fname.'-save' ); + if ( $i == 20 ) { + $this->mMemc->set( $this->mMemcKey.'-status', 'error', 60*5 ); + wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" ); } else { - $this->mDisable = true; - $success = false; - }*/ - $this->mCache = false; + $this->mMemc->delete( $this->mMemcKey.'-status' ); + } } + $this->unlock(); + } + + if ( !is_array( $this->mCache ) ) { + wfDebug( "MessageCache::load(): unable to load cache, disabled\n" ); + $this->mDisable = true; + $this->mCache = false; } wfProfileOut( $fname ); $this->mDeferred = false; @@ -137,46 +297,53 @@ * Loads all or main part of cacheable messages from the database */ function loadFromDB() { - $fname = 'MessageCache::loadFromDB'; - $dbr =& wfGetDB( DB_SLAVE ); - $conditions = array( 'page_is_redirect' => 0, - 'page_namespace' => NS_MEDIAWIKI); + global $wgMaxMsgCacheEntrySize; + + wfProfileIn( __METHOD__ ); + $dbr = wfGetDB( DB_SLAVE ); + $this->mCache = array(); + + # Load titles for all oversized pages in the MediaWiki namespace + $res = $dbr->select( 'page', 'page_title', + array( + 'page_len > ' . intval( $wgMaxMsgCacheEntrySize ), + 'page_is_redirect' => 0, + 'page_namespace' => NS_MEDIAWIKI, + ), + __METHOD__ ); + while ( $row = $dbr->fetchObject( $res ) ) { + $this->mCache[$row->page_title] = '!TOO BIG'; + } + $dbr->freeResult( $res ); + + # Load text for the remaining pages $res = $dbr->select( array( 'page', 'revision', 'text' ), array( 'page_title', 'old_text', 'old_flags' ), - 'page_is_redirect=0 AND page_namespace='.NS_MEDIAWIKI.' AND page_latest=rev_id AND rev_text_id=old_id', - $fname - ); + array( + 'page_is_redirect' => 0, + 'page_namespace' => NS_MEDIAWIKI, + 'page_latest=rev_id', + 'rev_text_id=old_id', + 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize ) ), + __METHOD__ ); - $this->mCache = array(); for ( $row = $dbr->fetchObject( $res ); $row; $row = $dbr->fetchObject( $res ) ) { - $this->mCache[$row->page_title] = Revision::getRevisionText( $row ); + $this->mCache[$row->page_title] = ' ' . Revision::getRevisionText( $row ); } - + $this->mCache['VERSION'] = MSG_CACHE_VERSION; $dbr->freeResult( $res ); - /* - # FIXME: This is too slow currently. - # We need to bulk-fetch revisions, but in a portable way... - $resultSet = Revision::fetchFromConds( $dbr, array( - 'page_namespace' => NS_MEDIAWIKI, - 'page_is_redirect' => 0, - 'page_id=rev_page' ) ); - while( $row = $resultSet->fetchObject() ) { - $revision = new Revision( $row ); - $title = $revision->getTitle(); - $this->mCache[$title->getDBkey()] = $revision->getText(); - } - $resultSet->free(); - */ + wfProfileOut( __METHOD__ ); } /** * Not really needed anymore */ function getKeys() { - global $wgAllMessagesEn, $wgContLang; + global $wgContLang; if ( !$this->mKeys ) { $this->mKeys = array(); - foreach ( $wgAllMessagesEn as $key => $value ) { + $allMessages = Language::getMessagesFor( 'en' ); + foreach ( $allMessages as $key => $unused ) { $title = $wgContLang->ucfirst( $key ); array_push( $this->mKeys, $title ); } @@ -184,21 +351,42 @@ return $this->mKeys; } - /** - * @deprecated - */ - function isCacheable( $key ) { - return true; - } - function replace( $title, $text ) { + global $wgLocalMessageCache, $wgLocalMessageCacheSerialized, $parserMemc; + global $wgMaxMsgCacheEntrySize; + + wfProfileIn( __METHOD__ ); $this->lock(); $this->load(); + $parserMemc->delete(wfMemcKey('sidebar')); if ( is_array( $this->mCache ) ) { - $this->mCache[$title] = $text; + if ( $text === false ) { + # Article was deleted + unset( $this->mCache[$title] ); + $this->mMemc->delete( "$this->mMemcKey:{$title}" ); + } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) { + $this->mCache[$title] = '!TOO BIG'; + $this->mMemc->set( "$this->mMemcKey:{$title}", ' '.$text, $this->mExpiry ); + } else { + $this->mCache[$title] = ' ' . $text; + $this->mMemc->delete( "$this->mMemcKey:{$title}" ); + } $this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry ); + + # Save to local cache + if ( $wgLocalMessageCache !== false ) { + $serialized = serialize( $this->mCache ); + $hash = md5( $serialized ); + $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry ); + if ($wgLocalMessageCacheSerialized) { + $this->saveToLocal( $serialized,$hash ); + } else { + $this->saveToScript( $this->mCache, $hash ); + } + } } $this->unlock(); + wfProfileOut( __METHOD__ ); } /** @@ -227,62 +415,89 @@ $this->mMemc->delete( $lockKey ); } - function get( $key, $useDB, $forcontent=true, $isfullkey = false ) { - global $wgContLanguageCode; - if( $forcontent ) { - global $wgContLang; + /** + * Get a message from either the content language or the user language. + * + * @param string $key The message cache key + * @param bool $useDB Get the message from the DB, false to use only the localisation + * @param bool $forContent Get the message from the content language rather than the + * user language + * @param bool $isFullKey Specifies whether $key is a two part key "lang/msg". + */ + function get( $key, $useDB = true, $forContent = true, $isFullKey = false ) { + global $wgContLanguageCode, $wgContLang, $wgLang; + if( $forContent ) { $lang =& $wgContLang; - $langcode = $wgContLanguageCode; } else { - global $wgLang, $wgLanguageCode; $lang =& $wgLang; - $langcode = $wgLanguageCode; } + $langcode = $lang->getCode(); # If uninitialised, someone is trying to call this halfway through Setup.php if( !$this->mInitialised ) { return '<' . htmlspecialchars($key) . '>'; } # If cache initialization was deferred, start it now. - if( $this->mDeferred ) { + if( $this->mDeferred && !$this->mDisable && $useDB ) { $this->load(); } $message = false; + + # Normalise title-case input + $lckey = $wgContLang->lcfirst( $key ); + $lckey = str_replace( ' ', '_', $lckey ); + + # Try the MediaWiki namespace if( !$this->mDisable && $useDB ) { - $title = $lang->ucfirst( $key ); - if(!$isfullkey && ($langcode != $wgContLanguageCode) ) { + $title = $wgContLang->ucfirst( $lckey ); + if(!$isFullKey && ($langcode != $wgContLanguageCode) ) { $title .= '/' . $langcode; } - $message = $this->getFromCache( $title ); + $message = $this->getMsgFromNamespace( $title ); } # Try the extension array - if( !$message ) { - $message = @$this->mExtensionMessages[$key]; + if( $message === false && isset( $this->mExtensionMessages[$langcode][$lckey] ) ) { + $message = $this->mExtensionMessages[$langcode][$lckey]; + } + if ( $message === false && isset( $this->mExtensionMessages['en'][$lckey] ) ) { + $message = $this->mExtensionMessages['en'][$lckey]; } # Try the array in the language object - if( !$message ) { + if( $message === false ) { + #wfDebug( "Trying language object for message $key\n" ); wfSuppressWarnings(); - $message = $lang->getMessage( $key ); + $message = $lang->getMessage( $lckey ); wfRestoreWarnings(); + if ( is_null( $message ) ) { + $message = false; + } } - # Try the English array - if( !$message && $langcode != 'en' ) { - wfSuppressWarnings(); - $message = Language::getMessage( $key ); - wfRestoreWarnings(); + # Try the array of another language + if( $message === false && strpos( $lckey, '/' ) ) { + $message = explode( '/', $lckey ); + if ( $message[1] ) { + wfSuppressWarnings(); + $message = Language::getMessageFor( $message[0], $message[1] ); + wfRestoreWarnings(); + if ( is_null( $message ) ) { + $message = false; + } + } else { + $message = false; + } } # Is this a custom message? Try the default language in the db... - if( !$message && + if( ($message === false || $message === '-' ) && !$this->mDisable && $useDB && - !$isfullkey && ($langcode != $wgContLanguageCode) ) { - $message = $this->getFromCache( $lang->ucfirst( $key ) ); + !$isFullKey && ($langcode != $wgContLanguageCode) ) { + $message = $this->getMsgFromNamespace( $wgContLang->ucfirst( $lckey ) ); } # Final fallback - if( !$message ) { + if( $message === false ) { return '<' . htmlspecialchars($key) . '>'; } @@ -291,46 +506,87 @@ return $message; } - function getFromCache( $title ) { + /** + * Get a message from the MediaWiki namespace, with caching. The key must + * first be converted to two-part lang/msg form if necessary. + * + * @param string $title Message cache key with initial uppercase letter + */ + function getMsgFromNamespace( $title ) { $message = false; + $type = false; # Try the cache - if( $this->mUseCache && is_array( $this->mCache ) && array_key_exists( $title, $this->mCache ) ) { - $message = $this->mCache[$title]; + if( $this->mUseCache && isset( $this->mCache[$title] ) ) { + $entry = $this->mCache[$title]; + $type = substr( $entry, 0, 1 ); + if ( $type == ' ' ) { + return substr( $entry, 1 ); + } } - if ( !$message && $this->mUseCache ) { - $message = $this->mMemc->get( $this->mMemcKey . ':' . $title ); - if( $message ) { - $this->mCache[$title] = $message; - } + # Call message hooks, in case they are defined + wfRunHooks('MessagesPreLoad', array( $title, &$message ) ); + if ( $message !== false ) { + return $message; + } + + # If there is no cache entry and no placeholder, it doesn't exist + if ( $type != '!' && $message === false ) { + return false; } - # Call message Hooks, in case they are defined - wfRunHooks('MessagesPreLoad',array($title,&$message)); - - # If it wasn't in the cache, load each message from the DB individually - if ( !$message ) { - $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) ); - if( $revision ) { - $message = $revision->getText(); - if ($this->mUseCache) { - $this->mCache[$title]=$message; - /* individual messages may be often - recached until proper purge code exists - */ - $this->mMemc->set( $this->mMemcKey . ':' . $title, $message, 300 ); + $memcKey = $this->mMemcKey . ':' . $title; + + # Try the individual message cache + if ( $this->mUseCache ) { + $entry = $this->mMemc->get( $memcKey ); + if ( $entry ) { + $type = substr( $entry, 0, 1 ); + + if ( $type == ' ' ) { + $message = substr( $entry, 1 ); + $this->mCache[$title] = $entry; + return $message; + } elseif ( $entry == '!NONEXISTENT' ) { + return false; + } else { + # Corrupt/obsolete entry, delete it + $this->mMemc->delete( $memcKey ); } + + } + } + + # Try loading it from the DB + $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) ); + if( $revision ) { + $message = $revision->getText(); + if ($this->mUseCache) { + $this->mCache[$title] = ' ' . $message; + $this->mMemc->set( $memcKey, $message, $this->mExpiry ); } + } else { + # Negative caching + # Use some special text instead of false, because false gets converted to '' somewhere + $this->mMemc->set( $memcKey, '!NONEXISTENT', $this->mExpiry ); + $this->mCache[$title] = false; } return $message; } function transform( $message ) { - if( !$this->mDisableTransform ) { + global $wgParser; + if ( !$this->mParser && isset( $wgParser ) ) { + # Do some initialisation so that we don't have to do it twice + $wgParser->firstCallInit(); + # Clone it and store it + $this->mParser = clone $wgParser; + } + if ( !$this->mDisableTransform && $this->mParser ) { if( strpos( $message, '{{' ) !== false ) { - $message = $this->mParser->transformMsg( $message, $this->mParserOptions ); + $message = $this->mParser->transformMsg( $message, $this->getParserOptions() ); } } return $message; @@ -340,35 +596,113 @@ function enable() { $this->mDisable = false; } function disableTransform() { $this->mDisableTransform = true; } function enableTransform() { $this->mDisableTransform = false; } + function setTransform( $x ) { $this->mDisableTransform = $x; } + function getTransform() { return $this->mDisableTransform; } /** * Add a message to the cache * * @param mixed $key * @param mixed $value + * @param string $lang The messages language, English by default */ - function addMessage( $key, $value ) { - $this->mExtensionMessages[$key] = $value; + function addMessage( $key, $value, $lang = 'en' ) { + $this->mExtensionMessages[$lang][$key] = $value; } /** * Add an associative array of message to the cache * * @param array $messages An associative array of key => values to be added + * @param string $lang The messages language, English by default */ - function addMessages( $messages ) { + function addMessages( $messages, $lang = 'en' ) { + wfProfileIn( __METHOD__ ); + if ( isset( $this->mExtensionMessages[$lang] ) ) { + $this->mExtensionMessages[$lang] = $messages + $this->mExtensionMessages[$lang]; + } else { + $this->mExtensionMessages[$lang] = $messages; + } + wfProfileOut( __METHOD__ ); + } + + /** + * Add a 2-D array of messages by lang. Useful for extensions. + * + * @param array $messages The array to be added + */ + function addMessagesByLang( $messages ) { + wfProfileIn( __METHOD__ ); foreach ( $messages as $key => $value ) { - $this->addMessage( $key, $value ); + $this->addMessages( $value, $key ); } + wfProfileOut( __METHOD__ ); + } + + /** + * Get the extension messages for a specific language + * + * @param string $lang The messages language, English by default + */ + function getExtensionMessagesFor( $lang = 'en' ) { + wfProfileIn( __METHOD__ ); + $messages = array(); + if ( isset( $this->mExtensionMessages[$lang] ) ) { + $messages = $this->mExtensionMessages[$lang]; + } + if ( $lang != 'en' ) { + $messages = $messages + $this->mExtensionMessages['en']; + } + wfProfileOut( __METHOD__ ); + return $messages; } /** * Clear all stored messages. Mainly used after a mass rebuild. */ function clear() { + global $wgLocalMessageCache; if( $this->mUseCache ) { + # Global cache $this->mMemc->delete( $this->mMemcKey ); + # Invalidate all local caches + $this->mMemc->delete( "{$this->mMemcKey}-hash" ); + } + } + + function loadAllMessages() { + global $wgExtensionMessagesFiles; + if ( $this->mAllMessagesLoaded ) { + return; + } + $this->mAllMessagesLoaded = true; + + # Some extensions will load their messages when you load their class file + wfLoadAllExtensions(); + # Others will respond to this hook + wfRunHooks( 'LoadAllMessages' ); + # Some register their messages in $wgExtensionMessagesFiles + foreach ( $wgExtensionMessagesFiles as $name => $file ) { + if ( $file ) { + $this->loadMessagesFile( $file ); + $wgExtensionMessagesFiles[$name] = false; + } + } + # Still others will respond to neither, they are EVIL. We sometimes need to know! + } + + /** + * Load messages from a given file + */ + function loadMessagesFile( $filename ) { + $magicWords = false; + require( $filename ); + $this->addMessagesByLang( $messages ); + + if ( $magicWords !== false ) { + global $wgContLang; + $wgContLang->addMagicWordsByLang( $magicWords ); } } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Metadata.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Metadata.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Metadata.php 2004-11-11 17:27:49.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Metadata.php 2007-06-28 21:19:14.000000000 -0400 @@ -15,14 +15,14 @@ * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * * @author Evan Prodromou - * @package MediaWiki */ /** - * + * TODO: Perhaps make this file into a Metadata class, with static methods (declared + * as private where indicated), to move these functions out of the global namespace? */ define('RDF_TYPE_PREFS', "application/rdf+xml,text/xml;q=0.7,application/xml;q=0.5,text/rdf;q=0.1"); @@ -71,16 +71,18 @@ * @private */ function rdfSetup() { - global $wgOut, $wgRdfMimeType, $_SERVER; + global $wgOut, $_SERVER; + + $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : null; - $rdftype = wfNegotiateType(wfAcceptToPrefs($_SERVER['HTTP_ACCEPT']), wfAcceptToPrefs(RDF_TYPE_PREFS)); + $rdftype = wfNegotiateType(wfAcceptToPrefs($httpaccept), wfAcceptToPrefs(RDF_TYPE_PREFS)); if (!$rdftype) { wfHttpError(406, "Not Acceptable", wfMsg("notacceptable")); return false; } else { $wgOut->disable(); - header( "Content-type: {$rdftype}" ); + header( "Content-type: {$rdftype}; charset=utf-8" ); $wgOut->sendCacheControl(); return true; } @@ -142,7 +144,7 @@ dcPerson('contributor', $user_parts[0], $user_parts[1], $user_parts[2]); } - dcRights($article); + dcRights(); } /** @@ -291,7 +293,7 @@ * different pages. * @private */ -function dcRights($article) { +function dcRights() { global $wgRightsPage, $wgRightsUrl, $wgRightsText; @@ -316,7 +318,11 @@ return $wgLicenseTerms; } else { $known = getKnownLicenses(); - return $known[$url]; + if( isset( $known[$url] ) ) { + return $known[$url]; + } else { + return array(); + } } } @@ -359,4 +365,4 @@ return $knownLicenses; } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/MimeMagic.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/MimeMagic.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/MimeMagic.php 2006-01-02 21:50:43.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/MimeMagic.php 2007-08-15 06:50:09.000000000 -0400 @@ -1,12 +1,11 @@ mMimeToExt= array(); - $this->mToMime= array(); - - $lines= explode("\n",$types); - foreach ($lines as $s) { - $s= trim($s); - if (empty($s)) continue; - if (strpos($s,'#')===0) continue; - - $s= strtolower($s); - $i= strpos($s,' '); - - if ($i===false) continue; - + + $types = str_replace( array( "\r\n", "\n\r", "\n\n", "\r\r", "\r" ), "\n", $types ); + $types = str_replace( "\t", " ", $types ); + + $this->mMimeToExt = array(); + $this->mToMime = array(); + + $lines = explode( "\n",$types ); + foreach ( $lines as $s ) { + $s = trim( $s ); + if ( empty( $s ) ) continue; + if ( strpos( $s, '#' ) === 0 ) continue; + + $s = strtolower( $s ); + $i = strpos( $s, ' ' ); + + if ( $i === false ) continue; + #print "processing MIME line $s
      "; - - $mime= substr($s,0,$i); - $ext= trim(substr($s,$i+1)); - - if (empty($ext)) continue; - - if (@$this->mMimeToExt[$mime]) $this->mMimeToExt[$mime] .= ' '.$ext; - else $this->mMimeToExt[$mime]= $ext; - - $extensions= explode(' ',$ext); - - foreach ($extensions as $e) { - $e= trim($e); - if (empty($e)) continue; - - if (@$this->mExtToMime[$e]) $this->mExtToMime[$e] .= ' '.$mime; - else $this->mExtToMime[$e]= $mime; + + $mime = substr( $s, 0, $i ); + $ext = trim( substr($s, $i+1 ) ); + + if ( empty( $ext ) ) continue; + + if ( !empty( $this->mMimeToExt[$mime] ) ) { + $this->mMimeToExt[$mime] .= ' ' . $ext; + } else { + $this->mMimeToExt[$mime] = $ext; + } + + $extensions = explode( ' ', $ext ); + + foreach ( $extensions as $e ) { + $e = trim( $e ); + if ( empty( $e ) ) continue; + + if ( !empty( $this->mExtToMime[$e] ) ) { + $this->mExtToMime[$e] .= ' ' . $mime; + } else { + $this->mExtToMime[$e] = $mime; + } } } - + /* * --- load mime.info --- */ - + global $wgMimeInfoFile; - - $info= MM_WELL_KNOWN_MIME_INFO; - - if ($wgMimeInfoFile) { - if (is_file($wgMimeInfoFile) and is_readable($wgMimeInfoFile)) { - wfDebug("MimeMagic::MimeMagic: loading mime info from $wgMimeInfoFile\n"); - - $info.= "\n"; - $info.= file_get_contents($wgMimeInfoFile); + if ( $wgMimeInfoFile == 'includes/mime.info' ) { + $wgMimeInfoFile = "$IP/$wgMimeInfoFile"; + } + + $info = MM_WELL_KNOWN_MIME_INFO; + + if ( $wgMimeInfoFile ) { + if ( is_file( $wgMimeInfoFile ) and is_readable( $wgMimeInfoFile ) ) { + wfDebug( __METHOD__.": loading mime info from $wgMimeInfoFile\n" ); + $info .= "\n"; + $info .= file_get_contents( $wgMimeInfoFile ); + } else { + wfDebug(__METHOD__.": can't load mime info from $wgMimeInfoFile\n"); } - else wfDebug("MimeMagic::MimeMagic: can't load mime info from $wgMimeInfoFile\n"); + } else { + wfDebug(__METHOD__.": no mime info file defined, using build-ins only.\n"); } - else wfDebug("MimeMagic::MimeMagic: no mime info file defined, using build-ins only.\n"); - - $info= str_replace(array("\r\n","\n\r","\n\n","\r\r","\r"),"\n",$info); - $info= str_replace("\t"," ",$info); - - $this->mMimeTypeAliases= array(); - $this->mMediaTypes= array(); - - $lines= explode("\n",$info); - foreach ($lines as $s) { - $s= trim($s); - if (empty($s)) continue; - if (strpos($s,'#')===0) continue; - - $s= strtolower($s); - $i= strpos($s,' '); - - if ($i===false) continue; - + + $info = str_replace( array( "\r\n", "\n\r", "\n\n", "\r\r", "\r" ), "\n", $info); + $info = str_replace( "\t", " ", $info ); + + $this->mMimeTypeAliases = array(); + $this->mMediaTypes = array(); + + $lines = explode( "\n", $info ); + foreach ( $lines as $s ) { + $s = trim( $s ); + if ( empty( $s ) ) continue; + if ( strpos( $s, '#' ) === 0 ) continue; + + $s = strtolower( $s ); + $i = strpos( $s, ' ' ); + + if ( $i === false ) continue; + #print "processing MIME INFO line $s
      "; - - $match= array(); - if (preg_match('!\[\s*(\w+)\s*\]!',$s,$match)) { - $s= preg_replace('!\[\s*(\w+)\s*\]!','',$s); - $mtype= trim(strtoupper($match[1])); + + $match = array(); + if ( preg_match( '!\[\s*(\w+)\s*\]!', $s, $match ) ) { + $s = preg_replace( '!\[\s*(\w+)\s*\]!', '', $s ); + $mtype = trim( strtoupper( $match[1] ) ); + } else { + $mtype = MEDIATYPE_UNKNOWN; } - else $mtype= MEDIATYPE_UNKNOWN; - - $m= explode(' ',$s); - - if (!isset($this->mMediaTypes[$mtype])) $this->mMediaTypes[$mtype]= array(); - - foreach ($m as $mime) { - $mime= trim($mime); - if (empty($mime)) continue; - - $this->mMediaTypes[$mtype][]= $mime; + + $m = explode( ' ', $s ); + + if ( !isset( $this->mMediaTypes[$mtype] ) ) { + $this->mMediaTypes[$mtype] = array(); } - - if (sizeof($m)>1) { - $main= $m[0]; - for ($i=1; $imMimeTypeAliases[$mime]= $main; + + foreach ( $m as $mime ) { + $mime = trim( $mime ); + if ( empty( $mime ) ) continue; + + $this->mMediaTypes[$mtype][] = $mime; + } + + if ( sizeof( $m ) > 1 ) { + $main = $m[0]; + for ( $i=1; $imMimeTypeAliases[$mime] = $main; } } } - + + } + + /** + * Get an instance of this class + */ + static function &singleton() { + if ( !isset( self::$instance ) ) { + self::$instance = new MimeMagic; + } + return self::$instance; } - + /** returns a list of file extensions for a given mime type * as a space separated string. */ - function getExtensionsForType($mime) { - $mime= strtolower($mime); - - $r= @$this->mMimeToExt[$mime]; - - if (@!$r and isset($this->mMimeTypeAliases[$mime])) { - $mime= $this->mMimeTypeAliases[$mime]; - $r= @$this->mMimeToExt[$mime]; + function getExtensionsForType( $mime ) { + $mime = strtolower( $mime ); + + $r = @$this->mMimeToExt[$mime]; + + if ( @!$r and isset( $this->mMimeTypeAliases[$mime] ) ) { + $mime = $this->mMimeTypeAliases[$mime]; + $r = @$this->mMimeToExt[$mime]; } - + return $r; } - + /** returns a list of mime types for a given file extension * as a space separated string. */ - function getTypesForExtension($ext) { - $ext= strtolower($ext); - - $r= @$this->mExtToMime[$ext]; + function getTypesForExtension( $ext ) { + $ext = strtolower( $ext ); + + $r = isset( $this->mExtToMime[$ext] ) ? $this->mExtToMime[$ext] : null; return $r; } - + /** returns a single mime type for a given file extension. * This is always the first type from the list returned by getTypesForExtension($ext). */ - function guessTypesForExtension($ext) { - $m= $this->getTypesForExtension( $ext ); - if( is_null($m) ) return NULL; - - $m= trim( $m ); - $m= preg_replace('/\s.*$/','',$m); - + function guessTypesForExtension( $ext ) { + $m = $this->getTypesForExtension( $ext ); + if ( is_null( $m ) ) return NULL; + + $m = trim( $m ); + $m = preg_replace( '/\s.*$/', '', $m ); + return $m; } - - + + /** tests if the extension matches the given mime type. - * returns true if a match was found, NULL if the mime type is unknown, + * returns true if a match was found, NULL if the mime type is unknown, * and false if the mime type is known but no matches where found. */ - function isMatchingExtension($extension,$mime) { - $ext= $this->getExtensionsForType($mime); - - if (!$ext) { + function isMatchingExtension( $extension, $mime ) { + $ext = $this->getExtensionsForType( $mime ); + + if ( !$ext ) { return NULL; //unknown } - - $ext= explode(' ',$ext); - - $extension= strtolower($extension); - if (in_array($extension,$ext)) { + + $ext = explode( ' ', $ext ); + + $extension = strtolower( $extension ); + if ( in_array( $extension, $ext ) ) { return true; } - + return false; } - + /** returns true if the mime type is known to represent * an image format supported by the PHP GD library. */ function isPHPImageType( $mime ) { #as defined by imagegetsize and image_type_to_mime - static $types = array( - 'image/gif', 'image/jpeg', 'image/png', + static $types = array( + 'image/gif', 'image/jpeg', 'image/png', 'image/x-bmp', 'image/xbm', 'image/tiff', - 'image/jp2', 'image/jpeg2000', 'image/iff', + 'image/jp2', 'image/jpeg2000', 'image/iff', 'image/xbm', 'image/x-xbitmap', - 'image/vnd.wap.wbmp', 'image/vnd.xiff', + 'image/vnd.wap.wbmp', 'image/vnd.xiff', 'image/x-photoshop', 'application/x-shockwave-flash', - ); - + ); + return in_array( $mime, $types ); } - + /** * Returns true if the extension represents a type which can * be reliably detected from its content. Use this to determine @@ -320,31 +354,33 @@ 'gif', 'jpeg', 'jpg', 'png', 'swf', 'psd', 'bmp', 'tiff', 'tif', 'jpc', 'jp2', 'jpx', 'jb2', 'swc', 'iff', 'wbmp', - 'xbm' + 'xbm', 'djvu' ); return in_array( strtolower( $extension ), $types ); } - - - /** mime type detection. This uses detectMimeType to detect the mim type of the file, + + + /** mime type detection. This uses detectMimeType to detect the mime type of the file, * but applies additional checks to determine some well known file formats that may be missed * or misinterpreter by the default mime detection (namely xml based formats like XHTML or SVG). * * @param string $file The file to check - * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default. + * @param mixed $ext The file extension, or true to extract it from the filename. + * Set it to false to ignore the extension. * * @return string the mime type of $file */ - function guessMimeType( $file, $useExt=true ) { - $fname = 'MimeMagic::guessMimeType'; - $mime= $this->detectMimeType($file,$useExt); - + function guessMimeType( $file, $ext = true ) { + $mime = $this->detectMimeType( $file, $ext ); + // Read a chunk of the file + wfSuppressWarnings(); $f = fopen( $file, "rt" ); + wfRestoreWarnings(); if( !$f ) return "unknown/unknown"; $head = fread( $f, 1024 ); fclose( $f ); - + $sub4 = substr( $head, 0, 4 ); if ( $sub4 == "\x01\x00\x09\x00" || $sub4 == "\xd7\xcd\xc6\x9a" ) { // WMF kill kill kill @@ -352,335 +388,379 @@ // The former of the above two checks is theoretically prone to false positives $mime = "application/x-msmetafile"; } - - if (strpos($mime,"text/")===0 || $mime==="application/xml") { - - $xml_type= NULL; - $script_type= NULL; - + + if ( strpos( $mime, "text/" ) === 0 || $mime === "application/xml" ) { + + $xml_type = NULL; + $script_type = NULL; + /* * look for XML formats (XHTML and SVG) */ - if ($mime==="text/sgml" || - $mime==="text/plain" || - $mime==="text/html" || - $mime==="text/xml" || - $mime==="application/xml") { - - if (substr($head,0,5)=="%sim',$head,$match)) $doctype= $match[1]; - if (preg_match('%<(\w+).*>%sim',$head,$match)) $tag= $match[1]; - + if ($mime === "text/sgml" || + $mime === "text/plain" || + $mime === "text/html" || + $mime === "text/xml" || + $mime === "application/xml") { + + if ( substr( $head, 0, 5 ) == "%sim', + $head, $match ) ) { + $doctype = $match[1]; + } + if ( preg_match( '%<(\w+).*>%sim', $head, $match ) ) { + $tag = $match[1]; + } + #print "
      ANALYSING $file ($mime): doctype= $doctype; tag= $tag
      "; - - if (strpos($doctype,"-//W3C//DTD SVG")===0) $mime= "image/svg"; - elseif ($tag==="svg") $mime= "image/svg"; - elseif (strpos($doctype,"-//W3C//DTD XHTML")===0) $mime= "text/html"; - elseif ($tag==="html") $mime= "text/html"; - - $test_more= false; + + if ( strpos( $doctype, "-//W3C//DTD SVG" ) === 0 ) { + $mime = "image/svg+xml"; + } elseif ( $tag === "svg" ) { + $mime = "image/svg+xml"; + } elseif ( strpos( $doctype, "-//W3C//DTD XHTML" ) === 0 ) { + $mime = "text/html"; + } elseif ( $tag === "html" ) { + $mime = "text/html"; + } } } - + /* * look for shell scripts */ - if (!$xml_type) { - $script_type= NULL; - - #detect by shebang - if (substr($head,0,2)=="#!") $script_type= "ASCII"; - elseif (substr($head,0,5)=="\xef\xbb\xbf#!") $script_type= "UTF-8"; - elseif (substr($head,0,7)=="\xfe\xff\x00#\x00!") $script_type= "UTF-16BE"; - elseif (substr($head,0,7)=="\xff\xfe#\x00!") $script_type= "UTF-16LE"; - - if ($script_type) { - if ($script_type!=="UTF-8" && $script_type!=="ASCII") $head= iconv($script_type,"ASCII//IGNORE",$head); - - $match= array(); - $prog= ""; - - if (preg_match('%/?([^\s]+/)(w+)%sim',$head,$match)) $script= $match[2]; - - $mime= "application/x-$prog"; + if ( !$xml_type ) { + $script_type = NULL; + + # detect by shebang + if ( substr( $head, 0, 2) == "#!" ) { + $script_type = "ASCII"; + } elseif ( substr( $head, 0, 5) == "\xef\xbb\xbf#!" ) { + $script_type = "UTF-8"; + } elseif ( substr( $head, 0, 7) == "\xfe\xff\x00#\x00!" ) { + $script_type = "UTF-16BE"; + } elseif ( substr( $head, 0, 7 ) == "\xff\xfe#\x00!" ) { + $script_type= "UTF-16LE"; + } + + if ( $script_type ) { + if ( $script_type !== "UTF-8" && $script_type !== "ASCII") { + $head = iconv( $script_type, "ASCII//IGNORE", $head); + } + + $match = array(); + + if ( preg_match( '%/?([^\s]+/)(\w+)%', $head, $match ) ) { + $mime = "application/x-{$match[2]}"; + } } } - + /* * look for PHP */ if( !$xml_type && !$script_type ) { - - if( ( strpos( $head, 'mMimeTypeAliases[$mime])) $mime= $this->mMimeTypeAliases[$mime]; - - wfDebug("$fname: final mime type of $file: $mime\n"); + + if ( isset( $this->mMimeTypeAliases[$mime] ) ) { + $mime = $this->mMimeTypeAliases[$mime]; + } + + wfDebug(__METHOD__.": final mime type of $file: $mime\n"); return $mime; } - - /** Internal mime type detection, please use guessMimeType() for application code instead. + + /** Internal mime type detection, please use guessMimeType() for application code instead. * Detection is done using an external program, if $wgMimeDetectorCommand is set. - * Otherwise, the fileinfo extension and mime_content_type are tried (in this order), if they are available. - * If the dections fails and $useExt is true, the mime type is guessed from the file extension, using guessTypesForExtension. + * Otherwise, the fileinfo extension and mime_content_type are tried (in this order), if they are available. + * If the dections fails and $ext is not false, the mime type is guessed from the file extension, using + * guessTypesForExtension. * If the mime type is still unknown, getimagesize is used to detect the mime type if the file is an image. * If no mime type can be determined, this function returns "unknown/unknown". * * @param string $file The file to check - * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default. + * @param mixed $ext The file extension, or true to extract it from the filename. + * Set it to false to ignore the extension. * * @return string the mime type of $file - * @private + * @access private */ - function detectMimeType( $file, $useExt=true ) { - $fname = 'MimeMagic::detectMimeType'; - + function detectMimeType( $file, $ext = true ) { global $wgMimeDetectorCommand; - - $m= NULL; - if ($wgMimeDetectorCommand) { - $fn= wfEscapeShellArg($file); - $m= `$wgMimeDetectorCommand $fn`; - } - else if (function_exists("finfo_open") && function_exists("finfo_file")) { - + + $m = NULL; + if ( $wgMimeDetectorCommand ) { + $fn = wfEscapeShellArg( $file ); + $m = `$wgMimeDetectorCommand $fn`; + } elseif ( function_exists( "finfo_open" ) && function_exists( "finfo_file" ) ) { + # This required the fileinfo extension by PECL, # see http://pecl.php.net/package/fileinfo # This must be compiled into PHP # - # finfo is the official replacement for the deprecated + # finfo is the official replacement for the deprecated # mime_content_type function, see below. # # If you may need to load the fileinfo extension at runtime, set # $wgLoadFileinfoExtension in LocalSettings.php - + $mime_magic_resource = finfo_open(FILEINFO_MIME); /* return mime type ala mimetype extension */ - + if ($mime_magic_resource) { - $m= finfo_file($mime_magic_resource, $file); - - finfo_close($mime_magic_resource); + $m = finfo_file( $mime_magic_resource, $file ); + finfo_close( $mime_magic_resource ); + } else { + wfDebug( __METHOD__.": finfo_open failed on ".FILEINFO_MIME."!\n" ); } - else wfDebug("$fname: finfo_open failed on ".FILEINFO_MIME."!\n"); - } - else if (function_exists("mime_content_type")) { - + } elseif ( function_exists( "mime_content_type" ) ) { + # NOTE: this function is available since PHP 4.3.0, but only if # PHP was compiled with --with-mime-magic or, before 4.3.2, with --enable-mime-magic. # - # On Winodws, you must set mime_magic.magicfile in php.ini to point to the mime.magic file bundeled with PHP; + # On Windows, you must set mime_magic.magicfile in php.ini to point to the mime.magic file bundeled with PHP; # sometimes, this may even be needed under linus/unix. # # Also note that this has been DEPRECATED in favor of the fileinfo extension by PECL, see above. # see http://www.php.net/manual/en/ref.mime-magic.php for details. - - $m= mime_content_type($file); + + $m = mime_content_type($file); + + if ( $m == 'text/plain' ) { + // mime_content_type sometimes considers DJVU files to be text/plain. + $deja = new DjVuImage( $file ); + if( $deja->isValid() ) { + wfDebug( __METHOD__.": (re)detected $file as image/vnd.djvu\n" ); + $m = 'image/vnd.djvu'; + } + } + } else { + wfDebug( __METHOD__.": no magic mime detector found!\n" ); } - else wfDebug("$fname: no magic mime detector found!\n"); - - if ($m) { - #normalize - $m= preg_replace('![;, ].*$!','',$m); #strip charset, etc - $m= trim($m); - $m= strtolower($m); - - if (strpos($m,'unknown')!==false) $m= NULL; - else { - wfDebug("$fname: magic mime type of $file: $m\n"); + + if ( $m ) { + # normalize + $m = preg_replace( '![;, ].*$!', '', $m ); #strip charset, etc + $m = trim( $m ); + $m = strtolower( $m ); + + if ( strpos( $m, 'unknown' ) !== false ) { + $m = NULL; + } else { + wfDebug( __METHOD__.": magic mime type of $file: $m\n" ); return $m; } } - - #if still not known, use getimagesize to find out the type of image - #TODO: skip things that do not have a well-known image extension? Would that be safe? + + # if still not known, use getimagesize to find out the type of image + # TODO: skip things that do not have a well-known image extension? Would that be safe? wfSuppressWarnings(); $gis = getimagesize( $file ); wfRestoreWarnings(); - - $notAnImage= false; - - if ($gis && is_array($gis) && $gis[2]) { - switch ($gis[2]) { - case IMAGETYPE_GIF: $m= "image/gif"; break; - case IMAGETYPE_JPEG: $m= "image/jpeg"; break; - case IMAGETYPE_PNG: $m= "image/png"; break; - case IMAGETYPE_SWF: $m= "application/x-shockwave-flash"; break; - case IMAGETYPE_PSD: $m= "application/photoshop"; break; - case IMAGETYPE_BMP: $m= "image/bmp"; break; - case IMAGETYPE_TIFF_II: $m= "image/tiff"; break; - case IMAGETYPE_TIFF_MM: $m= "image/tiff"; break; - case IMAGETYPE_JPC: $m= "image"; break; - case IMAGETYPE_JP2: $m= "image/jpeg2000"; break; - case IMAGETYPE_JPX: $m= "image/jpeg2000"; break; - case IMAGETYPE_JB2: $m= "image"; break; - case IMAGETYPE_SWC: $m= "application/x-shockwave-flash"; break; - case IMAGETYPE_IFF: $m= "image/vnd.xiff"; break; - case IMAGETYPE_WBMP: $m= "image/vnd.wap.wbmp"; break; - case IMAGETYPE_XBM: $m= "image/x-xbitmap"; break; - } + + $notAnImage = false; + + if ( $gis && is_array($gis) && $gis[2] ) { - if ($m) { - wfDebug("$fname: image mime type of $file: $m\n"); + switch ( $gis[2] ) { + case IMAGETYPE_GIF: $m = "image/gif"; break; + case IMAGETYPE_JPEG: $m = "image/jpeg"; break; + case IMAGETYPE_PNG: $m = "image/png"; break; + case IMAGETYPE_SWF: $m = "application/x-shockwave-flash"; break; + case IMAGETYPE_PSD: $m = "application/photoshop"; break; + case IMAGETYPE_BMP: $m = "image/bmp"; break; + case IMAGETYPE_TIFF_II: $m = "image/tiff"; break; + case IMAGETYPE_TIFF_MM: $m = "image/tiff"; break; + case IMAGETYPE_JPC: $m = "image"; break; + case IMAGETYPE_JP2: $m = "image/jpeg2000"; break; + case IMAGETYPE_JPX: $m = "image/jpeg2000"; break; + case IMAGETYPE_JB2: $m = "image"; break; + case IMAGETYPE_SWC: $m = "application/x-shockwave-flash"; break; + case IMAGETYPE_IFF: $m = "image/vnd.xiff"; break; + case IMAGETYPE_WBMP: $m = "image/vnd.wap.wbmp"; break; + case IMAGETYPE_XBM: $m = "image/x-xbitmap"; break; + } + + if ( $m ) { + wfDebug( __METHOD__.": image mime type of $file: $m\n" ); return $m; } - else $notAnImage= true; + else { + $notAnImage = true; + } + } else { + // Also test DjVu + $deja = new DjVuImage( $file ); + if( $deja->isValid() ) { + wfDebug( __METHOD__.": detected $file as image/vnd.djvu\n" ); + return 'image/vnd.djvu'; + } } - - #if desired, look at extension as a fallback. - if ($useExt) { + + # if desired, look at extension as a fallback. + if ( $ext === true ) { $i = strrpos( $file, '.' ); - $e= strtolower( $i ? substr( $file, $i + 1 ) : '' ); - - $m= $this->guessTypesForExtension($e); - - #TODO: if $notAnImage is set, do not trust the file extension if - # the results is one of the image types that should have been recognized + $ext = strtolower( $i ? substr( $file, $i + 1 ) : '' ); + } + if ( $ext ) { + $m = $this->guessTypesForExtension( $ext ); + + # TODO: if $notAnImage is set, do not trust the file extension if + # the results is one of the image types that should have been recognized # by getimagesize - - if ($m) { - wfDebug("$fname: extension mime type of $file: $m\n"); + + if ( $m ) { + wfDebug( __METHOD__.": extension mime type of $file: $m\n" ); return $m; } } - - #unknown type - wfDebug("$fname: failed to guess mime type for $file!\n"); + + #unknown type + wfDebug( __METHOD__.": failed to guess mime type for $file!\n" ); return "unknown/unknown"; } - + /** * Determine the media type code for a file, using its mime type, name and possibly * its contents. * - * This function relies on the findMediaType(), mapping extensions and mime + * This function relies on the findMediaType(), mapping extensions and mime * types to media types. * - * @todo analyse file if need be + * @todo analyse file if need be * @todo look at multiple extension, separately and together. * - * @param string $path full path to the image file, in case we have to look at the contents + * @param string $path full path to the image file, in case we have to look at the contents * (if null, only the mime type is used to determine the media type code). * @param string $mime mime type. If null it will be guessed using guessMimeType. - * + * * @return (int?string?) a value to be used with the MEDIATYPE_xxx constants. */ - function getMediaType($path=NULL,$mime=NULL) { + function getMediaType( $path = NULL, $mime = NULL ) { if( !$mime && !$path ) return MEDIATYPE_UNKNOWN; - - #if mime type is unknown, guess it - if( !$mime ) $mime= $this->guessMimeType($path,false); - #special code for ogg - detect if it's video (theora), - #else label it as sound. - if( $mime=="application/ogg" && file_exists($path) ) { - + # If mime type is unknown, guess it + if( !$mime ) $mime = $this->guessMimeType( $path, false ); + + # Special code for ogg - detect if it's video (theora), + # else label it as sound. + if( $mime == "application/ogg" && file_exists( $path ) ) { + // Read a chunk of the file $f = fopen( $path, "rt" ); - if( !$f ) return MEDIATYPE_UNKNOWN; + if ( !$f ) return MEDIATYPE_UNKNOWN; $head = fread( $f, 256 ); fclose( $f ); - - $head= strtolower( $head ); - - #This is an UGLY HACK, file should be parsed correctly - if( strpos($head,'theora')!==false ) return MEDIATYPE_VIDEO; - elseif( strpos($head,'vorbis')!==false ) return MEDIATYPE_AUDIO; - elseif( strpos($head,'flac')!==false ) return MEDIATYPE_AUDIO; - elseif( strpos($head,'speex')!==false ) return MEDIATYPE_AUDIO; + + $head = strtolower( $head ); + + # This is an UGLY HACK, file should be parsed correctly + if ( strpos( $head, 'theora' ) !== false ) return MEDIATYPE_VIDEO; + elseif ( strpos( $head, 'vorbis' ) !== false ) return MEDIATYPE_AUDIO; + elseif ( strpos( $head, 'flac' ) !== false ) return MEDIATYPE_AUDIO; + elseif ( strpos( $head, 'speex' ) !== false ) return MEDIATYPE_AUDIO; else return MEDIATYPE_MULTIMEDIA; - } - - #check for entry for full mime type + } + + # check for entry for full mime type if( $mime ) { - $type= $this->findMediaType($mime); - if( $type!==MEDIATYPE_UNKNOWN ) return $type; + $type = $this->findMediaType( $mime ); + if( $type !== MEDIATYPE_UNKNOWN ) return $type; } - - #check for entry for file extension - $e= NULL; - if( $path ) { + + # Check for entry for file extension + $e = NULL; + if ( $path ) { $i = strrpos( $path, '.' ); - $e= strtolower( $i ? substr( $path, $i + 1 ) : '' ); - - #TODO: look at multi-extension if this fails, parse from full path - - $type= $this->findMediaType('.'.$e); - if( $type!==MEDIATYPE_UNKNOWN ) return $type; + $e = strtolower( $i ? substr( $path, $i + 1 ) : '' ); + + # TODO: look at multi-extension if this fails, parse from full path + + $type = $this->findMediaType( '.' . $e ); + if ( $type !== MEDIATYPE_UNKNOWN ) return $type; } - - #check major mime type + + # Check major mime type if( $mime ) { - $i= strpos($mime,'/'); + $i = strpos( $mime, '/' ); if( $i !== false ) { - $major= substr($mime,0,$i); - $type= $this->findMediaType($major); - if( $type!==MEDIATYPE_UNKNOWN ) return $type; + $major = substr( $mime, 0, $i ); + $type = $this->findMediaType( $major ); + if( $type !== MEDIATYPE_UNKNOWN ) return $type; } } - - if( !$type ) $type= MEDIATYPE_UNKNOWN; - + + if( !$type ) $type = MEDIATYPE_UNKNOWN; + return $type; } - + /** returns a media code matching the given mime type or file extension. - * File extensions are represented by a string starting with a dot (.) to + * File extensions are represented by a string starting with a dot (.) to * distinguish them from mime types. * * This funktion relies on the mapping defined by $this->mMediaTypes - * @private + * @access private */ - function findMediaType($extMime) { - - if (strpos($extMime,'.')===0) { #if it's an extension, look up the mime types - $m= $this->getTypesForExtension(substr($extMime,1)); - if (!$m) return MEDIATYPE_UNKNOWN; - - $m= explode(' ',$m); - } - else { #normalize mime type - if (isset($this->mMimeTypeAliases[$extMime])) { - $extMime= $this->mMimeTypeAliases[$extMime]; + function findMediaType( $extMime ) { + if ( strpos( $extMime, '.' ) === 0 ) { #if it's an extension, look up the mime types + $m = $this->getTypesForExtension( substr( $extMime, 1 ) ); + if ( !$m ) return MEDIATYPE_UNKNOWN; + + $m = explode( ' ', $m ); + } else { + # Normalize mime type + if ( isset( $this->mMimeTypeAliases[$extMime] ) ) { + $extMime = $this->mMimeTypeAliases[$extMime]; } - - $m= array($extMime); + + $m = array($extMime); } - - foreach ($m as $mime) { - foreach ($this->mMediaTypes as $type => $codes) { - if (in_array($mime,$codes,true)) return $type; + + foreach ( $m as $mime ) { + foreach ( $this->mMediaTypes as $type => $codes ) { + if ( in_array($mime, $codes, true ) ) { + return $type; + } } } - + return MEDIATYPE_UNKNOWN; } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Namespace.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Namespace.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Namespace.php 2005-07-22 07:29:14.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Namespace.php 2007-08-13 21:14:42.000000000 -0400 @@ -1,15 +1,8 @@ 'Media', NS_SPECIAL => 'Special', - NS_TALK => 'Talk', + NS_TALK => 'Talk', NS_USER => 'User', NS_USER_TALK => 'User_talk', NS_PROJECT => 'Project', @@ -30,7 +23,7 @@ NS_TEMPLATE_TALK => 'Template_talk', NS_HELP => 'Help', NS_HELP_TALK => 'Help_talk', - NS_CATEGORY => 'Category', + NS_CATEGORY => 'Category', NS_CATEGORY_TALK => 'Category_talk', ); @@ -47,60 +40,71 @@ * These are synonyms for the names given in the language file * Users and translators should not change them * - * @package MediaWiki */ class Namespace { /** - * Check if the given namespace might be moved + * Can pages in the given namespace be moved? + * + * @param int $index Namespace index * @return bool */ - function isMovable( $index ) { - if ( $index < NS_MAIN || $index == NS_IMAGE || $index == NS_CATEGORY ) { - return false; - } - return true; + public static function isMovable( $index ) { + return !( $index < NS_MAIN || $index == NS_IMAGE || $index == NS_CATEGORY ); } /** - * Check if the give namespace is a talk page + * Is the given namespace is a subject (non-talk) namespace? + * + * @param int $index Namespace index * @return bool */ - function isTalk( $index ) { - global $wgExtraNamespaces; - return ( $index == NS_TALK || $index == NS_USER_TALK || - $index == NS_PROJECT_TALK || $index == NS_IMAGE_TALK || - $index == NS_MEDIAWIKI_TALK || $index == NS_TEMPLATE_TALK || - $index == NS_HELP_TALK || $index == NS_CATEGORY_TALK - || ( (isset($wgExtraNamespaces) && $index % 2) ) - ); - + public static function isMain( $index ) { + return !self::isTalk( $index ); } /** - * Get the talk namespace corresponding to the given index + * Is the given namespace a talk namespace? + * + * @param int $index Namespace index + * @return bool */ - function getTalk( $index ) { - if ( Namespace::isTalk( $index ) ) { - return $index; - } else { - # FIXME - return $index + 1; - } + public static function isTalk( $index ) { + return $index > NS_MAIN + && $index % 2; } - function getSubject( $index ) { - if ( Namespace::isTalk( $index ) ) { - return $index - 1; - } else { - return $index; - } + /** + * Get the talk namespace index for a given namespace + * + * @param int $index Namespace index + * @return int + */ + public static function getTalk( $index ) { + return self::isTalk( $index ) + ? $index + : $index + 1; + } + + /** + * Get the subject namespace index for a given namespace + * + * @param int $index Namespace index + * @return int + */ + public static function getSubject( $index ) { + return self::isTalk( $index ) + ? $index - 1 + : $index; } /** * Returns the canonical (English Wikipedia) name for a given index + * + * @param int $index Namespace index + * @return string */ - function getCanonicalName( $index ) { + public static function getCanonicalName( $index ) { global $wgCanonicalNamespaceNames; return $wgCanonicalNamespaceNames[$index]; } @@ -108,8 +112,11 @@ /** * Returns the index for a given canonical name, or NULL * The input *must* be converted to lower case first + * + * @param string $name Namespace name + * @return int */ - function getCanonicalIndex( $name ) { + public static function getCanonicalIndex( $name ) { global $wgCanonicalNamespaceNames; static $xNamespaces = false; if ( $xNamespaces === false ) { @@ -124,7 +131,37 @@ return NULL; } } -} - -} -?> + + /** + * Can this namespace ever have a talk namespace? + * + * @param $index Namespace index + * @return bool + */ + public static function canTalk( $index ) { + return $index >= NS_MAIN; + } + + /** + * Does this namespace contain content, for the purposes + * of calculating statistics, etc? + * + * @param $index Index to check + * @return bool + */ + public static function isContent( $index ) { + global $wgContentNamespaces; + return $index == NS_MAIN || in_array( $index, $wgContentNamespaces ); + } + + /** + * Can pages in a namespace be watched? + * + * @param int $index + * @return bool + */ + public static function isWatchable( $index ) { + return $index >= NS_MAIN; + } + +} \ No newline at end of file diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ObjectCache.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ObjectCache.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ObjectCache.php 2005-07-22 07:29:14.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ObjectCache.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,20 +1,14 @@ false, 'compress_threshold' => 1500 ) ); + $wgCaches[CACHE_DB] = new MemCachedClientforWiki( + array('persistant' => $wgMemCachedPersistent, 'compress_threshold' => 1500 ) ); $cache =& $wgCaches[CACHE_DB]; $cache->set_servers( $wgMemCachedServers ); $cache->set_debug( $wgMemCachedDebug ); @@ -73,10 +67,12 @@ } elseif ( $type == CACHE_ACCEL ) { if ( !array_key_exists( CACHE_ACCEL, $wgCaches ) ) { if ( function_exists( 'eaccelerator_get' ) ) { - require_once( 'BagOStuff.php' ); $wgCaches[CACHE_ACCEL] = new eAccelBagOStuff; + } elseif ( function_exists( 'apc_fetch') ) { + $wgCaches[CACHE_ACCEL] = new APCBagOStuff; + } elseif( function_exists( 'xcache_get' ) ) { + $wgCaches[CACHE_ACCEL] = new XCacheBagOStuff(); } elseif ( function_exists( 'mmcache_get' ) ) { - require_once( 'BagOStuff.php' ); $wgCaches[CACHE_ACCEL] = new TurckBagOStuff; } else { $wgCaches[CACHE_ACCEL] = false; @@ -85,16 +81,20 @@ if ( $wgCaches[CACHE_ACCEL] !== false ) { $cache =& $wgCaches[CACHE_ACCEL]; } + } elseif ( $type == CACHE_DBA ) { + if ( !array_key_exists( CACHE_DBA, $wgCaches ) ) { + $wgCaches[CACHE_DBA] = new DBABagOStuff; + } + $cache =& $wgCaches[CACHE_DBA]; } - + if ( $type == CACHE_DB || ( $inputType == CACHE_ANYTHING && $cache === false ) ) { if ( !array_key_exists( CACHE_DB, $wgCaches ) ) { - require_once( 'BagOStuff.php' ); $wgCaches[CACHE_DB] = new MediaWikiBagOStuff('objectcache'); } $cache =& $wgCaches[CACHE_DB]; } - + if ( $cache === false ) { if ( !array_key_exists( CACHE_NONE, $wgCaches ) ) { $wgCaches[CACHE_NONE] = new FakeMemCachedClient; @@ -123,4 +123,4 @@ return $ret; } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/OutputPage.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/OutputPage.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/OutputPage.php 2005-08-18 12:06:20.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/OutputPage.php 2007-08-31 17:33:44.000000000 -0400 @@ -1,71 +1,117 @@ mHeaders = $this->mCookies = $this->mMetatags = - $this->mKeywords = $this->mLinktags = array(); + function __construct() { + global $wgAllowUserJs; + $this->mAllowUserJs = $wgAllowUserJs; + $this->mMetatags = $this->mKeywords = $this->mLinktags = array(); $this->mHTMLtitle = $this->mPagetitle = $this->mBodytext = $this->mRedirect = $this->mLastModified = $this->mSubtitle = $this->mDebugtext = $this->mRobotpolicy = - $this->mOnloadHandler = ''; + $this->mOnloadHandler = $this->mPageLinkTitle = ''; $this->mIsArticleRelated = $this->mIsarticle = $this->mPrintable = true; $this->mSuppressQuickbar = $this->mPrintable = false; $this->mLanguageLinks = array(); - $this->mCategoryLinks = array() ; + $this->mCategoryLinks = array(); $this->mDoNothing = false; $this->mContainsOldMagic = $this->mContainsNewMagic = 0; - $this->mParserOptions = ParserOptions::newFromUser( $temp = NULL ); + $this->mParserOptions = null; $this->mSquidMaxage = 0; $this->mScripts = ''; + $this->mHeadItems = array(); $this->mETag = false; + $this->mRevisionId = null; + $this->mNewSectionLink = false; + $this->mTemplateIds = array(); + } + + public function redirect( $url, $responsecode = '302' ) { + # Strip newlines as a paranoia check for header injection in PHP<5.1.2 + $this->mRedirect = str_replace( "\n", '', $url ); + $this->mRedirectCode = $responsecode; } - function addHeader( $name, $val ) { array_push( $this->mHeaders, $name.': '.$val ) ; } - function addCookie( $name, $val ) { array_push( $this->mCookies, array( $name, $val ) ); } - function redirect( $url, $responsecode = '302' ) { $this->mRedirect = $url; $this->mRedirectCode = $responsecode; } + /** + * Set the HTTP status code to send with the output. + * + * @param int $statusCode + * @return nothing + */ + function setStatusCode( $statusCode ) { $this->mStatusCode = $statusCode; } # To add an http-equiv meta tag, precede the name with "http:" function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); } function addKeyword( $text ) { array_push( $this->mKeywords, $text ); } - function addScript( $script ) { $this->mScripts .= $script; } - function getScript() { return $this->mScripts; } + function addScript( $script ) { $this->mScripts .= "\t\t".$script; } + function addStyle( $style ) { + global $wgStylePath, $wgStyleVersion; + $this->addLink( + array( + 'rel' => 'stylesheet', + 'href' => $wgStylePath . '/' . $style . '?' . $wgStyleVersion ) ); + } + + /** + * Add a self-contained script tag with the given contents + * @param string $script JavaScript text, no "; + } + + function getScript() { + return $this->mScripts . $this->getHeadItems(); + } + + function getHeadItems() { + $s = ''; + foreach ( $this->mHeadItems as $item ) { + $s .= $item; + } + return $s; + } + + function addHeadItem( $name, $value ) { + $this->mHeadItems[$name] = $value; + } + + function hasHeadItem( $name ) { + return isset( $this->mHeadItems[$name] ); + } function setETag($tag) { $this->mETag = $tag; } function setArticleBodyOnly($only) { $this->mArticleBodyOnly = $only; } @@ -87,84 +133,80 @@ /** * checkLastModified tells the client to use the client-cached page if * possible. If sucessful, the OutputPage is disabled so that - * any future call to OutputPage->output() have no effect. The method - * returns true iff cache-ok headers was sent. + * any future call to OutputPage->output() have no effect. + * + * @return bool True iff cache-ok headers was sent. */ function checkLastModified ( $timestamp ) { - global $wgLang, $wgCachePages, $wgUser; + global $wgCachePages, $wgCacheEpoch, $wgUser, $wgRequest; + $fname = 'OutputPage::checkLastModified'; + if ( !$timestamp || $timestamp == '19700101000000' ) { - wfDebug( "CACHE DISABLED, NO TIMESTAMP\n" ); + wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n" ); return; } if( !$wgCachePages ) { - wfDebug( "CACHE DISABLED\n", false ); + wfDebug( "$fname: CACHE DISABLED\n", false ); return; } if( $wgUser->getOption( 'nocache' ) ) { - wfDebug( "USER DISABLED CACHE\n", false ); + wfDebug( "$fname: USER DISABLED CACHE\n", false ); return; } $timestamp=wfTimestamp(TS_MW,$timestamp); - $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched ) ); + $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched, $wgCacheEpoch ) ); if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { # IE sends sizes after the date like this: # Wed, 20 Aug 2003 06:51:19 GMT; length=5202 # this breaks strtotime(). $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] ); + + wfSuppressWarnings(); // E_STRICT system time bitching $modsinceTime = strtotime( $modsince ); + wfRestoreWarnings(); + $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 ); - wfDebug( "-- client send If-Modified-Since: " . $modsince . "\n", false ); - wfDebug( "-- we might send Last-Modified : $lastmod\n", false ); - if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) ) { + wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false ); + wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false ); + if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) { # Make sure you're in a place you can leave when you call us! - header( "HTTP/1.0 304 Not Modified" ); + $wgRequest->response()->header( "HTTP/1.0 304 Not Modified" ); $this->mLastModified = $lastmod; $this->sendCacheControl(); - wfDebug( "CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp\n", false ); + wfDebug( "$fname: CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); $this->disable(); - @ob_end_clean(); // Don't output compressed blob + + // Don't output a compressed blob when using ob_gzhandler; + // it's technically against HTTP spec and seems to confuse + // Firefox when the response gets split over two packets. + wfClearOutputBuffers(); + return true; } else { - wfDebug( "READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp\n", false ); + wfDebug( "$fname: READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false ); $this->mLastModified = $lastmod; } } else { - wfDebug( "client did not send If-Modified-Since header\n", false ); + wfDebug( "$fname: client did not send If-Modified-Since header\n", false ); $this->mLastModified = $lastmod; } } + function setPageTitleActionText( $text ) { + $this->mPageTitleActionText = $text; + } + function getPageTitleActionText () { - global $action; - switch($action) { - case 'edit': - return wfMsg('edit'); - case 'history': - return wfMsg('history_short'); - case 'protect': - return wfMsg('protect'); - case 'unprotect': - return wfMsg('unprotect'); - case 'delete': - return wfMsg('delete'); - case 'watch': - return wfMsg('watch'); - case 'unwatch': - return wfMsg('unwatch'); - case 'submit': - return wfMsg('preview'); - case 'info': - return wfMsg('info_short'); - default: - return ''; + if ( isset( $this->mPageTitleActionText ) ) { + return $this->mPageTitleActionText; } } - function setRobotpolicy( $str ) { $this->mRobotpolicy = $str; } - function setHTMLTitle( $name ) {$this->mHTMLtitle = $name; } - function setPageTitle( $name ) { + public function setRobotpolicy( $str ) { $this->mRobotpolicy = $str; } + public function setHTMLTitle( $name ) {$this->mHTMLtitle = $name; } + public function setPageTitle( $name ) { global $action, $wgContLang; $name = $wgContLang->convert($name, true); $this->mPagetitle = $name; @@ -174,152 +216,277 @@ $name .= ' - '.$taction; } } - + $this->setHTMLTitle( wfMsg( 'pagetitle', $name ) ); } - function getHTMLTitle() { return $this->mHTMLtitle; } - function getPageTitle() { return $this->mPagetitle; } - function setSubtitle( $str ) { $this->mSubtitle = /*$this->parse(*/$str/*)*/; } // @bug 2514 - function getSubtitle() { return $this->mSubtitle; } - function isArticle() { return $this->mIsarticle; } - function setPrintable() { $this->mPrintable = true; } - function isPrintable() { return $this->mPrintable; } - function setSyndicated( $show = true ) { $this->mShowFeedLinks = $show; } - function isSyndicated() { return $this->mShowFeedLinks; } - function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; } - function getOnloadHandler() { return $this->mOnloadHandler; } - function disable() { $this->mDoNothing = true; } + public function getHTMLTitle() { return $this->mHTMLtitle; } + public function getPageTitle() { return $this->mPagetitle; } + public function setSubtitle( $str ) { $this->mSubtitle = /*$this->parse(*/$str/*)*/; } // @bug 2514 + public function getSubtitle() { return $this->mSubtitle; } + public function isArticle() { return $this->mIsarticle; } + public function setPrintable() { $this->mPrintable = true; } + public function isPrintable() { return $this->mPrintable; } + public function setSyndicated( $show = true ) { $this->mShowFeedLinks = $show; } + public function isSyndicated() { return $this->mShowFeedLinks; } + public function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; } + public function getOnloadHandler() { return $this->mOnloadHandler; } + public function disable() { $this->mDoNothing = true; } - function setArticleRelated( $v ) { + public function setArticleRelated( $v ) { $this->mIsArticleRelated = $v; if ( !$v ) { $this->mIsarticle = false; } } - function setArticleFlag( $v ) { + public function setArticleFlag( $v ) { $this->mIsarticle = $v; if ( $v ) { $this->mIsArticleRelated = $v; } } - function isArticleRelated() { return $this->mIsArticleRelated; } + public function isArticleRelated() { return $this->mIsArticleRelated; } - function getLanguageLinks() { return $this->mLanguageLinks; } - function addLanguageLinks($newLinkArray) { + public function getLanguageLinks() { return $this->mLanguageLinks; } + public function addLanguageLinks($newLinkArray) { $this->mLanguageLinks += $newLinkArray; } - function setLanguageLinks($newLinkArray) { + public function setLanguageLinks($newLinkArray) { $this->mLanguageLinks = $newLinkArray; } - function getCategoryLinks() { + public function getCategoryLinks() { return $this->mCategoryLinks; } - function addCategoryLinks($newLinkArray) { - $this->mCategoryLinks += $newLinkArray; + + /** + * Add an array of categories, with names in the keys + */ + public function addCategoryLinks($categories) { + global $wgUser, $wgContLang; + + if ( !is_array( $categories ) ) { + return; + } + # Add the links to the link cache in a batch + $arr = array( NS_CATEGORY => $categories ); + $lb = new LinkBatch; + $lb->setArray( $arr ); + $lb->execute(); + + $sk = $wgUser->getSkin(); + foreach ( $categories as $category => $unused ) { + $title = Title::makeTitleSafe( NS_CATEGORY, $category ); + $text = $wgContLang->convertHtml( $title->getText() ); + $this->mCategoryLinks[] = $sk->makeLinkObj( $title, $text ); + } } - function setCategoryLinks($newLinkArray) { - $this->mCategoryLinks += $newLinkArray; + + public function setCategoryLinks($categories) { + $this->mCategoryLinks = array(); + $this->addCategoryLinks($categories); } - function suppressQuickbar() { $this->mSuppressQuickbar = true; } - function isQuickbarSuppressed() { return $this->mSuppressQuickbar; } + public function suppressQuickbar() { $this->mSuppressQuickbar = true; } + public function isQuickbarSuppressed() { return $this->mSuppressQuickbar; } + + public function disallowUserJs() { $this->mAllowUserJs = false; } + public function isUserJsAllowed() { return $this->mAllowUserJs; } - function addHTML( $text ) { $this->mBodytext .= $text; } - function clearHTML() { $this->mBodytext = ''; } - function getHTML() { return $this->mBodytext; } - function debug( $text ) { $this->mDebugtext .= $text; } + public function addHTML( $text ) { $this->mBodytext .= $text; } + public function clearHTML() { $this->mBodytext = ''; } + public function getHTML() { return $this->mBodytext; } + public function debug( $text ) { $this->mDebugtext .= $text; } - function setParserOptions( $options ) { + /* @deprecated */ + public function setParserOptions( $options ) { + return $this->parserOptions( $options ); + } + + public function parserOptions( $options = null ) { + if ( !$this->mParserOptions ) { + $this->mParserOptions = new ParserOptions; + } return wfSetVar( $this->mParserOptions, $options ); } /** + * Set the revision ID which will be seen by the wiki text parser + * for things such as embedded {{REVISIONID}} variable use. + * @param mixed $revid an integer, or NULL + * @return mixed previous value + */ + public function setRevisionId( $revid ) { + $val = is_null( $revid ) ? null : intval( $revid ); + return wfSetVar( $this->mRevisionId, $val ); + } + + /** * Convert wikitext to HTML and add it to the buffer * Default assumes that the current page title will * be used. + * + * @param string $text + * @param bool $linestart */ - function addWikiText( $text, $linestart = true ) { + public function addWikiText( $text, $linestart = true ) { global $wgTitle; $this->addWikiTextTitle($text, $wgTitle, $linestart); } - function addWikiTextWithTitle($text, &$title, $linestart = true) { + public function addWikiTextWithTitle($text, &$title, $linestart = true) { $this->addWikiTextTitle($text, $title, $linestart); } - function addWikiTextTitle($text, &$title, $linestart) { - global $wgParser, $wgUseTidy; - $parserOutput = $wgParser->parse( $text, $title, $this->mParserOptions, $linestart ); + function addWikiTextTitleTidy($text, &$title, $linestart = true) { + $this->addWikiTextTitle( $text, $title, $linestart, true ); + } + + public function addWikiTextTitle($text, &$title, $linestart, $tidy = false) { + global $wgParser; + + $fname = 'OutputPage:addWikiTextTitle'; + wfProfileIn($fname); + + wfIncrStats('pcache_not_possible'); + + $popts = $this->parserOptions(); + $popts->setTidy($tidy); + + $parserOutput = $wgParser->parse( $text, $title, $popts, + $linestart, true, $this->mRevisionId ); + + $this->addParserOutput( $parserOutput ); + + wfProfileOut($fname); + } + + /** + * @todo document + * @param ParserOutput object &$parserOutput + */ + public function addParserOutputNoText( &$parserOutput ) { $this->mLanguageLinks += $parserOutput->getLanguageLinks(); - $this->mCategoryLinks += $parserOutput->getCategoryLinks(); + $this->addCategoryLinks( $parserOutput->getCategories() ); + $this->mNewSectionLink = $parserOutput->getNewSection(); + $this->addKeywords( $parserOutput ); if ( $parserOutput->getCacheTime() == -1 ) { $this->enableClientCache( false ); } - $this->addHTML( $parserOutput->getText() ); + $this->mNoGallery = $parserOutput->getNoGallery(); + $this->mHeadItems = array_merge( $this->mHeadItems, (array)$parserOutput->mHeadItems ); + // Versioning... + $this->mTemplateIds += (array)$parserOutput->mTemplateIds; + + # Display title + if( ( $dt = $parserOutput->getDisplayTitle() ) !== false ) + $this->setPageTitle( $dt ); + + # Hooks registered in the object + global $wgParserOutputHooks; + foreach ( $parserOutput->getOutputHooks() as $hookInfo ) { + list( $hookName, $data ) = $hookInfo; + if ( isset( $wgParserOutputHooks[$hookName] ) ) { + call_user_func( $wgParserOutputHooks[$hookName], $this, $parserOutput, $data ); + } + } + + wfRunHooks( 'OutputPageParserOutput', array( &$this, $parserOutput ) ); + } + + /** + * @todo document + * @param ParserOutput &$parserOutput + */ + function addParserOutput( &$parserOutput ) { + $this->addParserOutputNoText( $parserOutput ); + $text = $parserOutput->getText(); + wfRunHooks( 'OutputPageBeforeHTML',array( &$this, &$text ) ); + $this->addHTML( $text ); } /** * Add wikitext to the buffer, assuming that this is the primary text for a page view - * Saves the text into the parser cache if possible + * Saves the text into the parser cache if possible. + * + * @param string $text + * @param Article $article + * @param bool $cache + * @deprecated Use Article::outputWikitext */ - function addPrimaryWikiText( $text, $cacheArticle ) { - global $wgParser, $wgParserCache, $wgUser, $wgTitle, $wgUseTidy; + public function addPrimaryWikiText( $text, $article, $cache = true ) { + global $wgParser, $wgUser; - $parserOutput = $wgParser->parse( $text, $wgTitle, $this->mParserOptions, true ); + $popts = $this->parserOptions(); + $popts->setTidy(true); + $parserOutput = $wgParser->parse( $text, $article->mTitle, + $popts, true, true, $this->mRevisionId ); + $popts->setTidy(false); + if ( $cache && $article && $parserOutput->getCacheTime() != -1 ) { + $parserCache =& ParserCache::singleton(); + $parserCache->save( $parserOutput, $article, $wgUser ); + } - $text = $parserOutput->getText(); + $this->addParserOutput( $parserOutput ); + } - if ( $cacheArticle && $parserOutput->getCacheTime() != -1 ) { - $wgParserCache->save( $parserOutput, $cacheArticle, $wgUser ); - } + /** + * @deprecated use addWikiTextTidy() + */ + public function addSecondaryWikiText( $text, $linestart = true ) { + global $wgTitle; + $this->addWikiTextTitleTidy($text, $wgTitle, $linestart); + } - $this->mLanguageLinks += $parserOutput->getLanguageLinks(); - $this->mCategoryLinks += $parserOutput->getCategoryLinks(); - if ( $parserOutput->getCacheTime() == -1 ) { - $this->enableClientCache( false ); - } - $this->addHTML( $text ); + /** + * Add wikitext with tidy enabled + */ + public function addWikiTextTidy( $text, $linestart = true ) { + global $wgTitle; + $this->addWikiTextTitleTidy($text, $wgTitle, $linestart); } + /** * Add the output of a QuickTemplate to the output buffer + * * @param QuickTemplate $template */ - function addTemplate( &$template ) { + public function addTemplate( &$template ) { ob_start(); $template->execute(); - $this->addHtml( ob_get_contents() ); + $this->addHTML( ob_get_contents() ); ob_end_clean(); } /** - * Parse wikitext and return the HTML. This is for special pages that add the text later + * Parse wikitext and return the HTML. + * + * @param string $text + * @param bool $linestart Is this the start of a line? + * @param bool $interface ?? */ - function parse( $text, $linestart = true ) { + public function parse( $text, $linestart = true, $interface = false ) { global $wgParser, $wgTitle; - $parserOutput = $wgParser->parse( $text, $wgTitle, $this->mParserOptions, $linestart ); + $popts = $this->parserOptions(); + if ( $interface) { $popts->setInterfaceMessage(true); } + $parserOutput = $wgParser->parse( $text, $wgTitle, $popts, + $linestart, true, $this->mRevisionId ); + if ( $interface) { $popts->setInterfaceMessage(false); } return $parserOutput->getText(); } /** - * @param $article - * @param $user + * @param Article $article + * @param User $user * - * @return bool + * @return bool True if successful, else false. */ - function tryParserCache( $article, $user ) { - global $wgParserCache; - $parserOutput = $wgParserCache->get( $article, $user ); + public function tryParserCache( &$article, $user ) { + $parserCache =& ParserCache::singleton(); + $parserOutput = $parserCache->get( $article, $user ); if ( $parserOutput !== false ) { - $this->mLanguageLinks += $parserOutput->getLanguageLinks(); - $this->mCategoryLinks += $parserOutput->getCategoryLinks(); - $this->addHTML( $parserOutput->getText() ); - $t = $parserOutput->getTitleText(); - if( !empty( $t ) ) { - $this->setPageTitle( $t ); - } + $this->addParserOutput( $parserOutput ); return true; } else { return false; @@ -327,18 +494,17 @@ } /** - * Set the maximum cache time on the Squid in seconds - * @param $maxage + * @param int $maxage Maximum cache time on the Squid, in seconds. */ - function setSquidMaxage( $maxage ) { + public function setSquidMaxage( $maxage ) { $this->mSquidMaxage = $maxage; } /** * Use enableClientCache(false) to force it to send nocache headers - * @param $state + * @param $state ?? */ - function enableClientCache( $state ) { + public function enableClientCache( $state ) { return wfSetVar( $this->mEnableClientCache, $state ); } @@ -348,54 +514,55 @@ && $wgRequest->getText('uselang', false) === false; } - function sendCacheControl() { - global $wgUseSquid, $wgUseESI; + public function sendCacheControl() { + global $wgUseSquid, $wgUseESI, $wgUseETag, $wgSquidMaxage, $wgRequest; + $fname = 'OutputPage::sendCacheControl'; - if ($this->mETag) - header("ETag: $this->mETag"); + if ($wgUseETag && $this->mETag) + $wgRequest->response()->header("ETag: $this->mETag"); # don't serve compressed data to clients who can't handle it # maintain different caches for logged-in users and non-logged in ones - header( 'Vary: Accept-Encoding, Cookie' ); + $wgRequest->response()->header( 'Vary: Accept-Encoding, Cookie' ); if( !$this->uncacheableBecauseRequestvars() && $this->mEnableClientCache ) { - if( $wgUseSquid && ! isset( $_COOKIE[ini_get( 'session.name') ] ) && + if( $wgUseSquid && session_id() == '' && ! $this->isPrintable() && $this->mSquidMaxage != 0 ) { if ( $wgUseESI ) { # We'll purge the proxy cache explicitly, but require end user agents # to revalidate against the proxy on each visit. # Surrogate-Control controls our Squid, Cache-Control downstream caches - wfDebug( "** proxy caching with ESI; {$this->mLastModified} **\n", false ); + wfDebug( "$fname: proxy caching with ESI; {$this->mLastModified} **\n", false ); # start with a shorter timeout for initial testing # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"'); - header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"'); - header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); + $wgRequest->response()->header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"'); + $wgRequest->response()->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); } else { # We'll purge the proxy cache for anons explicitly, but require end user agents # to revalidate against the proxy on each visit. # IMPORTANT! The Squid needs to replace the Cache-Control header with # Cache-Control: s-maxage=0, must-revalidate, max-age=0 - wfDebug( "** local proxy caching; {$this->mLastModified} **\n", false ); + wfDebug( "$fname: local proxy caching; {$this->mLastModified} **\n", false ); # start with a shorter timeout for initial testing # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" ); - header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' ); + $wgRequest->response()->header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' ); } } else { # We do want clients to cache if they can, but they *must* check for updates # on revisiting the page. - wfDebug( "** private caching; {$this->mLastModified} **\n", false ); - header( "Expires: -1" ); - header( "Cache-Control: private, must-revalidate, max-age=0" ); + wfDebug( "$fname: private caching; {$this->mLastModified} **\n", false ); + $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + $wgRequest->response()->header( "Cache-Control: private, must-revalidate, max-age=0" ); } - if($this->mLastModified) header( "Last-modified: {$this->mLastModified}" ); + if($this->mLastModified) $wgRequest->response()->header( "Last-modified: {$this->mLastModified}" ); } else { - wfDebug( "** no caching **\n", false ); + wfDebug( "$fname: no caching **\n", false ); # In general, the absence of a last modified header should be enough to prevent # the client from using its cache. We send a few other things just to make sure. - header( 'Expires: -1' ); - header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); - header( 'Pragma: no-cache' ); + $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); + $wgRequest->response()->header( 'Pragma: no-cache' ); } } @@ -403,10 +570,11 @@ * Finally, all the text has been munged and accumulated into * the object, let's actually output it: */ - function output() { - global $wgUser, $wgLang, $wgDebugComments, $wgCookieExpiration; - global $wgInputEncoding, $wgOutputEncoding, $wgContLanguageCode; - global $wgDebugRedirects, $wgMimeType, $wgProfiler; + public function output() { + global $wgUser, $wgOutputEncoding, $wgRequest; + global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType; + global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgAjaxSearch, $wgAjaxWatch; + global $wgServer, $wgStyleVersion; if( $this->mDoNothing ){ return; @@ -415,6 +583,21 @@ wfProfileIn( $fname ); $sk = $wgUser->getSkin(); + if ( $wgUseAjax ) { + $this->addScript( "\n" ); + + wfRunHooks( 'AjaxAddScript', array( &$this ) ); + + if( $wgAjaxSearch ) { + $this->addScript( "\n" ); + $this->addScript( "\n" ); + } + + if( $wgAjaxWatch && $wgUser->isLoggedIn() ) { + $this->addScript( "\n" ); + } + } + if ( '' != $this->mRedirect ) { if( substr( $this->mRedirect, 0, 4 ) != 'http' ) { # Standards require redirect URLs to be absolute @@ -423,42 +606,88 @@ } if( $this->mRedirectCode == '301') { if( !$wgDebugRedirects ) { - header("HTTP/1.1 {$this->mRedirectCode} Moved Permanently"); + $wgRequest->response()->header("HTTP/1.1 {$this->mRedirectCode} Moved Permanently"); } $this->mLastModified = wfTimestamp( TS_RFC2822 ); } $this->sendCacheControl(); + $wgRequest->response()->header("Content-Type: text/html; charset=utf-8"); if( $wgDebugRedirects ) { $url = htmlspecialchars( $this->mRedirect ); print "\n\nRedirect\n\n\n"; print "

      Location: $url

      \n"; print "\n\n"; } else { - header( 'Location: '.$this->mRedirect ); + $wgRequest->response()->header( 'Location: '.$this->mRedirect ); } - if ( isset( $wgProfiler ) ) { wfDebug( $wgProfiler->getOutput() ); } wfProfileOut( $fname ); return; } + elseif ( $this->mStatusCode ) + { + $statusMessage = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Request Range Not Satisfiable', + 417 => 'Expectation Failed', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 507 => 'Insufficient Storage' + ); + if ( $statusMessage[$this->mStatusCode] ) + $wgRequest->response()->header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $statusMessage[$this->mStatusCode] ); + } # Buffer output; final headers may depend on later processing ob_start(); - $this->transformBuffer(); - # Disable temporary placeholders, so that the skin produces HTML $sk->postParseLinkColour( false ); - header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" ); - header( 'Content-language: '.$wgContLanguageCode ); - - $exp = time() + $wgCookieExpiration; - foreach( $this->mCookies as $name => $val ) { - setcookie( $name, $val, $exp, '/' ); - } + $wgRequest->response()->header( "Content-type: $wgMimeType; charset={$wgOutputEncoding}" ); + $wgRequest->response()->header( 'Content-language: '.$wgContLanguageCode ); if ($this->mArticleBodyOnly) { $this->out($this->mBodytext); @@ -473,7 +702,11 @@ wfProfileOut( $fname ); } - function out( $ins ) { + /** + * @todo document + * @param string $ins + */ + public function out( $ins ) { global $wgInputEncoding, $wgOutputEncoding, $wgContLang; if ( 0 == strcmp( $wgInputEncoding, $wgOutputEncoding ) ) { $outs = $ins; @@ -484,17 +717,15 @@ print $outs; } - function setEncodings() { + /** + * @todo document + */ + public static function setEncodings() { global $wgInputEncoding, $wgOutputEncoding; global $wgUser, $wgContLang; $wgInputEncoding = strtolower( $wgInputEncoding ); - if( $wgUser->getOption( 'altencoding' ) ) { - $wgContLang->setAltEncoding(); - return; - } - if ( empty( $_SERVER['HTTP_ACCEPT_CHARSET'] ) ) { $wgOutputEncoding = strtolower( $wgOutputEncoding ); return; @@ -503,40 +734,82 @@ } /** - * Returns a HTML comment with the elapsed time since request. - * This method has no side effects. + * Deprecated, use wfReportTime() instead. * @return string + * @deprecated */ - function reportTime() { - global $wgRequestTime; + public function reportTime() { + $time = wfReportTime(); + return $time; + } - $now = wfTime(); - list( $usec, $sec ) = explode( ' ', $wgRequestTime ); - $start = (float)$sec + (float)$usec; - $elapsed = $now - $start; + /** + * Produce a "user is blocked" page. + * + * @param bool $return Whether to have a "return to $wgTitle" message or not. + * @return nothing + */ + function blockedPage( $return = true ) { + global $wgUser, $wgContLang, $wgTitle, $wgLang; - # Use real server name if available, so we know which machine - # in a server farm generated the current page. - if ( function_exists( 'posix_uname' ) ) { - $uname = @posix_uname(); + $this->setPageTitle( wfMsg( 'blockedtitle' ) ); + $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setArticleRelated( false ); + + $name = User::whoIs( $wgUser->blockedBy() ); + $reason = $wgUser->blockedFor(); + $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $wgUser->mBlock->mTimestamp ), true ); + $ip = wfGetIP(); + + $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]"; + + $blockid = $wgUser->mBlock->mId; + + $blockExpiry = $wgUser->mBlock->mExpiry; + if ( $blockExpiry == 'infinity' ) { + // Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite' + // Search for localization in 'ipboptions' + $scBlockExpiryOptions = wfMsg( 'ipboptions' ); + foreach ( explode( ',', $scBlockExpiryOptions ) as $option ) { + if ( strpos( $option, ":" ) === false ) + continue; + list( $show, $value ) = explode( ":", $option ); + if ( $value == 'infinite' || $value == 'indefinite' ) { + $blockExpiry = $show; + break; + } + } } else { - $uname = false; + $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true ); } - if( is_array( $uname ) && isset( $uname['nodename'] ) ) { - $hostname = $uname['nodename']; + + if ( $wgUser->mBlock->mAuto ) { + $msg = 'autoblockedtext'; } else { - # This may be a virtual server. - $hostname = $_SERVER['SERVER_NAME']; + $msg = 'blockedtext'; + } + + /* $ip returns who *is* being blocked, $intended contains who was meant to be blocked. + * This could be a username, an ip range, or a single ip. */ + $intended = $wgUser->mBlock->mAddress; + + $this->addWikiText( wfMsg( $msg, $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp ) ); + + # Don't auto-return to special pages + if( $return ) { + $return = $wgTitle->getNamespace() > -1 ? $wgTitle->getPrefixedText() : NULL; + $this->returnToMain( false, $return ); } - $com = sprintf( "", - $hostname, $elapsed ); - return $com; } /** - * Note: these arguments are keys into wfMsg(), not text! + * Output a standard error page + * + * @param string $title Message key for page title + * @param string $msg Message key for page text + * @param array $params Message parameters */ - function errorpage( $title, $msg ) { + public function showErrorPage( $title, $msg, $params = array() ) { global $wgTitle; $this->mDebugtext .= 'Original title: ' . @@ -547,42 +820,65 @@ $this->setArticleRelated( false ); $this->enableClientCache( false ); $this->mRedirect = ''; - $this->mBodytext = ''; - $this->addWikiText( wfMsg( $msg ) ); + + array_unshift( $params, 'parse' ); + array_unshift( $params, $msg ); + $this->addHtml( call_user_func_array( 'wfMsgExt', $params ) ); + $this->returnToMain( false ); + } + + /** + * Output a standard permission error page + * + * @param array $errors Error message keys + */ + public function showPermissionsErrorPage( $errors ) + { + global $wgTitle; - $this->output(); - wfErrorExit(); + $this->mDebugtext .= 'Original title: ' . + $wgTitle->getPrefixedText() . "\n"; + $this->setPageTitle( wfMsg( 'permissionserrors' ) ); + $this->setHTMLTitle( wfMsg( 'permissionserrors' ) ); + $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setArticleRelated( false ); + $this->enableClientCache( false ); + $this->mRedirect = ''; + $this->mBodytext = ''; + $this->addWikiText( $this->formatPermissionsErrorMessage( $errors ) ); } + /** @deprecated */ + public function errorpage( $title, $msg ) { + throw new ErrorPageError( $title, $msg ); + } + /** * Display an error page indicating that a given version of MediaWiki is * required to use it * * @param mixed $version The version of MediaWiki needed to use the page */ - function versionRequired( $version ) { - global $wgUser; - + public function versionRequired( $version ) { $this->setPageTitle( wfMsg( 'versionrequired', $version ) ); $this->setHTMLTitle( wfMsg( 'versionrequired', $version ) ); $this->setRobotpolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); $this->mBodytext = ''; - $sk = $wgUser->getSkin(); $this->addWikiText( wfMsg( 'versionrequiredtext', $version ) ); $this->returnToMain(); } /** * Display an error page noting that a given permission bit is required. - * This should generally replace the sysopRequired, developerRequired etc. + * * @param string $permission key required */ - function permissionRequired( $permission ) { - global $wgUser; + public function permissionRequired( $permission ) { + global $wgGroupPermissions, $wgUser; $this->setPageTitle( wfMsg( 'badaccess' ) ); $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); @@ -590,106 +886,157 @@ $this->setArticleRelated( false ); $this->mBodytext = ''; - $sk = $wgUser->getSkin(); - $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ) ); - $this->addHTML( wfMsgHtml( 'badaccesstext', $ap, $permission ) ); - $this->returnToMain(); + $groups = array(); + foreach( $wgGroupPermissions as $key => $value ) { + if( isset( $value[$permission] ) && $value[$permission] == true ) { + $groupName = User::getGroupName( $key ); + $groupPage = User::getGroupPage( $key ); + if( $groupPage ) { + $skin = $wgUser->getSkin(); + $groups[] = $skin->makeLinkObj( $groupPage, $groupName ); + } else { + $groups[] = $groupName; + } + } + } + $n = count( $groups ); + $groups = implode( ', ', $groups ); + switch( $n ) { + case 0: + case 1: + case 2: + $message = wfMsgHtml( "badaccess-group$n", $groups ); + break; + default: + $message = wfMsgHtml( 'badaccess-groups', $groups ); + } + $this->addHtml( $message ); + $this->returnToMain( false ); } /** + * Use permissionRequired. * @deprecated */ - function sysopRequired() { - global $wgUser; - - $this->setPageTitle( wfMsg( 'sysoptitle' ) ); - $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); - $this->setRobotpolicy( 'noindex,nofollow' ); - $this->setArticleRelated( false ); - $this->mBodytext = ''; - - $sk = $wgUser->getSkin(); - $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ), '' ); - $this->addHTML( wfMsgHtml( 'sysoptext', $ap ) ); - $this->returnToMain(); + public function sysopRequired() { + throw new MWException( "Call to deprecated OutputPage::sysopRequired() method\n" ); } /** + * Use permissionRequired. * @deprecated */ - function developerRequired() { - global $wgUser; - - $this->setPageTitle( wfMsg( 'developertitle' ) ); - $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); - $this->setRobotpolicy( 'noindex,nofollow' ); - $this->setArticleRelated( false ); - $this->mBodytext = ''; - - $sk = $wgUser->getSkin(); - $ap = $sk->makeKnownLink( wfMsgForContent( 'administrators' ), '' ); - $this->addHTML( wfMsgHtml( 'developertext', $ap ) ); - $this->returnToMain(); + public function developerRequired() { + throw new MWException( "Call to deprecated OutputPage::developerRequired() method\n" ); } - function loginToUse() { + /** + * Produce the stock "please login to use the wiki" page + */ + public function loginToUse() { global $wgUser, $wgTitle, $wgContLang; + if( $wgUser->isLoggedIn() ) { + $this->permissionRequired( 'read' ); + return; + } + + $skin = $wgUser->getSkin(); + $this->setPageTitle( wfMsg( 'loginreqtitle' ) ); - $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); - $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setHtmlTitle( wfMsg( 'errorpagetitle' ) ); + $this->setRobotPolicy( 'noindex,nofollow' ); $this->setArticleFlag( false ); - $this->mBodytext = ''; - $this->addWikiText( wfMsg( 'loginreqtext' ) ); + + $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); + $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $wgTitle->getPrefixedUrl() ); + $this->addHtml( wfMsgWikiHtml( 'loginreqpagetext', $loginLink ) ); + $this->addHtml( "\n" ); + + # Don't return to the main page if the user can't read it + # otherwise we'll end up in a pointless loop + $mainPage = Title::newMainPage(); + if( $mainPage->userCanRead() ) + $this->returnToMain( true, $mainPage ); + } - # We put a comment in the .html file so a Sysop can diagnose the page the - # user can't see. - $this->addHTML( "\n' ); - $this->returnToMain(); # Flip back to the main page after 10 seconds. + /** @deprecated */ + public function databaseError( $fname, $sql, $error, $errno ) { + throw new MWException( "OutputPage::databaseError is obsolete\n" ); } - function databaseError( $fname, $sql, $error, $errno ) { - global $wgUser, $wgCommandLineMode, $wgShowSQLErrors; + /** + * @param array $errors An array of arrays returned by Title::getUserPermissionsErrors + * @return string The error-messages, formatted into a list. + */ + public function formatPermissionsErrorMessage( $errors ) { + $text = ''; - $this->setPageTitle( wfMsgNoDB( 'databaseerror' ) ); - $this->setRobotpolicy( 'noindex,nofollow' ); - $this->setArticleRelated( false ); - $this->enableClientCache( false ); - $this->mRedirect = ''; + if (sizeof( $errors ) > 1) { - if( !$wgShowSQLErrors ) { - $sql = wfMsg( 'sqlhidden' ); - } + $text .= wfMsgExt( 'permissionserrorstext', array( 'parse' ), count( $errors ) ) . "\n"; + $text .= '
        ' . "\n"; - if ( $wgCommandLineMode ) { - $msg = wfMsgNoDB( 'dberrortextcl', htmlspecialchars( $sql ), - htmlspecialchars( $fname ), $errno, htmlspecialchars( $error ) ); + foreach( $errors as $error ) + { + $text .= '
      • '; + $text .= call_user_func_array( 'wfMsg', $error ); + $text .= "
      • \n"; + } + $text .= '
      '; } else { - $msg = wfMsgNoDB( 'dberrortext', htmlspecialchars( $sql ), - htmlspecialchars( $fname ), $errno, htmlspecialchars( $error ) ); + $text .= call_user_func_array( 'wfMsg', $errors[0]); } - if ( $wgCommandLineMode || !is_object( $wgUser )) { - print $msg."\n"; - wfErrorExit(); - } - $this->mBodytext = $msg; - $this->output(); - wfErrorExit(); + return $text; } - function readOnlyPage( $source = null, $protected = false ) { - global $wgUser, $wgReadOnlyFile, $wgReadOnly; + /** + * @todo document + * @param bool $protected Is the reason the page can't be reached because it's protected? + * @param mixed $source + * @param bool $protected, page is protected? + * @param array $reason, array of arrays( msg, args ) + */ + public function readOnlyPage( $source = null, $protected = false, $reasons = array() ) { + global $wgUser, $wgReadOnlyFile, $wgReadOnly, $wgTitle; + $skin = $wgUser->getSkin(); $this->setRobotpolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); + + if ( !empty($reasons) ) { + $this->setPageTitle( wfMsg( 'viewsource' ) ); + $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); - if( $protected ) { + $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons ) ); + } else if( $protected ) { $this->setPageTitle( wfMsg( 'viewsource' ) ); - $this->addWikiText( wfMsg( 'protectedtext' ) ); + $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); + list( $cascadeSources, /* $restrictions */ ) = $wgTitle->getCascadeProtectionSources(); + + // Show an appropriate explanation depending upon the reason + // for the protection...all of these should be moved to the + // callers + if( $wgTitle->getNamespace() == NS_MEDIAWIKI ) { + // User isn't allowed to edit the interface + $this->addWikiText( wfMsg( 'protectedinterface' ) ); + } elseif( $cascadeSources && ( $count = count( $cascadeSources ) ) > 0 ) { + // Cascading protection + $titles = ''; + foreach( $cascadeSources as $title ) + $titles .= "* [[:" . $title->getPrefixedText() . "]]\n"; + $this->addWikiText( wfMsgExt( 'cascadeprotected', 'parsemag', $count ) . "\n{$titles}" ); + } elseif( !$wgTitle->isProtected( 'edit' ) && $wgTitle->isNamespaceProtected() ) { + // Namespace protection + $ns = $wgTitle->getNamespace() == NS_MAIN + ? wfMsg( 'nstab-main' ) + : $wgTitle->getNsText(); + $this->addWikiText( wfMsg( 'namespaceprotected', $ns ) ); + } else { + // Standard protection + $this->addWikiText( wfMsg( 'protectedpagetext' ) ); + } } else { $this->setPageTitle( wfMsg( 'readonly' ) ); if ( $wgReadOnly ) { @@ -701,106 +1048,150 @@ } if( is_string( $source ) ) { - if( strcmp( $source, '' ) == 0 ) { - $source = wfMsg( 'noarticletext' ); - } - $rows = $wgUser->getOption( 'rows' ); - $cols = $wgUser->getOption( 'cols' ); - $text = "\n"; $this->addHTML( $text ); } + $article = new Article( $wgTitle ); + $this->addHTML( $skin->formatTemplates( $article->getUsedTemplates() ) ); $this->returnToMain( false ); } - function fatalError( $message ) { + /** @deprecated */ + public function fatalError( $message ) { + throw new FatalError( $message ); + } + + /** @deprecated */ + public function unexpectedValueError( $name, $val ) { + throw new FatalError( wfMsg( 'unexpected', $name, $val ) ); + } + + /** @deprecated */ + public function fileCopyError( $old, $new ) { + throw new FatalError( wfMsg( 'filecopyerror', $old, $new ) ); + } + + /** @deprecated */ + public function fileRenameError( $old, $new ) { + throw new FatalError( wfMsg( 'filerenameerror', $old, $new ) ); + } + + /** @deprecated */ + public function fileDeleteError( $name ) { + throw new FatalError( wfMsg( 'filedeleteerror', $name ) ); + } + + /** @deprecated */ + public function fileNotFoundError( $name ) { + throw new FatalError( wfMsg( 'filenotfound', $name ) ); + } + + public function showFatalError( $message ) { $this->setPageTitle( wfMsg( "internalerror" ) ); $this->setRobotpolicy( "noindex,nofollow" ); $this->setArticleRelated( false ); $this->enableClientCache( false ); $this->mRedirect = ''; - $this->mBodytext = $message; - $this->output(); - wfErrorExit(); } - function unexpectedValueError( $name, $val ) { - $this->fatalError( wfMsg( 'unexpected', $name, $val ) ); + public function showUnexpectedValueError( $name, $val ) { + $this->showFatalError( wfMsg( 'unexpected', $name, $val ) ); } - function fileCopyError( $old, $new ) { - $this->fatalError( wfMsg( 'filecopyerror', $old, $new ) ); + public function showFileCopyError( $old, $new ) { + $this->showFatalError( wfMsg( 'filecopyerror', $old, $new ) ); } - function fileRenameError( $old, $new ) { - $this->fatalError( wfMsg( 'filerenameerror', $old, $new ) ); + public function showFileRenameError( $old, $new ) { + $this->showFatalError( wfMsg( 'filerenameerror', $old, $new ) ); } - function fileDeleteError( $name ) { - $this->fatalError( wfMsg( 'filedeleteerror', $name ) ); + public function showFileDeleteError( $name ) { + $this->showFatalError( wfMsg( 'filedeleteerror', $name ) ); } - function fileNotFoundError( $name ) { - $this->fatalError( wfMsg( 'filenotfound', $name ) ); + public function showFileNotFoundError( $name ) { + $this->showFatalError( wfMsg( 'filenotfound', $name ) ); } /** - * return from error messages or notes - * @param $auto automatically redirect the user after 10 seconds - * @param $returnto page title to return to. Default is Main Page. + * Add a "return to" link pointing to a specified title + * + * @param Title $title Title to link */ - function returnToMain( $auto = true, $returnto = NULL ) { - global $wgUser, $wgOut, $wgRequest; + public function addReturnTo( $title ) { + global $wgUser; + $link = wfMsg( 'returnto', $wgUser->getSkin()->makeLinkObj( $title ) ); + $this->addHtml( "

      {$link}

      \n" ); + } + /** + * Add a "return to" link pointing to a specified title, + * or the title indicated in the request, or else the main page + * + * @param null $unused No longer used + * @param Title $returnto Title to return to + */ + public function returnToMain( $unused = null, $returnto = NULL ) { + global $wgRequest; + if ( $returnto == NULL ) { $returnto = $wgRequest->getText( 'returnto' ); } - $returnto = htmlspecialchars( $returnto ); - - $sk = $wgUser->getSkin(); - if ( '' == $returnto ) { - $returnto = wfMsgForContent( 'mainpage' ); + + if ( '' === $returnto ) { + $returnto = Title::newMainPage(); } - $link = $sk->makeKnownLink( $returnto, '' ); - $r = wfMsg( 'returnto', $link ); - if ( $auto ) { + if ( is_object( $returnto ) ) { + $titleObj = $returnto; + } else { $titleObj = Title::newFromText( $returnto ); - $wgOut->addMeta( 'http:Refresh', '10;url=' . $titleObj->escapeFullURL() ); } - $wgOut->addHTML( "\n

      $r

      \n" ); + if ( !is_object( $titleObj ) ) { + $titleObj = Title::newMainPage(); + } + + $this->addReturnTo( $titleObj ); } /** * This function takes the title (first item of mGoodLinks), categories, existing and broken links for the page * and uses the first 10 of them for META keywords + * + * @param ParserOutput &$parserOutput */ - function addMetaTags () { - global $wgLinkCache , $wgOut ; - $categories = array_keys ( $wgLinkCache->mCategoryLinks ) ; - $good = array_keys ( $wgLinkCache->mGoodLinks ) ; - $bad = array_keys ( $wgLinkCache->mBadLinks ) ; - $a = array_merge ( array_slice ( $good , 0 , 1 ), $categories, array_slice ( $good , 1 , 9 ) , $bad ) ; - $a = array_slice ( $a , 0 , 10 ) ; # 10 keywords max - $a = implode ( ',' , $a ) ; - $strip = array( - "/<.*?>/" => '', - "/_/" => ' ' - ); - $a = htmlspecialchars(preg_replace(array_keys($strip), array_values($strip),$a )); - - $wgOut->addMeta ( 'KEYWORDS' , $a ) ; + private function addKeywords( &$parserOutput ) { + global $wgTitle; + $this->addKeyword( $wgTitle->getPrefixedText() ); + $count = 1; + $links2d =& $parserOutput->getLinks(); + if ( !is_array( $links2d ) ) { + return; + } + foreach ( $links2d as $dbkeys ) { + foreach( $dbkeys as $dbkey => $unused ) { + $this->addKeyword( $dbkey ); + if ( ++$count > 10 ) { + break 2; + } + } + } } /** - * @private - * @return string + * @return string The doctype, opening , and head element. */ - function headElement() { + public function headElement() { global $wgDocType, $wgDTD, $wgContLanguageCode, $wgOutputEncoding, $wgMimeType; - global $wgUser, $wgContLang, $wgRequest, $wgUseTrackbacks, $wgTitle; + global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces; + global $wgUser, $wgContLang, $wgUseTrackbacks, $wgTitle, $wgStyleVersion; if( $wgMimeType == 'text/xml' || $wgMimeType == 'application/xhtml+xml' || $wgMimeType == 'application/xml' ) { $ret = "\n"; @@ -815,7 +1206,11 @@ } $rtl = $wgContLang->isRTL() ? " dir='RTL'" : ''; - $ret .= "\n"; + $ret .= " $ns) { + $ret .= "xmlns:{$tag}=\"{$ns}\" "; + } + $ret .= "xml:lang=\"$wgContLanguageCode\" lang=\"$wgContLanguageCode\" $rtl>\n"; $ret .= "\n" . htmlspecialchars( $this->getHTMLTitle() ) . "\n"; array_push( $this->mMetatags, array( "http:Content-type", "$wgMimeType; charset={$wgOutputEncoding}" ) ); @@ -826,13 +1221,14 @@ } else { $media = "media='print'"; } - $printsheet = htmlspecialchars( "$wgStylePath/common/wikiprintable.css" ); + $printsheet = htmlspecialchars( "$wgStylePath/common/wikiprintable.css?$wgStyleVersion" ); $ret .= "\n"; $sk = $wgUser->getSkin(); - $ret .= $sk->getHeadScripts(); + $ret .= $sk->getHeadScripts( $this->mAllowUserJs ); $ret .= $this->mScripts; $ret .= $sk->getUserStyles(); + $ret .= $this->getHeadItems(); if ($wgUseTrackbacks && $this->isArticleRelated()) $ret .= $wgTitle->trackbackRDF(); @@ -841,8 +1237,11 @@ return $ret; } - function getHeadLinks() { - global $wgRequest, $wgStylePath; + /** + * @return string HTML tag links to be put in the header. + */ + public function getHeadLinks() { + global $wgRequest; $ret = ''; foreach ( $this->mMetatags as $tag ) { if ( 0 == strcasecmp( 'http:', substr( $tag[0], 0, 5 ) ) ) { @@ -853,20 +1252,24 @@ } $ret .= "\n"; } + $p = $this->mRobotpolicy; - if ( '' == $p ) { $p = 'index,follow'; } - $ret .= "\n"; + if( $p !== '' && $p != 'index,follow' ) { + // http://www.robotstxt.org/wc/meta-user.html + // Only show if it's different from the default robots policy + $ret .= "\n"; + } if ( count( $this->mKeywords ) > 0 ) { $strip = array( "/<.*?>/" => '', "/_/" => ' ' ); - $ret .= "mKeywords ))) . "\" />\n"; } foreach ( $this->mLinktags as $tag ) { - $ret .= ' $val ) { $ret .= " $attr=\"" . htmlspecialchars( $val ) . "\""; } @@ -877,35 +1280,52 @@ $link = $wgRequest->escapeAppendQuery( 'feed=rss' ); $ret .= "\n"; $link = $wgRequest->escapeAppendQuery( 'feed=atom' ); - $ret .= "\n"; + $ret .= "\n"; } return $ret; } /** - * Run any necessary pre-output transformations on the buffer text - */ - function transformBuffer( $options = 0 ) { - } - - - /** * Turn off regular page output and return an error reponse * for when rate limiting has triggered. * @todo i18n - * @access public */ - function rateLimited() { + public function rateLimited() { global $wgOut; $wgOut->disable(); wfHttpError( 500, 'Internal Server Error', 'Sorry, the server has encountered an internal error. ' . 'Please wait a moment and hit "refresh" to submit the request again.' ); } - + + /** + * Show an "add new section" link? + * + * @return bool + */ + public function showNewSectionLink() { + return $this->mNewSectionLink; + } + + /** + * Show a warning about slave lag + * + * If the lag is higher than $wgSlaveLagCritical seconds, + * then the warning is a bit more obvious. If the lag is + * lower than $wgSlaveLagWarning, then no warning is shown. + * + * @param int $lag Slave lag + */ + public function showLagWarning( $lag ) { + global $wgSlaveLagWarning, $wgSlaveLagCritical; + if( $lag >= $wgSlaveLagWarning ) { + $message = $lag < $wgSlaveLagCritical + ? 'lag-warn-normal' + : 'lag-warn-high'; + $warning = wfMsgExt( $message, 'parse', $lag ); + $this->addHtml( "
      \n{$warning}\n
      \n" ); + } + } + } - -} // MediaWiki - -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/PageHistory.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/PageHistory.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/PageHistory.php 2005-07-09 21:16:05.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/PageHistory.php 2007-08-20 03:42:32.000000000 -0400 @@ -3,15 +3,8 @@ * Page history * * Split off from Article.php and Skin.php, 2003-12-22 - * @package MediaWiki */ -/** */ -include_once ( 'SpecialValidate.php' ); - -define('DIR_PREV', 0); -define('DIR_NEXT', 1); - /** * This class handles printing the history page for an article. In order to * be efficient, it uses timestamps rather than offsets for paging, to avoid @@ -20,14 +13,16 @@ * Construct it by passing in an Article, and call $h->history() to print the * history. * - * @package MediaWiki */ - class PageHistory { + const DIR_PREV = 0; + const DIR_NEXT = 1; + var $mArticle, $mTitle, $mSkin; var $lastdate; var $linesonpage; var $mNotificationTimestamp; + var $mLatestId = null; /** * Construct a new PageHistory. @@ -35,7 +30,7 @@ * @param Article $article * @returns nothing */ - function PageHistory($article) { + function __construct($article) { global $wgUser; $this->mArticle =& $article; @@ -50,8 +45,7 @@ * @returns nothing */ function history() { - global $wgUser, $wgOut, $wgLang, $wgShowUpdatedMarker, $wgRequest, - $wgTitle, $wgUseValidation; + global $wgOut, $wgRequest, $wgTitle; /* * Allow client caching. @@ -68,108 +62,58 @@ * Setup page variables. */ $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $wgOut->setSubtitle( wfMsg( 'revhistory' ) ); + $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) ); $wgOut->setArticleFlag( false ); $wgOut->setArticleRelated( true ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setSyndicated( true ); + + $logPage = SpecialPage::getTitleFor( 'Log' ); + $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ), 'page=' . $this->mTitle->getPrefixedUrl() ); + + $subtitle = wfMsgHtml( 'revhistory' ) . '
      ' . $logLink; + $wgOut->setSubtitle( $subtitle ); + + $feedType = $wgRequest->getVal( 'feed' ); + if( $feedType ) { + wfProfileOut( $fname ); + return $this->feed( $feedType ); + } /* * Fail if article doesn't exist. */ - $id = $this->mTitle->getArticleID(); - if( $id == 0 ) { + if( !$this->mTitle->exists() ) { $wgOut->addWikiText( wfMsg( 'nohistory' ) ); wfProfileOut( $fname ); return; } + /* - * Extract limit, the number of revisions to show, and - * offset, the timestamp to begin at, from the URL. - */ - $limit = $wgRequest->getInt('limit', 50); - $offset = $wgRequest->getText('offset'); - - /* Offset must be an integral. */ - if (!strlen($offset) || !preg_match("/^[0-9]+$/", $offset)) - $offset = 0; - - /* - * "go=last" means to jump to the last history page. - */ - if (($gowhere = $wgRequest->getText("go")) !== NULL) { - switch ($gowhere) { - case "first": - if (($lastid = $this->getLastOffsetForPaging($id, $limit)) === NULL) - break; - $gourl = $wgTitle->getLocalURL("action=history&limit={$limit}&offset={$lastid}"); - break; - default: - $gourl = NULL; - } - - if (!is_null($gourl)) { - $wgOut->redirect($gourl); - return; - } - } - - /* - * Fetch revisions. - * - * If the user clicked "previous", we retrieve the revisions backwards, - * then reverse them. This is to avoid needing to know the timestamp of - * previous revisions when generating the URL. + * "go=first" means to jump to the last (earliest) history page. + * This is deprecated, it no longer appears in the user interface */ - $direction = $this->getDirection(); - $revisions = $this->fetchRevisions($limit, $offset, $direction); - $navbar = $this->makeNavbar($revisions, $offset, $limit, $direction); - - /* - * We fetch one more revision than needed to get the timestamp of the - * one after this page (and to know if it exists). - * - * linesonpage stores the actual number of lines. - */ - if (count($revisions) < $limit + 1) - $this->linesonpage = count($revisions); - else - $this->linesonpage = count($revisions) - 1; - - /* Un-reverse revisions */ - if ($direction == DIR_PREV) - $revisions = array_reverse($revisions); - - /* - * Print the top navbar. - */ - $s = $navbar; - $s .= $this->beginHistoryList(); - $counter = 1; - - /* - * Print each revision, excluding the one-past-the-end, if any. - */ - foreach (array_slice($revisions, 0, $limit) as $i => $line) { - $first = !$i && $offset == 0; - $next = isset( $revisions[$i + 1] ) ? $revisions[$i + 1 ] : null; - $s .= $this->historyLine($line, $next, $counter, $this->getNotificationTimestamp(), $first); - $counter++; + if ( $wgRequest->getText("go") == 'first' ) { + $limit = $wgRequest->getInt( 'limit', 50 ); + $wgOut->redirect( $wgTitle->getLocalURL( "action=history&limit={$limit}&dir=prev" ) ); + return; } + + wfRunHooks( 'PageHistoryBeforeList', array( &$this->mArticle ) ); - /* - * End navbar. - */ - $s .= $this->endHistoryList(); - $s .= $navbar; - - /* - * Article validation line. + /** + * Do the list */ - if ($wgUseValidation) - $s .= '

      ' . Validation::getStatisticsLink( $this->mArticle ) . '

      ' ; - - $wgOut->addHTML( $s ); + $pager = new PageHistoryPager( $this ); + $this->linesonpage = $pager->getNumRows(); + $wgOut->addHTML( + $pager->getNavigationBar() . + $this->beginHistoryList() . + $pager->getBody() . + $this->endHistoryList() . + $pager->getNavigationBar() + ); wfProfileOut( $fname ); } @@ -177,19 +121,30 @@ function beginHistoryList() { global $wgTitle; $this->lastdate = ''; - $s = '

      ' . wfMsg( 'histlegend' ) . '

      '; + $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 .= '
        '; + $s .= '
          ' . "\n"; return $s; } /** @todo document */ function endHistoryList() { - $last = wfMsg( 'last' ); - $s = '
        '; $s .= $this->submitButton( array( 'id' => 'historysubmit' ) ); $s .= ''; @@ -204,123 +159,192 @@ 'class' => 'historysubmit', 'type' => 'submit', 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), - 'title' => wfMsg( 'tooltip-compareselectedversions' ), + 'title' => wfMsg( 'tooltip-compareselectedversions' ).' ['.wfMsg( 'accesskey-compareselectedversions' ).']', 'value' => wfMsg( 'compareselectedversions' ), ) ) ) : ''; } - /** @todo document */ - function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false ) { - global $wgLang, $wgContLang; + /** + * Returns a row from the history printout. + * + * @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 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. + * @param bool $firstInList Whether this row corresponds to the first displayed on this history page. + * @return string HTML output for the row + */ + function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false, $firstInList = false ) { + global $wgUser, $wgLang; + $rev = new Revision( $row ); + $rev->setTitle( $this->mTitle ); - static $message; - if( !isset( $message ) ) { - foreach( explode( ' ', 'cur last selectolderversionfordiff selectnewerversionfordiff minoreditletter' ) as $msg ) { - $message[$msg] = wfMsg( $msg ); + $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"; + + 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' ); + } else if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) { + // If revision was hidden from sysops + $del = wfMsgHtml( 'rev-delundel' ); + } else { + $del = $this->mSkin->makeKnownLinkObj( $revdel, + wfMsg( 'rev-delundel' ), + 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) . + '&oldid=' . urlencode( $rev->getId() ) ); } + $s .= " ($del) "; } - - $link = $this->revLink( $row ); - - if ( 0 == $row->rev_user ) { - $contribsPage =& Title::makeTitle( NS_SPECIAL, 'Contributions' ); - $ul = $this->mSkin->makeKnownLinkObj( $contribsPage, - htmlspecialchars( $row->rev_user_text ), - 'target=' . urlencode( $row->rev_user_text ) ); - } else { - $userPage =& Title::makeTitle( NS_USER, $row->rev_user_text ); - $ul = $this->mSkin->makeLinkObj( $userPage , htmlspecialchars( $row->rev_user_text ) ); + + $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 = '
      • '; - if( $row->rev_deleted ) { - $s .= ''; + if( $row->rev_minor_edit ) { + $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') ); } - $curlink = $this->curLink( $row, $latest ); - $lastlink = $this->lastLink( $row, $next, $counter ); - $arbitrary = $this->diffButtons( $row, $latest, $counter ); - $s .= "({$curlink}) ({$lastlink}) $arbitrary {$link} {$ul}"; - if( $row->rev_minor_edit ) { - $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), $message['minoreditletter'] ); + if (!is_null($size = $rev->getSize())) { + if ($size == 0) + $stxt = wfMsgHtml('historyempty'); + else + $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) ); + $s .= " $stxt"; } - $s .= $this->mSkin->commentBlock( $row->rev_comment, $this->mTitle ); - if ($this->getNotificationTimestamp() && ($row->rev_timestamp >= $this->getNotificationTimestamp())) { - $s .= wfMsg( 'updatedmarker' ); + #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( $row->rev_deleted ) { - $s .= " " . htmlspecialchars( wfMsg( 'deletedrev' ) ); + + 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' ); + } + + $tools = array(); + + if ( !is_null( $next ) && is_object( $next ) ) { + if( $wgUser->isAllowed( 'rollback' ) && $latest ) { + $tools[] = '' + . $this->mSkin->buildRollbackLink( $rev ) + . ''; + } + + $undolink = $this->mSkin->makeKnownLinkObj( + $this->mTitle, + wfMsgHtml( 'editundo' ), + 'action=edit&undoafter=' . $next->rev_id . '&undo=' . $rev->getId() + ); + $tools[] = "{$undolink}"; } - $s .= '
      • '; + + if( $tools ) { + $s .= ' (' . implode( ' | ', $tools ) . ')'; + } + + wfRunHooks( 'PageHistoryLineEnding', array( &$row , &$s ) ); + + $s .= "\n"; return $s; } - + /** @todo document */ - function revLink( $row ) { - global $wgUser, $wgLang; - $date = $wgLang->timeanddate( $row->rev_timestamp, true ); - if( $row->rev_deleted && !$wgUser->isAllowed( 'undelete' ) ) { - return $date; + 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() ); } else { - return $this->mSkin->makeKnownLinkObj( - $this->mTitle, - $date, - 'oldid='.$row->rev_id ); + $link = $date; + } + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + return '' . $link . ''; } + return $link; } /** @todo document */ - function curLink( $row, $latest ) { - global $wgUser; - $cur = htmlspecialchars( wfMsg( 'cur' ) ); - if( $latest - || ( $row->rev_deleted && !$wgUser->isAllowed( 'undelete' ) ) ) { + function curLink( $rev, $latest ) { + $cur = wfMsgExt( 'cur', array( 'escape') ); + if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) { return $cur; } else { return $this->mSkin->makeKnownLinkObj( - $this->mTitle, - $cur, - 'diff=' . $this->getLatestID($this->mTitle->getArticleID()) - . '&oldid=' . $row->rev_id ); + $this->mTitle, $cur, + 'diff=' . $this->getLatestID() . + "&oldid=" . $rev->getId() ); } } /** @todo document */ - function lastLink( $row, $next, $counter ) { - global $wgUser; - $last = htmlspecialchars( wfMsg( 'last' ) ); - if( is_null( $next ) - || ( $row->rev_deleted && !$wgUser->isAllowed( 'undelete' ) ) ) { + function lastLink( $rev, $next, $counter ) { + $last = wfMsgExt( 'last', array( 'escape' ) ); + if ( is_null( $next ) ) { + # Probably no next row + return $last; + } 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 $last; } else { return $this->mSkin->makeKnownLinkObj( - $this->mTitle, - $last, - "diff={$row->rev_id}&oldid={$next->rev_id}", - '', - '', - ' tabindex="'.$counter.'"' ); + $this->mTitle, + $last, + "diff=" . $rev->getId() . "&oldid={$next->rev_id}" + /*, + '', + '', + "tabindex={$counter}"*/ ); } } /** @todo document */ - function diffButtons( $row, $latest, $counter ) { - global $wgUser; + function diffButtons( $rev, $firstInList, $counter ) { if( $this->linesonpage > 1) { $radio = array( 'type' => 'radio', - 'value' => $row->rev_id, - 'title' => wfMsg( 'selectolderversionfordiff' ) + 'value' => $rev->getId(), +# do we really need to flood this on every item? +# 'title' => wfMsgHtml( 'selectolderversionfordiff' ) ); - if( $row->rev_deleted && !$wgUser->isAllowed( 'undelete' ) ) { + + if( !$rev->userCan( Revision::DELETED_TEXT ) ) { $radio['disabled'] = 'disabled'; } - # XXX: move title texts to javascript - if ( $latest ) { + /** @todo: move title texts to javascript */ + if ( $firstInList ) { $first = wfElement( 'input', array_merge( $radio, array( @@ -350,100 +374,51 @@ } /** @todo document */ - function getLatestOffset($id) { - return $this->getExtremeOffset( $id, 'max' ); - } - - /** @todo document */ - function getEarliestOffset($id) { - return $this->getExtremeOffset( $id, 'min' ); - } - - /** @todo document */ - function getExtremeOffset( $id, $func ) { - $db =& wfGetDB(DB_SLAVE); - return $db->selectField( 'revision', - "$func(rev_timestamp)", - array( 'rev_page' => $id ), - 'PageHistory::getExtremeOffset' ); - } - - /** @todo document */ - function getLatestID( $id ) { - $db =& wfGetDB(DB_SLAVE); - return $db->selectField( 'revision', - "max(rev_id)", - array( 'rev_page' => $id ), - 'PageHistory::getLatestID' ); - } - - /** @todo document */ - function getLastOffsetForPaging( $id, $step = 50 ) { - $db =& wfGetDB(DB_SLAVE); - $revision = $db->tableName( 'revision' ); - $sql = "SELECT rev_timestamp FROM $revision WHERE rev_page = $id " . - "ORDER BY rev_timestamp ASC LIMIT $step"; - $res = $db->query( $sql, "PageHistory::getLastOffsetForPaging" ); - $n = $db->numRows( $res ); - - $last = null; - while( $obj = $db->fetchObject( $res ) ) { - $last = $obj->rev_timestamp; + 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' ); } - $db->freeResult( $res ); - return $last; + return $this->mLatestId; } - /** @todo document */ - function getDirection() { - global $wgRequest; - - if ($wgRequest->getText("dir") == "prev") - return DIR_PREV; - else - return DIR_NEXT; - } - - /** @todo document */ + /** + * Fetch an array of revisions, specified by a given limit, offset and + * 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) { - global $wgUser, $wgShowUpdatedMarker; - - /* Check one extra row to see whether we need to show 'next' and diff links */ - $limitplus = $limit + 1; - - $namespace = $this->mTitle->getNamespace(); - $title = $this->mTitle->getText(); - $uid = $wgUser->getID(); - $db =& wfGetDB( DB_SLAVE ); + $fname = 'PageHistory::fetchRevisions'; - $use_index = $db->useIndexClause( 'page_timestamp' ); - $revision = $db->tableName( 'revision' ); + $dbr = wfGetDB( DB_SLAVE ); - $limits = $offsets = ""; - - if ($direction == DIR_PREV) + if ($direction == PageHistory::DIR_PREV) list($dirs, $oper) = array("ASC", ">="); - else /* $direction = DIR_NEXT */ + else /* $direction == PageHistory::DIR_NEXT */ list($dirs, $oper) = array("DESC", "<="); if ($offset) - $offsets .= " AND rev_timestamp $oper '$offset' "; + $offsets = array("rev_timestamp $oper '$offset'"); + else + $offsets = array(); - if ($limit) - $limits .= " LIMIT $limitplus "; $page_id = $this->mTitle->getArticleID(); - $sql = "SELECT rev_id,rev_user," . - "rev_comment,rev_user_text,rev_timestamp,rev_minor_edit,rev_deleted ". - "FROM $revision $use_index " . - "WHERE rev_page=$page_id " . - $offsets . - "ORDER BY rev_timestamp $dirs " . - $limits; - $res = $db->query($sql, "PageHistory::fetchRevisions"); + $res = $dbr->select( + 'revision', + Revision::selectFields(), + array_merge(array("rev_page=$page_id"), $offsets), + $fname, + array('ORDER BY' => "rev_timestamp $dirs", + 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit) + ); $result = array(); - while (($obj = $db->fetchObject($res)) != NULL) + while (($obj = $dbr->fetchObject($res)) != NULL) $result[] = $obj; return $result; @@ -452,92 +427,186 @@ /** @todo document */ function getNotificationTimestamp() { global $wgUser, $wgShowUpdatedMarker; + $fname = 'PageHistory::getNotficationTimestamp'; if ($this->mNotificationTimestamp !== NULL) return $this->mNotificationTimestamp; - if ($wgUser->getID() == 0 || !$wgShowUpdatedMarker) + if ($wgUser->isAnon() || !$wgShowUpdatedMarker) return $this->mNotificationTimestamp = false; - $db =& wfGetDB(DB_SLAVE); + $dbr = wfGetDB(DB_SLAVE); - $this->mNotificationTimestamp = $db->selectField( + $this->mNotificationTimestamp = $dbr->selectField( 'watchlist', 'wl_notificationtimestamp', array( 'wl_namespace' => $this->mTitle->getNamespace(), 'wl_title' => $this->mTitle->getDBkey(), 'wl_user' => $wgUser->getID() ), - "PageHistory::getNotficationTimestamp"); + $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; } - - /** @todo document */ - function makeNavbar($revisions, $offset, $limit, $direction) { - global $wgTitle, $wgLang; - - $revisions = array_slice($revisions, 0, $limit); - - $pageid = $this->mTitle->getArticleID(); - $latestTimestamp = $this->getLatestOffset( $pageid ); - $earliestTimestamp = $this->getEarliestOffset( $pageid ); - - /* - * When we're displaying previous revisions, we need to reverse - * the array, because it's queried in reverse order. - */ - if ($direction == DIR_PREV) - $revisions = array_reverse($revisions); - - /* - * lowts is the timestamp of the first revision on this page. - * hights is the timestamp of the last revision. - */ - - $lowts = $hights = 0; - - if( count( $revisions ) ) { - $latestShown = $revisions[0]->rev_timestamp; - $earliestShown = $revisions[count($revisions) - 1]->rev_timestamp; + + /** + * 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' ) ); + return; } - - $firsturl = $wgTitle->escapeLocalURL("action=history&limit={$limit}&go=first"); - $lasturl = $wgTitle->escapeLocalURL("action=history&limit={$limit}"); - $firsttext = wfMsgHtml('histfirst'); - $lasttext = wfMsgHtml('histlast'); - - $prevurl = $wgTitle->escapeLocalURL("action=history&dir=prev&offset={$latestShown}&limit={$limit}"); - $nexturl = $wgTitle->escapeLocalURL("action=history&offset={$earliestShown}&limit={$limit}"); - - $urls = array(); - foreach (array(20, 50, 100, 250, 500) as $num) { - $urls[] = "escapeLocalURL( - "action=history&offset={$offset}&limit={$num}")."\">".$wgLang->formatNum($num).""; + + $feed = new $wgFeedClasses[$type]( + $this->mTitle->getPrefixedText() . ' - ' . + wfMsgForContent( 'history-feed-title' ), + wfMsgForContent( 'history-feed-description' ), + $this->mTitle->getFullUrl( 'action=history' ) ); + + $items = $this->fetchRevisions(10, 0, PageHistory::DIR_NEXT); + $feed->outHeader(); + if( $items ) { + foreach( $items as $row ) { + $feed->outItem( $this->feedItem( $row ) ); + } + } else { + $feed->outItem( $this->feedEmpty() ); } + $feed->outFooter(); + } + + function feedEmpty() { + global $wgOut; + return new FeedItem( + wfMsgForContent( 'nohistory' ), + $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; + * includes a diff to the previous revision (if any). + * + * @param $row + * @return FeedItem + */ + 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() ); + + if( $rev->getComment() == '' ) { + global $wgContLang; + $title = wfMsgForContent( 'history-feed-item-nocomment', + $rev->getUserText(), + $wgContLang->timeanddate( $rev->getTimestamp() ) ); + } else { + $title = $rev->getUserText() . ": " . $this->stripComment( $rev->getComment() ); + } + + return new FeedItem( + $title, + $text, + $this->mTitle->getFullUrl( 'diff=' . $rev->getId() . '&oldid=prev' ), + $rev->getTimestamp(), + $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 ); + } +} - $bits = implode($urls, ' | '); - wfDebug("latestShown=$latestShown latestTimestamp=$latestTimestamp\n"); - if( $latestShown < $latestTimestamp ) { - $prevtext = "".wfMsgHtml("prevn", $limit).""; - $lasttext = "$lasttext"; +/** + * @addtogroup Pager + */ +class PageHistoryPager extends ReverseChronologicalPager { + public $mLastRow = false, $mPageHistory; + + function __construct( $pageHistory ) { + parent::__construct(); + $this->mPageHistory = $pageHistory; + } + + function getQueryInfo() { + return array( + 'tables' => 'revision', + 'fields' => Revision::selectFields(), + 'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ), + 'options' => array( 'USE INDEX' => 'page_timestamp' ) + ); + } + + function getIndexField() { + return 'rev_timestamp'; + } + + function formatRow( $row ) { + if ( $this->mLastRow ) { + $latest = $this->mCounter == 1 && $this->mOffset == ''; + $firstInList = $this->mCounter == 1; + $s = $this->mPageHistory->historyLine( $this->mLastRow, $row, $this->mCounter++, + $this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList ); } else { - $prevtext = wfMsgHtml("prevn", $limit); + $s = ''; } + $this->mLastRow = $row; + return $s; + } + + function getStartBody() { + $this->mLastRow = false; + $this->mCounter = 1; + return ''; + } - wfDebug("earliestShown=$earliestShown earliestTimestamp=$earliestTimestamp\n"); - if( $earliestShown > $earliestTimestamp ) { - $nexttext = "".wfMsgHtml("nextn", $limit).""; - $firsttext = "$firsttext"; + function getEndBody() { + if ( $this->mLastRow ) { + $latest = $this->mCounter == 1 && $this->mOffset == 0; + $firstInList = $this->mCounter == 1; + if ( $this->mIsBackwards ) { + # Next row is unknown, but for UI reasons, probably exists if an offset has been specified + if ( $this->mOffset == '' ) { + $next = null; + } else { + $next = 'unknown'; + } + } else { + # 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 ); } else { - $nexttext = wfMsgHtml("nextn", $limit); + $s = ''; } - - $firstlast = "($lasttext | $firsttext)"; - - return "$firstlast " . wfMsgHtml("viewprevnext", $prevtext, $nexttext, $bits); + return $s; } } -?> + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Parser.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Parser.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Parser.php 2006-03-26 14:38:24.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Parser.php 2007-08-29 12:10:36.000000000 -0400 @@ -1,34 +1,18 @@ "\\x00-\\x20\\x7F]' ); -# Including space -define( 'EXT_LINK_TEXT_CLASS', '[^\]\\x00-\\x1F\\x7F]' ); +define( 'EXT_LINK_URL_CLASS', '[^][<>"\\x00-\\x20\\x7F]' ); +# Including space, but excluding newlines +define( 'EXT_LINK_TEXT_CLASS', '[^\]\\x0a\\x0d]' ); define( 'EXT_IMAGE_FNAME_CLASS', '[A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]' ); define( 'EXT_IMAGE_EXTENSIONS', 'gif|png|jpg|jpeg' ); -define( 'EXT_LINK_BRACKETED', '/\[(\b('.$wgUrlProtocols.')'.EXT_LINK_URL_CLASS.'+) *('.EXT_LINK_TEXT_CLASS.'*?)\]/S' ); +define( 'EXT_LINK_BRACKETED', '/\[(\b(' . wfUrlProtocols() . ')'. + EXT_LINK_URL_CLASS.'+) *('.EXT_LINK_TEXT_CLASS.'*?)\]/S' ); define( 'EXT_IMAGE_REGEX', '/^('.HTTP_PROTOCOLS.')'. # Protocol '('.EXT_LINK_URL_CLASS.'+)\\/'. # Hostname and path '('.EXT_IMAGE_FNAME_CLASS.'+)\\.((?i)'.EXT_IMAGE_EXTENSIONS.')$/S' # Filename ); +// State constants for the definition list colon extraction +define( 'MW_COLON_STATE_TEXT', 0 ); +define( 'MW_COLON_STATE_TAG', 1 ); +define( 'MW_COLON_STATE_TAGSTART', 2 ); +define( 'MW_COLON_STATE_CLOSETAG', 3 ); +define( 'MW_COLON_STATE_TAGSLASH', 4 ); +define( 'MW_COLON_STATE_COMMENT', 5 ); +define( 'MW_COLON_STATE_COMMENTDASH', 6 ); +define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 ); + /** - * PHP Parser - * - * Processes wiki markup + * PHP Parser - Processes wiki markup (which uses a more user-friendly + * syntax, such as "[[link]]" for making links), and provides a one-way + * transformation of that wiki markup it into XHTML output / markup + * (which in turn the browser understands, and can display). * *
        - * There are three main entry points into the Parser class:
        + * There are four main entry points into the Parser class:
          * parse()
          *   produces HTML output
          * preSaveTransform().
          *   produces altered wiki markup.
          * transformMsg()
          *   performs brace substitution on MediaWiki messages
        + * preprocess()
        + *   removes HTML comments and expands templates
          *
          * Globals used:
        - *    objects:   $wgLang, $wgLinkCache
        + *    objects:   $wgLang, $wgContLang
          *
          * NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
          *
          * settings:
          *  $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*,
          *  $wgNamespacesWithSubpages, $wgAllowExternalImages*,
        - *  $wgLocaltimezone, $wgAllowSpecialInclusion*
        + *  $wgLocaltimezone, $wgAllowSpecialInclusion*,
        + *  $wgMaxArticleSize*
          *
          *  * only within ParserOptions
          * 
        * - * @package MediaWiki + * @addtogroup Parser */ class Parser { + const VERSION = MW_PARSER_VERSION; /**#@+ - * @access private + * @private */ # Persistent: - var $mTagHooks; - + var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables, + $mImageParams, $mImageParamsMagicArray; + # Cleared with clearState(): - var $mOutput, $mAutonumber, $mDTopen, $mStripState = array(); - var $mVariables, $mIncludeCount, $mArgStack, $mLastSection, $mInPre; + var $mOutput, $mAutonumber, $mDTopen, $mStripState; + var $mIncludeCount, $mArgStack, $mLastSection, $mInPre; var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix; - - # Temporary: - var $mOptions, $mTitle, $mOutputType, - $mTemplates, // cache of already loaded templates, avoids + var $mIncludeSizes, $mDefaultSort; + var $mTemplates, // cache of already loaded templates, avoids // multiple SQL queries for the same string $mTemplatePath; // stores an unsorted hash of all the templates already loaded // in this path. Used for loop detection. - var $mIWTransData = array(); + # Temporary + # These are variables reset at least once per parse regardless of $clearState + var $mOptions, // ParserOptions object + $mTitle, // Title context, used for self-link rendering and similar things + $mOutputType, // Output type, one of the OT_xxx constants + $ot, // Shortcut alias, see setOutputType() + $mRevisionId, // ID to display in {{REVISIONID}} tags + $mRevisionTimestamp, // The timestamp of the specified revision ID + $mRevIdForTs; // The revision ID which was used to fetch the timestamp /**#@-*/ /** * Constructor * - * @access public + * @public */ function Parser() { - global $wgContLang; - $this->mTemplates = array(); - $this->mTemplatePath = array(); $this->mTagHooks = array(); - $this->clearState(); + $this->mTransparentTagHooks = array(); + $this->mFunctionHooks = array(); + $this->mFunctionSynonyms = array( 0 => array(), 1 => array() ); + $this->mFirstCall = true; + } + + /** + * Do various kinds of initialisation on the first call of the parser + */ + function firstCallInit() { + if ( !$this->mFirstCall ) { + return; + } + + wfProfileIn( __METHOD__ ); + global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions; + + $this->setHook( 'pre', array( $this, 'renderPreTag' ) ); + + $this->setFunctionHook( 'int', array( 'CoreParserFunctions', 'intFunction' ), SFH_NO_HASH ); + $this->setFunctionHook( 'ns', array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH ); + $this->setFunctionHook( 'urlencode', array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH ); + $this->setFunctionHook( 'lcfirst', array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH ); + $this->setFunctionHook( 'ucfirst', array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH ); + $this->setFunctionHook( 'lc', array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH ); + $this->setFunctionHook( 'uc', array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH ); + $this->setFunctionHook( 'localurl', array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH ); + $this->setFunctionHook( 'localurle', array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH ); + $this->setFunctionHook( 'fullurl', array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH ); + $this->setFunctionHook( 'fullurle', array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH ); + $this->setFunctionHook( 'formatnum', array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH ); + $this->setFunctionHook( 'grammar', array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH ); + $this->setFunctionHook( 'plural', array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofpages', array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofusers', array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofarticles', array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberoffiles', array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofadmins', array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH ); + $this->setFunctionHook( 'numberofedits', array( 'CoreParserFunctions', 'numberofedits' ), SFH_NO_HASH ); + $this->setFunctionHook( 'language', array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH ); + $this->setFunctionHook( 'padleft', array( 'CoreParserFunctions', 'padleft' ), SFH_NO_HASH ); + $this->setFunctionHook( 'padright', array( 'CoreParserFunctions', 'padright' ), SFH_NO_HASH ); + $this->setFunctionHook( 'anchorencode', array( 'CoreParserFunctions', 'anchorencode' ), SFH_NO_HASH ); + $this->setFunctionHook( 'special', array( 'CoreParserFunctions', 'special' ) ); + $this->setFunctionHook( 'defaultsort', array( 'CoreParserFunctions', 'defaultsort' ), SFH_NO_HASH ); + + if ( $wgAllowDisplayTitle ) { + $this->setFunctionHook( 'displaytitle', array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH ); + } + if ( $wgAllowSlowParserFunctions ) { + $this->setFunctionHook( 'pagesinnamespace', array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH ); + } + + $this->initialiseVariables(); + $this->mFirstCall = false; + wfProfileOut( __METHOD__ ); } /** * Clear Parser state * - * @access private + * @private */ function clearState() { + wfProfileIn( __METHOD__ ); + if ( $this->mFirstCall ) { + $this->firstCallInit(); + } $this->mOutput = new ParserOutput; $this->mAutonumber = 0; $this->mLastSection = ''; $this->mDTopen = false; - $this->mVariables = false; $this->mIncludeCount = array(); - $this->mStripState = array(); + $this->mStripState = new StripState; $this->mArgStack = array(); $this->mInPre = false; $this->mInterwikiLinkHolders = array( @@ -150,33 +216,75 @@ 'texts' => array(), 'titles' => array() ); - $this->mUniqPrefix = 'UNIQ' . Parser::getRandomString(); + $this->mRevisionTimestamp = $this->mRevisionId = null; + + /** + * Prefix for temporary replacement strings for the multipass parser. + * \x07 should never appear in input as it's disallowed in XML. + * Using it at the front also gives us a little extra robustness + * since it shouldn't match when butted up against identifier-like + * string constructs. + */ + $this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString(); + + # Clear these on every parse, bug 4549 + $this->mTemplates = array(); + $this->mTemplatePath = array(); + + $this->mShowToc = true; + $this->mForceTocPosition = false; + $this->mIncludeSizes = array( + 'pre-expand' => 0, + 'post-expand' => 0, + 'arg' => 0 + ); + $this->mDefaultSort = false; + + wfRunHooks( 'ParserClearState', array( &$this ) ); + wfProfileOut( __METHOD__ ); + } + + function setOutputType( $ot ) { + $this->mOutputType = $ot; + // Shortcut alias + $this->ot = array( + 'html' => $ot == OT_HTML, + 'wiki' => $ot == OT_WIKI, + 'msg' => $ot == OT_MSG, + 'pre' => $ot == OT_PREPROCESS, + ); } /** * Accessor for mUniqPrefix. * - * @access public + * @public */ - function UniqPrefix() { + function uniqPrefix() { return $this->mUniqPrefix; } /** - * First pass--just handle sections, pass the rest off - * to internalParse() which does all the real work. + * Convert wikitext to HTML + * Do not call this function recursively. * - * @access private * @param string $text Text we want to parse * @param Title &$title A title object * @param array $options * @param boolean $linestart * @param boolean $clearState + * @param int $revid number to pass in {{REVISIONID}} * @return ParserOutput a ParserOutput */ - function parse( $text, &$title, $options, $linestart = true, $clearState = true ) { - global $wgUseTidy, $wgContLang; - $fname = 'Parser::parse'; + public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) { + /** + * First pass--just handle sections, pass the rest off + * to internalParse() which does all the real work. + */ + + global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang; + $fname = 'Parser::parse-' . wfGetCaller(); + wfProfileIn( __METHOD__ ); wfProfileIn( $fname ); if ( $clearState ) { @@ -185,30 +293,26 @@ $this->mOptions = $options; $this->mTitle =& $title; - $this->mOutputType = OT_HTML; - - $this->mStripState = NULL; - - //$text = $this->strip( $text, $this->mStripState ); - // VOODOO MAGIC FIX! Sometimes the above segfaults in PHP5. - $x =& $this->mStripState; - - wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$x ) ); - $text = $this->strip( $text, $x ); - wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) ); - + $oldRevisionId = $this->mRevisionId; + $oldRevisionTimestamp = $this->mRevisionTimestamp; + if( $revid !== null ) { + $this->mRevisionId = $revid; + $this->mRevisionTimestamp = null; + } + $this->setOutputType( OT_HTML ); + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->strip( $text, $this->mStripState ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); $text = $this->internalParse( $text ); - - $text = $this->unstrip( $text, $this->mStripState ); + $text = $this->mStripState->unstripGeneral( $text ); # Clean up special characters, only run once, next-to-last before doBlockLevels $fixtags = array( # french spaces, last one Guillemet-left # only if there is something before the space - '/(.) (?=\\?|:|;|!|\\302\\273)/' => '\\1 \\2', + '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1 \\2', # french spaces, Guillemet-right '/(\\302\\253) /' => '\\1 ', - '/
        (.*)<\\/center *>/i' => '
        \\1
        ', ); $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text ); @@ -217,294 +321,375 @@ $this->replaceLinkHolders( $text ); - # the position of the convert() call should not be changed. it - # assumes that the links are all replaces and the only thing left + # the position of the parserConvert() call should not be changed. it + # assumes that the links are all replaced and the only thing left # is the mark. - $text = $wgContLang->convert($text); - $this->mOutput->setTitleText($wgContLang->getParsedTitle()); + # Side-effects: this calls $this->mOutput->setTitleText() + $text = $wgContLang->parserConvert( $text, $this ); - $text = $this->unstripNoWiki( $text, $this->mStripState ); + $text = $this->mStripState->unstripNoWiki( $text ); wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) ); +//!JF Move to its own function + + $uniq_prefix = $this->mUniqPrefix; + $matches = array(); + $elements = array_keys( $this->mTransparentTagHooks ); + $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); + + foreach( $matches as $marker => $data ) { + list( $element, $content, $params, $tag ) = $data; + $tagName = strtolower( $element ); + if( isset( $this->mTransparentTagHooks[$tagName] ) ) { + $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], + array( $content, $params, $this ) ); + } else { + $output = $tag; + } + $this->mStripState->general->setPair( $marker, $output ); + } + $text = $this->mStripState->unstripGeneral( $text ); + $text = Sanitizer::normalizeCharReferences( $text ); - global $wgUseTidy; - if ($wgUseTidy) { + + if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) { $text = Parser::tidy($text); + } else { + # attempt to sanitize at least some nesting problems + # (bug #2702 and quite a few others) + $tidyregs = array( + # ''Something [http://www.cool.com cool''] --> + # Somethingcool> + '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' => + '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9', + # fix up an anchor inside another anchor, only + # at least for a single single nested link (bug 3695) + '/(]+>)([^<]*)(]+>[^<]*)<\/a>(.*)<\/a>/' => + '\\1\\2\\3\\1\\4', + # fix div inside inline elements- doBlockLevels won't wrap a line which + # contains a div, so fix it up here; replace + # div with escaped text + '/(<([aib]) [^>]+>)([^<]*)(]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' => + '\\1\\3<div\\5>\\6</div>\\8\\9', + # remove empty italic or bold tag pairs, some + # introduced by rules above + '/<([bi])><\/\\1>/' => '', + ); + + $text = preg_replace( + array_keys( $tidyregs ), + array_values( $tidyregs ), + $text ); } wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) ); + # Information on include size limits, for the benefit of users who try to skirt them + if ( max( $this->mIncludeSizes ) > 1000 ) { + $max = $this->mOptions->getMaxIncludeSize(); + $text .= "\n"; + } $this->mOutput->setText( $text ); + $this->mRevisionId = $oldRevisionId; + $this->mRevisionTimestamp = $oldRevisionTimestamp; wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); + return $this->mOutput; } /** + * Recursive parser entry point that can be called from an extension tag + * hook. + */ + function recursiveTagParse( $text ) { + wfProfileIn( __METHOD__ ); + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->strip( $text, $this->mStripState ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->internalParse( $text ); + wfProfileOut( __METHOD__ ); + return $text; + } + + /** + * Expand templates and variables in the text, producing valid, static wikitext. + * Also removes comments. + */ + function preprocess( $text, $title, $options, $revid = null ) { + wfProfileIn( __METHOD__ ); + $this->clearState(); + $this->setOutputType( OT_PREPROCESS ); + $this->mOptions = $options; + $this->mTitle = $title; + if( $revid !== null ) { + $this->mRevisionId = $revid; + } + wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); + $text = $this->strip( $text, $this->mStripState ); + wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); + if ( $this->mOptions->getRemoveComments() ) { + $text = Sanitizer::removeHTMLcomments( $text ); + } + $text = $this->replaceVariables( $text ); + $text = $this->mStripState->unstripBoth( $text ); + wfProfileOut( __METHOD__ ); + return $text; + } + + /** * Get a random string * - * @access private + * @private * @static */ function getRandomString() { return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff)); } + function &getTitle() { return $this->mTitle; } + function getOptions() { return $this->mOptions; } + + function getFunctionLang() { + global $wgLang, $wgContLang; + return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang; + } + /** - * Replaces all occurrences of <$tag>content in the text - * with a random marker and returns the new text. the output parameter - * $content will be an associative array filled with data on the form - * $unique_marker => content. - * - * If $content is already set, the additional entries will be appended - * If $tag is set to STRIP_COMMENTS, the function will extract - * + * Replaces all occurrences of HTML-style comments and the given tags + * in the text with a random marker and returns teh next text. The output + * parameter $matches will be an associative array filled with data in + * the form: + * 'UNIQ-xxxxx' => array( + * 'element', + * 'tag content', + * array( 'param' => 'x' ), + * 'tag content' ) ) + * + * @param $elements list of element names. Comments are always extracted. + * @param $text Source text string. + * @param $uniq_prefix * - * @access private + * @public * @static */ - function extractTagsAndParams($tag, $text, &$content, &$tags, &$params, $uniq_prefix = ''){ - $rnd = $uniq_prefix . '-' . $tag . Parser::getRandomString(); - if ( !$content ) { - $content = array( ); - } - $n = 1; + function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){ + static $n = 1; $stripped = ''; + $matches = array(); - if ( !$tags ) { - $tags = array( ); - } - - if ( !$params ) { - $params = array( ); - } - - if( $tag == STRIP_COMMENTS ) { - $start = '//'; - } else { - $start = "/<$tag(\\s+[^>]*|\\s*)>/i"; - $end = "/<\\/$tag\\s*>/i"; - } + $taglist = implode( '|', $elements ); + $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i"; while ( '' != $text ) { $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE ); $stripped .= $p[0]; - if( count( $p ) < 3 ) { + if( count( $p ) < 5 ) { break; } - $attributes = $p[1]; - $inside = $p[2]; + if( count( $p ) > 5 ) { + // comment + $element = $p[4]; + $attributes = ''; + $close = ''; + $inside = $p[5]; + } else { + // tag + $element = $p[1]; + $attributes = $p[2]; + $close = $p[3]; + $inside = $p[4]; + } - $marker = $rnd . sprintf('%08X', $n++); + $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . "-QINU\x07"; $stripped .= $marker; - $tags[$marker] = "<$tag$attributes>"; - $params[$marker] = Sanitizer::decodeTagAttributes( $attributes ); - - $q = preg_split( $end, $inside, 2 ); - $content[$marker] = $q[0]; - if( count( $q ) < 2 ) { - # No end tag -- let it run out to the end of the text. - break; + if ( $close === '/>' ) { + // Empty element tag, + $content = null; + $text = $inside; + $tail = null; } else { - $text = $q[1]; + if( $element == '!--' ) { + $end = '/(-->)/'; + } else { + $end = "/(<\\/$element\\s*>)/i"; + } + $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE ); + $content = $q[0]; + if( count( $q ) < 3 ) { + # No end tag -- let it run out to the end of the text. + $tail = ''; + $text = ''; + } else { + $tail = $q[1]; + $text = $q[2]; + } } + + $matches[$marker] = array( $element, + $content, + Sanitizer::decodeTagAttributes( $attributes ), + "<$element$attributes$close$content$tail" ); } return $stripped; } /** - * Wrapper function for extractTagsAndParams - * for cases where $tags and $params isn't needed - * i.e. where tags will never have params, like - * - * @access private - * @static - */ - function extractTags( $tag, $text, &$content, $uniq_prefix = '' ) { - $dummy_tags = array(); - $dummy_params = array(); - - return Parser::extractTagsAndParams( $tag, $text, $content, - $dummy_tags, $dummy_params, $uniq_prefix ); - } - - /** * Strips and renders nowiki, pre, math, hiero * If $render is set, performs necessary rendering operations on plugins * Returns the text, and fills an array with data needed in unstrip() - * If the $state is already a valid strip state, it adds to the state + * + * @param StripState $state * * @param bool $stripcomments when set, HTML comments * will be stripped in addition to other tags. This is important * for section editing, where these comments cause confusion when * counting the sections in the wikisource * - * @access private + * @param array dontstrip contains tags which should not be stripped; + * used to prevent stipping of when saving (fixes bug 2700) + * + * @private */ - function strip( $text, &$state, $stripcomments = false ) { + function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) { + global $wgContLang; + wfProfileIn( __METHOD__ ); $render = ($this->mOutputType == OT_HTML); - $html_content = array(); - $nowiki_content = array(); - $math_content = array(); - $pre_content = array(); - $comment_content = array(); - $ext_content = array(); - $ext_tags = array(); - $ext_params = array(); - $gallery_content = array(); - # Replace any instances of the placeholders $uniq_prefix = $this->mUniqPrefix; - #$text = str_replace( $uniq_prefix, wfHtmlEscapeFirst( $uniq_prefix ), $text ); - - # html + $commentState = new ReplacementArray; + $nowikiItems = array(); + $generalItems = array(); + + $elements = array_merge( + array( 'nowiki', 'gallery' ), + array_keys( $this->mTagHooks ) ); global $wgRawHtml; if( $wgRawHtml ) { - $text = Parser::extractTags('html', $text, $html_content, $uniq_prefix); - foreach( $html_content as $marker => $content ) { - if ($render ) { - # Raw and unchecked for validity. - $html_content[$marker] = $content; - } else { - $html_content[$marker] = ''.$content.''; - } - } - } - - # nowiki - $text = Parser::extractTags('nowiki', $text, $nowiki_content, $uniq_prefix); - foreach( $nowiki_content as $marker => $content ) { - if( $render ){ - $nowiki_content[$marker] = wfEscapeHTMLTagsOnly( $content ); - } else { - $nowiki_content[$marker] = ''.$content.''; - } + $elements[] = 'html'; } - - # math if( $this->mOptions->getUseTeX() ) { - $text = Parser::extractTags('math', $text, $math_content, $uniq_prefix); - foreach( $math_content as $marker => $content ){ - if( $render ) { - $math_content[$marker] = renderMath( $content ); - } else { - $math_content[$marker] = ''.$content.''; - } - } + $elements[] = 'math'; } - # pre - $text = Parser::extractTags('pre', $text, $pre_content, $uniq_prefix); - foreach( $pre_content as $marker => $content ){ - if( $render ){ - $pre_content[$marker] = '
        ' . wfEscapeHTMLTagsOnly( $content ) . '
        '; + # Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700) + foreach ( $elements AS $k => $v ) { + if ( !in_array ( $v , $dontstrip ) ) continue; + unset ( $elements[$k] ); + } + + $matches = array(); + $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); + + foreach( $matches as $marker => $data ) { + list( $element, $content, $params, $tag ) = $data; + if( $render ) { + $tagName = strtolower( $element ); + wfProfileIn( __METHOD__."-render-$tagName" ); + switch( $tagName ) { + case '!--': + // Comment + if( substr( $tag, -3 ) == '-->' ) { + $output = $tag; + } else { + // Unclosed comment in input. + // Close it so later stripping can remove it + $output = "$tag-->"; + } + break; + case 'html': + if( $wgRawHtml ) { + $output = $content; + break; + } + // Shouldn't happen otherwise. :) + case 'nowiki': + $output = Xml::escapeTagsOnly( $content ); + break; + case 'math': + $output = $wgContLang->armourMath( + MathRenderer::renderMath( $content, $params ) ); + break; + case 'gallery': + $output = $this->renderImageGallery( $content, $params ); + break; + default: + if( isset( $this->mTagHooks[$tagName] ) ) { + $output = call_user_func_array( $this->mTagHooks[$tagName], + array( $content, $params, $this ) ); + } else { + throw new MWException( "Invalid call hook $element" ); + } + } + wfProfileOut( __METHOD__."-render-$tagName" ); } else { - $pre_content[$marker] = '
        '.$content.'
        '; + // Just stripping tags; keep the source + $output = $tag; } - } - # gallery - $text = Parser::extractTags('gallery', $text, $gallery_content, $uniq_prefix); - foreach( $gallery_content as $marker => $content ) { - require_once( 'ImageGallery.php' ); - if ( $render ) { - $gallery_content[$marker] = Parser::renderImageGallery( $content ); - } else { - $gallery_content[$marker] = ''.$content.''; - } - } + // Unstrip the output, to support recursive strip() calls + $output = $state->unstripBoth( $output ); - # Comments - if($stripcomments) { - $text = Parser::extractTags(STRIP_COMMENTS, $text, $comment_content, $uniq_prefix); - foreach( $comment_content as $marker => $content ){ - $comment_content[$marker] = ''; + if( !$stripcomments && $element == '!--' ) { + $commentState->setPair( $marker, $output ); + } elseif ( $element == 'html' || $element == 'nowiki' ) { + $nowikiItems[$marker] = $output; + } else { + $generalItems[$marker] = $output; } } + # Add the new items to the state + # We do this after the loop instead of during it to avoid slowing + # down the recursive unstrip + $state->nowiki->mergeArray( $nowikiItems ); + $state->general->mergeArray( $generalItems ); - # Extensions - foreach ( $this->mTagHooks as $tag => $callback ) { - $ext_content[$tag] = array(); - $text = Parser::extractTagsAndParams( $tag, $text, $ext_content[$tag], - $ext_tags[$tag], $ext_params[$tag], $uniq_prefix ); - foreach( $ext_content[$tag] as $marker => $content ) { - $full_tag = $ext_tags[$tag][$marker]; - $params = $ext_params[$tag][$marker]; - if ( $render ) { - $ext_content[$tag][$marker] = call_user_func_array( $callback, array( $content, $params, &$this ) );; - } else { - $ext_content[$tag][$marker] = "$full_tag$content"; - } - } + # Unstrip comments unless explicitly told otherwise. + # (The comments are always stripped prior to this point, so as to + # not invoke any extension tags / parser hooks contained within + # a comment.) + if ( !$stripcomments ) { + // Put them all back and forget them + $text = $commentState->replace( $text ); } - # Merge state with the pre-existing state, if there is one - if ( $state ) { - $state['html'] = $state['html'] + $html_content; - $state['nowiki'] = $state['nowiki'] + $nowiki_content; - $state['math'] = $state['math'] + $math_content; - $state['pre'] = $state['pre'] + $pre_content; - $state['comment'] = $state['comment'] + $comment_content; - $state['gallery'] = $state['gallery'] + $gallery_content; - - foreach( $ext_content as $tag => $array ) { - if ( array_key_exists( $tag, $state ) ) { - $state[$tag] = $state[$tag] + $array; - } - } - } else { - $state = array( - 'html' => $html_content, - 'nowiki' => $nowiki_content, - 'math' => $math_content, - 'pre' => $pre_content, - 'comment' => $comment_content, - 'gallery' => $gallery_content, - ) + $ext_content; - } + wfProfileOut( __METHOD__ ); return $text; } /** - * restores pre, math, and hiero removed by strip() + * Restores pre, math, and other extensions removed by strip() * * always call unstripNoWiki() after this one - * @access private + * @private + * @deprecated use $this->mStripState->unstrip() */ - function unstrip( $text, &$state ) { - # Must expand in reverse order, otherwise nested tags will be corrupted - foreach( array_reverse( $state, true ) as $tag => $contentDict ) { - if( $tag != 'nowiki' && $tag != 'html' ) { - foreach( array_reverse( $contentDict, true ) as $uniq => $content ) { - $text = str_replace( $uniq, $content, $text ); - } - } - } - - return $text; + function unstrip( $text, $state ) { + return $state->unstripGeneral( $text ); } /** - * always call this after unstrip() to preserve the order + * Always call this after unstrip() to preserve the order * - * @access private + * @private + * @deprecated use $this->mStripState->unstrip() */ - function unstripNoWiki( $text, &$state ) { - # Must expand in reverse order, otherwise nested tags will be corrupted - for ( $content = end($state['nowiki']); $content !== false; $content = prev( $state['nowiki'] ) ) { - $text = str_replace( key( $state['nowiki'] ), $content, $text ); - } - - global $wgRawHtml; - if ($wgRawHtml) { - for ( $content = end($state['html']); $content !== false; $content = prev( $state['html'] ) ) { - $text = str_replace( key( $state['html'] ), $content, $text ); - } - } + function unstripNoWiki( $text, $state ) { + return $state->unstripNoWiki( $text ); + } - return $text; + /** + * @deprecated use $this->mStripState->unstripBoth() + */ + function unstripForHTML( $text ) { + return $this->mStripState->unstripBoth( $text ); } /** @@ -512,21 +697,11 @@ * Returns the unique tag which must be inserted into the stripped text * The tag will be replaced with the original text in unstrip() * - * @access private + * @private */ function insertStripItem( $text, &$state ) { $rnd = $this->mUniqPrefix . '-item' . Parser::getRandomString(); - if ( !$state ) { - $state = array( - 'html' => array(), - 'nowiki' => array(), - 'math' => array(), - 'pre' => array(), - 'comment' => array(), - 'gallery' => array(), - ); - } - $state['item'][$rnd] = $text; + $state->general->setPair( $rnd, $text ); return $rnd; } @@ -541,7 +716,7 @@ * * @param string $text Hideous HTML input * @return string Corrected HTML output - * @access public + * @public * @static */ function tidy( $text ) { @@ -564,7 +739,7 @@ /** * Spawn an external HTML tidy process and get corrected markup back from it. * - * @access private + * @private * @static */ function externalTidy( $text ) { @@ -578,11 +753,16 @@ $descriptorspec = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), - 2 => array('file', '/dev/null', 'a') + 2 => array('file', wfGetNull(), 'a') ); $pipes = array(); $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes); if (is_resource($process)) { + // Theoretically, this style of communication could cause a deadlock + // here. If the stdout buffer fills up, then writes to stdin could + // block. This doesn't appear to happen with tidy, because tidy only + // writes to stdout after it's finished reading from stdin. Search + // for tidyParseStdin and tidySaveStdout in console/tidy.c fwrite($pipes[0], $text); fclose($pipes[0]); while (!feof($pipes[1])) { @@ -610,7 +790,7 @@ * * 'pear install tidy' should be able to compile the extension module. * - * @access private + * @private * @static */ function internalTidy( $text ) { @@ -636,143 +816,233 @@ /** * parse the wiki syntax used to render tables * - * @access private + * @private */ - function doTableStuff ( $t ) { + function doTableStuff ( $text ) { $fname = 'Parser::doTableStuff'; wfProfileIn( $fname ); - $t = explode ( "\n" , $t ) ; - $td = array () ; # Is currently a td tag open? - $ltd = array () ; # Was it TD or TH? - $tr = array () ; # Is currently a tr tag open? - $ltr = array () ; # tr attributes - $indent_level = 0; # indent level of the table - foreach ( $t AS $k => $x ) + $lines = explode ( "\n" , $text ); + $td_history = array (); // Is currently a td tag open? + $last_tag_history = array (); // Save history of last lag activated (td, th or caption) + $tr_history = array (); // Is currently a tr tag open? + $tr_attributes = array (); // history of tr attributes + $has_opened_tr = array(); // Did this table open a element? + $indent_level = 0; // indent level of the table + foreach ( $lines as $key => $line ) { - $x = trim ( $x ) ; - $fc = substr ( $x , 0 , 1 ) ; - if ( preg_match( '/^(:*)\{\|(.*)$/', $x, $matches ) ) { + $line = trim ( $line ); + + if( $line == '' ) { // empty line, go to next line + continue; + } + $first_character = $line{0}; + $matches = array(); + + if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) { + // First check if we are starting a new table $indent_level = strlen( $matches[1] ); - - $attributes = $this->unstripForHTML( $matches[2] ); - $t[$k] = str_repeat( '
        ', $indent_level ) . - '' ; - array_push ( $td , false ) ; - array_push ( $ltd , '' ) ; - array_push ( $tr , false ) ; - array_push ( $ltr , '' ) ; - } - else if ( count ( $td ) == 0 ) { } # Don't do any of the following - else if ( '|}' == substr ( $x , 0 , 2 ) ) { - $z = "" . substr ( $x , 2); - $l = array_pop ( $ltd ) ; - if ( array_pop ( $tr ) ) $z = '' . $z ; - if ( array_pop ( $td ) ) $z = '' . $z ; - array_pop ( $ltr ) ; - $t[$k] = $z . str_repeat( '
        ', $indent_level ); - } - else if ( '|-' == substr ( $x , 0 , 2 ) ) { # Allows for |--------------- - $x = substr ( $x , 1 ) ; - while ( $x != '' && substr ( $x , 0 , 1 ) == '-' ) $x = substr ( $x , 1 ) ; - $z = '' ; - $l = array_pop ( $ltd ) ; - if ( array_pop ( $tr ) ) $z = '' . $z ; - if ( array_pop ( $td ) ) $z = '' . $z ; - array_pop ( $ltr ) ; - $t[$k] = $z ; - array_push ( $tr , false ) ; - array_push ( $td , false ) ; - array_push ( $ltd , '' ) ; - $attributes = $this->unstripForHTML( $x ); - array_push ( $ltr , Sanitizer::fixTagAttributes ( $attributes, 'tr' ) ) ; - } - else if ( '|' == $fc || '!' == $fc || '|+' == substr ( $x , 0 , 2 ) ) { # Caption - # $x is a table row - if ( '|+' == substr ( $x , 0 , 2 ) ) { - $fc = '+' ; - $x = substr ( $x , 1 ) ; - } - $after = substr ( $x , 1 ) ; - if ( $fc == '!' ) $after = str_replace ( '!!' , '||' , $after ) ; - $after = explode ( '||' , $after ) ; - $t[$k] = '' ; + $attributes = $this->mStripState->unstripBoth( $matches[2] ); + $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' ); + + $lines[$key] = str_repeat( '
        ' , $indent_level ) . ""; + array_push ( $td_history , false ); + array_push ( $last_tag_history , '' ); + array_push ( $tr_history , false ); + array_push ( $tr_attributes , '' ); + array_push ( $has_opened_tr , false ); + } else if ( count ( $td_history ) == 0 ) { + // Don't do any of the following + continue; + } else if ( substr ( $line , 0 , 2 ) == '|}' ) { + // We are ending a table + $line = '' . substr ( $line , 2 ); + $last_tag = array_pop ( $last_tag_history ); + + if ( !array_pop ( $has_opened_tr ) ) { + $line = "{$line}"; + } + + if ( array_pop ( $tr_history ) ) { + $line = "{$line}"; + } + + if ( array_pop ( $td_history ) ) { + $line = "{$line}"; + } + array_pop ( $tr_attributes ); + $lines[$key] = $line . str_repeat( '
        ' , $indent_level ); + } else if ( substr ( $line , 0 , 2 ) == '|-' ) { + // Now we have a table row + $line = preg_replace( '#^\|-+#', '', $line ); + + // Whats after the tag is now only attributes + $attributes = $this->mStripState->unstripBoth( $line ); + $attributes = Sanitizer::fixTagAttributes ( $attributes , 'tr' ); + array_pop ( $tr_attributes ); + array_push ( $tr_attributes , $attributes ); + + $line = ''; + $last_tag = array_pop ( $last_tag_history ); + array_pop ( $has_opened_tr ); + array_push ( $has_opened_tr , true ); + + if ( array_pop ( $tr_history ) ) { + $line = ''; + } + + if ( array_pop ( $td_history ) ) { + $line = "{$line}"; + } + + $lines[$key] = $line; + array_push ( $tr_history , false ); + array_push ( $td_history , false ); + array_push ( $last_tag_history , '' ); + } + else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 ) == '|+' ) { + // This might be cell elements, td, th or captions + if ( substr ( $line , 0 , 2 ) == '|+' ) { + $first_character = '+'; + $line = substr ( $line , 1 ); + } + + $line = substr ( $line , 1 ); - # Loop through each table cell - foreach ( $after AS $theline ) + if ( $first_character == '!' ) { + $line = str_replace ( '!!' , '||' , $line ); + } + + // Split up multiple cells on the same line. + // FIXME : This can result in improper nesting of tags processed + // by earlier parser steps, but should avoid splitting up eg + // attribute values containing literal "||". + $cells = StringUtils::explodeMarkup( '||' , $line ); + + $lines[$key] = ''; + + // Loop through each table cell + foreach ( $cells as $cell ) { - $z = '' ; - if ( $fc != '+' ) + $previous = ''; + if ( $first_character != '+' ) { - $tra = array_pop ( $ltr ) ; - if ( !array_pop ( $tr ) ) $z = '\n" ; - array_push ( $tr , true ) ; - array_push ( $ltr , '' ) ; - } - - $l = array_pop ( $ltd ) ; - if ( array_pop ( $td ) ) $z = '' . $z ; - if ( $fc == '|' ) $l = 'td' ; - else if ( $fc == '!' ) $l = 'th' ; - else if ( $fc == '+' ) $l = 'caption' ; - else $l = '' ; - array_push ( $ltd , $l ) ; - - # Cell parameters - $y = explode ( '|' , $theline , 2 ) ; - # Note that a '|' inside an invalid link should not - # be mistaken as delimiting cell parameters - if ( strpos( $y[0], '[[' ) !== false ) { - $y = array ($theline); + $tr_after = array_pop ( $tr_attributes ); + if ( !array_pop ( $tr_history ) ) { + $previous = "\n"; + } + array_push ( $tr_history , true ); + array_push ( $tr_attributes , '' ); + array_pop ( $has_opened_tr ); + array_push ( $has_opened_tr , true ); + } + + $last_tag = array_pop ( $last_tag_history ); + + if ( array_pop ( $td_history ) ) { + $previous = "{$previous}"; } - if ( count ( $y ) == 1 ) - $y = "{$z}<{$l}>{$y[0]}" ; + + if ( $first_character == '|' ) { + $last_tag = 'td'; + } else if ( $first_character == '!' ) { + $last_tag = 'th'; + } else if ( $first_character == '+' ) { + $last_tag = 'caption'; + } else { + $last_tag = ''; + } + + array_push ( $last_tag_history , $last_tag ); + + // A cell could contain both parameters and data + $cell_data = explode ( '|' , $cell , 2 ); + + // Bug 553: Note that a '|' inside an invalid link should not + // be mistaken as delimiting cell parameters + if ( strpos( $cell_data[0], '[[' ) !== false ) { + $cell = "{$previous}<{$last_tag}>{$cell}"; + } else if ( count ( $cell_data ) == 1 ) + $cell = "{$previous}<{$last_tag}>{$cell_data[0]}"; else { - $attributes = $this->unstripForHTML( $y[0] ); - $y = "{$z}<{$l}".Sanitizer::fixTagAttributes($attributes, $l).">{$y[1]}" ; + $attributes = $this->mStripState->unstripBoth( $cell_data[0] ); + $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag ); + $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}"; } - $t[$k] .= $y ; - array_push ( $td , true ) ; + + $lines[$key] .= $cell; + array_push ( $td_history , true ); } } } - # Closing open td, tr && table - while ( count ( $td ) > 0 ) + // Closing open td, tr && table + while ( count ( $td_history ) > 0 ) { - if ( array_pop ( $td ) ) $t[] = '' ; - if ( array_pop ( $tr ) ) $t[] = '' ; - $t[] = '' ; + if ( array_pop ( $td_history ) ) { + $lines[] = '' ; + } + if ( array_pop ( $tr_history ) ) { + $lines[] = '' ; + } + if ( !array_pop ( $has_opened_tr ) ) { + $lines[] = "" ; + } + + $lines[] = '' ; + } + + $output = implode ( "\n" , $lines ) ; + + // special case: don't return empty table + if( $output == "\n\n
        " ) { + $output = ''; } - $t = implode ( "\n" , $t ) ; wfProfileOut( $fname ); - return $t ; + + return $output; } /** * Helper function for parse() that transforms wiki markup into * HTML. Only called for $mOutputType == OT_HTML. * - * @access private + * @private */ function internalParse( $text ) { - global $wgContLang; $args = array(); $isMain = true; $fname = 'Parser::internalParse'; wfProfileIn( $fname ); + # Hook to suspend the parser in this state + if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) { + wfProfileOut( $fname ); + return $text ; + } + # Remove tags and sections + $text = strtr( $text, array( '' => '' , '' => '' ) ); $text = strtr( $text, array( '' => '', '' => '') ); - $text = preg_replace( '/.*?<\/includeonly>/s', '', $text ); + $text = StringUtils::delimiterReplace( '', '', '', $text ); + + $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), array(), array_keys( $this->mTransparentTagHooks ) ); - $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ) ); $text = $this->replaceVariables( $text, $args ); + wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) ); + + // Tables need to come after variable replacement for things to work + // properly; putting them before other transformations should keep + // exciting things like link expansions from showing up in surprising + // places. + $text = $this->doTableStuff( $text ); $text = preg_replace( '/(^|\n)-----*/', '\\1
        ', $text ); + $text = $this->stripToc( $text ); + $this->stripNoGallery( $text ); $text = $this->doHeadings( $text ); if($this->mOptions->getUseDynamicDates()) { $df =& DateFormatter::getInstance(); @@ -787,58 +1057,83 @@ $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text); $text = $this->doMagicLinks( $text ); - $text = $this->doTableStuff( $text ); $text = $this->formatHeadings( $text, $isMain ); - $regex = '//'; - $text = preg_replace_callback($regex, array(&$this, 'scarySubstitution'), $text); - wfProfileOut( $fname ); return $text; } - function scarySubstitution($matches) { -# return "[[".$matches[0]."]]"; - return $this->mIWTransData[(int)$matches[0]]; - } - /** * Replace special strings like "ISBN xxx" and "RFC xxx" with * magic external links. * - * @access private + * @private */ function &doMagicLinks( &$text ) { - $text = $this->magicISBN( $text ); - $text = $this->magicRFC( $text, 'RFC ', 'rfcurl' ); - $text = $this->magicRFC( $text, 'PMID ', 'pubmedurl' ); + wfProfileIn( __METHOD__ ); + $text = preg_replace_callback( + '!(?: # Start cases + | # Skip link text + <.*?> | # Skip stuff inside HTML elements + (?:RFC|PMID)\s+([0-9]+) | # RFC or PMID, capture number as m[1] + ISBN\s+(\b # ISBN, capture number as m[2] + (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix + (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters + [0-9Xx] # check digit + \b) + )!x', array( &$this, 'magicLinkCallback' ), $text ); + wfProfileOut( __METHOD__ ); return $text; } - /** - * Parse ^^ tokens and return html - * - * @access private - */ - function doExponent( $text ) { - $fname = 'Parser::doExponent'; - wfProfileIn( $fname ); - $text = preg_replace('/\^\^(.*)\^\^/','\\1', $text); - wfProfileOut( $fname ); + function magicLinkCallback( $m ) { + if ( substr( $m[0], 0, 1 ) == '<' ) { + # Skip HTML element + return $m[0]; + } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) { + $isbn = $m[2]; + $num = strtr( $isbn, array( + '-' => '', + ' ' => '', + 'x' => 'X', + )); + $titleObj = SpecialPage::getTitleFor( 'Booksources' ); + $text = 'ISBN $isbn"; + } else { + if ( substr( $m[0], 0, 3 ) == 'RFC' ) { + $keyword = 'RFC'; + $urlmsg = 'rfcurl'; + $id = $m[1]; + } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) { + $keyword = 'PMID'; + $urlmsg = 'pubmedurl'; + $id = $m[1]; + } else { + throw new MWException( __METHOD__.': unrecognised match type "' . + substr($m[0], 0, 20 ) . '"' ); + } + + $url = wfMsg( $urlmsg, $id); + $sk = $this->mOptions->getSkin(); + $la = $sk->getExternalLinkAttributes( $url, $keyword.$id ); + $text = "{$keyword} {$id}"; + } return $text; } /** * Parse headers and return html * - * @access private + * @private */ function doHeadings( $text ) { $fname = 'Parser::doHeadings'; wfProfileIn( $fname ); for ( $i = 6; $i >= 1; --$i ) { - $h = substr( '======', 0, $i ); - $text = preg_replace( "/^{$h}(.+){$h}(\\s|$)/m", + $h = str_repeat( '=', $i ); + $text = preg_replace( "/^{$h}(.+){$h}\\s*$/m", "\\1\\2", $text ); } wfProfileOut( $fname ); @@ -847,7 +1142,7 @@ /** * Replace single quotes with HTML markup - * @access private + * @private * @return string the altered text */ function doAllQuotes( $text ) { @@ -865,7 +1160,7 @@ /** * Helper function for doAllQuotes() - * @access private + * @private */ function doQuotes( $text ) { $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE ); @@ -899,9 +1194,9 @@ } # Count the number of occurrences of bold and italics mark-ups. # We are not counting sequences of five apostrophes. - if ( strlen( $arr[$i] ) == 2 ) $numitalics++; else - if ( strlen( $arr[$i] ) == 3 ) $numbold++; else - if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; } + if ( strlen( $arr[$i] ) == 2 ) { $numitalics++; } + else if ( strlen( $arr[$i] ) == 3 ) { $numbold++; } + else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; } } $i++; } @@ -1022,7 +1317,8 @@ $output .= ''; if ($state == 'bi') $output .= ''; - if ($state == 'both') + # There might be lonely ''''', so make sure we have a buffer + if ($state == 'both' && $buffer) $output .= ''.$buffer.''; return $output; } @@ -1034,14 +1330,14 @@ * Note: this is all very hackish and the order of execution matters a lot. * Make sure to run maintenance/parserTests.php if you change this code. * - * @access private + * @private */ function replaceExternalLinks( $text ) { global $wgContLang; $fname = 'Parser::replaceExternalLinks'; wfProfileIn( $fname ); - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); $bits = preg_split( EXT_LINK_BRACKETED, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); @@ -1057,6 +1353,7 @@ # The characters '<' and '>' (which were escaped by # removeHTMLtags()) should not be included in # URLs, per RFC 2396. + $m2 = array(); if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) { $text = substr($url, $m2[0][1]) . ' ' . $text; $url = substr($url, 0, $m2[0][1]); @@ -1076,8 +1373,8 @@ # No link text, e.g. [http://domain.tld/some.link] if ( $text == '' ) { - # Autonumber if allowed - if ( strpos( HTTP_PROTOCOLS, str_replace('/','\/', $protocol) ) !== false ) { + # Autonumber if allowed. See bug #5918 + if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) { $text = '[' . ++$this->mAutonumber . ']'; $linktype = 'autonumber'; } else { @@ -1093,21 +1390,23 @@ $text = $wgContLang->markNoConversion($text); - # Replace & from obsolete syntax with &. - # All HTML entities will be escaped by makeExternalLink() - # or maybeMakeExternalImage() - $url = str_replace( '&', '&', $url ); + $url = Sanitizer::cleanUrl( $url ); # Process the trail (i.e. everything after this link up until start of the next link), # replacing any non-bracketed links $trail = $this->replaceFreeExternalLinks( $trail ); - # Use the encoded URL # This means that users can paste URLs directly into the text # Funny characters like ö aren't valid in URLs anyway # This was changed in August 2004 - $s .= $sk->makeExternalLink( $url, $text, false, $linktype ) . $dtrail . $trail; + $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail; + + # Register link in the output object. + # Replace unnecessary URL escape codes with the referenced character + # This prevents spammers from hiding links from the filters + $pasteurized = Parser::replaceUnusualEscapes( $url ); + $this->mOutput->addExternalLink( $pasteurized ); } wfProfileOut( $fname ); @@ -1116,32 +1415,46 @@ /** * Replace anything that looks like a URL with a link - * @access private + * @private */ function replaceFreeExternalLinks( $text ) { - global $wgUrlProtocols; global $wgContLang; $fname = 'Parser::replaceFreeExternalLinks'; wfProfileIn( $fname ); - $bits = preg_split( '/(\b(?:'.$wgUrlProtocols.'))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); + $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); $s = array_shift( $bits ); $i = 0; - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); while ( $i < count( $bits ) ){ $protocol = $bits[$i++]; $remainder = $bits[$i++]; + $m = array(); if ( preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) { # Found some characters after the protocol that look promising $url = $protocol . $m[1]; $trail = $m[2]; + # special case: handle urls as url args: + # http://www.example.com/foo?=http://www.example.com/bar + if(strlen($trail) == 0 && + isset($bits[$i]) && + preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) && + preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m )) + { + # add protocol, arg + $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link + $i += 2; + $trail = $m[2]; + } + # The characters '<' and '>' (which were escaped by # removeHTMLtags()) should not be included in # URLs, per RFC 2396. + $m2 = array(); if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) { $trail = substr($url, $m2[0][1]) . $trail; $url = substr($url, 0, $m2[0][1]); @@ -1160,16 +1473,17 @@ $url = substr( $url, 0, -$numSepChars ); } - # Replace & from obsolete syntax with &. - # All HTML entities will be escaped by makeExternalLink() - # or maybeMakeExternalImage() - $url = str_replace( '&', '&', $url ); + $url = Sanitizer::cleanUrl( $url ); # Is this an external image? $text = $this->maybeMakeExternalImage( $url ); if ( $text === false ) { # Not an image, make a link - $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free' ); + $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() ); + # Register it in the output object... + # Replace unnecessary URL escape codes with their equivalent characters + $pasteurized = Parser::replaceUnusualEscapes( $url ); + $this->mOutput->addExternalLink( $pasteurized ); } $s .= $text . $trail; } else { @@ -1181,13 +1495,51 @@ } /** - * make an image if it's allowed - * @access private + * Replace unusual URL escape codes with their equivalent characters + * @param string + * @return string + * @static + * @todo This can merge genuinely required bits in the path or query string, + * breaking legit URLs. A proper fix would treat the various parts of + * the URL differently; as a workaround, just use the output for + * statistical records, not for actual linking/output. + */ + static function replaceUnusualEscapes( $url ) { + return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', + array( 'Parser', 'replaceUnusualEscapesCallback' ), $url ); + } + + /** + * Callback function used in replaceUnusualEscapes(). + * Replaces unusual URL escape codes with their equivalent character + * @static + * @private + */ + private static function replaceUnusualEscapesCallback( $matches ) { + $char = urldecode( $matches[0] ); + $ord = ord( $char ); + // Is it an unsafe or HTTP reserved character according to RFC 1738? + if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) { + // No, shouldn't be escaped + return $char; + } else { + // Yes, leave it escaped + return $matches[0]; + } + } + + /** + * make an image if it's allowed, either through the global + * option or through the exception + * @private */ function maybeMakeExternalImage( $url ) { - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); + $imagesfrom = $this->mOptions->getAllowExternalImagesFrom(); + $imagesexception = !empty($imagesfrom); $text = false; - if ( $this->mOptions->getAllowExternalImages() ) { + if ( $this->mOptions->getAllowExternalImages() + || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) { if ( preg_match( EXT_IMAGE_REGEX, $url ) ) { # Image found $text = $sk->makeExternalImage( htmlspecialchars( $url ) ); @@ -1199,10 +1551,10 @@ /** * Process [[ ]] wikilinks * - * @access private + * @private */ function replaceInternalLinks( $s ) { - global $wgContLang, $wgLinkCache, $wgUrlProtocols; + global $wgContLang; static $fname = 'Parser::replaceInternalLinks' ; wfProfileIn( $fname ); @@ -1212,7 +1564,7 @@ # the % is needed to support urlencoded titles as well if ( !$tc ) { $tc = Title::legalChars() . '#%'; } - $sk =& $this->mOptions->getSkin(); + $sk = $this->mOptions->getSkin(); #split the entire text string on occurences of [[ $a = explode( '[[', ' ' . $s ); @@ -1231,13 +1583,13 @@ $e2 = wfMsgForContent( 'linkprefix' ); $useLinkPrefixExtension = $wgContLang->linkPrefixExtension(); - if( is_null( $this->mTitle ) ) { - wfDebugDieBacktrace( 'nooo' ); + throw new MWException( __METHOD__.": \$this->mTitle is null\n" ); } $nottalk = !$this->mTitle->isTalkPage(); if ( $useLinkPrefixExtension ) { + $m = array(); if ( preg_match( $e2, $s, $m ) ) { $first_prefix = $m[2]; } else { @@ -1247,11 +1599,13 @@ $prefix = ''; } - $selflink = $this->mTitle->getPrefixedText(); - wfProfileOut( $fname.'-setup' ); - - $checkVariantLink = sizeof($wgContLang->getVariants())>1; + if($wgContLang->hasVariants()) { + $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText()); + } else { + $selflink = array($this->mTitle->getPrefixedText()); + } $useSubpages = $this->areSubpagesAllowed(); + wfProfileOut( $fname.'-setup' ); # Loop for each link for ($k = 0; isset( $a[$k] ); $k++) { @@ -1274,6 +1628,7 @@ $might_be_img = false; + wfProfileIn( "$fname-e1" ); if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt $text = $m[2]; # If we get a ] at the beginning of $m[3] that means we have a link that's something like: @@ -1284,32 +1639,39 @@ # Still some problems for cases where the ] is meant to be outside punctuation, # and no image is in sight. See bug 2095. # - if( $text !== '' && preg_match( "/^\](.*)/s", $m[3], $n ) ) { + if( $text !== '' && + substr( $m[3], 0, 1 ) === ']' && + strpos($text, '[') !== false + ) + { $text .= ']'; # so that replaceExternalLinks($text) works later - $m[3] = $n[1]; + $m[3] = substr( $m[3], 1 ); } # fix up urlencoded title texts - //if(preg_match('/%/', $m[1] )) $m[1] = urldecode($m[1]); - if(preg_match('/%/', $m[1] )) + if( strpos( $m[1], '%' ) !== false ) { # Should anchors '#' also be rejected? $m[1] = str_replace( array('<', '>'), array('<', '>'), urldecode($m[1]) ); + } $trail = $m[3]; } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption $might_be_img = true; $text = $m[2]; - if(preg_match('/%/', $m[1] )) - # Should anchors '#' also be rejected? - $m[1] = str_replace( array('<', '>'), array('<', '>'), urldecode($m[1]) ); + if ( strpos( $m[1], '%' ) !== false ) { + $m[1] = urldecode($m[1]); + } $trail = ""; } else { # Invalid form; output directly $s .= $prefix . '[[' . $line ; + wfProfileOut( "$fname-e1" ); continue; } + wfProfileOut( "$fname-e1" ); + wfProfileIn( "$fname-misc" ); # Don't allow internal links to pages containing # PROTO: where PROTO is a valid URL protocol; these # should be external links. - if (preg_match('/^(\b(?:'.$wgUrlProtocols.'))/', $m[1])) { + if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) { $s .= $prefix . '[[' . $line ; continue; } @@ -1327,38 +1689,37 @@ $link = substr($link, 1); } - $nt = Title::newFromText( $this->unstripNoWiki($link, $this->mStripState) ); + wfProfileOut( "$fname-misc" ); + wfProfileIn( "$fname-title" ); + $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) ); if( !$nt ) { $s .= $prefix . '[[' . $line; + wfProfileOut( "$fname-title" ); continue; } - #check other language variants of the link - #if the article does not exist - if( $checkVariantLink - && $nt->getArticleID() == 0 ) { - $wgContLang->findVariantLink($link, $nt); - } - $ns = $nt->getNamespace(); $iw = $nt->getInterWiki(); + wfProfileOut( "$fname-title" ); if ($might_be_img) { # if this is actually an invalid link + wfProfileIn( "$fname-might_be_img" ); if ($ns == NS_IMAGE && $noforce) { #but might be an image $found = false; while (isset ($a[$k+1]) ) { #look at the next 'line' to see if we can close it there $spliced = array_splice( $a, $k + 1, 1 ); $next_line = array_shift( $spliced ); - if( preg_match("/^(.*?]].*?)]](.*)$/sD", $next_line, $m) ) { - # the first ]] closes the inner link, the second the image + $m = explode( ']]', $next_line, 3 ); + if ( count( $m ) == 3 ) { + # the first ]] closes the inner link, the second the image $found = true; - $text .= '[[' . $m[1]; + $text .= "[[{$m[0]}]]{$m[1]}"; $trail = $m[2]; break; - } elseif( preg_match("/^.*?]].*$/sD", $next_line, $m) ) { + } elseif ( count( $m ) == 2 ) { #if there's exactly one ]] that's fine, we'll keep looking - $text .= '[[' . $m[0]; + $text .= "[[{$m[0]}]]{$m[1]}"; } else { #if $next_line is invalid too, we need look no further $text .= '[[' . $next_line; @@ -1369,35 +1730,40 @@ # we couldn't find the end of this imageLink, so output it raw #but don't ignore what might be perfectly normal links in the text we've examined $text = $this->replaceInternalLinks($text); - $s .= $prefix . '[[' . $link . '|' . $text; + $s .= "{$prefix}[[$link|$text"; # note: no $trail, because without an end, there *is* no trail + wfProfileOut( "$fname-might_be_img" ); continue; } } else { #it's not an image, so output it raw - $s .= $prefix . '[[' . $link . '|' . $text; + $s .= "{$prefix}[[$link|$text"; # note: no $trail, because without an end, there *is* no trail + wfProfileOut( "$fname-might_be_img" ); continue; } + wfProfileOut( "$fname-might_be_img" ); } $wasblank = ( '' == $text ); if( $wasblank ) $text = $link; - # Link not escaped by : , create the various objects if( $noforce ) { # Interwikis + wfProfileIn( "$fname-interwiki" ); if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) { - array_push( $this->mOutput->mLanguageLinks, $nt->getFullText() ); - $s = rtrim($s . "\n"); - $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail; + $this->mOutput->addLanguageLink( $nt->getFullText() ); + $s = rtrim($s . $prefix); + $s .= trim($trail, "\n") == '' ? '': $prefix . $trail; + wfProfileOut( "$fname-interwiki" ); continue; } + wfProfileOut( "$fname-interwiki" ); if ( $ns == NS_IMAGE ) { wfProfileIn( "$fname-image" ); - if ( !wfIsBadImage( $nt->getDBkey() ) ) { + if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) { # recursively parse links inside the image caption # actually, this will parse them in any other parameters, too, # but it might be hard to fix that, and it doesn't matter ATM @@ -1405,11 +1771,14 @@ $text = $this->replaceInternalLinks($text); # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them - $s .= $prefix . preg_replace( "/\b($wgUrlProtocols)/", "{$this->mUniqPrefix}NOPARSE$1", $this->makeImage( $nt, $text) ) . $trail; - $wgLinkCache->addImageLinkObj( $nt ); + $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail; + $this->mOutput->addImage( $nt->getDBkey() ); wfProfileOut( "$fname-image" ); continue; + } else { + # We still need to record the image's presence on the page + $this->mOutput->addImage( $nt->getDBkey() ); } wfProfileOut( "$fname-image" ); @@ -1417,25 +1786,17 @@ if ( $ns == NS_CATEGORY ) { wfProfileIn( "$fname-category" ); - $t = $wgContLang->convertHtml( $nt->getText() ); $s = rtrim($s . "\n"); # bug 87 - $wgLinkCache->suspend(); # Don't save in links/brokenlinks - $t = $sk->makeLinkObj( $nt, $t, '', '' , $prefix ); - $wgLinkCache->resume(); - if ( $wasblank ) { - if ( $this->mTitle->getNamespace() == NS_CATEGORY ) { - $sortkey = $this->mTitle->getText(); - } else { - $sortkey = $this->mTitle->getPrefixedText(); - } + $sortkey = $this->getDefaultSort(); } else { $sortkey = $text; } + $sortkey = Sanitizer::decodeCharReferences( $sortkey ); + $sortkey = str_replace( "\n", '', $sortkey ); $sortkey = $wgContLang->convertCategoryKey( $sortkey ); - $wgLinkCache->addCategoryLinkObj( $nt, $sortkey ); - $this->mOutput->addCategoryLink( $t ); + $this->mOutput->addCategory( $nt->getDBkey(), $sortkey ); /** * Strip the whitespace Category links produce, see bug 87 @@ -1448,40 +1809,40 @@ } } - if( ( $nt->getPrefixedText() === $selflink ) && - ( $nt->getFragment() === '' ) ) { - # Self-links are handled specially; generally de-link and change to bold. - $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail ); - continue; + # Self-link checking + if( $nt->getFragment() === '' ) { + if( in_array( $nt->getPrefixedText(), $selflink, true ) ) { + $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail ); + continue; + } } # Special and Media are pseudo-namespaces; no pages actually exist in them if( $ns == NS_MEDIA ) { $link = $sk->makeMediaLinkObj( $nt, $text ); # Cloak with NOPARSE to avoid replacement in replaceExternalLinks - $s .= $prefix . str_replace( 'http://', "http{$this->mUniqPrefix}NOPARSE://", $link ) . $trail; - $wgLinkCache->addImageLinkObj( $nt ); + $s .= $prefix . $this->armorLinks( $link ) . $trail; + $this->mOutput->addImage( $nt->getDBkey() ); continue; } elseif( $ns == NS_SPECIAL ) { - $s .= $prefix . $sk->makeKnownLinkObj( $nt, $text, '', $trail ); + if( SpecialPage::exists( $nt->getDBkey() ) ) { + $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); + } else { + $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix ); + } continue; + } elseif( $ns == NS_IMAGE ) { + $img = wfFindFile( $nt ); + if( $img ) { + // Force a blue link if the file exists; may be a remote + // upload on the shared repository, and we want to see its + // auto-generated page. + $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); + $this->mOutput->addLink( $nt ); + continue; + } } - if( !$nt->isExternal() && $nt->isAlwaysKnown() ) { - /** - * Skip lookups for special pages and self-links. - * External interwiki links are not included here because - * the HTTP urls would break output in the next parse step; - * they will have placeholders kept. - */ - $s .= $sk->makeKnownLinkObj( $nt, $text, '', $trail, $prefix ); - } else { - /** - * Add a link placeholder - * Later, this will be replaced by a real link, after the existence or - * non-existence of all the links is known - */ - $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix ); - } + $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix ); } wfProfileOut( $fname ); return $s; @@ -1490,11 +1851,12 @@ /** * Make a link placeholder. The text returned can be later resolved to a real link with * replaceLinkHolders(). This is done for two reasons: firstly to avoid further - * parsing of interwiki links, and secondly to allow all extistence checks and + * parsing of interwiki links, and secondly to allow all existence checks and * article length checks (for stub links) to be bundled into a single query. * */ function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + wfProfileIn( __METHOD__ ); if ( ! is_object($nt) ) { # Fail gracefully $retVal = "{$prefix}{$text}{$trail}"; @@ -1516,10 +1878,49 @@ $retVal = '{$trail}"; } } + wfProfileOut( __METHOD__ ); return $retVal; } /** + * Render a forced-blue link inline; protect against double expansion of + * URLs if we're in a mode that prepends full URL prefixes to internal links. + * Since this little disaster has to split off the trail text to avoid + * breaking URLs in the following text without breaking trails on the + * wiki links, it's been made into a horrible function. + * + * @param Title $nt + * @param string $text + * @param string $query + * @param string $trail + * @param string $prefix + * @return string HTML-wikitext mix oh yuck + */ + function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { + list( $inside, $trail ) = Linker::splitTrail( $trail ); + $sk = $this->mOptions->getSkin(); + $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix ); + return $this->armorLinks( $link ) . $trail; + } + + /** + * Insert a NOPARSE hacky thing into any inline links in a chunk that's + * going to go through further parsing steps before inline URL expansion. + * + * In particular this is important when using action=render, which causes + * full URLs to be included. + * + * Oh man I hate our multi-layer parser! + * + * @param string more-or-less HTML + * @return string less-or-more HTML with NOPARSE bits + */ + function armorLinks( $text ) { + return preg_replace( '/\b(' . wfUrlProtocols() . ')/', + "{$this->mUniqPrefix}NOPARSE$1", $text ); + } + + /** * Return true if subpage links should be expanded on this page. * @return bool */ @@ -1534,7 +1935,7 @@ * @param string $target the source of the link * @param string &$text the link text, modified as necessary * @return string the full name of the link - * @access private + * @private */ function maybeDoSubpageLink($target, &$text) { # Valid link forms: @@ -1552,19 +1953,29 @@ # Some namespaces don't allow subpages, # so only perform processing if subpages are allowed if( $this->areSubpagesAllowed() ) { + $hash = strpos( $target, '#' ); + if( $hash !== false ) { + $suffix = substr( $target, $hash ); + $target = substr( $target, 0, $hash ); + } else { + $suffix = ''; + } + # bug 7425 + $target = trim( $target ); # Look at the first character if( $target != '' && $target{0} == '/' ) { # / at end means we don't want the slash to be shown - if( substr( $target, -1, 1 ) == '/' ) { - $target = substr( $target, 1, -1 ); - $noslash = $target; + $m = array(); + $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m ); + if( $trailingSlashes ) { + $noslash = $target = substr( $target, 1, -strlen($m[0][0]) ); } else { $noslash = substr( $target, 1 ); } - $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash); + $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix; if( '' === $text ) { - $text = $target; + $text = $target . $suffix; } # this might be changed for ugliness reasons } else { # check for .. subpage backlinks @@ -1582,13 +1993,14 @@ if( substr( $nodotdot, -1, 1 ) == '/' ) { $nodotdot = substr( $nodotdot, 0, -1 ); if( '' === $text ) { - $text = $nodotdot; + $text = $nodotdot . $suffix; } } $nodotdot = trim( $nodotdot ); if( $nodotdot != '' ) { $ret .= '/' . $nodotdot; } + $ret .= $suffix; } } } @@ -1600,7 +2012,7 @@ /**#@+ * Used by doBlockLevels() - * @access private + * @private */ /* private */ function closeParagraph() { $result = ''; @@ -1677,7 +2089,7 @@ /** * Make lists from lines starting with ':', '*', '#', etc. * - * @access private + * @private * @return string the lists rendered as HTML */ function doBlockLevels( $text, $linestart ) { @@ -1766,12 +2178,13 @@ wfProfileIn( "$fname-paragraph" ); # No prefix (not in list)--go to paragraph mode // XXX: use a stack for nestable elements like span, table and div - $openmatch = preg_match('/(mUniqPrefix.'-pre|<\\/li|<\\/ul)/iS', $t ); + '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'. + 'mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t ); if ( $openmatch or $closematch ) { $paragraphStack = false; + # TODO bug 5718: paragraph closed $output .= $this->closeParagraph(); if ( $preOpenMatch and !$preCloseMatch ) { $this->mInPre = true; @@ -1842,118 +2255,378 @@ } /** - * Split up a string on ':', ignoring any occurences inside - * .. or ... + * Split up a string on ':', ignoring any occurences inside tags + * to prevent illegal overlapping. * @param string $str the string to split * @param string &$before set to everything before the ':' * @param string &$after set to everything after the ':' * return string the position of the ':', or false if none found */ function findColonNoLinks($str, &$before, &$after) { - # I wonder if we should make this count all tags, not just - # and . That would prevent us from matching a ':' that - # comes in the middle of italics other such formatting.... - # -- Wil $fname = 'Parser::findColonNoLinks'; wfProfileIn( $fname ); - $pos = 0; - do { - $colon = strpos($str, ':', $pos); - - if ($colon !== false) { - $before = substr($str, 0, $colon); - $after = substr($str, $colon + 1); - - # Skip any ':' within or pairs - $a = substr_count($before, ''); - $cs = substr_count($before, ''); - if ($a <= $ca and $s <= $cs) { - # Tags are balanced before ':'; ok + $pos = strpos( $str, ':' ); + if( $pos === false ) { + // Nothing to find! + wfProfileOut( $fname ); + return false; + } + + $lt = strpos( $str, '<' ); + if( $lt === false || $lt > $pos ) { + // Easy; no tag nesting to worry about + $before = substr( $str, 0, $pos ); + $after = substr( $str, $pos+1 ); + wfProfileOut( $fname ); + return $pos; + } + + // Ugly state machine to walk through avoiding tags. + $state = MW_COLON_STATE_TEXT; + $stack = 0; + $len = strlen( $str ); + for( $i = 0; $i < $len; $i++ ) { + $c = $str{$i}; + + switch( $state ) { + // (Using the number is a performance hack for common cases) + case 0: // MW_COLON_STATE_TEXT: + switch( $c ) { + case "<": + // Could be either a tag or an tag + $state = MW_COLON_STATE_TAGSTART; + break; + case ":": + if( $stack == 0 ) { + // We found it! + $before = substr( $str, 0, $i ); + $after = substr( $str, $i + 1 ); + wfProfileOut( $fname ); + return $i; + } + // Embedded in a tag; don't break it. + break; + default: + // Skip ahead looking for something interesting + $colon = strpos( $str, ':', $i ); + if( $colon === false ) { + // Nothing else interesting + wfProfileOut( $fname ); + return false; + } + $lt = strpos( $str, '<', $i ); + if( $stack === 0 ) { + if( $lt === false || $colon < $lt ) { + // We found it! + $before = substr( $str, 0, $colon ); + $after = substr( $str, $colon + 1 ); + wfProfileOut( $fname ); + return $i; + } + } + if( $lt === false ) { + // Nothing else interesting to find; abort! + // We're nested, but there's no close tags left. Abort! + break 2; + } + // Skip ahead to next tag start + $i = $lt; + $state = MW_COLON_STATE_TAGSTART; + } + break; + case 1: // MW_COLON_STATE_TAG: + // In a + switch( $c ) { + case ">": + $stack++; + $state = MW_COLON_STATE_TEXT; + break; + case "/": + // Slash may be followed by >? + $state = MW_COLON_STATE_TAGSLASH; + break; + default: + // ignore + } + break; + case 2: // MW_COLON_STATE_TAGSTART: + switch( $c ) { + case "/": + $state = MW_COLON_STATE_CLOSETAG; + break; + case "!": + $state = MW_COLON_STATE_COMMENT; + break; + case ">": + // Illegal early close? This shouldn't happen D: + $state = MW_COLON_STATE_TEXT; break; + default: + $state = MW_COLON_STATE_TAG; + } + break; + case 3: // MW_COLON_STATE_CLOSETAG: + // In a + if( $c == ">" ) { + $stack--; + if( $stack < 0 ) { + wfDebug( "Invalid input in $fname; too many close tags\n" ); + wfProfileOut( $fname ); + return false; + } + $state = MW_COLON_STATE_TEXT; + } + break; + case MW_COLON_STATE_TAGSLASH: + if( $c == ">" ) { + // Yes, a self-closed tag + $state = MW_COLON_STATE_TEXT; + } else { + // Probably we're jumping the gun, and this is an attribute + $state = MW_COLON_STATE_TAG; + } + break; + case 5: // MW_COLON_STATE_COMMENT: + if( $c == "-" ) { + $state = MW_COLON_STATE_COMMENTDASH; } - $pos = $colon + 1; + break; + case MW_COLON_STATE_COMMENTDASH: + if( $c == "-" ) { + $state = MW_COLON_STATE_COMMENTDASHDASH; + } else { + $state = MW_COLON_STATE_COMMENT; + } + break; + case MW_COLON_STATE_COMMENTDASHDASH: + if( $c == ">" ) { + $state = MW_COLON_STATE_TEXT; + } else { + $state = MW_COLON_STATE_COMMENT; + } + break; + default: + throw new MWException( "State machine error in $fname" ); } - } while ($colon !== false); + } + if( $stack > 0 ) { + wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" ); + return false; + } wfProfileOut( $fname ); - return $colon; + return false; } /** * Return value of a magic variable (like PAGENAME) * - * @access private + * @private */ function getVariableValue( $index ) { - global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgArticle, $wgScriptPath; + global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath; /** * Some of these require message or data lookups and can be * expensive to check many times. */ static $varCache = array(); - if( isset( $varCache[$index] ) ) return $varCache[$index]; + if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$varCache ) ) ) { + if ( isset( $varCache[$index] ) ) { + return $varCache[$index]; + } + } + + $ts = time(); + wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) ); + + # Use the time zone + global $wgLocaltimezone; + if ( isset( $wgLocaltimezone ) ) { + $oldtz = getenv( 'TZ' ); + putenv( 'TZ='.$wgLocaltimezone ); + } + + wfSuppressWarnings(); // E_STRICT system time bitching + $localTimestamp = date( 'YmdHis', $ts ); + $localMonth = date( 'm', $ts ); + $localMonthName = date( 'n', $ts ); + $localDay = date( 'j', $ts ); + $localDay2 = date( 'd', $ts ); + $localDayOfWeek = date( 'w', $ts ); + $localWeek = date( 'W', $ts ); + $localYear = date( 'Y', $ts ); + $localHour = date( 'H', $ts ); + if ( isset( $wgLocaltimezone ) ) { + putenv( 'TZ='.$oldtz ); + } + wfRestoreWarnings(); switch ( $index ) { - case MAG_CURRENTMONTH: - return $varCache[$index] = $wgContLang->formatNum( date( 'm' ) ); - case MAG_CURRENTMONTHNAME: - return $varCache[$index] = $wgContLang->getMonthName( date('n') ); - case MAG_CURRENTMONTHNAMEGEN: - return $varCache[$index] = $wgContLang->getMonthNameGen( date('n') ); - case MAG_CURRENTMONTHABBREV: - return $varCache[$index] = $wgContLang->getMonthAbbreviation( date('n') ); - case MAG_CURRENTDAY: - return $varCache[$index] = $wgContLang->formatNum( date('j') ); - case MAG_PAGENAME: - return $this->mTitle->getText(); - case MAG_PAGENAMEE: + case 'currentmonth': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) ); + case 'currentmonthname': + return $varCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) ); + case 'currentmonthnamegen': + return $varCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) ); + case 'currentmonthabbrev': + return $varCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) ); + case 'currentday': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) ); + case 'currentday2': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) ); + case 'localmonth': + return $varCache[$index] = $wgContLang->formatNum( $localMonth ); + case 'localmonthname': + return $varCache[$index] = $wgContLang->getMonthName( $localMonthName ); + case 'localmonthnamegen': + return $varCache[$index] = $wgContLang->getMonthNameGen( $localMonthName ); + case 'localmonthabbrev': + return $varCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName ); + case 'localday': + return $varCache[$index] = $wgContLang->formatNum( $localDay ); + case 'localday2': + return $varCache[$index] = $wgContLang->formatNum( $localDay2 ); + case 'pagename': + return wfEscapeWikiText( $this->mTitle->getText() ); + case 'pagenamee': return $this->mTitle->getPartialURL(); - case MAG_REVISIONID: - return $wgArticle->getRevIdFetched(); - case MAG_NAMESPACE: - # return Namespace::getCanonicalName($this->mTitle->getNamespace()); - return $wgContLang->getNsText($this->mTitle->getNamespace()); # Patch by Dori - case MAG_CURRENTDAYNAME: - return $varCache[$index] = $wgContLang->getWeekdayName( date('w')+1 ); - case MAG_CURRENTYEAR: - return $varCache[$index] = $wgContLang->formatNum( date( 'Y' ), true ); - case MAG_CURRENTTIME: - return $varCache[$index] = $wgContLang->time( wfTimestampNow(), false ); - case MAG_CURRENTWEEK: - return $varCache[$index] = $wgContLang->formatNum( intval( date('W') ) ); - case MAG_CURRENTDOW: - return $varCache[$index] = $wgContLang->formatNum( date('w') ); - case MAG_NUMBEROFARTICLES: - return $varCache[$index] = $wgContLang->formatNum( wfNumberOfArticles() ); - case MAG_NUMBEROFFILES: - return $varCache[$index] = $wgContLang->formatNum( wfNumberOfFiles() ); - case MAG_SITENAME: + case 'fullpagename': + return wfEscapeWikiText( $this->mTitle->getPrefixedText() ); + case 'fullpagenamee': + return $this->mTitle->getPrefixedURL(); + case 'subpagename': + return wfEscapeWikiText( $this->mTitle->getSubpageText() ); + case 'subpagenamee': + return $this->mTitle->getSubpageUrlForm(); + case 'basepagename': + return wfEscapeWikiText( $this->mTitle->getBaseText() ); + case 'basepagenamee': + return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) ); + case 'talkpagename': + if( $this->mTitle->canTalk() ) { + $talkPage = $this->mTitle->getTalkPage(); + return wfEscapeWikiText( $talkPage->getPrefixedText() ); + } else { + return ''; + } + case 'talkpagenamee': + if( $this->mTitle->canTalk() ) { + $talkPage = $this->mTitle->getTalkPage(); + return $talkPage->getPrefixedUrl(); + } else { + return ''; + } + case 'subjectpagename': + $subjPage = $this->mTitle->getSubjectPage(); + return wfEscapeWikiText( $subjPage->getPrefixedText() ); + case 'subjectpagenamee': + $subjPage = $this->mTitle->getSubjectPage(); + return $subjPage->getPrefixedUrl(); + case 'revisionid': + return $this->mRevisionId; + case 'revisionday': + return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) ); + case 'revisionday2': + return substr( $this->getRevisionTimestamp(), 6, 2 ); + case 'revisionmonth': + return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) ); + case 'revisionyear': + return substr( $this->getRevisionTimestamp(), 0, 4 ); + case 'revisiontimestamp': + return $this->getRevisionTimestamp(); + case 'namespace': + return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) ); + case 'namespacee': + return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) ); + case 'talkspace': + return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : ''; + case 'talkspacee': + return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : ''; + case 'subjectspace': + return $this->mTitle->getSubjectNsText(); + case 'subjectspacee': + return( wfUrlencode( $this->mTitle->getSubjectNsText() ) ); + case 'currentdayname': + return $varCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 ); + case 'currentyear': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true ); + case 'currenttime': + return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false ); + case 'currenthour': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true ); + case 'currentweek': + // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to + // int to remove the padding + return $varCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) ); + case 'currentdow': + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) ); + case 'localdayname': + return $varCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 ); + case 'localyear': + return $varCache[$index] = $wgContLang->formatNum( $localYear, true ); + case 'localtime': + return $varCache[$index] = $wgContLang->time( $localTimestamp, false, false ); + case 'localhour': + return $varCache[$index] = $wgContLang->formatNum( $localHour, true ); + case 'localweek': + // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to + // int to remove the padding + return $varCache[$index] = $wgContLang->formatNum( (int)$localWeek ); + case 'localdow': + return $varCache[$index] = $wgContLang->formatNum( $localDayOfWeek ); + case 'numberofarticles': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::articles() ); + case 'numberoffiles': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::images() ); + case 'numberofusers': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::users() ); + case 'numberofpages': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::pages() ); + case 'numberofadmins': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::admins() ); + case 'numberofedits': + return $varCache[$index] = $wgContLang->formatNum( SiteStats::edits() ); + case 'currenttimestamp': + return $varCache[$index] = wfTimestampNow(); + case 'localtimestamp': + return $varCache[$index] = $localTimestamp; + case 'currentversion': + return $varCache[$index] = SpecialVersion::getVersion(); + case 'sitename': return $wgSitename; - case MAG_SERVER: + case 'server': return $wgServer; - case MAG_SERVERNAME: + case 'servername': return $wgServerName; - case MAG_SCRIPTPATH: + case 'scriptpath': return $wgScriptPath; + case 'directionmark': + return $wgContLang->getDirMark(); + case 'contentlanguage': + global $wgContLanguageCode; + return $wgContLanguageCode; default: - return NULL; + $ret = null; + if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$varCache, &$index, &$ret ) ) ) + return $ret; + else + return null; } } /** * initialise the magic variables (like CURRENTMONTHNAME) * - * @access private + * @private */ function initialiseVariables() { $fname = 'Parser::initialiseVariables'; wfProfileIn( $fname ); - global $wgVariableIDs; + $variableIDs = MagicWord::getVariableIDs(); + $this->mVariables = array(); - foreach ( $wgVariableIDs as $id ) { + foreach ( $variableIDs as $id ) { $mw =& MagicWord::get( $id ); $mw->addToArray( $this->mVariables, $id ); } @@ -1961,6 +2634,181 @@ } /** + * parse any parentheses in format ((title|part|part)) + * and call callbacks to get a replacement text for any found piece + * + * @param string $text The text to parse + * @param array $callbacks rules in form: + * '{' => array( # opening parentheses + * 'end' => '}', # closing parentheses + * 'cb' => array(2 => callback, # replacement callback to call if {{..}} is found + * 3 => callback # replacement callback to call if {{{..}}} is found + * ) + * ) + * 'min' => 2, # Minimum parenthesis count in cb + * 'max' => 3, # Maximum parenthesis count in cb + * @private + */ + function replace_callback ($text, $callbacks) { + wfProfileIn( __METHOD__ ); + $openingBraceStack = array(); # this array will hold a stack of parentheses which are not closed yet + $lastOpeningBrace = -1; # last not closed parentheses + + $validOpeningBraces = implode( '', array_keys( $callbacks ) ); + + $i = 0; + while ( $i < strlen( $text ) ) { + # Find next opening brace, closing brace or pipe + if ( $lastOpeningBrace == -1 ) { + $currentClosing = ''; + $search = $validOpeningBraces; + } else { + $currentClosing = $openingBraceStack[$lastOpeningBrace]['braceEnd']; + $search = $validOpeningBraces . '|' . $currentClosing; + } + $rule = null; + $i += strcspn( $text, $search, $i ); + if ( $i < strlen( $text ) ) { + if ( $text[$i] == '|' ) { + $found = 'pipe'; + } elseif ( $text[$i] == $currentClosing ) { + $found = 'close'; + } elseif ( isset( $callbacks[$text[$i]] ) ) { + $found = 'open'; + $rule = $callbacks[$text[$i]]; + } else { + # Some versions of PHP have a strcspn which stops on null characters + # Ignore and continue + ++$i; + continue; + } + } else { + # All done + break; + } + + if ( $found == 'open' ) { + # found opening brace, let's add it to parentheses stack + $piece = array('brace' => $text[$i], + 'braceEnd' => $rule['end'], + 'title' => '', + 'parts' => null); + + # count opening brace characters + $piece['count'] = strspn( $text, $piece['brace'], $i ); + $piece['startAt'] = $piece['partStart'] = $i + $piece['count']; + $i += $piece['count']; + + # we need to add to stack only if opening brace count is enough for one of the rules + if ( $piece['count'] >= $rule['min'] ) { + $lastOpeningBrace ++; + $openingBraceStack[$lastOpeningBrace] = $piece; + } + } elseif ( $found == 'close' ) { + # lets check if it is enough characters for closing brace + $maxCount = $openingBraceStack[$lastOpeningBrace]['count']; + $count = strspn( $text, $text[$i], $i, $maxCount ); + + # check for maximum matching characters (if there are 5 closing + # characters, we will probably need only 3 - depending on the rules) + $matchingCount = 0; + $matchingCallback = null; + $cbType = $callbacks[$openingBraceStack[$lastOpeningBrace]['brace']]; + if ( $count > $cbType['max'] ) { + # The specified maximum exists in the callback array, unless the caller + # has made an error + $matchingCount = $cbType['max']; + } else { + # Count is less than the maximum + # Skip any gaps in the callback array to find the true largest match + # Need to use array_key_exists not isset because the callback can be null + $matchingCount = $count; + while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $cbType['cb'] ) ) { + --$matchingCount; + } + } + + if ($matchingCount <= 0) { + $i += $count; + continue; + } + $matchingCallback = $cbType['cb'][$matchingCount]; + + # let's set a title or last part (if '|' was found) + if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { + $openingBraceStack[$lastOpeningBrace]['title'] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); + } else { + $openingBraceStack[$lastOpeningBrace]['parts'][] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); + } + + $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount; + $pieceEnd = $i + $matchingCount; + + if( is_callable( $matchingCallback ) ) { + $cbArgs = array ( + 'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart), + 'title' => trim($openingBraceStack[$lastOpeningBrace]['title']), + 'parts' => $openingBraceStack[$lastOpeningBrace]['parts'], + 'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == "\n")), + ); + # finally we can call a user callback and replace piece of text + $replaceWith = call_user_func( $matchingCallback, $cbArgs ); + $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd); + $i = $pieceStart + strlen($replaceWith); + } else { + # null value for callback means that parentheses should be parsed, but not replaced + $i += $matchingCount; + } + + # reset last opening parentheses, but keep it in case there are unused characters + $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'], + 'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'], + 'count' => $openingBraceStack[$lastOpeningBrace]['count'], + 'title' => '', + 'parts' => null, + 'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']); + $openingBraceStack[$lastOpeningBrace--] = null; + + if ($matchingCount < $piece['count']) { + $piece['count'] -= $matchingCount; + $piece['startAt'] -= $matchingCount; + $piece['partStart'] = $piece['startAt']; + # do we still qualify for any callback with remaining count? + $currentCbList = $callbacks[$piece['brace']]['cb']; + while ( $piece['count'] ) { + if ( array_key_exists( $piece['count'], $currentCbList ) ) { + $lastOpeningBrace++; + $openingBraceStack[$lastOpeningBrace] = $piece; + break; + } + --$piece['count']; + } + } + } elseif ( $found == 'pipe' ) { + # lets set a title if it is a first separator, or next part otherwise + if (null === $openingBraceStack[$lastOpeningBrace]['parts']) { + $openingBraceStack[$lastOpeningBrace]['title'] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); + $openingBraceStack[$lastOpeningBrace]['parts'] = array(); + } else { + $openingBraceStack[$lastOpeningBrace]['parts'][] = + substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], + $i - $openingBraceStack[$lastOpeningBrace]['partStart']); + } + $openingBraceStack[$lastOpeningBrace]['partStart'] = ++$i; + } + } + + wfProfileOut( __METHOD__ ); + return $text; + } + + /** * Replace magic variables, templates, and template arguments * with the appropriate text. Templates are substituted recursively, * taking care to avoid infinite loops. @@ -1972,55 +2820,64 @@ * * @param string $tex The text to transform * @param array $args Key-value pairs representing template parameters to substitute - * @access private + * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion + * @private */ - function replaceVariables( $text, $args = array() ) { - + function replaceVariables( $text, $args = array(), $argsOnly = false ) { # Prevent too big inclusions - if( strlen( $text ) > MAX_INCLUDE_SIZE ) { + if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) { return $text; } - $fname = 'Parser::replaceVariables'; + $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; wfProfileIn( $fname ); - $titleChars = Title::legalChars(); - # This function is called recursively. To keep track of arguments we need a stack: array_push( $this->mArgStack, $args ); - # Variable substitution - $text = preg_replace_callback( "/{{([$titleChars]*?)}}/", array( &$this, 'variableSubstitution' ), $text ); - - if ( $this->mOutputType == OT_HTML || $this->mOutputType == OT_WIKI ) { - # Argument substitution - $text = preg_replace_callback( "/{{{([$titleChars]*?)}}}/", array( &$this, 'argSubstitution' ), $text ); - } - # Template substitution - $regex = '/(\\n|{)?{{(['.$titleChars.']*)(\\|.*?|)}}/s'; - $text = preg_replace_callback( $regex, array( &$this, 'braceSubstitution' ), $text ); - - array_pop( $this->mArgStack ); + $braceCallbacks = array(); + if ( !$argsOnly ) { + $braceCallbacks[2] = array( &$this, 'braceSubstitution' ); + } + if ( $this->mOutputType != OT_MSG ) { + $braceCallbacks[3] = array( &$this, 'argSubstitution' ); + } + if ( $braceCallbacks ) { + $callbacks = array( + '{' => array( + 'end' => '}', + 'cb' => $braceCallbacks, + 'min' => $argsOnly ? 3 : 2, + 'max' => isset( $braceCallbacks[3] ) ? 3 : 2, + ), + '[' => array( + 'end' => ']', + 'cb' => array(2=>null), + 'min' => 2, + 'max' => 2, + ) + ); + $text = $this->replace_callback ($text, $callbacks); + array_pop( $this->mArgStack ); + } wfProfileOut( $fname ); return $text; } /** * Replace magic variables - * @access private + * @private */ function variableSubstitution( $matches ) { - $fname = 'parser::variableSubstitution'; - $varname = $matches[1]; + global $wgContLang; + $fname = 'Parser::variableSubstitution'; + $varname = $wgContLang->lc($matches[1]); wfProfileIn( $fname ); - if ( !$this->mVariables ) { - $this->initialiseVariables(); - } $skip = false; if ( $this->mOutputType == OT_WIKI ) { # Do only magic variables prefixed by SUBST - $mwSubst =& MagicWord::get( MAG_SUBST ); + $mwSubst =& MagicWord::get( 'subst' ); if (!$mwSubst->matchStartAndRemove( $varname )) $skip = true; # Note that if we don't substitute the variable below, @@ -2029,8 +2886,14 @@ } if ( !$skip && array_key_exists( $varname, $this->mVariables ) ) { $id = $this->mVariables[$varname]; - $text = $this->getVariableValue( $id ); - $this->mOutput->mContainsOldMagic = true; + # Now check if we did really match, case sensitive or not + $mw =& MagicWord::get( $id ); + if ($mw->match($matches[1])) { + $text = $this->getVariableValue( $id ); + $this->mOutput->mContainsOldMagic = true; + } else { + $text = $matches[0]; + } } else { $text = $matches[0]; } @@ -2038,295 +2901,351 @@ return $text; } - # Split template arguments - function getTemplateArgs( $argsString ) { - if ( $argsString === '' ) { - return array(); - } - - $args = explode( '|', substr( $argsString, 1 ) ); - - # If any of the arguments contains a '[[' but no ']]', it needs to be - # merged with the next arg because the '|' character between belongs - # to the link syntax and not the template parameter syntax. - $argc = count($args); - for ( $i = 0; $i < $argc-1; $i++ ) { - if ( substr_count ( $args[$i], '[[' ) != substr_count ( $args[$i], ']]' ) ) { - $args[$i] .= '|'.$args[$i+1]; - array_splice($args, $i+1, 1); - $i--; - $argc--; + /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too. + static function createAssocArgs( $args ) { + $assocArgs = array(); + $index = 1; + foreach( $args as $arg ) { + $eqpos = strpos( $arg, '=' ); + if ( $eqpos === false ) { + $assocArgs[$index++] = $arg; + } else { + $name = trim( substr( $arg, 0, $eqpos ) ); + $value = trim( substr( $arg, $eqpos+1 ) ); + if ( $value === false ) { + $value = ''; + } + if ( $name !== false ) { + $assocArgs[$name] = $value; + } } } - return $args; + return $assocArgs; } /** * Return the text of a template, after recursively * replacing any variables or templates within the template. * - * @param array $matches The parts of the template - * $matches[1]: the title, i.e. the part before the | - * $matches[2]: the parameters (including a leading |), if any + * @param array $piece The parts of the template + * $piece['text']: matched text + * $piece['title']: the title, i.e. the part before the | + * $piece['parts']: the parameter array * @return string the text of the template - * @access private + * @private */ - function braceSubstitution( $matches ) { - global $wgLinkCache, $wgContLang; - $fname = 'Parser::braceSubstitution'; + function braceSubstitution( $piece ) { + global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces; + $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/; wfProfileIn( $fname ); + wfProfileIn( __METHOD__.'-setup' ); - $found = false; - $nowiki = false; - $noparse = false; + # Flags + $found = false; # $text has been filled + $nowiki = false; # wiki markup in $text should be escaped + $noparse = false; # Unsafe HTML tags should not be stripped, etc. + $noargs = false; # Don't replace triple-brace arguments in $text + $replaceHeadings = false; # Make the edit section links go to the template not the article + $headingOffset = 0; # Skip headings when number, to account for those that weren't transcluded. + $isHTML = false; # $text is HTML, armour it against wikitext transformation + $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered + # Title object, where $text came from $title = NULL; - # Need to know if the template comes at the start of a line, - # to treat the beginning of the template like the beginning - # of a line for tables and block-level elements. - $linestart = $matches[1]; + $linestart = ''; + # $part1 is the bit before the first |, and must contain only title characters # $args is a list of arguments, starting from index 0, not including $part1 - $part1 = $matches[2]; + $titleText = $part1 = $piece['title']; # If the third subpattern matched anything, it will start with | - $args = $this->getTemplateArgs($matches[3]); - $argc = count( $args ); - - # Don't parse {{{}}} because that's only for template arguments - if ( $linestart === '{' ) { - $text = $matches[0]; - $found = true; - $noparse = true; + if (null == $piece['parts']) { + $replaceWith = $this->variableSubstitution (array ($piece['text'], $piece['title'])); + if ($replaceWith != $piece['text']) { + $text = $replaceWith; + $found = true; + $noparse = true; + $noargs = true; + } } + $args = (null == $piece['parts']) ? array() : $piece['parts']; + wfProfileOut( __METHOD__.'-setup' ); + # SUBST + wfProfileIn( __METHOD__.'-modifiers' ); if ( !$found ) { - $mwSubst =& MagicWord::get( MAG_SUBST ); - if ( $mwSubst->matchStartAndRemove( $part1 ) xor ($this->mOutputType == OT_WIKI) ) { + $mwSubst =& MagicWord::get( 'subst' ); + if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) { # One of two possibilities is true: # 1) Found SUBST but not in the PST phase # 2) Didn't find SUBST and in the PST phase # In either case, return without further processing - $text = $matches[0]; + $text = $piece['text']; $found = true; $noparse = true; + $noargs = true; } } - # MSG, MSGNW and INT + # MSG, MSGNW and RAW if ( !$found ) { # Check for MSGNW: - $mwMsgnw =& MagicWord::get( MAG_MSGNW ); + $mwMsgnw =& MagicWord::get( 'msgnw' ); if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) { $nowiki = true; } else { # Remove obsolete MSG: - $mwMsg =& MagicWord::get( MAG_MSG ); + $mwMsg =& MagicWord::get( 'msg' ); $mwMsg->matchStartAndRemove( $part1 ); } - # Check if it is an internal message - $mwInt =& MagicWord::get( MAG_INT ); - if ( $mwInt->matchStartAndRemove( $part1 ) ) { - if ( $this->incrementIncludeCount( 'int:'.$part1 ) ) { - $text = $linestart . wfMsgReal( $part1, $args, true ); - $found = true; - } + # Check for RAW: + $mwRaw =& MagicWord::get( 'raw' ); + if ( $mwRaw->matchStartAndRemove( $part1 ) ) { + $forceRawInterwiki = true; } } + wfProfileOut( __METHOD__.'-modifiers' ); + + //save path level before recursing into functions & templates. + $lastPathLevel = $this->mTemplatePath; - # NS + # Parser functions if ( !$found ) { - # Check for NS: (namespace expansion) - $mwNs = MagicWord::get( MAG_NS ); - if ( $mwNs->matchStartAndRemove( $part1 ) ) { - if ( intval( $part1 ) ) { - $text = $linestart . $wgContLang->getNsText( intval( $part1 ) ); - $found = true; + wfProfileIn( __METHOD__ . '-pfunc' ); + + $colonPos = strpos( $part1, ':' ); + if ( $colonPos !== false ) { + # Case sensitive functions + $function = substr( $part1, 0, $colonPos ); + if ( isset( $this->mFunctionSynonyms[1][$function] ) ) { + $function = $this->mFunctionSynonyms[1][$function]; } else { - $index = Namespace::getCanonicalIndex( strtolower( $part1 ) ); - if ( !is_null( $index ) ) { - $text = $linestart . $wgContLang->getNsText( $index ); - $found = true; + # Case insensitive functions + $function = strtolower( $function ); + if ( isset( $this->mFunctionSynonyms[0][$function] ) ) { + $function = $this->mFunctionSynonyms[0][$function]; + } else { + $function = false; } } - } - } + if ( $function ) { + $funcArgs = array_map( 'trim', $args ); + $funcArgs = array_merge( array( &$this, trim( substr( $part1, $colonPos + 1 ) ) ), $funcArgs ); + $result = call_user_func_array( $this->mFunctionHooks[$function], $funcArgs ); + $found = true; - # LOCALURL and LOCALURLE - if ( !$found ) { - $mwLocal = MagicWord::get( MAG_LOCALURL ); - $mwLocalE = MagicWord::get( MAG_LOCALURLE ); + // The text is usually already parsed, doesn't need triple-brace tags expanded, etc. + //$noargs = true; + //$noparse = true; + + if ( is_array( $result ) ) { + if ( isset( $result[0] ) ) { + $text = $linestart . $result[0]; + unset( $result[0] ); + } - if ( $mwLocal->matchStartAndRemove( $part1 ) ) { - $func = 'getLocalURL'; - } elseif ( $mwLocalE->matchStartAndRemove( $part1 ) ) { - $func = 'escapeLocalURL'; - } else { - $func = ''; - } - - if ( $func !== '' ) { - $title = Title::newFromText( $part1 ); - if ( !is_null( $title ) ) { - if ( $argc > 0 ) { - $text = $linestart . $title->$func( $args[0] ); + // Extract flags into the local scope + // This allows callers to set flags such as nowiki, noparse, found, etc. + extract( $result ); } else { - $text = $linestart . $title->$func(); + $text = $linestart . $result; } - $found = true; } } - } - - # GRAMMAR - if ( !$found && $argc == 1 ) { - $mwGrammar =& MagicWord::get( MAG_GRAMMAR ); - if ( $mwGrammar->matchStartAndRemove( $part1 ) ) { - $text = $linestart . $wgContLang->convertGrammar( $args[0], $part1 ); - $found = true; - } + wfProfileOut( __METHOD__ . '-pfunc' ); } # Template table test # Did we encounter this template already? If yes, it is in the cache # and we need to check for loops. - if ( !$found && isset( $this->mTemplates[$part1] ) ) { + if ( !$found && isset( $this->mTemplates[$piece['title']] ) ) { $found = true; # Infinite loop test if ( isset( $this->mTemplatePath[$part1] ) ) { $noparse = true; + $noargs = true; $found = true; $text = $linestart . - "\{\{$part1}}" . - ''; - wfDebug( "$fname: template loop broken at '$part1'\n" ); + "[[$part1]]"; + wfDebug( __METHOD__.": template loop broken at '$part1'\n" ); } else { # set $text to cached message. - $text = $linestart . $this->mTemplates[$part1]; + $text = $linestart . $this->mTemplates[$piece['title']]; + #treat title for cached page the same as others + $ns = NS_TEMPLATE; + $subpage = ''; + $part1 = $this->maybeDoSubpageLink( $part1, $subpage ); + if ($subpage !== '') { + $ns = $this->mTitle->getNamespace(); + } + $title = Title::newFromText( $part1, $ns ); + //used by include size checking + $titleText = $title->getPrefixedText(); + //used by edit section links + $replaceHeadings = true; + } } # Load from database - $replaceHeadings = false; - $isHTML = false; - $lastPathLevel = $this->mTemplatePath; if ( !$found ) { + wfProfileIn( __METHOD__ . '-loadtpl' ); $ns = NS_TEMPLATE; - $part1 = $this->maybeDoSubpageLink( $part1, $subpage='' ); + # declaring $subpage directly in the function call + # does not work correctly with references and breaks + # {{/subpage}}-style inclusions + $subpage = ''; + $part1 = $this->maybeDoSubpageLink( $part1, $subpage ); if ($subpage !== '') { $ns = $this->mTitle->getNamespace(); } $title = Title::newFromText( $part1, $ns ); - if ($title) { - $interwiki = Title::getInterwikiLink($title->getInterwiki()); - if ($interwiki != '' && $title->isTrans()) { - return $this->scarytransclude($title, $interwiki); - } - } - - if ( !is_null( $title ) && !$title->isExternal() ) { - # Check for excessive inclusion - $dbk = $title->getPrefixedDBkey(); - if ( $this->incrementIncludeCount( $dbk ) ) { - if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() ) { - # Capture special page output + + if ( !is_null( $title ) ) { + $titleText = $title->getPrefixedText(); + # Check for language variants if the template is not found + if($wgContLang->hasVariants() && $title->getArticleID() == 0){ + $wgContLang->findVariantLink($part1, $title); + } + + if ( !$title->isExternal() ) { + if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) { $text = SpecialPage::capturePath( $title ); if ( is_string( $text ) ) { $found = true; $noparse = true; + $noargs = true; $isHTML = true; - $this->mOutput->setCacheTime( -1 ); + $this->disableCache(); } + } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) { + $found = false; //access denied + wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() ); } else { - $article = new Article( $title ); - $articleContent = $article->getContentWithoutUsingSoManyDamnGlobals(); + list($articleContent,$title) = $this->fetchTemplateAndtitle( $title ); if ( $articleContent !== false ) { $found = true; $text = $articleContent; $replaceHeadings = true; } } - } - # If the title is valid but undisplayable, make a link to it - if ( $this->mOutputType == OT_HTML && !$found ) { - $text = '[['.$title->getPrefixedText().']]'; + # If the title is valid but undisplayable, make a link to it + if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) { + $text = "[[:$titleText]]"; + $found = true; + } + } elseif ( $title->isTrans() ) { + // Interwiki transclusion + if ( $this->ot['html'] && !$forceRawInterwiki ) { + $text = $this->interwikiTransclude( $title, 'render' ); + $isHTML = true; + $noparse = true; + } else { + $text = $this->interwikiTransclude( $title, 'raw' ); + $replaceHeadings = true; + } $found = true; } # Template cache array insertion + # Use the original $piece['title'] not the mangled $part1, so that + # modifiers such as RAW: produce separate cache entries if( $found ) { - $this->mTemplates[$part1] = $text; + if( $isHTML ) { + // A special page; don't store it in the template cache. + } else { + $this->mTemplates[$piece['title']] = $text; + } $text = $linestart . $text; } } + wfProfileOut( __METHOD__ . '-loadtpl' ); + } + + if ( $found && !$this->incrementIncludeSize( 'pre-expand', strlen( $text ) ) ) { + # Error, oversize inclusion + $text = $linestart . + "[[$titleText]]"; + $noparse = true; + $noargs = true; } # Recursive parsing, escaping and link table handling # Only for HTML output - if ( $nowiki && $found && $this->mOutputType == OT_HTML ) { + if ( $nowiki && $found && ( $this->ot['html'] || $this->ot['pre'] ) ) { $text = wfEscapeWikiText( $text ); - } elseif ( ($this->mOutputType == OT_HTML || $this->mOutputType == OT_WIKI) && $found && !$noparse) { - # Clean up argument array - $assocArgs = array(); - $index = 1; - foreach( $args as $arg ) { - $eqpos = strpos( $arg, '=' ); - if ( $eqpos === false ) { - $assocArgs[$index++] = $arg; - } else { - $name = trim( substr( $arg, 0, $eqpos ) ); - $value = trim( substr( $arg, $eqpos+1 ) ); - if ( $value === false ) { - $value = ''; - } - if ( $name !== false ) { - $assocArgs[$name] = $value; - } - } + } elseif ( !$this->ot['msg'] && $found ) { + if ( $noargs ) { + $assocArgs = array(); + } else { + # Clean up argument array + $assocArgs = self::createAssocArgs($args); + # Add a new element to the templace recursion path + $this->mTemplatePath[$part1] = 1; } - # Add a new element to the templace recursion path - $this->mTemplatePath[$part1] = 1; - - if( $this->mOutputType == OT_HTML ) { + if ( !$noparse ) { + # If there are any tags, only include them + if ( in_string( '', $text ) && in_string( '', $text ) ) { + $replacer = new OnlyIncludeReplacer; + StringUtils::delimiterReplaceCallback( '', '', + array( &$replacer, 'replace' ), $text ); + $text = $replacer->output; + } # Remove sections and tags - $text = preg_replace( '/.*?<\/noinclude>/s', '', $text ); + $text = StringUtils::delimiterReplace( '', '', '', $text ); $text = strtr( $text, array( '' => '' , '' => '' ) ); - # Strip ,
        , etc.
        -				$text = $this->strip( $text, $this->mStripState );
        -				$text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs );
        -			}
        -			$text = $this->replaceVariables( $text, $assocArgs );
         
        -			# Resume the link cache and register the inclusion as a link
        -			if ( $this->mOutputType == OT_HTML && !is_null( $title ) ) {
        -				$wgLinkCache->addLinkObj( $title );
        -			}
        +				if( $this->ot['html'] || $this->ot['pre'] ) {
        +					# Strip , 
        , etc.
        +					$text = $this->strip( $text, $this->mStripState );
        +					if ( $this->ot['html'] ) {
        +						$text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs );
        +					} elseif ( $this->ot['pre'] && $this->mOptions->getRemoveComments() ) {
        +						$text = Sanitizer::removeHTMLcomments( $text );
        +					}
        +				}
        +				$text = $this->replaceVariables( $text, $assocArgs );
         
        -			# If the template begins with a table or block-level
        -			# element, it should be treated as beginning a new line.
        -			if ($linestart !== '\n' && preg_match('/^({\\||:|;|#|\*)/', $text)) {
        -				$text = "\n" . $text;
        +				# If the template begins with a table or block-level
        +				# element, it should be treated as beginning a new line.
        +				if (!$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{
        +					$text = "\n" . $text;
        +				}
        +			} elseif ( !$noargs ) {
        +				# $noparse and !$noargs
        +				# Just replace the arguments, not any double-brace items
        +				# This is used for rendered interwiki transclusion
        +				$text = $this->replaceVariables( $text, $assocArgs, true );
         			}
         		}
         		# Prune lower levels off the recursion check path
         		$this->mTemplatePath = $lastPathLevel;
         
        +		if ( $found && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
        +			# Error, oversize inclusion
        +			$text = $linestart .
        +				"[[$titleText]]";
        +			$noparse = true;
        +			$noargs = true;
        +		}
        +
         		if ( !$found ) {
         			wfProfileOut( $fname );
        -			return $matches[0];
        +			return $piece['text'];
         		} else {
        +			wfProfileIn( __METHOD__ . '-placeholders' );
         			if ( $isHTML ) {
         				# Replace raw HTML by a placeholder
         				# Add a blank line preceding, to prevent it from mucking up
        @@ -2335,7 +3254,7 @@
         			} else {
         				# replace ==section headers==
         				# XXX this needs to go away once we have a better parser.
        -				if ( $this->mOutputType != OT_WIKI && $replaceHeadings ) {
        +				if ( !$this->ot['wiki'] && !$this->ot['pre'] && $replaceHeadings ) {
         					if( !is_null( $title ) )
         						$encodedname = base64_encode($title->getPrefixedDBkey());
         					else
        @@ -2343,7 +3262,8 @@
         					$m = preg_split('/(^={1,6}.*?={1,6}\s*?$)/m', $text, -1,
         						PREG_SPLIT_DELIM_CAPTURE);
         					$text = '';
        -					$nsec = 0;
        +					$nsec = $headingOffset;
        +
         					for( $i = 0; $i < count($m); $i += 2 ) {
         						$text .= $m[$i];
         						if (!isset($m[$i + 1]) || $m[$i + 1] == "") continue;
        @@ -2352,6 +3272,7 @@
         							$text .= $hl;
         							continue;
         						}
        +						$m2 = array();
         						preg_match('/^(={1,6})(.*?)(={1,6})\s*?$/m', $hl, $m2);
         						$text .= $m2[1] . $m2[2] . "" . $m2[3];
        @@ -2360,6 +3281,7 @@
         					}
         				}
         			}
        +			wfProfileOut( __METHOD__ . '-placeholders' );
         		}
         
         		# Prune lower levels off the recursion check path
        @@ -2367,7 +3289,7 @@
         
         		if ( !$found ) {
         			wfProfileOut( $fname );
        -			return $matches[0];
        +			return $piece['text'];
         		} else {
         			wfProfileOut( $fname );
         			return $text;
        @@ -2375,41 +3297,90 @@
         	}
         
         	/**
        -	 * Translude an interwiki link.
        +	 * Fetch the unparsed text of a template and register a reference to it.
        +	 */
        +	function fetchTemplateAndtitle( $title ) {
        +		$text = $skip = false;
        +		$finalTitle = $title;
        +		// Loop to fetch the article, with up to 1 redirect
        +		for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
        +			# Give extensions a chance to select the revision instead
        +			$id = false; // Assume current
        +			wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( &$this, &$title, &$skip, &$id ) );
        +			
        +			if( $skip ) {
        +				$text = false;
        +				$this->mOutput->addTemplate( $title, $title->getArticleID(), null );
        +				break;
        +			}
        +			$rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title );
        +			$rev_id = $rev ? $rev->getId() : 0;
        +			
        +			$this->mOutput->addTemplate( $title, $title->getArticleID(), $rev_id );
        +			
        +			if( $rev ) {
        +				$text = $rev->getText();
        +			} elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
        +				global $wgLang;
        +				$message = $wgLang->lcfirst( $title->getText() );
        +				$text = wfMsgForContentNoTrans( $message );
        +				if( wfEmptyMsg( $message, $text ) ) {
        +					$text = false;
        +					break;
        +				}
        +			} else {
        +				break;
        +			}
        +			if ( $text === false ) {
        +				break;
        +			}
        +			// Redirect?
        +			$finalTitle = $title;
        +			$title = Title::newFromRedirect( $text );
        +		}
        +		return array($text,$finalTitle);
        +	}
        +
        +	function fetchTemplate( $title ) {
        +		$rv = $this->fetchTemplateAndtitle($title);
        +		return $rv[0];
        +	}
        +
        +	/**
        +	 * Transclude an interwiki link.
         	 */
        -	function scarytransclude($title, $interwiki) {
        +	function interwikiTransclude( $title, $action ) {
         		global $wgEnableScaryTranscluding;
         
         		if (!$wgEnableScaryTranscluding)
         			return wfMsg('scarytranscludedisabled');
         
        -		$articlename = "Template:" . $title->getDBkey();
        -		$url = str_replace('$1', urlencode($articlename), $interwiki);
        +		$url = $title->getFullUrl( "action=$action" );
        +
         		if (strlen($url) > 255)
         			return wfMsg('scarytranscludetoolong');
        -		$text = $this->fetchScaryTemplateMaybeFromCache($url);
        -		$this->mIWTransData[] = $text;
        -		return "";
        +		return $this->fetchScaryTemplateMaybeFromCache($url);
         	}
         
         	function fetchScaryTemplateMaybeFromCache($url) {
        +		global $wgTranscludeCacheExpiry;
         		$dbr = wfGetDB(DB_SLAVE);
         		$obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'),
         				array('tc_url' => $url));
         		if ($obj) {
         			$time = $obj->tc_time;
         			$text = $obj->tc_contents;
        -			if ($time && $time < (time() + (60*60))) {
        +			if ($time && time() < $time + $wgTranscludeCacheExpiry ) {
         				return $text;
         			}
         		}
         
        -		$text = wfGetHTTP($url . '?action=render');
        +		$text = Http::get($url);
         		if (!$text)
         			return wfMsg('scarytranscludefailed', $url);
         
         		$dbw = wfGetDB(DB_MASTER);
        -		$dbw->replace('transcache', array(), array(
        +		$dbw->replace('transcache', array('tc_url'), array(
         			'tc_url' => $url,
         			'tc_time' => time(),
         			'tc_contents' => $text));
        @@ -2419,33 +3390,82 @@
         
         	/**
         	 * Triple brace replacement -- used for template arguments
        -	 * @access private
        +	 * @private
         	 */
         	function argSubstitution( $matches ) {
        -		$arg = trim( $matches[1] );
        -		$text = $matches[0];
        +		$arg = trim( $matches['title'] );
        +		$text = $matches['text'];
         		$inputArgs = end( $this->mArgStack );
         
         		if ( array_key_exists( $arg, $inputArgs ) ) {
         			$text = $inputArgs[$arg];
        +		} else if (($this->mOutputType == OT_HTML || $this->mOutputType == OT_PREPROCESS ) &&
        +		null != $matches['parts'] && count($matches['parts']) > 0) {
        +			$text = $matches['parts'][0];
        +		}
        +		if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
        +			$text = $matches['text'] .
        +				'';
         		}
         
         		return $text;
         	}
         
         	/**
        -	 * Returns true if the function is allowed to include this entity
        -	 * @access private
        +	 * Increment an include size counter
        +	 *
        +	 * @param string $type The type of expansion
        +	 * @param integer $size The size of the text
        +	 * @return boolean False if this inclusion would take it over the maximum, true otherwise
         	 */
        -	function incrementIncludeCount( $dbk ) {
        -		if ( !array_key_exists( $dbk, $this->mIncludeCount ) ) {
        -			$this->mIncludeCount[$dbk] = 0;
        -		}
        -		if ( ++$this->mIncludeCount[$dbk] <= MAX_INCLUDE_REPEAT ) {
        -			return true;
        -		} else {
        +	function incrementIncludeSize( $type, $size ) {
        +		if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
         			return false;
        +		} else {
        +			$this->mIncludeSizes[$type] += $size;
        +			return true;
        +		}
        +	}
        +
        +	/**
        +	 * Detect __NOGALLERY__ magic word and set a placeholder
        +	 */
        +	function stripNoGallery( &$text ) {
        +		# if the string __NOGALLERY__ (not case-sensitive) occurs in the HTML,
        +		# do not add TOC
        +		$mw = MagicWord::get( 'nogallery' );
        +		$this->mOutput->mNoGallery = $mw->matchAndRemove( $text ) ;
        +	}
        +
        +	/**
        +	 * Find the first __TOC__ magic word and set a 
        +	 * placeholder that will then be replaced by the real TOC in
        +	 * ->formatHeadings, this works because at this points real
        +	 * comments will have already been discarded by the sanitizer.
        +	 *
        +	 * Any additional __TOC__ magic words left over will be discarded
        +	 * as there can only be one TOC on the page.
        +	 */
        +	function stripToc( $text ) {
        +		# if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
        +		# do not add TOC
        +		$mw = MagicWord::get( 'notoc' );
        +		if( $mw->matchAndRemove( $text ) ) {
        +			$this->mShowToc = false;
        +		}
        +
        +		$mw = MagicWord::get( 'toc' );
        +		if( $mw->match( $text ) ) {
        +			$this->mShowToc = true;
        +			$this->mForceTocPosition = true;
        +
        +			// Set a placeholder. At the end we'll fill it in with the TOC.
        +			$text = $mw->replace( '', $text, 1 );
        +
        +			// Only keep the first one.
        +			$text = $mw->replace( '', $text );
         		}
        +		return $text;
         	}
         
         	/**
        @@ -2460,68 +3480,55 @@
         	 *
         	 * @param string $text
         	 * @param boolean $isMain
        -	 * @access private
        +	 * @private
         	 */
         	function formatHeadings( $text, $isMain=true ) {
        -		global $wgMaxTocLevel, $wgContLang, $wgLinkHolders, $wgInterwikiLinkHolders;
        +		global $wgMaxTocLevel, $wgContLang;
         
         		$doNumberHeadings = $this->mOptions->getNumberHeadings();
        -		$doShowToc = true;
        -		$forceTocHere = false;
        -		if( !$this->mTitle->userCanEdit() ) {
        +		if( !$this->mTitle->quickUserCan( 'edit' ) ) {
         			$showEditLink = 0;
         		} else {
         			$showEditLink = $this->mOptions->getEditSection();
         		}
         
         		# Inhibit editsection links if requested in the page
        -		$esw =& MagicWord::get( MAG_NOEDITSECTION );
        +		$esw =& MagicWord::get( 'noeditsection' );
         		if( $esw->matchAndRemove( $text ) ) {
         			$showEditLink = 0;
         		}
        -		# if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
        -		# do not add TOC
        -		$mw =& MagicWord::get( MAG_NOTOC );
        -		if( $mw->matchAndRemove( $text ) ) {
        -			$doShowToc = false;
        -		}
         
         		# Get all headlines for numbering them and adding funky stuff like [edit]
         		# links - this is for later, but we need the number of headlines right now
        -		$numMatches = preg_match_all( '/)(.*?)<\/H[1-6] *>/i', $text, $matches );
        +		$matches = array();
        +		$numMatches = preg_match_all( '/[1-6])(?P.*?'.'>)(?P
        .*?)<\/H[1-6] *>/i', $text, $matches ); # if there are fewer than 4 headlines in the article, do not show TOC - if( $numMatches < 4 ) { - $doShowToc = false; + # unless it's been explicitly enabled. + $enoughToc = $this->mShowToc && + (($numMatches >= 4) || $this->mForceTocPosition); + + # Allow user to stipulate that a page should have a "new section" + # link added via __NEWSECTIONLINK__ + $mw =& MagicWord::get( 'newsectionlink' ); + if( $mw->matchAndRemove( $text ) ) + $this->mOutput->setNewSection( true ); + + # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML, + # override above conditions and always show TOC above first header + $mw =& MagicWord::get( 'forcetoc' ); + if ($mw->matchAndRemove( $text ) ) { + $this->mShowToc = true; + $enoughToc = true; } - # if the string __TOC__ (not case-sensitive) occurs in the HTML, - # override above conditions and always show TOC at that place - - $mw =& MagicWord::get( MAG_TOC ); - if($mw->match( $text ) ) { - $doShowToc = true; - $forceTocHere = true; - } else { - # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML, - # override above conditions and always show TOC above first header - $mw =& MagicWord::get( MAG_FORCETOC ); - if ($mw->matchAndRemove( $text ) ) { - $doShowToc = true; - } - } - - # Never ever show TOC if no headers - if( $numMatches < 1 ) { - $doShowToc = false; - } - - # We need this to perform operations on the HTML - $sk =& $this->mOptions->getSkin(); + # We need this to perform operations on the HTML + $sk = $this->mOptions->getSkin(); # headline counter $headlineCount = 0; $sectionCount = 0; # headlineCount excluding template sections + $numVisible = 0; # Ugh .. the TOC should have neat indentation levels which can be # passed to the skin functions. These are determined here @@ -2541,7 +3548,7 @@ $templatetitle = ''; $templatesection = 0; $numbering = ''; - + $mat = array(); if (preg_match("//", $headline, $mat)) { $istemplate = 1; $templatetitle = base64_decode($mat[1]); @@ -2555,13 +3562,17 @@ } $level = $matches[1][$headlineCount]; - if( $doNumberHeadings || $doShowToc ) { + if( $doNumberHeadings || $enoughToc ) { if ( $level > $prevlevel ) { # Increase TOC level $toclevel++; $sublevelCount[$toclevel] = 0; - $toc .= $sk->tocIndent(); + if( $toclevel<$wgMaxTocLevel ) { + $prevtoclevel = $toclevel; + $toc .= $sk->tocIndent(); + $numVisible++; + } } elseif ( $level < $prevlevel && $toclevel > 1 ) { # Decrease TOC level, find level to jump to @@ -2583,12 +3594,20 @@ } } } - - $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel ); + if( $toclevel<$wgMaxTocLevel ) { + if($prevtoclevel < $wgMaxTocLevel) { + # Unindent only if the previous toc level was shown :p + $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel ); + } else { + $toc .= $sk->tocLineEnd(); + } + } } else { # No change in level, end TOC line - $toc .= $sk->tocLineEnd(); + if( $toclevel<$wgMaxTocLevel ) { + $toc .= $sk->tocLineEnd(); + } } $levelCount[$toclevel] = $level; @@ -2609,8 +3628,7 @@ # The canonized header is a version of the header text safe to use for links # Avoid insertion of weird stuff like by expanding the relevant sections - $canonized_headline = $this->unstrip( $headline, $this->mStripState ); - $canonized_headline = $this->unstripNoWiki( $canonized_headline, $this->mStripState ); + $canonized_headline = $this->mStripState->unstripBoth( $headline ); # Remove link placeholders by the link text. # @@ -2623,19 +3641,25 @@ "\$this->mInterwikiLinkHolders['texts'][\$1]", $canonized_headline ); - # strip out HTML - $canonized_headline = preg_replace( '/<.*?' . '>/','',$canonized_headline ); - $tocline = trim( $canonized_headline ); - $canonized_headline = urlencode( Sanitizer::decodeCharReferences( str_replace(' ', '_', $tocline) ) ); - $replacearray = array( - '%3A' => ':', - '%' => '.' + # Strip out HTML (other than plain and : bug 8393) + $tocline = preg_replace( + array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ), + array( '', '<$1>'), + $canonized_headline ); - $canonized_headline = str_replace(array_keys($replacearray),array_values($replacearray),$canonized_headline); + $tocline = trim( $tocline ); + + # For the anchor, strip out HTML-y stuff period + $canonized_headline = preg_replace( '/<.*?'.'>/', '', $canonized_headline ); + $canonized_headline = trim( $canonized_headline ); + + # Save headline for section edit hint before it's escaped + $headline_hint = $canonized_headline; + $canonized_headline = Sanitizer::escapeId( $canonized_headline ); $refers[$headlineCount] = $canonized_headline; # count how many in assoc. array so we can track dupes in anchors - @$refers[$canonized_headline]++; + isset( $refers[$canonized_headline] ) ? $refers[$canonized_headline]++ : $refers[$canonized_headline] = 1; $refcount[$headlineCount]=$refers[$canonized_headline]; # Don't number the heading if it is the only one (looks silly) @@ -2649,29 +3673,34 @@ if($refcount[$headlineCount] > 1 ) { $anchor .= '_' . $refcount[$headlineCount]; } - if( $doShowToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) { + if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) { $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel); } + # give headline the correct tag if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) { - if ( empty( $head[$headlineCount] ) ) { - $head[$headlineCount] = ''; - } if( $istemplate ) - $head[$headlineCount] .= $sk->editSectionLinkForOther($templatetitle, $templatesection); + $editlink = $sk->editSectionLinkForOther($templatetitle, $templatesection); else - $head[$headlineCount] .= $sk->editSectionLink($this->mTitle, $sectionCount+1); + $editlink = $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint); + } else { + $editlink = ''; } - - # give headline the correct tag - @$head[$headlineCount] .= "'; + $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink ); $headlineCount++; if( !$istemplate ) $sectionCount++; } - if( $doShowToc ) { - $toc .= $sk->tocUnindent( $toclevel - 1 ); + # Never ever show TOC if no headers + if( $numVisible < 1 ) { + $enoughToc = false; + } + + if( $enoughToc ) { + if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) { + $toc .= $sk->tocUnindent( $prevtoclevel - 1 ); + } $toc = $sk->tocList( $toc ); } @@ -2690,8 +3719,8 @@ # $full .= $sk->editSectionLink(0); } $full .= $block; - if( $doShowToc && !$i && $isMain && !$forceTocHere) { - # Top anchor now in skin + if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) { + # Top anchor now in skin $full = $full.$toc; } @@ -2700,132 +3729,14 @@ } $i++; } - if($forceTocHere) { - $mw =& MagicWord::get( MAG_TOC ); - return $mw->replace( $toc, $full ); + if( $this->mForceTocPosition ) { + return str_replace( '', $toc, $full ); } else { return $full; } } /** - * Return an HTML link for the "ISBN 123456" text - * @access private - */ - function magicISBN( $text ) { - $fname = 'Parser::magicISBN'; - wfProfileIn( $fname ); - - $a = split( 'ISBN ', ' '.$text ); - if ( count ( $a ) < 2 ) { - wfProfileOut( $fname ); - return $text; - } - $text = substr( array_shift( $a ), 1); - $valid = '0123456789-Xx'; - - foreach ( $a as $x ) { - $isbn = $blank = '' ; - while ( ' ' == $x{0} ) { - $blank .= ' '; - $x = substr( $x, 1 ); - } - if ( $x == '' ) { # blank isbn - $text .= "ISBN $blank"; - continue; - } - while ( strstr( $valid, $x{0} ) != false ) { - $isbn .= $x{0}; - $x = substr( $x, 1 ); - } - $num = str_replace( '-', '', $isbn ); - $num = str_replace( ' ', '', $num ); - $num = str_replace( 'x', 'X', $num ); - - if ( '' == $num ) { - $text .= "ISBN $blank$x"; - } else { - $titleObj = Title::makeTitle( NS_SPECIAL, 'Booksources' ); - $text .= 'ISBN $isbn"; - $text .= $x; - } - } - wfProfileOut( $fname ); - return $text; - } - - /** - * Return an HTML link for the "RFC 1234" text - * - * @access private - * @param string $text Text to be processed - * @param string $keyword Magic keyword to use (default RFC) - * @param string $urlmsg Interface message to use (default rfcurl) - * @return string - */ - function magicRFC( $text, $keyword='RFC ', $urlmsg='rfcurl' ) { - - $valid = '0123456789'; - $internal = false; - - $a = split( $keyword, ' '.$text ); - if ( count ( $a ) < 2 ) { - return $text; - } - $text = substr( array_shift( $a ), 1); - - /* Check if keyword is preceed by [[. - * This test is made here cause of the array_shift above - * that prevent the test to be done in the foreach. - */ - if ( substr( $text, -2 ) == '[[' ) { - $internal = true; - } - - foreach ( $a as $x ) { - /* token might be empty if we have RFC RFC 1234 */ - if ( $x=='' ) { - $text.=$keyword; - continue; - } - - $id = $blank = '' ; - - /** remove and save whitespaces in $blank */ - while ( $x{0} == ' ' ) { - $blank .= ' '; - $x = substr( $x, 1 ); - } - - /** remove and save the rfc number in $id */ - while ( strstr( $valid, $x{0} ) != false ) { - $id .= $x{0}; - $x = substr( $x, 1 ); - } - - if ( $id == '' ) { - /* call back stripped spaces*/ - $text .= $keyword.$blank.$x; - } elseif( $internal ) { - /* normal link */ - $text .= $keyword.$id.$x; - } else { - /* build the external link*/ - $url = wfMsg( $urlmsg, $id); - $sk =& $this->mOptions->getSkin(); - $la = $sk->getExternalLinkAttributes( $url, $keyword.$id ); - $text .= "{$keyword}{$id}{$x}"; - } - - /* Check if the next RFC keyword is preceed by [[ */ - $internal = ( substr($x,-2) == '[[' ); - } - return $text; - } - - /** * Transform wiki markup when saving a page by doing \r\n -> \n * conversion, substitting signatures, {{subst:}} templates, etc. * @@ -2835,115 +3746,189 @@ * @param ParserOptions $options parsing options * @param bool $clearState whether to clear the parser state first * @return string the altered wiki markup - * @access public + * @public */ - function preSaveTransform( $text, &$title, &$user, $options, $clearState = true ) { + function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) { $this->mOptions = $options; $this->mTitle =& $title; - $this->mOutputType = OT_WIKI; + $this->setOutputType( OT_WIKI ); if ( $clearState ) { $this->clearState(); } - $stripState = false; + $stripState = new StripState; $pairs = array( "\r\n" => "\n", ); $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text ); - $text = $this->strip( $text, $stripState, false ); - $text = $this->pstPass2( $text, $user ); - $text = $this->unstrip( $text, $stripState ); - $text = $this->unstripNoWiki( $text, $stripState ); + $text = $this->strip( $text, $stripState, true, array( 'gallery' ) ); + $text = $this->pstPass2( $text, $stripState, $user ); + $text = $stripState->unstripBoth( $text ); return $text; } /** * Pre-save transform helper function - * @access private + * @private */ - function pstPass2( $text, &$user ) { + function pstPass2( $text, &$stripState, $user ) { global $wgContLang, $wgLocaltimezone; - # Variable replacement - # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags - $text = $this->replaceVariables( $text ); - - # Signatures - # - $n = $user->getName(); - $k = $user->getOption( 'nickname' ); - if ( '' == $k ) { $k = $n; } + /* Note: This is the timestamp saved as hardcoded wikitext to + * the database, we use $wgContLang here in order to give + * everyone the same signature and use the default one rather + * than the one selected in each user's preferences. + */ if ( isset( $wgLocaltimezone ) ) { $oldtz = getenv( 'TZ' ); putenv( 'TZ='.$wgLocaltimezone ); } - - /* Note: This is the timestamp saved as hardcoded wikitext to - * the database, we use $wgContLang here in order to give - * everyone the same signiture and use the default one rather - * than the one selected in each users preferences. - */ $d = $wgContLang->timeanddate( date( 'YmdHis' ), false, false) . ' (' . date( 'T' ) . ')'; if ( isset( $wgLocaltimezone ) ) { putenv( 'TZ='.$oldtz ); } - if( $user->getOption( 'fancysig' ) ) { - $sigText = $k; - } else { - $sigText = '[[' . $wgContLang->getNsText( NS_USER ) . ":$n|$k]]"; - } - $text = preg_replace( '/~~~~~/', $d, $text ); - $text = preg_replace( '/~~~~/', "$sigText $d", $text ); - $text = preg_replace( '/~~~/', $sigText, $text ); + # Variable replacement + # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags + $text = $this->replaceVariables( $text ); + + # Strip out etc. added via replaceVariables + $text = $this->strip( $text, $stripState, false, array( 'gallery' ) ); + + # Signatures + $sigText = $this->getUserSig( $user ); + $text = strtr( $text, array( + '~~~~~' => $d, + '~~~~' => "$sigText $d", + '~~~' => $sigText + ) ); # Context links: [[|name]] and [[name (context)|]] # - $tc = "[&;%\\-,.\\(\\)' _0-9A-Za-z\\/:\\x80-\\xff]"; - $np = "[&;%\\-,.' _0-9A-Za-z\\/:\\x80-\\xff]"; # No parens - $namespacechar = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii! - $conpat = "/^({$np}+) \\(({$tc}+)\\)$/"; - - $p1 = "/\[\[({$np}+) \\(({$np}+)\\)\\|]]/"; # [[page (context)|]] - $p2 = "/\[\[\\|({$tc}+)]]/"; # [[|page]] - $p3 = "/\[\[(:*$namespacechar+):({$np}+)\\|]]/"; # [[namespace:page|]] and [[:namespace:page|]] - $p4 = "/\[\[(:*$namespacechar+):({$np}+) \\(({$np}+)\\)\\|]]/"; # [[ns:page (cont)|]] and [[:ns:page (cont)|]] - $context = ''; - $t = $this->mTitle->getText(); - if ( preg_match( $conpat, $t, $m ) ) { - $context = $m[2]; - } - $text = preg_replace( $p4, '[[\\1:\\2 (\\3)|\\2]]', $text ); - $text = preg_replace( $p1, '[[\\1 (\\2)|\\1]]', $text ); - $text = preg_replace( $p3, '[[\\1:\\2|\\2]]', $text ); + global $wgLegalTitleChars; + $tc = "[$wgLegalTitleChars]"; + $nc = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii! + + $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/"; # [[ns:page (context)|]] + $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]] + $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]] + + # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]" + $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text ); + $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text ); - if ( '' == $context ) { - $text = preg_replace( $p2, '[[\\1]]', $text ); + $t = $this->mTitle->getText(); + $m = array(); + if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) { + $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); + } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) { + $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); } else { - $text = preg_replace( $p2, "[[\\1 ({$context})|\\1]]", $text ); + # if there's no context, don't bother duplicating the title + $text = preg_replace( $p2, '[[\\1]]', $text ); } # Trim trailing whitespace - # MAG_END (__END__) tag allows for trailing - # whitespace to be deliberately included $text = rtrim( $text ); - $mw =& MagicWord::get( MAG_END ); - $mw->matchAndRemove( $text ); return $text; } /** + * Fetch the user's signature text, if any, and normalize to + * validated, ready-to-insert wikitext. + * + * @param User $user + * @return string + * @private + */ + function getUserSig( &$user ) { + global $wgMaxSigChars; + + $username = $user->getName(); + $nickname = $user->getOption( 'nickname' ); + $nickname = $nickname === '' ? $username : $nickname; + + if( mb_strlen( $nickname ) > $wgMaxSigChars ) { + $nickname = $username; + wfDebug( __METHOD__ . ": $username has overlong signature.\n" ); + } elseif( $user->getBoolOption( 'fancysig' ) !== false ) { + # Sig. might contain markup; validate this + if( $this->validateSig( $nickname ) !== false ) { + # Validated; clean up (if needed) and return it + return $this->cleanSig( $nickname, true ); + } else { + # Failed to validate; fall back to the default + $nickname = $username; + wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" ); + } + } + + // Make sure nickname doesnt get a sig in a sig + $nickname = $this->cleanSigInSig( $nickname ); + + # If we're still here, make it a link to the user page + $userpage = $user->getUserPage(); + return( '[[' . $userpage->getPrefixedText() . '|' . wfEscapeWikiText( $nickname ) . ']]' ); + } + + /** + * Check that the user's signature contains no bad XML + * + * @param string $text + * @return mixed An expanded string, or false if invalid. + */ + function validateSig( $text ) { + return( wfIsWellFormedXmlFragment( $text ) ? $text : false ); + } + + /** + * Clean up signature text + * + * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig + * 2) Substitute all transclusions + * + * @param string $text + * @param $parsing Whether we're cleaning (preferences save) or parsing + * @return string Signature text + */ + function cleanSig( $text, $parsing = false ) { + global $wgTitle; + $this->startExternalParse( $wgTitle, new ParserOptions(), $parsing ? OT_WIKI : OT_MSG ); + + $substWord = MagicWord::get( 'subst' ); + $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase(); + $substText = '{{' . $substWord->getSynonym( 0 ); + + $text = preg_replace( $substRegex, $substText, $text ); + $text = $this->cleanSigInSig( $text ); + $text = $this->replaceVariables( $text ); + + $this->clearState(); + return $text; + } + + /** + * Strip ~~~, ~~~~ and ~~~~~ out of signatures + * @param string $text + * @return string Signature text with /~{3,5}/ removed + */ + function cleanSigInSig( $text ) { + $text = preg_replace( '/~{3,5}/', '', $text ); + return $text; + } + + /** * Set up some variables which are usually set up in parse() * so that an external function can call some class members with confidence - * @access public + * @public */ function startExternalParse( &$title, $options, $outputType, $clearState = true ) { $this->mTitle =& $title; $this->mOptions = $options; - $this->mOutputType = $outputType; + $this->setOutputType( $outputType ); if ( $clearState ) { $this->clearState(); } @@ -2955,41 +3940,132 @@ * @param string $text the text to transform * @param ParserOptions $options options * @return string the text with variables substituted - * @access public + * @public */ function transformMsg( $text, $options ) { global $wgTitle; static $executing = false; + $fname = "Parser::transformMsg"; + # Guard against infinite recursion if ( $executing ) { return $text; } $executing = true; - $this->mTitle = $wgTitle; + wfProfileIn($fname); + + if ( $wgTitle && !( $wgTitle instanceof FakeTitle ) ) { + $this->mTitle = $wgTitle; + } else { + $this->mTitle = Title::newFromText('msg'); + } $this->mOptions = $options; - $this->mOutputType = OT_MSG; + $this->setOutputType( OT_MSG ); $this->clearState(); $text = $this->replaceVariables( $text ); $executing = false; + wfProfileOut($fname); return $text; } /** * Create an HTML-style tag, e.g. special text - * Callback will be called with the text within - * Transform and return the text within - * @access public + * The callback should have the following form: + * function myParserHook( $text, $params, &$parser ) { ... } + * + * Transform and return $text. Use $parser for any required context, e.g. use + * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions + * + * @public + * + * @param mixed $tag The tag to use, e.g. 'hook' for + * @param mixed $callback The callback function (and object) to use for the tag + * + * @return The old value of the mTagHooks array associated with the hook */ function setHook( $tag, $callback ) { - $oldVal = @$this->mTagHooks[$tag]; + $tag = strtolower( $tag ); + $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null; $this->mTagHooks[$tag] = $callback; + + return $oldVal; + } + + function setTransparentTagHook( $tag, $callback ) { + $tag = strtolower( $tag ); + $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null; + $this->mTransparentTagHooks[$tag] = $callback; + return $oldVal; } /** + * Create a function, e.g. {{sum:1|2|3}} + * The callback function should have the form: + * function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... } + * + * The callback may either return the text result of the function, or an array with the text + * in element 0, and a number of flags in the other elements. The names of the flags are + * specified in the keys. Valid flags are: + * found The text returned is valid, stop processing the template. This + * is on by default. + * nowiki Wiki markup in the return value should be escaped + * noparse Unsafe HTML tags should not be stripped, etc. + * noargs Don't replace triple-brace arguments in the return value + * isHTML The returned text is HTML, armour it against wikitext transformation + * + * @public + * + * @param string $id The magic word ID + * @param mixed $callback The callback function (and object) to use + * @param integer $flags a combination of the following flags: + * SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}} + * + * @return The old callback function for this name, if any + */ + function setFunctionHook( $id, $callback, $flags = 0 ) { + $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id] : null; + $this->mFunctionHooks[$id] = $callback; + + # Add to function cache + $mw = MagicWord::get( $id ); + if( !$mw ) + throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' ); + + $synonyms = $mw->getSynonyms(); + $sensitive = intval( $mw->isCaseSensitive() ); + + foreach ( $synonyms as $syn ) { + # Case + if ( !$sensitive ) { + $syn = strtolower( $syn ); + } + # Add leading hash + if ( !( $flags & SFH_NO_HASH ) ) { + $syn = '#' . $syn; + } + # Remove trailing colon + if ( substr( $syn, -1, 1 ) == ':' ) { + $syn = substr( $syn, 0, -1 ); + } + $this->mFunctionSynonyms[$sensitive][$syn] = $id; + } + return $oldVal; + } + + /** + * Get all registered function hook identifiers + * + * @return array + */ + function getFunctionHooks() { + return array_keys( $this->mFunctionHooks ); + } + + /** * Replace link placeholders with actual links, in the buffer * Placeholders created in Skin::makeLinkObj() * Returns an array of links found, indexed by PDBK: @@ -2999,8 +4075,8 @@ * $options is a bit field, RLH_FOR_UPDATE to select for update */ function replaceLinkHolders( &$text, $options = 0 ) { - global $wgUser, $wgLinkCache; - global $wgOutputReplace; + global $wgUser; + global $wgContLang; $fname = 'Parser::replaceLinkHolders'; wfProfileIn( $fname ); @@ -3008,10 +4084,11 @@ $pdbks = array(); $colours = array(); $sk = $this->mOptions->getSkin(); + $linkCache =& LinkCache::singleton(); if ( !empty( $this->mLinkHolders['namespaces'] ) ) { wfProfileIn( $fname.'-check' ); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $page = $dbr->tableName( 'page' ); $threshold = $wgUser->getOption('stubthreshold'); @@ -3020,7 +4097,8 @@ # Generate query $query = false; - foreach ( $this->mLinkHolders['namespaces'] as $key => $val ) { + $current = null; + foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { # Make title object $title = $this->mLinkHolders['titles'][$key]; @@ -3031,23 +4109,28 @@ } $pdbk = $pdbks[$key] = $title->getPrefixedDBkey(); - # Check if it's in the link cache already - if ( $wgLinkCache->getGoodLinkID( $pdbk ) ) { + # Check if it's a static known link, e.g. interwiki + if ( $title->isAlwaysKnown() ) { + $colours[$pdbk] = 1; + } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) { $colours[$pdbk] = 1; - } elseif ( $wgLinkCache->isBadLink( $pdbk ) ) { + $this->mOutput->addLink( $title, $id ); + } elseif ( $linkCache->isBadLink( $pdbk ) ) { + $colours[$pdbk] = 0; + } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) { $colours[$pdbk] = 0; } else { # Not in the link cache, add it to the query if ( !isset( $current ) ) { - $current = $val; + $current = $ns; $query = "SELECT page_id, page_namespace, page_title"; if ( $threshold > 0 ) { $query .= ', page_len, page_is_redirect'; } - $query .= " FROM $page WHERE (page_namespace=$val AND page_title IN("; - } elseif ( $current != $val ) { - $current = $val; - $query .= ")) OR (page_namespace=$val AND page_title IN("; + $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN("; + } elseif ( $current != $ns ) { + $current = $ns; + $query .= ")) OR (page_namespace=$ns AND page_title IN("; } else { $query .= ', '; } @@ -3070,53 +4153,179 @@ while ( $s = $dbr->fetchObject($res) ) { $title = Title::makeTitle( $s->page_namespace, $s->page_title ); $pdbk = $title->getPrefixedDBkey(); - $wgLinkCache->addGoodLinkObj( $s->page_id, $title ); + $linkCache->addGoodLinkObj( $s->page_id, $title ); + $this->mOutput->addLink( $title, $s->page_id ); - if ( $threshold > 0 ) { - $size = $s->page_len; - if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) { - $colours[$pdbk] = 1; - } else { - $colours[$pdbk] = 2; + $colours[$pdbk] = ( $threshold == 0 || ( + $s->page_len >= $threshold || # always true if $threshold <= 0 + $s->page_is_redirect || + !Namespace::isContent( $s->page_namespace ) ) + ? 1 : 2 ); + } + } + wfProfileOut( $fname.'-check' ); + + # Do a second query for different language variants of links and categories + if($wgContLang->hasVariants()){ + $linkBatch = new LinkBatch(); + $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders) + $categoryMap = array(); // maps $category_variant => $category (dbkeys) + $varCategories = array(); // category replacements oldDBkey => newDBkey + + $categories = $this->mOutput->getCategoryLinks(); + + // Add variants of links to link batch + foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { + $title = $this->mLinkHolders['titles'][$key]; + if ( is_null( $title ) ) + continue; + + $pdbk = $title->getPrefixedDBkey(); + $titleText = $title->getText(); + + // generate all variants of the link title text + $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText); + + // if link was not found (in first query), add all variants to query + if ( !isset($colours[$pdbk]) ){ + foreach($allTextVariants as $textVariant){ + if($textVariant != $titleText){ + $variantTitle = Title::makeTitle( $ns, $textVariant ); + if(is_null($variantTitle)) continue; + $linkBatch->addObj( $variantTitle ); + $variantMap[$variantTitle->getPrefixedDBkey()][] = $key; + } } - } else { - $colours[$pdbk] = 1; + } + } + + // process categories, check if a category exists in some variant + foreach( $categories as $category ){ + $variants = $wgContLang->convertLinkToAllVariants($category); + foreach($variants as $variant){ + if($variant != $category){ + $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) ); + if(is_null($variantTitle)) continue; + $linkBatch->addObj( $variantTitle ); + $categoryMap[$variant] = $category; + } + } + } + + + if(!$linkBatch->isEmpty()){ + // construct query + $titleClause = $linkBatch->constructSet('page', $dbr); + + $variantQuery = "SELECT page_id, page_namespace, page_title"; + if ( $threshold > 0 ) { + $variantQuery .= ', page_len, page_is_redirect'; + } + + $variantQuery .= " FROM $page WHERE $titleClause"; + if ( $options & RLH_FOR_UPDATE ) { + $variantQuery .= ' FOR UPDATE'; + } + + $varRes = $dbr->query( $variantQuery, $fname ); + + // for each found variants, figure out link holders and replace + while ( $s = $dbr->fetchObject($varRes) ) { + + $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title ); + $varPdbk = $variantTitle->getPrefixedDBkey(); + $vardbk = $variantTitle->getDBkey(); + + $holderKeys = array(); + if(isset($variantMap[$varPdbk])){ + $holderKeys = $variantMap[$varPdbk]; + $linkCache->addGoodLinkObj( $s->page_id, $variantTitle ); + $this->mOutput->addLink( $variantTitle, $s->page_id ); + } + + // loop over link holders + foreach($holderKeys as $key){ + $title = $this->mLinkHolders['titles'][$key]; + if ( is_null( $title ) ) continue; + + $pdbk = $title->getPrefixedDBkey(); + + if(!isset($colours[$pdbk])){ + // found link in some of the variants, replace the link holder data + $this->mLinkHolders['titles'][$key] = $variantTitle; + $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey(); + + // set pdbk and colour + $pdbks[$key] = $varPdbk; + if ( $threshold > 0 ) { + $size = $s->page_len; + if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) { + $colours[$varPdbk] = 1; + } else { + $colours[$varPdbk] = 2; + } + } + else { + $colours[$varPdbk] = 1; + } + } + } + + // check if the object is a variant of a category + if(isset($categoryMap[$vardbk])){ + $oldkey = $categoryMap[$vardbk]; + if($oldkey != $vardbk) + $varCategories[$oldkey]=$vardbk; + } + } + + // rebuild the categories in original order (if there are replacements) + if(count($varCategories)>0){ + $newCats = array(); + $originalCats = $this->mOutput->getCategories(); + foreach($originalCats as $cat => $sortkey){ + // make the replacement + if( array_key_exists($cat,$varCategories) ) + $newCats[$varCategories[$cat]] = $sortkey; + else $newCats[$cat] = $sortkey; + } + $this->mOutput->setCategoryLinks($newCats); } } } - wfProfileOut( $fname.'-check' ); # Construct search and replace arrays wfProfileIn( $fname.'-construct' ); - $wgOutputReplace = array(); + $replacePairs = array(); foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) { $pdbk = $pdbks[$key]; $searchkey = ""; $title = $this->mLinkHolders['titles'][$key]; if ( empty( $colours[$pdbk] ) ) { - $wgLinkCache->addBadLinkObj( $title ); + $linkCache->addBadLinkObj( $title ); $colours[$pdbk] = 0; - $wgOutputReplace[$searchkey] = $sk->makeBrokenLinkObj( $title, + $this->mOutput->addLink( $title, 0 ); + $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title, $this->mLinkHolders['texts'][$key], $this->mLinkHolders['queries'][$key] ); } elseif ( $colours[$pdbk] == 1 ) { - $wgOutputReplace[$searchkey] = $sk->makeKnownLinkObj( $title, + $replacePairs[$searchkey] = $sk->makeKnownLinkObj( $title, $this->mLinkHolders['texts'][$key], $this->mLinkHolders['queries'][$key] ); } elseif ( $colours[$pdbk] == 2 ) { - $wgOutputReplace[$searchkey] = $sk->makeStubLinkObj( $title, + $replacePairs[$searchkey] = $sk->makeStubLinkObj( $title, $this->mLinkHolders['texts'][$key], $this->mLinkHolders['queries'][$key] ); } } + $replacer = new HashtableReplacer( $replacePairs, 1 ); wfProfileOut( $fname.'-construct' ); # Do the thing wfProfileIn( $fname.'-replace' ); - $text = preg_replace_callback( '/()/', - "wfOutputReplaceMatches", + $replacer->cb(), $text); wfProfileOut( $fname.'-replace' ); @@ -3127,15 +4336,16 @@ if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) { wfProfileIn( $fname.'-interwiki' ); # Make interwiki link HTML - $wgOutputReplace = array(); + $replacePairs = array(); foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) { $title = $this->mInterwikiLinkHolders['titles'][$key]; - $wgOutputReplace[$key] = $sk->makeLinkObj( $title, $link ); + $replacePairs[$key] = $sk->makeLinkObj( $title, $link ); } + $replacer = new HashtableReplacer( $replacePairs, 1 ); $text = preg_replace_callback( '//', - "wfOutputReplaceMatches", + $replacer->cb(), $text ); wfProfileOut( $fname.'-interwiki' ); } @@ -3151,9 +4361,6 @@ * @return string */ function replaceLinkHoldersText( $text ) { - global $wgUser, $wgLinkCache; - global $wgOutputReplace; - $fname = 'Parser::replaceLinkHoldersText'; wfProfileIn( $fname ); @@ -3169,7 +4376,7 @@ /** * @param array $matches * @return string - * @access private + * @private */ function replaceLinkHoldersTextCallback( $matches ) { $type = $matches[1]; @@ -3187,6 +4394,19 @@ } /** + * Tag hook handler for 'pre'. + */ + function renderPreTag( $text, $attribs ) { + // Backwards-compatibility hack + $content = StringUtils::delimiterReplace( '', '', '$1', $text, 'i' ); + + $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' ); + return wfOpenElement( 'pre', $attribs ) . + Xml::escapeTagsOnly( $content ) . + '
        '; + } + + /** * Renders an image gallery from a text with one line per image. * text labels may be given by using |-style alternative text. E.g. * Image:one.jpg|The number "1" @@ -3194,30 +4414,48 @@ * given as text will return the HTML of a gallery with two images, * labeled 'The number "1"' and * 'A tree'. - * - * @static */ - function renderImageGallery( $text ) { - # Setup the parser - global $wgUser, $wgTitle; - $parserOptions = ParserOptions::newFromUser( $wgUser ); - $localParser = new Parser(); - - global $wgLinkCache; + function renderImageGallery( $text, $params ) { $ig = new ImageGallery(); + $ig->setContextTitle( $this->mTitle ); $ig->setShowBytes( false ); $ig->setShowFilename( false ); - $lines = explode( "\n", $text ); + $ig->setParser( $this ); + $ig->setHideBadImages(); + $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) ); + $ig->useSkin( $this->mOptions->getSkin() ); + $ig->mRevisionId = $this->mRevisionId; + + if( isset( $params['caption'] ) ) { + $caption = $params['caption']; + $caption = htmlspecialchars( $caption ); + $caption = $this->replaceInternalLinks( $caption ); + $ig->setCaptionHtml( $caption ); + } + if( isset( $params['perrow'] ) ) { + $ig->setPerRow( $params['perrow'] ); + } + if( isset( $params['widths'] ) ) { + $ig->setWidths( $params['widths'] ); + } + if( isset( $params['heights'] ) ) { + $ig->setHeights( $params['heights'] ); + } + + wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) ); + $lines = explode( "\n", $text ); foreach ( $lines as $line ) { # match lines like these: # Image:someimage.jpg|This is some image + $matches = array(); preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches ); # Skip empty lines if ( count( $matches ) == 0 ) { continue; } - $nt = Title::newFromURL( $matches[1] ); + $tp = Title::newFromText( $matches[1] ); + $nt =& $tp; if( is_null( $nt ) ) { # Bogus title. Ignore these so we don't bomb out later. continue; @@ -3228,24 +4466,70 @@ $label = ''; } - $html = $localParser->parse( $label , $wgTitle, $parserOptions ); - $html = $html->mText; + $pout = $this->parse( $label, + $this->mTitle, + $this->mOptions, + false, // Strip whitespace...? + false // Don't clear state! + ); + $html = $pout->getText(); + + $ig->add( $nt, $html ); - $ig->add( new Image( $nt ), $html ); - $wgLinkCache->addImageLinkObj( $nt ); + # Only add real images (bug #5586) + if ( $nt->getNamespace() == NS_IMAGE ) { + $this->mOutput->addImage( $nt->getDBkey() ); + } } return $ig->toHTML(); } + function getImageParams( $handler ) { + if ( $handler ) { + $handlerClass = get_class( $handler ); + } else { + $handlerClass = ''; + } + if ( !isset( $this->mImageParams[$handlerClass] ) ) { + // Initialise static lists + static $internalParamNames = array( + 'horizAlign' => array( 'left', 'right', 'center', 'none' ), + 'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', + 'bottom', 'text-bottom' ), + 'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless', + 'upright', 'border' ), + ); + static $internalParamMap; + if ( !$internalParamMap ) { + $internalParamMap = array(); + foreach ( $internalParamNames as $type => $names ) { + foreach ( $names as $name ) { + $magicName = str_replace( '-', '_', "img_$name" ); + $internalParamMap[$magicName] = array( $type, $name ); + } + } + } + + // Add handler params + $paramMap = $internalParamMap; + if ( $handler ) { + $handlerParamMap = $handler->getParamMap(); + foreach ( $handlerParamMap as $magic => $paramName ) { + $paramMap[$magic] = array( 'handler', $paramName ); + } + } + $this->mImageParams[$handlerClass] = $paramMap; + $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) ); + } + return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ); + } + /** * Parse image options text and use it to make an image */ - function makeImage( &$nt, $options ) { - global $wgContLang, $wgUseImageResize; - global $wgUser, $wgThumbLimits; - - $align = ''; - + function makeImage( $title, $options ) { + # @TODO: let the MediaHandler specify its transform parameters + # # Check if the options text is of the form "options|alt text" # Options are: # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang @@ -3255,310 +4539,390 @@ # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox # * center center the image # * framed Keep original image size, no magnify-button. + # * frameless like 'thumb' but without a frame. Keeps user preferences for width + # * upright reduce width for upright images, rounded to full __0 px + # * border draw a 1px border around the image + # vertical-align values (no % or length right now): + # * baseline + # * sub + # * super + # * top + # * text-top + # * middle + # * bottom + # * text-bottom + + $parts = array_map( 'trim', explode( '|', $options) ); + $sk = $this->mOptions->getSkin(); - $part = explode( '|', $options); + # Give extensions a chance to select the file revision for us + $skip = $time = false; + wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time ) ); - $mwThumb =& MagicWord::get( MAG_IMG_THUMBNAIL ); - $mwLeft =& MagicWord::get( MAG_IMG_LEFT ); - $mwRight =& MagicWord::get( MAG_IMG_RIGHT ); - $mwNone =& MagicWord::get( MAG_IMG_NONE ); - $mwWidth =& MagicWord::get( MAG_IMG_WIDTH ); - $mwCenter =& MagicWord::get( MAG_IMG_CENTER ); - $mwFramed =& MagicWord::get( MAG_IMG_FRAMED ); - $caption = ''; + if ( $skip ) { + return $sk->makeLinkObj( $title ); + } - $width = $height = $framed = $thumb = false; - $manual_thumb = '' ; + # Get parameter map + $file = wfFindFile( $title, $time ); + $handler = $file ? $file->getHandler() : false; - foreach( $part as $key => $val ) { - $val_parts = explode ( '=' , $val , 2 ) ; - $left_part = array_shift ( $val_parts ) ; - if ( $wgUseImageResize && ! is_null( $mwThumb->matchVariableStartToEnd($val) ) ) { - $thumb=true; - } elseif ( $wgUseImageResize && count ( $val_parts ) == 1 && ! is_null( $mwThumb->matchVariableStartToEnd($left_part) ) ) { - # use manually specified thumbnail - $thumb=true; - $manual_thumb = array_shift ( $val_parts ) ; - } elseif ( ! is_null( $mwRight->matchVariableStartToEnd($val) ) ) { - # remember to set an alignment, don't render immediately - $align = 'right'; - } elseif ( ! is_null( $mwLeft->matchVariableStartToEnd($val) ) ) { - # remember to set an alignment, don't render immediately - $align = 'left'; - } elseif ( ! is_null( $mwCenter->matchVariableStartToEnd($val) ) ) { - # remember to set an alignment, don't render immediately - $align = 'center'; - } elseif ( ! is_null( $mwNone->matchVariableStartToEnd($val) ) ) { - # remember to set an alignment, don't render immediately - $align = 'none'; - } elseif ( $wgUseImageResize && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) { - wfDebug( "MAG_IMG_WIDTH match: $match\n" ); - # $match is the image width in pixels - if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $match, $m ) ) { - $width = intval( $m[1] ); - $height = intval( $m[2] ); - } else { - $width = intval($match); + list( $paramMap, $mwArray ) = $this->getImageParams( $handler ); + + # Process the input parameters + $caption = ''; + $params = array( 'frame' => array(), 'handler' => array(), + 'horizAlign' => array(), 'vertAlign' => array() ); + foreach( $parts as $part ) { + list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part ); + if ( isset( $paramMap[$magicName] ) ) { + list( $type, $paramName ) = $paramMap[$magicName]; + $params[$type][$paramName] = $value; + + // Special case; width and height come in one variable together + if( $type == 'handler' && $paramName == 'width' ) { + $m = array(); + if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $value, $m ) ) { + $params[$type]['width'] = intval( $m[1] ); + $params[$type]['height'] = intval( $m[2] ); + } else { + $params[$type]['width'] = intval( $value ); + } } - } elseif ( ! is_null( $mwFramed->matchVariableStartToEnd($val) ) ) { - $framed=true; } else { - $caption = $val; + $caption = $part; } } + + # Process alignment parameters + if ( $params['horizAlign'] ) { + $params['frame']['align'] = key( $params['horizAlign'] ); + } + if ( $params['vertAlign'] ) { + $params['frame']['valign'] = key( $params['vertAlign'] ); + } + + # Validate the handler parameters + if ( $handler ) { + foreach ( $params['handler'] as $name => $value ) { + if ( !$handler->validateParam( $name, $value ) ) { + unset( $params['handler'][$name] ); + } + } + } + # Strip bad stuff out of the alt text $alt = $this->replaceLinkHoldersText( $caption ); + + # make sure there are no placeholders in thumbnail attributes + # that are later expanded to html- so expand them now and + # remove the tags + $alt = $this->mStripState->unstripBoth( $alt ); $alt = Sanitizer::stripAllTags( $alt ); + $params['frame']['alt'] = $alt; + $params['frame']['caption'] = $caption; + # Linker does the rest - $sk =& $this->mOptions->getSkin(); - return $sk->makeImageLinkObj( $nt, $caption, $alt, $align, $width, $height, $framed, $thumb, $manual_thumb ); + $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'] ); + + # Give the handler a chance to modify the parser object + if ( $handler ) { + $handler->parserTransformHook( $this, $file ); + } + + return $ret; } /** - * Set a flag in the output object indicating that the content is dynamic and + * Set a flag in the output object indicating that the content is dynamic and * shouldn't be cached. */ function disableCache() { + wfDebug( "Parser output marked as uncacheable.\n" ); $this->mOutput->mCacheTime = -1; } - - /** + + /**#@+ * Callback from the Sanitizer for expanding items found in HTML attribute * values, so they can be safely tested and escaped. * @param string $text * @param array $args * @return string - * @access private + * @private */ function attributeStripCallback( &$text, $args ) { $text = $this->replaceVariables( $text, $args ); - $text = $this->unstripForHTML( $text ); + $text = $this->mStripState->unstripBoth( $text ); return $text; } - - function unstripForHTML( $text ) { - $text = $this->unstrip( $text, $this->mStripState ); - $text = $this->unstripNoWiki( $text, $this->mStripState ); - return $text; - } -} -/** - * @todo document - * @package MediaWiki - */ -class ParserOutput -{ - var $mText, $mLanguageLinks, $mCategoryLinks, $mContainsOldMagic; - var $mCacheTime; # Timestamp on this article, or -1 for uncacheable. Used in ParserCache. - var $mVersion; # Compatibility check - var $mTitleText; # title text of the chosen language variant - - function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(), - $containsOldMagic = false, $titletext = '' ) - { - $this->mText = $text; - $this->mLanguageLinks = $languageLinks; - $this->mCategoryLinks = $categoryLinks; - $this->mContainsOldMagic = $containsOldMagic; - $this->mCacheTime = ''; - $this->mVersion = MW_PARSER_VERSION; - $this->mTitleText = $titletext; - } - - function getText() { return $this->mText; } - function getLanguageLinks() { return $this->mLanguageLinks; } - function getCategoryLinks() { return array_keys( $this->mCategoryLinks ); } - function getCacheTime() { return $this->mCacheTime; } - function getTitleText() { return $this->mTitleText; } - function containsOldMagic() { return $this->mContainsOldMagic; } - function setText( $text ) { return wfSetVar( $this->mText, $text ); } - function setLanguageLinks( $ll ) { return wfSetVar( $this->mLanguageLinks, $ll ); } - function setCategoryLinks( $cl ) { return wfSetVar( $this->mCategoryLinks, $cl ); } - function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); } - function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); } - function setTitleText( $t ) { return wfSetVar ($this->mTitleText, $t); } - - function addCategoryLink( $c ) { $this->mCategoryLinks[$c] = 1; } - - function merge( $other ) { - $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $other->mLanguageLinks ); - $this->mCategoryLinks = array_merge( $this->mCategoryLinks, $this->mLanguageLinks ); - $this->mContainsOldMagic = $this->mContainsOldMagic || $other->mContainsOldMagic; + /**#@-*/ + + /**#@+ + * Accessor/mutator + */ + function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); } + function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); } + function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); } + /**#@-*/ + + /**#@+ + * Accessor + */ + function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); } + /**#@-*/ + + + /** + * Break wikitext input into sections, and either pull or replace + * some particular section's text. + * + * External callers should use the getSection and replaceSection methods. + * + * @param $text Page wikitext + * @param $section Numbered section. 0 pulls the text before the first + * heading; other numbers will pull the given section + * along with its lower-level subsections. + * @param $mode One of "get" or "replace" + * @param $newtext Replacement text for section data. + * @return string for "get", the extracted section text. + * for "replace", the whole page with the section replaced. + */ + private function extractSections( $text, $section, $mode, $newtext='' ) { + # I.... _hope_ this is right. + # Otherwise, sometimes we don't have things initialized properly. + $this->clearState(); + + # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML + # comments to be stripped as well) + $stripState = new StripState; + + $oldOutputType = $this->mOutputType; + $oldOptions = $this->mOptions; + $this->mOptions = new ParserOptions(); + $this->setOutputType( OT_WIKI ); + + $striptext = $this->strip( $text, $stripState, true ); + + $this->setOutputType( $oldOutputType ); + $this->mOptions = $oldOptions; + + # now that we can be sure that no pseudo-sections are in the source, + # split it up by section + $uniq = preg_quote( $this->uniqPrefix(), '/' ); + $comment = "(?:$uniq-!--.*?QINU\x07)"; + $secs = preg_split( + "/ + ( + ^ + (?:$comment|<\/?noinclude>)* # Initial comments will be stripped + (=+) # Should this be limited to 6? + .+? # Section title... + \\2 # Ending = count must match start + (?:$comment|<\/?noinclude>|[ \\t]+)* # Trailing whitespace ok + $ + | + + .*? + <\/h\\3\s*> + ) + /mix", + $striptext, -1, + PREG_SPLIT_DELIM_CAPTURE); + + if( $mode == "get" ) { + if( $section == 0 ) { + // "Section 0" returns the content before any other section. + $rv = $secs[0]; + } else { + //track missing section, will replace if found. + $rv = $newtext; + } + } elseif( $mode == "replace" ) { + if( $section == 0 ) { + $rv = $newtext . "\n\n"; + $remainder = true; + } else { + $rv = $secs[0]; + $remainder = false; + } + } + $count = 0; + $sectionLevel = 0; + for( $index = 1; $index < count( $secs ); ) { + $headerLine = $secs[$index++]; + if( $secs[$index] ) { + // A wiki header + $headerLevel = strlen( $secs[$index++] ); + } else { + // An HTML header + $index++; + $headerLevel = intval( $secs[$index++] ); + } + $content = $secs[$index++]; + + $count++; + if( $mode == "get" ) { + if( $count == $section ) { + $rv = $headerLine . $content; + $sectionLevel = $headerLevel; + } elseif( $count > $section ) { + if( $sectionLevel && $headerLevel > $sectionLevel ) { + $rv .= $headerLine . $content; + } else { + // Broke out to a higher-level section + break; + } + } + } elseif( $mode == "replace" ) { + if( $count < $section ) { + $rv .= $headerLine . $content; + } elseif( $count == $section ) { + $rv .= $newtext . "\n\n"; + $sectionLevel = $headerLevel; + } elseif( $count > $section ) { + if( $headerLevel <= $sectionLevel ) { + // Passed the section's sub-parts. + $remainder = true; + } + if( $remainder ) { + $rv .= $headerLine . $content; + } + } + } + } + if (is_string($rv)) + # reinsert stripped tags + $rv = trim( $stripState->unstripBoth( $rv ) ); + + return $rv; } /** - * Return true if this cached output object predates the global or - * per-article cache invalidation timestamps, or if it comes from - * an incompatible older version. + * This function returns the text of a section, specified by a number ($section). + * A section is text under a heading like == Heading == or \Heading\, or + * the first section before any such heading (section 0). * - * @param string $touched the affected article's last touched timestamp - * @return bool - * @access public + * If a section contains subsections, these are also returned. + * + * @param $text String: text to look in + * @param $section Integer: section number + * @param $deftext: default to return if section is not found + * @return string text of the requested section */ - function expired( $touched ) { - global $wgCacheEpoch; - return $this->getCacheTime() == -1 || // parser says it's uncacheable - $this->getCacheTime() <= $touched || - $this->getCacheTime() <= $wgCacheEpoch || - !isset( $this->mVersion ) || - version_compare( $this->mVersion, MW_PARSER_VERSION, "lt" ); + public function getSection( $text, $section, $deftext='' ) { + return $this->extractSections( $text, $section, "get", $deftext ); } -} -/** - * Set options of the Parser - * @todo document - * @package MediaWiki - */ -class ParserOptions -{ - # All variables are private - var $mUseTeX; # Use texvc to expand tags - var $mUseDynamicDates; # Use DateFormatter to format dates - var $mInterwikiMagic; # Interlanguage links are removed and returned in an array - var $mAllowExternalImages; # Allow external images inline - var $mSkin; # Reference to the preferred skin - var $mDateFormat; # Date format index - var $mEditSection; # Create "edit section" links - var $mNumberHeadings; # Automatically number headings - var $mAllowSpecialInclusion; # Allow inclusion of special pages - - function getUseTeX() { return $this->mUseTeX; } - function getUseDynamicDates() { return $this->mUseDynamicDates; } - function getInterwikiMagic() { return $this->mInterwikiMagic; } - function getAllowExternalImages() { return $this->mAllowExternalImages; } - function &getSkin() { return $this->mSkin; } - function getDateFormat() { return $this->mDateFormat; } - function getEditSection() { return $this->mEditSection; } - function getNumberHeadings() { return $this->mNumberHeadings; } - function getAllowSpecialInclusion() { return $this->mAllowSpecialInclusion; } - - - function setUseTeX( $x ) { return wfSetVar( $this->mUseTeX, $x ); } - function setUseDynamicDates( $x ) { return wfSetVar( $this->mUseDynamicDates, $x ); } - function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); } - function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); } - function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); } - function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); } - function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); } - function setAllowSpecialInclusion( $x ) { return wfSetVar( $this->mAllowSpecialInclusion, $x ); } + public function replaceSection( $oldtext, $section, $text ) { + return $this->extractSections( $oldtext, $section, "replace", $text ); + } - function setSkin( &$x ) { $this->mSkin =& $x; } + /** + * Get the timestamp associated with the current revision, adjusted for + * the default server-local timestamp + */ + function getRevisionTimestamp() { + if ( is_null( $this->mRevisionTimestamp ) ) { + wfProfileIn( __METHOD__ ); + global $wgContLang; + $dbr = wfGetDB( DB_SLAVE ); + $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', + array( 'rev_id' => $this->mRevisionId ), __METHOD__ ); - function ParserOptions() { - global $wgUser; - $this->initialiseFromUser( $wgUser ); + // Normalize timestamp to internal MW format for timezone processing. + // This has the added side-effect of replacing a null value with + // the current time, which gives us more sensible behavior for + // previews. + $timestamp = wfTimestamp( TS_MW, $timestamp ); + + // The cryptic '' timezone parameter tells to use the site-default + // timezone offset instead of the user settings. + // + // Since this value will be saved into the parser cache, served + // to other users, and potentially even used inside links and such, + // it needs to be consistent for all visitors. + $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' ); + + wfProfileOut( __METHOD__ ); + } + return $this->mRevisionTimestamp; } /** - * Get parser options - * @static + * Mutator for $mDefaultSort + * + * @param $sort New value */ - function newFromUser( &$user ) { - $popts = new ParserOptions; - $popts->initialiseFromUser( $user ); - return $popts; + public function setDefaultSort( $sort ) { + $this->mDefaultSort = $sort; } - /** Get user options */ - function initialiseFromUser( &$userInput ) { - global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages, - $wgAllowSpecialInclusion; - $fname = 'ParserOptions::initialiseFromUser'; - wfProfileIn( $fname ); - if ( !$userInput ) { - $user = new User; - $user->setLoaded( true ); + /** + * Accessor for $mDefaultSort + * Will use the title/prefixed title if none is set + * + * @return string + */ + public function getDefaultSort() { + if( $this->mDefaultSort !== false ) { + return $this->mDefaultSort; } else { - $user =& $userInput; + return $this->mTitle->getNamespace() == NS_CATEGORY + ? $this->mTitle->getText() + : $this->mTitle->getPrefixedText(); } - - $this->mUseTeX = $wgUseTeX; - $this->mUseDynamicDates = $wgUseDynamicDates; - $this->mInterwikiMagic = $wgInterwikiMagic; - $this->mAllowExternalImages = $wgAllowExternalImages; - wfProfileIn( $fname.'-skin' ); - $this->mSkin =& $user->getSkin(); - wfProfileOut( $fname.'-skin' ); - $this->mDateFormat = $user->getOption( 'date' ); - $this->mEditSection = true; - $this->mNumberHeadings = $user->getOption( 'numberheadings' ); - $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion; - wfProfileOut( $fname ); } } /** - * Callback function used by Parser::replaceLinkHolders() - * to substitute link placeholders. + * @todo document, briefly. + * @addtogroup Parser */ -function &wfOutputReplaceMatches( $matches ) { - global $wgOutputReplace; - return $wgOutputReplace[$matches[1]]; -} - -/** - * Return the total number of articles - */ -function wfNumberOfArticles() { - global $wgNumberOfArticles; +class OnlyIncludeReplacer { + var $output = ''; - wfLoadSiteStats(); - return $wgNumberOfArticles; + function replace( $matches ) { + if ( substr( $matches[1], -1 ) == "\n" ) { + $this->output .= substr( $matches[1], 0, -1 ); + } else { + $this->output .= $matches[1]; + } + } } /** - * Return the number of files + * @todo document, briefly. + * @addtogroup Parser */ -function wfNumberOfFiles() { - $fname = 'Parser::wfNumberOfFiles'; +class StripState { + var $general, $nowiki; - wfProfileIn( $fname ); - $dbr =& wfGetDB( DB_SLAVE ); - $res = $dbr->selectField('image', 'COUNT(*)', array(), $fname ); - wfProfileOut( $fname ); + function __construct() { + $this->general = new ReplacementArray; + $this->nowiki = new ReplacementArray; + } - return $res; -} + function unstripGeneral( $text ) { + wfProfileIn( __METHOD__ ); + $text = $this->general->replace( $text ); + wfProfileOut( __METHOD__ ); + return $text; + } -/** - * Get various statistics from the database - * @private - */ -function wfLoadSiteStats() { - global $wgNumberOfArticles, $wgTotalViews, $wgTotalEdits; - $fname = 'wfLoadSiteStats'; - - if ( -1 != $wgNumberOfArticles ) return; - $dbr =& wfGetDB( DB_SLAVE ); - $s = $dbr->selectRow( 'site_stats', - array( 'ss_total_views', 'ss_total_edits', 'ss_good_articles' ), - array( 'ss_row_id' => 1 ), $fname - ); - - if ( $s === false ) { - return; - } else { - $wgTotalViews = $s->ss_total_views; - $wgTotalEdits = $s->ss_total_edits; - $wgNumberOfArticles = $s->ss_good_articles; + function unstripNoWiki( $text ) { + wfProfileIn( __METHOD__ ); + $text = $this->nowiki->replace( $text ); + wfProfileOut( __METHOD__ ); + return $text; } -} -/** - * Escape html tags - * Basicly replacing " > and < with HTML entities ( ", >, <) - * - * @param string $in Text that might contain HTML tags - * @return string Escaped string - */ -function wfEscapeHTMLTagsOnly( $in ) { - return str_replace( - array( '"', '>', '<' ), - array( '"', '>', '<' ), - $in ); + function unstripBoth( $text ) { + wfProfileIn( __METHOD__ ); + $text = $this->general->replace( $text ); + $text = $this->nowiki->replace( $text ); + wfProfileOut( __METHOD__ ); + return $text; + } } - -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ParserCache.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ParserCache.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ParserCache.php 2005-07-03 03:27:43.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ParserCache.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,31 +1,44 @@ mMemc =& $memCached; } function getKey( &$article, &$user ) { - global $wgDBname, $action; + global $action; $hash = $user->getPageRenderingHash(); + if( !$article->mTitle->quickUserCan( 'edit' ) ) { + // section edit links are suppressed even if the user has them on + $edit = '!edit=0'; + } else { + $edit = ''; + } $pageid = intval( $article->getID() ); $renderkey = (int)($action == 'render'); - $key = "$wgDBname:pcache:idhash:$pageid-$renderkey!$hash"; + $key = wfMemcKey( 'pcache', 'idhash', "$pageid-$renderkey!$hash$edit" ); return $key; } @@ -38,8 +51,6 @@ $fname = 'ParserCache::get'; wfProfileIn( $fname ); - $hash = $user->getPageRenderingHash(); - $pageid = intval( $article->getID() ); $key = $this->getKey( $article, $user ); wfDebug( "Trying parser cache $key\n" ); @@ -60,8 +71,10 @@ } $this->mMemc->delete( $key ); $value = false; - } else { + if ( isset( $value->mTimestamp ) ) { + $article->mTimestamp = $value->mTimestamp; + } wfIncrStats( "pcache_hit" ); } } else { @@ -75,20 +88,32 @@ } function save( $parserOutput, &$article, &$user ){ + global $wgParserCacheExpireTime; $key = $this->getKey( $article, $user ); - $now = wfTimestampNow(); - $parserOutput->setCacheTime( $now ); - $parserOutput->mText .= "\n\n"; - wfDebug( "Saved in parser cache with key $key and timestamp $now\n" ); - if( $parserOutput->containsOldMagic() ){ - $expire = 3600; # 1 hour + if( $parserOutput->getCacheTime() != -1 ) { + + $now = wfTimestampNow(); + $parserOutput->setCacheTime( $now ); + + // Save the timestamp so that we don't have to load the revision row on view + $parserOutput->mTimestamp = $article->getTimestamp(); + + $parserOutput->mText .= "\n\n"; + wfDebug( "Saved in parser cache with key $key and timestamp $now\n" ); + + if( $parserOutput->containsOldMagic() ){ + $expire = 3600; # 1 hour + } else { + $expire = $wgParserCacheExpireTime; + } + $this->mMemc->set( $key, $parserOutput, $expire ); + } else { - $expire = 86400; # 1 day + wfDebug( "Parser output was marked as uncacheable and has not been saved.\n" ); } - $this->mMemc->set( $key, $parserOutput, $expire ); } + } -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ProxyTools.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ProxyTools.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/ProxyTools.php 2005-07-05 17:22:10.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/ProxyTools.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,93 +1,263 @@ $curIP ) { - if ( array_key_exists( $curIP, $trustedProxies ) ) { - if ( isset( $ipchain[$i + 1] ) && wfIsIPPublic( $ipchain[$i + 1] ) ) { - $ip = $ipchain[$i + 1]; - } - } else { - break; + # Append XFF on to $ipchain + $forwardedFor = wfGetForwardedFor(); + if ( isset( $forwardedFor ) ) { + $xff = array_map( 'trim', explode( ',', $forwardedFor ) ); + $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]; } + } else { + break; } } + wfDebug( "IP: $ip\n" ); + $wgIP = $ip; return $ip; } -/** */ -function wfIP2Unsigned( $ip ) { - $n = ip2long( $ip ); - if ( $n == -1 ) { - $n = false; - } elseif ( $n < 0 ) { - $n += pow( 2, 32 ); +/** + * Checks if an IP is a trusted proxy providor + * Useful to tell if X-Fowarded-For data is possibly bogus + * Squid cache servers for the site and AOL are whitelisted + * @param string $ip + * @return bool + */ +function wfIsTrustedProxy( $ip ) { + global $wgSquidServers, $wgSquidServersNoPurge; + + if ( in_array( $ip, $wgSquidServers ) || + in_array( $ip, $wgSquidServersNoPurge ) || + wfIsAOLProxy( $ip ) + ) { + $trusted = true; + } else { + $trusted = false; } - return $n; + wfRunHooks( 'IsTrustedProxy', array( &$ip, &$trusted ) ); + return $trusted; } /** - * Determine if an IP address really is an IP address, and if it is public, - * i.e. not RFC 1918 or similar + * Forks processes to scan the originating IP for an open proxy server + * MemCached can be used to skip IPs that have already been scanned */ -function wfIsIPPublic( $ip ) { - $n = wfIP2Unsigned( $ip ); - if ( !$n ) { +function wfProxyCheck() { + global $wgBlockOpenProxies, $wgProxyPorts, $wgProxyScriptPath; + global $wgUseMemCached, $wgMemc, $wgProxyMemcExpiry; + global $wgProxyKey; + + if ( !$wgBlockOpenProxies ) { + return; + } + + $ip = wfGetIP(); + + # Get MemCached key + $skip = false; + if ( $wgUseMemCached ) { + $mcKey = wfMemcKey( 'proxy', 'ip', $ip ); + $mcValue = $wgMemc->get( $mcKey ); + if ( $mcValue ) { + $skip = true; + } + } + + # Fork the processes + if ( !$skip ) { + $title = SpecialPage::getTitleFor( 'Blockme' ); + $iphash = md5( $ip . $wgProxyKey ); + $url = $title->getFullURL( 'ip='.$iphash ); + + foreach ( $wgProxyPorts as $port ) { + $params = implode( ' ', array( + escapeshellarg( $wgProxyScriptPath ), + escapeshellarg( $ip ), + escapeshellarg( $port ), + escapeshellarg( $url ) + )); + exec( "php $params &>/dev/null &" ); + } + # Set MemCached key + if ( $wgUseMemCached ) { + $wgMemc->set( $mcKey, 1, $wgProxyMemcExpiry ); + } + } +} + +/** + * Convert a network specification in CIDR notation to an integer network and a number of bits + * @return array(string, int) + */ +function wfParseCIDR( $range ) { + return IP::parseCIDR( $range ); +} + +/** + * Check if an IP address is in the local proxy list + * @return bool + */ +function wfIsLocallyBlockedProxy( $ip ) { + global $wgProxyList; + $fname = 'wfIsLocallyBlockedProxy'; + + if ( !$wgProxyList ) { return false; } + wfProfileIn( $fname ); + + if ( !is_array( $wgProxyList ) ) { + # Load from the specified file + $wgProxyList = array_map( 'trim', file( $wgProxyList ) ); + } - static $privateRanges = false; - if ( !$privateRanges ) { - $privateRanges = array( - array( '10.0.0.0', '10.255.255.255' ), # RFC 1918 (private) - array( '172.16.0.0', '172.31.255.255' ), # " - array( '192.168.0.0', '192.168.255.255' ), # " - array( '0.0.0.0', '0.255.255.255' ), # this network - array( '127.0.0.0', '127.255.255.255' ), # loopback - ); - } - - foreach ( $privateRanges as $r ) { - $start = wfIP2Unsigned( $r[0] ); - $end = wfIP2Unsigned( $r[1] ); - if ( $n >= $start && $n <= $end ) { - return false; + if ( !is_array( $wgProxyList ) ) { + $ret = false; + } elseif ( array_search( $ip, $wgProxyList ) !== false ) { + $ret = true; + } elseif ( array_key_exists( $ip, $wgProxyList ) ) { + # Old-style flipped proxy list + $ret = true; + } else { + $ret = false; + } + 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 ); } } - return true; + + $hex = IP::toHex( $ip ); + foreach ( $parsedRanges as $range ) { + if ( $hex >= $range[0] && $hex <= $range[1] ) { + return true; + } + } + return false; } - -?> + + + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/QueryPage.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/QueryPage.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/QueryPage.php 2005-08-14 23:03:19.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/QueryPage.php 2007-09-04 15:29:47.000000000 -0400 @@ -1,50 +1,85 @@ listoutput; + * + * @param bool $bool + */ + function setListoutput( $bool ) { + $this->listoutput = $bool; + } /** * Subclasses return their name here. Make sure the name is also @@ -56,6 +91,15 @@ } /** + * Return title object representing this page + * + * @return Title + */ + function getTitle() { + return SpecialPage::getTitleFor( $this->getName() ); + } + + /** * Subclasses return an SQL query here. * * Note that the query itself should return the following four columns: @@ -97,6 +141,18 @@ } /** + * Whether or not the output of the page in question is retrived from + * the database cache. + * + * @return bool + */ + function isCached() { + global $wgMiserMode; + + return $this->isExpensive() && $wgMiserMode; + } + + /** * Sometime we dont want to build rss / atom feeds. */ function isSyndicated() { @@ -119,7 +175,7 @@ function getPageHeader( ) { return ''; } - + /** * If using extra form wheely-dealies, return a set of parameters here * as an associative array. They will be encoded and added to the paging @@ -129,7 +185,7 @@ function linkParameters() { return array(); } - + /** * Some special pages (for example SpecialListusers) might not return the * current object formatted, but return the previous one instead. @@ -143,16 +199,16 @@ /** * Clear the cache and save new results */ - function recache( $ignoreErrors = true ) { + function recache( $limit, $ignoreErrors = true ) { $fname = get_class($this) . '::recache'; - $dbw =& wfGetDB( DB_MASTER ); - $dbr =& wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) ); + $dbw = wfGetDB( DB_MASTER ); + $dbr = wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) ); if ( !$dbw || !$dbr ) { return false; } $querycache = $dbr->tableName( 'querycache' ); - + if ( $ignoreErrors ) { $ignoreW = $dbw->ignoreErrors( true ); $ignoreR = $dbr->ignoreErrors( true ); @@ -161,7 +217,10 @@ # Clear out any old cached data $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname ); # Do query - $res = $dbr->query( $this->getSQL() . $this->getOrder() . $dbr->limitResult( 1000,0 ), $fname ); + $sql = $this->getSQL() . $this->getOrder(); + if ($limit !== false) + $sql = $dbr->limitResult($sql, $limit, 0); + $res = $dbr->query($sql, $fname); $num = false; if ( $res ) { $num = $dbr->numRows( $res ); @@ -202,6 +261,11 @@ $dbw->ignoreErrors( $ignoreW ); $dbr->ignoreErrors( $ignoreR ); } + + # Update the querycache_info record for the page + $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname ); + $dbw->insert( 'querycache_info', array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ), $fname ); + } return $num; } @@ -215,88 +279,180 @@ * @param $shownavigation show navigation like "next 200"? */ function doQuery( $offset, $limit, $shownavigation=true ) { - global $wgUser, $wgOut, $wgLang, $wgRequest, $wgContLang; - global $wgMiserMode; + global $wgUser, $wgOut, $wgLang, $wgContLang; + + $this->offset = $offset; + $this->limit = $limit; $sname = $this->getName(); $fname = get_class($this) . '::doQuery'; - $sql = $this->getSQL(); - $dbr =& wfGetDB( DB_SLAVE ); - $dbw =& wfGetDB( DB_MASTER ); - $querycache = $dbr->tableName( 'querycache' ); + $dbr = wfGetDB( DB_SLAVE ); $wgOut->setSyndicated( $this->isSyndicated() ); - if ( $this->isExpensive() ) { - // Disabled recache parameter due to retry problems -- TS - if( $wgMiserMode ) { - $type = $dbr->strencode( $sname ); - $sql = - "SELECT qc_type as type, qc_namespace as namespace,qc_title as title, qc_value as value - FROM $querycache WHERE qc_type='$type'"; - $wgOut->addWikiText( wfMsg( 'perfcached' ) ); + if ( !$this->isCached() ) { + $sql = $this->getSQL(); + } else { + # Get the cached result + $querycache = $dbr->tableName( 'querycache' ); + $type = $dbr->strencode( $sname ); + $sql = + "SELECT qc_type as type, qc_namespace as namespace,qc_title as title, qc_value as value + FROM $querycache WHERE qc_type='$type'"; + + if( !$this->listoutput ) { + + # 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}';" ); + } else { + $cacheNotice = wfMsg( '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' ) ); + } + } + } - - $res = $dbr->query( $sql . $this->getOrder() . - $dbr->limitResult( $limit,$offset ), $fname ); + + $sql .= $this->getOrder(); + $sql = $dbr->limitResult($sql, $limit, $offset); + $res = $dbr->query( $sql ); $num = $dbr->numRows($res); + + $this->preprocessResults( $dbr, $res ); + + $wgOut->addHtml( XML::openElement( 'div', array('class' => 'mw-spcontent') ) ); + + # Top header and navigation + if( $shownavigation ) { + $wgOut->addHtml( $this->getPageHeader() ); + if( $num > 0 ) { + $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 . '

        ' ); + } 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' ) ); + return; + } + } - $sk = $wgUser->getSkin( ); + # 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 + $this->outputResults( $wgOut, + $wgUser->getSkin(), + $dbr, # Should use a ResultWrapper for this + $res, + $dbr->numRows( $res ), + $offset ); + + # Repeat the paging links at the bottom + if( $shownavigation ) { + $wgOut->addHtml( '

        ' . $paging . '

        ' ); + } - if($shownavigation) { - $wgOut->addHTML( $this->getPageHeader() ); - $top = wfShowingResults( $offset, $num); - $wgOut->addHTML( "

        {$top}\n" ); + $wgOut->addHtml( XML::closeElement( 'div' ) ); + + return $num; + } - # often disable 'next' link when we reach the end - if($num < $limit) { $atend = true; } else { $atend = false; } + /** + * Format and output report results using the given information plus + * OutputPage + * + * @param OutputPage $out OutputPage to print to + * @param Skin $skin User skin to use + * @param Database $dbr Database (read) connection to use + * @param int $res Result pointer + * @param int $num Number of available result rows + * @param int $offset Paging offset + */ + protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) { + global $wgContLang; + + if( $num > 0 ) { + $html = array(); + if( !$this->listoutput ) + $html[] = $this->openList( $offset ); - $sl = wfViewPrevNext( $offset, $limit , - $wgContLang->specialPage( $sname ), - wfArrayToCGI( $this->linkParameters() ), $atend ); - $wgOut->addHTML( "
        {$sl}

        \n" ); - } - if ( $num > 0 ) { - $s = "
          "; - - # Only read at most $num rows, because $res may contain the whole 1000 - for ( $i = 0; $i < $num && $obj = $dbr->fetchObject( $res ); $i++ ) { - $format = $this->formatResult( $sk, $obj ); - if ( $format ) { - $attr = ( isset ( $obj->usepatrol ) && $obj->usepatrol && - $obj->patrolled == 0 ) ? ' class="not-patrolled"' : ''; - $s .= "{$format}\n"; + # $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++ ) { + $line = $this->formatResult( $skin, $row ); + if( $line ) { + $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 ) + ? ' class="not-patrolled"' + : ''; + $html[] = $this->listoutput + ? $line + : "{$line}\n"; } } - - if($this->tryLastResult()) { - // flush the very last result - $obj = null; - $format = $this->formatResult( $sk, $obj ); - if( $format ) { - $attr = ( isset ( $obj->usepatrol ) && $obj->usepatrol && - $obj->patrolled == 0 ) ? ' class="not-patrolled"' : ''; - $s .= "{$format}\n"; + + # Flush the final result + if( $this->tryLastResult() ) { + $row = null; + $line = $this->formatResult( $skin, $row ); + if( $line ) { + $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 ) + ? ' class="not-patrolled"' + : ''; + $html[] = $this->listoutput + ? $line + : "{$line}\n"; } } - $dbr->freeResult( $res ); - $s .= '
        '; - $wgOut->addHTML( $s ); - } - if($shownavigation) { - $wgOut->addHTML( "

        {$sl}

        \n" ); + if( !$this->listoutput ) + $html[] = $this->closeList(); + + $html = $this->listoutput + ? $wgContLang->listToText( $html ) + : implode( '', $html ); + + $out->addHtml( $html ); } - return $num; + } + + 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 ) {} + + /** * Similar to above, but packaging in a syndicated feed instead of a web page */ - function doFeed( $class = '' ) { + function doFeed( $class = '', $limit = 50 ) { global $wgFeedClasses; - global $wgOut, $wgLanguageCode, $wgLang; + if( isset($wgFeedClasses[$class]) ) { $feed = new $wgFeedClasses[$class]( $this->feedTitle(), @@ -304,8 +460,9 @@ $this->feedUrl() ); $feed->outHeader(); - $dbr =& wfGetDB( DB_SLAVE ); - $sql = $this->getSQL() . $this->getOrder().$dbr->limitResult( 50, 0 ); + $dbr = wfGetDB( DB_SLAVE ); + $sql = $this->getSQL() . $this->getOrder(); + $sql = $dbr->limitResult( $sql, $limit, 0 ); $res = $dbr->query( $sql, 'QueryPage::doFeed' ); while( $obj = $dbr->fetchObject( $res ) ) { $item = $this->feedResult( $obj ); @@ -328,14 +485,9 @@ if( !isset( $row->title ) ) { return NULL; } - $title = Title::MakeTitle( IntVal( $row->namespace ), $row->title ); + $title = Title::MakeTitle( intval( $row->namespace ), $row->title ); if( $title ) { - if( isset( $row->timestamp ) ) { - $date = $row->timestamp; - } else { - $date = ''; - } - + $date = isset( $row->timestamp ) ? $row->timestamp : ''; $comments = ''; if( $title ) { $talkpage = $title->getTalkPage(); @@ -355,24 +507,18 @@ } function feedItemDesc( $row ) { - return isset( $row->comment ) - ? htmlspecialchars( $row->comment ) - : ''; + return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : ''; } function feedItemAuthor( $row ) { - if( isset( $row->user_text ) ) { - return $row->user_text; - } else { - return ''; - } + return isset( $row->user_text ) ? $row->user_text : ''; } function feedTitle() { - global $wgLanguageCode, $wgSitename, $wgLang; + global $wgContLanguageCode, $wgSitename; $page = SpecialPage::getPage( $this->getName() ); $desc = $page->getDescription(); - return "$wgSitename - $desc [$wgLanguageCode]"; + return "$wgSitename - $desc [$wgContLanguageCode]"; } function feedDesc() { @@ -380,26 +526,9 @@ } function feedUrl() { - global $wgLang; - $title = Title::MakeTitle( NS_SPECIAL, $this->getName() ); + $title = SpecialPage::getTitleFor( $this->getName() ); return $title->getFullURL(); } } -/** - * This is a subclass for very simple queries that are just looking for page - * titles that match some criteria. It formats each result item as a link to - * that page. - * - * @package MediaWiki - */ -class PageQueryPage extends QueryPage { - - function formatResult( $skin, $result ) { - global $wgContLang; - $nt = Title::makeTitle( $result->namespace, $result->title ); - return $skin->makeKnownLinkObj( $nt, $wgContLang->convert( $nt->getPrefixedText() ) ); - } -} -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/RawPage.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/RawPage.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/RawPage.php 2005-06-26 23:48:05.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/RawPage.php 2007-07-17 11:50:50.000000000 -0400 @@ -1,59 +1,99 @@ - * http://www.aulinx.de/ + * Copyright (C) 2004 Gabriel Wicke + * http://wikidev.net/ * Based on PageHistory and SpecialExport - * + * * License: GPL (http://www.gnu.org/copyleft/gpl.html) * - * @author Gabriel Wicke - * @package MediaWiki + * @author Gabriel Wicke */ -/** */ -require_once( 'Revision.php' ); - /** - * @todo document - * @package MediaWiki + * A simple method to retrieve the plain source of an article, + * using "action=raw" in the GET request string. */ class RawPage { + var $mArticle, $mTitle, $mRequest; + var $mOldId, $mGen, $mCharset; + var $mSmaxage, $mMaxage; + var $mContentType, $mExpandTemplates; - function RawPage( $article ) { + function __construct( &$article, $request = false ) { global $wgRequest, $wgInputEncoding, $wgSquidMaxage, $wgJsMimeType; + $allowedCTypes = array('text/x-wiki', $wgJsMimeType, 'text/css', 'application/x-zope-edit'); $this->mArticle =& $article; $this->mTitle =& $article->mTitle; - - $ctype = $wgRequest->getText( 'ctype' ); - $smaxage = $wgRequest->getInt( 'smaxage', $wgSquidMaxage ); - $maxage = $wgRequest->getInt( 'maxage', $wgSquidMaxage ); - $this->mOldId = $wgRequest->getInt( 'oldid' ); + + if ( $request === false ) { + $this->mRequest =& $wgRequest; + } else { + $this->mRequest = $request; + } + + $ctype = $this->mRequest->getVal( 'ctype' ); + $smaxage = $this->mRequest->getIntOrNull( 'smaxage', $wgSquidMaxage ); + $maxage = $this->mRequest->getInt( 'maxage', $wgSquidMaxage ); + $this->mExpandTemplates = $this->mRequest->getVal( 'templates' ) === 'expand'; + $this->mUseMessageCache = $this->mRequest->getBool( 'usemsgcache' ); + + $oldid = $this->mRequest->getInt( 'oldid' ); + switch ( $wgRequest->getText( 'direction' ) ) { + case 'next': + # output next revision, or nothing if there isn't one + 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 ) { + # get the current revision so we can get the penultimate one + $this->mArticle->getTouched(); + $oldid = $this->mArticle->mLatest; + } + $prev = $this->mTitle->getPreviousRevisionId( $oldid ); + $oldid = $prev ? $prev : -1 ; + break; + case 'cur': + $oldid = 0; + break; + } + $this->mOldId = $oldid; + # special case for 'generated' raw things: user css/js - $gen = $wgRequest->getText( 'gen' ); + $gen = $this->mRequest->getVal( 'gen' ); + if($gen == 'css') { $this->mGen = $gen; - if($smaxage == '') $smaxage = $wgSquidMaxage; + if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage; if($ctype == '') $ctype = 'text/css'; - } else if ($gen == 'js') { + } elseif ($gen == 'js') { $this->mGen = $gen; - if($smaxage == '') $smaxage = $wgSquidMaxage; + if( is_null( $smaxage ) ) $smaxage = $wgSquidMaxage; if($ctype == '') $ctype = $wgJsMimeType; } else { $this->mGen = false; } $this->mCharset = $wgInputEncoding; - $this->mSmaxage = $smaxage; + $this->mSmaxage = intval( $smaxage ); $this->mMaxage = $maxage; - if(empty($ctype) or !in_array($ctype, $allowedCTypes)) { + + // Output may contain user-specific data; vary for open sessions + $this->mPrivateCache = ( $this->mSmaxage == 0 ) || + ( session_id() != '' ); + + if ( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) { $this->mContentType = 'text/x-wiki'; } else { $this->mContentType = $ctype; } } - + function view() { - global $wgUser, $wgOut, $wgScript; + global $wgOut, $wgScript; if( isset( $_SERVER['SCRIPT_URL'] ) ) { # Normally we use PHP_SELF to get the URL to the script @@ -70,7 +110,9 @@ } else { $url = $_SERVER['PHP_SELF']; } - if( strcmp( $wgScript, $url ) ) { + + $ua = @$_SERVER['HTTP_USER_AGENT']; + if( strcmp( $wgScript, $url ) && strpos( $ua, 'MSIE' ) !== false ) { # 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 @@ -87,53 +129,95 @@ 'Raw pages must be accessed through the primary script entry point.' ); return; } - + header( "Content-type: ".$this->mContentType.'; charset='.$this->mCharset ); # allow the client to cache this for 24 hours - header( 'Cache-Control: s-maxage='.$this->mSmaxage.', max-age='.$this->mMaxage ); + $mode = $this->mPrivateCache ? 'private' : 'public'; + header( 'Cache-Control: '.$mode.', s-maxage='.$this->mSmaxage.', max-age='.$this->mMaxage ); + $text = $this->getRawText(); + + if( !wfRunHooks( 'RawPageViewBeforeOutput', array( &$this, &$text ) ) ) { + wfDebug( __METHOD__ . ': RawPageViewBeforeOutput hook broke raw page output.' ); + } + + echo $text; + $wgOut->disable(); + } + + function getRawText() { + global $wgUser, $wgOut, $wgRequest; if($this->mGen) { $sk = $wgUser->getSkin(); $sk->initPage($wgOut); if($this->mGen == 'css') { - echo $sk->getUserStylesheet(); + return $sk->getUserStylesheet(); } else if($this->mGen == 'js') { - echo $sk->getUserJs(); + return $sk->getUserJs(); } } else { - echo $this->getrawtext(); + return $this->getArticleText(); } - $wgOut->disable(); } - - function getrawtext () { - global $wgInputEncoding, $wgContLang; - $fname = 'RawPage::getrawtext'; - + + function getArticleText() { + $found = false; + $text = ''; if( $this->mTitle ) { - # Special case for MediaWiki: messages; we can hit the message cache. - if( $this->mTitle->getNamespace() == NS_MEDIAWIKI) { - $rawtext = wfMsgForContent( $this->mTitle->getDbkey() ); - return $rawtext; - } - - # else get it from the DB - $rev = Revision::newFromTitle( $this->mTitle, $this->mOldId ); - if( $rev ) { - $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() ); - header( 'Last-modified: ' . $lastmod ); - return $rev->getText(); + // If it's a MediaWiki message we can just hit the message cache + if ( $this->mUseMessageCache && $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $key = $this->mTitle->getDBkey(); + $text = wfMsgForContentNoTrans( $key ); + # If the message doesn't exist, return a blank + if( wfEmptyMsg( $key, $text ) ) + $text = ''; + $found = true; + } else { + // Get it from the DB + $rev = Revision::newFromTitle( $this->mTitle, $this->mOldId ); + if ( $rev ) { + $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() ); + header( "Last-modified: $lastmod" ); + $text = $rev->getText(); + $found = true; + } } } - + # Bad title or page does not exist - if( $this->mContentType == 'text/x-wiki' ) { + if( !$found && $this->mContentType == 'text/x-wiki' ) { # Don't return a 404 response for CSS or JavaScript; # 404s aren't generally cached and it would create # extra hits when user CSS/JS are on and the user doesn't # have the pages. header( "HTTP/1.0 404 Not Found" ); } - return ''; + + // Special-case for empty CSS/JS + // + // Internet Explorer for Mac handles empty files badly; + // particularly so when keep-alive is active. It can lead + // to long timeouts as it seems to sit there waiting for + // more data that never comes. + // + // Give it a comment... + if( strlen( $text ) == 0 && + ($this->mContentType == 'text/css' || + $this->mContentType == 'text/javascript' ) ) { + return "/* Empty */"; + } + + return $this->parseArticleText( $text ); + } + + function parseArticleText( $text ) { + if ( $text === '' ) + return ''; + else + if ( $this->mExpandTemplates ) { + global $wgParser; + return $wgParser->preprocess( $text, $this->mTitle, new ParserOptions() ); + } else + return $text; } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/RecentChange.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/RecentChange.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/RecentChange.php 2005-07-01 19:50:11.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/RecentChange.php 2007-08-20 23:57:54.000000000 -0400 @@ -1,20 +1,9 @@ loadFromRow( $row ); return $rc; } - /* static */ function newFromCurRow( $row, $rc_this_oldid = 0 ) + public static function newFromCurRow( $row, $rc_this_oldid = 0 ) { $rc = new RecentChange; $rc->loadFromCurRow( $row, $rc_this_oldid ); @@ -71,6 +62,49 @@ $rc->numberofWatchingusers = false; return $rc; } + + /** + * Obtain the recent change with a given rc_id value + * + * @param $rcid rc_id value to retrieve + * @return RecentChange + */ + public static function newFromId( $rcid ) { + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'recentchanges', '*', array( 'rc_id' => $rcid ), __METHOD__ ); + if( $res && $dbr->numRows( $res ) > 0 ) { + $row = $dbr->fetchObject( $res ); + $dbr->freeResult( $res ); + return self::newFromRow( $row ); + } else { + return NULL; + } + } + + /** + * Find the first recent change matching some specific conditions + * + * @param array $conds Array of conditions + * @param mixed $fname Override the method name in profiling/logs + * @return RecentChange + */ + public static function newFromConds( $conds, $fname = false ) { + if( $fname === false ) + $fname = __METHOD__; + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( + 'recentchanges', + '*', + $conds, + $fname + ); + if( $res instanceof ResultWrapper && $res->numRows() > 0 ) { + $row = $res->fetchObject(); + $res->free(); + return self::newFromRow( $row ); + } + return null; + } # Accessors @@ -107,7 +141,7 @@ global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPPort, $wgRC2UDPPrefix; $fname = 'RecentChange::save'; - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); if ( !is_array($this->mExtra) ) { $this->mExtra = array(); } @@ -117,29 +151,45 @@ $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'] ); + } + # Fixup database timestamps - $this->mAttribs['rc_timestamp']=$dbw->timestamp($this->mAttribs['rc_timestamp']); - $this->mAttribs['rc_cur_time']=$dbw->timestamp($this->mAttribs['rc_cur_time']); + $this->mAttribs['rc_timestamp'] = $dbw->timestamp($this->mAttribs['rc_timestamp']); + $this->mAttribs['rc_cur_time'] = $dbw->timestamp($this->mAttribs['rc_cur_time']); + $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 ) { + unset ( $this->mAttribs['rc_cur_id'] ); + } # Insert new row $dbw->insert( 'recentchanges', $this->mAttribs, $fname ); + # Set the ID + $this->mAttribs['rc_id'] = $dbw->insertId(); + # Update old rows, if necessary if ( $this->mAttribs['rc_type'] == RC_EDIT ) { - $oldid = $this->mAttribs['rc_last_oldid']; - $ns = $this->mAttribs['rc_namespace']; - $title = $this->mAttribs['rc_title']; $lastTime = $this->mExtra['lastTimestamp']; - $now = $this->mAttribs['rc_timestamp']; - $curId = $this->mAttribs['rc_cur_id']; + #$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 + # 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 @@ -152,8 +202,8 @@ } # Update rc_cur_time - $dbw->update( 'recentchanges', array( 'rc_cur_time' => $now ), - array( 'rc_cur_id' => $curId ), $fname ); + #$dbw->update( 'recentchanges', array( 'rc_cur_time' => $now ), + # array( 'rc_cur_id' => $curId ), $fname ); } # Notify external application via UDP @@ -165,36 +215,63 @@ socket_close( $conn ); } } - } - # Marks a certain row as patrolled - function markPatrolled( $rcid ) - { - $fname = 'RecentChange::markPatrolled'; + # E-mail notifications + global $wgUseEnotif; + if( $wgUseEnotif ) { + # this would be better as an extension hook + global $wgUser; + include_once( "UserMailer.php" ); + $enotif = new EmailNotification(); + $title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] ); + $enotif->notifyOnPageChange( $wgUser, $title, + $this->mAttribs['rc_timestamp'], + $this->mAttribs['rc_comment'], + $this->mAttribs['rc_minor'], + $this->mAttribs['rc_last_oldid'] ); + } - $dbw =& wfGetDB( DB_MASTER ); + # Notify extensions + wfRunHooks( 'RecentChange_save', array( &$this ) ); + } - $dbw->update( 'recentchanges', - array( /* SET */ + /** + * Mark a given change as patrolled + * + * @param mixed $change RecentChange or corresponding rc_id + */ + public static function markPatrolled( $change ) { + $rcid = $change instanceof RecentChange + ? $change->mAttribs['rc_id'] + : $change; + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( + 'recentchanges', + array( 'rc_patrolled' => 1 - ), array( /* WHERE */ + ), + array( 'rc_id' => $rcid - ), $fname + ), + __METHOD__ ); } # Makes an entry in the database corresponding to an edit - /*static*/ function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, + public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId, $lastTimestamp, $bot = "default", $ip = '', $oldSize = 0, $newSize = 0, $newId = 0) { - if ( $bot == 'default ' ) { - $bot = $user->isBot(); + + if ( $bot === 'default' ) { + $bot = $user->isAllowed( 'bot' ); } if ( !$ip ) { - global $wgIP; - $ip = empty( $wgIP ) ? '' : $wgIP; + $ip = wfGetIP(); + if ( !$ip ) { + $ip = ''; + } } $rc = new RecentChange; @@ -214,9 +291,11 @@ 'rc_bot' => $bot ? 1 : 0, 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', - 'rc_ip' => $ip, - 'rc_patrolled' => 0, - 'rc_new' => 0 # obsolete + 'rc_ip' => $ip, + 'rc_patrolled' => 0, + 'rc_new' => 0, # obsolete + 'rc_old_len' => $oldSize, + 'rc_new_len' => $newSize ); $rc->mExtra = array( @@ -226,19 +305,25 @@ 'newSize' => $newSize, ); $rc->save(); + return( $rc->mAttribs['rc_id'] ); } - # Makes an entry in the database corresponding to page creation - # Note: the title object must be loaded with the new id using resetArticleID() - /*static*/ function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = "default", + /** + * Makes an entry in the database corresponding to page creation + * 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 ) { if ( !$ip ) { - global $wgIP; - $ip = empty( $wgIP ) ? '' : $wgIP; + $ip = wfGetIP(); + if ( !$ip ) { + $ip = ''; + } } - if ( $bot == 'default' ) { - $bot = $user->isBot(); + if ( $bot === 'default' ) { + $bot = $user->isAllowed( 'bot' ); } $rc = new RecentChange; @@ -260,7 +345,9 @@ 'rc_moved_to_title' => '', 'rc_ip' => $ip, 'rc_patrolled' => 0, - 'rc_new' => 1 # obsolete + 'rc_new' => 1, # obsolete + 'rc_old_len' => 0, + 'rc_new_len' => $size ); $rc->mExtra = array( @@ -270,15 +357,19 @@ 'newSize' => $size ); $rc->save(); + return( $rc->mAttribs['rc_id'] ); } # Makes an entry in the database corresponding to a rename - /*static*/ function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false ) + public static function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false ) { if ( !$ip ) { - global $wgIP; - $ip = empty( $wgIP ) ? '' : $wgIP; + $ip = wfGetIP(); + if ( !$ip ) { + $ip = ''; + } } + $rc = new RecentChange; $rc->mAttribs = array( 'rc_timestamp' => $timestamp, @@ -293,12 +384,14 @@ 'rc_comment' => $comment, 'rc_this_oldid' => 0, 'rc_last_oldid' => 0, - 'rc_bot' => $user->isBot() ? 1 : 0, + 'rc_bot' => $user->isAllowed( 'bot' ) ? 1 : 0, 'rc_moved_to_ns' => $newTitle->getNamespace(), 'rc_moved_to_title' => $newTitle->getDBkey(), 'rc_ip' => $ip, 'rc_new' => 0, # obsolete - 'rc_patrolled' => 1 + 'rc_patrolled' => 1, + 'rc_old_len' => NULL, + 'rc_new_len' => NULL, ); $rc->mExtra = array( @@ -309,22 +402,26 @@ $rc->save(); } - /* static */ function notifyMoveToNew( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) { + public static function notifyMoveToNew( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) { RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, false ); } - /* static */ function notifyMoveOverRedirect( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) { - RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip='', true ); + public static function notifyMoveOverRedirect( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) { + RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true ); } # A log entry is different to an edit in that previous revisions are # not kept - /*static*/ function notifyLog( $timestamp, &$title, &$user, $comment, $ip='' ) + public static function notifyLog( $timestamp, &$title, &$user, $comment, $ip='', + $type, $action, $target, $logComment, $params ) { if ( !$ip ) { - global $wgIP; - $ip = empty( $wgIP ) ? '' : $wgIP; + $ip = wfGetIP(); + if ( !$ip ) { + $ip = ''; + } } + $rc = new RecentChange; $rc->mAttribs = array( 'rc_timestamp' => $timestamp, @@ -339,16 +436,23 @@ 'rc_comment' => $comment, 'rc_this_oldid' => 0, 'rc_last_oldid' => 0, - 'rc_bot' => 0, + 'rc_bot' => $user->isAllowed( 'bot' ) ? 1 : 0, 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', 'rc_ip' => $ip, 'rc_patrolled' => 1, - 'rc_new' => 0 # obsolete + 'rc_new' => 0, # obsolete + 'rc_old_len' => NULL, + 'rc_new_len' => NULL, ); $rc->mExtra = array( 'prefixedDBkey' => $title->getPrefixedDBkey(), - 'lastTimestamp' => 0 + 'lastTimestamp' => 0, + 'logType' => $type, + 'logAction' => $action, + 'logComment' => $logComment, + 'logTarget' => $target, + 'logParams' => $params ); $rc->save(); } @@ -357,14 +461,15 @@ function loadFromRow( $row ) { $this->mAttribs = get_object_vars( $row ); + $this->mAttribs["rc_timestamp"] = wfTimestamp(TS_MW, $this->mAttribs["rc_timestamp"]); $this->mExtra = array(); } - # Makes a pseudo-RC entry from a cur row, for watchlists and things + # Makes a pseudo-RC entry from a cur row function loadFromCurRow( $row ) { $this->mAttribs = array( - 'rc_timestamp' => $row->rev_timestamp, + 'rc_timestamp' => wfTimestamp(TS_MW, $row->rev_timestamp), 'rc_cur_time' => $row->rev_timestamp, 'rc_user' => $row->rev_user, 'rc_user_text' => $row->rev_user_text, @@ -380,14 +485,25 @@ 'rc_moved_to_ns' => 0, 'rc_moved_to_title' => '', 'rc_ip' => '', - 'rc_patrolled' => '1', # we can't support patrolling on the Watchlist - # currently because it uses cur, not recentchanges - 'rc_new' => $row->page_is_new # obsolete + 'rc_id' => $row->rc_id, + 'rc_patrolled' => $row->rc_patrolled, + 'rc_new' => $row->page_is_new, # obsolete + 'rc_old_len' => $row->rc_old_len, + 'rc_new_len' => $row->rc_new_len, ); $this->mExtra = array(); } + /** + * Get an attribute value + * + * @param $name Attribute name + * @return mixed + */ + public function getAttribute( $name ) { + return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : NULL; + } /** * Gets the end part of the diff URL associated with this object @@ -409,44 +525,104 @@ return $trail; } + function cleanupForIRC( $text ) { + return str_replace(array("\n", "\r"), array("", ""), $text); + } + function getIRCLine() { + global $wgUseRCPatrol; + + // 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(); + } else { + $title = $titleObj->getPrefixedText(); + } + $title = $this->cleanupForIRC( $title ); $bad = array("\n", "\r"); $empty = array("", ""); $title = $titleObj->getPrefixedText(); $title = str_replace($bad, $empty, $title); - if ( $rc_new ) { - $url = $titleObj->getFullURL(); + // FIXME: *HACK* these should be getFullURL(), hacked for SSL madness --brion 2005-12-26 + 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->getFullURL("diff=0&oldid=$rc_last_oldid"); + $url = $titleObj->getInternalURL("diff=$rc_this_oldid&oldid=$rc_last_oldid"); } if ( isset( $oldSize ) && isset( $newSize ) ) { $szdiff = $newSize - $oldSize; - if ($szdiff < -500) + if ($szdiff < -500) { $szdiff = "\002$szdiff\002"; - else if ($szdiff >= 0) - $szdiff = "+$szdiff"; - $szdiff = "($szdiff)"; + } elseif ($szdiff >= 0) { + $szdiff = '+' . $szdiff ; + } + $szdiff = '(' . $szdiff . ')' ; } else { $szdiff = ''; } - $comment = str_replace($bad, $empty, $rc_comment); - $user = str_replace($bad, $empty, $rc_user_text); - $flag = ($rc_minor ? "M" : "") . ($rc_new ? "N" : ""); - # see http://www.irssi.org/?page=docs&doc=formats for some colour codes. prefix is \003, + $user = $this->cleanupForIRC( $rc_user_text ); + + if ( $rc_type == RC_LOG ) { + $logTargetText = $logTarget->getPrefixedText(); + $comment = $this->cleanupForIRC( str_replace( $logTargetText, "\00302$logTargetText\00310", $rc_comment ) ); + $flag = $logAction; + } else { + $comment = $this->cleanupForIRC( $rc_comment ); + $flag = ($rc_minor ? "M" : "") . ($rc_new ? "N" : ""); + } + # see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003, # no colour (\003) switches back to the term default - $comment = preg_replace("/\/\* (.*) \*\/(.*)/", "\00315\$1\003 - \00310\$2\003", $comment); $fullString = "\00314[[\00307$title\00314]]\0034 $flag\00310 " . "\00302$url\003 \0035*\003 \00303$user\003 \0035*\003 $szdiff \00310$comment\003\n"; - return $fullString; } + + /** + * Returns the change size (HTML). + * The lengths can be given optionally. + */ + function getCharacterDifference( $old = 0, $new = 0 ) { + global $wgRCChangedSizeThreshold, $wgLang; + + 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 . ')'; + } + } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Revision.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Revision.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Revision.php 2006-01-27 05:07:06.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Revision.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,32 +1,31 @@ IntVal( $id ) ) ); + 'rev_id' => intval( $id ) ) ); } - + /** * Load either the current, or a specified, revision * that's attached to a given title. If not attached @@ -38,9 +37,9 @@ * @access public * @static */ - function newFromTitle( &$title, $id = 0 ) { + public static function newFromTitle( &$title, $id = 0 ) { if( $id ) { - $matchId = IntVal( $id ); + $matchId = intval( $id ); } else { $matchId = 'page_latest'; } @@ -50,7 +49,22 @@ 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDbkey() ) ); } - + + /** + * Load a page revision from a given revision ID number. + * Returns null if no such revision can be found. + * + * @param Database $db + * @param int $id + * @access public + * @static + */ + public static function loadFromId( &$db, $id ) { + return Revision::loadFromConds( $db, + array( 'page_id=rev_page', + 'rev_id' => intval( $id ) ) ); + } + /** * Load either the current, or a specified, revision * that's attached to a given page. If not attached @@ -61,8 +75,9 @@ * @param int $id * @return Revision * @access public + * @static */ - function loadFromPageId( &$db, $pageid, $id = 0 ) { + public static function loadFromPageId( $db, $pageid, $id = 0 ) { $conds=array('page_id=rev_page','rev_page'=>intval( $pageid ), 'page_id'=>intval( $pageid )); if( $id ) { $conds['rev_id']=intval($id); @@ -71,7 +86,7 @@ } return Revision::loadFromConds( $db, $conds ); } - + /** * Load either the current, or a specified, revision * that's attached to a given page. If not attached @@ -82,10 +97,11 @@ * @param int $id * @return Revision * @access public + * @static */ - function loadFromTitle( &$db, $title, $id = 0 ) { + public static function loadFromTitle( &$db, $title, $id = 0 ) { if( $id ) { - $matchId = IntVal( $id ); + $matchId = intval( $id ); } else { $matchId = 'page_latest'; } @@ -96,7 +112,7 @@ 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDbkey() ) ); } - + /** * Load the revision for the given title with the given timestamp. * WARNING: Timestamps may in some circumstances not be unique, @@ -109,7 +125,7 @@ * @access public * @static */ - function loadFromTimestamp( &$db, &$title, $timestamp ) { + public static function loadFromTimestamp( &$db, &$title, $timestamp ) { return Revision::loadFromConds( $db, array( 'rev_timestamp' => $db->timestamp( $timestamp ), @@ -117,25 +133,25 @@ 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDbkey() ) ); } - + /** * Given a set of conditions, fetch a revision. * * @param array $conditions * @return Revision - * @static * @access private + * @static */ - function newFromConds( $conditions ) { - $db =& wfGetDB( DB_SLAVE ); + private static function newFromConds( $conditions ) { + $db = wfGetDB( DB_SLAVE ); $row = Revision::loadFromConds( $db, $conditions ); if( is_null( $row ) ) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $row = Revision::loadFromConds( $dbw, $conditions ); } return $row; } - + /** * Given a set of conditions, fetch a revision from * the given database connection. @@ -143,21 +159,23 @@ * @param Database $db * @param array $conditions * @return Revision - * @static * @access private + * @static */ - function loadFromConds( &$db, $conditions ) { + private static function loadFromConds( $db, $conditions ) { $res = Revision::fetchFromConds( $db, $conditions ); if( $res ) { $row = $res->fetchObject(); $res->free(); if( $row ) { - return new Revision( $row ); + $ret = new Revision( $row ); + return $ret; } } - return null; + $ret = null; + return $ret; } - + /** * Return a wrapper for a series of database rows to * fetch all of a given page's revisions in turn. @@ -165,17 +183,17 @@ * * @param Title $title * @return ResultWrapper - * @static * @access 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_id=rev_page' ) ); + 'page_id=rev_page' ) ); } - + /** * Return a wrapper for a series of database rows to * fetch all of a given page's revisions in turn. @@ -183,18 +201,18 @@ * * @param Title $title * @return ResultWrapper - * @static * @access 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_id=rev_page' ) ); + 'page_id=rev_page' ) ); } - + /** * Given a set of conditions, return a ResultWrapper * which will return matching database rows with the @@ -203,10 +221,10 @@ * @param Database $db * @param array $conditions * @return ResultWrapper - * @static * @access private + * @static */ - function fetchFromConds( &$db, $conditions ) { + private static function fetchFromConds( $db, $conditions ) { $res = $db->select( array( 'page', 'revision' ), array( 'page_namespace', @@ -220,80 +238,126 @@ 'rev_user', 'rev_minor_edit', 'rev_timestamp', - 'rev_deleted' ), + 'rev_deleted', + 'rev_len' ), $conditions, 'Revision::fetchRow', array( 'LIMIT' => 1 ) ); - return $db->resultObject( $res ); + $ret = $db->resultObject( $res ); + return $ret; } - + + /** + * Return the list of revision fields that should be selected to create + * a new revision. + */ + static function selectFields() { + return array( + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_timestamp', + 'rev_comment', + 'rev_minor_edit', + 'rev_user', + 'rev_user_text,'. + 'rev_deleted', + 'rev_len' + ); + } + /** * @param object $row * @access private */ function Revision( $row ) { if( is_object( $row ) ) { - $this->mId = IntVal( $row->rev_id ); - $this->mPage = IntVal( $row->rev_page ); - $this->mTextId = IntVal( $row->rev_text_id ); + $this->mId = intval( $row->rev_id ); + $this->mPage = intval( $row->rev_page ); + $this->mTextId = intval( $row->rev_text_id ); $this->mComment = $row->rev_comment; $this->mUserText = $row->rev_user_text; - $this->mUser = IntVal( $row->rev_user ); - $this->mMinorEdit = IntVal( $row->rev_minor_edit ); + $this->mUser = intval( $row->rev_user ); + $this->mMinorEdit = intval( $row->rev_minor_edit ); $this->mTimestamp = $row->rev_timestamp; - $this->mDeleted = IntVal( $row->rev_deleted ); - - $this->mCurrent = ( $row->rev_id == $row->page_latest ); - $this->mTitle = Title::makeTitle( $row->page_namespace, - $row->page_title ); + $this->mDeleted = intval( $row->rev_deleted ); + if( !isset( $row->rev_len ) || is_null( $row->rev_len ) ) + $this->mSize = null; + else + $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 ); + } else { + $this->mCurrent = false; + $this->mTitle = null; + } + + // Lazy extraction... + $this->mText = null; if( isset( $row->old_text ) ) { - $this->mText = $this->getRevisionText( $row ); + $this->mTextRow = $row; } else { - $this->mText = null; + // 'text' table row entry will be lazy-loaded + $this->mTextRow = null; } } elseif( is_array( $row ) ) { // Build a new revision to be saved... global $wgUser; - - $this->mId = isset( $row['id'] ) ? IntVal( $row['id'] ) : null; - $this->mPage = isset( $row['page'] ) ? IntVal( $row['page'] ) : null; - $this->mTextId = isset( $row['text_id'] ) ? IntVal( $row['text_id'] ) : null; - $this->mUserText = isset( $row['user_text'] ) ? StrVal( $row['user_text'] ) : $wgUser->getName(); - $this->mUser = isset( $row['user'] ) ? IntVal( $row['user'] ) : $wgUser->getId(); - $this->mMinorEdit = isset( $row['minor_edit'] ) ? IntVal( $row['minor_edit'] ) : 0; - $this->mTimestamp = isset( $row['timestamp'] ) ? StrVal( $row['timestamp'] ) : wfTimestamp( TS_MW ); - $this->mDeleted = isset( $row['deleted'] ) ? IntVal( $row['deleted'] ) : 0; + + $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null; + $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null; + $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null; + $this->mUserText = isset( $row['user_text'] ) ? strval( $row['user_text'] ) : $wgUser->getName(); + $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId(); + $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0; + $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; // 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; - + $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null; + $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null; + $this->mTextRow = null; + $this->mTitle = null; # Load on demand if needed $this->mCurrent = false; + # If we still have no len_size, see it we have the text to figure it out + if ( !$this->mSize ) + $this->mSize = is_null($this->mText) ? null : strlen($this->mText); } else { - wfDebugDieBacktrace( 'Revision constructor passed invalid row format.' ); + throw new MWException( 'Revision constructor passed invalid row format.' ); } } - + /**#@+ * @access public */ - + /** * @return int */ function getId() { return $this->mId; } - + /** * @return int */ function getTextId() { return $this->mTextId; } - + + /** + * Returns the length of the text in this revision, or null if unknown. + */ + function getSize() { + return $this->mSize; + } + /** * Returns the title of the page associated with this entry. * @return Title @@ -302,66 +366,127 @@ if( isset( $this->mTitle ) ) { return $this->mTitle; } - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( array( 'page', 'revision' ), array( 'page_namespace', 'page_title' ), array( 'page_id=rev_page', 'rev_id' => $this->mId ), - 'Revision::getTItle' ); + 'Revision::getTitle' ); if( $row ) { $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); } return $this->mTitle; } - + + /** + * Set the title of the revision + * @param Title $title + */ + function setTitle( $title ) { + $this->mTitle = $title; + } + /** * @return int */ function getPage() { return $this->mPage; } - + /** + * Fetch revision's user id if it's available to all users * @return int */ function getUser() { + if( $this->isDeleted( self::DELETED_USER ) ) { + return 0; + } else { + return $this->mUser; + } + } + + /** + * Fetch revision's user id without regard for the current user's permissions + * @return string + */ + function getRawUser() { return $this->mUser; } - + /** + * Fetch revision's username if it's available to all users * @return string */ function getUserText() { + if( $this->isDeleted( self::DELETED_USER ) ) { + return ""; + } else { + return $this->mUserText; + } + } + + /** + * Fetch revision's username without regard for view restrictions + * @return string + */ + function getRawUserText() { return $this->mUserText; } /** + * Fetch revision comment if it's available to all users * @return string */ function getComment() { + if( $this->isDeleted( self::DELETED_COMMENT ) ) { + return ""; + } else { + return $this->mComment; + } + } + + /** + * Fetch revision comment without regard for the current user's permissions + * @return string + */ + function getRawComment() { return $this->mComment; } - + /** * @return bool */ function isMinor() { return (bool)$this->mMinorEdit; } - + /** + * int $field one of DELETED_* bitfield constants * @return bool */ - function isDeleted() { - return (bool)$this->mDeleted; + function isDeleted( $field ) { + return ($this->mDeleted & $field) == $field; } - + /** + * Fetch revision text if it's available to all users * @return string */ function getText() { + if( $this->isDeleted( self::DELETED_TEXT ) ) { + return ""; + } else { + return $this->getRawText(); + } + } + + /** + * Fetch revision text without regard for view restrictions + * @return string + */ + function getRawText() { if( is_null( $this->mText ) ) { // Revision text is immutable. Load on demand: $this->mText = $this->loadText(); @@ -370,25 +495,41 @@ } /** + * 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() { - return $this->mTimestamp; + return wfTimestamp(TS_MW, $this->mTimestamp); } - + /** * @return bool */ function isCurrent() { return $this->mCurrent; } - + /** * @return Revision */ function getPrevious() { $prev = $this->mTitle->getPreviousRevisionID( $this->mId ); - return Revision::newFromTitle( $this->mTitle, $prev ); + if ( $prev ) { + return Revision::newFromTitle( $this->mTitle, $prev ); + } else { + return null; + } } /** @@ -396,7 +537,11 @@ */ function getNext() { $next = $this->mTitle->getNextRevisionID( $this->mId ); - return Revision::newFromTitle( $this->mTitle, $next ); + if ( $next ) { + return Revision::newFromTitle( $this->mTitle, $next ); + } else { + return null; + } } /**#@-*/ @@ -409,10 +554,10 @@ * @param string $prefix table prefix (default 'old_') * @return string $text|false the text requested */ - function getRevisionText( $row, $prefix = 'old_' ) { + public static function getRevisionText( $row, $prefix = 'old_' ) { $fname = 'Revision::getRevisionText'; wfProfileIn( $fname ); - + # Get data $textField = $prefix . 'text'; $flagsField = $prefix . 'flags'; @@ -433,40 +578,41 @@ # Use external methods for external objects, text in table is URL-only then if ( in_array( 'external', $flags ) ) { $url=$text; - @list($proto,$path)=explode('://',$url,2); + @list(/* $proto */,$path)=explode('://',$url,2); if ($path=="") { wfProfileOut( $fname ); return false; } - require_once('ExternalStore.php'); $text=ExternalStore::fetchFromURL($url); } - if( in_array( 'gzip', $flags ) ) { - # Deal with optional compression of archived pages. - # This can be done periodically via maintenance/compressOld.php, and - # as pages are saved if $wgCompressRevisions is set. - $text = gzinflate( $text ); - } - - if( in_array( 'object', $flags ) ) { - # Generic compressed storage - $obj = unserialize( $text ); + // If the text was fetched without an error, convert it + if ( $text !== false ) { + if( in_array( 'gzip', $flags ) ) { + # Deal with optional compression of archived pages. + # This can be done periodically via maintenance/compressOld.php, and + # as pages are saved if $wgCompressRevisions is set. + $text = gzinflate( $text ); + } - # Bugger, corrupted my test database by double-serializing - if ( !is_object( $obj ) ) { - $obj = unserialize( $obj ); + if( in_array( 'object', $flags ) ) { + # Generic compressed storage + $obj = unserialize( $text ); + if ( !is_object( $obj ) ) { + // Invalid object + wfProfileOut( $fname ); + return false; + } + $text = $obj->getText(); } - $text = $obj->getText(); - } - - global $wgLegacyEncoding; - if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) { - # Old revisions kept around in a legacy encoding? - # Upconvert on demand. - global $wgInputEncoding, $wgContLang; - $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding, $text ); + global $wgLegacyEncoding; + if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) ) { + # Old revisions kept around in a legacy encoding? + # Upconvert on demand. + global $wgInputEncoding, $wgContLang; + $text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding, $text ); + } } wfProfileOut( $fname ); return $text; @@ -486,11 +632,11 @@ function compressRevisionText( &$text ) { global $wgCompressRevisions; $flags = array(); - + # Revisions not marked this way will be converted # on load if $wgLegacyCharset is set in the future. $flags[] = 'utf-8'; - + if( $wgCompressRevisions ) { if( function_exists( 'gzdeflate' ) ) { $text = gzdeflate( $text ); @@ -501,7 +647,7 @@ } return implode( ',', $flags ); } - + /** * Insert a new revision into the database, returning the new revision ID * number on success and dies horribly on failure. @@ -510,25 +656,47 @@ * @return int */ function insertOn( &$dbw ) { + global $wgDefaultExternalStore; + $fname = 'Revision::insertOn'; wfProfileIn( $fname ); - - $mungedText = $this->mText; - $flags = Revision::compressRevisionText( $mungedText ); - - # Record the text to the text table + + $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; + } + // 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" ); + } + if ( $flags ) { + $flags .= ','; + } + $flags .= 'external'; + } + + # Record the text (or external storage URL) to the text table if( !isset( $this->mTextId ) ) { $old_id = $dbw->nextSequenceValue( 'text_old_id_val' ); $dbw->insert( 'text', array( 'old_id' => $old_id, - 'old_text' => $mungedText, + 'old_text' => $data, 'old_flags' => $flags, ), $fname ); $this->mTextId = $dbw->insertId(); } - + # Record the edit in revisions $rev_id = isset( $this->mId ) ? $this->mId @@ -544,15 +712,15 @@ 'rev_user_text' => $this->mUserText, 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ), 'rev_deleted' => $this->mDeleted, + 'rev_len' => $this->mSize, ), $fname ); - - $this->mId = $dbw->insertId(); - + + $this->mId = !is_null($rev_id) ? $rev_id : $dbw->insertId(); wfProfileOut( $fname ); return $this->mId; } - + /** * Lazy-load the revision's text. * Currently hardcoded to the 'text' table storage engine. @@ -564,23 +732,51 @@ $fname = 'Revision::loadText'; wfProfileIn( $fname ); - $dbr =& wfGetDB( DB_SLAVE ); - $row = $dbr->selectRow( 'text', - array( 'old_text', 'old_flags' ), - array( 'old_id' => $this->getTextId() ), - $fname); + // 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 ); + return $text; + } + } + + // If we kept data for lazy extraction, use it now... + if ( isset( $this->mTextRow ) ) { + $row = $this->mTextRow; + $this->mTextRow = null; + } else { + $row = null; + } if( !$row ) { - $dbw =& wfGetDB( DB_MASTER ); + // 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); + } + + if( !$row ) { + // Possible slave lag! + $dbw = wfGetDB( DB_MASTER ); $row = $dbw->selectRow( 'text', array( 'old_text', 'old_flags' ), array( 'old_id' => $this->getTextId() ), $fname); } - + $text = Revision::getRevisionText( $row ); - wfProfileOut( $fname ); + if( $wgRevisionCacheExpiry ) { + $wgMemc->set( $key, $text, $wgRevisionCacheExpiry ); + } + + wfProfileOut( $fname ); + return $text; } @@ -601,7 +797,7 @@ function newNullRevision( &$dbw, $pageId, $summary, $minor ) { $fname = 'Revision::newNullRevision'; wfProfileIn( $fname ); - + $current = $dbw->selectRow( array( 'page', 'revision' ), array( 'page_latest', 'rev_text_id' ), @@ -610,7 +806,7 @@ 'page_latest=rev_id', ), $fname ); - + if( $current ) { $revision = new Revision( array( 'page' => $pageId, @@ -621,10 +817,75 @@ } else { $revision = null; } - + wfProfileOut( $fname ); return $revision; } + /** + * Determine if the current user is allowed to view a particular + * field of this revision, if it's marked as deleted. + * @param int $field one of self::DELETED_TEXT, + * self::DELETED_COMMENT, + * self::DELETED_USER + * @return bool + */ + function userCan( $field ) { + if( ( $this->mDeleted & $field ) == $field ) { + global $wgUser; + $permission = ( $this->mDeleted & self::DELETED_RESTRICTED ) == self::DELETED_RESTRICTED + ? 'hiderevision' + : 'deleterevision'; + wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" ); + return $wgUser->isAllowed( $permission ); + } else { + return true; + } + } + + + /** + * Get rev_timestamp from rev_id, without loading the rest of the row + * @param integer $id + */ + static function getTimestampFromID( $id ) { + $dbr = wfGetDB( DB_SLAVE ); + $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', + array( 'rev_id' => $id ), __METHOD__ ); + if ( $timestamp === false ) { + # Not in slave, try master + $dbw = wfGetDB( DB_MASTER ); + $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', + array( 'rev_id' => $id ), __METHOD__ ); + } + return $timestamp; + } + + static function countByPageId( $db, $id ) { + $row = $db->selectRow( 'revision', 'COUNT(*) AS revCount', + array( 'rev_page' => $id ), __METHOD__ ); + if( $row ) { + return $row->revCount; + } + return 0; + } + + static function countByTitle( $db, $title ) { + $id = $title->getArticleId(); + if( $id ) { + return Revision::countByPageId( $db, $id ); + } + return 0; + } } -?> + +/** + * Aliases for backwards compatibility with 1.6 + */ +define( 'MW_REV_DELETED_TEXT', Revision::DELETED_TEXT ); +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.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Sanitizer.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Sanitizer.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Sanitizer.php 2005-10-25 21:58:39.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Sanitizer.php 2007-08-31 00:48:45.000000000 -0400 @@ -1,13 +1,13 @@ et al * http://www.mediawiki.org/ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or + * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, @@ -17,11 +17,10 @@ * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * - * @package MediaWiki - * @subpackage Parser + * @addtogroup Parser */ /** @@ -29,7 +28,7 @@ * Sanitizer::normalizeCharReferences and Sanitizer::decodeCharReferences */ define( 'MW_CHAR_REFS_REGEX', - '/&([A-Za-z0-9]+); + '/&([A-Za-z0-9\x80-\xff]+); |&\#([0-9]+); |&\#x([0-9A-Za-z]+); |&\#X([0-9A-Za-z]+); @@ -40,7 +39,7 @@ * Allows some... latitude. * Used in Sanitizer::fixTagAttributes and Sanitizer::decodeTagAttributes */ -$attrib = '[A-Za-z0-9]'; +$attrib = '[A-Za-z0-9]'; $space = '[\x09\x0a\x0d\x20]'; define( 'MW_ATTRIBS_REGEX', "/(?:^|$space)($attrib+) @@ -59,7 +58,7 @@ /** * List of all named character entities defined in HTML 4.01 * http://www.w3.org/TR/html4/sgml/entities.html - * @access private + * @private */ global $wgHtmlEntities; $wgHtmlEntities = array( @@ -316,30 +315,50 @@ 'zwj' => 8205, 'zwnj' => 8204 ); -/** @package MediaWiki */ +/** + * Character entity aliases accepted by MediaWiki + */ +global $wgHtmlEntityAliases; +$wgHtmlEntityAliases = array( + 'רלמ' => 'rlm', + 'رلم' => 'rlm', +); + + +/** + * XHTML sanitizer for MediaWiki + * @addtogroup Parser + */ class Sanitizer { + const NONE = 0; + const INITIAL_NONLETTER = 1; + /** * Cleans up HTML, removes dangerous tags and attributes, and * removes HTML comments - * @access private + * @private * @param string $text * @param callback $processCallback to do any variable or parameter replacements in HTML attribute values * @param array $args for the processing callback * @return string */ - function removeHTMLtags( $text, $processCallback = null, $args = array() ) { - global $wgUseTidy, $wgUserHtml; - $fname = 'Parser::removeHTMLtags'; - wfProfileIn( $fname ); + static function removeHTMLtags( $text, $processCallback = null, $args = array(), $extratags = array() ) { + global $wgUseTidy; + + static $htmlpairs, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags, + $htmllist, $listtags, $htmlsingleallowed, $htmlelements, $staticInitialised; + + wfProfileIn( __METHOD__ ); - if( $wgUserHtml ) { - $htmlpairs = array( # Tags that must be closed + if ( !$staticInitialised ) { + + $htmlpairs = array_merge( $extratags, array( # Tags that must be closed 'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's', 'strike', 'strong', 'tt', 'var', 'div', 'center', 'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre', - 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span' - ); + 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'u' + ) ); $htmlsingle = array( 'br', 'hr', 'li', 'dt', 'dd' ); @@ -350,62 +369,103 @@ 'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul', 'dl', 'font', 'big', 'small', 'sub', 'sup', 'span' ); - $tabletags = array( # Can only appear inside table - 'td', 'th', 'tr' + $tabletags = array( # Can only appear inside table, we will close them + 'td', 'th', 'tr', + ); + $htmllist = array( # Tags used by list + 'ul','ol', + ); + $listtags = array( # Tags that can appear in a list + 'li', ); - } else { - $htmlpairs = array(); - $htmlsingle = array(); - $htmlnest = array(); - $tabletags = array(); - } - $htmlsingle = array_merge( $tabletags, $htmlsingle ); - $htmlelements = array_merge( $htmlsingle, $htmlpairs ); + $htmlsingleallowed = array_merge( $htmlsingle, $tabletags ); + $htmlelements = array_merge( $htmlsingle, $htmlpairs, $htmlnest ); + + # Convert them all to hashtables for faster lookup + $vars = array( 'htmlpairs', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags', + 'htmllist', 'listtags', 'htmlsingleallowed', 'htmlelements' ); + foreach ( $vars as $var ) { + $$var = array_flip( $$var ); + } + $staticInitialised = true; + } # Remove HTML comments $text = Sanitizer::removeHTMLcomments( $text ); - $bits = explode( '<', $text ); - $text = array_shift( $bits ); + $text = str_replace( '>', '>', array_shift( $bits ) ); if(!$wgUseTidy) { - $tagstack = array(); $tablestack = array(); + $tagstack = $tablestack = array(); foreach ( $bits as $x ) { - $prev = error_reporting( E_ALL & ~( E_NOTICE | E_WARNING ) ); - preg_match( '/^(\\/?)(\\w+)([^>]*?)(\\/{0,1}>)([^<]*)$/', - $x, $regs ); - list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs; - error_reporting( $prev ); + $regs = array(); + if( preg_match( '!^(/?)(\\w+)([^>]*?)(/{0,1}>)([^<]*)$!', $x, $regs ) ) { + list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs; + } else { + $slash = $t = $params = $brace = $rest = null; + } $badtag = 0 ; - if ( in_array( $t = strtolower( $t ), $htmlelements ) ) { + if ( isset( $htmlelements[$t = strtolower( $t )] ) ) { # Check our stack if ( $slash ) { # Closing a tag... - if( in_array( $t, $htmlsingleonly ) ) { - $badtag = 1; - } elseif( !in_array( $t, $htmlsingle ) && - ( $ot = @array_pop( $tagstack ) ) != $t ) { - @array_push( $tagstack, $ot ); + if( isset( $htmlsingleonly[$t] ) ) { $badtag = 1; + } elseif ( ( $ot = @array_pop( $tagstack ) ) != $t ) { + if ( isset( $htmlsingleallowed[$ot] ) ) { + # Pop all elements with an optional close tag + # and see if we find a match below them + $optstack = array(); + array_push ($optstack, $ot); + while ( ( ( $ot = @array_pop( $tagstack ) ) != $t ) && + isset( $htmlsingleallowed[$ot] ) ) + { + array_push ($optstack, $ot); + } + if ( $t != $ot ) { + # No match. Push the optinal elements back again + $badtag = 1; + while ( $ot = @array_pop( $optstack ) ) { + array_push( $tagstack, $ot ); + } + } + } else { + @array_push( $tagstack, $ot ); + #
      • can be nested in
          or
            , skip those cases: + if(!(isset( $htmllist[$ot] ) && isset( $listtags[$t] ) )) { + $badtag = 1; + } + } } else { if ( $t == 'table' ) { $tagstack = array_pop( $tablestack ); } - $newparams = ''; } + $newparams = ''; } else { # Keep track for later - if ( in_array( $t, $tabletags ) && + if ( isset( $tabletags[$t] ) && ! in_array( 'table', $tagstack ) ) { $badtag = 1; } else if ( in_array( $t, $tagstack ) && - ! in_array ( $t , $htmlnest ) ) { + ! isset( $htmlnest [$t ] ) ) { $badtag = 1 ; - } elseif( in_array( $t, $htmlsingleonly ) ) { + # Is it a self closed htmlpair ? (bug 5487) + } else if( $brace == '/>' && + isset( $htmlpairs[$t] ) ) { + $badtag = 1; + } elseif( isset( $htmlsingleonly[$t] ) ) { # Hack to force empty tag for uncloseable elements $brace = '/>'; - } else if ( ! in_array( $t, $htmlsingle ) ) { + } else if( isset( $htmlsingle[$t] ) ) { + # Hack to not close $htmlsingle tags + $brace = NULL; + } else if( isset( $tabletags[$t] ) + && in_array($t ,$tagstack) ) { + // New table tag but forgot to close the previous one + $text .= ""; + } else { if ( $t == 'table' ) { array_push( $tablestack, $tagstack ); $tagstack = array(); @@ -424,7 +484,7 @@ } if ( ! $badtag ) { $rest = str_replace( '>', '>', $rest ); - $close = ( $brace == '/>' ) ? ' /' : ''; + $close = ( $brace == '/>' && !$slash ) ? ' /' : ''; $text .= "<$slash$t$newparams$close>$rest"; continue; } @@ -441,8 +501,8 @@ foreach ( $bits as $x ) { preg_match( '/^(\\/?)(\\w+)([^>]*?)(\\/{0,1}>)([^<]*)$/', $x, $regs ); - @list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs; - if ( in_array( $t = strtolower( $t ), $htmlelements ) ) { + @list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs; + if ( isset( $htmlelements[$t = strtolower( $t )] ) ) { if( is_callable( $processCallback ) ) { call_user_func_array( $processCallback, array( &$params, $args ) ); } @@ -454,7 +514,7 @@ } } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $text; } @@ -463,14 +523,13 @@ * To avoid leaving blank lines, when a comment is both preceded * and followed by a newline (ignoring spaces), trim leading and * trailing spaces and one of the newlines. - * - * @access private + * + * @private * @param string $text * @return string */ - function removeHTMLcomments( $text ) { - $fname='Parser::removeHTMLcomments'; - wfProfileIn( $fname ); + static function removeHTMLcomments( $text ) { + wfProfileIn( __METHOD__ ); while (($start = strpos($text, '', $start + 4); if ($end === false) { @@ -500,116 +559,282 @@ $text = substr_replace($text, '', $start, $end - $start); } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $text; } /** - * Take a tag soup fragment listing an HTML element's attributes - * and normalize it to well-formed XML, discarding unwanted attributes. + * Take an array of attribute names and values and normalize or discard + * illegal values for the given element type. * - * - Normalizes attribute names to lowercase * - Discards attributes not on a whitelist for the given element - * - Turns broken or invalid entities into plaintext - * - Double-quotes all attribute values - * - Attributes without values are given the name as attribute - * - Double attributes are discarded * - Unsafe style attributes are discarded - * - Prepends space if there are attributes. + * - Invalid id attributes are reencoded * - * @param string $text + * @param array $attribs * @param string $element - * @return string + * @return array * * @todo Check for legal values where the DTD limits things. * @todo Check for unique id attribute :P */ - function fixTagAttributes( $text, $element ) { - global $wgUrlProtocols; - if( trim( $text ) == '' ) { - return ''; - } - - # Unquoted attribute - # Since we quote this later, this can be anything distinguishable - # from the end of the attribute - if( !preg_match_all( - MW_ATTRIBS_REGEX, - $text, - $pairs, - PREG_SET_ORDER ) ) { - return ''; - } - - $whitelist = array_flip( Sanitizer::attributeWhitelist( $element ) ); - $attribs = array(); - foreach( $pairs as $set ) { - $attribute = strtolower( $set[1] ); + static function validateTagAttributes( $attribs, $element ) { + 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. + * + * - Discards attributes not the given whitelist + * - Unsafe style attributes are discarded + * - Invalid id attributes are reencoded + * + * @param array $attribs + * @param array $whitelist list of allowed attribute names + * @return array + * + * @todo Check for legal values where the DTD limits things. + * @todo Check for unique id attribute :P + */ + static function validateAttributes( $attribs, $whitelist ) { + $whitelist = array_flip( $whitelist ); + $out = array(); + foreach( $attribs as $attribute => $value ) { if( !isset( $whitelist[$attribute] ) ) { continue; } - - $raw = Sanitizer::getTagAttributeCallback( $set ); - $value = Sanitizer::normalizeAttributeValue( $raw ); - # Strip javascript "expression" from stylesheets. # http://msdn.microsoft.com/workshop/author/dhtml/overview/recalc.asp if( $attribute == 'style' ) { - $stripped = Sanitizer::decodeCharReferences( $value ); - - // Remove any comments; IE gets token splitting wrong - $stripped = preg_replace( '!/\\*.*?\\*/!S', ' ', $stripped ); - $value = htmlspecialchars( $stripped ); - - // ... and continue checks - $stripped = preg_replace( '!\\\\([0-9A-Fa-f]{1,6})[ \\n\\r\\t\\f]?!e', - 'codepointToUtf8(hexdec("$1"))', $stripped ); - $stripped = str_replace( '\\', '', $stripped ); - if( preg_match( '/(expression|tps*:\/\/|url\\s*\().*/is', - $stripped ) ) { + $value = Sanitizer::checkCss( $value ); + if( $value === false ) { # haxx0r continue; } } - - # Templates and links may be expanded in later parsing, - # creating invalid or dangerous output. Suppress this. - $value = strtr( $value, array( - '{' => '{', - '[' => '[', - "''" => '''', - 'ISBN' => 'ISBN', - 'RFC' => 'RFC', - 'PMID' => 'PMID', - ) ); - - # Stupid hack - $value = preg_replace_callback( - '/(' . $wgUrlProtocols . ')/', - array( 'Sanitizer', 'armorLinksCallback' ), - $value ); - + + if ( $attribute === 'id' ) + $value = Sanitizer::escapeId( $value ); + // If this attribute was previously set, override it. // Output should only have one attribute of each name. - $attribs[$attribute] = "$attribute=\"$value\""; + $out[$attribute] = $value; } - if( empty( $attribs ) ) { - return ''; - } else { - return ' ' . implode( ' ', $attribs ); + 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. + * + * @todo implement merging for other attributes such as style + * @param array $a + * @param array $b + * @return array + */ + 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 ) ) ); } + 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. + * + * Currently URL references, 'expression', 'tps' are forbidden. + * + * @param string $value + * @return mixed + */ + static function checkCss( $value ) { + $stripped = Sanitizer::decodeCharReferences( $value ); + + // Remove any comments; IE gets token splitting wrong + $stripped = StringUtils::delimiterReplace( '/*', '*/', ' ', $stripped ); + + $value = $stripped; + + // ... and continue checks + $stripped = preg_replace( '!\\\\([0-9A-Fa-f]{1,6})[ \\n\\r\\t\\f]?!e', + 'codepointToUtf8(hexdec("$1"))', $stripped ); + $stripped = str_replace( '\\', '', $stripped ); + if( preg_match( '/(?:expression|tps*:\/\/|url\\s*\().*/is', + $stripped ) ) { + # haxx0r + return false; + } + + return $value; + } + + /** + * Take a tag soup fragment listing an HTML element's attributes + * and normalize it to well-formed XML, discarding unwanted attributes. + * Output is safe for further wikitext processing, with escaping of + * values that could trigger problems. + * + * - Normalizes attribute names to lowercase + * - Discards attributes not on a whitelist for the given element + * - Turns broken or invalid entities into plaintext + * - Double-quotes all attribute values + * - Attributes without values are given the name as attribute + * - Double attributes are discarded + * - Unsafe style attributes are discarded + * - Prepends space if there are attributes. + * + * @param string $text + * @param string $element + * @return string + */ + static function fixTagAttributes( $text, $element ) { + if( trim( $text ) == '' ) { + return ''; + } + + $stripped = Sanitizer::validateTagAttributes( + Sanitizer::decodeTagAttributes( $text ), $element ); + + $attribs = array(); + foreach( $stripped as $attribute => $value ) { + $encAttribute = htmlspecialchars( $attribute ); + $encValue = Sanitizer::safeEncodeAttribute( $value ); + + $attribs[] = "$encAttribute=\"$encValue\""; + } + return count( $attribs ) ? ' ' . implode( ' ', $attribs ) : ''; + } + + /** + * Encode an attribute value for HTML output. + * @param $text + * @return HTML-encoded text fragment + */ + static function encodeAttribute( $text ) { + $encValue = htmlspecialchars( $text ); + + // Whitespace is normalized during attribute decoding, + // so if we've been passed non-spaces we must encode them + // ahead of time or they won't be preserved. + $encValue = strtr( $encValue, array( + "\n" => ' ', + "\r" => ' ', + "\t" => ' ', + ) ); + + return $encValue; + } + + /** + * Encode an attribute value for HTML tags, with extra armoring + * against further wiki processing. + * @param $text + * @return HTML-encoded text fragment + */ + static function safeEncodeAttribute( $text ) { + $encValue = Sanitizer::encodeAttribute( $text ); + + # Templates and links may be expanded in later parsing, + # creating invalid or dangerous output. Suppress this. + $encValue = strtr( $encValue, array( + '<' => '<', // This should never happen, + '>' => '>', // we've received invalid input + '"' => '"', // which should have been escaped. + '{' => '{', + '[' => '[', + "''" => '''', + 'ISBN' => 'ISBN', + 'RFC' => 'RFC', + 'PMID' => 'PMID', + '|' => '|', + '__' => '__', + ) ); + + # Stupid hack + $encValue = preg_replace_callback( + '/(' . wfUrlProtocols() . ')/', + array( 'Sanitizer', 'armorLinksCallback' ), + $encValue ); + return $encValue; + } + + /** + * Given a value escape it so that it can be used in an id attribute and + * return it, this does not validate the value however (see first link) + * + * @see http://www.w3.org/TR/html401/types.html#type-name Valid characters + * in the id and + * 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. + * @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"; + } + return $id; + } + + /** + * Given a value, escape it so that it can be used as a CSS class and + * return it. + * + * @todo For extra validity, input should be validated UTF-8. + * + * @see http://www.w3.org/TR/CSS21/syndata.html Valid characters/format + * + * @param string $class + * @return string + */ + static function escapeClass( $class ) { + // Convert ugly stuff to underscores and kill underscores in ugly places + return rtrim(preg_replace( + array('/(^[0-9\\-])|[\\x00-\\x20!"#$%&\'()*+,.\\/:;<=>?@[\\]^`{|}~]|\\xC2\\xA0/','/_+/'), + '_', + $class ), '_'); + } + + /** * Regex replace callback for armoring links against further processing. * @param array $matches * @return string - * @access private + * @private */ - function armorLinksCallback( $matches ) { + private static function armorLinksCallback( $matches ) { return str_replace( ':', ':', $matches[1] ); } - + /** * Return an associative array of attribute names and values from * a partial tag string. Attribute names are forces to lowercase, @@ -618,13 +843,14 @@ * @param string * @return array */ - function decodeTagAttributes( $text ) { + static function decodeTagAttributes( $text ) { $attribs = array(); - + if( trim( $text ) == '' ) { return $attribs; } - + + $pairs = array(); if( !preg_match_all( MW_ATTRIBS_REGEX, $text, @@ -636,20 +862,26 @@ foreach( $pairs as $set ) { $attribute = strtolower( $set[1] ); $value = Sanitizer::getTagAttributeCallback( $set ); + + // Normalize whitespace + $value = preg_replace( '/[\t\r\n ]+/', ' ', $value ); + $value = trim( $value ); + + // Decode character references $attribs[$attribute] = Sanitizer::decodeCharReferences( $value ); } return $attribs; } - + /** * Pick the appropriate attribute value from a match set from the * MW_ATTRIBS_REGEX matches. * * @param array $set * @return string - * @access private + * @private */ - function getTagAttributeCallback( $set ) { + private static function getTagAttributeCallback( $set ) { if( isset( $set[6] ) ) { # Illegal #XXXXXX color with no quotes. return $set[6]; @@ -667,10 +899,10 @@ # For 'reduced' form, return explicitly the attribute name here. return $set[1]; } else { - wfDebugDieBacktrace( "Tag conditions not met. This should never happen and is a bug." ); + throw new MWException( "Tag conditions not met. This should never happen and is a bug." ); } } - + /** * Normalize whitespace and character references in an XML source- * encoded text for an attribute value. @@ -681,16 +913,21 @@ * * @param string $text * @return string - * @access private + * @private */ - function normalizeAttributeValue( $text ) { + private static function normalizeAttributeValue( $text ) { return str_replace( '"', '"', - preg_replace( - '/\r\n|[\x20\x0d\x0a\x09]/', - ' ', + self::normalizeWhitespace( Sanitizer::normalizeCharReferences( $text ) ) ); } + private static function normalizeWhitespace( $text ) { + return preg_replace( + '/\r\n|[\x20\x0d\x0a\x09]/', + ' ', + $text ); + } + /** * Ensure that any entities and character references are legal * for XML and XHTML specifically. Any stray bits will be @@ -703,9 +940,9 @@ * * @param string $text * @return string - * @access private + * @private */ - function normalizeCharReferences( $text ) { + static function normalizeCharReferences( $text ) { return preg_replace_callback( MW_CHAR_REFS_REGEX, array( 'Sanitizer', 'normalizeCharReferencesCallback' ), @@ -715,7 +952,7 @@ * @param string $matches * @return string */ - function normalizeCharReferencesCallback( $matches ) { + static function normalizeCharReferencesCallback( $matches ) { $ret = null; if( $matches[1] != '' ) { $ret = Sanitizer::normalizeEntity( $matches[1] ); @@ -732,34 +969,38 @@ return $ret; } } - + /** * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD, - * return the named entity reference as is. Otherwise, returns - * HTML-escaped text of pseudo-entity source (eg &foo;) + * 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 * @return string + * @static */ - function normalizeEntity( $name ) { - global $wgHtmlEntities; - if( isset( $wgHtmlEntities[$name] ) ) { + static function normalizeEntity( $name ) { + global $wgHtmlEntities, $wgHtmlEntityAliases; + if ( isset( $wgHtmlEntityAliases[$name] ) ) { + return "&{$wgHtmlEntityAliases[$name]};"; + } elseif( isset( $wgHtmlEntities[$name] ) ) { return "&$name;"; } else { return "&$name;"; } } - - function decCharReference( $codepoint ) { - $point = IntVal( $codepoint ); + + static function decCharReference( $codepoint ) { + $point = intval( $codepoint ); if( Sanitizer::validateCodepoint( $point ) ) { return sprintf( '&#%d;', $point ); } else { return null; } } - - function hexCharReference( $codepoint ) { + + static function hexCharReference( $codepoint ) { $point = hexdec( $codepoint ); if( Sanitizer::validateCodepoint( $point ) ) { return sprintf( '&#x%x;', $point ); @@ -767,13 +1008,13 @@ return null; } } - + /** * Returns true if a given Unicode codepoint is a valid character in XML. * @param int $codepoint * @return bool */ - function validateCodepoint( $codepoint ) { + private static function validateCodepoint( $codepoint ) { return ($codepoint == 0x09) || ($codepoint == 0x0a) || ($codepoint == 0x0d) @@ -788,20 +1029,21 @@ * * @param string $text * @return string - * @access public + * @public + * @static */ - function decodeCharReferences( $text ) { + public static function decodeCharReferences( $text ) { return preg_replace_callback( MW_CHAR_REFS_REGEX, array( 'Sanitizer', 'decodeCharReferencesCallback' ), $text ); } - + /** * @param string $matches * @return string */ - function decodeCharReferencesCallback( $matches ) { + static function decodeCharReferencesCallback( $matches ) { if( $matches[1] != '' ) { return Sanitizer::decodeEntity( $matches[1] ); } elseif( $matches[2] != '' ) { @@ -814,22 +1056,22 @@ # Last case should be an ampersand by itself return $matches[0]; } - + /** * Return UTF-8 string for a codepoint if that is a valid * character reference, otherwise U+FFFD REPLACEMENT CHARACTER. * @param int $codepoint * @return string - * @access private + * @private */ - function decodeChar( $codepoint ) { + static function decodeChar( $codepoint ) { if( Sanitizer::validateCodepoint( $codepoint ) ) { return codepointToUtf8( $codepoint ); } else { return UTF8_REPLACEMENT; } } - + /** * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD, * return the UTF-8 encoding of that character. Otherwise, returns @@ -838,15 +1080,18 @@ * @param string $name * @return string */ - function decodeEntity( $name ) { - global $wgHtmlEntities; + static function decodeEntity( $name ) { + global $wgHtmlEntities, $wgHtmlEntityAliases; + if ( isset( $wgHtmlEntityAliases[$name] ) ) { + $name = $wgHtmlEntityAliases[$name]; + } if( isset( $wgHtmlEntities[$name] ) ) { return codepointToUtf8( $wgHtmlEntities[$name] ); } else { return "&$name;"; } } - + /** * Fetch the whitelist of acceptable attributes for a given * element name. @@ -854,7 +1099,7 @@ * @param string $element * @return array */ - function attributeWhitelist( $element ) { + static function attributeWhitelist( $element ) { static $list; if( !isset( $list ) ) { $list = Sanitizer::setupAttributeWhitelist(); @@ -863,11 +1108,12 @@ ? $list[$element] : array(); } - + /** + * @todo Document it a bit * @return array */ - function setupAttributeWhitelist() { + static function setupAttributeWhitelist() { $common = array( 'id', 'class', 'lang', 'dir', 'title', 'style' ); $block = array_merge( $common, array( 'align' ) ); $tablealign = array( 'align', 'char', 'charoff', 'valign' ); @@ -882,7 +1128,7 @@ 'height', # deprecated 'bgcolor' # deprecated ); - + # Numbers refer to sections in HTML 4.01 standard describing the element. # See: http://www.w3.org/TR/html4/ $whitelist = array ( @@ -890,7 +1136,7 @@ 'div' => $block, 'center' => $common, # deprecated 'span' => $block, # ?? - + # 7.5.5 'h1' => $block, 'h2' => $block, @@ -898,13 +1144,13 @@ 'h4' => $block, 'h5' => $block, 'h6' => $block, - + # 7.5.6 # address - + # 8.2.4 # bdo - + # 9.2.1 'em' => $common, 'strong' => $common, @@ -916,64 +1162,69 @@ 'var' => $common, # abbr # acronym - + # 9.2.2 'blockquote' => array_merge( $common, array( 'cite' ) ), # q - + # 9.2.3 'sub' => $common, 'sup' => $common, - + # 9.3.1 'p' => $block, - + # 9.3.2 'br' => array( 'id', 'class', 'title', 'style', 'clear' ), - + # 9.3.4 'pre' => array_merge( $common, array( 'width' ) ), - + # 9.4 'ins' => array_merge( $common, array( 'cite', 'datetime' ) ), 'del' => array_merge( $common, array( 'cite', 'datetime' ) ), - + # 10.2 'ul' => array_merge( $common, array( 'type' ) ), 'ol' => array_merge( $common, array( 'type', 'start' ) ), 'li' => array_merge( $common, array( 'type', 'value' ) ), - + # 10.3 'dl' => $common, 'dd' => $common, 'dt' => $common, - + # 11.2.1 'table' => array_merge( $common, array( 'summary', 'width', 'border', 'frame', - 'rules', 'cellspacing', 'cellpadding', - 'align', 'bgcolor', 'frame', 'rules', - 'border' ) ), - + 'rules', 'cellspacing', 'cellpadding', + 'align', 'bgcolor', + ) ), + # 11.2.2 'caption' => array_merge( $common, array( 'align' ) ), - + # 11.2.3 'thead' => array_merge( $common, $tablealign ), 'tfoot' => array_merge( $common, $tablealign ), 'tbody' => array_merge( $common, $tablealign ), - + # 11.2.4 'colgroup' => array_merge( $common, array( 'span', 'width' ), $tablealign ), 'col' => array_merge( $common, array( 'span', 'width' ), $tablealign ), - + # 11.2.5 'tr' => array_merge( $common, array( 'bgcolor' ), $tablealign ), - + # 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 + 'img' => array_merge( $common, array( 'alt' ) ), + # 15.2.1 'tt' => $common, 'b' => $common, @@ -983,14 +1234,14 @@ 'strike' => $common, 's' => $common, 'u' => $common, - + # 15.2.2 'font' => array_merge( $common, array( 'size', 'color', 'face' ) ), # basefont - + # 15.3 'hr' => array_merge( $common, array( 'noshade', 'size', 'width' ) ), - + # XHTML Ruby annotation text module, simple ruby only. # http://www.w3c.org/TR/ruby/ 'ruby' => $common, @@ -999,35 +1250,98 @@ '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/ + 'math' => array( 'class', 'style', 'id', 'title' ), ); return $whitelist; } - + /** * Take a fragment of (potentially invalid) HTML and return - * a version with any tags removed, encoded suitably for literal - * inclusion in an attribute value. + * a version with any tags removed, encoded as plain text. + * + * Warning: this return value must be further escaped for literal + * inclusion in HTML output as of 1.10! * * @param string $text HTML fragment * @return string */ - function stripAllTags( $text ) { + static function stripAllTags( $text ) { # Actual - $text = preg_replace( '/<[^>]*>/', '', $text ); - + $text = StringUtils::delimiterReplace( '<', '>', '', $text ); + # Normalize &entities and whitespace - $text = Sanitizer::normalizeAttributeValue( $text ); - - # Will be placed into "double-quoted" attributes, - # make sure remaining bits are safe. - $text = str_replace( - array('<', '>', '"'), - array('<', '>', '"'), - $text ); - + $text = self::decodeCharReferences( $text ); + $text = self::normalizeWhitespace( $text ); + return $text; } + /** + * Hack up a private DOCTYPE with HTML's standard entity declarations. + * PHP 4 seemed to know these if you gave it an HTML doctype, but + * PHP 5.1 doesn't. + * + * Use for passing XHTML fragments to PHP's XML parsing functions + * + * @return string + * @static + */ + static function hackDocType() { + global $wgHtmlEntities; + $out = " $codepoint ) { + $out .= ""; + } + $out .= "]>\n"; + return $out; + } + + static function cleanUrl( $url, $hostname=true ) { + # Normalize any HTML entities in input. They will be + # re-escaped by makeExternalLink(). + $url = Sanitizer::decodeCharReferences( $url ); + + # Escape any control characters introduced by the above step + $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url ); + + # Validate hostname portion + $matches = array(); + if( preg_match( '!^([^:]+:)(//[^/]+)?(.*)$!iD', $url, $matches ) ) { + list( /* $whole */, $protocol, $host, $rest ) = $matches; + + // Characters that will be ignored in IDNs. + // http://tools.ietf.org/html/3454#section-3.1 + // Strip them before further processing so blacklists and such work. + $strip = "/ + \\s| # general whitespace + \xc2\xad| # 00ad SOFT HYPHEN + \xe1\xa0\x86| # 1806 MONGOLIAN TODO SOFT HYPHEN + \xe2\x80\x8b| # 200b ZERO WIDTH SPACE + \xe2\x81\xa0| # 2060 WORD JOINER + \xef\xbb\xbf| # feff ZERO WIDTH NO-BREAK SPACE + \xcd\x8f| # 034f COMBINING GRAPHEME JOINER + \xe1\xa0\x8b| # 180b MONGOLIAN FREE VARIATION SELECTOR ONE + \xe1\xa0\x8c| # 180c MONGOLIAN FREE VARIATION SELECTOR TWO + \xe1\xa0\x8d| # 180d MONGOLIAN FREE VARIATION SELECTOR THREE + \xe2\x80\x8c| # 200c ZERO WIDTH NON-JOINER + \xe2\x80\x8d| # 200d ZERO WIDTH JOINER + [\xef\xb8\x80-\xef\xb8\x8f] # fe00-fe00f VARIATION SELECTOR-1-16 + /xuD"; + + $host = preg_replace( $strip, '', $host ); + + // @fixme: validate hostnames here + + return $protocol . $host . $rest; + } else { + return $url; + } + } + } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SearchEngine.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SearchEngine.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SearchEngine.php 2005-07-13 02:47:17.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SearchEngine.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,20 +1,15 @@ getNamespace() == NS_SPECIAL || $title->exists() ) { - return $title; - } + $allSearchTerms = array($searchterm); - # Now try all lower case (i.e. first letter capitalized) - # - $title = Title::newFromText( strtolower( $term ) ); - if ( $title->exists() ) { - return $title; + if($wgContLang->hasVariants()){ + $allSearchTerms = array_merge($allSearchTerms,$wgContLang->convertLinkToAllVariants($searchterm)); } - # Now try capitalized string - # - $title = Title::newFromText( ucwords( strtolower( $term ) ) ); - if ( $title->exists() ) { - return $title; - } + foreach($allSearchTerms as $term){ - # Now try all upper case - # - $title = Title::newFromText( strtoupper( $term ) ); - if ( $title->exists() ) { - return $title; - } - - global $wgCapitalLinks, $wgContLang; - if( !$wgCapitalLinks ) { - // Catch differs-by-first-letter-case-only - $title = Title::newFromText( $wgContLang->ucfirst( $term ) ); + # Exact match? No need to look further. + $title = Title::newFromText( $term ); + if (is_null($title)) + return NULL; + + if ( $title->getNamespace() == NS_SPECIAL || $title->exists() ) { + return $title; + } + + # Now try all lower case (i.e. first letter capitalized) + # + $title = Title::newFromText( $wgContLang->lc( $term ) ); + if ( $title->exists() ) { + return $title; + } + + # Now try capitalized string + # + $title = Title::newFromText( $wgContLang->ucwords( $term ) ); if ( $title->exists() ) { return $title; } - $title = Title::newFromText( $wgContLang->lcfirst( $term ) ); + + # Now try all upper case + # + $title = Title::newFromText( $wgContLang->uc( $term ) ); + if ( $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() ) { 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; + } + } } - $title = Title::newFromText( $term ); + $title = Title::newFromText( $searchterm ); # Entering an IP address goes to the contributions page if ( ( $title->getNamespace() == NS_USER && User::isIP($title->getText() ) ) - || User::isIP( trim( $term ) ) ) { - return Title::makeTitle( NS_SPECIAL, "Contributions/" . $title->getDbkey() ); + || User::isIP( trim( $searchterm ) ) ) { + return SpecialPage::getTitleFor( 'Contributions', $title->getDbkey() ); } @@ -108,15 +118,32 @@ 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 ) { + $image = wfFindFile( $title ); + if( $image ) { + return $title; + } + } + + # MediaWiki namespace? Page may be "implied" if not customized. + # Just return it, with caps forced as the message system likes it. + if( $title->getNamespace() == NS_MEDIAWIKI ) { + return Title::makeTitle( NS_MEDIAWIKI, $wgContLang->ucfirst( $title->getText() ) ); + } + # Quoted term? Try without the quotes... - if( preg_match( '/^"([^"]+)"$/', $term, $matches ) ) { + $matches = array(); + if( preg_match( '/^"([^"]+)"$/', $searchterm, $matches ) ) { return SearchEngine::getNearMatch( $matches[1] ); } return NULL; } - - function legalSearchChars() { + + public static function legalSearchChars() { return "A-Za-z_'0-9\\x80-\\xFF\\-"; } @@ -129,10 +156,10 @@ * @access public */ function setLimitOffset( $limit, $offset = 0 ) { - $this->limit = IntVal( $limit ); - $this->offset = IntVal( $offset ); + $this->limit = intval( $limit ); + $this->offset = intval( $offset ); } - + /** * Set which namespaces the search should include. * Give an array of namespace index numbers. @@ -143,13 +170,12 @@ function setNamespaces( $namespaces ) { $this->namespaces = $namespaces; } - + /** * Make a list of searchable namespaces and their canonical names. * @return array - * @access public */ - function searchableNamespaces() { + public static function searchableNamespaces() { global $wgContLang; $arr = array(); foreach( $wgContLang->getNamespaces() as $ns => $name ) { @@ -159,7 +185,7 @@ } return $arr; } - + /** * Return a 'cleaned up' search string * @@ -175,23 +201,17 @@ * active database backend, and return a configured instance. * * @return SearchEngine - * @access private */ - function create() { - global $wgDBtype, $wgDBmysql4, $wgSearchType; + public static function create() { + global $wgDBtype, $wgSearchType; if( $wgSearchType ) { $class = $wgSearchType; } elseif( $wgDBtype == 'mysql' ) { - if( $wgDBmysql4 ) { - $class = 'SearchMySQL4'; - require_once( 'SearchMySQL4.php' ); - } else { - $class = 'SearchMysql3'; - require_once( 'SearchMySQL3.php' ); - } - } else if ( $wgDBtype == 'PostgreSQL' ) { - $class = 'SearchTsearch2'; - require_once( 'SearchTsearch2.php' ); + $class = 'SearchMySQL4'; + } else if ( $wgDBtype == 'postgres' ) { + $class = 'SearchPostgres'; + } else if ( $wgDBtype == 'oracle' ) { + $class = 'SearchOracle'; } else { $class = 'SearchEngineDummy'; } @@ -199,7 +219,7 @@ $search->setLimitOffset(0,0); return $search; } - + /** * Create or update the search index record for the given page. * Title and text should be pre-processed. @@ -221,12 +241,15 @@ * @param string $title * @abstract */ - function updateTitle( $id, $title ) { + function updateTitle( $id, $title ) { // no-op - } + } } -/** @package MediaWiki */ + +/** + * @addtogroup Search + */ class SearchResultSet { /** * Fetch an array of regular expression fragments for matching @@ -239,11 +262,11 @@ function termMatches() { return array(); } - + function numRows() { return 0; } - + /** * Return true if results are included in this result set. * @return bool @@ -252,7 +275,7 @@ function hasResults() { return false; } - + /** * Some search modes return a total hit count for the query * in the entire article database. This may include pages @@ -267,7 +290,7 @@ function getTotalHits() { return null; } - + /** * Some search modes return a suggested alternate term if there are * no exact hits. Returns true if there is one on this set. @@ -278,7 +301,7 @@ function hasSuggestion() { return false; } - + /** * Some search modes return a suggested alternate term if there are * no exact hits. Check hasSuggestion() first. @@ -289,7 +312,7 @@ function getSuggestion() { return ''; } - + /** * Fetches next search result, or false. * @return SearchResult @@ -299,14 +322,25 @@ function next() { return false; } + + /** + * Frees the result set, if applicable. + * @ access public + */ + function free() { + // ... + } } -/** @package MediaWiki */ + +/** + * @addtogroup Search + */ class SearchResult { function SearchResult( $row ) { $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); } - + /** * @return Title * @access public @@ -314,7 +348,7 @@ function getTitle() { return $this->mTitle; } - + /** * @return double or null if not supported */ @@ -324,11 +358,17 @@ } /** - * @package MediaWiki + * @addtogroup Search */ class SearchEngineDummy { function search( $term ) { return null; } + function setLimitOffset($l, $o) {} + function legalSearchChars() {} + function update() {} + function setnamespaces() {} + function searchtitle() {} + function searchtext() {} } diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SearchMySQL.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SearchMySQL.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SearchMySQL.php 2005-08-10 17:13:49.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SearchMySQL.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,33 +1,27 @@ # http://www.mediawiki.org/ -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # 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. - * @package MediaWiki - * @subpackage Search + * @addtogroup Search */ - -/** */ -require_once( 'SearchEngine.php' ); - -/** @package MediaWiki */ class SearchMySQL extends SearchEngine { /** * Perform a full text search query and return a result set. @@ -52,12 +46,12 @@ $resultSet = $this->db->resultObject( $this->db->query( $this->getQuery( $this->filter( $term ), false ) ) ); return new MySQLSearchResultSet( $resultSet, $this->searchTerms ); } - - + + /** * Return a partial WHERE clause to exclude redirects, if so set * @return string - * @access private + * @private */ function queryRedirect() { if( $this->showRedirects ) { @@ -66,11 +60,11 @@ return 'AND page_is_redirect=0'; } } - + /** * Return a partial WHERE clause to limit the search to the given namespaces * @return string - * @access private + * @private */ function queryNamespaces() { $namespaces = implode( ',', $this->namespaces ); @@ -79,32 +73,32 @@ } return 'AND page_namespace IN (' . $namespaces . ')'; } - + /** * Return a LIMIT clause to limit results on the query. * @return string - * @access private + * @private */ function queryLimit() { - return $this->db->limitResult( $this->limit, $this->offset ); + return $this->db->limitResult( '', $this->limit, $this->offset ); } /** * Does not do anything for generic search engine * subclasses may define this though * @return string - * @access private + * @private */ function queryRanking( $filteredTerm, $fulltext ) { return ''; } - + /** * Construct the full SQL query to do the search. * The guts shoulds be constructed in queryMain() * @param string $filteredTerm * @param bool $fulltext - * @access private + * @private */ function getQuery( $filteredTerm, $fulltext ) { return $this->queryMain( $filteredTerm, $fulltext ) . ' ' . @@ -133,7 +127,7 @@ * @param string $filteredTerm * @param bool $fulltext * @return string - * @access private + * @private */ function queryMain( $filteredTerm, $fulltext ) { $match = $this->parseQuery( $filteredTerm, $fulltext ); @@ -153,7 +147,7 @@ * @param string $text */ function update( $id, $title, $text ) { - $dbw=& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->replace( 'searchindex', array( 'si_page' ), array( @@ -171,8 +165,8 @@ * @param string $title */ function updateTitle( $id, $title ) { - $dbw =& wfGetDB( DB_MASTER ); - + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'searchindex', array( 'si_title' => $title ), array( 'si_page' => $id ), @@ -181,21 +175,23 @@ } } -/** @package MediaWiki */ +/** + * @addtogroup Search + */ class MySQLSearchResultSet extends SearchResultSet { function MySQLSearchResultSet( $resultSet, $terms ) { $this->mResultSet = $resultSet; $this->mTerms = $terms; } - + function termMatches() { return $this->mTerms; } - + function numRows() { return $this->mResultSet->numRows(); } - + function next() { $row = $this->mResultSet->fetchObject(); if( $row === false ) { @@ -204,6 +200,10 @@ return new SearchResult( $row ); } } + + function free() { + $this->mResultSet->free(); + } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SearchMySQL4.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SearchMySQL4.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SearchMySQL4.php 2005-11-16 06:01:43.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SearchMySQL4.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,40 +1,32 @@ # http://www.mediawiki.org/ -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** * Search engine hook for MySQL 4+ - * @package MediaWiki - * @subpackage Search - */ - -require_once( 'SearchMySQL.php' ); - -/** - * @package MediaWiki - * @subpackage Search + * @addtogroup Search */ class SearchMySQL4 extends SearchMySQL { var $strictMatching = true; - + /** @todo document */ - function SearchMySQL4( &$db ) { - $this->db =& $db; + function SearchMySQL4( $db ) { + $this->db = $db; } /** @todo document */ @@ -45,6 +37,7 @@ $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 ) { @@ -62,14 +55,14 @@ $this->searchTerms[] = $regexp; } wfDebug( "Would search with '$searchon'\n" ); - wfDebug( "Match with /\b" . implode( '\b|\b', $this->searchTerms ) . "\b/\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) "; } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SearchTsearch2.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SearchTsearch2.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SearchTsearch2.php 2005-01-27 14:50:34.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SearchTsearch2.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,45 +1,39 @@ , Domas Mituzas # http://www.mediawiki.org/ -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** * Search engine hook for PostgreSQL / Tsearch2 - * @package MediaWiki - * @subpackage Search + * @addtogroup Search */ -/** */ -require_once( 'SearchEngine.php' ); - /** * @todo document - * @package MediaWiki - * @subpackage Search + * @addtogroup Search */ class SearchTsearch2 extends SearchEngine { var $strictMatching = false; - + function SearchTsearch2( &$db ) { $this->db =& $db; - $this->db->setSchema('tsearch'); $this->mRanking = true; } - + function getIndexField( $fulltext ) { return $fulltext ? 'si_text' : 'si_title'; } @@ -51,6 +45,7 @@ $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 ) { @@ -68,11 +63,11 @@ $this->searchTerms[] = $regexp; } wfDebug( "Would search with '$searchon'\n" ); - wfDebug( "Match with /\b" . implode( '\b|\b', $this->searchTerms ) . "\b/\n" ); + wfDebug( 'Match with /\b' . implode( '\b|\b', $this->searchTerms ) . "\b/\n" ); } else { wfDebug( "Can't understand search query '{$this->filteredText}'\n" ); } - + $searchon = preg_replace('/(\s+)/','&',$searchon); $searchon = $this->db->strencode( $searchon ); return $searchon; @@ -86,7 +81,7 @@ else return ""; } - + function queryMain( $filteredTerm, $fulltext ) { $match = $this->parseQuery( $filteredTerm, $fulltext ); @@ -95,12 +90,12 @@ $searchindex = $this->db->tableName( 'searchindex' ); return 'SELECT cur_id, cur_namespace, cur_title, cur_text ' . "FROM $cur,$searchindex " . - 'WHERE cur_id=si_page AND ' . + 'WHERE cur_id=si_page AND ' . " $field @@ to_tsquery ('$match') " ; } - function update( $id, $title, $text ) { - $dbw=& wfGetDB(DB_MASTER); + function update( $id, $title, $text ) { + $dbw = wfGetDB(DB_MASTER); $searchindex = $dbw->tableName( 'searchindex' ); $sql = "DELETE FROM $searchindex WHERE si_page={$id}"; $dbw->query($sql,"SearchTsearch2:update"); @@ -110,18 +105,18 @@ "'),to_tsvector('". $dbw->strencode( $text)."')) "; $dbw->query($sql,"SearchTsearch2:update"); - } + } - function updateTitle($id,$title) { - $dbw=& wfGetDB(DB_MASTER); - $searchindex = $dbw->tableName( 'searchindex' ); - $sql = "UPDATE $searchindex SET si_title=to_tsvector('" . - $db->strencode( $title ) . - "') WHERE si_page={$id}"; + function updateTitle($id,$title) { + $dbw = wfGetDB(DB_MASTER); + $searchindex = $dbw->tableName( 'searchindex' ); + $sql = "UPDATE $searchindex SET si_title=to_tsvector('" . + $dbw->strencode( $title ) . + "') WHERE si_page={$id}"; - $dbw->query( $sql, "SearchMySQL4::updateTitle" ); - } + $dbw->query( $sql, "SearchMySQL4::updateTitle" ); + } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SearchUpdate.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SearchUpdate.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SearchUpdate.php 2005-07-22 07:29:14.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SearchUpdate.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,12 +1,7 @@ mId ) { return false; @@ -37,10 +32,9 @@ $fname = 'SearchUpdate::doUpdate'; wfProfileIn( $fname ); - require_once( 'SearchEngine.php' ); $search = SearchEngine::create(); - $lc = $search->legalSearchChars() . '&#;'; - + $lc = SearchEngine::legalSearchChars() . '&#;'; + if( $this->mText === false ) { $search->updateTitle($this->mId, Title::indexTitle( $this->mNamespace, $this->mTitle )); @@ -54,8 +48,8 @@ wfProfileIn( $fname.'-regexps' ); $text = preg_replace( "/<\\/?\\s*[A-Za-z][A-Za-z0-9]*\\s*([^>]*?)>/", ' ', strtolower( " " . $text /*$this->mText*/ . " " ) ); # Strip HTML markup - $text = preg_replace( "/(^|\\n)\\s*==\\s+([^\\n]+)\\s+==\\s/sD", - "\\2 \\2 \\2 ", $text ); # Emphasize headings + $text = preg_replace( "/(^|\\n)==\\s*([^\\n]+)\\s*==(\\s)/sD", + "\\1\\2 \\2 \\2\\3", $text ); # Emphasize headings # Strip external URLs $uc = "A-Za-z0-9_\\/:.,~%\\-+&;#?!=()@\\xA0-\\xFF"; @@ -99,18 +93,23 @@ # Strip wiki '' and ''' $text = preg_replace( "/''[']*/", " ", $text ); wfProfileOut( "$fname-regexps" ); + + wfRunHooks( 'SearchUpdate', array( $this->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 - * @package MediaWiki + * @addtogroup Search */ class SearchUpdateMyISAM extends SearchUpdate { # Inherits everything } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Setup.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Setup.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Setup.php 2007-10-16 23:22:58.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Setup.php 2007-07-22 10:45:12.000000000 -0400 @@ -1,14 +1,16 @@ 'LocalRepo', + 'name' => 'local', + 'directory' => $wgUploadDirectory, + 'url' => $wgUploadBaseUrl ? $wgUploadBaseUrl . $wgUploadPath : $wgUploadPath, + 'hashLevels' => $wgHashedUploadDirectory ? 2 : 0, + 'thumbScriptUrl' => $wgThumbnailScriptPath, + 'transformVia404' => !$wgGenerateThumbnailOnParse, + 'initialCapital' => $wgCapitalLinks, + 'deletedDir' => $wgFileStore['deleted']['directory'], + 'deletedHashLevels' => $wgFileStore['deleted']['hash'] + ); +} +/** + * Initialise shared repo from backwards-compatible settings + */ +if ( $wgUseSharedUploads ) { + if ( $wgSharedUploadDBname ) { + $wgForeignFileRepos[] = array( + 'class' => 'ForeignDBRepo', + 'name' => 'shared', + 'directory' => $wgSharedUploadDirectory, + 'url' => $wgSharedUploadPath, + 'hashLevels' => $wgHashedSharedUploadDirectory ? 2 : 0, + 'thumbScriptUrl' => $wgSharedThumbnailScriptPath, + 'transformVia404' => !$wgGenerateThumbnailOnParse, + 'dbType' => $wgDBtype, + 'dbServer' => $wgDBserver, + 'dbUser' => $wgDBuser, + 'dbPassword' => $wgDBpassword, + 'dbName' => $wgSharedUploadDBname, + 'dbFlags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT, + 'tablePrefix' => $wgSharedUploadDBprefix, + 'hasSharedCache' => $wgCacheSharedUploads, + 'descBaseUrl' => $wgRepositoryBaseUrl, + 'fetchDescription' => $wgFetchCommonsDescriptions, + ); + } else { + $wgForeignFileRepos[] = array( + 'class' => 'FSRepo', + 'name' => 'shared', + 'directory' => $wgSharedUploadDirectory, + 'url' => $wgSharedUploadPath, + 'hashLevels' => $wgHashedSharedUploadDirectory ? 2 : 0, + 'thumbScriptUrl' => $wgSharedThumbnailScriptPath, + 'transformVia404' => !$wgGenerateThumbnailOnParse, + 'descBaseUrl' => $wgRepositoryBaseUrl, + 'fetchDescription' => $wgFetchCommonsDescriptions, + ); + } } +require_once( "$IP/includes/AutoLoader.php" ); + +wfProfileIn( $fname.'-exception' ); +require_once( "$IP/includes/Exception.php" ); +wfInstallExceptionHandler(); +wfProfileOut( $fname.'-exception' ); + +wfProfileIn( $fname.'-includes' ); +require_once( "$IP/includes/GlobalFunctions.php" ); +require_once( "$IP/includes/Hooks.php" ); +require_once( "$IP/includes/Namespace.php" ); +require_once( "$IP/includes/ProxyTools.php" ); +require_once( "$IP/includes/ObjectCache.php" ); +require_once( "$IP/includes/ImageFunctions.php" ); +require_once( "$IP/includes/StubObject.php" ); wfProfileOut( $fname.'-includes' ); wfProfileIn( $fname.'-misc1' ); -$wgIP = wfGetIP(); -$wgRequest = new WebRequest(); + +$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 ) { - # wfDebug( '"' . implode( '" "', $argv ) . '"' ); + wfDebug( "\n\nStart command line script $self\n" ); } elseif ( function_exists( 'getallheaders' ) ) { wfDebug( "\n\nStart request\n" ); wfDebug( $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . "\n" ); @@ -103,6 +165,14 @@ $wgUseEnotif = $wgEnotifUserTalk || $wgEnotifWatchlist; +if($wgMetaNamespace === FALSE) { + $wgMetaNamespace = str_replace( ' ', '_', $wgSitename ); +} + +# These are now the same, always +# To determine the user language, use $wgLang->getCode() +$wgContLanguageCode = $wgLanguageCode; + wfProfileOut( $fname.'-misc1' ); wfProfileIn( $fname.'-memcached' ); @@ -118,24 +188,30 @@ wfProfileIn( $fname.'-SetupSession' ); if ( $wgDBprefix ) { - $wgCookiePrefix = str_replace("+", "", $wgDBname . '_' . $wgDBprefix); + $wgCookiePrefix = $wgDBname . '_' . $wgDBprefix; } elseif ( $wgSharedDB ) { $wgCookiePrefix = $wgSharedDB; } else { - $wgCookiePrefix = str_replace("+", "", $wgDBname); + $wgCookiePrefix = $wgDBname; } +$wgCookiePrefix = strtr($wgCookiePrefix, "=,; +.\"'\\[", "__________"); -session_name( $wgCookiePrefix . '_session' ); +# If session.auto_start is there, we can't touch session name +# +if( !ini_get( 'session.auto_start' ) ) + session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' ); -if( !$wgCommandLineMode && ( isset( $_COOKIE[session_name()] ) || isset( $_COOKIE[$wgCookiePrefix.'Token'] ) ) ) { - User::SetupSession(); +if( !$wgCommandLineMode && ( $wgRequest->checkSessionCookie() || isset( $_COOKIE[$wgCookiePrefix.'Token'] ) ) ) { + wfIncrStats( 'request_with_session' ); + wfSetupSession(); $wgSessionStarted = true; } else { + wfIncrStats( 'request_without_session' ); $wgSessionStarted = false; } wfProfileOut( $fname.'-SetupSession' ); -wfProfileIn( $fname.'-database' ); +wfProfileIn( $fname.'-globals' ); if ( !$wgDBservers ) { $wgDBservers = array(array( @@ -148,162 +224,55 @@ 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT )); } -$wgLoadBalancer = LoadBalancer::newFromParams( $wgDBservers, false, $wgMasterWaitTimeout ); -$wgLoadBalancer->loadMasterPos(); - -wfProfileOut( $fname.'-database' ); -wfProfileIn( $fname.'-language1' ); - -require_once( "$IP/languages/Language.php" ); - -function setupLangObj($langclass) { - global $IP; - - if( ! class_exists( $langclass ) ) { - # Default to English/UTF-8 - $baseclass = 'LanguageUtf8'; - require_once( "$IP/languages/$baseclass.php" ); - $lc = strtolower(substr($langclass, 8)); - $snip = " - class $langclass extends $baseclass { - function getVariants() { - return array(\"$lc\"); - } - - }"; - eval($snip); - } - - $lang = new $langclass(); - - return $lang; -} -# $wgLanguageCode may be changed later to fit with user preference. -# The content language will remain fixed as per the configuration, -# so let's keep it. -$wgContLanguageCode = $wgLanguageCode; -$wgContLangClass = 'Language' . str_replace( '-', '_', ucfirst( $wgContLanguageCode ) ); +$wgLoadBalancer = new StubObject( 'wgLoadBalancer', 'LoadBalancer', + array( $wgDBservers, false, $wgMasterWaitTimeout, true ) ); +$wgContLang = new StubContLang; + +// Now that variant lists may be available... +$wgRequest->interpolateTitle(); + +$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() ) ); -$wgContLang = setupLangObj( $wgContLangClass ); -$wgContLang->initEncoding(); - -wfProfileOut( $fname.'-language1' ); +wfProfileOut( $fname.'-globals' ); wfProfileIn( $fname.'-User' ); # 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 ) { - $func(); + call_user_func( $func ); } if( !is_object( $wgAuth ) ) { - require_once( 'AuthPlugin.php' ); - $wgAuth = new AuthPlugin(); -} - -if( $wgCommandLineMode ) { - # Used for some maintenance scripts; user session cookies can screw things up - # when the database is in an in-between state. - $wgUser = new User(); - # Prevent loading User settings from the DB. - $wgUser->setLoaded( true ); -} else { - $wgUser = null; - wfRunHooks('AutoAuthenticate',array(&$wgUser)); - if ($wgUser === null) { - $wgUser = User::loadFromSession(); - } + $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' ); + wfRunHooks( 'AuthPluginSetup', array( &$wgAuth ) ); } wfProfileOut( $fname.'-User' ); -wfProfileIn( $fname.'-language2' ); - -// wgLanguageCode now specifically means the UI language -$wgLanguageCode = $wgRequest->getText('uselang', ''); -if ($wgLanguageCode == '') - $wgLanguageCode = $wgUser->getOption('language'); -# Validate $wgLanguageCode, which will soon be sent to an eval() -if( empty( $wgLanguageCode ) || !preg_match( '/^[a-z]+(-[a-z]+)?$/', $wgLanguageCode ) ) { - $wgLanguageCode = $wgContLanguageCode; -} - -$wgLangClass = 'Language'. str_replace( '-', '_', ucfirst( $wgLanguageCode ) ); - -if( $wgLangClass == $wgContLangClass ) { - $wgLang = &$wgContLang; -} else { - wfSuppressWarnings(); - include_once("$IP/languages/$wgLangClass.php"); - wfRestoreWarnings(); - - $wgLang = setupLangObj( $wgLangClass ); -} - -wfProfileOut( $fname.'-language2' ); -wfProfileIn( $fname.'-MessageCache' ); - -$wgMessageCache = new MessageCache; -$wgMessageCache->initialise( $parserMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry, $wgDBname); - -wfProfileOut( $fname.'-MessageCache' ); - -# -# I guess the warning about UI switching might still apply... -# -# FIXME: THE ABOVE MIGHT BREAK NAMESPACES, VARIABLES, -# SEARCH INDEX UPDATES, AND MANY MANY THINGS. -# DO NOT USE THIS MODE EXCEPT FOR TESTING RIGHT NOW. -# -# To disable it, the easiest thing could be to uncomment the -# following; they should effectively disable the UI switch functionality -# -# $wgLangClass = $wgContLangClass; -# $wgLanguageCode = $wgContLanguageCode; -# $wgLang = $wgContLang; -# -# TODO: Need to change reference to $wgLang to $wgContLang at proper -# places, including namespaces, dates in signatures, magic words, -# and links -# -# TODO: Need to look at the issue of input/output encoding -# - - -wfProfileIn( $fname.'-OutputPage' ); - -$wgOut = new OutputPage(); -wfProfileOut( $fname.'-OutputPage' ); -wfProfileIn( $fname.'-BlockCache' ); - -$wgBlockCache = new BlockCache( true ); - -wfProfileOut( $fname.'-BlockCache' ); wfProfileIn( $fname.'-misc2' ); $wgDeferredUpdateList = array(); $wgPostCommitUpdateList = array(); -$wgLinkCache = new LinkCache(); -$wgMagicWords = array(); -$wgMwRedir =& MagicWord::get( MAG_REDIRECT ); -$wgParserCache = new ParserCache( $messageMemc ); - -if ( $wgUseXMLparser ) { - require_once( 'ParserXML.php' ); - $wgParser = new ParserXML(); -} else { - $wgParser = new Parser(); -} -$wgOut->setParserOptions( ParserOptions::newFromUser( $wgUser ) ); -$wgMsgParserOptions = ParserOptions::newFromUser($wgUser); +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 = Title::makeTitle( NS_SPECIAL, 'Error' ); -$wgArticle = new Article($wgTitle); +$wgTitle = null; +$wgArticle = null; wfProfileOut( $fname.'-misc2' ); wfProfileIn( $fname.'-extensions' ); @@ -313,13 +282,22 @@ # of the extension file. This allows the extension to perform # any necessary initialisation in the fully initialised environment foreach ( $wgExtensionFunctions as $func ) { - $func(); + $profName = $fname.'-extensions-'.strval( $func ); + wfProfileIn( $profName ); + call_user_func( $func ); + wfProfileOut( $profName ); } -wfDebug( "\n" ); +// For compatibility +wfRunHooks( 'LogPageValidTypes', array( &$wgLogTypes ) ); +wfRunHooks( 'LogPageLogName', array( &$wgLogNames ) ); +wfRunHooks( 'LogPageLogHeader', array( &$wgLogHeaders ) ); +wfRunHooks( 'LogPageActionText', array( &$wgLogActions ) ); + + +wfDebug( "Fully initialised\n" ); $wgFullyInitialised = true; wfProfileOut( $fname.'-extensions' ); wfProfileOut( $fname ); -} -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SiteConfiguration.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SiteConfiguration.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SiteConfiguration.php 2005-07-10 20:46:58.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SiteConfiguration.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,37 +1,39 @@ 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']; + 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; } + if ( !is_null( $retval ) && count( $params ) ) { foreach ( $params as $key => $value ) { $retval = str_replace( '$' . $key, $value, $retval ); @@ -41,20 +43,29 @@ } /** */ + function getAll( $wiki, $suffix, $params ) { + $localSettings = array(); + foreach ( $this->settings as $varname => $stuff ) { + $value = $this->get( $varname, $wiki, $suffix, $params ); + if ( !is_null( $value ) ) { + $localSettings[$varname] = $value; + } + } + return $localSettings; + } + + /** */ function getBool( $setting, $wiki, $suffix ) { return (bool)($this->get( $setting, $wiki, $suffix )); } /** */ function &getLocalDatabases() { - return $this->localDatabases; + return $this->wikis; } /** */ function initialise() { - foreach ( $this->wikis as $db ) { - $this->localDatabases[$db] = $db; - } } /** */ @@ -104,5 +115,5 @@ } } } - -?> + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Skin.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Skin.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/Skin.php 2005-08-24 20:45:45.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/Skin.php 2007-08-23 18:34:12.000000000 -0400 @@ -1,114 +1,212 @@ read())) { - if(preg_match('/^([^.].*)\.php$/',$file, $matches)) { - $aSkin = $matches[1]; - $wgValidSkinNames[strtolower($aSkin)] = $aSkin; - } -} -$skinDir->close(); -unset($matches); - -require_once( 'RecentChange.php' ); /** - * @todo document - * @package MediaWiki - */ -class RCCacheEntry extends RecentChange -{ - var $secureName, $link; - var $curlink , $difflink, $lastlink , $usertalklink , $versionlink ; - var $userlink, $timestamp, $watched; - - function newFromParent( $rc ) - { - $rc2 = new RCCacheEntry; - $rc2->mAttribs = $rc->mAttribs; - $rc2->mExtra = $rc->mExtra; - return $rc2; - } -} ; - - -/** - * The main skin class that provide methods and properties for all other skins - * including PHPTal skins. + * The main skin class that provide methods and properties for all other skins. * This base class is also the "Standard" skin. - * @package MediaWiki + * + * See docs/skin.txt for more information. + * + * @addtogroup Skins */ class Skin extends Linker { /**#@+ - * @access private + * @private */ var $lastdate, $lastline; var $rc_cache ; # Cache for Enhanced Recent Changes var $rcCacheIndex ; # Recent Changes Cache Counter for visibility toggle var $rcMoveIndex; + var $mWatchLinkNum = 0; // Appended to end of watch link id's /**#@-*/ + protected $mRevisionId; // The revision ID we're looking at, null if not applicable. + protected $skinname = 'standard' ; /** Constructor, call parent constructor */ - function Skin() { parent::Linker(); } + function Skin() { parent::__construct(); } - function getSkinNames() { + /** + * Fetch the set of available skins. + * @return array of strings + * @static + */ + static function getSkinNames() { global $wgValidSkinNames; + static $skinsInitialised = false; + if ( !$skinsInitialised ) { + # Get a list of available skins + # Build using the regular expression '^(.*).php$' + # Array keys are all lower case, array value keep the case used by filename + # + wfProfileIn( __METHOD__ . '-init' ); + global $wgStyleDirectory; + $skinDir = dir( $wgStyleDirectory ); + + # while code from www.php.net + while (false !== ($file = $skinDir->read())) { + // Skip non-PHP files, hidden files, and '.dep' includes + $matches = array(); + if(preg_match('/^([^.]*)\.php$/',$file, $matches)) { + $aSkin = $matches[1]; + $wgValidSkinNames[strtolower($aSkin)] = $aSkin; + } + } + $skinDir->close(); + $skinsInitialised = true; + wfProfileOut( __METHOD__ . '-init' ); + } return $wgValidSkinNames; } + /** + * Normalize a skin preference value to a form that can be loaded. + * If a skin can't be found, it will fall back to the configured + * default (or the old 'Classic' skin if that's broken). + * @param string $key + * @return string + * @static + */ + static function normalizeKey( $key ) { + global $wgDefaultSkin; + $skinNames = Skin::getSkinNames(); + + if( $key == '' ) { + // Don't return the default immediately; + // in a misconfiguration we need to fall back. + $key = $wgDefaultSkin; + } + + if( isset( $skinNames[$key] ) ) { + return $key; + } + + // Older versions of the software used a numeric setting + // in the user preferences. + $fallback = array( + 0 => $wgDefaultSkin, + 1 => 'nostalgia', + 2 => 'cologneblue' ); + + if( isset( $fallback[$key] ) ){ + $key = $fallback[$key]; + } + + if( isset( $skinNames[$key] ) ) { + return $key; + } else { + // The old built-in skin + return 'standard'; + } + } + + /** + * Factory method for loading a skin of a given type + * @param string $key 'monobook', 'standard', etc + * @return Skin + * @static + */ + static function &newFromKey( $key ) { + global $wgStyleDirectory; + + $key = Skin::normalizeKey( $key ); + + $skinNames = Skin::getSkinNames(); + $skinName = $skinNames[$key]; + + # Grab the skin class and initialise it. + // Preload base classes to work around APC/PHP5 bug + $deps = "{$wgStyleDirectory}/{$skinName}.deps.php"; + if( file_exists( $deps ) ) include_once( $deps ); + require_once( "{$wgStyleDirectory}/{$skinName}.php" ); + + # Check if we got if not failback to default skin + $className = 'Skin'.$skinName; + if( !class_exists( $className ) ) { + # DO NOT die if the class isn't found. This breaks maintenance + # scripts and can cause a user account to be unrecoverable + # except by SQL manipulation if a previously valid skin name + # is no longer valid. + wfDebug( "Skin class does not exist: $className\n" ); + $className = 'SkinStandard'; + require_once( "{$wgStyleDirectory}/Standard.php" ); + } + $skin = new $className; + return $skin; + } + /** @return string path to the skin stylesheet */ - function getStylesheet() { return 'common/wikistandard.css'; } + function getStylesheet() { + return 'common/wikistandard.css'; + } /** @return string skin name */ - function getSkinName() { - return 'standard'; + public function getSkinName() { + return $this->skinname; } function qbSetting() { global $wgOut, $wgUser; if ( $wgOut->isQuickbarSuppressed() ) { return 0; } - $q = $wgUser->getOption( 'quickbar' ); - if ( '' == $q ) { $q = 0; } + $q = $wgUser->getOption( 'quickbar', 0 ); return $q; } function initPage( &$out ) { + global $wgFavicon, $wgScriptPath, $wgSitename, $wgLanguageCode, $wgLanguageNames; + $fname = 'Skin::initPage'; wfProfileIn( $fname ); - $out->addLink( array( 'rel' => 'shortcut icon', 'href' => '/favicon.ico' ) ); + if( false !== $wgFavicon ) { + $out->addLink( array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) ); + } + + # OpenSearch description link + $out->addLink( array( + 'rel' => 'search', + 'type' => 'application/opensearchdescription+xml', + 'href' => "$wgScriptPath/opensearch_desc.php", + 'title' => "$wgSitename ({$wgLanguageNames[$wgLanguageCode]})", + )); $this->addMetadataLinks($out); + $this->mRevisionId = $out->mRevisionId; + + $this->preloadExistence(); + wfProfileOut( $fname ); } + /** + * Preload the existence of three commonly-requested pages in a single query + */ + function preloadExistence() { + global $wgUser, $wgTitle; + + // User/talk link + $titles = array( $wgUser->getUserPage(), $wgUser->getTalkPage() ); + + // Other tab link + if ( $wgTitle->getNamespace() == NS_SPECIAL ) { + // nothing + } elseif ( $wgTitle->isTalkPage() ) { + $titles[] = $wgTitle->getSubjectPage(); + } else { + $titles[] = $wgTitle->getTalkPage(); + } + + $lb = new LinkBatch( $titles ); + $lb->execute(); + } + function addMetadataLinks( &$out ) { - global $wgTitle, $wgEnableDublinCoreRdf, $wgEnableCreativeCommonsRdf, $wgRdfMimeType, $action; - global $wgRightsPage, $wgRightsUrl, $wgUseTrackbacks; + global $wgTitle, $wgEnableDublinCoreRdf, $wgEnableCreativeCommonsRdf; + global $wgRightsPage, $wgRightsUrl; if( $out->isArticleRelated() ) { # note: buggy CC software only reads first "meta" link @@ -145,7 +243,7 @@ function outputPage( &$out ) { global $wgDebugComments; - wfProfileIn( 'Skin::outputPage' ); + wfProfileIn( __METHOD__ ); $this->initPage( $out ); $out->out( $out->headElement() ); @@ -167,18 +265,105 @@ $out->out( $this->afterContent() ); - wfProfileClose(); + $out->out( $this->bottomScripts() ); + $out->out( $out->reportTime() ); $out->out( "\n" ); + wfProfileOut( __METHOD__ ); + } + + static function makeVariablesScript( $data ) { + global $wgJsMimeType; + + $r = "\n"; + + return $r; } - function getHeadScripts() { - global $wgStylePath, $wgUser, $wgContLang, $wgAllowUserJs, $wgJsMimeType; - $r = "\n"; - if( $wgAllowUserJs && $wgUser->isLoggedIn() ) { + /** + * Make a
            '.htmlspecialchars($navText).'
            '); + wfProfileOut( __METHOD__ . '-setup' ); + + wfProfileIn( __METHOD__ . '-output' ); + if ( $ot == 'php' ) { + $navText .= makePhp( $messages ); + $wgOut->addHTML( 'PHP | HTML
            ' . htmlspecialchars( $navText ) . '
            ' ); } else { - $wgOut->addHTML( 'PHP | HTML' ); + $wgOut->addHTML( 'PHP | HTML' ); $wgOut->addWikiText( $navText ); $wgOut->addHTML( makeHTMLText( $messages ) ); } - wfProfileOut( "$fname-output" ); - - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ . '-output' ); + + wfProfileOut( __METHOD__ ); } /** - * + * Create the messages array, formatted in PHP to copy to language files. + * @param $messages Messages array. + * @return The PHP messages array. + * @todo Make suitable for language files. */ -function makePhp($messages) { - global $wgLanguageCode; - $txt = "\n\n".'$wgAllMessages'.ucfirst($wgLanguageCode).' = array('."\n"; +function makePhp( $messages ) { + global $wgLang; + $txt = "\n\n\$messages = array(\n"; foreach( $messages as $key => $m ) { - if(strtolower($wgLanguageCode) != 'en' and $m['msg'] == $m['enmsg'] ) { - if (strstr($m['msg'],"\n")) { - $txt.='/* '; - $comment=' */'; - } else { - $txt .= '#'; - $comment = ''; - } - } elseif ($m['msg'] == '<'.$key.'>'){ + if( $wgLang->getCode() != 'en' && $m['msg'] == $m['enmsg'] ) { + continue; + } else if ( wfEmptyMsg( $key, $m['msg'] ) ) { $m['msg'] = ''; $comment = ' #empty'; } else { $comment = ''; } - $txt .= "'$key' => '" . preg_replace( "/(? '" . preg_replace( '/(?getSkin(); - $talk = $wgLang->getNsText( NS_TALK ); - $mwnspace = $wgLang->getNsText( NS_MEDIAWIKI ); - $mwtalk = $wgLang->getNsText( NS_MEDIAWIKI_TALK ); - $txt = " - - - - + global $wgLang, $wgContLang, $wgUser; + wfProfileIn( __METHOD__ ); + + $sk = $wgUser->getSkin(); + $talk = wfMsg( 'talkpagelinktext' ); + + $input = wfElement( 'input', array( + 'type' => 'text', + 'id' => 'allmessagesinput', + 'onkeyup' => 'allmessagesfilter()' + ), '' ); + $checkbox = wfElement( 'input', array( + 'type' => 'button', + 'value' => wfMsgHtml( 'allmessagesmodified' ), + 'id' => 'allmessagescheckbox', + 'onclick' => 'allmessagesmodified()' + ), '' ); + + $txt = ''; + + $txt .= ' +
            " . wfMsgHtml('allmessagesname') . "" . wfMsgHtml('allmessagesdefault') . "
            + + + - - "; - - wfProfileIn( "$fname-check" ); + + '; + + wfProfileIn( __METHOD__ . "-check" ); + # This is a nasty hack to avoid doing independent existence checks # without sending the links and table through the slow wiki parser. $pageExists = array( NS_MEDIAWIKI => array(), NS_MEDIAWIKI_TALK => array() ); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $page = $dbr->tableName( 'page' ); $sql = "SELECT page_namespace,page_title FROM $page WHERE page_namespace IN (" . NS_MEDIAWIKI . ", " . NS_MEDIAWIKI_TALK . ")"; $res = $dbr->query( $sql ); @@ -120,68 +132,69 @@ $pageExists[$s->page_namespace][$s->page_title] = true; } $dbr->freeResult( $res ); - wfProfileOut( "$fname-check" ); + wfProfileOut( __METHOD__ . "-check" ); - wfProfileIn( "$fname-output" ); + wfProfileIn( __METHOD__ . "-output" ); - foreach( $messages as $key => $m ) { + $i = 0; + foreach( $messages as $key => $m ) { $title = $wgLang->ucfirst( $key ); - if($wgLanguageCode != $wgContLanguageCode) - $title.="/$wgLanguageCode"; + if( $wgLang->getCode() != $wgContLang->getCode() ) { + $title .= '/' . $wgLang->getCode(); + } $titleObj =& Title::makeTitle( NS_MEDIAWIKI, $title ); $talkPage =& Title::makeTitle( NS_MEDIAWIKI_TALK, $title ); - $changed = ($m['statmsg'] != $m['msg']); + $changed = ( $m['statmsg'] != $m['msg'] ); $message = htmlspecialchars( $m['statmsg'] ); $mw = htmlspecialchars( $m['msg'] ); - - #$pageLink = $sk->makeLinkObj( $titleObj, htmlspecialchars( $key ) ); - #$talkLink = $sk->makeLinkObj( $talkPage, htmlspecialchars( $talk ) ); + if( isset( $pageExists[NS_MEDIAWIKI][$title] ) ) { - $pageLink = $sk->makeKnownLinkObj( $titleObj, htmlspecialchars( $key ) ); + $pageLink = $sk->makeKnownLinkObj( $titleObj, "" . htmlspecialchars( $key ) . '' ); } else { - $pageLink = $sk->makeBrokenLinkObj( $titleObj, htmlspecialchars( $key ) ); + $pageLink = $sk->makeBrokenLinkObj( $titleObj, "" . htmlspecialchars( $key ) . '' ); } if( isset( $pageExists[NS_MEDIAWIKI_TALK][$title] ) ) { $talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) ); } else { $talkLink = $sk->makeBrokenLinkObj( $talkPage, htmlspecialchars( $talk ) ); } + + $anchor = 'msg_' . htmlspecialchars( strtolower( $title ) ); + $anchor = ""; - if($changed) { - - $txt .= - " - + - + "; } else { - - $txt .= - " + $txt .= " + "; - } + $i++; } - $txt .= "
            ' . wfMsgHtml( 'allmessagesname' ) . '' . wfMsgHtml( 'allmessagesdefault' ) . '
            " . wfMsgHtml('allmessagescurrent') . "
            ' . wfMsgHtml( 'allmessagescurrent' ) . '
            - $pageLink
            $talkLink + if( $changed ) { + $txt .= " +
            + $anchor$pageLink
            $talkLink
            $message
            $mw
            - $pageLink
            $talkLink + $anchor$pageLink
            $talkLink
            $mw
            "; - wfProfileOut( "$fname-output" ); + $txt .= ''; + wfProfileOut( __METHOD__ . '-output' ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $txt; } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialAllpages.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialAllpages.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialAllpages.php 2005-08-02 07:08:19.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialAllpages.php 2007-09-02 14:03:10.000000000 -0400 @@ -1,116 +1,130 @@ getVal( 'from' ); $namespace = $wgRequest->getInt( 'namespace' ); - + $namespaces = $wgContLang->getNamespaces(); - if( !in_array($namespace, array_keys($namespaces)) ) - $namespace = 0; + $indexPage = new SpecialAllpages(); - $wgOut->setPagetitle( $namespace > 0 ? - wfMsg( 'allinnamespace', $namespaces[$namespace] ) : + $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ? + wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) : wfMsg( 'allarticles' ) ); - + if ( isset($par) ) { - indexShowChunk( $namespace, $par, $specialPage->including() ); + $indexPage->showChunk( $namespace, $par, $specialPage->including() ); } elseif ( isset($from) ) { - indexShowChunk( $namespace, $from, $specialPage->including() ); + $indexPage->showChunk( $namespace, $from, $specialPage->including() ); } else { - indexShowToplevel ( $namespace, $specialPage->including() ); + $indexPage->showToplevel ( $namespace, $specialPage->including() ); } } /** + * Implements Special:Allpages + * @addtogroup SpecialPage + */ +class SpecialAllpages { + var $maxPerPage=960; + var $topLevelMax=50; + var $name='Allpages'; + # Determines, which message describes the input field 'nsfrom' (->SpecialPrefixindex.php) + var $nsfromMsg='allpagesfrom'; + +/** * HTML for the top form * @param integer $namespace A namespace constant (default NS_MAIN). * @param string $from Article name we are starting listing at. */ -function indexNamespaceForm ( $namespace = NS_MAIN, $from = '' ) { - global $wgContLang, $wgScript; - $t = Title::makeTitle( NS_SPECIAL, "Allpages" ); - - $namespaceselect = HTMLnamespaceselector($namespace, null); - - $frombox = "'; - $submitbutton = ''; - - $out = "
            "; - $out .= ''; - $out .= " - - - - - - - - - -
            " . wfMsgHtml('allpagesfrom') . "
            - $namespaceselect $submitbutton -
            -"; - $out .= '
            '; - return $out; +function namespaceForm ( $namespace = NS_MAIN, $from = '' ) { + global $wgScript, $wgContLang; + $t = SpecialPage::getTitleFor( $this->name ); + $align = $wgContLang->isRtl() ? 'left' : 'right'; + + $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); + $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + $out .= Xml::hidden( 'title', $t->getPrefixedText() ); + $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) ); + $out .= " + " . + Xml::label( wfMsg( $this->nsfromMsg ), 'nsfrom' ) . + " + " . + Xml::input( 'from', 20, htmlspecialchars ( $from ), array( 'id' => 'nsfrom' ) ) . + " + + + " . + Xml::label( wfMsg( 'namespace' ), 'namespace' ) . + " + " . + Xml::namespaceSelector( $namespace, null ) . + Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . + " + "; + $out .= Xml::closeElement( 'table' ); + $out .= Xml::closeElement( 'form' ); + $out .= Xml::closeElement( 'div' ); + return $out; } /** * @param integer $namespace (default NS_MAIN) */ -function indexShowToplevel ( $namespace = NS_MAIN, $including = false ) { - global $wgOut, $indexMaxperpage, $toplevelMaxperpage, $wgContLang, $wgRequest, $wgUser; - $sk = $wgUser->getSkin(); +function showToplevel ( $namespace = NS_MAIN, $including = false ) { + global $wgOut, $wgContLang; $fname = "indexShowToplevel"; + $align = $wgContLang->isRtl() ? 'left' : 'right'; # TODO: Either make this *much* faster or cache the title index points # in the querycache table. - $dbr =& wfGetDB( DB_SLAVE ); - $page = $dbr->tableName( 'page' ); - $fromwhere = "FROM $page WHERE page_namespace=$namespace"; - $order_arr = array ( 'ORDER BY' => 'page_title' ); - $order_str = 'ORDER BY page_title'; + $dbr = wfGetDB( DB_SLAVE ); $out = ""; $where = array( 'page_namespace' => $namespace ); - global $wgMemc, $wgDBname; - $key = "$wgDBname:allpages:ns:$namespace"; + global $wgMemc; + $key = wfMemcKey( 'allpages', 'ns', $namespace ); $lines = $wgMemc->get( $key ); - + if( !is_array( $lines ) ) { - $firstTitle = $dbr->selectField( 'page', 'page_title', $where, $fname, array( 'LIMIT' => 1 ) ); + $options = array( 'LIMIT' => 1 ); + if ( ! $dbr->implicitOrderby() ) { + $options['ORDER BY'] = 'page_title'; + } + $firstTitle = $dbr->selectField( 'page', 'page_title', $where, $fname, $options ); $lastTitle = $firstTitle; - + # This array is going to hold the page_titles in order. $lines = array( $firstTitle ); - + # If we are going to show n rows, we need n+1 queries to find the relevant titles. $done = false; for( $i = 0; !$done; ++$i ) { // Fetch the last title of this chunk and the first of the next $chunk = is_null( $lastTitle ) - ? '1=1' + ? '' : 'page_title >= ' . $dbr->addQuotes( $lastTitle ); - $sql = "SELECT page_title $fromwhere AND $chunk $order_str " . - $dbr->limitResult( 2, $indexMaxperpage - 1 ); - $res = $dbr->query( $sql, $fname ); + $res = $dbr->select( + 'page', /* FROM */ + 'page_title', /* WHAT */ + $where + array( $chunk), + $fname, + array ('LIMIT' => 2, 'OFFSET' => $this->maxPerPage - 1, 'ORDER BY' => 'page_title') ); + if ( $s = $dbr->fetchObject( $res ) ) { array_push( $lines, $s->page_title ); } else { @@ -135,25 +149,24 @@ } $wgMemc->add( $key, $lines, 3600 ); } - + // If there are only two or less sections, don't even display them. // Instead, display the first section directly. if( count( $lines ) <= 2 ) { - indexShowChunk( $namespace, '', false, $including ); + $this->showChunk( $namespace, '', $including ); return; } # At this point, $lines should contain an even number of elements. - $out .= ""; + $out .= "
            "; while ( count ( $lines ) > 0 ) { $inpoint = array_shift ( $lines ); $outpoint = array_shift ( $lines ); - $out .= indexShowline ( $inpoint, $outpoint, $namespace, false ); + $out .= $this->showline ( $inpoint, $outpoint, $namespace, false ); } $out .= '
            '; - - $nsForm = indexNamespaceForm ( $namespace, '', false ); - + $nsForm = $this->namespaceForm ( $namespace, '', false ); + # Is there more? if ( $including ) { $out2 = ''; @@ -161,8 +174,8 @@ $morelinks = ''; if ( $morelinks != '' ) { $out2 = ''; - $out2 .= '
            ' . $nsForm; - $out2 .= ''; + $out2 .= '
            ' . $nsForm; + $out2 .= ''; $out2 .= $morelinks . '

            '; } else { $out2 = $nsForm . '
            '; @@ -174,106 +187,201 @@ /** * @todo Document - * @param string $from + * @param string $from * @param integer $namespace (Default NS_MAIN) */ -function indexShowline( $inpoint, $outpoint, $namespace = NS_MAIN ) { - global $wgOut, $wgLang, $wgUser; - $sk = $wgUser->getSkin(); - $dbr =& wfGetDB( DB_SLAVE ); - +function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) { + global $wgContLang; + $align = $wgContLang->isRtl() ? 'left' : 'right'; $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) ); $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) ); $queryparams = ($namespace ? "namespace=$namespace" : ''); - $special = Title::makeTitle( NS_SPECIAL, 'Allpages/' . $inpoint ); + $special = SpecialPage::getTitleFor( $this->name, $inpoint ); $link = $special->escapeLocalUrl( $queryparams ); - - $out = wfMsg( + + $out = wfMsgHtml( 'alphaindexline', "$inpointf", - "$outpointf" + "$outpointf" ); - return ''.$out.''; + return ''.$out.''; } /** * @param integer $namespace (Default NS_MAIN) * @param string $from list all pages from this name (default FALSE) */ -function indexShowChunk( $namespace = NS_MAIN, $from, $including = false ) { - global $wgOut, $wgUser, $indexMaxperpage, $wgContLang; +function showChunk( $namespace = NS_MAIN, $from, $including = false ) { + global $wgOut, $wgUser, $wgContLang; $fname = 'indexShowChunk'; - $sk = $wgUser->getSkin(); - $fromTitle = Title::newFromURL( $from ); - $fromKey = is_null( $fromTitle ) ? '' : $fromTitle->getDBkey(); - - $dbr =& wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'page', - array( 'page_namespace', 'page_title', 'page_is_redirect' ), - array( - 'page_namespace' => $namespace, - 'page_title >= ' . $dbr->addQuotes( $fromKey ) - ), - $fname, - array( - 'ORDER BY' => 'page_title', - 'LIMIT' => $indexMaxperpage + 1, - 'USE INDEX' => 'name_title', - ) - ); - - ### FIXME: side link to previous + $fromList = $this->getNamespaceKeyAndText($namespace, $from); + $namespaces = $wgContLang->getNamespaces(); + $align = $wgContLang->isRtl() ? 'left' : 'right'; $n = 0; - $out = ''; - - $namespaces = $wgContLang->getFormattedNamespaces(); - while( ($n < $indexMaxperpage) && ($s = $dbr->fetchObject( $res )) ) { - $t = Title::makeTitle( $s->page_namespace, $s->page_title ); - if( $t ) { - $link = ($s->page_is_redirect ? '
            ' : '' ) . - $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) . - ($s->page_is_redirect ? '
            ' : '' ); - } else { - $link = '[[' . htmlspecialchars( $s->page_title ) . ']]'; - } - if( $n % 3 == 0 ) { - $out .= ''; + + if ( !$fromList ) { + $out = wfMsgWikiHtml( 'allpagesbadtitle' ); + } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) { + // Show errormessage and reset to NS_MAIN + $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace ); + $namespace = NS_MAIN; + } else { + list( $namespace, $fromKey, $from ) = $fromList; + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'page', + array( 'page_namespace', 'page_title', 'page_is_redirect' ), + array( + 'page_namespace' => $namespace, + 'page_title >= ' . $dbr->addQuotes( $fromKey ) + ), + $fname, + array( + 'ORDER BY' => 'page_title', + 'LIMIT' => $this->maxPerPage + 1, + 'USE INDEX' => 'name_title', + ) + ); + + $out = '
            '; + + while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) { + $t = Title::makeTitle( $s->page_namespace, $s->page_title ); + if( $t ) { + $link = ($s->page_is_redirect ? '
            ' : '' ) . + $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) . + ($s->page_is_redirect ? '
            ' : '' ); + } else { + $link = '[[' . htmlspecialchars( $s->page_title ) . ']]'; + } + if( $n % 3 == 0 ) { + $out .= ''; + } + $out .= ""; + $n++; + if( $n % 3 == 0 ) { + $out .= ''; + } } - $out .= ""; - $n++; - if( $n % 3 == 0 ) { + if( ($n % 3) != 0 ) { $out .= ''; } + $out .= '
            $link
            $link
            '; } - if( ($n % 3) != 0 ) { - $out .= ''; - } - $out .= ''; - + if ( $including ) { $out2 = ''; } else { - $nsForm = indexNamespaceForm ( $namespace, $from ); + if( $from == '' ) { + // First chunk; no previous link. + $prevTitle = null; + } else { + # Get the last title from previous chunk + $dbr = wfGetDB( DB_SLAVE ); + $res_prev = $dbr->select( + 'page', + 'page_title', + array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ), + $fname, + array( 'ORDER BY' => 'page_title DESC', 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) ) + ); + + # Get first title of previous complete chunk + if( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) { + $pt = $dbr->fetchObject( $res_prev ); + $prevTitle = Title::makeTitle( $namespace, $pt->page_title ); + } else { + # The previous chunk is not complete, need to link to the very first title + # available in the database + $options = array( 'LIMIT' => 1 ); + if ( ! $dbr->implicitOrderby() ) { + $options['ORDER BY'] = 'page_title'; + } + $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), $fname, $options ); + # Show the previous link if it s not the current requested chunk + if( $from != $reallyFirstPage_title ) { + $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title ); + } else { + $prevTitle = null; + } + } + } + + $nsForm = $this->namespaceForm ( $namespace, $from ); $out2 = ''; - $out2 .= '
            ' . $nsForm; - $out2 .= '' . + $out2 .= '
            ' . $nsForm; + $out2 .= '' . $sk->makeKnownLink( $wgContLang->specialPage( "Allpages" ), - wfMsg ( 'allpages' ) ); - if ( ($n == $indexMaxperpage) && ($s = $dbr->fetchObject( $res )) ) { - $namespaceparam = $namespace ? "&namespace=$namespace" : ""; - $out2 .= " | " . $sk->makeKnownLink( - $wgContLang->specialPage( "Allpages" ), - wfMsg ( 'nextpage', $s->page_title ), - "from=" . wfUrlEncode ( $s->page_title ) . $namespaceparam ); + wfMsgHtml ( 'allpages' ) ); + + $self = SpecialPage::getTitleFor( 'Allpages' ); + + # Do we put a previous link ? + if( isset( $prevTitle ) && $pt = $prevTitle->getText() ) { + $q = 'from=' . $prevTitle->getPartialUrl() . ( $namespace ? '&namespace=' . $namespace : '' ); + $prevLink = $sk->makeKnownLinkObj( $self, wfMsgHTML( 'prevpage', $pt ), $q ); + $out2 .= ' | ' . $prevLink; + } + + if( $n == $this->maxPerPage && $s = $dbr->fetchObject($res) ) { + # $s is the first link of the next chunk + $t = Title::MakeTitle($namespace, $s->page_title); + $q = 'from=' . $t->getPartialUrl() . ( $namespace ? '&namespace=' . $namespace : '' ); + $nextLink = $sk->makeKnownLinkObj( $self, wfMsgHtml( 'nextpage', $t->getText() ), $q ); + $out2 .= ' | ' . $nextLink; } $out2 .= "

            "; } $wgOut->addHtml( $out2 . $out ); + if( isset($prevLink) or isset($nextLink) ) { + $wgOut->addHtml( '

            ' ); + if( isset( $prevLink ) ) { + $wgOut->addHTML( $prevLink ); + } + if( isset( $prevLink ) && isset( $nextLink ) ) { + $wgOut->addHTML( ' | ' ); + } + if( isset( $nextLink ) ) { + $wgOut->addHTML( $nextLink ); + } + $wgOut->addHTML( '

            ' ); + + } + +} + +/** + * @param int $ns the namespace of the article + * @param string $text the name of the article + * @return array( int namespace, string dbkey, string pagename ) or NULL on error + * @static (sort of) + * @access private + */ +function getNamespaceKeyAndText ($ns, $text) { + if ( $text == '' ) + return array( $ns, '', '' ); # shortcut for common case + + $t = Title::makeTitleSafe($ns, $text); + if ( $t && $t->isLocal() ) { + return array( $t->getNamespace(), $t->getDBkey(), $t->getText() ); + } else if ( $t ) { + return NULL; + } + + # try again, in case the problem was an empty pagename + $text = preg_replace('/(#|$)/', 'X$1', $text); + $t = Title::makeTitleSafe($ns, $text); + if ( $t && $t->isLocal() ) { + return array( $t->getNamespace(), '', '' ); + } else { + return NULL; + } +} } ?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialAncientpages.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialAncientpages.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialAncientpages.php 2005-06-28 01:47:36.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialAncientpages.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,19 +1,12 @@ tableName( 'page' ); $revision = $db->tableName( 'revision' ); #$use_index = $db->useIndexClause( 'cur_timestamp' ); # FIXME! this is gone + $epoch = $wgDBtype == 'mysql' ? 'UNIX_TIMESTAMP(rev_timestamp)' : + 'EXTRACT(epoch FROM rev_timestamp)'; return "SELECT 'Ancientpages' as type, page_namespace as namespace, page_title as title, - UNIX_TIMESTAMP(rev_timestamp) as value + $epoch as value FROM $page, $revision WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0 AND page_latest=rev_id"; } - + function sortDescending() { return false; } @@ -51,8 +47,8 @@ $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $result->value ), true ); $title = Title::makeTitle( $result->namespace, $result->title ); - $link = $skin->makeKnownLinkObj( $title, $wgContLang->convert( $title->getPrefixedText() ) ); - return "{$link} ({$d})"; + $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) ); + return wfSpecialList($link, $d); } } @@ -64,4 +60,4 @@ $app->doQuery( $offset, $limit ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialBlockip.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialBlockip.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialBlockip.php 2005-08-25 00:32:20.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialBlockip.php 2007-06-28 21:19:14.000000000 -0400 @@ -2,21 +2,28 @@ /** * Constructor for Special:Blockip page * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ /** * Constructor */ -function wfSpecialBlockip() { +function wfSpecialBlockip( $par ) { global $wgUser, $wgOut, $wgRequest; - if ( ! $wgUser->isAllowed('block') ) { - $wgOut->sysopRequired(); + # Can't block when the database is locked + if( wfReadOnly() ) { + $wgOut->readOnlyPage(); return; } - $ipb = new IPBlockForm(); + + # Permission check + if( !$wgUser->isAllowed( 'block' ) ) { + $wgOut->permissionRequired( 'block' ); + return; + } + + $ipb = new IPBlockForm( $par ); $action = $wgRequest->getVal( 'action' ); if ( 'success' == $action ) { @@ -30,52 +37,63 @@ } /** - * Form object + * Form object for the Special:Blockip page. * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ class IPBlockForm { var $BlockAddress, $BlockExpiry, $BlockReason; +# var $BlockEmail; + + function IPBlockForm( $par ) { + global $wgRequest, $wgUser; - function IPBlockForm() { - global $wgRequest; - $this->BlockAddress = $wgRequest->getVal( 'wpBlockAddress', $wgRequest->getVal( 'ip' ) ); + $this->BlockAddress = $wgRequest->getVal( 'wpBlockAddress', $wgRequest->getVal( 'ip', $par ) ); + $this->BlockAddress = strtr( $this->BlockAddress, '_', ' ' ); $this->BlockReason = $wgRequest->getText( 'wpBlockReason' ); + $this->BlockReasonList = $wgRequest->getText( 'wpBlockReasonList' ); $this->BlockExpiry = $wgRequest->getVal( 'wpBlockExpiry', wfMsg('ipbotheroption') ); $this->BlockOther = $wgRequest->getVal( 'wpBlockOther', '' ); + + # Unchecked checkboxes are not included in the form data at all, so having one + # that is true by default is a bit tricky + $byDefault = !$wgRequest->wasPosted(); + $this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly', $byDefault ); + $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', $byDefault ); + $this->BlockEnableAutoblock = $wgRequest->getBool( 'wpEnableAutoblock', $byDefault ); + $this->BlockEmail = $wgRequest->getBool( 'wpEmailBan', false ); + # Re-check user's rights to hide names, very serious, defaults to 0 + $this->BlockHideName = ( $wgRequest->getBool( 'wpHideName', 0 ) && $wgUser->isAllowed( 'hideuser' ) ) ? 1 : 0; } - + function showForm( $err ) { - global $wgOut, $wgUser, $wgLang; - global $wgRequest, $wgSysopUserBans; + global $wgOut, $wgUser, $wgSysopUserBans, $wgContLang; - $wgOut->setPagetitle( htmlspecialchars( wfMsg( 'blockip' ) ) ); + $wgOut->setPagetitle( wfMsg( 'blockip' ) ); $wgOut->addWikiText( wfMsg( 'blockiptext' ) ); if($wgSysopUserBans) { - $mIpaddress = htmlspecialchars( wfMsg( 'ipadressorusername' ) ); + $mIpaddress = Xml::label( wfMsg( 'ipadressorusername' ), 'mw-bi-target' ); } else { - $mIpaddress = htmlspecialchars( wfMsg( 'ipaddress' ) ); + $mIpaddress = Xml::label( wfMsg( 'ipadress' ), 'mw-bi-target' ); } - $mIpbexpiry = htmlspecialchars( wfMsg( 'ipbexpiry' ) ); - $mIpbother = htmlspecialchars( wfMsg( 'ipbother' ) ); - $mIpbothertime = htmlspecialchars( wfMsg( 'ipbotheroption' ) ); - $mIpbreason = htmlspecialchars( wfMsg( 'ipbreason' ) ); - $mIpbsubmit = htmlspecialchars( wfMsg( 'ipbsubmit' ) ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Blockip' ); + $mIpbexpiry = Xml::label( wfMsg( 'ipbexpiry' ), 'wpBlockExpiry' ); + $mIpbother = Xml::label( wfMsg( 'ipbother' ), 'mw-bi-other' ); + $mIpbothertime = wfMsgHtml( 'ipbotheroption' ); + $mIpbreasonother = Xml::label( wfMsg( 'ipbreason' ), 'wpBlockReasonList' ); + $mIpbreason = Xml::label( wfMsg( 'ipbotherreason' ), 'mw-bi-reason' ); + $mIpbreasonotherlist = wfMsgHtml( 'ipbreasonotherlist' ); + + $titleObj = SpecialPage::getTitleFor( 'Blockip' ); $action = $titleObj->escapeLocalURL( "action=submit" ); + $alignRight = $wgContLang->isRtl() ? 'left' : 'right'; if ( "" != $err ) { $wgOut->setSubtitle( wfMsgHtml( 'formerror' ) ); $wgOut->addHTML( "

            {$err}

            \n" ); } - $scBlockAddress = htmlspecialchars( $this->BlockAddress ); - $scBlockExpiry = htmlspecialchars( $this->BlockExpiry ); - $scBlockReason = htmlspecialchars( $this->BlockReason ); - $scBlockOtherTime = htmlspecialchars( $this->BlockOther ); - $scBlockExpiryOptions = htmlspecialchars( wfMsgForContent( 'ipboptions' ) ); + $scBlockExpiryOptions = wfMsgForContent( 'ipboptions' ); $showblockoptions = $scBlockExpiryOptions != '-'; if (!$showblockoptions) @@ -93,22 +111,62 @@ $blockExpiryFormOptions .= ""; } - $token = htmlspecialchars( $wgUser->editToken() ); - + $scBlockReasonList = wfMsgForContent( 'ipbreason-dropdown' ); + $blockReasonList = ''; + if ( $scBlockReasonList != '' && $scBlockReasonList != '-' ) { + $blockReasonList = ""; + $optgroup = ""; + foreach ( explode( "\n", $scBlockReasonList ) as $option) { + $value = trim( htmlspecialchars($option) ); + if ( $value == '' ) { + continue; + } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) { + // A new group is starting ... + $value = trim( substr( $value, 1 ) ); + $blockReasonList .= "$optgroup"; + $optgroup = ""; + } elseif ( substr( $value, 0, 2) == '**' ) { + // groupmember + $selected = ""; + $value = trim( substr( $value, 2 ) ); + if ( $this->BlockReasonList === $value) + $selected = ' selected="selected"'; + $blockReasonList .= ""; + } else { + // groupless block reason + $selected = ""; + if ( $this->BlockReasonList === $value) + $selected = ' selected="selected"'; + $blockReasonList .= "$optgroup"; + $optgroup = ""; + } + } + $blockReasonList .= $optgroup; + } + + $token = $wgUser->editToken(); + + global $wgStylePath, $wgStyleVersion; $wgOut->addHTML( " +
            - - + "); if ($showblockoptions) { $wgOut->addHTML(" - - + - - + + "); + if ( $blockReasonList != '' ) { + $wgOut->addHTML(" + + + + "); + } + $wgOut->addHTML(" + + + - - - + + - + + + + + - -
            {$mIpaddress}: - + {$mIpaddress} + " . Xml::input( 'wpBlockAddress', 45, $this->BlockAddress, + array( + 'tabindex' => '1', + 'id' => 'mw-bi-target', + 'onchange' => 'updateBlockOptions()' ) ) . "
            {$mIpbexpiry}: + {$mIpbexpiry} @@ -118,42 +176,142 @@ $wgOut->addHTML("
            {$mIpbother}: - + {$mIpbother} + " . Xml::input( 'wpBlockOther', 45, $this->BlockOther, + array( 'tabindex' => '3', 'id' => 'mw-bi-other' ) ) . " +
            {$mIpbreasonother} + +
            {$mIpbreason} + " . Xml::input( 'wpBlockReason', 45, $this->BlockReason, + array( 'tabindex' => '5', 'id' => 'mw-bi-reason', + 'maxlength'=> '200' ) ) . "
            {$mIpbreason}: - +
              + " . wfCheckLabel( wfMsgHtml( 'ipbanononly' ), + 'wpAnonOnly', 'wpAnonOnly', $this->BlockAnonOnly, + array( 'tabindex' => '6' ) ) . "
              + " . wfCheckLabel( wfMsgHtml( 'ipbcreateaccount' ), + 'wpCreateAccount', 'wpCreateAccount', $this->BlockCreateAccount, + array( 'tabindex' => '7' ) ) . " +
              - + + " . wfCheckLabel( wfMsgHtml( 'ipbenableautoblock' ), + 'wpEnableAutoblock', 'wpEnableAutoblock', $this->BlockEnableAutoblock, + array( 'tabindex' => '8' ) ) . "
            - -
            \n" ); + "); + // Allow some users to hide name from block log, blocklist and listusers + if ( $wgUser->isAllowed( 'hideuser' ) ) { + $wgOut->addHTML(" + +   + + " . wfCheckLabel( wfMsgHtml( 'ipbhidename' ), + 'wpHideName', 'wpHideName', $this->BlockHideName, + array( 'tabindex' => '9' ) ) . " + + + "); + } + global $wgSysopEmailBans; + + if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) { + $wgOut->addHTML(" + +   + + " . wfCheckLabel( wfMsgHtml( 'ipbemailban' ), + 'wpEmailBan', 'wpEmailBan', $this->BlockEmail, + array( 'tabindex' => '10' )) . " + + + "); + } + $wgOut->addHTML(" + +   + + " . Xml::submitButton( wfMsg( 'ipbsubmit' ), + array( 'name' => 'wpBlock', 'tabindex' => '11' ) ) . " + + + " . + Xml::hidden( 'wpEditToken', $token ) . +" + +\n" ); + + $wgOut->addHtml( $this->getConvenienceLinks() ); + + $user = User::newFromName( $this->BlockAddress ); + if( is_object( $user ) ) { + $this->showLogFragment( $wgOut, $user->getUserPage() ); + } elseif( preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', $this->BlockAddress ) ) { + $this->showLogFragment( $wgOut, Title::makeTitle( NS_USER, $this->BlockAddress ) ); + } elseif( preg_match( '/^\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}/', $this->BlockAddress ) ) { + $this->showLogFragment( $wgOut, Title::makeTitle( NS_USER, $this->BlockAddress ) ); + } } function doSubmit() { - global $wgOut, $wgUser, $wgLang; - global $wgSysopUserBans, $wgSysopRangeBans; - - $userId = 0; - $this->BlockAddress = trim( $this->BlockAddress ); - $rxIP = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'; + global $wgOut, $wgUser, $wgSysopUserBans, $wgSysopRangeBans; + $userId = 0; + # Expand valid IPv6 addresses, usernames are left as is + $this->BlockAddress = IP::sanitizeIP( $this->BlockAddress ); + # isIPv4() and IPv6() are used for final validation + $rxIP4 = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'; + $rxIP6 = '\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}'; + $rxIP = "($rxIP4|$rxIP6)"; + # Check for invalid specifications - if ( ! preg_match( "/^$rxIP$/", $this->BlockAddress ) ) { - if ( preg_match( "/^($rxIP)\\/(\\d{1,2})$/", $this->BlockAddress, $matches ) ) { + if ( !preg_match( "/^$rxIP$/", $this->BlockAddress ) ) { + $matches = array(); + if ( preg_match( "/^($rxIP4)\\/(\\d{1,2})$/", $this->BlockAddress, $matches ) ) { + # IPv4 + if ( $wgSysopRangeBans ) { + if ( !IP::isIPv4( $this->BlockAddress ) || $matches[2] < 16 || $matches[2] > 32 ) { + $this->showForm( wfMsg( 'ip_range_invalid' ) ); + return; + } + $this->BlockAddress = Block::normaliseRange( $this->BlockAddress ); + } else { + # Range block illegal + $this->showForm( wfMsg( 'range_block_disabled' ) ); + return; + } + } else if ( preg_match( "/^($rxIP6)\\/(\\d{1,3})$/", $this->BlockAddress, $matches ) ) { + # IPv6 if ( $wgSysopRangeBans ) { - if ( $matches[2] > 31 || $matches[2] < 16 ) { + if ( !IP::isIPv6( $this->BlockAddress ) || $matches[2] < 64 || $matches[2] > 128 ) { $this->showForm( wfMsg( 'ip_range_invalid' ) ); return; } @@ -165,9 +323,13 @@ } } else { # Username block - if ( $wgSysopUserBans ) { - $userId = User::idFromName( $this->BlockAddress ); - if ( $userId == 0 ) { + if ( $wgSysopUserBans ) { + $user = User::newFromName( $this->BlockAddress ); + if( !is_null( $user ) && $user->getID() ) { + # Use canonical name + $this->BlockAddress = $user->getName(); + $userId = $user->getID(); + } else { $this->showForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->BlockAddress ) ) ); return; } @@ -178,6 +340,14 @@ } } + $reasonstr = $this->BlockReasonList; + if ( $reasonstr != 'other' && $this->BlockReason != '') { + // Entry from drop down menu + additional comment + $reasonstr .= ': ' . $this->BlockReason; + } elseif ( $reasonstr == 'other' ) { + $reasonstr = $this->BlockReason; + } + $expirestr = $this->BlockExpiry; if( $expirestr == 'other' ) $expirestr = $this->BlockOther; @@ -188,52 +358,138 @@ } if ( $expirestr == 'infinite' || $expirestr == 'indefinite' ) { - $expiry = ''; + $expiry = Block::infinity(); } else { - # Convert GNU-style date, returns -1 on error + # Convert GNU-style date, on error returns -1 for PHP <5.1 and false for PHP >=5.1 $expiry = strtotime( $expirestr ); - if ( $expiry < 0 ) { + if ( $expiry < 0 || $expiry === false ) { $this->showForm( wfMsg( 'ipb_expiry_invalid' ) ); return; } - - $expiry = wfTimestamp( TS_MW, $expiry ); + $expiry = wfTimestamp( TS_MW, $expiry ); } - + # Create block # Note: for a user block, ipb_address is only for display purposes + $block = new Block( $this->BlockAddress, $userId, $wgUser->getID(), + $reasonstr, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly, + $this->BlockCreateAccount, $this->BlockEnableAutoblock, $this->BlockHideName, + $this->BlockEmail); + + if (wfRunHooks('BlockIp', array(&$block, &$wgUser))) { + + if ( !$block->insert() ) { + $this->showForm( wfMsg( 'ipb_already_blocked', + htmlspecialchars( $this->BlockAddress ) ) ); + return; + } - $ban = new Block( $this->BlockAddress, $userId, $wgUser->getID(), - $this->BlockReason, wfTimestampNow(), 0, $expiry ); - - if (wfRunHooks('BlockIp', array(&$ban, &$wgUser))) { - - $ban->insert(); - - wfRunHooks('BlockIpComplete', array($ban, $wgUser)); - - # Make log entry - $log = new LogPage( 'block' ); - $log->addEntry( 'block', Title::makeTitle( NS_USER, $this->BlockAddress ), - $this->BlockReason, $expirestr ); + wfRunHooks('BlockIpComplete', array($block, $wgUser)); + + # Prepare log parameters + $logParams = array(); + $logParams[] = $expirestr; + $logParams[] = $this->blockLogFlags(); + + # Make log entry, if the name is hidden, put it in the oversight log + $log_type = ($this->BlockHideName) ? 'oversight' : 'block'; + $log = new LogPage( $log_type ); + $log->addEntry( 'block', Title::makeTitle( NS_USER, $this->BlockAddress ), + $reasonstr, $logParams ); # Report to the user - $titleObj = Title::makeTitle( NS_SPECIAL, 'Blockip' ); + $titleObj = SpecialPage::getTitleFor( 'Blockip' ); $wgOut->redirect( $titleObj->getFullURL( 'action=success&ip=' . urlencode( $this->BlockAddress ) ) ); } } function showSuccess() { - global $wgOut, $wgUser; + global $wgOut; $wgOut->setPagetitle( wfMsg( 'blockip' ) ); $wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) ); $text = wfMsg( 'blockipsuccesstext', $this->BlockAddress ); $wgOut->addWikiText( $text ); } + + function showLogFragment( $out, $title ) { + $out->addHtml( wfElement( 'h2', NULL, LogPage::logName( 'block' ) ) ); + $request = new FauxRequest( array( 'page' => $title->getPrefixedText(), 'type' => 'block' ) ); + $viewer = new LogViewer( new LogReader( $request ) ); + $viewer->showList( $out ); + } + + /** + * Return a comma-delimited list of "flags" to be passed to the log + * reader for this block, to provide more information in the logs + * + * @return array + */ + private function blockLogFlags() { + $flags = array(); + if( $this->BlockAnonOnly && IP::isIPAddress( $this->BlockAddress ) ) + // when blocking a user the option 'anononly' is not available/has no effect -> do not write this into log + $flags[] = 'anononly'; + if( $this->BlockCreateAccount ) + $flags[] = 'nocreate'; + if( !$this->BlockEnableAutoblock ) + $flags[] = 'noautoblock'; + if ( $this->BlockEmail ) + $flags[] = 'noemail'; + return implode( ',', $flags ); + } + + /** + * Builds unblock and block list links + * + * @return string + */ + private function getConvenienceLinks() { + global $wgUser; + $skin = $wgUser->getSkin(); + $links[] = $skin->makeLink ( 'MediaWiki:ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) ); + $links[] = $this->getUnblockLink( $skin ); + $links[] = $this->getBlockListLink( $skin ); + return ''; + } + + /** + * Build a convenient link to unblock the given username or IP + * address, if available; otherwise link to a blank unblock + * form + * + * @param $skin Skin to use + * @return string + */ + private function getUnblockLink( $skin ) { + $list = SpecialPage::getTitleFor( 'Ipblocklist' ); + if( $this->BlockAddress ) { + $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) ); + return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock-addr', $addr ), + 'action=unblock&ip=' . urlencode( $this->BlockAddress ) ); + } else { + return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock' ), 'action=unblock' ); + } + } + + /** + * Build a convenience link to the block list + * + * @param $skin Skin to use + * @return string + */ + private function getBlockListLink( $skin ) { + $list = SpecialPage::getTitleFor( 'Ipblocklist' ); + if( $this->BlockAddress ) { + $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) ); + return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist-addr', $addr ), + 'ip=' . urlencode( $this->BlockAddress ) ); + } else { + return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist' ) ); + } + } } -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialBlockme.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialBlockme.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialBlockme.php 2004-09-03 19:00:00.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialBlockme.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,21 +1,21 @@ addWikiText( wfMsg( "disabled" ) ); + $ip = wfGetIP(); + + if( !$wgBlockOpenProxies || $wgRequest->getText( 'ip' ) != md5( $ip . $wgProxyKey ) ) { + $wgOut->addWikiText( wfMsg( 'disabled' ) ); return; - } + } $blockerName = wfMsg( "proxyblocker" ); $reason = wfMsg( "proxyblockreason" ); @@ -31,9 +31,9 @@ $id = $u->getID(); } - $block = new Block( $wgIP, 0, $id, $reason, wfTimestampNow() ); + $block = new Block( $ip, 0, $id, $reason, wfTimestampNow() ); $block->insert(); $wgOut->addWikiText( $success ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialBooksources.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialBooksources.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialBooksources.php 2005-07-16 18:07:38.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialBooksources.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,107 +1,113 @@ + * @todo Validate ISBNs using the standard check-digit method */ +class SpecialBookSources extends SpecialPage { -/** - * Constructor - */ -function wfSpecialBooksources( $par ) { - global $wgRequest; - - $isbn = $par; - if( empty( $par ) ) { - $isbn = $wgRequest->getVal( 'isbn' ); + /** + * ISBN passed to the page, if any + */ + private $isbn = ''; + + /** + * Constructor + */ + public function __construct() { + parent::__construct( 'Booksources' ); } - $isbn = preg_replace( '/[^0-9X]/', '', $isbn ); - - $bsl = new BookSourceList( $isbn ); - $bsl->show(); -} - -/** - * - * @package MediaWiki - * @subpackage SpecialPage - */ -class BookSourceList { - var $mIsbn; - function BookSourceList( $isbn ) { - $this->mIsbn = $isbn; + /** + * Show the special page + * + * @param $isbn ISBN passed as a subpage parameter + */ + public function execute( $isbn ) { + global $wgOut, $wgRequest; + $this->setHeaders(); + $this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) ); + $wgOut->addWikiText( wfMsgNoTrans( 'booksources-summary' ) ); + $wgOut->addHtml( $this->makeForm() ); + if( strlen( $this->isbn ) > 0 ) + $this->showList(); } - function show() { - global $wgOut; + /** + * Trim ISBN and remove characters which aren't required + * + * @param $isbn Unclean ISBN + * @return string + */ + private function cleanIsbn( $isbn ) { + return trim( preg_replace( '![^0-9X]!', '', $isbn ) ); + } - $wgOut->setPagetitle( wfMsg( "booksources" ) ); - if( $this->mIsbn == '' ) { - $this->askForm(); - } else { - $this->showList(); - } + /** + * Generate a form to allow users to enter an ISBN + * + * @return string + */ + private function makeForm() { + global $wgScript; + $title = self::getTitleFor( 'Booksources' ); + $form = '
            ' . wfMsgHtml( 'booksources-search-legend' ) . ''; + $form .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + $form .= Xml::hidden( 'title', $title->getPrefixedText() ); + $form .= '

            ' . Xml::inputLabel( wfMsg( 'booksources-isbn' ), 'isbn', 'isbn', 20, $this->isbn ); + $form .= ' ' . Xml::submitButton( wfMsg( 'booksources-go' ) ) . '

            '; + $form .= Xml::closeElement( 'form' ); + $form .= '
            '; + return $form; } - - function showList() { - global $wgOut, $wgUser, $wgContLang; - $fname = "BookSourceList::showList()"; - - # First, see if we have a custom list setup in - # [[Wikipedia:Book sources]] or equivalent. - $bstitle = Title::makeTitleSafe( NS_PROJECT, wfMsg( "booksources" ) ); - $bsarticle = new Article( $bstitle ); - if( $bsarticle->exists() ) { - $bstext = $bsarticle->getContent( false ); - if( $bstext ) { - $bstext = str_replace( "MAGICNUMBER", $this->mIsbn, $bstext ); - $wgOut->addWikiText( $bstext ); - return; - } - } - - # Otherwise, use the list of links in the default Language.php file. - $s = wfMsg( "booksourcetext" ) . "
              \n"; - $bs = $wgContLang->getBookstoreList() ; - $bsn = array_keys ( $bs ) ; - foreach ( $bsn as $name ) { - $adr = $bs[$name] ; - if ( ! $this->mIsbn ) { - $adr = explode( ":" , $adr , 2 ); - $adr = explode( "/" , $adr[1] ); - $a = ""; - while ( $a == "" ) { - $a = array_shift( $adr ); - } - $adr = "http://".$a ; - } else { - $adr = str_replace ( "$1" , $this->mIsbn , $adr ) ; - } - $name = htmlspecialchars( $name ); - $adr = htmlspecialchars( $adr ); - $s .= "
            • {$name}
            • \n" ; + + /** + * Determine where to get the list of book sources from, + * format and output them + * + * @return string + */ + private function showList() { + global $wgOut, $wgContLang; + + # Hook to allow extensions to insert additional HTML, + # e.g. for API-interacting plugins and so on + wfRunHooks( 'BookInformation', array( $this->isbn, &$wgOut ) ); + + # Check for a local page such as Project:Book_sources and use that if available + $title = Title::makeTitleSafe( NS_PROJECT, wfMsgForContent( 'booksources' ) ); # Show list in content language + if( is_object( $title ) && $title->exists() ) { + $rev = Revision::newFromTitle( $title ); + $wgOut->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) ); + return true; } - $s .= "
            \n"; - $wgOut->addHTML( $s ); + # Fall back to the defaults given in the language file + $wgOut->addWikiText( wfMsgNoTrans( 'booksources-text' ) ); + $wgOut->addHtml( '
              ' ); + $items = $wgContLang->getBookstoreList(); + foreach( $items as $label => $url ) + $wgOut->addHtml( $this->makeListItem( $label, $url ) ); + $wgOut->addHtml( '
            ' ); + return true; } - function askForm() { - global $wgOut, $wgLang, $wgTitle; - $fname = "BookSourceList::askForm()"; - - $action = $wgTitle->escapeLocalUrl(); - $isbn = htmlspecialchars( wfMsg( "isbn" ) ); - $go = htmlspecialchars( wfMsg( "go" ) ); - $out = "
            - $isbn: - -
            "; - $wgOut->addHTML( $out ); + /** + * Format a book source list item + * + * @param $label Book source label + * @param $url Book source URL + * @return string + */ + private function makeListItem( $label, $url ) { + $url = str_replace( '$1', $this->isbn, $url ); + return '
          1. ' . htmlspecialchars( $label ) . '
          2. '; } + } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialBrokenRedirects.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialBrokenRedirects.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialBrokenRedirects.php 2005-10-26 17:32:11.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialBrokenRedirects.php 2007-08-16 16:41:33.000000000 -0400 @@ -1,19 +1,13 @@ '.wfMsg('brokenredirectstext')."


            \n"; + return wfMsgExt( 'brokenredirectstext', array( 'parse' ) ); } function getSQL() { - $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'pagelinks' ) ); + $dbr = wfGetDB( DB_SLAVE ); + list( $page, $redirect ) = $dbr->tableNamesN( 'page', 'redirect' ); $sql = "SELECT 'BrokenRedirects' AS type, p1.page_namespace AS namespace, p1.page_title AS title, - pl_namespace, - pl_title - FROM ($pagelinks, $page AS p1) - LEFT JOIN $page AS p2 - ON pl_namespace=p2.page_namespace AND pl_title=p2.page_title - WHERE p1.page_is_redirect=1 - AND pl_from=p1.page_id - AND p2.page_namespace IS NULL"; + rd_namespace, + rd_title + FROM $redirect AS rd + JOIN $page p1 ON (rd.rd_from=p1.page_id) + LEFT JOIN $page AS p2 ON (rd_namespace=p2.page_namespace AND rd_title=p2.page_title ) + WHERE rd_namespace >= 0 + AND p2.page_namespace IS NULL"; return $sql; } @@ -52,9 +45,11 @@ } function formatResult( $skin, $result ) { + global $wgUser, $wgContLang; + $fromObj = Title::makeTitle( $result->namespace, $result->title ); - if ( isset( $result->pl_title ) ) { - $toObj = Title::makeTitle( $result->pl_namespace, $result->pl_title ); + if ( isset( $result->rd_title ) ) { + $toObj = Title::makeTitle( $result->rd_namespace, $result->rd_title ); } else { $blinks = $fromObj->getBrokenLinksFrom(); if ( $blinks ) { @@ -70,10 +65,19 @@ } $from = $skin->makeKnownLinkObj( $fromObj ,'', 'redirect=no' ); - $edit = $skin->makeBrokenLinkObj( $fromObj , "(".wfMsg("qbedit").")" , 'redirect=no'); + $edit = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-edit' ), 'action=edit' ); $to = $skin->makeBrokenLinkObj( $toObj ); - - return "$from $edit → $to"; + $arr = $wgContLang->getArrow(); + + $out = "{$from} {$edit}"; + + if( $wgUser->isAllowed( 'delete' ) ) { + $delete = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-delete' ), 'action=delete' ); + $out .= " {$delete}"; + } + + $out .= " {$arr} {$to}"; + return $out; } } @@ -82,10 +86,10 @@ */ function wfSpecialBrokenRedirects() { list( $limit, $offset ) = wfCheckLimits(); - + $sbr = new BrokenRedirectsPage(); - + return $sbr->doQuery( $offset, $limit ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialCategories.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialCategories.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialCategories.php 2004-11-13 15:40:28.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialCategories.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,66 +1,63 @@ addHTML( + wfMsgWikiHtml( 'categoriespagetext' ) . + $cap->getNavigationBar() + . '
              ' . $cap->getBody() . '
            ' . + $cap->getNavigationBar() + ); +} /** - * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage + * @addtogroup Pager */ -class CategoriesPage extends QueryPage { - - function getName() { - return "Categories"; - } - - function isExpensive() { - return false; - } - - function isSyndicated() { return false; } - - function getPageHeader() { - return '

            '.wfMsg('categoriespagetext')."


            \n"; +class CategoryPager extends AlphabeticPager { + function getQueryInfo() { + return array( + 'tables' => array('categorylinks'), + 'fields' => array('cl_to','count(*) AS count'), + 'options' => array('GROUP BY' => 'cl_to') + ); } - function getSQL() { - $NScat = NS_CATEGORY; - $dbr =& wfGetDB( DB_SLAVE ); - $categorylinks = $dbr->tableName( 'categorylinks' ); - return "SELECT DISTINCT 'Categories' as type, - {$NScat} as namespace, - cl_to as title, - 1 as value - FROM $categorylinks"; + + function getIndexField() { + return "cl_to"; } - function sortDescending() { - return false; + /* Override getBody to apply LinksBatch on resultset before actually outputting anything. */ + function getBody() { + if (!$this->mQueryDone) { + $this->doQuery(); + } + $batch = new LinkBatch; + + $this->mResult->rewind(); + + while ( $row = $this->mResult->fetchObject() ) { + $batch->addObj( Title::makeTitleSafe( NS_CATEGORY, $row->cl_to ) ); + } + $batch->execute(); + $this->mResult->rewind(); + return parent::getBody(); } - - function formatResult( $skin, $result ) { + + function formatRow($result) { global $wgLang; - $title = Title::makeTitle( NS_CATEGORY, $result->title ); - return $skin->makeLinkObj( $title, $title->getText() ); + $title = Title::makeTitle( NS_CATEGORY, $result->cl_to ); + $titleText = $this->getSkin()->makeLinkObj( $title, htmlspecialchars( $title->getText() ) ); + $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'), + $wgLang->formatNum( $result->count ) ); + return Xml::tags('li', null, "$titleText ($count)" ) . "\n"; } } -/** - * - */ -function wfSpecialCategories() { - list( $limit, $offset ) = wfCheckLimits(); - - $cap = new CategoriesPage(); - - return $cap->doQuery( $offset, $limit ); -} -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialConfirmemail.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialConfirmemail.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialConfirmemail.php 2005-07-05 17:22:12.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialConfirmemail.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,108 +1,104 @@ */ - -/** @todo document */ -function wfSpecialConfirmemail( $code ) { - $form = new ConfirmationForm(); - $form->show( $code ); -} - -/** @package MediaWiki */ -class ConfirmationForm { - /** */ - function show( $code ) { +class EmailConfirmation extends UnlistedSpecialPage { + + /** + * Constructor + */ + public function __construct() { + parent::__construct( 'Confirmemail' ); + } + + /** + * Main execution point + * + * @param $code Confirmation code passed to the page + */ + function execute( $code ) { + global $wgUser, $wgOut; + $this->setHeaders(); if( empty( $code ) ) { - $this->showEmpty( $this->checkAndSend() ); - } else { - $this->showCode( $code ); - } - } - - /** */ - function showCode( $code ) { - $user = User::newFromConfirmationCode( $code ); - if( is_null( $user ) ) { - $this->showInvalidCode(); - } else { - $this->confirmAndShow( $user ); - } - } - - /** */ - function confirmAndShow( $user ) { - if( $user->confirmEmail() ) { - $this->showSuccess(); + if( $wgUser->isLoggedIn() ) { + if( User::isValidEmailAddr( $wgUser->getEmail() ) ) { + $this->showRequestForm(); + } else { + $wgOut->addWikiText( wfMsg( 'confirmemail_noemail' ) ); + } + } else { + $title = SpecialPage::getTitleFor( 'Userlogin' ); + $self = SpecialPage::getTitleFor( 'Confirmemail' ); + $skin = $wgUser->getSkin(); + $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $self->getPrefixedUrl() ); + $wgOut->addHtml( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) ); + } } else { - $this->showError(); + $this->attemptConfirm( $code ); } } - - /** */ - function checkAndSend() { - global $wgUser, $wgRequest; - if( $wgRequest->wasPosted() && - $wgUser->isLoggedIn() && - $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { - $result = $wgUser->sendConfirmationMail(); - if( WikiError::isError( $result ) ) { - return 'confirmemail_sendfailed'; + + /** + * Show a nice form for the user to request a confirmation mail + */ + function showRequestForm() { + global $wgOut, $wgUser, $wgLang, $wgRequest; + if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getText( 'token' ) ) ) { + $ok = $wgUser->sendConfirmationMail(); + if ( WikiError::isError( $ok ) ) { + $wgOut->addWikiText( wfMsg( 'confirmemail_sendfailed', $ok->toString() ) ); } else { - return 'confirmemail_sent'; + $wgOut->addWikiText( wfMsg( 'confirmemail_sent' ) ); } } else { - # boo - return ''; - } - } - - /** */ - function showEmpty( $err ) { - require_once( 'templates/Confirmemail.php' ); - global $wgOut, $wgUser; - - $tpl = new ConfirmemailTemplate(); - $tpl->set( 'error', $err ); - $tpl->set( 'edittoken', $wgUser->editToken() ); - - $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail' ); - $tpl->set( 'action', $title->getLocalUrl() ); - - - $wgOut->addTemplate( $tpl ); - } - - /** */ - function showInvalidCode() { - global $wgOut; - $wgOut->addWikiText( wfMsg( 'confirmemail_invalid' ) ); - } - - /** */ - function showError() { - global $wgOut; - $wgOut->addWikiText( wfMsg( 'confirmemail_error' ) ); - } - - /** */ - function showSuccess() { - global $wgOut, $wgRequest, $wgUser; - - if( $wgUser->isLoggedIn() ) { - $wgOut->addWikiText( wfMsg( 'confirmemail_loggedin' ) ); + if( $wgUser->isEmailConfirmed() ) { + $time = $wgLang->timeAndDate( $wgUser->mEmailAuthenticated, true ); + $wgOut->addWikiText( wfMsg( 'emailauthenticated', $time ) ); + } + if( $wgUser->isEmailConfirmationPending() ) { + $wgOut->addWikiText( wfMsg( 'confirmemail_pending' ) ); + } + $wgOut->addWikiText( wfMsg( 'confirmemail_text' ) ); + $self = SpecialPage::getTitleFor( 'Confirmemail' ); + $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) ); + $form .= wfHidden( 'token', $wgUser->editToken() ); + $form .= wfSubmitButton( wfMsgHtml( 'confirmemail_send' ) ); + $form .= wfCloseElement( 'form' ); + $wgOut->addHtml( $form ); + } + } + + /** + * Attempt to confirm the user's email address and show success or failure + * as needed; if successful, take the user to log in + * + * @param $code Confirmation code + */ + function attemptConfirm( $code ) { + global $wgUser, $wgOut; + $user = User::newFromConfirmationCode( $code ); + if( is_object( $user ) ) { + if( $user->confirmEmail() ) { + $message = $wgUser->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success'; + $wgOut->addWikiText( wfMsg( $message ) ); + if( !$wgUser->isLoggedIn() ) { + $title = SpecialPage::getTitleFor( 'Userlogin' ); + $wgOut->returnToMain( true, $title->getPrefixedText() ); + } + } else { + $wgOut->addWikiText( wfMsg( 'confirmemail_error' ) ); + } } else { - $wgOut->addWikiText( wfMsg( 'confirmemail_success' ) ); - require_once( 'SpecialUserlogin.php' ); - $form = new LoginForm( $wgRequest ); - $form->execute(); + $wgOut->addWikiText( wfMsg( 'confirmemail_invalid' ) ); } } + } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialContributions.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialContributions.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialContributions.php 2005-11-27 09:57:04.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialContributions.php 2007-08-25 17:25:12.000000000 -0400 @@ -1,391 +1,446 @@ username = $username; - $this->namespace = false; - $this->dbr =& wfGetDB(DB_SLAVE); + function __construct( $target, $namespace = false, $year = false, $month = false ) { + parent::__construct(); + foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist minoreditletter' ) as $msg ) { + $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') ); + } + $this->target = $target; + $this->namespace = $namespace; + + $year = intval($year); + $month = intval($month); + + $this->year = ($year > 0 && $year < 10000) ? $year : false; + $this->month = ($month > 0 && $month < 13) ? $month : false; + $this->getDateCond(); + + $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); + } + + function getDefaultQuery() { + $query = parent::getDefaultQuery(); + $query['target'] = $this->target; + return $query; + } + + function getQueryInfo() { + list( $index, $userCond ) = $this->getUserCond(); + $conds = array_merge( array('page_id=rev_page'), $userCond, $this->getNamespaceCond() ); + + return array( + 'tables' => array( 'page', 'revision' ), + 'fields' => array( + 'page_namespace', 'page_title', 'page_is_new', 'page_latest', 'rev_id', 'rev_page', + 'rev_text_id', 'rev_timestamp', 'rev_comment', 'rev_minor_edit', 'rev_user', + 'rev_user_text', 'rev_deleted' + ), + 'conds' => $conds, + 'options' => array( 'USE INDEX' => $index ) + ); } - function set_namespace($ns) { - $this->namespace = $ns; + function getUserCond() { + $condition = array(); + + if ( $this->target == 'newbies' ) { + $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ ); + $condition[] = 'rev_user >' . (int)($max - $max / 100); + $index = 'user_timestamp'; + } else { + $condition['rev_user_text'] = $this->target; + $index = 'usertext_timestamp'; + } + return array( $index, $condition ); } - function set_limit($limit) { - $this->limit = $limit; + function getNamespaceCond() { + if ( $this->namespace !== '' ) { + return array( 'page_namespace' => (int)$this->namespace ); + } else { + return array(); + } } + + function getDateCond() { + if ( $this->year || $this->month ) { + // Assume this year if only a month is given + if ( $this->year ) { + $year_start = $this->year; + } else { + $year_start = substr( wfTimestampNow(), 0, 4 ); + $thisMonth = gmdate( 'n' ); + if( $this->month > $thisMonth ) { + // Future contributions aren't supposed to happen. :) + $year_start--; + } + } + + if ( $this->month ) { + $month_end = str_pad($this->month + 1, 2, '0', STR_PAD_LEFT); + $year_end = $year_start; + } else { + $month_end = 0; + $year_end = $year_start + 1; + } + $ts_end = str_pad($year_end . $month_end, 14, '0' ); - function set_offset($offset) { - $this->offset = $offset; + $this->mOffset = $ts_end; + } } - function get_edit_limit($dir) { - list($index, $usercond) = $this->get_user_cond(); - $nscond = $this->get_namespace_cond(); - $use_index = $this->dbr->useIndexClause($index); - extract($this->dbr->tableNames('revision', 'page')); - $sql = "SELECT rev_timestamp " . - " FROM $page,$revision $use_index " . - " WHERE rev_page=page_id AND $usercond $nscond" . - " ORDER BY rev_timestamp $dir LIMIT 1"; + function getIndexField() { + return 'rev_timestamp'; + } - $res = $this->dbr->query($sql, "contribs_finder::get_edit_limit"); - while ($o = $this->dbr->fetchObject($res)) - $row = $o; - return $row->rev_timestamp; + function getStartBody() { + return "
              \n"; } - function get_edit_limits() { - return array( - $this->get_edit_limit("ASC"), - $this->get_edit_limit("DESC") - ); + function getEndBody() { + return "
            \n"; } - function get_user_cond() { - $condition = ""; + function getNavigationBar() { + if ( isset( $this->mNavigationBar ) ) { + return $this->mNavigationBar; + } + $linkTexts = array( + 'prev' => wfMsgHtml( "sp-contributions-newer", $this->mLimit ), + 'next' => wfMsgHtml( 'sp-contributions-older', $this->mLimit ), + 'first' => wfMsgHtml('sp-contributions-newest'), + 'last' => wfMsgHtml( 'sp-contributions-oldest' ) + ); - if ($this->username == 'newbies') { - $max = $this->dbr->selectField('user', 'max(user_id)', false, "make_sql"); - $condition = '>' . (int)($max - $max / 100); + $pagingLinks = $this->getPagingLinks( $linkTexts ); + $limitLinks = $this->getLimitLinks(); + $limits = implode( ' | ', $limitLinks ); + + $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . + wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); + return $this->mNavigationBar; + } + + /** + * Generates each row in the contributions list. + * + * Contributions which are marked "top" are currently on top of the history. + * For these contributions, a [rollback] link is shown for users with sysop + * privileges. The rollback link restores the most recent version that was not + * written by the target user. + * + * @todo This would probably look a lot nicer in a table. + */ + function formatRow( $row ) { + wfProfileIn( __METHOD__ ); + + global $wgLang, $wgUser, $wgContLang; + + $sk = $this->getSkin(); + $rev = new Revision( $row ); + + $page = Title::makeTitle( $row->page_namespace, $row->page_title ); + $link = $sk->makeKnownLinkObj( $page ); + $difftext = $topmarktext = ''; + if( $row->rev_id == $row->page_latest ) { + $topmarktext .= '' . $this->messages['uctop'] . ''; + if( !$row->page_is_new ) { + $difftext .= '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'], 'diff=0' ) . ')'; + } else { + $difftext .= $this->messages['newarticle']; + } + + if( $wgUser->isAllowed( 'rollback' ) ) { + $topmarktext .= ' '.$sk->generateRollback( $rev ); + } + + } + if( $rev->userCan( Revision::DELETED_TEXT ) ) { + $difftext = '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'], 'diff=prev&oldid='.$row->rev_id ) . ')'; + } else { + $difftext = '(' . $this->messages['diff'] . ')'; } + $histlink='('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')'; - if ($condition == "") { - $condition = " rev_user_text=" . $this->dbr->addQuotes($this->username); - $index = 'usertext_timestamp'; + $comment = $wgContLang->getDirMark() . $sk->revComment( $rev ); + $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true ); + + if( $this->target == 'newbies' ) { + $userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text ); + $userlink .= ' (' . $sk->userTalkLink( $row->rev_user, $row->rev_user_text ) . ') '; } else { - $condition = " rev_user {$condition}"; - $index = 'user_timestamp'; + $userlink = ''; + } + + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $d = '' . $d . ''; + } + + if( $row->rev_minor_edit ) { + $mflag = '' . $this->messages['minoreditletter'] . ' '; + } else { + $mflag = ''; } - return array($index, $condition); - } - function get_namespace_cond() { - if ($this->namespace !== false) - return " AND page_namespace = " . (int)$this->namespace; - return ""; - } - - function get_previous_offset_for_paging() { - list($index, $usercond) = $this->get_user_cond(); - $nscond = $this->get_namespace_cond(); - - $use_index = $this->dbr->useIndexClause($index); - extract($this->dbr->tableNames('page', 'revision')); - - $sql = "SELECT rev_timestamp FROM $page, $revision $use_index " . - "WHERE page_id = rev_page AND rev_timestamp > '" . $this->offset . "' AND " . - "rev_user_text = " . $this->dbr->addQuotes($this->username) . - $nscond; - $sql .= " ORDER BY rev_timestamp ASC LIMIT " . ($this->limit+1); - $res = $this->dbr->query($sql); - $rows = array(); - while ($obj = $this->dbr->fetchObject($res)) - $rows[] = $obj; - $this->dbr->freeResult($res); - return $rows[count($rows) - 1]->rev_timestamp; - } - - function get_first_offset_for_paging() { - list($index, $usercond) = $this->get_user_cond(); - $use_index = $this->dbr->useIndexClause($index); - extract($this->dbr->tableNames('page', 'revision')); - $nscond = $this->get_namespace_cond(); - $sql = "SELECT rev_timestamp FROM $page, $revision $use_index " . - "WHERE page_id = rev_page AND " . - "rev_user_text = " . $this->dbr->addQuotes($this->username) . - $nscond; - $sql .= " ORDER BY rev_timestamp ASC LIMIT " . $this->limit; - $res = $this->dbr->query($sql); - $rows = array(); - while ($obj = $this->dbr->fetchObject($res)) - $rows[] = $obj; - $this->dbr->freeResult($res); - return $rows[count($rows) - 1]->rev_timestamp; - } - - /* private */ function make_sql() { - $userCond = $condition = $index = $offsetQuery = $limitQuery = ""; - - extract($this->dbr->tableNames('page', 'revision')); - list($index, $userCond) = $this->get_user_cond(); - - $limitQuery = "LIMIT {$this->limit}"; - if ($this->offset) - $offsetQuery = "AND rev_timestamp <= '{$this->offset}'"; - - $nscond = $this->get_namespace_cond(); - $use_index = $this->dbr->useIndexClause($index); - $sql = "SELECT - page_namespace,page_title,page_is_new,page_latest, - rev_id,rev_timestamp,rev_comment,rev_minor_edit,rev_user_text, - rev_deleted - FROM $page,$revision $use_index - WHERE page_id=rev_page AND $userCond $nscond $offsetQuery - ORDER BY rev_timestamp DESC $limitQuery"; - return $sql; - } - - function find() { - $contribs = array(); - $res = $this->dbr->query($this->make_sql(), "contribs_finder::find"); - while ($c = $this->dbr->fetchObject($res)) - $contribs[] = $c; - $this->dbr->freeResult($res); - return $contribs; + $ret = "{$d} {$histlink} {$difftext} {$mflag} {$link}{$userlink}{$comment} {$topmarktext}"; + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $ret .= ' ' . wfMsgHtml( 'deletedrev' ); + } + $ret = "
          3. $ret
          4. \n"; + wfProfileOut( __METHOD__ ); + return $ret; + } + + /** + * Get the Database object in use + * + * @return Database + */ + public function getDatabase() { + return $this->mDb; } -}; + +} /** * Special page "user contributions". * Shows a list of the contributions of a user. * * @return none - * @param string $par (optional) user name of the user for which to show the contributions + * @param $par String: (optional) user name of the user for which to show the contributions */ function wfSpecialContributions( $par = null ) { - global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest, $wgTitle, - $wgScript; - $fname = 'wfSpecialContributions'; - - $target = isset($par) ? $par : $wgRequest->getVal( 'target' ); - if (!strlen($target)) { - $wgOut->errorpage('notargettitle', 'notargettext'); - return; - } + global $wgUser, $wgOut, $wgLang, $wgRequest; - $nt = Title::newFromURL( $target ); - if (!$nt) { - $wgOut->errorpage( 'notargettitle', 'notargettext' ); - return; + $options = array(); + + if ( isset( $par ) && $par == 'newbies' ) { + $target = 'newbies'; + $options['contribs'] = 'newbie'; + } elseif ( isset( $par ) ) { + $target = $par; + } else { + $target = $wgRequest->getVal( 'target' ); } - $nt =& Title::makeTitle(NS_USER, $nt->getDBkey()); - list( $limit, $offset) = wfCheckLimits(); - $offset = $wgRequest->getVal('offset'); - /* Offset must be an integral. */ - if (!strlen($offset) || !preg_match("/^[0-9]+$/", $offset)) - $offset = 0; - - $title = Title::makeTitle(NS_SPECIAL, "Contributions"); - $urlbits = "target=" . wfUrlEncode($target); - $myurl = $title->escapeLocalURL($urlbits); - - $finder = new contribs_finder(($target == 'newbies') ? 'newbies' : $nt->getText()); - - $finder->set_limit($limit); - $finder->set_offset($offset); - - $nsurl = $xnsurl = ""; - if (($ns = $wgRequest->getVal('namespace', null)) !== null) { - $nsurl = "&namespace=$ns"; - $xnsurl = htmlspecialchars($nsurl); - $finder->set_namespace($ns); - } - - if ($wgRequest->getText('go') == "prev") { - $prevts = $finder->get_previous_offset_for_paging(); - $prevurl = $title->getLocalURL($urlbits . "&offset=$prevts&limit=$limit$nsurl"); - $wgOut->redirect($prevurl); - return; + // check for radiobox + if ( $wgRequest->getVal( 'contribs' ) == 'newbie' ) { + $target = 'newbies'; + $options['contribs'] = 'newbie'; } - if ($wgRequest->getText('go') == "first") { - $prevts = $finder->get_first_offset_for_paging(); - $prevurl = $title->getLocalURL($urlbits . "&offset=$prevts&limit=$limit$nsurl"); - $wgOut->redirect($prevurl); + if ( !strlen( $target ) ) { + $wgOut->addHTML( contributionsForm( '' ) ); return; } - $sk = $wgUser->getSkin(); + $options['limit'] = $wgRequest->getInt( 'limit', 50 ); + $options['target'] = $target; - $id = User::idFromName($nt->getText()); + $nt = Title::makeTitleSafe( NS_USER, $target ); + if ( !$nt ) { + $wgOut->addHTML( contributionsForm( '' ) ); + return; + } + $id = User::idFromName( $nt->getText() ); - if ( 0 == $id ) { - $ul = $nt->getText(); + if ( $target != 'newbies' ) { + $target = $nt->getText(); + $wgOut->setSubtitle( contributionsSub( $nt, $id ) ); } else { - $ul = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) ); - $userCond = '=' . $id; + $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') ); } - $talk = $nt->getTalkPage(); - if( $talk ) { - $ul .= ' (' . $sk->makeLinkObj( $talk, $wgLang->getNsText( NS_TALK ) ) . ')'; + + if ( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) { + $options['namespace'] = intval( $ns ); + } else { + $options['namespace'] = ''; } - - if ($target == 'newbies') { - $ul = wfMsg ('newbies'); + if ( $wgUser->isAllowed( 'rollback' ) && $wgRequest->getBool( 'bot' ) ) { + $options['bot'] = '1'; + } + + $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev'; + # Offset overrides year/month selection + if ( ( $month = $wgRequest->getIntOrNull( 'month' ) ) !== null && $month !== -1 ) { + $options['month'] = intval( $month ); + } else { + $options['month'] = ''; + } + if ( ( $year = $wgRequest->getIntOrNull( 'year' ) ) !== null ) { + $options['year'] = intval( $year ); + } else if( $options['month'] ) { + $thisMonth = intval( gmdate( 'n' ) ); + $thisYear = intval( gmdate( 'Y' ) ); + if( intval( $options['month'] ) > $thisMonth ) { + $thisYear--; + } + $options['year'] = $thisYear; + } else { + $options['year'] = ''; } - $wgOut->setSubtitle( wfMsgHtml( 'contribsub', $ul ) ); - - wfRunHooks('SpecialContributionsBeforeMainOutput', $id ); - - $arr = $wgContLang->getFormattedNamespaces(); - $nsform = "
            \n"; - $nsform .= wfElement("input", array( - "name" => "title", - "type" => "hidden", - "value" => $wgTitle->getPrefixedText())); - $nsform .= wfElement("input", array( - "name" => "offset", - "type" => "hidden", - "value" => $offset)); - $nsform .= wfElement("input", array( - "name" => "limit", - "type" => "hidden", - "value" => $limit)); - $nsform .= wfElement("input", array( - "name" => "target", - "type" => "hidden", - "value" => $target)); - $nsform .= "

            "; - $nsform .= wfMsgHtml('namespace'); - - $nsform .= HTMLnamespaceselector($ns, ''); - - $nsform .= wfElement("input", array( - "type" => "submit", - "value" => wfMsg('allpagessubmit'))); - $nsform .= "

            \n"; - - $wgOut->addHTML($nsform); - - $contribsPage = Title::makeTitle( NS_SPECIAL, 'Contributions' ); - $contribs = $finder->find(); + wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id ); - if (count($contribs) == 0) { - $wgOut->addWikiText( wfMsg( "nocontribs" ) ); - return; + $wgOut->addHTML( contributionsForm( $options ) ); + # Show original selected options, don't apply them so as to allow paging + $_GET['year'] = ''; // hack for Pager + $_GET['month'] = ''; // hack for Pager + if( $skip ) { + $options['year'] = ''; + $options['month'] = ''; } - list($early, $late) = $finder->get_edit_limits(); - $lastts = count($contribs) ? $contribs[count($contribs) - 1]->rev_timestamp : 0; - $atstart = (!count($contribs) || $late == $contribs[0]->rev_timestamp); - $atend = (!count($contribs) || $early == $lastts); - - $lasturl = $wgTitle->escapeLocalURL("action=history&limit={$limit}"); - - $firsttext = wfMsgHtml("histfirst"); - $lasttext = wfMsgHtml("histlast"); - - $prevtext = wfMsg("prevn", $limit); - if ($atstart) { - $lastlink = $lasttext; - $prevlink = $prevtext; - } else { - $lastlink = "$lasttext"; - $prevlink = "$prevtext"; + $pager = new ContribsPager( $target, $options['namespace'], $options['year'], $options['month'] ); + if ( !$pager->getNumRows() ) { + $wgOut->addWikiText( wfMsg( 'nocontribs' ) ); + return; } - $nexttext = wfMsg("nextn", $limit); - if ($atend) { - $firstlink = $firsttext; - $nextlink = $nexttext; - } else { - $firstlink = "$firsttext"; - $nextlink = "$nexttext"; + # Show a message about slave lag, if applicable + if( ( $lag = $pager->getDatabase()->getLag() ) > 0 ) + $wgOut->showLagWarning( $lag ); + + $wgOut->addHTML( + '

            ' . $pager->getNavigationBar() . '

            ' . + $pager->getBody() . + '

            ' . $pager->getNavigationBar() . '

            ' ); + + # If there were contributions, and it was a valid user or IP, show + # the appropriate "footer" message - WHOIS tools, etc. + if( $target != 'newbies' ) { + $message = IP::isIPAddress( $target ) + ? 'sp-contributions-footer-anon' + : 'sp-contributions-footer'; + + + $text = wfMsg( $message, $target ); + if( !wfEmptyMsg( $message, $text ) && $text != '-' ) { + $wgOut->addHtml( '' ); + } } - $firstlast = "($lastlink | $firstlink)"; +} - $urls = array(); - foreach (array(20, 50, 100, 250, 500) as $num) - $urls[] = "".$wgLang->formatNum($num).""; - $bits = implode($urls, ' | '); +/** + * Generates the subheading with links + * @param Title $nt Title object for the target + * @param integer $id User ID for the target + * @return String: appropriately-escaped HTML to be output literally + */ +function contributionsSub( $nt, $id ) { + global $wgSysopUserBans, $wgLang, $wgUser; - $prevnextbits = "$firstlast " . wfMsgHtml("viewprevnext", $prevlink, $nextlink, $bits); + $sk = $wgUser->getSkin(); - $wgOut->addHTML( "

            {$prevnextbits}

            \n"); + if ( 0 == $id ) { + $user = $nt->getText(); + } else { + $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) ); + } + $talk = $nt->getTalkPage(); + if( $talk ) { + # Talk page link + $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) ); + if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && User::isIP( $nt->getText() ) ) ) { + # Block link + if( $wgUser->isAllowed( 'block' ) ) + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) ); + # Block log link + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() ); + } + # Other logs link + $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() ); - $wgOut->addHTML( "
              \n" ); + wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) ); - foreach ($contribs as $contrib) - $wgOut->addHTML(ucListEdit($sk, $contrib)); + $links = implode( ' | ', $tools ); + } - $wgOut->addHTML( "
            \n" ); - $wgOut->addHTML( "

            {$prevnextbits}

            \n"); + // Old message 'contribsub' had one parameter, but that doesn't work for + // languages that want to put the "for" bit right after $user but before + // $links. If 'contribsub' is around, use it for reverse compatibility, + // otherwise use 'contribsub2'. + if( wfEmptyMsg( 'contribsub', wfMsg( 'contribsub' ) ) ) { + return wfMsgHtml( 'contribsub2', $user, $links ); + } else { + return wfMsgHtml( 'contribsub', "$user ($links)" ); + } } - /** - * Generates each row in the contributions list. - * - * Contributions which are marked "top" are currently on top of the history. - * For these contributions, a [rollback] link is shown for users with sysop - * privileges. The rollback link restores the most recent version that was not - * written by the target user. - * - * If the contributions page is called with the parameter &bot=1, all rollback - * links also get that parameter. It causes the edit itself and the rollback - * to be marked as "bot" edits. Bot edits are hidden by default from recent - * changes, so this allows sysops to combat a busy vandal without bothering - * other users. - * - * @todo This would probably look a lot nicer in a table. + * Generates the namespace selector form with hidden attributes. + * @param $options Array: the options to be included. */ -function ucListEdit( $sk, $row ) { - $fname = 'ucListEdit'; - wfProfileIn( $fname ); - - global $wgLang, $wgOut, $wgUser, $wgRequest; - static $messages; - if( !isset( $messages ) ) { - foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist minoreditletter' ) as $msg ) { - $messages[$msg] = wfMsg( $msg ); - } - } +function contributionsForm( $options ) { + global $wgScript, $wgTitle, $wgRequest; - $page =& Title::makeTitle( $row->page_namespace, $row->page_title ); - $link = $sk->makeKnownLinkObj( $page, '' ); - $difftext = $topmarktext = ''; - if( $row->rev_id == $row->page_latest ) { - $topmarktext .= '' . $messages['uctop'] . ''; - if( !$row->page_is_new ) { - $difftext .= '(' . $sk->makeKnownLinkObj( $page, $messages['diff'], 'diff=0' ) . ')'; - } else { - $difftext .= $messages['newarticle']; - } + $options['title'] = $wgTitle->getPrefixedText(); + if ( !isset( $options['target'] ) ) { + $options['target'] = ''; + } else { + $options['target'] = str_replace( '_' , ' ' , $options['target'] ); + } - if( $wgUser->isAllowed('rollback') ) { - $extraRollback = $wgRequest->getBool( 'bot' ) ? '&bot=1' : ''; - $extraRollback .= '&token=' . urlencode( - $wgUser->editToken( array( $page->getPrefixedText(), $row->rev_user_text ) ) ); - $topmarktext .= ' ['. $sk->makeKnownLinkObj( $page, - $messages['rollbacklink'], - 'action=rollback&from=' . urlencode( $row->rev_user_text ) . $extraRollback ) .']'; - } + if ( !isset( $options['namespace'] ) ) { + $options['namespace'] = ''; + } + if ( !isset( $options['contribs'] ) ) { + $options['contribs'] = 'user'; } - if( $row->rev_deleted && !$wgUser->isAllowed( 'undelete' ) ) { - $difftext = '(' . $messages['diff'] . ')'; - } else { - $difftext = '(' . $sk->makeKnownLinkObj( $page, $messages['diff'], 'diff=prev&oldid='.$row->rev_id ) . ')'; + + if ( !isset( $options['year'] ) ) { + $options['year'] = ''; } - $histlink='('.$sk->makeKnownLinkObj( $page, $messages['hist'], 'action=history' ) . ')'; - $comment = $sk->commentBlock( $row->rev_comment, $page ); - $d = $wgLang->timeanddate( $row->rev_timestamp, true ); + if ( !isset( $options['month'] ) ) { + $options['month'] = ''; + } - if( $row->rev_minor_edit ) { - $mflag = '' . $messages['minoreditletter'] . ' '; - } else { - $mflag = ''; + if ( $options['contribs'] == 'newbie' ) { + $options['target'] = ''; } - $ret = "{$d} {$histlink} {$difftext} {$mflag} {$link} {$comment} {$topmarktext}"; - if( $row->rev_deleted ) { - $ret = '' . $ret . ' ' . htmlspecialchars( wfMsg( 'deletedrev' ) ); + $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + + foreach ( $options as $name => $value ) { + if ( in_array( $name, array( 'namespace', 'target', 'contribs', 'year', 'month' ) ) ) { + continue; + } + $f .= "\t" . Xml::hidden( $name, $value ) . "\n"; } - $ret = "
          5. $ret
          6. \n"; - wfProfileOut( $fname ); - return $ret; -} -?> + $f .= '
            ' . + Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) . + Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ), 'contribs' , 'newbie' , 'newbie', $options['contribs'] == 'newbie' ? true : false ) . '
            ' . + Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ), 'contribs' , 'user', 'user', $options['contribs'] == 'user' ? true : false ) . ' ' . + Xml::input( 'target', 20, $options['target']) . ' '. + Xml::label( wfMsg( 'namespace' ), 'namespace' ) . + Xml::namespaceSelector( $options['namespace'], '' ) . + Xml::openElement( 'p' ) . + Xml::label( wfMsg( 'year' ), 'year' ) . ' '. + Xml::input( 'year', 4, $options['year'], array('id' => 'year', 'maxlength' => 4) ) . ' '. + Xml::label( wfMsg( 'month' ), 'month' ) . ' '. + Xml::monthSelector( $options['month'], -1 ) . ' '. + Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) . + Xml::closeElement( 'p' ); + + $explain = wfMsgExt( 'sp-contributions-explain', 'parseinline' ); + if( !wfEmptyMsg( 'sp-contributions-explain', $explain ) ) + $f .= "

            {$explain}

            "; + + $f .= '
            ' . + Xml::closeElement( 'form' ); + return $f; +} diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialDeadendpages.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialDeadendpages.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialDeadendpages.php 2005-05-26 06:23:35.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialDeadendpages.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,19 +1,12 @@ tableNames( 'page', 'pagelinks' ) ); - return "SELECT 'Deadendpages' as type, page_namespace AS namespace, page_title as title, page_title AS value " . + $dbr = wfGetDB( DB_SLAVE ); + list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' ); + return "SELECT 'Deadendpages' as type, page_namespace AS namespace, page_title as title, page_title AS value " . "FROM $page LEFT JOIN $pagelinks ON page_id = pl_from " . "WHERE pl_from IS NULL " . "AND page_namespace = 0 " . "AND page_is_redirect = 0"; - } + } } /** * Constructor */ function wfSpecialDeadendpages() { - - list( $limit, $offset ) = wfCheckLimits(); - $depp = new DeadendPagesPage(); - - return $depp->doQuery( $offset, $limit ); + list( $limit, $offset ) = wfCheckLimits(); + + $depp = new DeadendPagesPage(); + + return $depp->doQuery( $offset, $limit ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialDisambiguations.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialDisambiguations.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialDisambiguations.php 2005-07-11 16:47:34.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialDisambiguations.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,70 +1,95 @@ getSkin(); - - #FIXME : probably need to add a backlink to the maintenance page. - return '

            '.wfMsg("disambiguationstext", $sk->makeKnownLink(wfMsgForContent('disambiguationspage')) )."


            \n"; + return wfMsgExt( 'disambiguations-text', array( 'parse' ) ); } function getSQL() { - $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'pagelinks' ) ); - - $dp = Title::newFromText(wfMsgForContent("disambiguationspage")); - $id = $dp->getArticleId(); - $dns = $dp->getNamespace(); - $dtitle = $dbr->addQuotes( $dp->getDBkey() ); - - $sql = "SELECT 'Disambiguations' AS type, pa.page_namespace AS namespace," - ." pa.page_title AS title, la.pl_from AS value" - ." FROM {$pagelinks} AS lb, {$page} AS pa, {$pagelinks} AS la" - ." WHERE lb.pl_namespace = $dns AND lb.pl_title = $dtitle" # disambiguation template - ." AND pa.page_id = lb.pl_from" - ." AND pa.page_namespace = la.pl_namespace" - ." AND pa.page_title = la.pl_title"; + $dbr = wfGetDB( DB_SLAVE ); + + $dMsgText = wfMsgForContent('disambiguationspage'); + + $linkBatch = new LinkBatch; + + # If the text can be treated as a title, use it verbatim. + # Otherwise, pull the titles from the links table + $dp = Title::newFromText($dMsgText); + if( $dp ) { + if($dp->getNamespace() != NS_TEMPLATE) { + # FIXME we assume the disambiguation message is a template but + # the page can potentially be from another namespace :/ + wfDebug("Mediawiki:disambiguationspage message does not refer to a template!\n"); + } + $linkBatch->addObj( $dp ); + } else { + # Get all the templates linked from the Mediawiki:Disambiguationspage + $disPageObj = Title::makeTitleSafe( NS_MEDIAWIKI, 'disambiguationspage' ); + $res = $dbr->select( + array('pagelinks', 'page'), + 'pl_title', + array('page_id = pl_from', 'pl_namespace' => NS_TEMPLATE, + 'page_namespace' => $disPageObj->getNamespace(), 'page_title' => $disPageObj->getDBkey()), + __METHOD__ ); + + while ( $row = $dbr->fetchObject( $res ) ) { + $linkBatch->addObj( Title::makeTitle( NS_TEMPLATE, $row->pl_title )); + } + + $dbr->freeResult( $res ); + } + + $set = $linkBatch->constructSet( 'lb.tl', $dbr ); + if( $set === false ) { + # We must always return a valid sql query, but this way DB will always quicly return an empty result + $set = 'FALSE'; + wfDebug("Mediawiki:disambiguationspage message does not link to any templates!\n"); + } + + list( $page, $pagelinks, $templatelinks) = $dbr->tableNamesN( 'page', 'pagelinks', 'templatelinks' ); + + $sql = "SELECT 'Disambiguations' AS \"type\", pb.page_namespace AS namespace," + ." pb.page_title AS title, la.pl_from AS value" + ." FROM {$templatelinks} AS lb, {$page} AS pb, {$pagelinks} AS la, {$page} AS pa" + ." WHERE $set" # disambiguation template(s) + .' AND pa.page_id = la.pl_from' + .' AND pa.page_namespace = ' . NS_MAIN # Limit to just articles in the main namespace + .' AND pb.page_id = lb.tl_from' + .' AND pb.page_namespace = la.pl_namespace' + .' AND pb.page_title = la.pl_title' + .' ORDER BY lb.tl_namespace, lb.tl_title'; + return $sql; } function getOrder() { return ''; } - + function formatResult( $skin, $result ) { - global $wgContLang ; + global $wgContLang; $title = Title::newFromId( $result->value ); - $dp = Title::makeTitle( $result->namespace, $result->title ); + $dp = Title::makeTitle( $result->namespace, $result->title ); - $from = $skin->makeKnownLinkObj( $title,''); - $edit = $skin->makeBrokenLinkObj( $title, "(".wfMsg("qbedit").")" , 'redirect=no'); - $to = $skin->makeKnownLinkObj( $dp,''); - - return "$from $edit => $to"; + $from = $skin->makeKnownLinkObj( $title, '' ); + $edit = $skin->makeKnownLinkObj( $title, "(".wfMsgHtml("qbedit").")" , 'redirect=no&action=edit' ); + $arr = $wgContLang->getArrow(); + $to = $skin->makeKnownLinkObj( $dp, '' ); + + return "$from $edit $arr $to"; } } @@ -73,9 +98,9 @@ */ function wfSpecialDisambiguations() { list( $limit, $offset ) = wfCheckLimits(); - + $sd = new DisambiguationsPage(); - + return $sd->doQuery( $offset, $limit ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialDoubleRedirects.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialDoubleRedirects.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialDoubleRedirects.php 2005-05-26 06:23:35.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialDoubleRedirects.php 2007-08-28 12:59:24.000000000 -0400 @@ -1,97 +1,92 @@ '.wfMsg("doubleredirectstext")."


            \n"; + return wfMsgExt( 'doubleredirectstext', array( 'parse' ) ); } - function getSQL() { - $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'pagelinks' ) ); + function getSQLText( &$dbr, $namespace = null, $title = null ) { + + list( $page, $redirect ) = $dbr->tableNamesN( 'page', 'redirect' ); + + $limitToTitle = !( $namespace === null && $title === null ); + $sql = $limitToTitle ? "SELECT" : "SELECT 'DoubleRedirects' as type," ; + $sql .= + " pa.page_namespace as namespace, pa.page_title as title," . + " pb.page_namespace as nsb, pb.page_title as tb," . + " pc.page_namespace as nsc, pc.page_title as tc" . + " FROM $redirect AS ra, $redirect AS rb, $page AS pa, $page AS pb, $page AS pc" . + " WHERE ra.rd_from=pa.page_id" . + " AND ra.rd_namespace=pb.page_namespace" . + " AND ra.rd_title=pb.page_title" . + " AND rb.rd_from=pb.page_id" . + " AND rb.rd_namespace=pc.page_namespace" . + " AND rb.rd_title=pc.page_title"; + + if( $limitToTitle ) { + $encTitle = $dbr->addQuotes( $title ); + $sql .= " AND pa.page_namespace=$namespace" . + " AND pa.page_title=$encTitle"; + } - $sql = "SELECT 'DoubleRedirects' as type," . - " pa.page_namespace as namespace, pa.page_title as title," . - " pb.page_namespace as nsb, pb.page_title as tb," . - " pc.page_namespace as nsc, pc.page_title as tc" . - " FROM $pagelinks AS la, $pagelinks AS lb, $page AS pa, $page AS pb, $page AS pc" . - " WHERE pa.page_is_redirect=1 AND pb.page_is_redirect=1" . - " AND la.pl_from=pa.page_id" . - " AND la.pl_namespace=pb.page_namespace" . - " AND la.pl_title=pb.page_title" . - " AND lb.pl_from=pb.page_id" . - " AND lb.pl_namespace=pc.page_namespace" . - " AND lb.pl_title=pc.page_title"; return $sql; } + + function getSQL() { + $dbr = wfGetDB( DB_SLAVE ); + return $this->getSQLText( $dbr ); + } function getOrder() { return ''; } - + function formatResult( $skin, $result ) { + global $wgContLang; + $fname = 'DoubleRedirectsPage::formatResult'; $titleA = Title::makeTitle( $result->namespace, $result->title ); + $linkA = $skin->makeKnownLinkObj( $titleA,'', 'redirect=no' ); if ( $result && !isset( $result->nsb ) ) { - $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'page', 'pagelinks' ) ); - $encTitle = $dbr->addQuotes( $result->title ); - - $sql = "SELECT pa.page_namespace as namespace, pa.page_title as title," . - " pb.page_namespace as nsb, pb.page_title as tb," . - " pc.page_namespace as nsc, pc.page_title as tc" . - " FROM $pagelinks AS la, $pagelinks AS lb, $page AS pa, $page AS pb, $page AS pc" . - " WHERE pa.page_is_redirect=1 AND pb.page_is_redirect=1" . - " AND la.pl_from=pa.page_id" . - " AND la.pl_namespace=pb.page_namespace" . - " AND la.pl_title=pb.page_title" . - " AND lb.pl_from=pb.page_id" . - " AND lb.pl_namespace=pc.page_namespace" . - " AND lb.pl_title=pc.page_title" . - " AND pa.page_namespace={$result->namespace}" . - " AND pa.page_title=$encTitle"; + $dbr = wfGetDB( DB_SLAVE ); + $sql = $this->getSQLText( $dbr, $result->namespace, $result->title ); $res = $dbr->query( $sql, $fname ); if ( $res ) { $result = $dbr->fetchObject( $res ); + $dbr->freeResult( $res ); } } if ( !$result ) { - return ''; + return "{$linkA}\n"; } - + $titleB = Title::makeTitle( $result->nsb, $result->tb ); $titleC = Title::makeTitle( $result->nsc, $result->tc ); - $linkA = $skin->makeKnownLinkObj( $titleA,'', 'redirect=no' ); $edit = $skin->makeBrokenLinkObj( $titleA, "(".wfMsg("qbedit").")" , 'redirect=no'); $linkB = $skin->makeKnownLinkObj( $titleB, '', 'redirect=no' ); $linkC = $skin->makeKnownLinkObj( $titleC ); - - return "$linkA $edit → $linkB → $linkC"; + $arr = $wgContLang->getArrow() . $wgContLang->getDirMark(); + + return( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" ); } } @@ -100,10 +95,10 @@ */ function wfSpecialDoubleRedirects() { list( $limit, $offset ) = wfCheckLimits(); - + $sdr = new DoubleRedirectsPage(); - + return $sdr->doQuery( $offset, $limit ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialEmailuser.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialEmailuser.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialEmailuser.php 2005-05-06 07:20:37.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialEmailuser.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,58 +1,70 @@ errorpage( "nosuchspecialpage", "nospecialpagetext" ); + $wgOut->showErrorPage( "nosuchspecialpage", "nospecialpagetext" ); return; } - + if( !$wgUser->canSendEmail() ) { wfDebug( "User can't send.\n" ); - $wgOut->errorpage( "mailnologin", "mailnologintext" ); + $wgOut->showErrorPage( "mailnologin", "mailnologintext" ); return; } - + $action = $wgRequest->getVal( 'action' ); $target = isset($par) ? $par : $wgRequest->getVal( 'target' ); if ( "" == $target ) { wfDebug( "Target is empty.\n" ); - $wgOut->errorpage( "notargettitle", "notargettext" ); + $wgOut->showErrorPage( "notargettitle", "notargettext" ); return; } - + $nt = Title::newFromURL( $target ); if ( is_null( $nt ) ) { wfDebug( "Target is invalid title.\n" ); - $wgOut->errorpage( "notargettitle", "notargettext" ); + $wgOut->showErrorPage( "notargettitle", "notargettext" ); return; } - + $nu = User::newFromName( $nt->getText() ); if( is_null( $nu ) || !$nu->canReceiveEmail() ) { wfDebug( "Target is invalid user or can't receive.\n" ); - $wgOut->errorpage( "noemailtitle", "noemailtext" ); + $wgOut->showErrorPage( "noemailtitle", "noemailtext" ); return; } - $address = $nu->getEmail(); - $f = new EmailUserForm( $nu->getName() . " <{$address}>", $target ); + if ( $wgUser->isBlockedFromEmailUser() ) { + // User has been blocked from sending e-mail. Show the std blocked form. + wfDebug( "User is blocked from sending e-mail.\n" ); + $wgOut->blockedPage(); + return; + } + + $f = new EmailUserForm( $nu ); if ( "success" == $action ) { - $f->showSuccess(); + $f->showSuccess( $nu ); } else if ( "submit" == $action && $wgRequest->wasPosted() && - $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { + $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) + { + # Check against the rate limiter + if( $wgUser->pingLimiter( 'emailuser' ) ) { + $wgOut->rateLimited(); + return; + } + $f->doSubmit(); } else { $f->showForm(); @@ -60,51 +72,54 @@ } /** - * @todo document - * @package MediaWiki - * @subpackage SpecialPage + * Implements the Special:Emailuser web interface, and invokes userMailer for sending the email message. + * @addtogroup SpecialPage */ class EmailUserForm { - var $mAddress; var $target; var $text, $subject; + var $cc_me; // Whether user requested to be sent a separate copy of their email. - function EmailUserForm( $addr, $target ) { + /** + * @param User $target + */ + function EmailUserForm( $target ) { global $wgRequest; - $this->mAddress = $addr; $this->target = $target; $this->text = $wgRequest->getText( 'wpText' ); $this->subject = $wgRequest->getText( 'wpSubject' ); + $this->cc_me = $wgRequest->getBool( 'wpCCMe' ); } function showForm() { - global $wgOut, $wgUser, $wgLang; + global $wgOut, $wgUser; $wgOut->setPagetitle( wfMsg( "emailpage" ) ); $wgOut->addWikiText( wfMsg( "emailpagetext" ) ); - if ( $this->subject === "" ) { - $this->subject = wfMsg( "defemailsubject" ); + if ( $this->subject === "" ) { + $this->subject = wfMsg( "defemailsubject" ); } $emf = wfMsg( "emailfrom" ); $sender = $wgUser->getName(); $emt = wfMsg( "emailto" ); - $rcpt = str_replace( "_", " ", $this->target ); + $rcpt = $this->target->getName(); $emr = wfMsg( "emailsubject" ); $emm = wfMsg( "emailmessage" ); $ems = wfMsg( "emailsend" ); + $emc = wfMsg( "emailccme" ); $encSubject = htmlspecialchars( $this->subject ); - - $titleObj = Title::makeTitle( NS_SPECIAL, "Emailuser" ); + + $titleObj = SpecialPage::getTitleFor( "Emailuser" ); $action = $titleObj->escapeLocalURL( "target=" . - urlencode( $this->target ) . "&action=submit" ); - $token = $wgUser->editToken(); + urlencode( $this->target->getName() ) . "&action=submit" ); + $token = htmlspecialchars( $wgUser->editToken() ); $wgOut->addHTML( "
            - +
            @@ -113,50 +128,68 @@ - - - -
            {$emf}: " . htmlspecialchars( $sender ) . "
            {$emr}: - +
            {$emm}: - -
              +" . wfCheckLabel( $emc, 'wpCCMe', 'wpCCMe', $wgUser->getBoolOption( 'ccmeonemails' ) ) . "
            -
            \n" ); } function doSubmit() { - global $wgOut, $wgUser, $wgLang, $wgOutputEncoding; - - $from = wfQuotedPrintable( $wgUser->getName() ) . " <" . $wgUser->getEmail() . ">"; - $subject = wfQuotedPrintable( $this->subject ); - - if (wfRunHooks('EmailUser', array(&$this->mAddress, &$from, &$subject, &$this->text))) { - - $mailResult = userMailer( $this->mAddress, $from, $subject, $this->text ); - + global $wgOut, $wgUser; + + $to = new MailAddress( $this->target ); + $from = new MailAddress( $wgUser ); + $subject = $this->subject; + + if( wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$this->text ) ) ) { + + $mailResult = userMailer( $to, $from, $subject, $this->text ); + if( WikiError::isError( $mailResult ) ) { $wgOut->addHTML( wfMsg( "usermailererror" ) . $mailResult); } else { - $titleObj = Title::makeTitle( NS_SPECIAL, "Emailuser" ); - $encTarget = wfUrlencode( $this->target ); + + // if the user requested a copy of this mail, do this now, + // unless they are emailing themselves, in which case one copy of the message is sufficient. + if ($this->cc_me && $to != $from) { + $cc_subject = wfMsg('emailccsubject', $this->target->getName(), $subject); + if( wfRunHooks( 'EmailUser', array( &$from, &$from, &$cc_subject, &$this->text ) ) ) { + $ccResult = userMailer( $from, $from, $cc_subject, $this->text ); + if( WikiError::isError( $ccResult ) ) { + // At this stage, the user's CC mail has failed, but their + // original mail has succeeded. It's unlikely, but still, what to do? + // We can either show them an error, or we can say everything was fine, + // or we can say we sort of failed AND sort of succeeded. Of these options, + // simply saying there was an error is probably best. + $wgOut->addHTML( wfMsg( "usermailererror" ) . $ccResult); + return; + } + } + } + + $titleObj = SpecialPage::getTitleFor( "Emailuser" ); + $encTarget = wfUrlencode( $this->target->getName() ); $wgOut->redirect( $titleObj->getFullURL( "target={$encTarget}&action=success" ) ); - wfRunHooks('EmailUserComplete', array($this->mAddress, $from, $subject, $this->text)); + wfRunHooks( 'EmailUserComplete', array( $to, $from, $subject, $this->text ) ); } } } - function showSuccess() { - global $wgOut, $wgUser; + function showSuccess( &$user ) { + global $wgOut; $wgOut->setPagetitle( wfMsg( "emailsent" ) ); $wgOut->addHTML( wfMsg( "emailsenttext" ) ); - $wgOut->returnToMain( false ); + $wgOut->returnToMain( false, $user->getUserPage() ); } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialExport.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialExport.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialExport.php 2006-01-27 17:11:45.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialExport.php 2007-07-17 14:14:36.000000000 -0400 @@ -1,435 +1,196 @@ # http://www.mediawiki.org/ -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ -/** */ -require_once( 'Revision.php' ); +function wfExportGetPagesFromCategory( $title ) { + global $wgContLang; + + $name = $title->getDBKey(); + + $dbr = wfGetDB( DB_SLAVE ); + + list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' ); + $sql = "SELECT page_namespace, page_title FROM $page " . + "JOIN $categorylinks ON cl_from = page_id " . + "WHERE cl_to = " . $dbr->addQuotes( $name ); + + $pages = array(); + $res = $dbr->query( $sql, 'wfExportGetPagesFromCategory' ); + while ( $row = $dbr->fetchObject( $res ) ) { + $n = $row->page_title; + if ($row->page_namespace) { + $ns = $wgContLang->getNsText( $row->page_namespace ); + $n = $ns . ':' . $n; + } + + $pages[] = $n; + } + $dbr->freeResult($res); + + return $pages; +} /** * */ function wfSpecialExport( $page = '' ) { - global $wgOut, $wgLang, $wgRequest; - - if( $wgRequest->getVal( 'action' ) == 'submit') { - $page = $wgRequest->getText( 'pages' ); - $curonly = $wgRequest->getCheck( 'curonly' ); - } else { - # Pre-check the 'current version only' box in the UI - $curonly = true; - } - - if( $page != '' ) { - $wgOut->disable(); - header( "Content-type: application/xml; charset=utf-8" ); - $pages = explode( "\n", $page ); - - $db =& wfGetDB( DB_SLAVE ); - $history = $curonly ? MW_EXPORT_CURRENT : MW_EXPORT_FULL; - $exporter = new WikiExporter( $db, $history ); - $exporter->openStream(); - $exporter->pagesByName( $pages ); - $exporter->closeStream(); - return; - } - - $wgOut->addWikiText( wfMsg( "exporttext" ) ); - $titleObj = Title::makeTitle( NS_SPECIAL, "Export" ); - $action = $titleObj->escapeLocalURL( 'action=submit' ); - $wgOut->addHTML( " -
            - -
            -
            - -
            -" ); -} + global $wgOut, $wgRequest, $wgSitename, $wgExportAllowListContributors; + global $wgExportAllowHistory, $wgExportMaxHistory; -define( 'MW_EXPORT_FULL', 0 ); -define( 'MW_EXPORT_CURRENT', 1 ); + $curonly = true; + $doexport = false; -define( 'MW_EXPORT_BUFFER', 0 ); -define( 'MW_EXPORT_STREAM', 1 ); - -/** - * @package MediaWiki - * @subpackage SpecialPage - */ -class WikiExporter { - var $pageCallback = null; - var $revCallback = null; - - /** - * If using MW_EXPORT_STREAM to stream a large amount of data, - * provide a database connection which is not managed by - * LoadBalancer to read from: some history blob types will - * make additional queries to pull source data while the - * main query is still running. - * - * @param Database $db - * @param int $history one of MW_EXPORT_FULL or MW_EXPORT_CURRENT - * @param int $buffer one of MW_EXPORT_BUFFER or MW_EXPORT_STREAM - */ - function WikiExporter( &$db, $history = MW_EXPORT_CURRENT, - $buffer = MW_EXPORT_BUFFER ) { - $this->db =& $db; - $this->history = $history; - $this->buffer = $buffer; - } - - /** - * Set a callback to be called after each page in the output - * stream is closed. The callback will be passed a database row - * object with the last revision output. - * - * A set callback can be removed by passing null here. - * - * @param mixed $callback - */ - function setPageCallback( $callback ) { - $this->pageCallback = $callback; - } - - /** - * Set a callback to be called after each revision in the output - * stream is closed. The callback will be passed a database row - * object with the revision data. - * - * A set callback can be removed by passing null here. - * - * @param mixed $callback - */ - function setRevisionCallback( $callback ) { - $this->revCallback = $callback; - } - - /** - * Returns the export schema version. - * @return string - */ - function schemaVersion() { - return "0.3"; - } - - /** - * Opens the XML output stream's root element. - * This does not include an xml directive, so is safe to include - * as a subelement in a larger XML stream. Namespace and XML Schema - * references are included. - * - * To capture the stream to a string, use PHP's output buffering - * functions. Output will be encoded in UTF-8. - */ - function openStream() { - global $wgContLanguageCode; - $ver = $this->schemaVersion(); - print wfElement( 'mediawiki', array( - 'xmlns' => "http://www.mediawiki.org/xml/export-$ver/", - 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance", - 'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " . - "http://www.mediawiki.org/xml/export-$ver.xsd", - 'version' => $ver, - 'xml:lang' => $wgContLanguageCode ), - null ) . "\n"; - $this->siteInfo(); - } - - function siteInfo() { - $info = array( - $this->sitename(), - $this->homelink(), - $this->generator(), - $this->caseSetting(), - $this->namespaces() ); - print "\n"; - foreach( $info as $item ) { - print " $item\n"; - } - print "\n"; - } - - function sitename() { - global $wgSitename; - return wfElement( 'sitename', array(), $wgSitename ); - } - - function generator() { - global $wgVersion; - return wfElement( 'generator', array(), "MediaWiki $wgVersion" ); - } - - function homelink() { - $page = Title::newFromText( wfMsgForContent( 'mainpage' ) ); - return wfElement( 'base', array(), $page->getFullUrl() ); - } - - function caseSetting() { - global $wgCapitalLinks; - // "case-insensitive" option is reserved for future - $sensitivity = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; - return wfElement( 'case', array(), $sensitivity ); - } - - function namespaces() { - global $wgContLang; - $spaces = "\n"; - foreach( $wgContLang->getNamespaces() as $ns => $title ) { - $spaces .= ' ' . wfElement( 'namespace', - array( 'key' => $ns ), - str_replace( '_', ' ', $title ) ) . "\n"; + if ( $wgRequest->getCheck( 'addcat' ) ) { + $page = $wgRequest->getText( 'pages' ); + $catname = $wgRequest->getText( 'catname' ); + + if ( $catname !== '' && $catname !== NULL && $catname !== false ) { + $t = Title::makeTitleSafe( NS_CATEGORY, $catname ); + if ( $t ) { + $catpages = wfExportGetPagesFromCategory( $t ); + if ( $catpages ) $page .= "\n" . implode( "\n", $catpages ); + } } - $spaces .= " "; - return $spaces; } - - /** - * Closes the output stream with the closing root element. - * Call when finished dumping things. - */ - function closeStream() { - print "\n"; - } - - /** - * Dumps a series of page and revision records for all pages - * in the database, either including complete history or only - * the most recent version. - * - * - * @param Database $db - */ - function allPages() { - return $this->dumpFrom( '' ); - } - - /** - * @param Title $title - */ - function pageByTitle( $title ) { - return $this->dumpFrom( - 'page_namespace=' . $title->getNamespace() . - ' AND page_title=' . $this->db->addQuotes( $title->getDbKey() ) ); - } - - function pageByName( $name ) { - $title = Title::newFromText( $name ); - if( is_null( $title ) ) { - return new WikiError( "Can't export invalid title" ); + else if( $wgRequest->wasPosted() && $page == '' ) { + $page = $wgRequest->getText( 'pages' ); + $curonly = $wgRequest->getCheck( 'curonly' ); + $rawOffset = $wgRequest->getVal( 'offset' ); + if( $rawOffset ) { + $offset = wfTimestamp( TS_MW, $rawOffset ); } else { - return $this->pageByTitle( $title ); - } - } - - function pagesByName( $names ) { - foreach( $names as $name ) { - $this->pageByName( $name ); + $offset = null; } - } - - - // -------------------- private implementation below -------------------- - - function dumpFrom( $cond = '' ) { - $fname = 'WikiExporter::dumpFrom'; - wfProfileIn( $fname ); - - $page = $this->db->tableName( 'page' ); - $revision = $this->db->tableName( 'revision' ); - $text = $this->db->tableName( 'text' ); - - if( $this->history == MW_EXPORT_FULL ) { - $join = 'page_id=rev_page'; - } elseif( $this->history == MW_EXPORT_CURRENT ) { - $join = 'page_id=rev_page AND page_latest=rev_id'; - } else { - wfProfileOut( $fname ); - return new WikiError( "$fname given invalid history dump type." ); + $limit = $wgRequest->getInt( 'limit' ); + $dir = $wgRequest->getVal( 'dir' ); + $history = array( + 'dir' => 'asc', + 'offset' => false, + 'limit' => $wgExportMaxHistory, + ); + $historyCheck = $wgRequest->getCheck( 'history' ); + if ( $curonly ) { + $history = WikiExporter::CURRENT; + } elseif ( !$historyCheck ) { + if ( $limit > 0 && $limit < $wgExportMaxHistory ) { + $history['limit'] = $limit; + } + if ( !is_null( $offset ) ) { + $history['offset'] = $offset; + } + if ( strtolower( $dir ) == 'desc' ) { + $history['dir'] = 'desc'; + } } - $where = ( $cond == '' ) ? '' : "$cond AND"; - if( $this->buffer == MW_EXPORT_STREAM ) { - $prev = $this->db->bufferResults( false ); - } - if( $cond == '' ) { - // Optimization hack for full-database dump - $pageindex = 'FORCE INDEX (PRIMARY)'; - $revindex = 'FORCE INDEX(page_timestamp)'; + if( $page != '' ) $doexport = true; + } else { + // Default to current-only for GET requests + $page = $wgRequest->getText( 'pages', $page ); + $historyCheck = $wgRequest->getCheck( 'history' ); + if( $historyCheck ) { + $history = WikiExporter::FULL; } else { - $pageindex = ''; - $revindex = ''; + $history = WikiExporter::CURRENT; } - $result = $this->db->query( - "SELECT * FROM - $page $pageindex, - $revision $revindex, - $text - WHERE $where $join AND rev_text_id=old_id - ORDER BY page_id", $fname ); - $wrapper = $this->db->resultObject( $result ); - $this->outputStream( $wrapper ); - if( $this->buffer == MW_EXPORT_STREAM ) { - $this->db->bufferResults( $prev ); - } - - wfProfileOut( $fname ); - } - - /** - * Runs through a query result set dumping page and revision records. - * The result set should be sorted/grouped by page to avoid duplicate - * page records in the output. - * - * The result set will be freed once complete. Should be safe for - * streaming (non-buffered) queries, as long as it was made on a - * separate database connection not managed by LoadBalancer; some - * blob storage types will make queries to pull source data. - * - * @param ResultWrapper $resultset - * @access private - */ - function outputStream( $resultset ) { - $last = null; - while( $row = $resultset->fetchObject() ) { - if( is_null( $last ) || - $last->page_namespace != $row->page_namespace || - $last->page_title != $row->page_title ) { - if( isset( $last ) ) { - $this->closePage( $last ); - } - $this->openPage( $row ); - $last = $row; - } - $this->dumpRev( $row ); - } - if( isset( $last ) ) { - $this->closePage( $last ); - } - $resultset->free(); + if( $page != '' ) $doexport = true; } - - /** - * Opens a section on the output stream, with data - * from the given database row. - * - * @param object $row - * @access private - */ - function openPage( $row ) { - print "\n"; - $title = Title::makeTitle( $row->page_namespace, $row->page_title ); - print ' ' . wfElementClean( 'title', array(), $title->getPrefixedText() ) . "\n"; - print ' ' . wfElement( 'id', array(), $row->page_id ) . "\n"; - if( '' != $row->page_restrictions ) { - print ' ' . wfElement( 'restrictions', array(), - $row->page_restrictions ) . "\n"; - } - } - - /** - * Closes a section on the output stream. - * If a per-page callback has been set, it will be called - * and passed the last database row used for this page. - * - * @param object $row - * @access private - */ - function closePage( $row ) { - print "\n"; - if( isset( $this->pageCallback ) ) { - call_user_func( $this->pageCallback, $row ); - } + + if( !$wgExportAllowHistory ) { + // Override + $history = WikiExporter::CURRENT; } - /** - * Dumps a section on the output stream, with - * data filled in from the given database row. - * - * @param object $row - * @access private - */ - function dumpRev( $row ) { - $fname = 'WikiExporter::dumpRev'; - wfProfileIn( $fname ); - - print " \n"; - print " " . wfElement( 'id', null, $row->rev_id ) . "\n"; - - $ts = wfTimestamp2ISO8601( $row->rev_timestamp ); - print " " . wfElement( 'timestamp', null, $ts ) . "\n"; + $list_authors = $wgRequest->getCheck( 'listauthors' ); + if ( !$curonly || !$wgExportAllowListContributors ) $list_authors = false ; + + if ( $doexport ) { + $wgOut->disable(); - print " "; - if( $row->rev_user ) { - print wfElementClean( 'username', null, $row->rev_user_text ); - print wfElement( 'id', null, $row->rev_user ); - } else { - print wfElementClean( 'ip', null, $row->rev_user_text ); + // Cancel output buffering and gzipping if set + // This should provide safer streaming for pages with history + wfResetOutputBuffers(); + header( "Content-type: application/xml; charset=utf-8" ); + if( $wgRequest->getCheck( 'wpDownload' ) ) { + // Provide a sane filename suggestion + $filename = urlencode( $wgSitename . '-' . wfTimestampNow() . '.xml' ); + $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" ); } - print "\n"; + $pages = explode( "\n", $page ); + + $db = wfGetDB( DB_SLAVE ); + $exporter = new WikiExporter( $db, $history ); + $exporter->list_authors = $list_authors ; + $exporter->openStream(); - if( $row->rev_minor_edit ) { - print " \n"; - } - if( $row->rev_comment != '' ) { - print " " . wfElementClean( 'comment', null, $row->rev_comment ) . "\n"; + foreach( $pages as $page ) { + /* + if( $wgExportMaxHistory && !$curonly ) { + $title = Title::newFromText( $page ); + if( $title ) { + $count = Revision::countByTitle( $db, $title ); + if( $count > $wgExportMaxHistory ) { + wfDebug( __FUNCTION__ . + ": Skipped $page, $count revisions too big\n" ); + continue; + } + } + }*/ + + #Bug 8824: Only export pages the user can read + $title = Title::newFromText( $page ); + if( is_null( $title ) ) continue; #TODO: perhaps output an tag or something. + if( !$title->userCan( 'read' ) ) continue; #TODO: perhaps output an tag or something. + + $exporter->pageByTitle( $title ); } - - $text = Revision::getRevisionText( $row ); - print " " . wfElementClean( 'text', - array( 'xml:space' => 'preserve' ), $text ) . "\n"; - - print " \n"; - wfProfileOut( $fname ); - - if( isset( $this->revCallback ) ) { - call_user_func( $this->revCallback, $row ); - } + $exporter->closeStream(); + return; } -} - -function wfTimestamp2ISO8601( $ts ) { - #2003-08-05T18:30:02Z - return preg_replace( '/^(....)(..)(..)(..)(..)(..)$/', '$1-$2-$3T$4:$5:$6Z', $ts ); -} - -function xmlsafe( $string ) { - $fname = 'xmlsafe'; - wfProfileIn( $fname ); + $self = SpecialPage::getTitleFor( 'Export' ); + $wgOut->addHtml( wfMsgExt( 'exporttext', 'parse' ) ); - /** - * The page may contain old data which has not been properly normalized. - * Invalid UTF-8 sequences or forbidden control characters will make our - * XML output invalid, so be sure to strip them out. - */ - $string = UtfNormal::cleanUp( $string ); + $form = Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $self->getLocalUrl( 'action=submit' ) ) ); - $string = htmlspecialchars( $string ); - wfProfileOut( $fname ); - return $string; -} - -?> + $form .= Xml::inputLabel( wfMsg( 'export-addcattext' ) , 'catname', 'catname', 40 ) . ' '; + $form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '
            '; + + $form .= Xml::openElement( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ) ); + $form .= htmlspecialchars( $page ); + $form .= Xml::closeElement( 'textarea' ); + $form .= '
            '; + + if( $wgExportAllowHistory ) { + $form .= Xml::checkLabel( wfMsg( 'exportcuronly' ), 'curonly', 'curonly', true ) . '
            '; + } else { + $wgOut->addHtml( wfMsgExt( 'exportnohistory', 'parse' ) ); + } + $form .= Xml::checkLabel( wfMsg( 'export-download' ), 'wpDownload', 'wpDownload', true ) . '
            '; + + $form .= Xml::submitButton( wfMsg( 'export-submit' ) ); + $form .= Xml::closeElement( 'form' ); + $wgOut->addHtml( $form ); +} \ No newline at end of file diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialImagelist.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialImagelist.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialImagelist.php 2005-06-19 11:23:01.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialImagelist.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,136 +1,166 @@ getVal( 'sort' ); - $wpIlMatch = $wgRequest->getText( 'wpIlMatch' ); - $dbr =& wfGetDB( DB_SLAVE ); - $image = $dbr->tableName( 'image' ); - $sql = "SELECT img_size,img_name,img_user,img_user_text," . - "img_description,img_timestamp FROM $image"; - - $byname = wfMsg( "byname" ); - $bydate = wfMsg( "bydate" ); - $bysize = wfMsg( "bysize" ); - - if ( !$wgMiserMode && !empty( $wpIlMatch ) ) { - $nt = Title::newFromUrl( $wpIlMatch ); - if($nt ) { - $m = $dbr->strencode( strtolower( $nt->getDBkey() ) ); - $m = str_replace( "%", "\\%", $m ); - $m = str_replace( "_", "\\_", $m ); - $sql .= " WHERE LCASE(img_name) LIKE '%{$m}%'"; - } - } - if ( "bysize" == $sort ) { - $sql .= " ORDER BY img_size DESC"; - $st = $bysize; - } else if ( "byname" == $sort ) { - $sql .= " ORDER BY img_name"; - $st = $byname; - } else { - $sort = "bydate"; - $sql .= " ORDER BY img_timestamp DESC"; - $st = $bydate; - } - list( $limit, $offset ) = wfCheckLimits( 50 ); - if ( 0 == $limit ) { - $lt = wfMsg( 'imagelistall' ); - } else { - $lt = $wgLang->formatNum( "${limit}" ); - $sql .= " LIMIT {$limit}"; - } - $wgOut->addHTML( "

            " . wfMsg( "imglegend" ) . "

            \n" ); - - $text = wfMsg( "imagelisttext", - "{$lt}", "{$st}" ); - $wgOut->addHTML( "

            {$text}\n

            " ); - - $sk = $wgUser->getSkin(); - $sub = wfMsg( "ilsubmit" ); - $titleObj = Title::makeTitle( NS_SPECIAL, "Imagelist" ); - $action = $titleObj->escapeLocalURL( "sort={$sort}&limit={$limit}" ); - - if ( !$wgMiserMode ) { - $wgOut->addHTML( "
            " . - " " . - "
            " ); - } - $nums = array( 50, 100, 250, 500 ); - $here = Title::makeTitle( NS_SPECIAL, 'Imagelist' ); - - $fill = ""; - $first = true; - foreach ( $nums as $num ) { - if ( ! $first ) { $fill .= " | "; } - $first = false; - - $fill .= $sk->makeKnownLinkObj( $here, $wgLang->formatNum( $num ), - "sort=byname&limit={$num}&wpIlMatch=" . urlencode( $wpIlMatch ) ); - } - $text = wfMsg( "showlast", $fill, $byname ); - $wgOut->addHTML( "

            {$text}
            \n" ); - - $fill = ""; - $first = true; - foreach ( $nums as $num ) { - if ( ! $first ) { $fill .= " | "; } - $first = false; - - $fill .= $sk->makeKnownLinkObj( $here, $wgLang->formatNum( $num ), - "sort=bysize&limit={$num}&wpIlMatch=" . urlencode( $wpIlMatch ) ); - } - $text = wfMsg( "showlast", $fill, $bysize ); - $wgOut->addHTML( "{$text}
            \n" ); - - $fill = ""; - $first = true; - foreach ( $nums as $num ) { - if ( ! $first ) { $fill .= " | "; } - $first = false; - - $fill .= $sk->makeKnownLinkObj( $here, $wgLang->formatNum( $num ), - "sort=bydate&limit={$num}&wpIlMatch=" . urlencode( $wpIlMatch ) ); - } - $text = wfMsg( "showlast", $fill, $bydate ); - $wgOut->addHTML( "{$text}

            \n

            " ); - - $res = $dbr->query( $sql, "wfSpecialImagelist" ); - while ( $s = $dbr->fetchObject( $res ) ) { - $name = $s->img_name; - $ut = $s->img_user_text; - if ( 0 == $s->img_user ) { - $ul = $ut; + global $wgOut; + + $pager = new ImageListPager; + + $limit = $pager->getForm(); + $body = $pager->getBody(); + $nav = $pager->getNavigationBar(); + $wgOut->addHTML( + $limit + . '
            ' + . $body + . '
            ' + . $nav ); +} + +/** + * @addtogroup SpecialPage + * @addtogroup Pager + */ +class ImageListPager extends TablePager { + var $mFieldNames = null; + var $mMessages = array(); + var $mQueryConds = array(); + + function __construct() { + global $wgRequest, $wgMiserMode; + if ( $wgRequest->getText( 'sort', 'img_date' ) == 'img_date' ) { + $this->mDefaultDirection = true; } else { - $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut ); + $this->mDefaultDirection = false; } + $search = $wgRequest->getText( 'ilsearch' ); + if ( $search != '' && !$wgMiserMode ) { + $nt = Title::newFromUrl( $search ); + if( $nt ) { + $dbr = wfGetDB( DB_SLAVE ); + $m = $dbr->strencode( strtolower( $nt->getDBkey() ) ); + $m = str_replace( "%", "\\%", $m ); + $m = str_replace( "_", "\\_", $m ); + $this->mQueryConds = array( "LOWER(img_name) LIKE '%{$m}%'" ); + } + } + + parent::__construct(); + } - $ilink = "" . strtr(htmlspecialchars( $name ), '_', ' ') . ""; + function getFieldNames() { + if ( !$this->mFieldNames ) { + $this->mFieldNames = array( + 'img_timestamp' => wfMsg( 'imagelist_date' ), + 'img_name' => wfMsg( 'imagelist_name' ), + 'img_user_text' => wfMsg( 'imagelist_user' ), + 'img_size' => wfMsg( 'imagelist_size' ), + 'img_description' => wfMsg( 'imagelist_description' ), + ); + } + return $this->mFieldNames; + } - $nb = wfMsg( "nbytes", $wgLang->formatNum( $s->img_size ) ); - $l = "(" . - $sk->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), - wfMsg( "imgdesc" ) ) . - ") {$ilink} . . {$nb} . . {$ul} . . " . - $wgLang->timeanddate( $s->img_timestamp, true ); + function isFieldSortable( $field ) { + static $sortable = array( 'img_timestamp', 'img_name', 'img_size' ); + return in_array( $field, $sortable ); + } + + function getQueryInfo() { + $fields = $this->getFieldNames(); + $fields = array_keys( $fields ); + $fields[] = 'img_user'; + return array( + 'tables' => 'image', + 'fields' => $fields, + 'conds' => $this->mQueryConds + ); + } + + function getDefaultSort() { + return 'img_timestamp'; + } + + function getStartBody() { + # Do a link batch query for user pages + if ( $this->mResult->numRows() ) { + $lb = new LinkBatch; + $this->mResult->seek( 0 ); + while ( $row = $this->mResult->fetchObject() ) { + if ( $row->img_user ) { + $lb->add( NS_USER, str_replace( ' ', '_', $row->img_user_text ) ); + } + } + $lb->execute(); + } - $l .= $sk->commentBlock( $s->img_description ); - $wgOut->addHTML( "{$l}
            \n" ); + # Cache messages used in each row + $this->mMessages['imgdesc'] = wfMsgHtml( 'imgdesc' ); + $this->mMessages['imgfile'] = wfMsgHtml( 'imgfile' ); + + return parent::getStartBody(); + } + + function formatValue( $field, $value ) { + global $wgLang; + switch ( $field ) { + case 'img_timestamp': + return $wgLang->timeanddate( $value, true ); + case 'img_name': + $name = $this->mCurrentRow->img_name; + $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), $value ); + $image = wfLocalFile( $value ); + $url = $image->getURL(); + $download = Xml::element('a', array( "href" => $url ), $this->mMessages['imgfile'] ); + return "$link ($download)"; + case 'img_user_text': + if ( $this->mCurrentRow->img_user ) { + $link = $this->getSkin()->makeLinkObj( Title::makeTitle( NS_USER, $value ), + htmlspecialchars( $value ) ); + } else { + $link = htmlspecialchars( $value ); + } + return $link; + case 'img_size': + return $this->getSkin()->formatSize( $value ); + case 'img_description': + return $this->getSkin()->commentBlock( $value ); + } + } + + function getForm() { + global $wgRequest, $wgMiserMode; + $url = $this->getTitle()->escapeLocalURL(); + $search = $wgRequest->getText( 'ilsearch' ); + $s = "

            \n" . + wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ); + if ( !$wgMiserMode ) { + $s .= "
            \n" . + Xml::inputLabel( wfMsg( 'imagelist_search_for' ), 'ilsearch', 'mw-ilsearch', 20, $search ); + } + $s .= " " . Xml::submitButton( wfMsg( 'table_pager_limit_submit' ) ) ." \n" . + $this->getHiddenFields( array( 'limit', 'ilsearch' ) ) . + "
            \n"; + return $s; + } + + function getTableClass() { + return 'imagelist ' . parent::getTableClass(); + } + + function getNavClass() { + return 'imagelist_nav ' . parent::getNavClass(); + } + + function getSortHeaderClass() { + return 'imagelist_sort ' . parent::getSortHeaderClass(); } - $wgOut->addHTML( "

            " ); - $dbr->freeResult( $res ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialImport.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialImport.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialImport.php 2005-10-29 02:42:21.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialImport.php 2007-08-13 12:41:10.000000000 -0400 @@ -3,44 +3,44 @@ * MediaWiki page data importer * Copyright (C) 2003,2005 Brion Vibber * http://www.mediawiki.org/ - * + * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or + * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ -/** */ -require_once( 'WikiError.php' ); - /** * Constructor */ function wfSpecialImport( $page = '' ) { - global $wgUser, $wgOut, $wgLang, $wgRequest, $wgTitle; - global $wgImportSources; - - ### -# $wgOut->addWikiText( "Special:Import is not ready for this beta release, sorry." ); -# return; - ### - + global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources; + global $wgImportTargetNamespace; + + $interwiki = false; + $namespace = $wgImportTargetNamespace; + $frompage = ''; + $history = true; + if( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit') { + $isUpload = false; + $namespace = $wgRequest->getIntOrNull( 'namespace' ); + switch( $wgRequest->getVal( "source" ) ) { case "upload": + $isUpload = true; if( $wgUser->isAllowed( 'importupload' ) ) { $source = ImportStreamSource::newFromUpload( "xmlimport" ); } else { @@ -48,19 +48,33 @@ } break; case "interwiki": + $interwiki = $wgRequest->getVal( 'interwiki' ); + $history = $wgRequest->getCheck( 'interwikiHistory' ); + $frompage = $wgRequest->getText( "frompage" ); $source = ImportStreamSource::newFromInterwiki( - $wgRequest->getVal( "interwiki" ), - $wgRequest->getText( "frompage" ) ); + $interwiki, + $frompage, + $history ); break; default: - $source = new WikiError( "Unknown import source type" ); + $source = new WikiErrorMsg( "importunknownsource" ); } - + if( WikiError::isError( $source ) ) { $wgOut->addWikiText( wfEscapeWikiText( $source->getMessage() ) ); } else { + $wgOut->addWikiText( wfMsg( "importstart" ) ); + $importer = new WikiImporter( $source ); + if( !is_null( $namespace ) ) { + $importer->setTargetNamespace( $namespace ); + } + $reporter = new ImportReporter( $importer, $isUpload, $interwiki ); + + $reporter->open(); $result = $importer->doImport(); + $reporter->close(); + if( WikiError::isError( $result ) ) { $wgOut->addWikiText( wfMsg( "importfailed", wfEscapeWikiText( $result->getMessage() ) ) ); @@ -70,9 +84,9 @@ } } } - - $action = $wgTitle->escapeLocalUrl( 'action=submit' ); - + + $action = $wgTitle->getLocalUrl( 'action=submit' ); + if( $wgUser->isAllowed( 'importupload' ) ) { $wgOut->addWikiText( wfMsg( "importtext" ) ); $wgOut->addHTML( " @@ -83,7 +97,7 @@ - +
  • " ); @@ -92,24 +106,52 @@ $wgOut->addWikiText( wfMsg( 'importnosources' ) ); } } - + if( !empty( $wgImportSources ) ) { $wgOut->addHTML( "
    " . wfMsgHtml('importinterwiki') . " -
    + " . + $wgOut->parse( wfMsg( 'import-interwiki-text' ) ) . " - " ); + foreach( $wgImportSources as $prefix ) { + $iw = htmlspecialchars( $prefix ); + $selected = ($interwiki === $prefix) ? ' selected="selected"' : ''; + $wgOut->addHTML( "\n" ); } $wgOut->addHTML( " - - - + + + " . + wfInput( 'frompage', 50, $frompage ) . + " + + + + " . + wfCheckLabel( wfMsg( 'import-interwiki-history' ), + 'interwikiHistory', 'interwikiHistory', $history ) . + " + + + + + " . wfMsgHtml( 'import-interwiki-namespace' ) . " " . + HTMLnamespaceselector( $namespace, '' ) . " + + + + + " . + wfSubmitButton( wfMsg( 'import-interwiki-submit' ) ) . + " + +
    " ); @@ -117,80 +159,159 @@ } /** + * Reporting callback + * @addtogroup SpecialPage + */ +class ImportReporter { + function __construct( $importer, $upload, $interwiki ) { + $importer->setPageOutCallback( array( $this, 'reportPage' ) ); + $this->mPageCount = 0; + $this->mIsUpload = $upload; + $this->mInterwiki = $interwiki; + } + + function open() { + global $wgOut; + $wgOut->addHtml( "
      \n" ); + } + + function reportPage( $title, $origTitle, $revisionCount, $successCount ) { + global $wgOut, $wgUser, $wgLang, $wgContLang; + + $skin = $wgUser->getSkin(); + + $this->mPageCount++; + + $localCount = $wgLang->formatNum( $successCount ); + $contentCount = $wgContLang->formatNum( $successCount ); + + $wgOut->addHtml( "
    • " . $skin->makeKnownLinkObj( $title ) . + " " . + wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) . + "
    • \n" ); + + if( $successCount > 0 ) { + $log = new LogPage( 'import' ); + if( $this->mIsUpload ) { + $detail = wfMsgForContent( 'import-logentry-upload-detail', + $contentCount ); + $log->addEntry( 'upload', $title, $detail ); + } else { + $interwiki = '[[:' . $this->mInterwiki . ':' . + $origTitle->getPrefixedText() . ']]'; + $detail = wfMsgForContent( 'import-logentry-interwiki-detail', + $contentCount, $interwiki ); + $log->addEntry( 'interwiki', $title, $detail ); + } + + $comment = $detail; // quick + $dbw = wfGetDB( DB_MASTER ); + $nullRevision = Revision::newNullRevision( + $dbw, $title->getArticleId(), $comment, true ); + $nullRevision->insertOn( $dbw ); + # Update page record + $article = new Article( $title ); + $article->updateRevisionOn( $dbw, $nullRevision ); + } + } + + function close() { + global $wgOut; + if( $this->mPageCount == 0 ) { + $wgOut->addHtml( "
    • " . wfMsgHtml( 'importnopages' ) . "
    • \n" ); + } + $wgOut->addHtml( "
    \n" ); + } +} + +/** * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ class WikiRevision { - var $title = NULL; + var $title = null; + var $id = 0; var $timestamp = "20010115000000"; var $user = 0; var $user_text = ""; var $text = ""; var $comment = ""; var $minor = false; - - function setTitle( $text ) { - $this->title = Title::newFromText( $text ); + + function setTitle( $title ) { + if( is_object( $title ) ) { + $this->title = $title; + } elseif( is_null( $title ) ) { + throw new MWException( "WikiRevision given a null title in import. You may need to adjust \$wgLegalTitleChars." ); + } else { + throw new MWException( "WikiRevision given non-object title in import." ); + } + } + + function setID( $id ) { + $this->id = $id; } - + function setTimestamp( $ts ) { # 2003-08-05T18:30:02Z - $this->timestamp = preg_replace( '/^(....)-(..)-(..)T(..):(..):(..)Z$/', '$1$2$3$4$5$6', $ts ); + $this->timestamp = wfTimestamp( TS_MW, $ts ); } - + function setUsername( $user ) { $this->user_text = $user; } - + function setUserIP( $ip ) { $this->user_text = $ip; } - + function setText( $text ) { $this->text = $text; } - + function setComment( $text ) { $this->comment = $text; } - + function setMinor( $minor ) { $this->minor = (bool)$minor; } - + function getTitle() { return $this->title; } - + + function getID() { + return $this->id; + } + function getTimestamp() { return $this->timestamp; } - + function getUser() { return $this->user_text; } - + function getText() { return $this->text; } - + function getComment() { return $this->comment; } - + function getMinor() { return $this->minor; } function importOldRevision() { - $fname = "WikiImporter::importOldRevision"; - $dbw =& wfGetDB( DB_MASTER ); - + $dbw = wfGetDB( DB_MASTER ); + # Sneak a single revision into place $user = User::newFromName( $this->getUser() ); if( $user ) { - $userId = IntVal( $user->getId() ); + $userId = intval( $user->getId() ); $userText = $user->getName(); } else { $userId = 0; @@ -198,24 +319,35 @@ } // avoid memory leak...? - global $wgLinkCache; - $wgLinkCache->clear(); - + $linkCache =& LinkCache::singleton(); + $linkCache->clear(); + $article = new Article( $this->title ); $pageId = $article->getId(); if( $pageId == 0 ) { # must create the page... $pageId = $article->insertOn( $dbw ); + $created = true; + } else { + $created = false; + + $prior = Revision::loadFromTimestamp( $dbw, $this->title, $this->timestamp ); + if( !is_null( $prior ) ) { + // FIXME: this could fail slightly for multiple matches :P + wfDebug( __METHOD__ . ": skipping existing revision for [[" . + $this->title->getPrefixedText() . "]], timestamp " . + $this->timestamp . "\n" ); + return false; + } } - - # FIXME: Check for exact conflicts + # FIXME: Use original rev_id optionally # FIXME: blah blah blah - + #if( $numrows > 0 ) { # return wfMsg( "importhistoryconflict" ); #} - + # Insert the row $revision = new Revision( array( 'page' => $pageId, @@ -227,49 +359,70 @@ 'minor_edit' => $this->minor, ) ); $revId = $revision->insertOn( $dbw ); - $article->updateIfNewerOn( $dbw, $revision ); - + $changed = $article->updateIfNewerOn( $dbw, $revision ); + + if( $created ) { + wfDebug( __METHOD__ . ": running onArticleCreate\n" ); + Article::onArticleCreate( $this->title ); + + wfDebug( __METHOD__ . ": running create updates\n" ); + $article->createUpdates( $revision ); + + } elseif( $changed ) { + wfDebug( __METHOD__ . ": running onArticleEdit\n" ); + Article::onArticleEdit( $this->title ); + + wfDebug( __METHOD__ . ": running edit updates\n" ); + $article->editUpdates( + $this->getText(), + $this->getComment(), + $this->minor, + $this->timestamp, + $revId ); + } + return true; } } /** - * - * @package MediaWiki - * @subpackage SpecialPage + * implements Special:Import + * @addtogroup SpecialPage */ class WikiImporter { var $mSource = null; var $mPageCallback = null; + var $mPageOutCallback = null; var $mRevisionCallback = null; + var $mTargetNamespace = null; var $lastfield; - + function WikiImporter( $source ) { $this->setRevisionCallback( array( &$this, "importRevision" ) ); $this->mSource = $source; } - + function throwXmlError( $err ) { $this->debug( "FAILURE: $err" ); wfDebug( "WikiImporter XML error: $err\n" ); } - + # -------------- - + function doImport() { if( empty( $this->mSource ) ) { return new WikiErrorMsg( "importnotext" ); } - + $parser = xml_parser_create( "UTF-8" ); - + # case folding violates XML standard, turn it off xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); - + xml_set_object( $parser, $this ); xml_set_element_handler( $parser, "in_start", "" ); - + $offset = 0; // for context extraction on error reporting do { $chunk = $this->mSource->readChunk(); @@ -280,24 +433,24 @@ $offset += strlen( $chunk ); } while( $chunk !== false && !$this->mSource->atEnd() ); xml_parser_free( $parser ); - + return true; } - + function debug( $data ) { #wfDebug( "IMPORT: $data\n" ); } - + function notice( $data ) { global $wgCommandLineMode; if( $wgCommandLineMode ) { print "$data\n"; } else { global $wgOut; - $wgOut->addHTML( "
  • $data
  • \n" ); + $wgOut->addHTML( "
  • " . htmlspecialchars( $data ) . "
  • \n" ); } } - + /** * Sets the action to perform as each new page in the stream is reached. * @param callable $callback @@ -308,7 +461,22 @@ $this->mPageCallback = $callback; return $previous; } - + + /** + * Sets the action to perform as each page in the stream is completed. + * Callback accepts the page title (as a Title object), a second object + * with the original title form (in case it's been overridden into a + * local namespace), and a count of revisions. + * + * @param callable $callback + * @return callable + */ + function setPageOutCallback( $callback ) { + $previous = $this->mPageOutCallback; + $this->mPageOutCallback = $callback; + return $previous; + } + /** * Sets the action to perform as each page revision is reached. * @param callable $callback @@ -319,21 +487,36 @@ $this->mRevisionCallback = $callback; return $previous; } - + + /** + * Set a target namespace to override the defaults + */ + function setTargetNamespace( $namespace ) { + if( is_null( $namespace ) ) { + // Don't override namespaces + $this->mTargetNamespace = null; + } elseif( $namespace >= 0 ) { + // FIXME: Check for validity + $this->mTargetNamespace = intval( $namespace ); + } else { + return false; + } + } + /** * Default per-revision callback, performs the import. * @param WikiRevision $revision - * @access private + * @private */ function importRevision( &$revision ) { - $dbw =& wfGetDB( DB_MASTER ); - $dbw->deadlockLoop( array( &$revision, 'importOldRevision' ) ); + $dbw = wfGetDB( DB_MASTER ); + return $dbw->deadlockLoop( array( &$revision, 'importOldRevision' ) ); } /** * Alternate per-revision callback, for debugging. * @param WikiRevision $revision - * @access private + * @private */ function debugRevisionHandler( &$revision ) { $this->debug( "Got revision:" ); @@ -347,24 +530,39 @@ $this->debug( "-- Comment: " . $revision->comment ); $this->debug( "-- Text: " . $revision->text ); } - + /** * Notify the callback function when a new is reached. * @param Title $title - * @access private + * @private */ function pageCallback( $title ) { if( is_callable( $this->mPageCallback ) ) { call_user_func( $this->mPageCallback, $title ); } } - - + + /** + * Notify the callback function when a is closed. + * @param Title $title + * @param Title $origTitle + * @param int $revisionCount + * @param int $successCount number of revisions for which callback returned true + * @private + */ + function pageOutCallback( $title, $origTitle, $revisionCount, $successCount ) { + if( is_callable( $this->mPageOutCallback ) ) { + call_user_func( $this->mPageOutCallback, $title, $origTitle, + $revisionCount, $successCount ); + } + } + + # XML parser callbacks from here out -- beware! function donothing( $parser, $x, $y="" ) { #$this->debug( "donothing" ); } - + function in_start( $parser, $name, $attribs ) { $this->debug( "in_start $name" ); if( $name != "mediawiki" ) { @@ -372,12 +570,14 @@ } xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" ); } - + function in_mediawiki( $parser, $name, $attribs ) { $this->debug( "in_mediawiki $name" ); if( $name == 'siteinfo' ) { xml_set_element_handler( $parser, "in_siteinfo", "out_siteinfo" ); } elseif( $name == 'page' ) { + $this->workRevisionCount = 0; + $this->workSuccessCount = 0; xml_set_element_handler( $parser, "in_page", "out_page" ); } else { return $this->throwXMLerror( "Expected , got <$name>" ); @@ -390,8 +590,8 @@ } xml_set_element_handler( $parser, "donothing", "donothing" ); } - - + + function in_siteinfo( $parser, $name, $attribs ) { // no-ops for now $this->debug( "in_siteinfo $name" ); @@ -407,13 +607,13 @@ return $this->throwXMLerror( "Element <$name> not allowed in ." ); } } - + function out_siteinfo( $parser, $name ) { if( $name == "siteinfo" ) { xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" ); } } - + function in_page( $parser, $name, $attribs ) { $this->debug( "in_page $name" ); @@ -428,26 +628,39 @@ xml_set_character_data_handler( $parser, "char_append" ); break; case "revision": - $this->workRevision = new WikiRevision; - $this->workRevision->setTitle( $this->workTitle ); + if( is_object( $this->pageTitle ) ) { + $this->workRevision = new WikiRevision; + $this->workRevision->setTitle( $this->pageTitle ); + $this->workRevisionCount++; + } else { + // Skipping items due to invalid page title + $this->workRevision = null; + } xml_set_element_handler( $parser, "in_revision", "out_revision" ); break; default: return $this->throwXMLerror( "Element <$name> not allowed in a ." ); } } - + function out_page( $parser, $name ) { $this->debug( "out_page $name" ); if( $name != "page" ) { return $this->throwXMLerror( "Expected , got " ); } xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" ); - - $this->workTitle = NULL; - $this->workRevision = NULL; + + $this->pageOutCallback( $this->pageTitle, $this->origTitle, + $this->workRevisionCount, $this->workSuccessCount ); + + $this->workTitle = null; + $this->workRevision = null; + $this->workRevisionCount = 0; + $this->workSuccessCount = 0; + $this->pageTitle = null; + $this->origTitle = null; } - + function in_nothing( $parser, $name, $attribs ) { $this->debug( "in_nothing $name" ); return $this->throwXMLerror( "No child elements allowed here; got <$name>" ); @@ -463,29 +676,53 @@ } xml_set_element_handler( $parser, "in_$this->parenttag", "out_$this->parenttag" ); xml_set_character_data_handler( $parser, "donothing" ); - + switch( $this->appendfield ) { case "title": $this->workTitle = $this->appenddata; - $this->pageCallback( $this->workTitle ); + $this->origTitle = Title::newFromText( $this->workTitle ); + if( !is_null( $this->mTargetNamespace ) && !is_null( $this->origTitle ) ) { + $this->pageTitle = Title::makeTitle( $this->mTargetNamespace, + $this->origTitle->getDbKey() ); + } else { + $this->pageTitle = Title::newFromText( $this->workTitle ); + } + if( is_null( $this->pageTitle ) ) { + // Invalid page title? Ignore the page + $this->notice( "Skipping invalid page title '$this->workTitle'" ); + } else { + $this->pageCallback( $this->workTitle ); + } + break; + case "id": + if ( $this->parenttag == 'revision' ) { + if( $this->workRevision ) + $this->workRevision->setID( $this->appenddata ); + } break; case "text": - $this->workRevision->setText( $this->appenddata ); + if( $this->workRevision ) + $this->workRevision->setText( $this->appenddata ); break; case "username": - $this->workRevision->setUsername( $this->appenddata ); + if( $this->workRevision ) + $this->workRevision->setUsername( $this->appenddata ); break; case "ip": - $this->workRevision->setUserIP( $this->appenddata ); + if( $this->workRevision ) + $this->workRevision->setUserIP( $this->appenddata ); break; case "timestamp": - $this->workRevision->setTimestamp( $this->appenddata ); + if( $this->workRevision ) + $this->workRevision->setTimestamp( $this->appenddata ); break; case "comment": - $this->workRevision->setComment( $this->appenddata ); + if( $this->workRevision ) + $this->workRevision->setComment( $this->appenddata ); break; case "minor": - $this->workRevision->setMinor( true ); + if( $this->workRevision ) + $this->workRevision->setMinor( true ); break; default: $this->debug( "Bad append: {$this->appendfield}" ); @@ -493,7 +730,7 @@ $this->appendfield = ""; $this->appenddata = ""; } - + function in_revision( $parser, $name, $attribs ) { $this->debug( "in_revision $name" ); switch( $name ) { @@ -514,22 +751,23 @@ return $this->throwXMLerror( "Element <$name> not allowed in a ." ); } } - + function out_revision( $parser, $name ) { $this->debug( "out_revision $name" ); if( $name != "revision" ) { return $this->throwXMLerror( "Expected , got " ); } xml_set_element_handler( $parser, "in_page", "out_page" ); - - $out = call_user_func_array( $this->mRevisionCallback, - array( &$this->workRevision, &$this ) ); - if( !empty( $out ) ) { - global $wgOut; - $wgOut->addHTML( "
  • " . $out . "
  • \n" ); + + if( $this->workRevision ) { + $ok = call_user_func_array( $this->mRevisionCallback, + array( &$this->workRevision, &$this ) ); + if( $ok ) { + $this->workSuccessCount++; + } } } - + function in_contributor( $parser, $name, $attribs ) { $this->debug( "in_contributor $name" ); switch( $name ) { @@ -545,7 +783,7 @@ $this->throwXMLerror( "Invalid tag <$name> in " ); } } - + function out_contributor( $parser, $name ) { $this->debug( "out_contributor $name" ); if( $name != "contributor" ) { @@ -556,17 +794,20 @@ } -/** @package MediaWiki */ +/** + * @todo document (e.g. one-sentence class description). + * @addtogroup SpecialPage + */ class ImportStringSource { function ImportStringSource( $string ) { $this->mString = $string; $this->mRead = false; } - + function atEnd() { return $this->mRead; } - + function readChunk() { if( $this->atEnd() ) { return false; @@ -577,34 +818,35 @@ } } -/** @package MediaWiki */ +/** + * @todo document (e.g. one-sentence class description). + * @addtogroup SpecialPage + */ class ImportStreamSource { function ImportStreamSource( $handle ) { $this->mHandle = $handle; } - + function atEnd() { return feof( $this->mHandle ); } - + function readChunk() { return fread( $this->mHandle, 32768 ); } - - function newFromFile( $filename ) { + + static function newFromFile( $filename ) { $file = @fopen( $filename, 'rt' ); if( !$file ) { - return new WikiError( "Couldn't open import file" ); + return new WikiErrorMsg( "importcantopen" ); } return new ImportStreamSource( $file ); } - function newFromUpload( $fieldname = "xmlimport" ) { - global $wgOut; - + static function newFromUpload( $fieldname = "xmlimport" ) { $upload =& $_FILES[$fieldname]; - - if( !isset( $upload ) ) { + + if( !isset( $upload ) || !$upload['name'] ) { return new WikiErrorMsg( 'importnofile' ); } if( !empty( $upload['error'] ) ) { @@ -617,26 +859,37 @@ return new WikiErrorMsg( 'importnofile' ); } } - - function newFromURL( $url ) { - # fopen-wrappers are normally turned off for security. - ini_set( "allow_url_fopen", true ); - $ret = ImportStreamSource::newFromFile( $url ); - ini_set( "allow_url_fopen", false ); - return $ret; - } - - function newFromInterwiki( $interwiki, $page ) { - $base = Title::getInterwikiLink( $interwiki ); - if( empty( $base ) ) { - return new WikiError( 'Bad interwiki link' ); - } else { - $import = wfUrlencode( "Special:Export/$page" ); - $url = str_replace( "$1", $import, $base ); - return ImportStreamSource::newFromURL( $url ); + + function newFromURL( $url, $method = 'GET' ) { + wfDebug( __METHOD__ . ": opening $url\n" ); + # Use the standard HTTP fetch function; it times out + # quicker and sorts out user-agent problems which might + # otherwise prevent importing from large sites, such + # as the Wikimedia cluster, etc. + $data = Http::request( $method, $url ); + if( $data !== false ) { + $file = tmpfile(); + fwrite( $file, $data ); + fflush( $file ); + fseek( $file, 0 ); + return new ImportStreamSource( $file ); + } else { + return new WikiErrorMsg( 'importcantopen' ); + } + } + + public static function newFromInterwiki( $interwiki, $page, $history=false ) { + $link = Title::newFromText( "$interwiki:Special:Export/$page" ); + if( is_null( $link ) || $link->getInterwiki() == '' ) { + return new WikiErrorMsg( 'importbadinterwiki' ); + } else { + $params = $history ? 'history=1' : ''; + $url = $link->getFullUrl( $params ); + # For interwikis, use POST to avoid redirects. + return ImportStreamSource::newFromURL( $url, "POST" ); } } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialIpblocklist.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialIpblocklist.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialIpblocklist.php 2005-08-12 14:32:28.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialIpblocklist.php 2007-08-06 03:21:46.000000000 -0400 @@ -1,8 +1,7 @@ getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) ); + $id = $wgRequest->getVal( 'id' ); $reason = $wgRequest->getText( 'wpUnblockReason' ); $action = $wgRequest->getText( 'action' ); - - $ipu = new IPUnblockForm( $ip, $reason ); + $successip = $wgRequest->getVal( 'successip' ); + + $ipu = new IPUnblockForm( $ip, $id, $reason ); - if ( "success" == $action ) { - $msg = wfMsg( "ipusuccess", htmlspecialchars( $ip ) ); - $ipu->showList( $msg ); - } else if ( "submit" == $action && $wgRequest->wasPosted() && - $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { - if ( ! $wgUser->isAllowed('block') ) { - $wgOut->sysopRequired(); + if( $action == 'unblock' ) { + # Check permissions + if( !$wgUser->isAllowed( 'block' ) ) { + $wgOut->permissionRequired( 'block' ); return; } + # Check for database lock + if( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return; + } + # Show unblock form + $ipu->showForm( '' ); + } elseif( $action == 'submit' && $wgRequest->wasPosted() + && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { + # Check permissions + if( !$wgUser->isAllowed( 'block' ) ) { + $wgOut->permissionRequired( 'block' ); + return; + } + # Check for database lock + if( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return; + } + # Remove blocks and redirect user to success page $ipu->doSubmit(); - } else if ( "unblock" == $action ) { - $ipu->showForm( "" ); + } elseif( $action == 'success' ) { + # Inform the user of a successful unblock + # (No need to check permissions or locks here, + # if something was done, then it's too late!) + if ( substr( $successip, 0, 1) == '#' ) { + // A block ID was unblocked + $ipu->showList( $wgOut->parse( wfMsg( 'unblocked-id', $successip ) ) ); + } else { + // A username/IP was unblocked + $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $successip ) ) ); + } } else { - $ipu->showList( "" ); + # Just show the block list + $ipu->showList( '' ); } + } /** - * - * @package MediaWiki - * @subpackage SpecialPage + * implements Special:ipblocklist GUI + * @addtogroup SpecialPage */ class IPUnblockForm { - var $ip, $reason; - - function IPUnblockForm( $ip, $reason ) { - $this->ip = $ip; + var $ip, $reason, $id; + + function IPUnblockForm( $ip, $id, $reason ) { + $this->ip = strtr( $ip, '_', ' ' ); + $this->id = $id; $this->reason = $reason; } - - function showForm( $err ) - { - global $wgOut, $wgUser, $wgLang, $wgSysopUserBans; - $wgOut->setPagetitle( wfMsg( "unblockip" ) ); - $wgOut->addWikiText( wfMsg( "unblockiptext" ) ); + function showForm( $err ) { + global $wgOut, $wgUser, $wgSysopUserBans, $wgContLang; + + $wgOut->setPagetitle( wfMsg( 'unblockip' ) ); + $wgOut->addWikiText( wfMsg( 'unblockiptext' ) ); $ipa = wfMsgHtml( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' ); $ipr = wfMsgHtml( 'ipbreason' ); $ipus = wfMsgHtml( 'ipusubmit' ); - $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); - $action = $titleObj->escapeLocalURL( "action=submit" ); + $titleObj = SpecialPage::getTitleFor( "Ipblocklist" ); + $action = $titleObj->getLocalURL( "action=submit" ); + $alignRight = $wgContLang->isRtl() ? 'left' : 'right'; if ( "" != $err ) { $wgOut->setSubtitle( wfMsg( "formerror" ) ); $wgOut->addWikitext( "{$err}\n" ); } $token = htmlspecialchars( $wgUser->editToken() ); - - $wgOut->addHTML( " -
    - - - - - - - - - - - - - -
    {$ipa}: - ip ) . "\" /> -
    {$ipr}: - reason ) . "\" /> -
      - -
    - -
    \n" ); + + $addressPart = false; + if ( $this->id ) { + $block = Block::newFromID( $this->id ); + if ( $block ) { + $encName = htmlspecialchars( $block->getRedactedName() ); + $encId = $this->id; + $addressPart = $encName . Xml::hidden( 'id', $encId ); + } + } + if ( !$addressPart ) { + $addressPart = Xml::input( 'wpUnblockAddress', 20, $this->ip, array( 'type' => 'text', 'tabindex' => '1' ) ); + } + + $wgOut->addHTML( + Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'unblockip' ) ) . + Xml::openElement( 'table', array( 'border' => '0' ) ). + " + + {$ipa} + + + {$addressPart} + + + + + {$ipr} + + " . + Xml::input( 'wpUnblockReason', 40, $this->reason, array( 'type' => 'text', 'tabindex' => '2' ) ) . + " + + +   + " . + Xml::submitButton( $ipus, array( 'name' => 'wpBlock', 'tabindex' => '3' ) ) . + " + " . + Xml::closeElement( 'table' ) . + Xml::hidden( 'wpEditToken', $token ) . + Xml::closeElement( 'form' ) . "\n" + ); } - - function doSubmit() { - global $wgOut, $wgUser, $wgLang; - $block = new Block(); - $this->ip = trim( $this->ip ); + function doSubmit() { + global $wgOut; - if ( $this->ip{0} == "#" ) { - $block->mId = substr( $this->ip, 1 ); + if ( $this->id ) { + $block = Block::newFromID( $this->id ); + if ( $block ) { + $this->ip = $block->getRedactedName(); + } } else { - $block->mAddress = $this->ip; + $block = new Block(); + $this->ip = trim( $this->ip ); + if ( substr( $this->ip, 0, 1 ) == "#" ) { + $id = substr( $this->ip, 1 ); + $block = Block::newFromID( $id ); + } else { + $block = Block::newFromDB( $this->ip ); + if ( !$block ) { + $block = null; + } + } + } + $success = false; + if ( $block ) { + # Delete block + if ( $block->delete() ) { + # Make log entry + $log = new LogPage( 'block' ); + $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $this->ip ), $this->reason ); + $success = true; + } } - # Delete block (if it exists) - # We should probably check for errors rather than just declaring success - $block->delete(); - - # Make log entry - $log = new LogPage( 'block' ); - $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $this->ip ), $this->reason ); - - # Report to the user - $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); - $success = $titleObj->getFullURL( "action=success&ip=" . urlencode( $this->ip ) ); - $wgOut->redirect( $success ); + if ( $success ) { + # Report to the user + $titleObj = SpecialPage::getTitleFor( "Ipblocklist" ); + $success = $titleObj->getFullURL( "action=success&successip=" . urlencode( $this->ip ) ); + $wgOut->redirect( $success ); + } else { + if ( !$this->ip && $this->id ) { + $this->ip = '#' . $this->id; + } + $this->showForm( wfMsg( 'ipb_cant_unblock', htmlspecialchars( $this->id ) ) ); + } } function showList( $msg ) { - global $wgOut; - + global $wgOut, $wgUser; + $wgOut->setPagetitle( wfMsg( "ipblocklist" ) ); if ( "" != $msg ) { $wgOut->setSubtitle( $msg ); } - $wgOut->addHTML( "
      " ); - // FIXME hack to solve #bug 1487 - if(!Block::enumBlocks( "wfAddRow", 0 )) - $wgOut->addHTML( '
    • '.wfMsg( 'ipblocklistempty' ).'
    • ' ); - $wgOut->addHTML( "
    \n" ); + + // Purge expired entries on one in every 10 queries + if ( !mt_rand( 0, 10 ) ) { + Block::purgeExpired(); + } + + $conds = array(); + $matches = array(); + // Is user allowed to see all the blocks? + if ( !$wgUser->isAllowed( 'oversight' ) ) + $conds['ipb_deleted'] = 0; + if ( $this->ip == '' ) { + // No extra conditions + } elseif ( substr( $this->ip, 0, 1 ) == '#' ) { + $conds['ipb_id'] = substr( $this->ip, 1 ); + } elseif ( IP::toUnsigned( $this->ip ) !== false ) { + $conds['ipb_address'] = $this->ip; + $conds['ipb_auto'] = 0; + } elseif( preg_match( '/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\\/(\\d{1,2})$/', $this->ip, $matches ) ) { + $conds['ipb_address'] = Block::normaliseRange( $this->ip ); + $conds['ipb_auto'] = 0; + } else { + $user = User::newFromName( $this->ip ); + if ( $user && ( $id = $user->getID() ) != 0 ) { + $conds['ipb_user'] = $id; + } else { + // Uh...? + $conds['ipb_address'] = $this->ip; + $conds['ipb_auto'] = 0; + } + } + + $pager = new IPBlocklistPager( $this, $conds ); + if ( $pager->getNumRows() ) { + $wgOut->addHTML( + $this->searchForm() . + $pager->getNavigationBar() . + Xml::tags( 'ul', null, $pager->getBody() ) . + $pager->getNavigationBar() + ); + } elseif ( $this->ip != '') { + $wgOut->addHTML( $this->searchForm() ); + $wgOut->addWikiText( wfMsg( 'ipblocklist-no-results' ) ); + } else { + $wgOut->addWikiText( wfMsg( 'ipblocklist-empty' ) ); + } + } + + function searchForm() { + global $wgTitle, $wgScript, $wgRequest; + return + Xml::tags( 'form', array( 'action' => $wgScript ), + Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) . + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, wfMsg( 'ipblocklist-legend' ) ) . + Xml::inputLabel( wfMsg( 'ipblocklist-username' ), 'ip', 'ip', /* size */ false, $this->ip ) . + ' ' . + Xml::submitButton( wfMsg( 'ipblocklist-submit' ) ) . + Xml::closeElement( 'fieldset' ) + ); + } + + /** + * Callback function to output a block + */ + function formatRow( $block ) { + global $wgUser, $wgLang; + + wfProfileIn( __METHOD__ ); + + static $sk=null, $msg=null; + + if( is_null( $sk ) ) + $sk = $wgUser->getSkin(); + if( is_null( $msg ) ) { + $msg = array(); + $keys = array( 'infiniteblock', 'expiringblock', 'contribslink', 'unblocklink', + 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock' ); + foreach( $keys as $key ) { + $msg[$key] = wfMsgHtml( $key ); + } + $msg['blocklistline'] = wfMsg( 'blocklistline' ); + $msg['contribslink'] = wfMsg( 'contribslink' ); + } + + # Prepare links to the blocker's user and talk pages + $blocker_id = $block->getBy(); + $blocker_name = $block->getByName(); + $blocker = $sk->userLink( $blocker_id, $blocker_name ); + $blocker .= $sk->userToolLinks( $blocker_id, $blocker_name ); + + # Prepare links to the block target's user and contribs. pages (as applicable, don't do it for autoblocks) + if( $block->mAuto ) { + $target = $block->getRedactedName(); # Hide the IP addresses of auto-blocks; privacy + } else { + $target = $sk->userLink( $block->mUser, $block->mAddress ) + . $sk->userToolLinks( $block->mUser, $block->mAddress, false, Linker::TOOL_LINKS_NOBLOCK ); + } + + $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true ); + + $properties = array(); + if ( $block->mExpiry === "" || $block->mExpiry === Block::infinity() ) { + $properties[] = $msg['infiniteblock']; + } else { + $properties[] = wfMsgReplaceArgs( $msg['expiringblock'], + array( $wgLang->timeanddate( $block->mExpiry, true ) ) ); + } + if ( $block->mAnonOnly ) { + $properties[] = $msg['anononlyblock']; + } + if ( $block->mCreateAccount ) { + $properties[] = $msg['createaccountblock']; + } + if (!$block->mEnableAutoblock && $block->mUser ) { + $properties[] = $msg['noautoblockblock']; + } + + if ( $block->mBlockEmail && $block->mUser ) { + $properties[] = $msg['emailblock']; + } + + $properties = implode( ', ', $properties ); + + $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $properties ) ); + + $unblocklink = ''; + if ( $wgUser->isAllowed('block') ) { + $titleObj = SpecialPage::getTitleFor( "Ipblocklist" ); + $unblocklink = ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&id=' . urlencode( $block->mId ) ) . ')'; + } + + $comment = $sk->commentBlock( $block->mReason ); + + $s = "{$line} $comment"; + if ( $block->mHideName ) + $s = '' . $s . ''; + + wfProfileOut( __METHOD__ ); + return "
  • $s $unblocklink
  • \n"; } } /** - * Callback function to output a block + * @todo document + * @addtogroup Pager */ -function wfAddRow( $block, $tag ) { - global $wgOut, $wgUser, $wgLang, $wgContLang; - - $sk = $wgUser->getSkin(); +class IPBlocklistPager extends ReverseChronologicalPager { + public $mForm, $mConds; - # Hide addresses blocked by User::spreadBlocks, for privacy - $addr = $block->mAuto ? "#{$block->mId}" : $block->mAddress; + function __construct( $form, $conds = array() ) { + $this->mForm = $form; + $this->mConds = $conds; + parent::__construct(); + } - $name = User::whoIs( $block->mBy ); - $ulink = $sk->makeKnownLinkObj( Title::makeTitle( NS_USER, $name ), $name ); - $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true ); - - if ( $block->mExpiry === "" ) { - $formattedExpiry = wfMsgHtml('infiniteblock'); - } else { - $formattedExpiry = wfMsgHtml('expiringblock', $wgLang->timeanddate( $block->mExpiry, true ) ); + function getStartBody() { + wfProfileIn( __METHOD__ ); + # Do a link batch query + $this->mResult->seek( 0 ); + $lb = new LinkBatch; + + /* + while ( $row = $this->mResult->fetchObject() ) { + $lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); + $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) ); + $lb->addObj( Title::makeTitleSafe( NS_USER, $row->ipb_address ) ); + $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ipb_address ) ); + }*/ + # Faster way + # Usernames and titles are in fact related by a simple substitution of space -> underscore + # The last few lines of Title::secureAndSplit() tell the story. + while ( $row = $this->mResult->fetchObject() ) { + $name = str_replace( ' ', '_', $row->user_name ); + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + $name = str_replace( ' ', '_', $row->ipb_address ); + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + } + $lb->execute(); + wfProfileOut( __METHOD__ ); + return ''; } - $line = wfMsg( "blocklistline", $formattedTime, $ulink, $addr, $formattedExpiry ); - - $wgOut->addHTML( "
  • {$line}" ); + function formatRow( $row ) { + $block = new Block; + $block->initFromRow( $row ); + return $this->mForm->formatRow( $block ); + } - if ( !$block->mAuto ) { - $titleObj = Title::makeTitle( NS_SPECIAL, "Contributions" ); - $wgOut->addHTML( ' (' . $sk->makeKnownLinkObj($titleObj, wfMsgHtml( 'contribslink' ), "target={$block->mAddress}") . ')' ); + function getQueryInfo() { + $conds = $this->mConds; + $conds[] = 'ipb_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() ); + $conds[] = 'ipb_by=user_id'; + return array( + 'tables' => array( 'ipblocks', 'user' ), + 'fields' => $this->mDb->tableName( 'ipblocks' ) . '.*,user_name', + 'conds' => $conds, + ); } - if ( $wgUser->isAllowed('block') ) { - $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); - $wgOut->addHTML( ' (' . $sk->makeKnownLinkObj($titleObj, wfMsgHtml( 'unblocklink' ), 'action=unblock&ip=' . urlencode( $addr ) ) . ')' ); + function getIndexField() { + return 'ipb_timestamp'; } - $wgOut->addHTML( $sk->commentBlock( $block->mReason ) ); - $wgOut->addHTML( "
  • \n" ); } -?> diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialListusers.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialListusers.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialListusers.php 2005-07-13 11:14:12.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialListusers.php 2007-08-20 23:57:54.000000000 -0400 @@ -1,184 +1,194 @@ +# # http://www.mediawiki.org/ -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** * - * @package MediaWiki - * @subpackage SpecialPage - */ - -/** - * + * @addtogroup SpecialPage */ -require_once('QueryPage.php'); /** * This class is used to get a list of user. The ones with specials * rights (sysop, bureaucrat, developer) will have them displayed * next to their names. * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ -class ListUsersPage extends QueryPage { - var $requestedGroup = ''; - var $requestedUser = ''; - var $previousResult = null; - var $concatGroups = ''; - - function getName() { - return 'Listusers'; + +class UsersPager extends AlphabeticPager { + + function __construct($group=null) { + global $wgRequest; + $this->requestedGroup = $group != "" ? $group : $wgRequest->getVal( 'group' ); + $un = $wgRequest->getText( 'username' ); + $this->requestedUser = ''; + if ( $un != '' ) { + $username = Title::makeTitleSafe( NS_USER, $un ); + if( ! is_null( $username ) ) { + $this->requestedUser = $username->getText(); + } + } + parent::__construct(); } - function isSyndicated() { return false; } - /** - * Show a drop down list to select a group as well as a user name - * search box. - * @todo localize - */ - function getPageHeader( ) { - global $wgScript; - - // Various variables used for the form - $action = htmlspecialchars( $wgScript ); - $title = Title::makeTitle( NS_SPECIAL, 'Listusers' ); - $special = htmlspecialchars( $title->getPrefixedDBkey() ); - - // form header - $out = '
    ' . - '' . - wfMsgHtml( 'groups-editgroup-name' ) . ' '; - - $out .= wfMsgHtml( 'specialloguserlabel' ) . ' '; - - // OK button, end of form. - $out .= '
    '; - // congratulations the form is now build - return $out; - } - - function getSQL() { - $dbr =& wfGetDB( DB_SLAVE ); - $user = $dbr->tableName( 'user' ); - $user_groups = $dbr->tableName( 'user_groups' ); - - // We need to get an 'atomic' list of users, so that we - // don't break the list half-way through a user's group set - // and so that lists by group will show all group memberships. - // - // On MySQL 4.1 we could use GROUP_CONCAT to grab group - // assignments together with users pretty easily. On other - // versions, it's not so easy to do it consistently. - // For now we'll just grab the number of memberships, so - // we can then do targetted checks on those who are in - // non-default groups as we go down the list. - - $userspace = NS_USER; - $sql = "SELECT 'Listusers' as type, $userspace AS namespace, user_name AS title, " . - "user_name as value, user_id, COUNT(ug_group) as numgroups " . - "FROM $user ". - "LEFT JOIN $user_groups ON user_id=ug_user " . - $this->userQueryWhere( $dbr ) . - " GROUP BY user_name"; - - return $sql; - } - - function userQueryWhere( &$dbr ) { - $conds = $this->userQueryConditions(); - return empty( $conds ) - ? "" - : "WHERE " . $dbr->makeList( $conds, LIST_AND ); - } - - function userQueryConditions() { - $conds = array(); - if( $this->requestedGroup != '' ) { + + function getIndexField() { + return 'user_name'; + } + + function getQueryInfo() { + $conds=array(); + // don't show hidden names + $conds[]='ipb_deleted IS NULL OR ipb_deleted = 0'; + if ($this->requestedGroup != "") { $conds['ug_group'] = $this->requestedGroup; } - if( $this->requestedUser != '' ) { - $conds['user_name'] = $this->requestedUser; + if ($this->requestedUser != "") { + $conds[] = 'user_name >= ' . wfGetDB()->addQuotes( $this->requestedUser ); } - return $conds; + + list ($user,$user_groups,$ipblocks) = wfGetDB()->tableNamesN('user','user_groups','ipblocks'); + + return array( + 'tables' => " $user LEFT JOIN $user_groups ON user_id=ug_user LEFT JOIN $ipblocks ON user_id=ipb_user AND ipb_auto=0 ", + 'fields' => array('user_name', + 'MAX(user_id) AS user_id', + 'COUNT(ug_group) AS numgroups', + 'MAX(ug_group) AS singlegroup'), + 'options' => array('GROUP BY' => 'user_name'), + 'conds' => $conds + ); + } - - function linkParameters() { - $conds = array(); - if( $this->requestedGroup != '' ) { - $conds['group'] = $this->requestedGroup; - } - if( $this->requestedUser != '' ) { - $conds['username'] = $this->requestedUser; - } - return $conds; - } - - function sortDescending() { - return false; - } - - function formatResult( $skin, $result ) { - global $wgContLang; - - $userPage = Title::makeTitle( $result->namespace, $result->title ); - $name = $skin->makeLinkObj( $userPage, htmlspecialchars( $userPage->getText() ) ); - - if( !isset( $result->numgroups ) || $result->numgroups > 0 ) { - $dbr =& wfGetDB( DB_SLAVE ); - $result = $dbr->select( 'user_groups', - array( 'ug_group' ), - array( 'ug_user' => $result->user_id ), - 'ListUsersPage::formatResult' ); - $groups = array(); - while( $row = $dbr->fetchObject( $result ) ) { - $groups[] = User::getGroupName( $row->ug_group ); - } - $dbr->freeResult( $result ); - - if( count( $groups ) > 0 ) { - $name .= ' (' . - $skin->makeLink( wfMsgForContent( 'administrators' ), - htmlspecialchars( implode( ', ', $groups ) ) ) . - ')'; - } + + function formatRow( $row ) { + $userPage = Title::makeTitle( NS_USER, $row->user_name ); + $name = $this->getSkin()->makeLinkObj( $userPage, htmlspecialchars( $userPage->getText() ) ); + + if( $row->numgroups > 1 || ( $this->requestedGroup && $row->numgroups == 1 ) ) { + $list = array(); + foreach( self::getGroups( $row->user_id ) as $group ) + $list[] = self::buildGroupLink( $group ); + $groups = implode( ', ', $list ); + } elseif( $row->numgroups == 1 ) { + $groups = self::buildGroupLink( $row->singlegroup ); + } else { + $groups = ''; } - return $name; - } + return '
  • ' . wfSpecialList( $name, $groups ) . '
  • '; + } + + function getBody() { + if (!$this->mQueryDone) { + $this->doQuery(); + } + $batch = new LinkBatch; + + $this->mResult->rewind(); + + while ( $row = $this->mResult->fetchObject() ) { + $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); + } + $batch->execute(); + $this->mResult->rewind(); + return parent::getBody(); + } + + function getPageHeader( ) { + global $wgScript, $wgRequest; + $self = $this->getTitle(); + + # Form tag + $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . + '
    ' . + Xml::element( 'legend', array(), wfMsg( 'listusers' ) ); + $out .= Xml::hidden( 'title', $self->getPrefixedDbKey() ); + + # Username field + $out .= Xml::label( wfMsg( 'listusersfrom' ), 'offset' ) . ' ' . + Xml::input( 'username', 20, $this->requestedUser, array( 'id' => 'offset' ) ) . ' '; + + # Group drop-down list + $out .= Xml::label( wfMsg( 'group' ), 'group' ) . ' ' . + Xml::openElement('select', array( 'name' => 'group', 'id' => 'group' ) ) . + Xml::option( wfMsg( 'group-all' ), '' ); + foreach( User::getAllGroups() as $group ) + $out .= Xml::option( User::getGroupName( $group ), $group, $group == $this->requestedGroup ); + $out .= Xml::closeElement( 'select' ) . ' '; + + # Submit button and form bottom + if( $this->mLimit ) + $out .= Xml::hidden( 'limit', $this->mLimit ); + $out .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . + '
    ' . + Xml::closeElement( 'form' ); + + return $out; + } + + /** + * Preserve group and username offset parameters when paging + * @return array + */ + function getDefaultQuery() { + $query = parent::getDefaultQuery(); + if( $this->requestedGroup != '' ) + $query['group'] = $this->requestedGroup; + if( $this->requestedUser != '' ) + $query['username'] = $this->requestedUser; + return $query; + } + + /** + * Get a list of groups the specified user belongs to + * + * @param int $uid + * @return array + */ + private static function getGroups( $uid ) { + $dbr = wfGetDB( DB_SLAVE ); + $groups = array(); + $res = $dbr->select( 'user_groups', 'ug_group', array( 'ug_user' => $uid ), __METHOD__ ); + if( $res && $dbr->numRows( $res ) > 0 ) { + while( $row = $dbr->fetchObject( $res ) ) + $groups[] = $row->ug_group; + $dbr->freeResult( $res ); + } + return $groups; + } + + /** + * Format a link to a group description page + * + * @param string $group + * @return string + */ + private static function buildGroupLink( $group ) { + static $cache = array(); + if( !isset( $cache[$group] ) ) + $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) ); + return $cache[$group]; + } } /** @@ -186,21 +196,22 @@ * $par string (optional) A group to list users from */ function wfSpecialListusers( $par = null ) { - global $wgRequest; - - list( $limit, $offset ) = wfCheckLimits(); + global $wgRequest, $wgOut; + $up = new UsersPager($par); - $slu = new ListUsersPage(); - - /** - * Get some parameters - */ - $groupTarget = isset($par) ? $par : $wgRequest->getVal( 'group' ); - $slu->requestedGroup = $groupTarget; - $slu->requestedUser = $wgRequest->getVal('username'); + # getBody() first to check, if empty + $usersbody = $up->getBody(); + $s = $up->getPageHeader(); + if( $usersbody ) { + $s .= $up->getNavigationBar(); + $s .= '
      ' . $usersbody . '
    '; + $s .= $up->getNavigationBar() ; + } else { + $s .= '

    ' . wfMsgHTML('listusers-noresult') . '

    '; + }; - return $slu->doQuery( $offset, $limit ); + $wgOut->addHTML( $s ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialLockdb.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialLockdb.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialLockdb.php 2005-07-05 16:39:09.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialLockdb.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,8 +1,7 @@ isAllowed('siteadmin') ) { - $wgOut->developerRequired(); + if( !$wgUser->isAllowed( 'siteadmin' ) ) { + $wgOut->permissionRequired( 'siteadmin' ); return; } + + # If the lock file isn't writable, we can do sweet bugger all + global $wgReadOnlyFile; + if( !is_writable( dirname( $wgReadOnlyFile ) ) ) { + DBLockForm::notWritable(); + return; + } + $action = $wgRequest->getVal( 'action' ); $f = new DBLockForm(); @@ -29,21 +36,19 @@ } /** - * - * @package MediaWiki - * @subpackage SpecialPage + * A form to make the database readonly (eg for maintenance purposes). + * @addtogroup SpecialPage */ class DBLockForm { var $reason = ''; - + function DBLockForm() { global $wgRequest; $this->reason = $wgRequest->getText( 'wpLockReason' ); } - - function showForm( $err ) - { - global $wgOut, $wgUser, $wgLang; + + function showForm( $err ) { + global $wgOut, $wgUser; $wgOut->setPagetitle( wfMsg( 'lockdb' ) ); $wgOut->addWikiText( wfMsg( 'lockdbtext' ) ); @@ -55,14 +60,15 @@ $lc = htmlspecialchars( wfMsg( 'lockconfirm' ) ); $lb = htmlspecialchars( wfMsg( 'lockbtn' ) ); $elr = htmlspecialchars( wfMsg( 'enterlockreason' ) ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Lockdb' ); + $titleObj = SpecialPage::getTitleFor( 'Lockdb' ); $action = $titleObj->escapeLocalURL( 'action=submit' ); + $reason = htmlspecialchars( $this->reason ); $token = htmlspecialchars( $wgUser->editToken() ); $wgOut->addHTML( << {$elr}: - + + + "; $err = ''; } else { $wgOut->addWikiText( wfMsg( 'movepagetext' ) ); $movepagebtn = wfMsgHtml( 'movepagebtn' ); $submitVar = 'wpMove'; + $confirm = false; } - if ( !$ot->isTalkPage() ) { + $oldTalk = $ot->getTalkPage(); + $considerTalk = ( !$ot->isTalkPage() && $oldTalk->exists() ); + + if ( $considerTalk ) { $wgOut->addWikiText( wfMsg( 'movepagetalktext' ) ); } $movearticle = wfMsgHtml( 'movearticle' ); $newtitle = wfMsgHtml( 'newtitle' ); - $movetalk = wfMsgHtml( 'movetalk' ); $movereason = wfMsgHtml( 'movereason' ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Movepage' ); + $titleObj = SpecialPage::getTitleFor( 'Movepage' ); $action = $titleObj->escapeLocalURL( 'action=submit' ); $token = htmlspecialchars( $wgUser->editToken() ); @@ -122,41 +139,46 @@ } $moveTalkChecked = $this->moveTalk ? ' checked="checked"' : ''; - + $wgOut->addHTML( "
    @@ -92,10 +98,13 @@ $this->showForm( wfMsg( 'locknoconfirm' ) ); return; } - $fp = fopen( $wgReadOnlyFile, 'w' ); + $fp = @fopen( $wgReadOnlyFile, 'w' ); if ( false === $fp ) { - $wgOut->fileNotFoundError( $wgReadOnlyFile ); + # This used to show a file not found error, but the likeliest reason for fopen() + # to fail at this point is insufficient permission to write to the file...good old + # is_writable() is plain wrong in some cases, it seems... + $this->notWritable(); return; } fwrite( $fp, $this->reason ); @@ -103,17 +112,23 @@ $wgLang->timeanddate( wfTimestampNow() ) . ")\n" ); fclose( $fp ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Lockdb' ); + $titleObj = SpecialPage::getTitleFor( 'Lockdb' ); $wgOut->redirect( $titleObj->getFullURL( 'action=success' ) ); } function showSuccess() { - global $wgOut, $wgUser; + global $wgOut; $wgOut->setPagetitle( wfMsg( 'lockdb' ) ); $wgOut->setSubtitle( wfMsg( 'lockdbsuccesssub' ) ); $wgOut->addWikiText( wfMsg( 'lockdbsuccesstext' ) ); } + + public static function notWritable() { + global $wgOut; + $wgOut->errorPage( 'lockdb', 'lockfilenotwritable' ); + } + } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialLog.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialLog.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialLog.php 2005-06-27 05:14:32.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialLog.php 2007-08-08 14:37:30.000000000 -0400 @@ -1,26 +1,25 @@ # http://www.mediawiki.org/ -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ /** @@ -28,31 +27,30 @@ */ function wfSpecialLog( $par = '' ) { global $wgRequest; - $logReader =& new LogReader( $wgRequest ); - if( '' == $wgRequest->getVal( 'type' ) && !empty( $par ) ) { + $logReader = new LogReader( $wgRequest ); + if( $wgRequest->getVal( 'type' ) == '' && $par != '' ) { $logReader->limitType( $par ); } - $logViewer =& new LogViewer( $logReader ); + $logViewer = new LogViewer( $logReader ); $logViewer->show(); } /** * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ class LogReader { var $db, $joinClauses, $whereClauses; - var $type = '', $user = '', $title = null; - + var $type = '', $user = '', $title = null, $pattern = false; + /** * @param WebRequest $request For internal use use a FauxRequest object to pass arbitrary parameters. */ function LogReader( $request ) { - $this->db =& wfGetDB( DB_SLAVE ); + $this->db = wfGetDB( DB_SLAVE ); $this->setupQuery( $request ); } - + /** * Basic setup and applies the limiting factors from the WebRequest object. * @param WebRequest $request @@ -61,18 +59,25 @@ function setupQuery( $request ) { $page = $this->db->tableName( 'page' ); $user = $this->db->tableName( 'user' ); - $this->joinClauses = array( "LEFT OUTER JOIN $page ON log_namespace=page_namespace AND log_title=page_title" ); - $this->whereClauses = array( 'user_id=log_user' ); - + $this->joinClauses = array( + "LEFT OUTER JOIN $page ON log_namespace=page_namespace AND log_title=page_title", + "INNER JOIN $user ON user_id=log_user" ); + $this->whereClauses = array(); + $this->limitType( $request->getVal( 'type' ) ); $this->limitUser( $request->getText( 'user' ) ); - $this->limitTitle( $request->getText( 'page' ) ); + $this->limitTitle( $request->getText( 'page' ) , $request->getBool( 'pattern' ) ); $this->limitTime( $request->getVal( 'from' ), '>=' ); $this->limitTime( $request->getVal( 'until' ), '<=' ); - + list( $this->limit, $this->offset ) = $request->getLimitOffset(); + + // XXX This all needs to use Pager, ugly hack for now. + global $wgMiserMode; + if( $wgMiserMode ) + $this->offset = min( $this->offset, 10000 ); } - + /** * Set the log reader to return only entries of the given type. * @param string $type A log type ('upload', 'delete', etc) @@ -86,40 +91,55 @@ $safetype = $this->db->strencode( $type ); $this->whereClauses[] = "log_type='$safetype'"; } - + /** * Set the log reader to return only entries by the given user. - * @param string $name Valid user name + * @param string $name (In)valid user name * @private */ function limitUser( $name ) { - $title = Title::makeTitle( NS_USER, $name ); - if( empty( $name ) || is_null( $title ) ) { + if ( $name == '' ) return false; - } - $this->user = str_replace( '_', ' ', $title->getDBkey() ); - $safename = $this->db->strencode( $this->user ); - $user = $this->db->tableName( 'user' ); - $this->whereClauses[] = "user_name='$safename'"; + $usertitle = Title::makeTitleSafe( NS_USER, $name ); + if ( is_null( $usertitle ) ) + return false; + $this->user = $usertitle->getText(); + + /* Fetch userid at first, if known, provides awesome query plan afterwards */ + $userid = $this->db->selectField('user','user_id',array('user_name'=>$this->user)); + if (!$userid) + /* It should be nicer to abort query at all, + but for now it won't pass anywhere behind the optimizer */ + $this->whereClauses[] = "NULL"; + else + $this->whereClauses[] = "log_user=$userid"; } - + /** * Set the log reader to return only entries affecting the given page. * (For the block and rights logs, this is a user page.) * @param string $page Title name as text * @private */ - function limitTitle( $page ) { + function limitTitle( $page , $pattern ) { + global $wgMiserMode; $title = Title::newFromText( $page ); - if( empty( $page ) || is_null( $title ) ) { + + if( strlen( $page ) == 0 || !$title instanceof Title ) return false; - } + $this->title =& $title; - $safetitle = $this->db->strencode( $title->getDBkey() ); + $this->pattern = $pattern; $ns = $title->getNamespace(); - $this->whereClauses[] = "log_namespace=$ns AND log_title='$safetitle'"; + if ( $pattern && !$wgMiserMode ) { + $safetitle = $this->db->escapeLike( $title->getDBkey() ); // use escapeLike to avoid expensive search patterns like 't%st%' + $this->whereClauses[] = "log_namespace=$ns AND log_title LIKE '$safetitle%'"; + } else { + $safetitle = $this->db->strencode( $title->getDBkey() ); + $this->whereClauses[] = "log_namespace=$ns AND log_title = '$safetitle'"; + } } - + /** * Set the log reader to return only entries in a given time range. * @param string $time Timestamp of one endpoint @@ -134,7 +154,7 @@ $safetime = $this->db->strencode( wfTimestamp( TS_MW, $time ) ); $this->whereClauses[] = "log_timestamp $direction '$safetime'"; } - + /** * Build an SQL query from all the set parameters. * @return string the SQL query @@ -142,44 +162,51 @@ */ function getQuery() { $logging = $this->db->tableName( "logging" ); - $user = $this->db->tableName( 'user' ); - $sql = "SELECT log_type, log_action, log_timestamp, + $sql = "SELECT /*! STRAIGHT_JOIN */ log_type, log_action, log_timestamp, log_user, user_name, log_namespace, log_title, page_id, - log_comment, log_params FROM $user, $logging "; + log_comment, log_params FROM $logging "; if( !empty( $this->joinClauses ) ) { - $sql .= implode( ',', $this->joinClauses ); + $sql .= implode( ' ', $this->joinClauses ); } if( !empty( $this->whereClauses ) ) { $sql .= " WHERE " . implode( ' AND ', $this->whereClauses ); } $sql .= " ORDER BY log_timestamp DESC "; - $sql .= $this->db->limitResult( $this->limit, $this->offset ); + $sql = $this->db->limitResult($sql, $this->limit, $this->offset ); return $sql; } - + /** * Execute the query and start returning results. * @return ResultWrapper result object to return the relevant rows */ function getRows() { - return $this->db->resultObject( $this->db->query( $this->getQuery() ) ); + $res = $this->db->query( $this->getQuery(), 'LogReader::getRows' ); + return $this->db->resultObject( $res ); } - + /** * @return string The query type that this LogReader has been limited to. */ function queryType() { return $this->type; } - + /** * @return string The username type that this LogReader has been limited to, if any. */ function queryUser() { return $this->user; } - + + /** + * @return boolean The checkbox, if titles should be searched by a pattern too + */ + function queryPattern() { + return $this->pattern; + } + /** * @return string The text of the title that this LogReader has been limited to. */ @@ -190,29 +217,51 @@ return $this->title->getPrefixedText(); } } + + /** + * Is there at least one row? + * + * @return bool + */ + public function hasRows() { + # Little hack... + $limit = $this->limit; + $this->limit = 1; + $res = $this->db->query( $this->getQuery() ); + $this->limit = $limit; + $ret = $this->db->numRows( $res ) > 0; + $this->db->freeResult( $res ); + return $ret; + } + } /** * - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ class LogViewer { + const NO_ACTION_LINK = 1; + /** * @var LogReader $reader */ var $reader; var $numResults = 0; - + var $flags = 0; + /** * @param LogReader &$reader where to get our data from + * @param integer $flags Bitwise combination of flags: + * self::NO_ACTION_LINK Don't show restore/unblock/block links */ - function LogViewer( &$reader ) { + function LogViewer( &$reader, $flags = 0 ) { global $wgUser; - $this->skin =& $wgUser->getSkin(); + $this->skin = $wgUser->getSkin(); $this->reader =& $reader; + $this->flags = $flags; } - + /** * Take over the whole output page in $wgOut with the log display. */ @@ -221,22 +270,25 @@ $this->showHeader( $wgOut ); $this->showOptions( $wgOut ); $result = $this->getLogRows(); - $this->showPrevNext( $wgOut ); - $this->doShowList( $wgOut, $result ); - $this->showPrevNext( $wgOut ); + if ( $this->numResults > 0 ) { + $this->showPrevNext( $wgOut ); + $this->doShowList( $wgOut, $result ); + $this->showPrevNext( $wgOut ); + } else { + $this->showError( $wgOut ); + } } /** * Load the data from the linked LogReader * Preload the link cache * Initialise numResults - * + * * Must be called before calling showPrevNext * * @return object database result set */ function getLogRows() { - global $wgLinkCache; $result = $this->reader->getRows(); $this->numResults = 0; @@ -244,8 +296,8 @@ $batch = new LinkBatch; while ( $s = $result->fetchObject() ) { // User link - $title = Title::makeTitleSafe( NS_USER, $s->user_name ); - $batch->addObj( $title ); + $batch->addObj( Title::makeTitleSafe( NS_USER, $s->user_name ) ); + $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $s->user_name ) ); // Move destination link if ( $s->log_type == 'move' ) { @@ -253,14 +305,14 @@ $title = Title::newFromText( $paramArray[0] ); $batch->addObj( $title ); } - $this->numResults++; + ++$this->numResults; } - $batch->execute( $wgLinkCache ); + $batch->execute(); return $result; } - + /** * Output just the list of entries given by the linked LogReader, * with extraneous UI elements. Use for displaying log fragments in @@ -268,64 +320,99 @@ * @param OutputPage $out where to send output */ function showList( &$out ) { - $this->doShowList( $out, $this->getLogRows() ); + $result = $this->getLogRows(); + if ( $this->numResults > 0 ) { + $this->doShowList( $out, $result ); + } else { + $this->showError( $out ); + } } - + function doShowList( &$out, $result ) { // Rewind result pointer and go through it again, making the HTML - $html=''; - if ($this->numResults > 0) { - $html = "\n
      \n"; - $result->seek( 0 ); - while( $s = $result->fetchObject() ) { - $html .= $this->logLine( $s ); - } - $html .= "\n
    \n"; + $html = "\n
      \n"; + $result->seek( 0 ); + while( $s = $result->fetchObject() ) { + $html .= $this->logLine( $s ); } - $result->free(); + $html .= "\n
    \n"; $out->addHTML( $html ); + $result->free(); } - + + function showError( &$out ) { + $out->addWikiText( wfMsg( 'logempty' ) ); + } + /** * @param Object $s a single row from the result set * @return string Formatted HTML list item * @private */ function logLine( $s ) { - global $wgLang, $wgLinkCache; + global $wgLang, $wgUser, $wgContLang; + $skin = $wgUser->getSkin(); $title = Title::makeTitle( $s->log_namespace, $s->log_title ); - $user = Title::makeTitleSafe( NS_USER, $s->user_name ); - $time = $wgLang->timeanddate( $s->log_timestamp, true ); + $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $s->log_timestamp), true ); // Enter the existence or non-existence of this page into the link cache, // for faster makeLinkObj() in LogPage::actionText() + $linkCache =& LinkCache::singleton(); if( $s->page_id ) { - $wgLinkCache->addGoodLinkObj( $s->page_id, $title ); + $linkCache->addGoodLinkObj( $s->page_id, $title ); } else { - $wgLinkCache->addBadLinkObj( $title ); + $linkCache->addBadLinkObj( $title ); } - - $userLink = $this->skin->makeLinkObj( $user, htmlspecialchars( $s->user_name ) ); - $comment = $this->skin->commentBlock( $s->log_comment ); + + $userLink = $this->skin->userLink( $s->log_user, $s->user_name ) . $this->skin->userToolLinksRedContribs( $s->log_user, $s->user_name ); + $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $s->log_comment ); $paramArray = LogPage::extractParams( $s->log_params ); $revert = ''; - if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) { - $specialTitle = Title::makeTitle( NS_SPECIAL, 'Movepage' ); - $destTitle = Title::newFromText( $paramArray[0] ); - if ( $destTitle ) { - $revert = '(' . $this->skin->makeKnownLinkObj( $specialTitle, wfMsg( 'revertmove' ), - 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) . - '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) . - '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) . - '&wpMovetalk=0' ) . ')'; + // show revertmove link + if ( !( $this->flags & self::NO_ACTION_LINK ) ) { + if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) { + $destTitle = Title::newFromText( $paramArray[0] ); + if ( $destTitle ) { + $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ), + wfMsg( 'revertmove' ), + 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) . + '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) . + '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) . + '&wpMovetalk=0' ) . ')'; + } + // show undelete link + } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) { + $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ), + wfMsg( 'undeletebtn' ) , + 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')'; + + // show unblock link + } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) { + $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ), + wfMsg( 'unblocklink' ), + 'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')'; + // show change protection link + } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) { + $revert = '(' . $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')'; + // show user tool links for self created users + // TODO: The extension should be handling this, get it out of core! + } elseif ( $s->log_action == 'create2' ) { + if( isset( $paramArray[0] ) ) { + $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true ); + } else { + # Fall back to a blue contributions link + $revert = $this->skin->userToolLinks( 1, $s->log_title ); + } + # Suppress $comment from old entries, not needed and can contain incorrect links + $comment = ''; } } - $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true ); + $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true, true ); $out = "
  • $time $userLink $action $comment $revert
  • \n"; return $out; } - + /** * @param OutputPage &$out where to send output * @private @@ -337,58 +424,84 @@ $out->addWikiText( LogPage::logHeader( $type ) ); } } - + /** * @param OutputPage &$out where to send output * @private */ function showOptions( &$out ) { - global $wgScript; + global $wgScript, $wgMiserMode; $action = htmlspecialchars( $wgScript ); - $title = Title::makeTitle( NS_SPECIAL, 'Log' ); + $title = SpecialPage::getTitleFor( 'Log' ); $special = htmlspecialchars( $title->getPrefixedDBkey() ); $out->addHTML( "
    \n" . - "\n" . - $this->getTypeMenu() . - $this->getUserInput() . - $this->getTitleInput() . - "" . - "
    " ); + '
    ' . + Xml::element( 'legend', array(), wfMsg( 'log' ) ) . + Xml::hidden( 'title', $special ) . "\n" . + $this->getTypeMenu() . "\n" . + $this->getUserInput() . "\n" . + $this->getTitleInput() . "\n" . + (!$wgMiserMode?($this->getTitlePattern()."\n"):"") . + Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" . + "
    " ); } - + /** * @return string Formatted HTML * @private */ function getTypeMenu() { $out = "\n"; + + // Second pass to sort by name + ksort($m); + + // Third pass generates sorted XHTML content + foreach( $m as $text => $type ) { + $selected = ($type == $this->reader->queryType()); + $out .= Xml::option( $text, $type, $selected ) . "\n"; + } + + $out .= ''; return $out; } - + /** * @return string Formatted HTML * @private */ function getUserInput() { - $user = htmlspecialchars( $this->reader->queryUser() ); - return wfMsg('specialloguserlabel') . "\n"; + $user = $this->reader->queryUser(); + return Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'user', 12, $user ); } - + /** * @return string Formatted HTML * @private */ function getTitleInput() { - $title = htmlspecialchars( $this->reader->queryTitle() ); - return wfMsg('speciallogtitlelabel') . "\n"; + $title = $this->reader->queryTitle(); + return Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'page', 20, $title ); } - + + /** + * @return boolean Checkbox + * @private + */ + function getTitlePattern() { + $pattern = $this->reader->queryPattern(); + return Xml::checkLabel( wfMsg( 'log-title-wildcard' ), 'pattern', 'pattern', $pattern ); + } + /** * @param OutputPage &$out where to send output * @private @@ -399,9 +512,10 @@ $pieces[] = 'type=' . urlencode( $this->reader->queryType() ); $pieces[] = 'user=' . urlencode( $this->reader->queryUser() ); $pieces[] = 'page=' . urlencode( $this->reader->queryTitle() ); + $pieces[] = 'pattern=' . urlencode( $this->reader->queryPattern() ); $bits = implode( '&', $pieces ); list( $limit, $offset ) = $wgRequest->getLimitOffset(); - + # TODO: use timestamps instead of offsets to make it more natural # to go huge distances in time $html = wfViewPrevNext( $offset, $limit, @@ -413,4 +527,4 @@ } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialLonelypages.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialLonelypages.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialLonelypages.php 2005-05-26 06:23:35.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialLonelypages.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,25 +1,22 @@ tableNames( 'page', 'pagelinks' ) ); + $dbr = wfGetDB( DB_SLAVE ); + list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' ); return "SELECT 'Lonelypages' AS type, @@ -60,4 +57,4 @@ return $lpp->doQuery( $offset, $limit ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialLongpages.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialLongpages.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialLongpages.php 2004-10-03 06:42:02.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialLongpages.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,19 +1,12 @@ doQuery( $offset, $limit ); +function wfSpecialLongpages() { + list( $limit, $offset ) = wfCheckLimits(); + + $lpp = new LongPagesPage(); + + $lpp->doQuery( $offset, $limit ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialMostlinked.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialMostlinked.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialMostlinked.php 2005-08-14 23:03:19.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialMostlinked.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,31 +1,29 @@ + * @author Rob Church + * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason + * @copyright © 2006 Rob Church + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later */ class MostlinkedPage extends QueryPage { - function getName() { - return 'Mostlinked'; - } - - function isExpensive() { - return true; - } + function getName() { return 'Mostlinked'; } + function isExpensive() { return true; } function isSyndicated() { return false; } + /** + * Note: Getting page_namespace only works if $this->isCached() is false + */ function getSQL() { - $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'pagelinks', 'page' ) ); + $dbr = wfGetDB( DB_SLAVE ); + list( $pagelinks, $page ) = $dbr->tableNamesN( 'pagelinks', 'page' ); return "SELECT 'Mostlinked' AS type, pl_namespace AS namespace, @@ -34,24 +32,50 @@ page_namespace FROM $pagelinks LEFT JOIN $page ON pl_namespace=page_namespace AND pl_title=page_title - GROUP BY pl_namespace,pl_title + GROUP BY 1,2,3,5 HAVING COUNT(*) > 1"; } - function formatResult( $skin, $result ) { - global $wgContLang; + /** + * Pre-fill the link cache + */ + function preprocessResults( &$db, &$res ) { + if( $db->numRows( $res ) > 0 ) { + $linkBatch = new LinkBatch(); + while( $row = $db->fetchObject( $res ) ) + $linkBatch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) ); + $db->dataSeek( $res, 0 ); + $linkBatch->execute(); + } + } - $nt = Title::makeTitle( $result->namespace, $result->title ); - $text = $wgContLang->convert( $nt->getPrefixedText() ); - if ( is_null( $result->page_namespace ) ) - $plink = $skin->makeBrokenLink( $nt->getPrefixedText(), $text ); - else - $plink = $skin->makeKnownLink( $nt->getPrefixedText(), $text ); - - $nl = wfMsg( "nlinks", $result->value ); - $nlink = $skin->makeKnownLink( $wgContLang->specialPage( "Whatlinkshere" ), $nl, "target=" . $nt->getPrefixedURL() ); + /** + * Make a link to "what links here" for the specified title + * + * @param $title Title being queried + * @param $skin Skin to use + * @return string + */ + function makeWlhLink( &$title, $caption, &$skin ) { + $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() ); + return $skin->makeKnownLinkObj( $wlh, $caption ); + } - return "{$plink} ({$nlink})"; + /** + * Make links to the page corresponding to the item, and the "what links here" page for it + * + * @param $skin Skin to be used + * @param $result Result row + * @return string + */ + function formatResult( $skin, $result ) { + global $wgLang; + $title = Title::makeTitleSafe( $result->namespace, $result->title ); + $link = $skin->makeLinkObj( $title ); + $wlh = $this->makeWlhLink( $title, + wfMsgExt( 'nlinks', array( 'parsemag', 'escape'), + $wgLang->formatNum( $result->value ) ), $skin ); + return wfSpecialList( $link, $wlh ); } } @@ -66,4 +90,4 @@ $wpp->doQuery( $offset, $limit ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialMovepage.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialMovepage.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialMovepage.php 2005-10-22 12:15:17.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialMovepage.php 2007-08-20 23:57:54.000000000 -0400 @@ -1,27 +1,28 @@ isAllowed( 'move' ) ) { + $wgOut->showPermissionsErrorPage( array( $wgUser->isAnon() ? 'movenologintext' : 'movenotallowed' ) ); + return; + } - # check rights. We don't want newbies to move pages to prevents possible attack - if ( !$wgUser->isAllowed( 'move' ) or $wgUser->isBlocked() or ($wgOnlySysopMayMove and $wgUser->isNewbie())) { - $wgOut->errorpage( "movenologin", "movenologintext" ); + # Don't allow blocked users to move pages + if ( $wgUser->isBlocked() ) { + $wgOut->blockedPage(); return; } - # We don't move protected pages + + # Check for database lock if ( wfReadOnly() ) { $wgOut->readOnlyPage(); return; @@ -40,36 +41,45 @@ } /** - * - * @package MediaWiki - * @subpackage SpecialPage + * HTML form for Special:Movepage + * @addtogroup SpecialPage */ class MovePageForm { var $oldTitle, $newTitle, $reason; # Text input var $moveTalk, $deleteAndMove; - + + private $watch = false; + function MovePageForm( $par ) { global $wgRequest; $target = isset($par) ? $par : $wgRequest->getVal( 'target' ); $this->oldTitle = $wgRequest->getText( 'wpOldTitle', $target ); $this->newTitle = $wgRequest->getText( 'wpNewTitle' ); $this->reason = $wgRequest->getText( 'wpReason' ); - $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true ); - $this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ); + if ( $wgRequest->wasPosted() ) { + $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', false ); + } else { + $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true ); + } + $this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ) && $wgRequest->getBool( 'wpConfirm' ); + $this->watch = $wgRequest->getCheck( 'wpWatch' ); } - + function showForm( $err ) { - global $wgOut, $wgUser, $wgLang; + global $wgOut, $wgUser, $wgContLang; + + $start = $wgContLang->isRTL() ? 'right' : 'left'; + $end = $wgContLang->isRTL() ? 'left' : 'right'; $wgOut->setPagetitle( wfMsg( 'movepage' ) ); $ot = Title::newFromURL( $this->oldTitle ); if( is_null( $ot ) ) { - $wgOut->errorpage( 'notargettitle', 'notargettext' ); + $wgOut->showErrorPage( 'notargettitle', 'notargettext' ); return; } $oldTitle = $ot->getPrefixedText(); - + $encOldTitle = htmlspecialchars( $oldTitle ); if( $this->newTitle == '' ) { # Show the current title as a default @@ -96,23 +106,30 @@ $wgOut->addWikiText( wfMsg( 'delete_and_move_text', $encNewTitle ) ); $movepagebtn = wfMsgHtml( 'delete_and_move' ); $submitVar = 'wpDeleteAndMove'; + $confirm = " +
    " . Xml::checkLabel( wfMsg( 'delete_and_move_confirm' ), 'wpConfirm', 'wpConfirm' ) . "
    - - + + - - + - - + " ); - if ( ! $ot->isTalkPage() ) { + if ( $considerTalk ) { $wgOut->addHTML( " - - + " ); } + + $watchChecked = $this->watch || $wgUser->getBoolOption( 'watchmoves' ) || $ot->userIsWatching(); + $watch = ''; + $watch .= ''; + $watch .= ''; + $wgOut->addHtml( $watch ); + $wgOut->addHTML( " + {$confirm} - @@ -164,19 +186,18 @@ \n" ); + $this->showLogFragment( $ot, $wgOut ); + } function doSubmit() { - global $wgOut, $wgUser, $wgLang; - global $wgDeferredUpdateList, $wgMessageCache; - global $wgUseSquid, $wgRequest; - $fname = "MovePageForm::doSubmit"; - + global $wgOut, $wgUser, $wgRequest; + if ( $wgUser->pingLimiter( 'move' ) ) { $wgOut->rateLimited(); return; } - + # Variables beginning with 'o' for old article 'n' for new article $ot = Title::newFromText( $this->oldTitle ); @@ -200,33 +221,43 @@ $this->showForm( $error ); return; } - - # Move talk page if - # (1) the checkbox says to, - # (2) the namespaces are not themselves talk namespaces, and of course - # (3) it exists. - if ( ( $wgRequest->getVal('wpMovetalk') == 1 ) && - !$ot->isTalkPage() && - !$nt->isTalkPage() ) { - - $ott = $ot->getTalkPage(); - $ntt = $nt->getTalkPage(); - - # Attempt the move - $error = $ott->moveTo( $ntt, true, $this->reason ); - if ( $error === true ) { - $talkmoved = 1; + + wfRunHooks( 'SpecialMovepageAfterMove', array( &$this , &$ot , &$nt ) ) ; + + # Move the talk page if relevant, if it exists, and if we've been told to + $ott = $ot->getTalkPage(); + if( $ott->exists() ) { + if( $this->moveTalk && !$ot->isTalkPage() && !$nt->isTalkPage() ) { + $ntt = $nt->getTalkPage(); + + # Attempt the move + $error = $ott->moveTo( $ntt, true, $this->reason ); + if ( $error === true ) { + $talkmoved = 1; + wfRunHooks( 'SpecialMovepageAfterMove', array( &$this , &$ott , &$ntt ) ) ; + } else { + $talkmoved = $error; + } } else { - $talkmoved = $error; + # Stay silent on the subject of talk. + $talkmoved = ''; } } else { - # Stay silent on the subject of talk. - $talkmoved = ''; + $talkmoved = 'notalkpage'; } + # Deal with watches + if( $this->watch ) { + $wgUser->addWatch( $ot ); + $wgUser->addWatch( $nt ); + } else { + $wgUser->removeWatch( $ot ); + $wgUser->removeWatch( $nt ); + } + # Give back result to user. - $titleObj = Title::makeTitle( NS_SPECIAL, 'Movepage' ); - $success = $titleObj->getFullURL( + $titleObj = SpecialPage::getTitleFor( 'Movepage' ); + $success = $titleObj->getFullURL( 'action=success&oldtitle=' . wfUrlencode( $ot->getPrefixedText() ) . '&newtitle=' . wfUrlencode( $nt->getPrefixedText() ) . '&talkmoved='.$talkmoved ); @@ -235,32 +266,46 @@ } function showSuccess() { - global $wgOut, $wgRequest, $wgRawHtml; - + global $wgOut, $wgRequest, $wgUser; + + $old = Title::newFromText( $wgRequest->getVal( 'oldtitle' ) ); + $new = Title::newFromText( $wgRequest->getVal( 'newtitle' ) ); + + if( is_null( $old ) || is_null( $new ) ) { + throw new ErrorPageError( 'badtitle', 'badtitletext' ); + } + $wgOut->setPagetitle( wfMsg( 'movepage' ) ); $wgOut->setSubtitle( wfMsg( 'pagemovedsub' ) ); - $oldtitle = $wgRequest->getVal('oldtitle'); - $newtitle = $wgRequest->getVal('newtitle'); - $talkmoved = $wgRequest->getVal('talkmoved'); - $text = wfMsg( 'pagemovedtext', $oldtitle, $newtitle ); - - # Temporarily disable raw html wikitext option out of XSS paranoia - $marchingantofdoom = $wgRawHtml; - $wgRawHtml = false; - $wgOut->addWikiText( $text ); - $wgRawHtml = $marchingantofdoom; + $talkmoved = $wgRequest->getVal( 'talkmoved' ); + $oldUrl = $old->getFullUrl( 'redirect=no' ); + $newUrl = $new->getFullURl(); + $oldText = $old->getPrefixedText(); + $newText = $new->getPrefixedText(); + $oldLink = "[$oldUrl $oldText]"; + $newLink = "[$newUrl $newText]"; + + $s = wfMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText ); if ( $talkmoved == 1 ) { - $wgOut->addWikiText( wfMsg( 'talkpagemoved' ) ); + $s .= "\n\n" . wfMsg( 'talkpagemoved' ); } elseif( 'articleexists' == $talkmoved ) { - $wgOut->addWikiText( wfMsg( 'talkexists' ) ); + $s .= "\n\n" . wfMsg( 'talkexists' ); } else { - $ot = Title::newFromURL( $oldtitle ); - if ( ! $ot->isTalkPage() ) { - $wgOut->addWikiText( wfMsg( 'talkpagenotmoved', wfMsg( $talkmoved ) ) ); + if( !$old->isTalkPage() && $talkmoved != 'notalkpage' ) { + $s .= "\n\n" . wfMsg( 'talkpagenotmoved', wfMsg( $talkmoved ) ); } } + $wgOut->addWikiText( $s ); + } + + function showLogFragment( $title, &$out ) { + $out->addHtml( wfElement( 'h2', NULL, LogPage::logName( 'move' ) ) ); + $request = new FauxRequest( array( 'page' => $title->getPrefixedText(), 'type' => 'move' ) ); + $viewer = new LogViewer( new LogReader( $request ) ); + $viewer->showList( $out ); } + } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialNewimages.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialNewimages.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialNewimages.php 2005-05-16 00:47:51.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialNewimages.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,61 +1,111 @@ getText( 'wpIlMatch' ); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $sk = $wgUser->getSkin(); + $shownav = !$specialPage->including(); + $hidebots = $wgRequest->getBool('hidebots',1); + + $hidebotsql = ''; + if ($hidebots) { + + /** Make a list of group names which have the 'bot' flag + set. + */ + $botconds=array(); + foreach ($wgGroupPermissions as $groupname=>$perms) { + if(array_key_exists('bot',$perms) && $perms['bot']) { + $botconds[]="ug_group='$groupname'"; + } + } + + /* If not bot groups, do not set $hidebotsql */ + if ($botconds) { + $isbotmember=$dbr->makeList($botconds, LIST_OR); + + /** This join, in conjunction with WHERE ug_group + IS NULL, returns only those rows from IMAGE + where the uploading user is not a member of + a group which has the 'bot' permission set. + */ + $ug = $dbr->tableName('user_groups'); + $hidebotsql = " LEFT OUTER JOIN $ug ON img_user=ug_user AND ($isbotmember)"; + } + } + + $image = $dbr->tableName('image'); + + $sql="SELECT img_timestamp from $image"; + if ($hidebotsql) { + $sql .= "$hidebotsql WHERE ug_group IS NULL"; + } + $sql.=' ORDER BY img_timestamp DESC LIMIT 1'; + $res = $dbr->query($sql, 'wfSpecialNewImages'); + $row = $dbr->fetchRow($res); + if($row!==false) { + $ts=$row[0]; + } else { + $ts=false; + } + $dbr->freeResult($res); + $sql=''; /** If we were clever, we'd use this to cache. */ - $latestTimestamp = wfTimestamp( TS_MW, $dbr->selectField( - 'image', 'img_timestamp', - '', 'wfSpecialNewimages', - array( 'ORDER BY' => 'img_timestamp DESC', - 'LIMIT' => 1 ) ) ); - + $latestTimestamp = wfTimestamp( TS_MW, $ts); + /** Hardcode this for now. */ $limit = 48; - + + if ( $parval = intval( $par ) ) { + if ( $parval <= $limit && $parval > 0 ) { + $limit = $parval; + } + } + $where = array(); - if ( $wpIlMatch != '' ) { + $searchpar = ''; + if ( $wpIlMatch != '' && !$wgMiserMode) { $nt = Title::newFromUrl( $wpIlMatch ); if($nt ) { $m = $dbr->strencode( strtolower( $nt->getDBkey() ) ); $m = str_replace( '%', "\\%", $m ); $m = str_replace( '_', "\\_", $m ); - $where[] = "LCASE(img_name) LIKE '%{$m}%'"; + $where[] = "LOWER(img_name) LIKE '%{$m}%'"; + $searchpar = '&wpIlMatch=' . urlencode( $wpIlMatch ); } - } - + } + $invertSort = false; if( $until = $wgRequest->getVal( 'until' ) ) { - $where[] = 'img_timestamp < ' . $dbr->timestamp( $until ); + $where[] = "img_timestamp < '" . $dbr->timestamp( $until ) . "'"; } if( $from = $wgRequest->getVal( 'from' ) ) { - $where[] = 'img_timestamp >= ' . $dbr->timestamp( $from ); + $where[] = "img_timestamp >= '" . $dbr->timestamp( $from ) . "'"; $invertSort = true; } - - $res = $dbr->select( 'image', - array( 'img_size', 'img_name', 'img_user', 'img_user_text', - 'img_description', 'img_timestamp' ), - $where, - 'wfSpecialNewimages', - array( 'LIMIT' => $limit + 1, - 'ORDER BY' => 'img_timestamp' . ( $invertSort ? '' : ' DESC' ) ) - ); + $sql='SELECT img_size, img_name, img_user, img_user_text,'. + "img_description,img_timestamp FROM $image"; + + if($hidebotsql) { + $sql .= $hidebotsql; + $where[]='ug_group IS NULL'; + } + if(count($where)) { + $sql.=' WHERE '.$dbr->makeList($where, LIST_AND); + } + $sql.=' ORDER BY img_timestamp '. ( $invertSort ? '' : ' DESC' ); + $sql.=' LIMIT '.($limit+1); + $res = $dbr->query($sql, 'wfSpecialNewImages'); /** * We have to flip things around to get the last N after a certain date @@ -69,7 +119,7 @@ } } $dbr->freeResult( $res ); - + $gallery = new ImageGallery(); $firstTimestamp = null; $lastTimestamp = null; @@ -80,66 +130,78 @@ # don't actually show it. break; } - + $name = $s->img_name; $ut = $s->img_user_text; $nt = Title::newFromText( $name, NS_IMAGE ); - $img = Image::newFromTitle( $nt ); $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut ); - $gallery->add( $img, "$ul
    \n".$wgLang->timeanddate( $s->img_timestamp, true )."
    \n" ); - + $gallery->add( $nt, "$ul
    \n".$wgLang->timeanddate( $s->img_timestamp, true )."
    \n" ); + $timestamp = wfTimestamp( TS_MW, $s->img_timestamp ); if( empty( $firstTimestamp ) ) { $firstTimestamp = $timestamp; } $lastTimestamp = $timestamp; } - + $bydate = wfMsg( 'bydate' ); $lt = $wgLang->formatNum( min( $shownImages, $limit ) ); - $text = wfMsg( "imagelisttext", - "{$lt}", "{$bydate}" ); - $wgOut->addHTML( "

    {$text}\n

    " ); + if ($shownav) { + $text = wfMsgExt( 'imagelisttext', array('parse'), $lt, $bydate ); + $wgOut->addHTML( $text . "\n" ); + } $sub = wfMsg( 'ilsubmit' ); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Newimages' ); - $action = $titleObj->escapeLocalURL( "limit={$limit}" ); - - $wgOut->addHTML( "" . - " " . - "" ); - $here = $wgContLang->specialPage( 'Newimages' ); + $titleObj = SpecialPage::getTitleFor( 'Newimages' ); + $action = $titleObj->escapeLocalURL( $hidebots ? '' : 'hidebots=0' ); + if ($shownav && !$wgMiserMode) { + $wgOut->addHTML( "" . + Xml::input( 'wpIlMatch', 20, $wpIlMatch ) . ' ' . + Xml::submitButton( $sub, array( 'name' => 'wpIlSubmit' ) ) . + "" ); + } /** * Paging controls... */ + + # If we change bot visibility, this needs to be carried along. + if(!$hidebots) { + $botpar='&hidebots=0'; + } else { + $botpar=''; + } $now = wfTimestampNow(); - $date = $wgLang->timeanddate( $now ); - $dateLink = $sk->makeKnownLinkObj( $titleObj, wfMsg( 'rclistfrom', $date ), 'from=' . $now ); - - $prevLink = wfMsg( 'prevn', $wgLang->formatNum( $limit ) ); + $date = $wgLang->timeanddate( $now, true ); + $dateLink = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml( 'sp-newimages-showfrom', $date ), 'from='.$now.$botpar.$searchpar ); + + $botLink = $sk->makeKnownLinkObj($titleObj, wfMsgHtml( 'showhidebots', ($hidebots ? wfMsgHtml('show') : wfMsgHtml('hide'))),'hidebots='.($hidebots ? '0' : '1').$searchpar); + + $prevLink = wfMsgHtml( 'prevn', $wgLang->formatNum( $limit ) ); if( $firstTimestamp && $firstTimestamp != $latestTimestamp ) { - $prevLink = $sk->makeKnownLinkObj( $titleObj, $prevLink, 'from=' . $firstTimestamp ); + $prevLink = $sk->makeKnownLinkObj( $titleObj, $prevLink, 'from=' . $firstTimestamp . $botpar . $searchpar ); } - - $nextLink = wfMsg( 'nextn', $wgLang->formatNum( $limit ) ); + + $nextLink = wfMsgHtml( 'nextn', $wgLang->formatNum( $limit ) ); if( $shownImages > $limit && $lastTimestamp ) { - $nextLink = $sk->makeKnownLinkObj( $titleObj, $nextLink, 'until=' . $lastTimestamp ); + $nextLink = $sk->makeKnownLinkObj( $titleObj, $nextLink, 'until=' . $lastTimestamp.$botpar.$searchpar ); } - - $prevnext = '

    ' . wfMsg( 'viewprevnext', $prevLink, $nextLink, $dateLink ) . '

    '; - $wgOut->addHTML( $prevnext ); - + + $prevnext = '

    ' . $botLink . ' '. wfMsgHtml( 'viewprevnext', $prevLink, $nextLink, $dateLink ) .'

    '; + + if ($shownav) + $wgOut->addHTML( $prevnext ); + if( count( $images ) ) { $wgOut->addHTML( $gallery->toHTML() ); - $wgOut->addHTML( $prevnext ); + if ($shownav) + $wgOut->addHTML( $prevnext ); } else { $wgOut->addWikiText( wfMsg( 'noimages' ) ); } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialNewpages.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialNewpages.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialNewpages.php 2005-09-16 01:16:30.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialNewpages.php 2007-08-26 08:22:41.000000000 -0400 @@ -1,22 +1,23 @@ namespace = $namespace; + $this->username = $username; + } + function getName() { return 'Newpages'; } @@ -24,114 +25,188 @@ function isExpensive() { # Indexed on RC, and will *not* work with querycache yet. return false; - #return parent::isExpensive(); + } + + function makeUserWhere( &$dbo ) { + $title = Title::makeTitleSafe( NS_USER, $this->username ); + if( $title ) { + return ' AND rc_user_text = ' . $dbo->addQuotes( $title->getText() ); + } else { + return ''; + } + } + + private function makeNamespaceWhere() { + return $this->namespace !== 'all' + ? ' AND rc_namespace = ' . intval( $this->namespace ) + : ''; } function getSQL() { - global $wgUser, $wgOnlySysopsCanPatrol, $wgUseRCPatrol; - $usepatrol = ( $wgUseRCPatrol && $wgUser->isLoggedIn() && - ( $wgUser->isAllowed('patrol') || !$wgOnlySysopsCanPatrol ) ) ? 1 : 0; - $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'recentchanges', 'page', 'text' ) ); + global $wgUser, $wgUseRCPatrol; + $usepatrol = ( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) ? 1 : 0; + $dbr = wfGetDB( DB_SLAVE ); + list( $recentchanges, $page ) = $dbr->tableNamesN( 'recentchanges', 'page' ); + + $nsfilter = $this->makeNamespaceWhere(); + $uwhere = $this->makeUserWhere( $dbr ); # FIXME: text will break with compression return "SELECT 'Newpages' as type, - rc_namespace AS namespace, - rc_title AS title, - rc_cur_id AS value, - rc_user AS user, - rc_user_text AS user_text, - rc_comment as comment, - rc_timestamp AS timestamp, - '{$usepatrol}' as usepatrol, - rc_patrolled AS patrolled, - rc_id AS rcid, - page_len as length, - page_latest as rev_id + rc_namespace AS namespace, + rc_title AS title, + rc_cur_id AS cur_id, + rc_user AS \"user\", + rc_user_text AS user_text, + rc_comment as \"comment\", + rc_timestamp AS timestamp, + rc_timestamp AS value, + '{$usepatrol}' as usepatrol, + rc_patrolled AS patrolled, + rc_id AS rcid, + page_len as length, + page_latest as rev_id FROM $recentchanges,$page WHERE rc_cur_id=page_id AND rc_new=1 - AND rc_namespace=".NS_MAIN." AND page_is_redirect=0"; + {$nsfilter} + AND page_is_redirect = 0 + {$uwhere}"; } - - function formatResult( $skin, $result ) { - global $wgLang, $wgContLang, $wgUser, $wgOnlySysopsCanPatrol, $wgUseRCPatrol; - $u = $result->user; - $ut = $result->user_text; - - $length = wfMsg( 'nbytes', $wgLang->formatNum( $result->length ) ); - - if ( $u == 0 ) { # not by a logged-in user - $userPage = Title::makeTitle( NS_SPECIAL, 'Contributions' ); - $linkParams = 'target=' . urlencode( $ut ); - } else { - $userPage = Title::makeTitle( NS_USER, $ut ); - $linkParams = ''; + + function preprocessResults( &$dbo, &$res ) { + # Do a batch existence check on the user and talk pages + $linkBatch = new LinkBatch(); + while( $row = $dbo->fetchObject( $res ) ) { + $linkBatch->addObj( Title::makeTitleSafe( NS_USER, $row->user_text ) ); + $linkBatch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_text ) ); } - $ul = $skin->makeLinkObj( $userPage, htmlspecialchars( $ut ), $linkParams ); - - $d = $wgLang->timeanddate( $result->timestamp, true ); - - # Since there is no diff link, we need to give users a way to - # mark the article as patrolled if it isn't already - if ( $wgUseRCPatrol && !is_null ( $result->usepatrol ) && $result->usepatrol && - $result->patrolled == 0 && $wgUser->isLoggedIn() && - ( $wgUser->isAllowed('patrol') || !$wgOnlySysopsCanPatrol ) ) - $link = $skin->makeKnownLink( $result->title, '', "rcid={$result->rcid}" ); - else - $link = $skin->makeKnownLink( $result->title, '' ); - - $s = "{$d} {$link} ({$length}) . . {$ul}"; + $linkBatch->execute(); + # Seek to start + if( $dbo->numRows( $res ) > 0 ) + $dbo->dataSeek( $res, 0 ); + } - $s .= $skin->commentBlock( $result->comment ); + /** + * Format a row, providing the timestamp, links to the page/history, size, user links, and a comment + * + * @param $skin Skin to use + * @param $result Result row + * @return string + */ + function formatResult( $skin, $result ) { + global $wgLang, $wgContLang; + $dm = $wgContLang->getDirMark(); - return $s; + $title = Title::makeTitleSafe( $result->namespace, $result->title ); + $time = $wgLang->timeAndDate( $result->timestamp, true ); + $plink = $skin->makeKnownLinkObj( $title, '', $this->patrollable( $result ) ? 'rcid=' . $result->rcid : '' ); + $hist = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' ); + $length = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( htmlspecialchars( $result->length ) ) ); + $ulink = $skin->userLink( $result->user, $result->user_text ) . ' ' . $skin->userToolLinks( $result->user, $result->user_text ); + $comment = $skin->commentBlock( $result->comment ); + + return "{$time} {$dm}{$plink} ({$hist}) {$dm}[{$length}] {$dm}{$ulink} {$comment}"; + } + + /** + * Should a specific result row provide "patrollable" links? + * + * @param $result Result row + * @return bool + */ + function patrollable( $result ) { + global $wgUser, $wgUseRCPatrol; + return $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) && !$result->patrolled; } - + function feedItemDesc( $row ) { if( isset( $row->rev_id ) ) { $revision = Revision::newFromId( $row->rev_id ); if( $revision ) { - return '

    ' . htmlspecialchars( wfMsg( 'summary' ) ) . ': ' . $text . "

    \n
    \n
    " . + return '

    ' . htmlspecialchars( wfMsg( 'summary' ) ) . ': ' . + htmlspecialchars( $revision->getComment() ) . "

    \n
    \n
    " . nl2br( htmlspecialchars( $revision->getText() ) ) . "
    "; } } return parent::feedItemDesc( $row ); } + + /** + * Show a form for filtering namespace and username + * + * @return string + */ + function getPageHeader() { + global $wgScript; + $self = SpecialPage::getTitleFor( $this->getName() ); + $form = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + $form .= Xml::hidden( 'title', $self->getPrefixedDBkey() ); + # Namespace selector + $form .= '
    {$movearticle}:{$oldTitle}{$movearticle}{$oldTitle}
    {$newtitle}: - + +

    {$movereason}:

    +


    - - " . Xml::checkLabel( wfMsg( 'movetalk' ), 'wpMovetalk', 'wpMovetalk', $moveTalkChecked ) . "
    ' . Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) . '
      +
    '; + $form .= ''; + # Username filter + $form .= ''; + $form .= ''; + + $form .= '
    ' . Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '' . Xml::namespaceSelector( $this->namespace, 'all' ) . '
    ' . Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) . '' . Xml::input( 'username', 30, $this->username, array( 'id' => 'mw-np-username' ) ) . '
    ' . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '
    '; + $form .= Xml::hidden( 'offset', $this->offset ) . Xml::hidden( 'limit', $this->limit ) . ''; + return $form; + } + + /** + * Link parameters + * + * @return array + */ + function linkParameters() { + return( array( 'namespace' => $this->namespace, 'username' => $this->username ) ); + } + } /** * constructor */ -function wfSpecialNewpages($par, $specialPage) -{ - global $wgRequest; +function wfSpecialNewpages($par, $specialPage) { + global $wgRequest, $wgContLang; + list( $limit, $offset ) = wfCheckLimits(); - if( $par ) { + $namespace = NS_MAIN; + $username = ''; + + if ( $par ) { $bits = preg_split( '/\s*,\s*/', trim( $par ) ); foreach ( $bits as $bit ) { - if ( 'shownav' == $bit ) $shownavigation = 1; - if ( is_numeric( $bit ) ) { + if ( 'shownav' == $bit ) + $shownavigation = true; + if ( is_numeric( $bit ) ) $limit = $bit; - } - if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) { + $m = array(); + if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) $limit = intval($m[1]); - } - if ( preg_match( '/^offset=(\d+)$/', $bit, $m ) ) { + if ( preg_match( '/^offset=(\d+)$/', $bit, $m ) ) $offset = intval($m[1]); + if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) { + $ns = $wgContLang->getNsIndex( $m[1] ); + if( $ns !== false ) { + $namespace = $ns; + } } } + } else { + if( $ns = $wgRequest->getText( 'namespace', NS_MAIN ) ) + $namespace = $ns; + if( $un = $wgRequest->getText( 'username' ) ) + $username = $un; } - if(!isset($shownavigation)) { - $shownavigation=!$specialPage->including(); - } + + if ( ! isset( $shownavigation ) ) + $shownavigation = ! $specialPage->including(); - $npp = new NewPagesPage(); + $npp = new NewPagesPage( $namespace, $username ); - if( !$npp->doFeed( $wgRequest->getVal( 'feed' ) ) ) { + if ( ! $npp->doFeed( $wgRequest->getVal( 'feed' ), $limit ) ) $npp->doQuery( $offset, $limit, $shownavigation ); - } -} - -?> +} \ No newline at end of file diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialPage.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialPage.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialPage.php 2005-08-14 23:04:06.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialPage.php 2007-08-22 12:25:07.000000000 -0400 @@ -1,95 +1,33 @@ new SpecialPage ( 'DoubleRedirects' ), - 'BrokenRedirects' => new SpecialPage ( 'BrokenRedirects' ), - 'Disambiguations' => new SpecialPage ( 'Disambiguations' ), - - 'Userlogin' => new SpecialPage( 'Userlogin' ), - 'Userlogout' => new UnlistedSpecialPage( 'Userlogout' ), - 'Preferences' => new SpecialPage( 'Preferences' ), - 'Watchlist' => new SpecialPage( 'Watchlist' ), - - 'Recentchanges' => new IncludableSpecialPage( 'Recentchanges' ), - 'Upload' => new SpecialPage( 'Upload' ), - 'Imagelist' => new SpecialPage( 'Imagelist' ), - 'Newimages' => new SpecialPage( 'Newimages' ), - 'Listusers' => new SpecialPage( 'Listusers' ), - 'Statistics' => new SpecialPage( 'Statistics' ), - 'Random' => new SpecialPage( 'Randompage' ), - 'Lonelypages' => new SpecialPage( 'Lonelypages' ), - 'Uncategorizedpages'=> new SpecialPage( 'Uncategorizedpages' ), - 'Uncategorizedcategories'=> new SpecialPage( 'Uncategorizedcategories' ), - 'Unusedcategories' => new SpecialPage( 'Unusedcategories' ), - 'Unusedimages' => new SpecialPage( 'Unusedimages' ), - 'Wantedpages' => new SpecialPage( 'Wantedpages' ), - 'Mostlinked' => new SpecialPage( 'Mostlinked' ), - 'Shortpages' => new SpecialPage( 'Shortpages' ), - 'Longpages' => new SpecialPage( 'Longpages' ), - 'Newpages' => new IncludableSpecialPage( 'Newpages' ), - 'Ancientpages' => new SpecialPage( 'Ancientpages' ), - 'Deadendpages' => new SpecialPage( 'Deadendpages' ), - 'Allpages' => new IncludableSpecialPage( 'Allpages' ), - 'Ipblocklist' => new SpecialPage( 'Ipblocklist' ), - 'Specialpages' => new UnlistedSpecialPage( 'Specialpages' ), - 'Contributions' => new UnlistedSpecialPage( 'Contributions' ), - 'Emailuser' => new UnlistedSpecialPage( 'Emailuser' ), - 'Whatlinkshere' => new UnlistedSpecialPage( 'Whatlinkshere' ), - 'Recentchangeslinked' => new UnlistedSpecialPage( 'Recentchangeslinked' ), - 'Movepage' => new UnlistedSpecialPage( 'Movepage' ), - 'Blockme' => new UnlistedSpecialPage( 'Blockme' ), - 'Booksources' => new SpecialPage( 'Booksources' ), - 'Categories' => new SpecialPage( 'Categories' ), - 'Export' => new SpecialPage( 'Export' ), - 'Version' => new SpecialPage( 'Version' ), - 'Allmessages' => new SpecialPage( 'Allmessages' ), - 'Log' => new SpecialPage( 'Log' ), - 'Blockip' => new SpecialPage( 'Blockip', 'block' ), - 'Undelete' => new SpecialPage( 'Undelete', 'delete' ), - "Import" => new SpecialPage( "Import", 'import' ), - 'Lockdb' => new SpecialPage( 'Lockdb', 'siteadmin' ), - 'Unlockdb' => new SpecialPage( 'Unlockdb', 'siteadmin' ), - 'Userrights' => new SpecialPage( 'Userrights', 'userrights' ), -); - -if ( $wgUseValidation ) - $wgSpecialPages['Validate'] = new SpecialPage( 'Validate' ); - -if( !$wgDisableCounters ) { - $wgSpecialPages['Popularpages'] = new SpecialPage( 'Popularpages' ); -} - -if( !$wgDisableInternalSearch ) { - $wgSpecialPages['Search'] = new UnlistedSpecialPage( 'Search' ); -} - -if( $wgEmailAuthentication ) { - $wgSpecialPages['Confirmemail'] = new UnlistedSpecialPage( 'Confirmemail' ); -} /** * Parent special page class, also static functions for handling the special - * page list - * @package MediaWiki + * page list. + * @addtogroup SpecialPage */ class SpecialPage { @@ -97,11 +35,15 @@ * @access private */ /** - * The name of the class, used in the URL. + * The canonical name of this special page * Also used for the default

    heading, @see getDescription() */ var $mName; /** + * The local name of this special page + */ + var $mLocalName; + /** * Minimum user level required to access this page, or "" for anyone. * Also used to categorise the pages in Special:Specialpages */ @@ -126,30 +68,236 @@ * Whether the special page can be included in an article */ var $mIncludable; + /** + * Query parameters that can be passed through redirects + */ + var $mAllowedRedirectParams = array(); + + static public $mList = array( + 'DoubleRedirects' => array( 'SpecialPage', 'DoubleRedirects' ), + 'BrokenRedirects' => array( 'SpecialPage', 'BrokenRedirects' ), + 'Disambiguations' => array( 'SpecialPage', 'Disambiguations' ), + + 'Userlogin' => array( 'SpecialPage', 'Userlogin' ), + 'Userlogout' => array( 'UnlistedSpecialPage', 'Userlogout' ), + 'Preferences' => array( 'SpecialPage', 'Preferences' ), + 'Watchlist' => array( 'SpecialPage', 'Watchlist' ), + + 'Recentchanges' => array( 'IncludableSpecialPage', 'Recentchanges' ), + 'Upload' => array( 'SpecialPage', 'Upload' ), + 'Imagelist' => array( 'SpecialPage', 'Imagelist' ), + 'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ), + 'Listusers' => array( 'SpecialPage', 'Listusers' ), + 'Statistics' => array( 'SpecialPage', 'Statistics' ), + 'Randompage' => array( 'SpecialPage', 'Randompage' ), + 'Lonelypages' => array( 'SpecialPage', 'Lonelypages' ), + 'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ), + 'Uncategorizedcategories' => array( 'SpecialPage', 'Uncategorizedcategories' ), + 'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ), + 'Uncategorizedtemplates' => array( 'SpecialPage', 'Uncategorizedtemplates' ), + 'Unusedcategories' => array( 'SpecialPage', 'Unusedcategories' ), + 'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ), + 'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ), + 'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ), + 'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ), + 'Mostlinkedcategories' => array( 'SpecialPage', 'Mostlinkedcategories' ), + 'Mostlinkedtemplates' => array( 'SpecialPage', 'Mostlinkedtemplates' ), + 'Mostcategories' => array( 'SpecialPage', 'Mostcategories' ), + 'Mostimages' => array( 'SpecialPage', 'Mostimages' ), + 'Mostrevisions' => array( 'SpecialPage', 'Mostrevisions' ), + 'Fewestrevisions' => array( 'SpecialPage', 'Fewestrevisions' ), + 'Shortpages' => array( 'SpecialPage', 'Shortpages' ), + 'Longpages' => array( 'SpecialPage', 'Longpages' ), + 'Newpages' => array( 'IncludableSpecialPage', 'Newpages' ), + 'Ancientpages' => array( 'SpecialPage', 'Ancientpages' ), + 'Deadendpages' => array( 'SpecialPage', 'Deadendpages' ), + 'Protectedpages' => array( 'SpecialPage', 'Protectedpages' ), + 'Allpages' => array( 'IncludableSpecialPage', 'Allpages' ), + 'Prefixindex' => array( 'IncludableSpecialPage', 'Prefixindex' ) , + 'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ), + 'Specialpages' => array( 'UnlistedSpecialPage', 'Specialpages' ), + 'Contributions' => array( 'SpecialPage', 'Contributions' ), + 'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ), + 'Whatlinkshere' => array( 'UnlistedSpecialPage', 'Whatlinkshere' ), + 'Recentchangeslinked' => array( 'UnlistedSpecialPage', 'Recentchangeslinked' ), + 'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ), + 'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ), + 'Resetpass' => array( 'UnlistedSpecialPage', 'Resetpass' ), + 'Booksources' => 'SpecialBookSources', + 'Categories' => array( 'SpecialPage', 'Categories' ), + 'Export' => array( 'SpecialPage', 'Export' ), + 'Version' => array( 'SpecialPage', 'Version' ), + 'Allmessages' => array( 'SpecialPage', 'Allmessages' ), + 'Log' => array( 'SpecialPage', 'Log' ), + 'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ), + 'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ), + 'Import' => array( 'SpecialPage', "Import", 'import' ), + 'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ), + 'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ), + 'Userrights' => array( 'SpecialPage', 'Userrights', 'userrights' ), + 'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ), + 'Unwatchedpages' => array( 'SpecialPage', 'Unwatchedpages', 'unwatchedpages' ), + 'Listredirects' => array( 'SpecialPage', 'Listredirects' ), + 'Revisiondelete' => array( 'SpecialPage', 'Revisiondelete', 'deleterevision' ), + 'Unusedtemplates' => array( 'SpecialPage', 'Unusedtemplates' ), + 'Randomredirect' => array( 'SpecialPage', 'Randomredirect' ), + 'Withoutinterwiki' => array( 'SpecialPage', 'Withoutinterwiki' ), + + 'Mypage' => array( 'SpecialMypage' ), + 'Mytalk' => array( 'SpecialMytalk' ), + 'Mycontributions' => array( 'SpecialMycontributions' ), + 'Listadmins' => array( 'SpecialRedirectToSpecial', 'Listadmins', 'Listusers', 'sysop' ), + ); + static public $mAliases; + static public $mListInitialised = false; /**#@-*/ + /** + * Initialise the special page list + * This must be called before accessing SpecialPage::$mList + */ + static function initList() { + global $wgSpecialPages; + global $wgDisableCounters, $wgDisableInternalSearch, $wgEmailAuthentication; + + if ( self::$mListInitialised ) { + return; + } + wfProfileIn( __METHOD__ ); + + # Better to set this now, to avoid infinite recursion in carelessly written hooks + self::$mListInitialised = true; + + if( !$wgDisableCounters ) { + self::$mList['Popularpages'] = array( 'SpecialPage', 'Popularpages' ); + } + + if( !$wgDisableInternalSearch ) { + self::$mList['Search'] = array( 'SpecialPage', 'Search' ); + } + + if( $wgEmailAuthentication ) { + self::$mList['Confirmemail'] = 'EmailConfirmation'; + } + + # Add extension special pages + self::$mList = array_merge( self::$mList, $wgSpecialPages ); + + # Run hooks + # This hook can be used to remove undesired built-in special pages + wfRunHooks( 'SpecialPage_initList', array( &self::$mList ) ); + wfProfileOut( __METHOD__ ); + } + + static function initAliasList() { + if ( !is_null( self::$mAliases ) ) { + return; + } + + global $wgContLang; + $aliases = $wgContLang->getSpecialPageAliases(); + $missingPages = self::$mList; + self::$mAliases = array(); + foreach ( $aliases as $realName => $aliasList ) { + foreach ( $aliasList as $alias ) { + self::$mAliases[$wgContLang->caseFold( $alias )] = $realName; + } + unset( $missingPages[$realName] ); + } + foreach ( $missingPages as $name => $stuff ) { + self::$mAliases[$wgContLang->caseFold( $name )] = $name; + } + } + + /** + * Given a special page alias, return the special page name. + * Returns false if there is no such alias. + */ + static function resolveAlias( $alias ) { + global $wgContLang; + + if ( !self::$mListInitialised ) self::initList(); + if ( is_null( self::$mAliases ) ) self::initAliasList(); + $caseFoldedAlias = $wgContLang->caseFold( $alias ); + if ( isset( self::$mAliases[$caseFoldedAlias] ) ) { + return self::$mAliases[$caseFoldedAlias]; + } else { + return false; + } + } + + /** + * Given a special page name with a possible subpage, return an array + * where the first element is the special page name and the second is the + * subpage. + */ + static function resolveAliasWithSubpage( $alias ) { + $bits = explode( '/', $alias, 2 ); + $name = self::resolveAlias( $bits[0] ); + if( !isset( $bits[1] ) ) { // bug 2087 + $par = NULL; + } else { + $par = $bits[1]; + } + return array( $name, $par ); + } /** - * Add a page to the list of valid special pages - * $obj->execute() must send HTML to $wgOut then return - * Use this for a special page extension + * Add a page to the list of valid special pages. This used to be the preferred + * method for adding special pages in extensions. It's now suggested that you add + * an associative record to $wgSpecialPages. This avoids autoloading SpecialPage. + * + * @param mixed $page Must either be an array specifying a class name and + * constructor parameters, or an object. The object, + * when constructed, must have an execute() method which + * sends HTML to $wgOut. * @static */ - function addPage( &$obj ) { - global $wgSpecialPages; - $wgSpecialPages[$obj->mName] = $obj; + static function addPage( &$page ) { + if ( !self::$mListInitialised ) { + self::initList(); + } + self::$mList[$page->mName] = $page; } /** * Remove a special page from the list - * Occasionally used to disable expensive or dangerous special pages + * Formerly used to disable expensive or dangerous special pages. The + * preferred method is now to add a SpecialPage_initList hook. + * * @static */ - function removePage( $name ) { - global $wgSpecialPages; - unset( $wgSpecialPages[$name] ); + static function removePage( $name ) { + if ( !self::$mListInitialised ) { + self::initList(); + } + unset( self::$mList[$name] ); + } + + /** + * Check if a given name exist as a special page or as a special page alias + * @param $name string: name of a special page + * @return boolean: true if a special page exists with this name + */ + static function exists( $name ) { + global $wgContLang; + if ( !self::$mListInitialised ) { + self::initList(); + } + if( !self::$mAliases ) { + self::initAliasList(); + } + + # Remove special pages inline parameters: + $bits = explode( '/', $name ); + $name = $wgContLang->caseFold($bits[0]); + + return + array_key_exists( $name, self::$mList ) + or array_key_exists( $name, self::$mAliases ) + ; } /** @@ -157,54 +305,76 @@ * @static * @param string $name */ - function getPage( $name ) { - global $wgSpecialPages; - if ( array_key_exists( $name, $wgSpecialPages ) ) { - return $wgSpecialPages[$name]; + static function getPage( $name ) { + if ( !self::$mListInitialised ) { + self::initList(); + } + if ( array_key_exists( $name, self::$mList ) ) { + $rec = self::$mList[$name]; + if ( is_string( $rec ) ) { + $className = $rec; + self::$mList[$name] = new $className; + } elseif ( is_array( $rec ) ) { + $className = array_shift( $rec ); + self::$mList[$name] = wfCreateObject( $className, $rec ); + } + return self::$mList[$name]; + } else { + return NULL; + } + } + + /** + * Get a special page with a given localised name, or NULL if there + * is no such special page. + */ + static function getPageByAlias( $alias ) { + $realName = self::resolveAlias( $alias ); + if ( $realName ) { + return self::getPage( $realName ); } else { return NULL; } } /** + * Return categorised listable special pages for all users * @static - * @param string $name - * @return mixed Title object if the redirect exists, otherwise NULL */ - function getRedirect( $name ) { - global $wgUser; - switch ( $name ) { - case 'Mypage': - return Title::makeTitle( NS_USER, $wgUser->getName() ); - case 'Mytalk': - return Title::makeTitle( NS_USER_TALK, $wgUser->getName() ); - case 'Mycontributions': - return Title::makeTitle( NS_SPECIAL, 'Contributions/' . $wgUser->getName() ); - case 'Listadmins': - return Title::makeTitle( NS_SPECIAL, 'Listusers/sysop' ); # @bug 2832 - case 'Randompage': - return Title::makeTitle( NS_SPECIAL, 'Random' ); - default: - return NULL; + static function getRegularPages() { + if ( !self::$mListInitialised ) { + self::initList(); + } + $pages = array(); + + foreach ( self::$mList as $name => $rec ) { + $page = self::getPage( $name ); + if ( $page->isListed() && $page->getRestriction() == '' ) { + $pages[$name] = $page; + } } + return $pages; } /** - * Return categorised listable special pages - * Returns a 2d array where the first index is the restriction name + * Return categorised listable special pages which are available + * for the current user, but not for everyone * @static */ - function getPages() { - global $wgSpecialPages; - $pages = array( - '' => array(), - 'sysop' => array(), - 'developer' => array() - ); + static function getRestrictedPages() { + global $wgUser; + if ( !self::$mListInitialised ) { + self::initList(); + } + $pages = array(); - foreach ( $wgSpecialPages as $name => $page ) { + foreach ( self::$mList as $name => $rec ) { + $page = self::getPage( $name ); if ( $page->isListed() ) { - $pages[$page->getRestriction()][$page->getName()] =& $wgSpecialPages[$name]; + $restriction = $page->getRestriction(); + if ( $restriction != '' && $wgUser->isAllowed( $restriction ) ) { + $pages[$name] = $page; + } } } return $pages; @@ -221,51 +391,75 @@ * @param $title a title object * @param $including output is being captured for use in {{special:whatever}} */ - function executePath( &$title, $including = false ) { - global $wgSpecialPages, $wgOut, $wgTitle; + static function executePath( &$title, $including = false ) { + global $wgOut, $wgTitle, $wgRequest; + wfProfileIn( __METHOD__ ); - $bits = split( "/", $title->getDBkey(), 2 ); + # FIXME: redirects broken due to this call + $bits = explode( '/', $title->getDBkey(), 2 ); $name = $bits[0]; if( !isset( $bits[1] ) ) { // bug 2087 $par = NULL; } else { $par = $bits[1]; } + $page = SpecialPage::getPageByAlias( $name ); - $page = SpecialPage::getPage( $name ); - if ( is_null( $page ) ) { - if ( $including ) { - return false; - } else { - $redir = SpecialPage::getRedirect( $name ); - if ( isset( $redir ) ) { - if ( isset( $par ) ) - $wgOut->redirect( $redir->getFullURL() . '/' . $par ); - else - $wgOut->redirect( $redir->getFullURL() ); - $retVal = $redir; - } else { - $wgOut->setArticleRelated( false ); - $wgOut->setRobotpolicy( "noindex,follow" ); - $wgOut->errorpage( "nosuchspecialpage", "nospecialpagetext" ); - $retVal = false; - } + # Nonexistent? + if ( !$page ) { + if ( !$including ) { + $wgOut->setArticleRelated( false ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setStatusCode( 404 ); + $wgOut->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' ); } - } else { - if ( $including && !$page->includable() ) { - return false; - } - if($par !== NULL) { - $wgTitle = Title::makeTitle( NS_SPECIAL, $name ); - } else { - $wgTitle = $title; + wfProfileOut( __METHOD__ ); + return false; + } + + # Check for redirect + if ( !$including ) { + $redirect = $page->getRedirect( $par ); + if ( $redirect ) { + $query = $page->getRedirectQuery(); + $url = $redirect->getFullUrl( $query ); + $wgOut->redirect( $url ); + wfProfileOut( __METHOD__ ); + return $redirect; } - $page->including( $including ); + } + + # Redirect to canonical alias for GET commands + # Not for POST, we'd lose the post data, so it's best to just distribute + # the request. Such POST requests are possible for old extensions that + # generate self-links without being aware that their default name has + # changed. + if ( !$including && $name != $page->getLocalName() && !$wgRequest->wasPosted() ) { + $query = $_GET; + unset( $query['title'] ); + $query = wfArrayToCGI( $query ); + $title = $page->getTitle( $par ); + $url = $title->getFullUrl( $query ); + $wgOut->redirect( $url ); + wfProfileOut( __METHOD__ ); + return $redirect; + } - $page->execute( $par ); - $retVal = true; + if ( $including && !$page->includable() ) { + wfProfileOut( __METHOD__ ); + return false; + } elseif ( !$including ) { + $wgTitle = $page->getTitle(); } - return $retVal; + $page->including( $including ); + + // Execute special page + $profName = 'Special:' . $page->getName(); + wfProfileIn( $profName ); + $page->execute( $par ); + wfProfileOut( $profName ); + wfProfileOut( __METHOD__ ); + return true; } /** @@ -274,7 +468,7 @@ * a redirect. * @static */ - function capturePath( &$title ) { + static function capturePath( &$title ) { global $wgOut, $wgTitle; $oldTitle = $wgTitle; @@ -291,6 +485,58 @@ } /** + * Get the local name for a specified canonical name + */ + static function getLocalNameFor( $name, $subpage = false ) { + global $wgContLang; + $aliases = $wgContLang->getSpecialPageAliases(); + if ( isset( $aliases[$name][0] ) ) { + $name = $aliases[$name][0]; + } + if ( $subpage !== false && !is_null( $subpage ) ) { + $name = "$name/$subpage"; + } + return $name; + } + + /** + * Get a localised Title object for a specified special page name + */ + static function getTitleFor( $name, $subpage = false ) { + $name = self::getLocalNameFor( $name, $subpage ); + if ( $name ) { + return Title::makeTitle( NS_SPECIAL, $name ); + } else { + throw new MWException( "Invalid special page name \"$name\"" ); + } + } + + /** + * Get a localised Title object for a page name with a possibly unvalidated subpage + */ + static function getSafeTitleFor( $name, $subpage = false ) { + $name = self::getLocalNameFor( $name, $subpage ); + if ( $name ) { + return Title::makeTitleSafe( NS_SPECIAL, $name ); + } else { + return null; + } + } + + /** + * Get a title for a given alias + * @return Title or null if there is no such alias + */ + static function getTitleForAlias( $alias ) { + $name = self::resolveAlias( $alias ); + if ( $name ) { + return self::getTitleFor( $name ); + } else { + return null; + } + } + + /** * Default constructor for special pages * Derivative classes should call this from their constructor * Note that if the user does not have the required level, an error message will @@ -301,7 +547,7 @@ * and displayRestrictionError() * * @param string $name Name of the special page, as seen in links and URLs - * @param string $restriction Minimum user level required, e.g. "sysop" or "developer". + * @param string $restriction User right required, e.g. "block" or "delete" * @param boolean $listed Whether the page is listed in Special:Specialpages * @param string $function Function called by execute(). By default it is constructed from $name * @param string $file File which is included by execute(). It is also constructed from $name by default @@ -317,34 +563,51 @@ $this->mFunction = $function; } if ( $file === 'default' ) { - $this->mFile = "Special{$name}.php"; + $this->mFile = dirname(__FILE__) . "/Special{$name}.php"; } else { $this->mFile = $file; } } - # Accessor functions, see the descriptions of the associated variables above + /**#@+ + * Accessor + * + * @deprecated + */ function getName() { return $this->mName; } function getRestriction() { return $this->mRestriction; } - function isListed() { return $this->mListed; } function getFile() { return $this->mFile; } - function including( $x = NULL ) { return wfSetVar( $this->mIncluding, $x ); } + function isListed() { return $this->mListed; } + /**#@-*/ + + /**#@+ + * Accessor and mutator + */ + function name( $x = NULL ) { return wfSetVar( $this->mName, $x ); } + function restrictions( $x = NULL) { return wfSetVar( $this->mRestrictions, $x ); } + function listed( $x = NULL) { return wfSetVar( $this->mListed, $x ); } + function func( $x = NULL) { return wfSetVar( $this->mFunction, $x ); } + function file( $x = NULL) { return wfSetVar( $this->mFile, $x ); } function includable( $x = NULL ) { return wfSetVar( $this->mIncludable, $x ); } + function including( $x = NULL ) { return wfSetVar( $this->mIncluding, $x ); } + /**#@-*/ + + /** + * Get the localised name of the special page + */ + function getLocalName() { + if ( !isset( $this->mLocalName ) ) { + $this->mLocalName = self::getLocalNameFor( $this->mName ); + } + return $this->mLocalName; + } /** * Checks if the given user (identified by an object) can execute this * special page (as defined by $mRestriction) */ function userCanExecute( &$user ) { - if ( $this->mRestriction == "" ) { - return true; - } else { - if ( in_array( $this->mRestriction, $user->getRights() ) ) { - return true; - } else { - return false; - } - } + return $user->isAllowed( $this->mRestriction ); } /** @@ -361,30 +624,50 @@ function setHeaders() { global $wgOut; $wgOut->setArticleRelated( false ); - $wgOut->setRobotPolicy( "noindex,follow" ); + $wgOut->setRobotPolicy( "noindex,nofollow" ); $wgOut->setPageTitle( $this->getDescription() ); } /** * Default execute method * Checks user permissions, calls the function given in mFunction + * + * This may be overridden by subclasses. */ function execute( $par ) { - global $wgUser, $wgOut, $wgTitle; + global $wgUser; $this->setHeaders(); if ( $this->userCanExecute( $wgUser ) ) { - if ( $this->mFile ) { + $func = $this->mFunction; + // only load file if the function does not exist + if(!function_exists($func) and $this->mFile) { require_once( $this->mFile ); } - $func = $this->mFunction; + # FIXME: these hooks are broken for extensions and anything else that subclasses SpecialPage. + if ( wfRunHooks( 'SpecialPageExecuteBeforeHeader', array( &$this, &$par, &$func ) ) ) + $this->outputHeader(); + if ( ! wfRunHooks( 'SpecialPageExecuteBeforePage', array( &$this, &$par, &$func ) ) ) + return; $func( $par, $this ); + if ( ! wfRunHooks( 'SpecialPageExecuteAfterPage', array( &$this, &$par, &$func ) ) ) + return; } else { $this->displayRestrictionError(); } } + function outputHeader() { + global $wgOut, $wgContLang; + + $msg = $wgContLang->lc( $this->name() ) . '-summary'; + $out = wfMsg( $msg ); + if ( ! wfEmptyMsg( $msg, $out ) and $out !== '' and ! $this->including() ) + $wgOut->addWikiText( $out ); + + } + # Returns the name that goes in the

    in the special page itself, and also the name that # will be listed in Special:Specialpages # @@ -397,8 +680,8 @@ /** * Get a self-referential title object */ - function getTitle() { - return Title::makeTitle( NS_SPECIAL, $this->mName ); + function getTitle( $subpage = false) { + return self::getTitleFor( $this->mName, $subpage ); } /** @@ -408,11 +691,35 @@ return wfSetVar( $this->mListed, $listed ); } + /** + * If the special page is a redirect, then get the Title object it redirects to. + * False otherwise. + */ + function getRedirect( $subpage ) { + return false; + } + + /** + * Return part of the request string for a special redirect page + * This allows passing, e.g. action=history to Special:Mypage, etc. + * + * @return string + */ + function getRedirectQuery() { + global $wgRequest; + $params = array(); + foreach( $this->mAllowedRedirectParams as $arg ) { + if( $val = $wgRequest->getVal( $arg, false ) ) + $params[] = $arg . '=' . $val; + } + + return count( $params ) ? implode( '&', $params ) : false; + } } /** * Shortcut to construct a special page which is unlisted by default - * @package MediaWiki + * @addtogroup SpecialPage */ class UnlistedSpecialPage extends SpecialPage { @@ -423,7 +730,7 @@ /** * Shortcut to construct an includable special page - * @package MediaWiki + * @addtogroup SpecialPage */ class IncludableSpecialPage extends SpecialPage { @@ -431,4 +738,90 @@ SpecialPage::SpecialPage( $name, $restriction, $listed, $function, $file, true ); } } -?> + +/** + * Shortcut to construct a special page alias. + * @addtogroup SpecialPage + */ +class SpecialRedirectToSpecial extends UnlistedSpecialPage { + var $redirName, $redirSubpage; + + function __construct( $name, $redirName, $redirSubpage = false, $redirectParams = array() ) { + parent::__construct( $name ); + $this->redirName = $redirName; + $this->redirSubpage = $redirSubpage; + $this->mAllowedRedirectParams = $redirectParams; + } + + function getRedirect( $subpage ) { + if ( $this->redirSubpage === false ) { + return SpecialPage::getTitleFor( $this->redirName, $subpage ); + } else { + return SpecialPage::getTitleFor( $this->redirName, $this->redirSubpage ); + } + } +} + +/** SpecialMypage, SpecialMytalk and SpecialMycontributions special pages + * are used to get user independant links pointing to the user page, talk + * page and list of contributions. + * This can let us cache a single copy of any generated content for all + * users. + */ + +/** + * Shortcut to construct a special page pointing to current user user's page. + * @addtogroup SpecialPage + */ +class SpecialMypage extends UnlistedSpecialPage { + function __construct() { + parent::__construct( 'Mypage' ); + $this->mAllowedRedirectParams = array( 'action' ); + } + + function getRedirect( $subpage ) { + global $wgUser; + if ( strval( $subpage ) !== '' ) { + return Title::makeTitle( NS_USER, $wgUser->getName() . '/' . $subpage ); + } else { + return Title::makeTitle( NS_USER, $wgUser->getName() ); + } + } +} + +/** + * Shortcut to construct a special page pointing to current user talk page. + * @addtogroup SpecialPage + */ +class SpecialMytalk extends UnlistedSpecialPage { + function __construct() { + parent::__construct( 'Mytalk' ); + $this->mAllowedRedirectParams = array( 'action' ); + } + + function getRedirect( $subpage ) { + global $wgUser; + if ( strval( $subpage ) !== '' ) { + return Title::makeTitle( NS_USER_TALK, $wgUser->getName() . '/' . $subpage ); + } else { + return Title::makeTitle( NS_USER_TALK, $wgUser->getName() ); + } + } +} + +/** + * Shortcut to construct a special page pointing to current user contributions. + * @addtogroup SpecialPage + */ +class SpecialMycontributions extends UnlistedSpecialPage { + function __construct() { + parent::__construct( 'Mycontributions' ); + } + + function getRedirect( $subpage ) { + global $wgUser; + return SpecialPage::getTitleFor( 'Contributions', $wgUser->getName() ); + } +} + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialPopularpages.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialPopularpages.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialPopularpages.php 2005-06-05 03:55:54.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialPopularpages.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,19 +1,12 @@ tableName( 'page' ); - return + $query = "SELECT 'Popularpages' as type, page_namespace as namespace, page_title as title, page_counter as value - FROM $page - WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0"; + FROM $page "; + $where = + "WHERE page_is_redirect=0 AND page_namespace"; + + global $wgContentNamespaces; + if( empty( $wgContentNamespaces ) ) { + $where .= '='.NS_MAIN; + } else if( count( $wgContentNamespaces ) > 1 ) { + $where .= ' in (' . implode( ', ', $wgContentNamespaces ) . ')'; + } else { + $where .= '='.$wgContentNamespaces[0]; + } + + return $query . $where; } function formatResult( $skin, $result ) { global $wgLang, $wgContLang; $title = Title::makeTitle( $result->namespace, $result->title ); - $link = $skin->makeKnownLinkObj( $title, $wgContLang->convert( $title->getPrefixedText() ) ); - $nv = wfMsg( "nviews", $wgLang->formatNum( $result->value ) ); - return "{$link} ({$nv})"; + $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) ); + $nv = wfMsgExt( 'nviews', array( 'parsemag', 'escape'), + $wgLang->formatNum( $result->value ) ); + return wfSpecialList($link, $nv); } } @@ -53,11 +59,11 @@ * Constructor */ function wfSpecialPopularpages() { - list( $limit, $offset ) = wfCheckLimits(); - - $ppp = new PopularPagesPage(); - - return $ppp->doQuery( $offset, $limit ); + list( $limit, $offset ) = wfCheckLimits(); + + $ppp = new PopularPagesPage(); + + return $ppp->doQuery( $offset, $limit ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialPreferences.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialPreferences.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialPreferences.php 2005-10-21 16:47:39.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialPreferences.php 2007-08-13 16:03:18.000000000 -0400 @@ -1,13 +1,9 @@ mQuickbar = $request->getVal( 'wpQuickbar' ); $this->mOldpass = $request->getVal( 'wpOldpass' ); @@ -51,12 +46,13 @@ $this->mDate = $request->getVal( 'wpDate' ); $this->mUserEmail = $request->getVal( 'wpUserEmail' ); $this->mRealName = $wgAllowRealName ? $request->getVal( 'wpRealName' ) : ''; - $this->mEmailFlag = $request->getCheck( 'wpEmailFlag' ) ? 1 : 0; + $this->mEmailFlag = $request->getCheck( 'wpEmailFlag' ) ? 0 : 1; $this->mNick = $request->getVal( 'wpNick' ); $this->mUserLanguage = $request->getVal( 'wpUserLanguage' ); $this->mUserVariant = $request->getVal( 'wpUserVariant' ); $this->mSearch = $request->getVal( 'wpSearch' ); $this->mRecent = $request->getVal( 'wpRecent' ); + $this->mRecentDays = $request->getVal( 'wpRecentDays' ); $this->mHourDiff = $request->getVal( 'wpHourDiff' ); $this->mSearchLines = $request->getVal( 'wpSearchLines' ); $this->mSearchChars = $request->getVal( 'wpSearchChars' ); @@ -67,6 +63,8 @@ $this->mReset = $request->getCheck( 'wpReset' ); $this->mPosted = $request->wasPosted(); $this->mSuccess = $request->getCheck( 'success' ); + $this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' ); + $this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' ); $this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) && $this->mPosted && @@ -75,7 +73,7 @@ # User toggles (the big ugly unsorted list of checkboxes) $this->mToggles = array(); if ( $this->mPosted ) { - $togs = $wgLang->getUserToggles(); + $togs = User::getToggles(); foreach ( $togs as $tname ) { $this->mToggles[$tname] = $request->getCheck( "wpOp$tname" ) ? 1 : 0; } @@ -99,13 +97,15 @@ if ( !preg_match( '/^[a-z\-]*$/', $this->mUserLanguage ) ) { $this->mUserLanguage = 'nolanguage'; } + + wfRunHooks( "InitPreferencesForm", array( $this, $request ) ); } function execute() { global $wgUser, $wgOut; if ( $wgUser->isAnon() ) { - $wgOut->errorpage( 'prefsnologin', 'prefsnologintext' ); + $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext' ); return; } if ( wfReadOnly() ) { @@ -135,6 +135,16 @@ /** * @access private */ + function validateFloat( &$val, $min, $max=0x7fffffff ) { + $val = floatval( $val ); + $val = min( $val, $max ); + $val = max( $val, $min ); + return( $val ); + } + + /** + * @access private + */ function validateIntOrNull( &$val, $min=0, $max=0x7fffffff ) { $val = trim($val); if($val === '') { @@ -147,16 +157,21 @@ /** * @access private */ - function validateDate( &$val, $min = 0, $max=0x7fffffff ) { - if ( ( sprintf('%d', $val) === $val && $val >= $min && $val <= $max ) || $val == 'ISO 8601' ) + function validateDate( $val ) { + global $wgLang, $wgContLang; + if ( $val !== false && ( + in_array( $val, (array)$wgLang->getDatePreferences() ) || + in_array( $val, (array)$wgContLang->getDatePreferences() ) ) ) + { return $val; - else - return 0; + } else { + return $wgLang->getDefaultDateFormat(); + } } /** * Used to validate the user inputed timezone before saving it as - * 'timeciorrection', will return '00:00' if fed bogus data. + * 'timecorrection', will return '00:00' if fed bogus data. * Note: It's not a 100% correct implementation timezone-wise, it will * accept stuff like '14:30', * @access private @@ -190,32 +205,34 @@ * @access private */ function savePreferences() { - global $wgUser, $wgLang, $wgOut; + global $wgUser, $wgOut, $wgParser; global $wgEnableUserEmail, $wgEnableEmail; - global $wgEmailAuthentication, $wgMinimalPasswordLength; + global $wgEmailAuthentication; global $wgAuth; - if ( '' != $this->mNewpass ) { + if ( '' != $this->mNewpass && $wgAuth->allowPasswordChange() ) { if ( $this->mNewpass != $this->mRetypePass ) { + wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'badretype' ) ); $this->mainPrefsForm( 'error', wfMsg( 'badretype' ) ); return; } - if ( strlen( $this->mNewpass ) < $wgMinimalPasswordLength ) { - $this->mainPrefsForm( 'error', wfMsg( 'passwordtooshort', $wgMinimalPasswordLength ) ); - return; - } - if (!$wgUser->checkPassword( $this->mOldpass )) { + wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'wrongpassword' ) ); $this->mainPrefsForm( 'error', wfMsg( 'wrongpassword' ) ); return; } - if (!$wgAuth->setPassword( $wgUser, $this->mNewpass )) { - $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) ); + + try { + $wgUser->setPassword( $this->mNewpass ); + wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'success' ) ); + $this->mNewpass = $this->mOldpass = $this->mRetypePass = ''; + } catch( PasswordError $e ) { + wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'error' ) ); + $this->mainPrefsForm( 'error', $e->getMessage() ); return; } - $wgUser->setPassword( $this->mNewpass ); } $wgUser->setRealName( $this->mRealName ); @@ -225,6 +242,25 @@ $needRedirect = false; } + # Validate the signature and clean it up as needed + global $wgMaxSigChars; + if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) { + global $wgLang; + $this->mainPrefsForm( 'error', + wfMsg( 'badsiglength', $wgLang->formatNum( $wgMaxSigChars ) ) ); + return; + } elseif( $this->mToggles['fancysig'] ) { + if( Parser::validateSig( $this->mNick ) !== false ) { + $this->mNick = $wgParser->cleanSig( $this->mNick ); + } else { + $this->mainPrefsForm( 'error', wfMsg( 'badsig' ) ); + return; + } + } else { + // When no fancy sig used, make sure ~{3,5} get removed. + $this->mNick = $wgParser->cleanSigInSig( $this->mNick ); + } + $wgUser->setOption( 'language', $this->mUserLanguage ); $wgUser->setOption( 'variant', $this->mUserVariant ); $wgUser->setOption( 'nickname', $this->mNick ); @@ -234,11 +270,13 @@ if( $wgUseTeX ) { $wgUser->setOption( 'math', $this->mMath ); } - $wgUser->setOption( 'date', $this->validateDate( $this->mDate, 0, 20 ) ); + $wgUser->setOption( 'date', $this->validateDate( $this->mDate ) ); $wgUser->setOption( 'searchlimit', $this->validateIntOrNull( $this->mSearch ) ); $wgUser->setOption( 'contextlines', $this->validateIntOrNull( $this->mSearchLines ) ); $wgUser->setOption( 'contextchars', $this->validateIntOrNull( $this->mSearchChars ) ); $wgUser->setOption( 'rclimit', $this->validateIntOrNull( $this->mRecent ) ); + $wgUser->setOption( 'rcdays', $this->validateInt( $this->mRecentDays, 1, 7 ) ); + $wgUser->setOption( 'wllimit', $this->validateIntOrNull( $this->mWatchlistEdits, 0, 1000 ) ); $wgUser->setOption( 'rows', $this->validateInt( $this->mRows, 4, 1000 ) ); $wgUser->setOption( 'cols', $this->validateInt( $this->mCols, 4, 1000 ) ); $wgUser->setOption( 'stubthreshold', $this->validateIntOrNull( $this->mStubs ) ); @@ -246,6 +284,7 @@ $wgUser->setOption( 'imagesize', $this->mImageSize ); $wgUser->setOption( 'thumbsize', $this->mThumbSize ); $wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) ); + $wgUser->setOption( 'watchlistdays', $this->validateFloat( $this->mWatchlistDays, 0, 7 ) ); # Set search namespace options foreach( $this->mSearchNs as $i => $value ) { @@ -261,9 +300,17 @@ $wgUser->setOption( $tname, $tvalue ); } if (!$wgAuth->updateExternalDB($wgUser)) { - $this->mainPrefsForm( wfMsg( 'externaldberror' ) ); + $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) ); + return; + } + + $msg = ''; + if ( !wfRunHooks( "SavePreferences", array( $this, $wgUser, &$msg ) ) ) { + print "(($msg))"; + $this->mainPrefsForm( 'error', $msg ); return; } + $wgUser->setCookies(); $wgUser->saveSettings(); @@ -295,16 +342,18 @@ $wgUser->setCookies(); $wgUser->saveSettings(); } + if( $oldadr != $newadr ) { + wfRunHooks( "PrefsEmailAudit", array( $wgUser, $oldadr, $newadr ) ); + } } if( $needRedirect && $error === false ) { - $title =& Title::makeTitle( NS_SPECIAL, "Preferences" ); + $title =& SpecialPage::getTitleFor( "Preferences" ); $wgOut->redirect($title->getFullURL('success')); return; } $wgOut->setParserOptions( ParserOptions::newFromUser( $wgUser ) ); - $po = ParserOptions::newFromUser( $wgUser ); $this->mainPrefsForm( $error === false ? 'success' : 'error', $error); } @@ -312,26 +361,24 @@ * @access private */ function resetPrefs() { - global $wgUser, $wgLang, $wgContLang, $wgAllowRealName; + global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName; $this->mOldpass = $this->mNewpass = $this->mRetypePass = ''; $this->mUserEmail = $wgUser->getEmail(); $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp(); $this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : ''; - $this->mUserLanguage = $wgUser->getOption( 'language' ); - if( empty( $this->mUserLanguage ) ) { - # Quick hack for conversions, where this value is blank - global $wgContLanguageCode; - $this->mUserLanguage = $wgContLanguageCode; - } + + # language value might be blank, default to content language + $this->mUserLanguage = $wgUser->getOption( 'language', $wgContLanguageCode ); + $this->mUserVariant = $wgUser->getOption( 'variant'); $this->mEmailFlag = $wgUser->getOption( 'disablemail' ) == 1 ? 1 : 0; $this->mNick = $wgUser->getOption( 'nickname' ); $this->mQuickbar = $wgUser->getOption( 'quickbar' ); - $this->mSkin = $wgUser->getOption( 'skin' ); + $this->mSkin = Skin::normalizeKey( $wgUser->getOption( 'skin' ) ); $this->mMath = $wgUser->getOption( 'math' ); - $this->mDate = $wgUser->getOption( 'date' ); + $this->mDate = $wgUser->getDatePreference(); $this->mRows = $wgUser->getOption( 'rows' ); $this->mCols = $wgUser->getOption( 'cols' ); $this->mStubs = $wgUser->getOption( 'stubthreshold' ); @@ -342,11 +389,13 @@ $this->mImageSize = $wgUser->getOption( 'imagesize' ); $this->mThumbSize = $wgUser->getOption( 'thumbsize' ); $this->mRecent = $wgUser->getOption( 'rclimit' ); + $this->mRecentDays = $wgUser->getOption( 'rcdays' ); + $this->mWatchlistEdits = $wgUser->getOption( 'wllimit' ); $this->mUnderline = $wgUser->getOption( 'underline' ); + $this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' ); - $togs = $wgLang->getUserToggles(); + $togs = User::getToggles(); foreach ( $togs as $tname ) { - $ttext = wfMsg('tog-'.$tname); $this->mToggles[$tname] = $wgUser->getOption( $tname ); } @@ -356,13 +405,15 @@ $this->mSearchNs[$i] = $wgUser->getOption( 'searchNs'.$i ); } } + + wfRunHooks( "ResetPreferences", array( $this, $wgUser ) ); } /** * @access private */ function namespacesCheckboxes() { - global $wgContLang, $wgUser; + global $wgContLang; # Determine namespace checkboxes $namespaces = $wgContLang->getNamespaces(); @@ -377,21 +428,22 @@ if ( empty($name) ) $name = wfMsg( 'blanknamespace' ); - $r1 .= "\n"; + $r1 .= "
    \n"; } return $r1; } - function getToggle( $tname, $trailer = false) { + function getToggle( $tname, $trailer = false, $disabled = false ) { global $wgUser, $wgLang; $this->mUsedToggles[$tname] = true; $ttext = $wgLang->getUserToggle( $tname ); $checked = $wgUser->getOption( $tname ) == 1 ? ' checked="checked"' : ''; + $disabled = $disabled ? ' disabled="disabled"' : ''; $trailer = $trailer ? $trailer : ''; - return "
    " . + return "
    " . " $trailer
    \n"; } @@ -416,41 +468,70 @@ } /** + * Helper function for user information panel + * @param $td1 label for an item + * @param $td2 item or null + * @param $td3 optional help or null + * @return xhtml block + */ + function tableRow( $td1, $td2 = null, $td3 = null ) { + global $wgContLang; + + $align['align'] = $wgContLang->isRtl() ? 'right' : 'left'; + + if ( is_null( $td3 ) ) { + $td3 = ''; + } else { + $td3 = Xml::tags( 'tr', null, + Xml::tags( 'td', array( 'colspan' => '2' ), $td3 ) + ); + } + + if ( is_null( $td2 ) ) { + $td1 = Xml::tags( 'td', $align + array( 'colspan' => '2' ), $td1 ); + $td2 = ''; + } else { + $td1 = Xml::tags( 'td', $align, $td1 ); + $td2 = Xml::tags( 'td', $align, $td2 ); + } + + return Xml::tags( 'tr', null, $td1 . $td2 ). $td3 . "\n"; + + } + + /** * @access private */ function mainPrefsForm( $status , $message = '' ) { - global $wgUser, $wgOut, $wgLang, $wgContLang, $wgValidSkinNames; + global $wgUser, $wgOut, $wgLang, $wgContLang; global $wgAllowRealName, $wgImageLimits, $wgThumbLimits; global $wgDisableLangConversion; global $wgEnotifWatchlist, $wgEnotifUserTalk,$wgEnotifMinorEdits; global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress; global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication; - global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins; + global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth; $wgOut->setPageTitle( wfMsg( 'preferences' ) ); $wgOut->setArticleRelated( false ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc. + if ( $this->mSuccess || 'success' == $status ) { - $wgOut->addWikitext( ''. wfMsg( 'savedprefs' ) . "\n----" ); + $wgOut->addWikitext( '
    '. wfMsg( 'savedprefs' ) . '
    ' ); } else if ( 'error' == $status ) { - $wgOut->addWikitext( "" . $message . "\n----" ); + $wgOut->addWikitext( '
    ' . $message . '
    ' ); } else if ( '' != $status ) { $wgOut->addWikitext( $message . "\n----" ); } - $uname = $wgUser->getName(); - $uid = $wgUser->getID(); - - $wgOut->addWikiText( wfMsg( 'prefslogintext', $uname, $uid ) ); - $wgOut->addWikiText( wfMsg('clearyourcache')); $qbs = $wgLang->getQuickbarSettings(); $skinNames = $wgLang->getSkinNames(); $mathopts = $wgLang->getMathNames(); - $dateopts = $wgLang->getDateFormats(); - $togs = $wgLang->getUserToggles(); + $dateopts = $wgLang->getDatePreferences(); + $togs = User::getToggles(); - $titleObj = Title::makeTitle( NS_SPECIAL, 'Preferences' ); + $titleObj = SpecialPage::getTitleFor( 'Preferences' ); $action = $titleObj->escapeLocalURL(); # Pre-expire some toggles so they won't show if disabled @@ -460,165 +541,218 @@ $this->mUsedToggles[ 'enotifusertalkpages' ] = true; $this->mUsedToggles[ 'enotifminoredits' ] = true; $this->mUsedToggles[ 'enotifrevealaddr' ] = true; + $this->mUsedToggles[ 'ccmeonemails' ] = true; + $this->mUsedToggles[ 'uselivepreview' ] = true; - # Enotif - # - $this->mUserEmail = htmlspecialchars( $this->mUserEmail ); - $this->mRealName = htmlspecialchars( $this->mRealName ); - $this->mNick = htmlspecialchars( $this->mNick ); - if ( $this->mEmailFlag ) { $emfc = 'checked="checked"'; } + + if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; } else { $emfc = ''; } + if ($wgEmailAuthentication && ($this->mUserEmail != '') ) { if( $wgUser->getEmailAuthenticationTimestamp() ) { $emailauthenticated = wfMsg('emailauthenticated',$wgLang->timeanddate($wgUser->getEmailAuthenticationTimestamp(), true ) ).'
    '; + $disableEmailPrefs = false; } else { + $disableEmailPrefs = true; $skin = $wgUser->getSkin(); $emailauthenticated = wfMsg('emailnotauthenticated').'
    ' . - $skin->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Confirmemail' ), - wfMsg( 'emailconfirmlink' ) ); + $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Confirmemail' ), + wfMsg( 'emailconfirmlink' ) ) . '
    '; } } else { $emailauthenticated = ''; + $disableEmailPrefs = false; } if ($this->mUserEmail == '') { - $emailauthenticated = wfMsg( 'noemailprefs' ); + $emailauthenticated = wfMsg( 'noemailprefs' ) . '
    '; } $ps = $this->namespacesCheckboxes(); - $enotifwatchlistpages = ($wgEnotifWatchlist) ? $this->getToggle( 'enotifwatchlistpages' ) : ''; - $enotifusertalkpages = ($wgEnotifUserTalk) ? $this->getToggle( 'enotifusertalkpages' ) : ''; - $enotifminoredits = ($wgEnotifWatchlist && $wgEnotifMinorEdits) ? $this->getToggle( 'enotifminoredits' ) : ''; - $enotifrevealaddr = (($wgEnotifWatchlist || $wgEnotifUserTalk) && $wgEnotifRevealEditorAddress) ? $this->getToggle( 'enotifrevealaddr' ) : ''; - $prefs_help_email_enotif = ( $wgEnotifWatchlist || $wgEnotifUserTalk) ? ' ' . wfMsg('prefs-help-email-enotif') : ''; - $prefs_help_realname = ''; + $enotifwatchlistpages = ($wgEnotifWatchlist) ? $this->getToggle( 'enotifwatchlistpages', false, $disableEmailPrefs ) : ''; + $enotifusertalkpages = ($wgEnotifUserTalk) ? $this->getToggle( 'enotifusertalkpages', false, $disableEmailPrefs ) : ''; + $enotifminoredits = ($wgEnotifWatchlist && $wgEnotifMinorEdits) ? $this->getToggle( 'enotifminoredits', false, $disableEmailPrefs ) : ''; + $enotifrevealaddr = (($wgEnotifWatchlist || $wgEnotifUserTalk) && $wgEnotifRevealEditorAddress) ? $this->getToggle( 'enotifrevealaddr', false, $disableEmailPrefs ) : ''; #
    - $wgOut->addHTML( "
    " ); + $wgOut->addHTML( "" ); + $wgOut->addHTML( "
    " ); # User data - # - $wgOut->addHTML( "
    \n" . wfMsg('prefs-personal') . "\n\n"); + $wgOut->addHTML( + Xml::openElement( 'fieldset ' ) . + Xml::element( 'legend', null, wfMsg('prefs-personal') ) . + Xml::openElement( 'table' ) . + $this->tableRow( Xml::element( 'h2', null, wfMsg( 'prefs-personal' ) ) ) + ); + + $userInformationHtml = + $this->tableRow( wfMsgHtml( 'username' ), htmlspecialchars( $wgUser->getName() ) ) . + $this->tableRow( wfMsgHtml( 'uid' ), htmlspecialchars( $wgUser->getID() ) ) . + $this->tableRow( + wfMsgHtml( 'prefs-edits' ), + $wgLang->formatNum( User::edits( $wgUser->getId() ) ) + ); - if ($wgAllowRealName) { + if( wfRunHooks( 'PreferencesUserInformationPanel', array( $this, &$userInformationHtml ) ) ) { + $wgOut->addHtml( $userInformationHtml ); + } + + if ( $wgAllowRealName ) { $wgOut->addHTML( - $this->addRow( - wfMsg('yourrealname'), - "mRealName}\" size='25' />" + $this->tableRow( + Xml::label( wfMsg('yourrealname'), 'wpRealName' ), + Xml::input( 'wpRealName', 25, $this->mRealName, array( 'id' => 'wpRealName' ) ), + Xml::tags('div', array( 'class' => 'prefsectiontip' ), + wfMsgExt( 'prefs-help-realname', 'parseinline' ) + ) ) ); } - if ($wgEnableEmail) { + if ( $wgEnableEmail ) { $wgOut->addHTML( - $this->addRow( - wfMsg( 'youremail' ), - "mUserEmail}\" size='25' />" + $this->tableRow( + Xml::label( wfMsg('youremail'), 'wpUserEmail' ), + Xml::input( 'wpUserEmail', 25, $this->mUserEmail, array( 'id' => 'wpUserEmail' ) ), + Xml::tags('div', array( 'class' => 'prefsectiontip' ), + wfMsgExt( 'prefs-help-email', 'parseinline' ) + ) ) ); } + global $wgParser, $wgMaxSigChars; + if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) { + $invalidSig = $this->tableRow( + ' ', + Xml::element( 'span', array( 'class' => 'error' ), + wfMsg( 'badsiglength', $wgLang->formatNum( $wgMaxSigChars ) ) ) + ); + } elseif( !empty( $this->mToggles['fancysig'] ) && + false === $wgParser->validateSig( $this->mNick ) ) { + $invalidSig = $this->tableRow( + ' ', + Xml::element( 'span', array( 'class' => 'error' ), wfMsg( 'badsig' ) ) + ); + } else { + $invalidSig = ''; + } + $wgOut->addHTML( - $this->addRow( - wfMsg( 'yournick' ), - "mNick}\" size='25' />" + $this->tableRow( + Xml::label( wfMsg( 'yournick' ), 'wpNick' ), + Xml::input( 'wpNick', 25, $this->mNick, + array( + 'id' => 'wpNick', + // Note: $wgMaxSigChars is enforced in Unicode characters, + // both on the backend and now in the browser. + // Badly-behaved requests may still try to submit + // an overlong string, however. + 'maxlength' => $wgMaxSigChars ) ) ) . - # FIXME: The part should be where the   is, getToggle() needs - # to be changed to out return its output in two parts. -ævar - $this->addRow( - ' ', - $this->getToggle( 'fancysig' ) - ) + $invalidSig . + $this->tableRow( ' ', $this->getToggle( 'fancysig' ) ) ); - /** - * If a bogus value is set, default to the content language. - * Otherwise, no default is selected and the user ends up - * with an Afrikaans interface since it's first in the list. - */ - $languages = $wgLang->getLanguageNames(); - $selectedLang = isset( $languages[$this->mUserLanguage] ) ? $this->mUserLanguage : $wgContLanguageCode; - $selbox = null; - foreach($languages as $code => $name) { - global $IP; - /* only add languages that have a file */ - $langfile="$IP/languages/Language".str_replace('-', '_', ucfirst($code)).".php"; - if(file_exists($langfile) || $code == $wgContLanguageCode) { - $sel = ($code == $selectedLang)? ' selected="selected"' : ''; - $selbox .= "\n"; - } - } - $wgOut->addHTML( $this->addRow( wfMsg('yourlanguage'), "" )); + list( $lsLabel, $lsSelect) = Xml::languageSelector( $this->mUserLanguage ); + $wgOut->addHTML( + $this->tableRow( $lsLabel, $lsSelect ) + ); /* see if there are multiple language variants to choose from*/ if(!$wgDisableLangConversion) { $variants = $wgContLang->getVariants(); + $variantArray = array(); + $languages = Language::getLanguageNames( true ); foreach($variants as $v) { $v = str_replace( '_', '-', strtolower($v)); - if($name = $languages[$v]) { - $variantArray[$v] = $name; + if( array_key_exists( $v, $languages ) ) { + // If it doesn't have a name, we'll pretend it doesn't exist + $variantArray[$v] = $languages[$v]; } } - $selbox = null; - foreach($variantArray as $code => $name) { - $sel = $code == $this->mUserVariant ? 'selected="selected"' : ''; - $selbox .= ""; + $options = "\n"; + foreach( $variantArray as $code => $name ) { + $selected = ($code == $this->mUserVariant); + $options .= Xml::option( "$code - $name", $code, $selected ) . "\n"; } if(count($variantArray) > 1) { $wgOut->addHtml( - $this->addRow( wfMsg( 'yourvariant' ), "" ) + $this->tableRow( + Xml::label( wfMsg( 'yourvariant' ), 'wpUserVariant' ), + Xml::tags( 'select', + array( 'name' => 'wpUserVariant', 'id' => 'wpUserVariant' ), + $options + ) + ) ); } } - $wgOut->addHTML('
    '); # Password - $this->mOldpass = htmlspecialchars( $this->mOldpass ); - $this->mNewpass = htmlspecialchars( $this->mNewpass ); - $this->mRetypePass = htmlspecialchars( $this->mRetypePass ); - - $wgOut->addHTML( '
    ' . wfMsg( 'changepassword' ) . ''); - $wgOut->addHTML( - $this->addRow( wfMsg( 'oldpassword' ), "mOldpass}\" size='20' />" ) . - $this->addRow( wfMsg( 'newpassword' ), "mNewpass}\" size='20' />" ) . - $this->addRow( wfMsg( 'retypenew' ), "mRetypePass}\" size='20' />" ) . - "
    \n" . - $this->getToggle( "rememberpassword" ) . "
    \n\n" ); + if( $wgAuth->allowPasswordChange() ) { + $wgOut->addHTML( + $this->tableRow( Xml::element( 'h2', null, wfMsg( 'changepassword' ) ) ) . + $this->tableRow( + Xml::label( wfMsg( 'oldpassword' ), 'wpOldpass' ), + Xml::password( 'wpOldpass', 25, $this->mOldpass, array( 'id' => 'wpOldpass' ) ) + ) . + $this->tableRow( + Xml::label( wfMsg( 'newpassword' ), 'wpNewpass' ), + Xml::password( 'wpNewpass', 25, $this->mNewpass, array( 'id' => 'wpNewpass' ) ) + ) . + $this->tableRow( + Xml::label( wfMsg( 'retypenew' ), 'wpRetypePass' ), + Xml::password( 'wpRetypePass', 25, $this->mRetypePass, array( 'id' => 'wpRetypePass' ) ) + ) . + Xml::tags( 'tr', null, + Xml::tags( 'td', array( 'colspan' => '2' ), + $this->getToggle( "rememberpassword" ) + ) + ) + ); + } # # Enotif - if ($wgEnableEmail) { - $wgOut->addHTML( '
    ' . wfMsg( 'email' ) . '' ); - $wgOut->addHTML( - $emailauthenticated. - $enotifrevealaddr. - $enotifwatchlistpages. - $enotifusertalkpages. - $enotifminoredits ); - if ($wgEnableUserEmail) { - $emf = wfMsg( 'emailflag' ); - $wgOut->addHTML( - "
    " ); - } + if ( $wgEnableEmail ) { + + $moreEmail = ''; + if ($wgEnableUserEmail) { + $emf = wfMsg( 'allowemail' ); + $disabled = $disableEmailPrefs ? ' disabled="disabled"' : ''; + $moreEmail = + " "; + } - $wgOut->addHTML( '
    ' ); - } - #
    - if ($wgAllowRealName || $wgEnableEmail) { - $wgOut->addHTML("
    "); - $rn = $wgAllowRealName ? wfMsg('prefs-help-realname') : ''; - $em = $wgEnableEmail ? '
    ' . wfMsg('prefs-help-email') : ''; - $wgOut->addHTML( $rn . $em . '
    '); + $wgOut->addHTML( + $this->tableRow( Xml::element( 'h2', null, wfMsg( 'email' ) ) ) . + $this->tableRow( + $emailauthenticated. + $enotifrevealaddr. + $enotifwatchlistpages. + $enotifusertalkpages. + $enotifminoredits. + $moreEmail. + $this->getToggle( 'ccmeonemails' ) + ) + ); } + # + + $wgOut->addHTML( + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) + ); - $wgOut->addHTML( '
    ' ); # Quickbar # @@ -633,7 +767,7 @@ } else { # Need to output a hidden option even if the relevant skin is not in use, # otherwise the preference will get reset to 0 on submit - $wgOut->addHTML( "" ); + $wgOut->addHtml( wfHidden( 'wpQuickbar', $this->mQuickbar ) ); } # Skin @@ -641,20 +775,28 @@ $wgOut->addHTML( "
    \n\n" . wfMsg('skin') . "\n" ); $mptitle = Title::newMainPage(); $previewtext = wfMsg('skinpreview'); - # Only show members of $wgValidSkinNames rather than + # Only show members of Skin::getSkinNames() rather than # $skinNames (skins is all skin names from Language.php) - foreach ($wgValidSkinNames as $skinkey => $skinname ) { + $validSkinNames = Skin::getSkinNames(); + # Sort by UI skin name. First though need to update validSkinNames as sometimes + # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI). + foreach ($validSkinNames as $skinkey => & $skinname ) { + if ( isset( $skinNames[$skinkey] ) ) { + $skinname = $skinNames[$skinkey]; + } + } + asort($validSkinNames); + foreach ($validSkinNames as $skinkey => $sn ) { if ( in_array( $skinkey, $wgSkipSkins ) ) { continue; } $checked = $skinkey == $this->mSkin ? ' checked="checked"' : ''; - $sn = isset( $skinNames[$skinkey] ) ? $skinNames[$skinkey] : $skinname; $mplink = htmlspecialchars($mptitle->getLocalURL("useskin=$skinkey")); $previewlink = "$previewtext"; if( $skinkey == $wgDefaultSkin ) $sn .= ' (' . wfMsg( 'default' ) . ')'; - $wgOut->addHTML( " {$sn} $previewlink
    \n" ); + $wgOut->addHTML( " $previewlink
    \n" ); } $wgOut->addHTML( "
    \n\n" ); @@ -664,71 +806,103 @@ if( $wgUseTeX ) { $wgOut->addHTML( "
    \n" . wfMsg('math') . '' ); foreach ( $mathopts as $k => $v ) { - $checked = $k == $this->mMath ? ' checked="checked"' : ''; - $wgOut->addHTML( "
    \n" ); + $checked = ($k == $this->mMath); + $wgOut->addHTML( + Xml::openElement( 'div' ) . + Xml::radioLabel( wfMsg( $v ), 'wpMath', $k, "mw-sp-math-$k", $checked ) . + Xml::closeElement( 'div' ) . "\n" + ); } $wgOut->addHTML( "
    \n\n" ); } # Files # - $wgOut->addHTML("
    - " . wfMsg( 'files' ) . " -
    -
    \n\n"); + $wgOut->addHTML( + "
    \n" . Xml::element( 'legend', null, wfMsg( 'files' ) ) . "\n" + ); + + $imageLimitOptions = null; + foreach ( $wgImageLimits as $index => $limits ) { + $selected = ($index == $this->mImageSize); + $imageLimitOptions .= Xml::option( "{$limits[0]}×{$limits[1]}" . + wfMsg('unit-pixel'), $index, $selected ); + } + + $imageSizeId = 'wpImageSize'; + $wgOut->addHTML( + "
    " . Xml::label( wfMsg('imagemaxsize'), $imageSizeId ) . " " . + Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) . + $imageLimitOptions . + Xml::closeElement( 'select' ) . "
    \n" + ); + + $imageThumbOptions = null; + foreach ( $wgThumbLimits as $index => $size ) { + $selected = ($index == $this->mThumbSize); + $imageThumbOptions .= Xml::option($size . wfMsg('unit-pixel'), $index, + $selected); + } + + $thumbSizeId = 'wpThumbSize'; + $wgOut->addHTML( + "
    " . Xml::label( wfMsg('thumbsize'), $thumbSizeId ) . " " . + Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) . + $imageThumbOptions . + Xml::closeElement( 'select' ) . "
    \n" + ); + + $wgOut->addHTML( "
    \n\n" ); + + # Date format + # + # Date/Time + # + + $wgOut->addHTML( "
    \n" . wfMsg( 'datetime' ) . "\n" ); - # Date format - # if ($dateopts) { - $wgOut->addHTML( "
    \n" . wfMsg('dateformat') . "\n" ); - foreach($dateopts as $key => $option) { + $wgOut->addHTML( "
    \n" . wfMsg( 'dateformat' ) . "\n" ); + $idCnt = 0; + $epoch = '20010115161234'; # Wikipedia day + foreach( $dateopts as $key ) { + if( $key == 'default' ) { + $formatted = wfMsgHtml( 'datedefault' ); + } else { + $formatted = htmlspecialchars( $wgLang->timeanddate( $epoch, false, $key ) ); + } ($key == $this->mDate) ? $checked = ' checked="checked"' : $checked = ''; - $wgOut->addHTML( "
    \n" ); + $wgOut->addHTML( "
    \n" ); + $idCnt++; } - $wgOut->addHTML( "
    \n\n"); + $wgOut->addHTML( "
    \n" ); } - # Time zone - # - $nowlocal = $wgLang->time( $now = wfTimestampNow(), true ); $nowserver = $wgLang->time( $now, false ); - $wgOut->addHTML( '
    ' . wfMsg( 'timezonelegend' ) . '' . + $wgOut->addHTML( '
    ' . wfMsg( 'timezonelegend' ). '
    ' . $this->addRow( wfMsg( 'servertime' ), $nowserver ) . $this->addRow( wfMsg( 'localtime' ), $nowlocal ) . $this->addRow( - wfMsg( 'timezoneoffset' ), - "mHourDiff ) . "\" size='6' />" + '', + "mHourDiff ) . "\" size='6' />" ) . "
    -
    -
    ¹" . wfMsg( 'timezonetext' ) . "
    +
    ¹" . wfMsg( 'timezonetext' ) . "
    \n\n" ); # Editing # + global $wgLivePreview; $wgOut->addHTML( '
    ' . wfMsg( 'textboxsize' ) . ' -
    - - -
    " . +
    ' . + wfInputLabel( wfMsg( 'rows' ), 'wpRows', 'wpRows', 3, $this->mRows ) . + ' ' . + wfInputLabel( wfMsg( 'columns' ), 'wpCols', 'wpCols', 3, $this->mCols ) . + "
    " . $this->getToggles( array( 'editsection', 'editsectiononrightclick', @@ -737,59 +911,98 @@ 'showtoolbar', 'previewonfirst', 'previewontop', - 'watchdefault', 'minordefault', 'externaleditor', - 'externaldiff' ) - ) . '
    ' + 'externaldiff', + $wgLivePreview ? 'uselivepreview' : false, + 'forceeditsummary', + ) ) . '' ); - $wgOut->addHTML( '
    ' . htmlspecialchars(wfMsg('prefs-rc')) . ' - ' . - $this->addRow( - wfMsg ( 'stubthreshold' ), - "mStubs\" size='6' />" - ) . - $this->addRow( - wfMsg( 'recentchangescount' ), - "mRecent\" size='6' />" - ) . - '
    ' . - $this->getToggles( array( - 'hideminor', - $wgRCShowWatchingUsers ? 'shownumberswatching' : false, - 'usenewrc' ) - ) . '
    ' - ); + # Recent changes + $wgOut->addHtml( '
    ' . wfMsgHtml( 'prefs-rc' ) . '' ); + + $rc = ''; + $rc .= ''; + $rc .= ''; + $rc .= ''; + $rc .= ''; + $rc .= ''; + $rc .= '
    ' . Xml::label( wfMsg( 'recentchangesdays' ), 'wpRecentDays' ) . '' . Xml::input( 'wpRecentDays', 3, $this->mRecentDays, array( 'id' => 'wpRecentDays' ) ) . '
    ' . Xml::label( wfMsg( 'recentchangescount' ), 'wpRecent' ) . '' . Xml::input( 'wpRecent', 3, $this->mRecent, array( 'id' => 'wpRecent' ) ) . '
    '; + $wgOut->addHtml( $rc ); + + $wgOut->addHtml( '
    ' ); + + $toggles[] = 'hideminor'; + if( $wgRCShowWatchingUsers ) + $toggles[] = 'shownumberswatching'; + $toggles[] = 'usenewrc'; + $wgOut->addHtml( $this->getToggles( $toggles ) ); + + $wgOut->addHtml( '
    ' ); + + # Watchlist + $wgOut->addHtml( '
    ' . wfMsgHtml( 'prefs-watchlist' ) . '' ); + + $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-days' ), 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) ); + $wgOut->addHtml( '

    ' ); + + $wgOut->addHtml( $this->getToggle( 'extendwatchlist' ) ); + $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) ); + $wgOut->addHtml( '

    ' ); + + $wgOut->addHtml( $this->getToggles( array( 'watchlisthideown', 'watchlisthidebots', 'watchlisthideminor' ) ) ); + + if( $wgUser->isAllowed( 'createpage' ) || $wgUser->isAllowed( 'createtalk' ) ) + $wgOut->addHtml( $this->getToggle( 'watchcreations' ) ); + foreach( array( 'edit' => 'watchdefault', 'move' => 'watchmoves', 'delete' => 'watchdeletion' ) as $action => $toggle ) { + if( $wgUser->isAllowed( $action ) ) + $wgOut->addHtml( $this->getToggle( $toggle ) ); + } + $this->mUsedToggles['watchcreations'] = true; + $this->mUsedToggles['watchdefault'] = true; + $this->mUsedToggles['watchmoves'] = true; + $this->mUsedToggles['watchdeletion'] = true; + + $wgOut->addHtml( '
    ' ); + # Search $wgOut->addHTML( '
    ' . wfMsg( 'searchresultshead' ) . '' . - $this->addRow( wfMsg( 'resultsperpage' ), "mSearch\" size='4' />" ) . - $this->addRow( wfMsg( 'contextlines' ), "mSearchLines\" size='4' />" ) . - $this->addRow( wfMsg( 'contextchars' ), "mSearchChars\" size='4' />" ) . + $this->addRow( + wfLabel( wfMsg( 'resultsperpage' ), 'wpSearch' ), + wfInput( 'wpSearch', 4, $this->mSearch, array( 'id' => 'wpSearch' ) ) + ) . + $this->addRow( + wfLabel( wfMsg( 'contextlines' ), 'wpSearchLines' ), + wfInput( 'wpSearchLines', 4, $this->mSearchLines, array( 'id' => 'wpSearchLines' ) ) + ) . + $this->addRow( + wfLabel( wfMsg( 'contextchars' ), 'wpSearchChars' ), + wfInput( 'wpSearchChars', 4, $this->mSearchChars, array( 'id' => 'wpSearchChars' ) ) + ) . "
    " . wfMsg( 'defaultns' ) . "$ps
    " ); # Misc # $wgOut->addHTML('
    ' . wfMsg('prefs-misc') . ''); - - $msgUnderline = htmlspecialchars(wfMsg('tog-underline')); - $msgUnderlinenever = htmlspecialchars(wfMsg('underline-never')); - $msgUnderlinealways = htmlspecialchars(wfMsg('underline-always')); - $msgUnderlinedefault = htmlspecialchars(wfMsg('underline-default')); - $uopt = $wgUser->getOption('underline'); + $wgOut->addHtml( ' ' ); + $wgOut->addHtml( Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) ); + $msgUnderline = htmlspecialchars( wfMsg ( 'tog-underline' ) ); + $msgUnderlinenever = htmlspecialchars( wfMsg ( 'underline-never' ) ); + $msgUnderlinealways = htmlspecialchars( wfMsg ( 'underline-always' ) ); + $msgUnderlinedefault = htmlspecialchars( wfMsg ( 'underline-default' ) ); + $uopt = $wgUser->getOption("underline"); $s0 = $uopt == 0 ? ' selected="selected"' : ''; $s1 = $uopt == 1 ? ' selected="selected"' : ''; $s2 = $uopt == 2 ? ' selected="selected"' : ''; $wgOut->addHTML(" -
    -
    -"); +

    "); + foreach ( $togs as $tname ) { if( !array_key_exists( $tname, $this->mUsedToggles ) ) { $wgOut->addHTML( $this->getToggle( $tname ) ); @@ -797,19 +1010,26 @@ } $wgOut->addHTML( '' ); - $token = $wgUser->editToken(); + wfRunHooks( "RenderPreferencesForm", array( $this, $wgOut ) ); + + $token = htmlspecialchars( $wgUser->editToken() ); + $skin = $wgUser->getSkin(); $wgOut->addHTML( "
    - - + tooltipAndAccesskey('save')." /> +
    - -
    \n" ); + +
    \n" ); + + $wgOut->addHtml( Xml::tags( 'div', array( 'class' => "prefcache" ), + wfMsgExt( 'clearyourcache', 'parseinline' ) ) + ); + } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialRandompage.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialRandompage.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialRandompage.php 2005-08-01 20:01:45.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialRandompage.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,60 +1,108 @@ , Ilmari Karonen + * @license GNU General Public Licence 2.0 or later */ /** - * Constructor - * - * @param string $par the namespace to get a random page from (default NS_MAIN), - * used as e.g. Special:Randompage/Category + * Main execution point + * @param $par Namespace to select the page from */ -function wfSpecialRandompage( $par = NS_MAIN ) { - global $wgOut, $wgTitle, $wgArticle, $wgExtraRandompageSQL, $wgContLang; - $fname = 'wfSpecialRandompage'; - - # Determine the namespace to get a random page from. - $namespace = $wgContLang->getNsIndex($par); - if ($namespace === false || $namespace < NS_MAIN) { - $namespace = NS_MAIN; - } - - # NOTE! We use a literal constant in the SQL instead of the RAND() - # function because RAND() will return a different value for every row - # in the table. That's both very slow and returns results heavily - # biased towards low values, as rows later in the table will likely - # never be reached for comparison. - # - # Using a literal constant means the whole thing gets optimized on - # the index, and the comparison is both fast and fair. - - # interpolation and sprintf() can muck up with locale-specific decimal separator - $randstr = wfRandom(); - - $db =& wfGetDB( DB_SLAVE ); - $use_index = $db->useIndexClause( 'page_random' ); - $page = $db->tableName( 'page' ); - - $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : ''; - $sql = "SELECT page_id,page_title - FROM $page $use_index - WHERE page_namespace=$namespace AND page_is_redirect=0 $extra - AND page_random>$randstr - ORDER BY page_random - LIMIT 1"; - $res = $db->query( $sql, $fname ); - - $title = null; - if( $s = $db->fetchObject( $res ) ) { - $title =& Title::makeTitle( $namespace, $s->page_title ); - } +function wfSpecialRandompage( $par = null ) { + global $wgOut, $wgContLang; + + $rnd = new RandomPage(); + $rnd->setNamespace( $wgContLang->getNsIndex( $par ) ); + $rnd->setRedirect( false ); + + $title = $rnd->getRandomTitle(); + if( is_null( $title ) ) { - # That's not supposed to happen :) - $title = Title::newFromText( wfMsg( 'mainpage' ) ); + $wgOut->addWikiText( wfMsg( 'randompage-nopages' ) ); + return; } - $wgOut->reportTime(); # for logfile + + $wgOut->reportTime(); $wgOut->redirect( $title->getFullUrl() ); } -?> + +/** + * Special page to direct the user to a random page + * + * @addtogroup SpecialPage + */ +class RandomPage { + private $namespace = NS_MAIN; // namespace to select pages from + private $redirect = false; // select redirects instead of normal pages? + + public function getNamespace ( ) { + return $this->namespace; + } + public function setNamespace ( $ns ) { + if( $ns < NS_MAIN ) $ns = NS_MAIN; + $this->namespace = $ns; + } + public function getRedirect ( ) { + return $this->redirect; + } + public function setRedirect ( $redirect ) { + $this->redirect = $redirect; + } + + /** + * Choose a random title. + * @return Title object (or null if nothing to choose from) + */ + public function getRandomTitle ( ) { + $randstr = wfRandom(); + $row = $this->selectRandomPageFromDB( $randstr ); + + /* If we picked a value that was higher than any in + * the DB, wrap around and select the page with the + * lowest value instead! One might think this would + * skew the distribution, but in fact it won't cause + * any more bias than what the page_random scheme + * causes anyway. Trust me, I'm a mathematician. :) + */ + if( !$row ) + $row = $this->selectRandomPageFromDB( "0" ); + + if( $row ) + return Title::makeTitleSafe( $this->namespace, $row->page_title ); + else + return null; + } + + private function selectRandomPageFromDB ( $randstr ) { + global $wgExtraRandompageSQL; + $fname = 'RandomPage::selectRandomPageFromDB'; + + $dbr = wfGetDB( DB_SLAVE ); + + $use_index = $dbr->useIndexClause( 'page_random' ); + $page = $dbr->tableName( 'page' ); + + $ns = (int) $this->namespace; + $redirect = $this->redirect ? 1 : 0; + + $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : ""; + $sql = "SELECT page_title + FROM $page $use_index + WHERE page_namespace = $ns + AND page_is_redirect = $redirect + AND page_random >= $randstr + $extra + ORDER BY page_random"; + + $sql = $dbr->limitResult( $sql, 1, 0 ); + $res = $dbr->query( $sql, $fname ); + return $dbr->fetchObject( $res ); + } +} + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialRecentchanges.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialRecentchanges.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialRecentchanges.php 2006-02-11 06:57:56.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialRecentchanges.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,65 +1,63 @@ getVal( 'feed' ); + /* Checkbox values can't be true by default, because + * we cannot differentiate between unset and not set at all + */ $defaults = array( /* int */ 'days' => $wgUser->getDefaultOption('rcdays'), /* int */ 'limit' => $wgUser->getDefaultOption('rclimit'), /* bool */ 'hideminor' => false, /* bool */ 'hidebots' => true, + /* bool */ 'hideanons' => false, /* bool */ 'hideliu' => false, /* bool */ 'hidepatrolled' => false, /* bool */ 'hidemyself' => false, /* text */ 'from' => '', /* text */ 'namespace' => null, /* bool */ 'invert' => false, + /* bool */ 'categories_any' => false, ); extract($defaults); - - $days = $wgUser->getOption( 'rcdays' ); - if ( !$days ) { $days = $defaults['days']; } + + $days = $wgUser->getOption( 'rcdays', $defaults['days']); $days = $wgRequest->getInt( 'days', $days ); - $limit = $wgUser->getOption( 'rclimit' ); - if ( !$limit ) { $limit = $defaults['limit']; } + $limit = $wgUser->getOption( 'rclimit', $defaults['limit'] ); # list( $limit, $offset ) = wfCheckLimits( 100, 'rclimit' ); $limit = $wgRequest->getInt( 'limit', $limit ); /* order of selection: url > preferences > default */ - $hideminor = $wgRequest->getBool( 'hideminor', $wgUser->getOption( 'hideminor') ? true : $defaults['hideminor'] ); + $hideminor = $wgRequest->getBool( 'hideminor', $wgUser->getOption( 'hideminor') ? true : $defaults['hideminor'] ); # As a feed, use limited settings only if( $feedFormat ) { global $wgFeedLimit; if( $limit > $wgFeedLimit ) { - $options['limit'] = $wgFeedLimit; + $limit = $wgFeedLimit; } } else { @@ -67,10 +65,11 @@ $namespace = $wgRequest->getIntOrNull( 'namespace' ); $invert = $wgRequest->getBool( 'invert', $defaults['invert'] ); $hidebots = $wgRequest->getBool( 'hidebots', $defaults['hidebots'] ); + $hideanons = $wgRequest->getBool( 'hideanons', $defaults['hideanons'] ); $hideliu = $wgRequest->getBool( 'hideliu', $defaults['hideliu'] ); $hidepatrolled = $wgRequest->getBool( 'hidepatrolled', $defaults['hidepatrolled'] ); $hidemyself = $wgRequest->getBool ( 'hidemyself', $defaults['hidemyself'] ); - $from = $wgRequest->getVal( 'from', $defaults['from'] ); + $from = $wgRequest->getVal( 'from', $defaults['from'] ); # Get query parameters from path if( $par ) { @@ -82,12 +81,14 @@ if ( 'minor' == $bit ) $hideminor = 0; if ( 'hideliu' == $bit ) $hideliu = 1; if ( 'hidepatrolled' == $bit ) $hidepatrolled = 1; + if ( 'hideanons' == $bit ) $hideanons = 1; if ( 'hidemyself' == $bit ) $hidemyself = 1; if ( is_numeric( $bit ) ) { $limit = $bit; } - + + $m = array(); if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) { $limit = $m[1]; } @@ -103,10 +104,10 @@ # Database connection and caching - $dbr =& wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( 'recentchanges', 'watchlist' ) ); + $dbr = wfGetDB( DB_SLAVE ); + list( $recentchanges, $watchlist ) = $dbr->tableNamesN( 'recentchanges', 'watchlist' ); + - $cutoff_unixtime = time() - ( $days * 86400 ); $cutoff_unixtime = $cutoff_unixtime - ($cutoff_unixtime % 86400); $cutoff = $dbr->timestamp( $cutoff_unixtime ); @@ -115,7 +116,7 @@ } else { $from = $defaults['from']; } - + # 10 seconds server-side caching max $wgOut->setSquidMaxage( 10 ); @@ -129,23 +130,40 @@ } } - $hidem = $hideminor ? 'AND rc_minor=0' : ''; - $hidem .= $hidebots ? ' AND rc_bot=0' : ''; - $hidem .= ( $hideliu && !$hidemyself ) ? ' AND rc_user=0' : ''; - $hidem .= $hidepatrolled ? ' AND rc_patrolled=0' : ''; - $hidem .= $hidemyself ? ' AND rc_user <> '.$wgUser->getID() : ''; + # It makes no sense to hide both anons and logged-in users + # Where this occurs, force anons to be shown + if( $hideanons && $hideliu ) + $hideanons = false; + + # Form WHERE fragments for all the options + $hidem = $hideminor ? 'AND rc_minor = 0' : ''; + $hidem .= $hidebots ? ' AND rc_bot = 0' : ''; + $hidem .= $hideliu ? ' AND rc_user = 0' : ''; + $hidem .= ( $wgUseRCPatrol && $hidepatrolled ) ? ' AND rc_patrolled = 0' : ''; + $hidem .= $hideanons ? ' AND rc_user != 0' : ''; + + if( $hidemyself ) { + if( $wgUser->getID() ) { + $hidem .= ' AND rc_user != ' . $wgUser->getID(); + } else { + $hidem .= ' AND rc_user_text != ' . $dbr->addQuotes( $wgUser->getName() ); + } + } + + # Namespace filtering $hidem .= is_null( $namespace ) ? '' : ' AND rc_namespace' . ($invert ? '!=' : '=') . $namespace; // This is the big thing! $uid = $wgUser->getID(); - $notifts = ($wgShowUpdatedMarker?",wl_notificationtimestamp":""); // Perform query - $sql2 = "SELECT $recentchanges.*" . ($uid ? ",wl_user".$notifts : "") . " FROM $recentchanges " . + $forceclause = $dbr->useIndexClause("rc_timestamp"); + $sql2 = "SELECT * FROM $recentchanges $forceclause". ($uid ? "LEFT OUTER JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "") . - "WHERE rc_timestamp > '{$cutoff}' {$hidem} " . - "ORDER BY rc_timestamp DESC LIMIT {$limit}"; + "WHERE rc_timestamp >= '{$cutoff}' {$hidem} " . + "ORDER BY rc_timestamp DESC"; + $sql2 = $dbr->limitResult($sql2, $limit, 0); $res = $dbr->query( $sql2, $fname ); // Fetch results, prepare a batch link existence check query @@ -153,53 +171,66 @@ $batch = new LinkBatch; while( $row = $dbr->fetchObject( $res ) ){ $rows[] = $row; - // User page link - $title = Title::makeTitleSafe( NS_USER, $row->rc_user_text ); - $batch->addObj( $title ); - - // User talk - $title = Title::makeTitleSafe( NS_USER_TALK, $row->rc_user_text ); - $batch->addObj( $title ); + if ( !$feedFormat ) { + // User page link + $title = Title::makeTitleSafe( NS_USER, $row->rc_user_text ); + $batch->addObj( $title ); + + // User talk + $title = Title::makeTitleSafe( NS_USER_TALK, $row->rc_user_text ); + $batch->addObj( $title ); + } } $dbr->freeResult( $res ); - // Run existence checks - $batch->execute( $wgLinkCache ); - if( $feedFormat ) { rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ); } else { # Web output... + // Run existence checks + $batch->execute(); + $any = $wgRequest->getBool( 'categories_any', $defaults['categories_any']); + // Output header if ( !$specialPage->including() ) { - $wgOut->addWikiText( wfMsgForContent( "recentchangestext" ) ); - + $wgOut->addWikiText( wfMsgForContentNoTrans( "recentchangestext" ) ); + // Dump everything here $nondefaults = array(); - + wfAppendToArrayIfNotDefault( 'days', $days, $defaults, $nondefaults); wfAppendToArrayIfNotDefault( 'limit', $limit , $defaults, $nondefaults); wfAppendToArrayIfNotDefault( 'hideminor', $hideminor, $defaults, $nondefaults); wfAppendToArrayIfNotDefault( 'hidebots', $hidebots, $defaults, $nondefaults); + wfAppendToArrayIfNotDefault( 'hideanons', $hideanons, $defaults, $nondefaults ); wfAppendToArrayIfNotDefault( 'hideliu', $hideliu, $defaults, $nondefaults); wfAppendToArrayIfNotDefault( 'hidepatrolled', $hidepatrolled, $defaults, $nondefaults); wfAppendToArrayIfNotDefault( 'hidemyself', $hidemyself, $defaults, $nondefaults); wfAppendToArrayIfNotDefault( 'from', $from, $defaults, $nondefaults); wfAppendToArrayIfNotDefault( 'namespace', $namespace, $defaults, $nondefaults); wfAppendToArrayIfNotDefault( 'invert', $invert, $defaults, $nondefaults); + wfAppendToArrayIfNotDefault( 'categories_any', $any, $defaults, $nondefaults); // Add end of the texts - $wgOut->addHTML( '
    ' . rcOptionsPanel( $defaults, $nondefaults ) ); - $wgOut->addHTML( rcNamespaceForm( $namespace, $invert, $nondefaults) . '
    '); + $wgOut->addHTML( '
    ' . rcOptionsPanel( $defaults, $nondefaults ) . "\n" ); + $wgOut->addHTML( rcNamespaceForm( $namespace, $invert, $nondefaults, $any ) . '
    '."\n"); } // And now for the content - $sk = $wgUser->getSkin(); $wgOut->setSyndicated( true ); - $list =& new ChangesList( $sk ); + + $list = ChangesList::newFromUser( $wgUser ); + + if ( $wgAllowCategorizedRecentChanges ) { + $categories = trim ( $wgRequest->getVal ( 'categories' , "" ) ) ; + $categories = str_replace ( "|" , "\n" , $categories ) ; + $categories = explode ( "\n" , $categories ) ; + rcFilterByCategories ( $rows , $categories , $any ) ; + } + $s = $list->beginRecentChangesList(); $counter = 1; foreach( $rows as $obj ){ @@ -237,23 +268,68 @@ } } +function rcFilterByCategories ( &$rows , $categories , $any ) { + # Filter categories + $cats = array () ; + foreach ( $categories AS $cat ) { + $cat = trim ( $cat ) ; + if ( $cat == "" ) continue ; + $cats[] = $cat ; + } + + # Filter articles + $articles = array () ; + $a2r = array () ; + foreach ( $rows AS $k => $r ) { + $nt = Title::newFromText ( $r->rc_title , $r->rc_namespace ) ; + $id = $nt->getArticleID() ; + if ( $id == 0 ) continue ; # Page might have been deleted... + if ( !in_array ( $id , $articles ) ) { + $articles[] = $id ; + } + if ( !isset ( $a2r[$id] ) ) { + $a2r[$id] = array() ; + } + $a2r[$id][] = $k ; + } + + # Shortcut? + if ( count ( $articles ) == 0 OR count ( $cats ) == 0 ) + return ; + + # Look up + $c = new Categoryfinder ; + $c->seed ( $articles , $cats , $any ? "OR" : "AND" ) ; + $match = $c->run () ; + + # Filter + $newrows = array () ; + foreach ( $match AS $id ) { + foreach ( $a2r[$id] AS $rev ) { + $k = $rev ; + $newrows[$k] = $rows[$k] ; + } + } + $rows = $newrows ; +} + function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) { - global $messageMemc, $wgDBname, $wgFeedCacheTimeout; + global $messageMemc, $wgFeedCacheTimeout; global $wgFeedClasses, $wgTitle, $wgSitename, $wgContLanguageCode; - + if( !isset( $wgFeedClasses[$feedFormat] ) ) { wfHttpError( 500, "Internal Server Error", "Unsupported feed type." ); return false; } - - $timekey = "$wgDBname:rcfeed:$feedFormat:timestamp"; - $key = "$wgDBname:rcfeed:$feedFormat:limit:$limit:minor:$hideminor"; - + + $timekey = wfMemcKey( 'rcfeed', $feedFormat, 'timestamp' ); + $key = wfMemcKey( 'rcfeed', $feedFormat, 'limit', $limit, 'minor', $hideminor ); + $feedTitle = $wgSitename . ' - ' . wfMsgForContent( 'recentchanges' ) . ' [' . $wgContLanguageCode . ']'; $feed = new $wgFeedClasses[$feedFormat]( $feedTitle, - htmlspecialchars( wfMsgForContent( 'recentchangestext' ) ), + htmlspecialchars( wfMsgForContent( 'recentchanges-feed-description' ) ), $wgTitle->getFullUrl() ); /** @@ -262,7 +338,7 @@ * gets it quick too. */ $cachedFeed = false; - if( $feedLastmod = $messageMemc->get( $timekey ) ) { + if( ( $wgFeedCacheTimeout > 0 ) && ( $feedLastmod = $messageMemc->get( $timekey ) ) ) { /** * If the cached feed was rendered very recently, we may * go ahead and use it even if there have been edits made @@ -289,7 +365,7 @@ rcDoOutputFeed( $rows, $feed ); $cachedFeed = ob_get_contents(); ob_end_flush(); - + $expire = 3600 * 24; # One day $messageMemc->set( $key, $cachedFeed ); $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire ); @@ -298,10 +374,11 @@ } function rcDoOutputFeed( $rows, &$feed ) { - global $wgSitename, $wgFeedClasses, $wgContLanguageCode; - + $fname = 'rcDoOutputFeed'; + wfProfileIn( $fname ); + $feed->outHeader(); - + # Merge adjacent edits by one user $sorted = array(); $n = 0; @@ -315,9 +392,8 @@ $sorted[$n] = $obj; $n++; } - $first = false; } - + foreach( $sorted as $obj ) { $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title ); $talkpage = $title->getTalkPage(); @@ -332,6 +408,7 @@ $feed->outItem( $item ); } $feed->outFooter(); + wfProfileOut( $fname ); } /** @@ -375,7 +452,14 @@ rcDaysLink( $limit, 14, $page, $more ) . ' | ' . rcDaysLink( $limit, 30, $page, $more ) . ( $doall ? ( ' | ' . rcDaysLink( $limit, 0, $page, $more ) ) : '' ); - $shm = wfMsg( 'showhideminor', $minorLink, $botLink, $liuLink, $patrLink, $myselfLink ); + + $linkParts = array( 'minorLink' => 'minor', 'botLink' => 'bots', 'liuLink' => 'liu', 'patrLink' => 'patr', 'myselfLink' => 'mine' ); + foreach( $linkParts as $linkVar => $linkMsg ) { + if( $$linkVar != '' ) + $links[] = wfMsgHtml( 'rcshowhide' . $linkMsg, $$linkVar ); + } + + $shm = implode( ' | ', $links ); $note = wfMsg( 'rclinks', $cl, $dl, $shm ); return $note; } @@ -383,46 +467,53 @@ /** * Makes change an option link which carries all the other options + * @param $title see Title + * @param $override + * @param $options */ function makeOptionsLink( $title, $override, $options ) { - global $wgUser, $wgLang, $wgContLang; + global $wgUser, $wgContLang; $sk = $wgUser->getSkin(); return $sk->makeKnownLink( $wgContLang->specialPage( 'Recentchanges' ), - $title, wfArrayToCGI( $override, $options ) ); + htmlspecialchars( $title ), wfArrayToCGI( $override, $options ) ); } /** - * Creates the options panel + * Creates the options panel. + * @param $defaults + * @param $nondefaults */ function rcOptionsPanel( $defaults, $nondefaults ) { - global $wgLang; + global $wgLang, $wgUseRCPatrol; $options = $nondefaults + $defaults; if( $options['from'] ) - $note = wfMsg( 'rcnotefrom', $wgLang->formatNum( $options['limit'] ), $wgLang->timeanddate( $options['from'], true ) ); + $note = wfMsgExt( 'rcnotefrom', array( 'parseinline' ), + $wgLang->formatNum( $options['limit'] ), + $wgLang->timeanddate( $options['from'], true ) ); else - $note = wfMsg( 'rcnote', $wgLang->formatNum( $options['limit'] ), $wgLang->formatNum( $options['days'] ) ); + $note = wfMsgExt( 'rcnote', array( 'parseinline' ), + $wgLang->formatNum( $options['limit'] ), + $wgLang->formatNum( $options['days'] ), + $wgLang->timeAndDate( wfTimestampNow(), true ) ); // limit links - $cl = ''; $options_limit = array(50, 100, 250, 500); - $i = 0; - while ( $i+1 < count($options_limit) ) { - $cl .= makeOptionsLink( $options_limit[$i], array( 'limit' => $options_limit[$i] ), $nondefaults) . ' | ' ; - $i++; + foreach( $options_limit as $value ) { + $cl[] = makeOptionsLink( $wgLang->formatNum( $value ), + array( 'limit' => $value ), $nondefaults) ; } - $cl .= makeOptionsLink( $options_limit[$i], array( 'limit' => $options_limit[$i] ), $nondefaults) ; + $cl = implode( ' | ', $cl); // day links, reset 'from' to none - $dl = ''; $options_days = array(1, 3, 7, 14, 30); - $i = 0; - while ( $i+1 < count($options_days) ) { - $dl .= makeOptionsLink( $options_days[$i], array( 'days' => $options_days[$i], 'from' => '' ), $nondefaults) . ' | ' ; - $i++; + foreach( $options_days as $value ) { + $dl[] = makeOptionsLink( $wgLang->formatNum( $value ), + array( 'days' => $value, 'from' => '' ), $nondefaults) ; } - $dl .= makeOptionsLink( $options_days[$i], array( 'days' => $options_days[$i], 'from' => '' ), $nondefaults) ; + $dl = implode( ' | ', $dl); + // show/hide links $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' )); @@ -430,44 +521,70 @@ array( 'hideminor' => 1-$options['hideminor'] ), $nondefaults); $botLink = makeOptionsLink( $showhide[1-$options['hidebots']], array( 'hidebots' => 1-$options['hidebots'] ), $nondefaults); + $anonsLink = makeOptionsLink( $showhide[ 1 - $options['hideanons'] ], + array( 'hideanons' => 1 - $options['hideanons'] ), $nondefaults ); $liuLink = makeOptionsLink( $showhide[1-$options['hideliu']], array( 'hideliu' => 1-$options['hideliu'] ), $nondefaults); $patrLink = makeOptionsLink( $showhide[1-$options['hidepatrolled']], array( 'hidepatrolled' => 1-$options['hidepatrolled'] ), $nondefaults); - $myselfLink = makeOptionsLink( $showhide[1-$options['hidemyself']], + $myselfLink = makeOptionsLink( $showhide[1-$options['hidemyself']], array( 'hidemyself' => 1-$options['hidemyself'] ), $nondefaults); - $hl = wfMsg( 'showhideminor', $minorLink, $botLink, $liuLink, $patrLink, $myselfLink ); - + + $links[] = wfMsgHtml( 'rcshowhideminor', $minorLink ); + $links[] = wfMsgHtml( 'rcshowhidebots', $botLink ); + $links[] = wfMsgHtml( 'rcshowhideanons', $anonsLink ); + $links[] = wfMsgHtml( 'rcshowhideliu', $liuLink ); + if( $wgUseRCPatrol ) + $links[] = wfMsgHtml( 'rcshowhidepatr', $patrLink ); + $links[] = wfMsgHtml( 'rcshowhidemine', $myselfLink ); + $hl = implode( ' | ', $links ); + // show from this onward link $now = $wgLang->timeanddate( wfTimestampNow(), true ); $tl = makeOptionsLink( $now, array( 'from' => wfTimestampNow()), $nondefaults ); - - $rclinks = wfMsg( 'rclinks', $cl, $dl, $hl ); - $rclistfrom = wfMsg( 'rclistfrom', $tl ); + + $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter'), + $cl, $dl, $hl ); + $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter'), $tl ); return "$note
    $rclinks
    $rclistfrom"; } -/** +/** * Creates the choose namespace selection * - * @access private + * @private * - * @param mixed $namespace The key of the currently selected namespace, empty string + * @param $namespace Mixed: the key of the currently selected namespace, empty string * if there is none - * @param bool $invert Whether to invert the namespace selection - * @param array $nondefaults An array of non default options to be remembered + * @param $invert Bool: whether to invert the namespace selection + * @param $nondefaults Array: an array of non default options to be remembered + * @param $categories_any Bool: Default value for the checkbox * * @return string */ -function rcNamespaceForm ( $namespace, $invert, $nondefaults ) { - global $wgContLang, $wgScript; - $t = Title::makeTitle( NS_SPECIAL, 'Recentchanges' ); +function rcNamespaceForm( $namespace, $invert, $nondefaults, $categories_any ) { + global $wgScript, $wgAllowCategorizedRecentChanges, $wgRequest; + $t = SpecialPage::getTitleFor( 'Recentchanges' ); $namespaceselect = HTMLnamespaceselector($namespace, ''); - $submitbutton = ''; + $submitbutton = '\n"; $invertbox = "'; - + + if ( $wgAllowCategorizedRecentChanges ) { + $categories = trim ( $wgRequest->getVal ( 'categories' , "" ) ) ; + $cb_arr = array( 'type' => 'checkbox', 'name' => 'categories_any', 'value' => "1" ) ; + if ( $categories_any ) $cb_arr['checked'] = "checked" ; + $catbox = "
    " ; + $catbox .= wfMsgExt('rc_categories', array('parseinline')) . " "; + $catbox .= wfElement('input', array( 'type' => 'text', 'name' => 'categories', 'value' => $categories)); + $catbox .= "  " ; + $catbox .= wfElement('input', $cb_arr ); + $catbox .= wfMsgExt('rc_categories_any', array('parseinline')); + } else { + $catbox = "" ; + } + $out = "
    \n"; foreach ( $nondefaults as $key => $value ) { @@ -479,8 +596,7 @@ $out .= "
    - $namespaceselect $submitbutton $invertbox -
    "; + {$namespaceselect}{$submitbutton}{$invertbox} {$catbox}\n
    "; $out .= ''; return $out; } @@ -490,62 +606,57 @@ * Format a diff for the newsfeed */ function rcFormatDiff( $row ) { + $titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title ); + $timestamp = wfTimestamp( TS_MW, $row->rc_timestamp ); + return rcFormatDiffRow( $titleObj, + $row->rc_last_oldid, $row->rc_this_oldid, + $timestamp, + $row->rc_comment ); +} + +function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment ) { + global $wgFeedDiffCutoff, $wgContLang, $wgUser; $fname = 'rcFormatDiff'; wfProfileIn( $fname ); - - require_once( 'DifferenceEngine.php' ); - $comment = "

    " . htmlspecialchars( $row->rc_comment ) . "

    \n"; - - if( $row->rc_namespace >= 0 ) { - global $wgContLang; - - #$diff =& new DifferenceEngine( $row->rc_this_oldid, $row->rc_last_oldid, $row->rc_id ); - #$diff->showDiffPage(); - - $titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title ); - $dbr =& wfGetDB( DB_SLAVE ); - $newrev =& Revision::newFromTitle( $titleObj, $row->rc_this_oldid ); - if( $newrev ) { - $newtext = $newrev->getText(); - } else { - $diffText = "

    Can't load revision $row->rc_this_oldid

    "; - wfProfileOut( $fname ); - return $comment . $diffText; - } - if( $row->rc_last_oldid ) { + $skin = $wgUser->getSkin(); + $completeText = '

    ' . $skin->formatComment( $comment ) . "

    \n"; + + if( $title->getNamespace() >= 0 && $title->userCan( 'read' ) ) { + if( $oldid ) { wfProfileIn( "$fname-dodiff" ); - $oldrev =& Revision::newFromId( $row->rc_last_oldid ); - if( !$oldrev ) { - $diffText = "

    Can't load old revision $row->rc_last_oldid

    "; - wfProfileOut( $fname ); - return $comment . $diffText; - } - $oldtext = $oldrev->getText(); + + $de = new DifferenceEngine( $title, $oldid, $newid ); + #$diffText = $de->getDiff( wfMsg( 'revisionasof', + # $wgContLang->timeanddate( $timestamp ) ), + # wfMsg( 'currentrev' ) ); + $diffText = $de->getDiff( + wfMsg( 'previousrevision' ), // hack + wfMsg( 'revisionasof', + $wgContLang->timeanddate( $timestamp ) ) ); - # Old entries may contain illegal characters - # which will damage output - $oldtext = UtfNormal::cleanUp( $oldtext ); - - global $wgFeedDiffCutoff; - if( strlen( $newtext ) > $wgFeedDiffCutoff || - strlen( $oldtext ) > $wgFeedDiffCutoff ) { - $diffLink = $titleObj->escapeFullUrl( - 'diff=' . $row->rc_this_oldid . - '&oldid=' . $row->rc_last_oldid ); + + if ( strlen( $diffText ) > $wgFeedDiffCutoff ) { + // Omit large diffs + $diffLink = $title->escapeFullUrl( + 'diff=' . $newid . + '&oldid=' . $oldid ); $diffText = '' . htmlspecialchars( wfMsgForContent( 'difference' ) ) . ''; + } elseif ( $diffText === false ) { + // Error in diff engine, probably a missing revision + $diffText = "

    Can't load revision $newid

    "; } else { - $diffText = DifferenceEngine::getDiff( $oldtext, $newtext, - wfMsg( 'revisionasof', $wgContLang->timeanddate( $row->rc_timestamp ) ), - wfMsg( 'currentrev' ) ); + // Diff output fine, clean up any illegal UTF-8 + $diffText = UtfNormal::cleanUp( $diffText ); + $diffText = rcApplyDiffStyle( $diffText ); } wfProfileOut( "$fname-dodiff" ); } else { - $rev = Revision::newFromId( $row->rc_this_oldid ); + $rev = Revision::newFromId( $newid ); if( is_null( $rev ) ) { $newtext = ''; } else { @@ -554,13 +665,39 @@ $diffText = '

    ' . wfMsg( 'newpage' ) . '

    ' . '
    ' . nl2br( htmlspecialchars( $newtext ) ) . '
    '; } - - wfProfileOut( $fname ); - return $comment . $diffText; + $completeText .= $diffText; } - + wfProfileOut( $fname ); - return $comment; + return $completeText; } -?> +/** + * Hacky application of diff styles for the feeds. + * Might be 'cleaner' to use DOM or XSLT or something, + * but *gack* it's a pain in the ass. + * + * @param $text String: + * @return string + * @private + */ +function rcApplyDiffStyle( $text ) { + $styles = array( + 'diff' => 'background-color: white; color:black;', + 'diff-otitle' => 'background-color: white; color:black;', + 'diff-ntitle' => 'background-color: white; color:black;', + 'diff-addedline' => 'background: #cfc; color:black; font-size: smaller;', + 'diff-deletedline' => 'background: #ffa; color:black; font-size: smaller;', + 'diff-context' => 'background: #eee; color:black; font-size: smaller;', + 'diffchange' => 'color: red; font-weight: bold; text-decoration: none;', + ); + + foreach( $styles as $class => $style ) { + $text = preg_replace( "/(<[^>]+)class=(['\"])$class\\2([^>]*>)/", + "\\1style=\"$style\"\\3", $text ); + } + + return $text; +} + + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialRecentchangeslinked.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialRecentchangeslinked.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialRecentchangeslinked.php 2005-08-25 00:32:20.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialRecentchangeslinked.php 2007-08-28 12:50:31.000000000 -0400 @@ -1,8 +1,7 @@ getInt( 'days' ); $target = isset($par) ? $par : $wgRequest->getText( 'target' ); $hideminor = $wgRequest->getBool( 'hideminor' ) ? 1 : 0; - + $wgOut->setPagetitle( wfMsg( 'recentchangeslinked' ) ); $sk = $wgUser->getSkin(); @@ -35,97 +34,137 @@ return; } $id = $nt->getArticleId(); - + + $wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $nt->getPrefixedText() ) ); $wgOut->setSubtitle( htmlspecialchars( wfMsg( 'rclsub', $nt->getPrefixedText() ) ) ); if ( ! $days ) { - $days = $wgUser->getOption( 'rcdays' ); - if ( ! $days ) { $days = 7; } + $days = (int)$wgUser->getOption( 'rcdays', 7 ); } - $days = (int)$days; - list( $limit, $offset ) = wfCheckLimits( 100, 'rclimit' ); + list( $limit, /* offset */ ) = wfCheckLimits( 100, 'rclimit' ); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE,'recentchangeslinked' ); $cutoff = $dbr->timestamp( time() - ( $days * 86400 ) ); $hideminor = ($hideminor ? 1 : 0); if ( $hideminor ) { $mlink = $sk->makeKnownLink( $wgContLang->specialPage( 'Recentchangeslinked' ), - WfMsg( 'show' ), 'target=' . htmlspecialchars( $nt->getPrefixedURL() ) . + wfMsg( 'show' ), 'target=' . htmlspecialchars( $nt->getPrefixedURL() ) . "&days={$days}&limit={$limit}&hideminor=0" ); } else { $mlink = $sk->makeKnownLink( $wgContLang->specialPage( "Recentchangeslinked" ), - WfMsg( "hide" ), "target=" . htmlspecialchars( $nt->getPrefixedURL() ) . + wfMsg( "hide" ), "target=" . htmlspecialchars( $nt->getPrefixedURL() ) . "&days={$days}&limit={$limit}&hideminor=1" ); } if ( $hideminor ) { - $cmq = 'AND rev_minor_edit=0'; + $cmq = 'AND rc_minor=0'; } else { $cmq = ''; } - extract( $dbr->tableNames( 'categorylinks', 'pagelinks', 'revision', 'page' ) ); - + list($recentchanges, $categorylinks, $pagelinks, $watchlist) = + $dbr->tableNamesN( 'recentchanges', 'categorylinks', 'pagelinks', "watchlist" ); + + $uid = $wgUser->getID(); + + $GROUPBY = " + GROUP BY rc_cur_id,rc_namespace,rc_title, + rc_user,rc_comment,rc_user_text,rc_timestamp,rc_minor,rc_deleted, + rc_new, rc_id, rc_this_oldid, rc_last_oldid, rc_bot, rc_patrolled, rc_type, rc_old_len, rc_new_len +" . ($uid ? ",wl_user" : "") . " + ORDER BY rc_timestamp DESC + LIMIT {$limit}"; + // If target is a Category, use categorylinks and invert from and to if( $nt->getNamespace() == NS_CATEGORY ) { $catkey = $dbr->addQuotes( $nt->getDBKey() ); - $sql = - "SELECT page_id,page_namespace,page_title,rev_id,rev_user,rev_comment, - rev_user_text,rev_timestamp,rev_minor_edit, - page_is_new - FROM $categorylinks, $revision, $page - WHERE rev_timestamp > '{$cutoff}' - {$cmq} - AND rev_page=page_id - AND cl_from=page_id - AND cl_to=$catkey -GROUP BY page_id,page_namespace,page_title, - rev_user,rev_comment,rev_user_text,rev_timestamp,rev_minor_edit, - page_is_new -ORDER BY rev_timestamp DESC - LIMIT {$limit}"; + $sql = "SELECT /* wfSpecialRecentchangeslinked */ + rc_id, + rc_cur_id, + rc_namespace, + rc_title, + rc_this_oldid, + rc_last_oldid, + rc_user, + rc_comment, + rc_user_text, + rc_timestamp, + rc_minor, + rc_bot, + rc_new, + rc_patrolled, + rc_type, + rc_old_len, + rc_new_len, + rc_deleted +" . ($uid ? ",wl_user" : "") . " + FROM $categorylinks, $recentchanges +" . ($uid ? "LEFT OUTER JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "") . " + WHERE rc_timestamp > '{$cutoff}' + {$cmq} + AND cl_from=rc_cur_id + AND cl_to=$catkey +$GROUPBY + "; } else { $sql = - "SELECT page_id,page_namespace,page_title, - rev_user,rev_comment,rev_user_text,rev_id,rev_timestamp,rev_minor_edit, - page_is_new - FROM $pagelinks, $revision, $page - WHERE rev_timestamp > '{$cutoff}' - {$cmq} - AND rev_page=page_id - AND pl_namespace=page_namespace - AND pl_title=page_title +"SELECT /* wfSpecialRecentchangeslinked */ + rc_id, + rc_cur_id, + rc_namespace, + rc_title, + rc_user, + rc_comment, + rc_user_text, + rc_this_oldid, + rc_last_oldid, + rc_timestamp, + rc_minor, + rc_bot, + rc_new, + rc_patrolled, + rc_type, + rc_old_len, + rc_new_len, + rc_deleted +" . ($uid ? ",wl_user" : "") . " + FROM $pagelinks, $recentchanges +" . ($uid ? " LEFT OUTER JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "") . " + WHERE rc_timestamp > '{$cutoff}' + {$cmq} + AND pl_namespace=rc_namespace + AND pl_title=rc_title AND pl_from=$id -GROUP BY page_id,page_namespace,page_title, - rev_user,rev_comment,rev_user_text,rev_timestamp,rev_minor_edit, - page_is_new -ORDER BY rev_timestamp DESC - LIMIT {$limit}"; +$GROUPBY +"; } $res = $dbr->query( $sql, $fname ); - $wgOut->addHTML("< ".$sk->makeKnownLinkObj($nt, "", "redirect=no" )."
    \n"); - $note = wfMsg( "rcnote", $limit, $days ); + $wgOut->addHTML("< ".$sk->makeLinkObj($nt, "", "redirect=no" )."
    \n"); + $note = wfMsgExt( "rcnote", array ( 'parseinline' ), $limit, $days, $wgLang->timeAndDate( wfTimestampNow(), true ) ); $wgOut->addHTML( "
    \n{$note}\n
    " ); $note = rcDayLimitlinks( $days, $limit, "Recentchangeslinked", - "target=" . $nt->getPrefixedURL() . "&hideminor={$hideminor}", - false, $mlink ); + "target=" . $nt->getPrefixedURL() . "&hideminor={$hideminor}", + false, $mlink ); $wgOut->addHTML( $note."\n" ); - $list =& new ChangesList( $sk ); + $list = ChangesList::newFromUser( $wgUser ); $s = $list->beginRecentChangesList(); $count = $dbr->numRows( $res ); - - $counter = 1; - while ( $limit ) { - if ( 0 == $count ) { break; } - $obj = $dbr->fetchObject( $res ); - --$count; - - $rc = RecentChange::newFromCurRow( $obj ); - $rc->counter = $counter++; - $s .= $list->recentChangesLine( $rc ); - --$limit; + + if ( $count ) { + $counter = 1; + while ( $limit ) { + if ( 0 == $count ) { break; } + $obj = $dbr->fetchObject( $res ); + --$count; + $rc = RecentChange::newFromRow( $obj ); + $rc->counter = $counter++; + $s .= $list->recentChangesLine( $rc , !empty( $obj->wl_user) ); + --$limit; + } + } else { + $wgOut->addWikiText( wfMsg('recentchangeslinked-noresult') ); } $s .= $list->endRecentChangesList(); @@ -133,4 +172,4 @@ $wgOut->addHTML( $s ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialSearch.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialSearch.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialSearch.php 2005-10-13 05:11:09.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialSearch.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,40 +1,35 @@ # http://www.mediawiki.org/ -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** * Run text & title search and display the output - * @package MediaWiki - * @subpackage SpecialPage + * @addtogroup SpecialPage */ -/** */ -require_once( 'SearchEngine.php' ); -require_once( 'Revision.php' ); - /** * Entry point * - * @param string $par (default '') + * @param $par String: (default '') */ function wfSpecialSearch( $par = '' ) { global $wgRequest, $wgUser; - + $search = $wgRequest->getText( 'search', $par ); $searchPage = new SpecialSearch( $wgRequest, $wgUser ); if( $wgRequest->getVal( 'fulltext' ) || @@ -47,9 +42,8 @@ } /** - * @todo document - * @package MediaWiki - * @subpackage SpecialPage + * implements Special:Search - Run text & title search and display the output + * @addtogroup SpecialPage */ class SpecialSearch { @@ -59,37 +53,36 @@ * * @param WebRequest $request * @param User $user - * @access public + * @public */ function SpecialSearch( &$request, &$user ) { list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' ); - + if( $request->getCheck( 'searchx' ) ) { $this->namespaces = $this->powerSearch( $request ); } else { $this->namespaces = $this->userNamespaces( $user ); } - + $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false; } - + /** - * If an exact title match can be found, jump straight ahead to + * If an exact title match can be found, jump straight ahead to it. * @param string $term - * @access public + * @public */ function goResult( $term ) { global $wgOut; global $wgGoToEdit; - + $this->setupPage( $term ); # Try to go to page as entered. - # $t = Title::newFromText( $term ); # If the string cannot be used to create a title - if( is_null( $t ) ){ + if( is_null( $t ) ){ return $this->showResults( $term ); } @@ -99,39 +92,35 @@ $wgOut->redirect( $t->getFullURL() ); return; } - + # No match, generate an edit URL $t = Title::newFromText( $term ); - if( is_null( $t ) ) { - $editurl = ''; # hrm... - } else { + if( ! is_null( $t ) ) { + wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) ); # If the feature is enabled, go straight to the edit page if ( $wgGoToEdit ) { $wgOut->redirect( $t->getFullURL( 'action=edit' ) ); return; - } else { - $editurl = $t->escapeLocalURL( 'action=edit' ); - } + } } - $wgOut->addWikiText( wfMsg('nogomatch', ":$term" ) ); + $wgOut->addWikiText( wfMsg( 'noexactmatch', wfEscapeWikiText( $term ) ) ); return $this->showResults( $term ); } - + /** * @param string $term - * @access public + * @public */ function showResults( $term ) { $fname = 'SpecialSearch::showResults'; wfProfileIn( $fname ); - + $this->setupPage( $term ); - - global $wgUser, $wgOut; - $sk = $wgUser->getSkin(); + + global $wgOut; $wgOut->addWikiText( wfMsg( 'searchresulttext' ) ); - + #if ( !$this->parseQuery() ) { if( '' === trim( $term ) ) { $wgOut->setSubtitle( '' ); @@ -139,7 +128,7 @@ wfProfileOut( $fname ); return; } - + global $wgDisableTextSearch; if ( $wgDisableTextSearch ) { global $wgForwardSearchUrl; @@ -154,7 +143,7 @@ wfMsg( 'googlesearch', htmlspecialchars( $term ), htmlspecialchars( $wgInputEncoding ), - htmlspecialchars( wfMsg( 'search' ) ) + htmlspecialchars( wfMsg( 'searchbutton' ) ) ) ); wfProfileOut( $fname ); @@ -167,22 +156,25 @@ $search->showRedirects = $this->searchRedirects; $titleMatches = $search->searchTitle( $term ); $textMatches = $search->searchText( $term ); - + $num = ( $titleMatches ? $titleMatches->numRows() : 0 ) + ( $textMatches ? $textMatches->numRows() : 0); - if ( $num >= $this->limit ) { - $top = wfShowingResults( $this->offset, $this->limit ); - } else { - $top = wfShowingResultsNum( $this->offset, $this->limit, $num ); + if ( $num > 0 ) { + if ( $num >= $this->limit ) { + $top = wfShowingResults( $this->offset, $this->limit ); + } else { + $top = wfShowingResultsNum( $this->offset, $this->limit, $num ); + } + $wgOut->addHTML( "

    {$top}

    \n" ); } - $wgOut->addHTML( "

    {$top}

    \n" ); if( $num || $this->offset ) { $prevnext = wfViewPrevNext( $this->offset, $this->limit, - 'Special:Search', + SpecialPage::getTitleFor( 'Search' ), wfArrayToCGI( $this->powerSearchOptions(), - array( 'search' => $term ) ) ); + array( 'search' => $term ) ), + ($num < $this->limit) ); $wgOut->addHTML( "
    {$prevnext}\n" ); } @@ -193,8 +185,9 @@ } else { $wgOut->addWikiText( '==' . wfMsg( 'notitlematches' ) . "==\n" ); } + $titleMatches->free(); } - + if( $textMatches ) { if( $textMatches->numRows() ) { $wgOut->addWikiText( '==' . wfMsg( 'textmatches' ) . "==\n" ); @@ -203,8 +196,9 @@ # Don't show the 'no text matches' if we received title matches $wgOut->addWikiText( '==' . wfMsg( 'notextmatches' ) . "==\n" ); } + $textMatches->free(); } - + if ( $num == 0 ) { $wgOut->addWikiText( wfMsg( 'nonefound' ) ); } @@ -214,28 +208,29 @@ $wgOut->addHTML( $this->powerSearchBox( $term ) ); wfProfileOut( $fname ); } - + #------------------------------------------------------------------ # Private methods below this line - + /** - * + * */ function setupPage( $term ) { global $wgOut; $wgOut->setPageTitle( wfMsg( 'searchresults' ) ); - $wgOut->setSubtitle( htmlspecialchars( wfMsg( 'searchquery', $term ) ) ); + $subtitlemsg = ( Title::newFromText($term) ? 'searchsubtitle' : 'searchsubtitleinvalid' ); + $wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ) ); $wgOut->setArticleRelated( false ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); } - + /** * Extract default namespaces to search from the given user's * settings, returning a list of index numbers. * * @param User $user * @return array - * @access private + * @private */ function userNamespaces( &$user ) { $arr = array(); @@ -246,14 +241,14 @@ } return $arr; } - + /** * Extract "power search" namespace settings from the request object, * returning a list of index numbers to search. * * @param WebRequest $request * @return array - * @access private + * @private */ function powerSearch( &$request ) { $arr = array(); @@ -264,11 +259,11 @@ } return $arr; } - + /** * Reconstruct the 'power search' options for links * @return array - * @access private + * @private */ function powerSearchOptions() { $opt = array(); @@ -279,7 +274,7 @@ $opt['searchx'] = 1; return $opt; } - + /** * @param SearchResultSet $matches * @param string $terms partial regexp for highlighting terms @@ -287,12 +282,11 @@ function showMatches( &$matches ) { $fname = 'SpecialSearch::showMatches'; wfProfileIn( $fname ); - + global $wgContLang; $tm = $wgContLang->convertForSearchResult( $matches->termMatches() ); $terms = implode( '|', $tm ); - - global $wgOut; + $off = $this->offset + 1; $out = "
      \n"; @@ -307,7 +301,7 @@ wfProfileOut( $fname ); return $out; } - + /** * Format a single hit result * @param SearchResult $result @@ -316,32 +310,39 @@ function showHit( $result, $terms ) { $fname = 'SpecialSearch::showHit'; wfProfileIn( $fname ); - global $wgUser, $wgContLang; + global $wgUser, $wgContLang, $wgLang; $t = $result->getTitle(); if( is_null( $t ) ) { wfProfileOut( $fname ); return "\n"; } - $sk =& $wgUser->getSkin(); + $sk = $wgUser->getSkin(); - $contextlines = $wgUser->getOption( 'contextlines' ); - if ( '' == $contextlines ) { $contextlines = 5; } - $contextchars = $wgUser->getOption( 'contextchars' ); - if ( '' == $contextchars ) { $contextchars = 50; } + $contextlines = $wgUser->getOption( 'contextlines', 5 ); + $contextchars = $wgUser->getOption( 'contextchars', 50 ); $link = $sk->makeKnownLinkObj( $t ); + + //If page content is not readable, just return the title. + //This is not quite safe, but better than showing excerpts from non-readable pages + //Note that hiding the entry entirely would screw up paging. + if (!$t->userCanRead()) { + return "
    1. {$link}
    2. \n"; + } + $revision = Revision::newFromTitle( $t ); $text = $revision->getText(); - $size = wfMsg( 'nbytes', strlen( $text ) ); + $size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'), + $wgLang->formatNum( strlen( $text ) ) ); $lines = explode( "\n", $text ); - $max = IntVal( $contextchars ) + 1; + $max = intval( $contextchars ) + 1; $pat1 = "/(.*)($terms)(.{0,$max})/i"; $lineno = 0; - + $extract = ''; wfProfileIn( "$fname-extract" ); foreach ( $lines as $line ) { @@ -349,6 +350,7 @@ break; } ++$lineno; + $m = array(); if ( ! preg_match( $pat1, $line, $m ) ) { continue; } @@ -374,7 +376,7 @@ wfProfileOut( $fname ); return "
    3. {$link} ({$size}){$extract}
    4. \n"; } - + function powerSearchBox( $term ) { $namespaces = ''; foreach( SearchEngine::searchableNamespaces() as $ns => $name ) { @@ -388,28 +390,28 @@ $namespaces .= " \n"; } - + $checked = $this->searchRedirects ? ' checked="checked"' : ''; $redirect = "\n"; - - $searchField = "\n"; - + + $searchField = '\n"; + $searchButton = '\n"; - + $ret = wfMsg( 'powersearchtext', $namespaces, $redirect, $searchField, '', '', '', '', '', # Dummy placeholders $searchButton ); - - $title = Title::makeTitle( NS_SPECIAL, 'Search' ); + + $title = SpecialPage::getTitleFor( 'Search' ); $action = $title->escapeLocalURL(); return "

      \n
      \n{$ret}\n
      \n"; } } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialShortpages.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialShortpages.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialShortpages.php 2005-03-12 06:51:01.000000000 -0500 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialShortpages.php 2007-06-28 21:19:14.000000000 -0400 @@ -1,20 +1,13 @@ tableName( 'page' ); $name = $dbr->addQuotes( $this->getName() ); - + + $forceindex = $dbr->useIndexClause("page_len"); return "SELECT $name as type, - page_namespace as namespace, + page_namespace as namespace, page_title as title, page_len AS value - FROM $page + FROM $page $forceindex WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0"; } - + + function preprocessResults( &$db, &$res ) { + # There's no point doing a batch check if we aren't caching results; + # the page must exist for it to have been pulled out of the table + if( $this->isCached() ) { + $batch = new LinkBatch(); + while( $row = $db->fetchObject( $res ) ) + $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) ); + $batch->execute(); + if( $db->numRows( $res ) > 0 ) + $db->dataSeek( $res, 0 ); + } + } + function sortDescending() { return false; } function formatResult( $skin, $result ) { global $wgLang, $wgContLang; - $nb = htmlspecialchars( wfMsg( "nbytes", $wgLang->formatNum( $result->value ) ) ); - $title = Title::makeTitle( $result->namespace, $result->title ); - $link = $skin->makeKnownLinkObj( $title, $wgContLang->convert( $title->getPrefixedText() ) ); - return "{$link} ({$nb})"; + $dm = $wgContLang->getDirMark(); + + $title = Title::makeTitleSafe( $result->namespace, $result->title ); + if ( !$title ) { + return ''; + } + $hlink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' ); + $plink = $this->isCached() + ? $skin->makeLinkObj( $title ) + : $skin->makeKnownLinkObj( $title ); + $size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( htmlspecialchars( $result->value ) ) ); + + return $title->exists() + ? "({$hlink}) {$dm}{$plink} {$dm}[{$size}]" + : "({$hlink}) {$dm}{$plink} {$dm}[{$size}]"; } } @@ -71,4 +89,4 @@ return $spp->doQuery( $offset, $limit ); } -?> + diff -urN mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialSpecialpages.php mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialSpecialpages.php --- mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.5.8/includes/SpecialSpecialpages.php 2005-05-02 08:17:28.000000000 -0400 +++ mediawiki-1.5.8-to-1.11.0.proposal/mediawiki-1.11.0/includes/SpecialSpecialpages.php 2007-08-15 06:50:09.000000000 -0400 @@ -1,37 +1,25 @@ loadAllMessages(); + $wgOut->setRobotpolicy( 'index,nofollow' ); - $sk = $wgUser->getSkin(); - - # Get listable pages, in a 2-d array with the first dimension being user right - $pages = SpecialPage::getPages(); + $sk = $wgUser->getSkin(); /** Pages available to all */ - wfSpecialSpecialpages_gen($pages[''],'spheading',$sk); + wfSpecialSpecialpages_gen( SpecialPage::getRegularPages(), 'spheading', $sk ); /** Restricted special pages */ - $rpages = array(); - foreach($wgAvailableRights as $right) { - /** only show pages a user can access */ - if( $wgUser->isAllowed($right) ) { - /** some rights might not have any special page associated */ - if(isset($pages[$right])) { - $rpages = array_merge( $rpages, $pages[$right] ); - } - } - } - wfSpecialSpecialpages_gen( $rpages, 'restrictedpheading', $sk ); + wfSpecialSpecialpages_gen( SpecialPage::getRestrictedPages(), 'restrictedpheading', $sk ); } /** @@ -41,33 +29,33 @@ * @param $sk skin object ??? */ function wfSpecialSpecialpages_gen($pages,$heading,$sk) { - global $wgLang, $wgOut, $wgSortSpecialPages; + global $wgOut, $wgSortSpecialPages; if( count( $pages ) == 0 ) { # Yeah, that was pointless. Thanks for coming. return; } - + /** Put them into a sortable array */ $sortedPages = array(); - foreach ( $pages as $name => $page ) { + foreach ( $pages as $page ) { if ( $page->isListed() ) { $sortedPages[$page->getDescription()] = $page->getTitle(); } } - + /** Sort */ if ( $wgSortSpecialPages ) { ksort( $sortedPages ); } /** Now output the HTML */ - $wgOut->addHTML( '

      ' . wfMsg( $heading ) . "

      \n