From: IAlex Date: Sun, 21 Oct 2012 17:10:38 +0000 (+0000) Subject: Merge "CologneBlue rewrite: fix talkLink() to use generic nav links" X-Git-Tag: 1.31.0-rc.0~21923 X-Git-Url: http://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/operations/recherche.php?a=commitdiff_plain;h=4f53553527eb78196537424c21490886a2122148;hp=4329cd87f75a5d71ddd2ef3b3d90bf13e0185716;p=lhc%2Fweb%2Fwiklou.git Merge "CologneBlue rewrite: fix talkLink() to use generic nav links" --- diff --git a/.gitignore b/.gitignore index 0be75c5ac1..319f19640e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ cache images/[0-9a-f] images/archive images/deleted +images/lockdir images/temp images/thumb ## Extension:EasyTimeline diff --git a/INSTALL b/INSTALL index c4bb8be987..e393631d5c 100644 --- a/INSTALL +++ b/INSTALL @@ -18,7 +18,7 @@ work on Windows as well. If your PHP is configured as a CGI plug-in rather than an Apache module you may experience problems, as this configuration is not well tested. safe_mode is also -not tested and unlikely to work. +not tested and unlikely to work. If you want math support see the instructions in math/README @@ -34,7 +34,7 @@ http://www.mediawiki.org/wiki/Manual:Installation_guide ******************* WARNING ******************* -REMEMBER: ALWAYS BACK UP YOUR DATABASE BEFORE +REMEMBER: ALWAYS BACK UP YOUR DATABASE BEFORE ATTEMPTING TO INSTALL OR UPGRADE!!! ******************* WARNING ******************* diff --git a/README b/README index 805b8ee411..c9ce5db474 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -For system requirements, installation and upgrade details, see the files +For system requirements, installation and upgrade details, see the files RELEASE-NOTES, INSTALL, and UPGRADE. == MediaWiki == @@ -62,7 +62,7 @@ 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. -MediaWiki makes use of the Sajax Toolkit by modernmethod, +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 diff --git a/RELEASE-NOTES-1.20 b/RELEASE-NOTES-1.20 index 66cca2ecc4..52c4e8685c 100644 --- a/RELEASE-NOTES-1.20 +++ b/RELEASE-NOTES-1.20 @@ -67,7 +67,7 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki. * (bug 35685) api.php URL and other entry point URLs are now listed on Special:Version * Edit notices can now be translated. -* jQuery upgraded to 1.8.1 +* jQuery upgraded to 1.8.2. * jQuery UI upgraded to 1.8.23. * QUnit upgraded from v1.2.0 to v1.10.0. * (bug 37604) jquery.cookie upgraded to 2011 version. @@ -247,6 +247,7 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki. * (bug 39635) PostgreSQL LOCK IN SHARE MODE option is a syntax error. * (bug 36329) Accesskey tooltips for Firefox 14 on Mac should use "ctrl-option-" prefix. * (bug 32552) Drop unused database field cat_hidden from table category. +* (bug 24502) Do not allow multiple language links to the same language. * (bug 40214) Category pages no longer use deprecated "width" HTML attribute. * (bug 39941) Add missing stylesheets to the installer pages * In HTML5 mode, allow new input element types values (such as color, range..) @@ -259,6 +260,8 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki. * (bug 31676) ResourceLoader should work around IE stylesheet limit. * (bug 40498) ResourceLoader should not output an empty "@media print { }" block. * (bug 40500) ResourceLoader should not ignore media-type for urls in debug mode. +* (bug 40660) ResourceLoaderWikiModule should not convert " " to a space + for pages from the MediaWiki-namespace. === API changes in 1.20 === * (bug 34316) Add ability to retrieve maximum upload size from MediaWiki API. @@ -295,8 +298,8 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki. * (bug 38904) prop=revisions&rvstart=... no longer blows up when continuing. * (bug 39032) ApiQuery generates help in constructor. * (bug 11142) Improve file extension blacklist error reporting in API upload. -* (bug 39665) Cache AllowedGenerator array so it doesn't autoload all query classes - on every request. +* (bug 39665) List of query generators is now not built using reflection, instead it is + defined in code. === Languages updated in 1.20 === diff --git a/RELEASE-NOTES-1.21 b/RELEASE-NOTES-1.21 index 50c3b2d83e..8eb6a7612f 100644 --- a/RELEASE-NOTES-1.21 +++ b/RELEASE-NOTES-1.21 @@ -1,7 +1,7 @@ = 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. +Security reminder: MediaWiki does not require PHP's register_globals. If you +have it on, turn it '''off''' if you can. == MediaWiki 1.21 == @@ -12,31 +12,60 @@ production. === Configuration changes in 1.21 === * (bug 29374) $wgVectorUseSimpleSearch is now enabled by default. +* Deprecated $wgAllowRealName is removed. Use $wgHiddenPrefs[] = 'realname' + instead. === New features in 1.21 === +* (bug 34876) jquery.makeCollapsible has been improved in performance. +* Added ContentHandler facility to allow extensions to support other content than wikitext. + See docs/contenthandler.txt for details. +* New feature was developed for showing high-DPI thumbnails for high-DPI mobile + and desktop displays (configurable with $wgResponsiveImages). +* Added new backend to represent and store information about sites and site + specific configuration. +* jQuery UI upgraded from 1.8.23 to 1.8.24. +* Added separate fa_sha1 field to filearchive table. This allows sha1 + searches with the api in miser mode for deleted files. +* Add initial and programmatic sorting for tablesorter. +* Add the event "sortEnd.tablesorter", triggered after sorting has completed. +* The Job system was refactored to allow for different backing stores for queues + as well as cross-wiki access to queues, among other things. The schema for the + DB queue was changed to support better concurrency and reduce deadlock errors. +* Added ApiQueryORM class to facilitate creation of query API modules based on + tables that have a corresponding ORMTable class. +* (bug 40876) Icon for PSD (Adobe Photoshop) file types. === Bug fixes in 1.21 === * (bug 40353) SpecialDoubleRedirect should support interwiki redirects. * (bug 40352) fixDoubleRedirects.php should support interwiki redirects. * (bug 9237) SpecialBrokenRedirect should not list interwiki redirects. -* (bug 34960) Drop unused fields rc_moved_to_ns and rc_moved_to_title from recentchanges table. +* (bug 34960) Drop unused fields rc_moved_to_ns and rc_moved_to_title from + recentchanges table. * (bug 32951) Do not register internal externals with absolute protocol, when server has relative protocol. +* (bug 39005) When purging proxies listed in $wgSquidServers using HTTP PURGE + method requests, we now send a Host header by default, for Varnish + compatibility. This also works with Squid in reverse-proxy mode. If you wish + to support Squid configured in forward-proxy mode, set + $wgSquidPurgeUseHostHeader to false. === API changes in 1.21 === +* prop=revisions can now report the contentmodel and contentformat, see docs/contenthandler.txt +* action=edit and action=parse now support contentmodel and contentformat parameters to control the interpretation of + page content; See docs/contenthandler.txt for details. * (bug 35693) ApiQueryImageInfo now suppresses errors when unserializing metadata. +* (bug 40111) Disable minor edit for page/section creation by API === Languages updated in 1.21 === MediaWiki supports over 350 languages. Many localisations are updated regularly. Below only new and removed languages are listed, as well as changes to languages because of Bugzilla reports. - === Other changes in 1.21 === == Compatibility == -MediaWiki 1.21 requires PHP 5.3.2. PHP 4 is no longer supported. +MediaWiki 1.21 requires PHP 5.3.2 or later. MySQL is the recommended DBMS. PostgreSQL or SQLite can also be used, but support for them is somewhat less mature. There is experimental support for IBM @@ -52,7 +81,9 @@ The supported versions are: == Upgrading == 1.21 has several database changes since 1.20, and will not work without schema -updates. +updates. Note that due to changes to some very large tables like the revision +table, the schema update may take quite long (minutes on a medium sized site, +many hours on a large site). If upgrading from before 1.11, and you are using a wiki as a commons repository, make sure that it is updated as well. Otherwise, errors may arise diff --git a/StartProfiler.sample b/StartProfiler.sample index 6bce634d4e..ba8fe8bbac 100644 --- a/StartProfiler.sample +++ b/StartProfiler.sample @@ -12,7 +12,7 @@ * } else { * $wgProfiler['class'] = 'ProfilerStub'; * } - * + * * Configuration of the profiler output can be done in LocalSettings.php */ diff --git a/UPGRADE b/UPGRADE index cdaf4f919e..46f5e65aa5 100644 --- a/UPGRADE +++ b/UPGRADE @@ -53,8 +53,8 @@ deleted file archives, and any custom skins. ==== From the web ==== -If you browse to the web-based installation script (usually at -/mw-config/index.php) from your wiki installation you can follow the script and +If you browse to the web-based installation script (usually at +/mw-config/index.php) from your wiki installation you can follow the script and upgrade your database in place. ==== From the command line ==== @@ -141,7 +141,7 @@ 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: +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, diff --git a/bin/ulimit4.sh b/bin/ulimit4.sh old mode 100755 new mode 100644 diff --git a/docs/contenthandler.txt b/docs/contenthandler.txt new file mode 100644 index 0000000000..899554af57 --- /dev/null +++ b/docs/contenthandler.txt @@ -0,0 +1,184 @@ +The ContentHandler facility adds support for arbitrary content types on wiki pages, instead of relying on wikitext +for everything. It was introduced in MediaWiki 1.21. + +Each kind of content ("content model") supported by MediaWiki is identified by unique name. The content model determines +how a page's content is rendered, compared, stored, edited, and so on. + +Built-in content types are: + +* wikitext - wikitext, as usual +* javascript - user provided javascript code +* css - user provided css code +* text - plain text + +In PHP, use the corresponding CONTENT_MODEL_XXX constant. + +A page's content model is available using the Title::getContentModel() method. A page's default model is determined by +ContentHandler::getDefaultModelFor($title) as follows: + +* The global setting $wgNamespaceContentModels specifies a content model for the given namespace. +* The hook ContentHandlerDefaultModelFor may be used to override the page's default model. +* Pages in NS_MEDIAWIKI and NS_USER default to the CSS or JavaScript model if they end in .js or .css, respectively. + Pages in NS_MEDIAWIKI default to the wikitext model otherwise. +* The hook TitleIsCssOrJsPage may be used to force a page to use the CSS or JavaScript model. + This is a compatibility feature. The ContentHandlerDefaultModelFor hook should be used instead if possible. +* The hook TitleIsWikitextPage may be used to force a page to use the wikitext model. + This is a compatibility feature. The ContentHandlerDefaultModelFor hook should be used instead if possible. +* Otherwise, the wikitext model is used. + +Note that is currently no mechanism to convert a page from one content model to another, and there is no guarantee that +revisions of a page will all have the same content model. Use Revision::getContentModel() to find it. + + +== Architecture == + +Two class hierarchies are used to provide the functionality associated with the different content models: + +* Content interface (and AbstractContent base class) define functionality that acts on the concrete content of a page, and +* ContentHandler base class provides functionality specific to a content model, but not acting on concrete content. + +The most important function of ContentHandler is to act as a factory for the appropriate implementation of Content. These +Content objects are to be used by MediaWiki everywhere, instead of passing page content around as text. All manipulation +and analysis of page content must be done via the appropriate methods of the Content object. + +For each content model, a subclass of ContentHandler has to be registered with $wgContentHandlers. The ContentHandler +object for a given content model can be obtained using ContentHandler::getForModelID( $id ). Also Title, WikiPage and +Revision now have getContentHandler() methods for convenience. + +ContentHandler objects are singletons that provide functionality specific to the content type, but not directly acting +on the content of some page. ContentHandler::makeEmptyContent() and ContentHandler::unserializeContent() can be used to +create a Content object of the appropriate type. However, it is recommended to instead use WikiPage::getContent() resp. +Revision::getContent() to get a page's content as a Content object. These two methods should be the ONLY way in which +page content is accessed. + +Another important function of ContentHandler objects is to define custom action handlers for a content model, see +ContentHandler::getActionOverrides(). This is similar to what WikiPage::getActionOverrides() was already doing. + + +== Serialization == + +With the ContentHandler facility, page content no longer has to be text based. Objects implementing the Content interface +are used to represent and handle the content internally. For storage and data exchange, each content model supports +at least one serialization format via ContentHandler::serializeContent( $content ). The list of supported formats for +a given content model can be accessed using ContentHandler::getSupportedFormats(). + +Content serialization formats are identified using MIME type like strings. The following formats are built in: + +* text/x-wiki - wikitext +* text/javascript - for js pages +* text/css - for css pages +* text/plain - for future use, e.g. with plain text messages. +* text/html - for future use, e.g. with plain html messages. +* application/vnd.php.serialized - for future use with the api and for extensions +* application/json - for future use with the api, and for use by extensions +* application/xml - for future use with the api, and for use by extensions + +In PHP, use the corresponding CONTENT_FORMAT_XXX constant. + +Note that when using the API to access page content, especially action=edit, action=parse and action=query&prop=revisions, +the model and format of the content should always be handled explicitly. Without that information, interpretation of +the provided content is not reliable. The same applies to XML dumps generated via maintenance/dumpBackup.php or +Special:Export. + +Also note that the API will provide encapsulated, serialized content - so if the API was called with format=json, and +contentformat is also json (or rather, application/json), the page content is represented as a string containing an +escaped json structure. Extensions that use JSON to serialize some types of page content may provide specialized API +modules that allow access to that content in a more natural form. + + +== Compatibility == + +The ContentHandler facility is introduced in a way that should allow all existing code to keep functioning at least +for pages that contain wikitext or other text based content. However, a number of functions and hooks have been +deprecated in favor of new versions that are aware of the page's content model, and will now generate warnings when +used. + +Most importantly, the following functions have been deprecated: + +* Revisions::getText() and Revisions::getRawText() is deprecated in favor Revisions::getContent() +* WikiPage::getText() is deprecated in favor WikiPage::getContent() + +Also, the old Article::getContent() (which returns text) is superceded by Article::getContentObject(). However, both +methods should be avoided since they do not provide clean access to the page's actual content. For instance, they may +return a system message for non-existing pages. Use WikiPage::getContent() instead. + +Code that relies on a textual representation of the page content should eventually be rewritten. However, +ContentHandler::getContentText() provides a stop-gap that can be used to get text for a page. Its behavior is controlled +by $wgContentHandlerTextFallback; per default it will return the text for text based content, and null for any other +content. + +For rendering page content, Content::getParserOutput() should be used instead of accessing the parser directly. +ContentHandler::makeParserOptions() can be used to construct appropriate options. + + +Besides some functions, some hooks have also been replaced by new versions (see hooks.txt for details). +These hooks will now trigger a warning when used: + +* ArticleAfterFetchContent was replaced by ArticleAfterFetchContentObject +* ArticleInsertComplete was replaced by PageContentInsertComplete +* ArticleSave was replaced by PageContentSave +* ArticleSaveComplete was replaced by PageContentSaveComplete +* ArticleViewCustom was replaced by ArticleContentViewCustom (also consider a custom implementation of the view action) +* EditFilterMerged was replaced by EditFilterMergedContent +* EditPageGetDiffText was replaced by EditPageGetDiffContent +* EditPageGetPreviewText was replaced by EditPageGetPreviewContent +* ShowRawCssJs was deprecated in favor of custom rendering implemented in the respective ContentHandler object. + + +== Database Storage == + +Page content is stored in the database using the same mechanism as before. Non-text content is serialized first. The +appropriate serialization and deserialization is handled by the Revision class. + +Each revision's content model and serialization format is stored in the revision table (resp. in the archive table, if +the revision was deleted). The page's (current) content model (that is, the content model of the latest revision) is also +stored in the page table. + +Note however that the content model and format is only stored if it differs from the page's default, as determined by +ContentHandler::getDefaultModelFor( $title ). The default values are represented as NULL in the database, to preserve +space. + +Storage of content model and format can be disabled altogether by setting $wgContentHandlerUseDB = false. In that case, +the page's default model (and the model's default format) will be used everywhere. Attempts to store a revision of a page +using a model or format different from the default will result in an error. + + +== Globals == + +There are some new globals that can be used to control the behavior of the ContentHandler facility: + +* $wgContentHandlers associates content model IDs with the names of the appropriate ContentHandler subclasses. + +* $wgNamespaceContentModels maps namespace IDs to a content model that should be the default for that namespace. + +* $wgContentHandlerUseDB determines whether each revision's content model should be stored in the database. + Defaults is true. + +* $wgContentHandlerTextFallback determines how the compatibility method ContentHandler::getContentText() will behave for + non-text content: + 'ignore' causes null to be returned for non-text content (default). + 'serialize' causes the serialized form of any non-text content to be returned (scary). + 'fail' causes an exception to be thrown for non-text content (strict). + + +== Caveats == + +There are some changes in behavior that might be surprising to users: + +* Javascript and CSS pages are no longer parsed as wikitext (though pre-save transform is still applied). Most +importantly, this means that links, including categorization links, contained in the code will not work. + +* With $wgContentHandlerUseDB = false, pages can not be moved in a way that would change the +default model. E.g. [[MediaWiki:foo.js]] can not be moved to [[MediaWiki:foo bar]], but can still be moved to +[[User:John/foo.js]]. Also, in this mode, changing the default content model for a page (e.g. by changing +$wgNamespaceContentModels) may cause it to become inaccessible. + +* action=edit will fail for pages with non-text content, unless the respective ContentHandler implementation has +provided a specialized handler for the edit action. This is true for the API as well. + +* action=raw will fail for all non-text content. This seems better than serving content in other formats to an +unsuspecting recipient. This will also cause client-side diffs to fail. + +* File pages provide their own action overrides that do not combine gracefully with any custom handlers defined by a +ContentHandler. If for example a File page used a content model with a custom revert action, this would be overridden by +WikiFilePage's handler for the revert action. diff --git a/docs/export-0.8.xsd b/docs/export-0.8.xsd new file mode 100644 index 0000000000..a18c608e58 --- /dev/null +++ b/docs/export-0.8.xsd @@ -0,0 +1,289 @@ + + + + + + + MediaWiki's page export format + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/hooks.txt b/docs/hooks.txt index 96757cde15..46ddcfea7e 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -283,6 +283,11 @@ $article: Article object $user: the User object that was created. (Parameter added in 1.7) $byEmail: true when account was created "by email" (added in 1.12) +'AfterFinalPageOutput': At the end of OutputPage::output() but before +final ob_end_flush() which will send the buffered output to the client. +This allows for last-minute modification of the output within the buffer +by using ob_get_clean(). + 'AfterImportPage': When a page import is completed $title: Title under which the revisions were imported $origTitle: Title provided by the XML file @@ -429,9 +434,14 @@ token types. used to retrieve this type of tokens. 'ArticleAfterFetchContent': after fetching content of an article from +the database. DEPRECATED, use ArticleAfterFetchContentObject instead. +$article: the article (object) being loaded from the database +&$content: the content (string) of the article + +'ArticleAfterFetchContentObject': after fetching content of an article from the database $article: the article (object) being loaded from the database -$content: the content (string) of the article +&$content: the content of the article, as a Content object 'ArticleConfirmDelete': before writing the confirmation form for article deletion @@ -458,6 +468,8 @@ $article: the WikiPage that was deleted $user: the user that deleted the article $reason: the reason the article was deleted $id: id of the article that was deleted +$content: the Content of the deleted page +$logEntry: the ManualLogEntry used to record the deletion 'ArticleEditUpdateNewTalk': before updating user_newtalk when a user talk page was changed @@ -479,7 +491,7 @@ Wiki::articleFromTitle() $title: title (object) used to create the article object $article: article (object) that will be returned -'ArticleInsertComplete': After a new article is created +'ArticleInsertComplete': After a new article is created. DEPRECATED, use PageContentInsertComplete $article: WikiPage created $user: User creating the article $text: New content @@ -487,7 +499,7 @@ $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 WikiPage::doEdit() +$flags: Flags passed to WikiPage::doEditContent() $revision: New Revision of the article 'ArticleMergeComplete': after merging to article using Special:Mergehistory @@ -538,7 +550,7 @@ $user: the user who did the rollback $revision: the revision the page was reverted back to $current: the reverted revision -'ArticleSave': before an article is saved +'ArticleSave': before an article is saved. DEPRECATED, use PageContentSave instead $article: the WikiPage (object) being saved $user: the user (object) saving the article $text: the new article text @@ -547,7 +559,7 @@ $isminor: minor flag $iswatch: watch flag $section: section # -'ArticleSaveComplete': After an article has been updated +'ArticleSaveComplete': After an article has been updated. DEPRECATED, use PageContentSaveComplete instead. $article: WikiPage modified $user: User performing the modification $text: New content @@ -555,9 +567,9 @@ $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 WikiPage::doEdit() +$flags: Flags passed to WikiPage::doEditContent() $revision: New Revision of the article -$status: Status object about to be returned by doEdit() +$status: Status object about to be returned by doEditContent() $baseRevId: the rev ID (or false) this edit was based on 'ArticleUndelete': When one or more revisions of an article are restored @@ -586,11 +598,19 @@ object to both indicate that the output is done and what parser output was used. follwed an redirect $article: target article (object) -'ArticleViewCustom': allows to output the text of the article in a different format than wikitext +'ArticleViewCustom': allows to output the text of the article in a different format than wikitext. +DEPRECATED, use ArticleContentViewCustom instead. +Note that it is preferrable to implement proper handing for a custom data type using the ContentHandler facility. $text: text of the page $title: title of the page $output: reference to $wgOut +'ArticleContentViewCustom': allows to output the text of the article in a different format than wikitext. +Note that it is preferrable to implement proper handing for a custom data type using the ContentHandler facility. +$content: content of the page, as a Content object +$title: title of the page +$output: reference to $wgOut + 'AuthPluginAutoCreate': Called when creating a local account for an user logged in from an external authentication method $user: User object created locally @@ -722,6 +742,16 @@ the collation given in $collationName. 'ConfirmEmailComplete': Called after a user's email has been confirmed successfully $user: user (object) whose email is being confirmed +'ContentHandlerDefaultModelFor': Called when the default content model is determiend +for a given title. May be used to assign a different model for that title. +$title: the Title in question +&$model: the model name. Use with CONTENT_MODEL_XXX constants. + +'ContentHandlerForModelID': Called when a ContentHandler is requested for a given +cointent model name, but no entry for that model exists in $wgContentHandlers. +$modeName: the requested content model name +&$handler: set this to a ContentHandler object, if desired. + 'ContribsPager::getQueryInfo': Before the contributions query is about to run &$pager: Pager object for contributions &$queryInfo: The query for the contribs Pager @@ -795,12 +825,19 @@ $section: Section being edited &$error: Error message to return $summary: Edit summary for page -'EditFilterMerged': Post-section-merge edit filter +'EditFilterMerged': Post-section-merge edit filter. +DEPRECATED, use EditFilterMergedContent instead. $editor: EditPage instance (object) $text: content of the edit box &$error: error message to return $summary: Edit summary for page +'EditFilterMergedContent': Post-section-merge edit filter +$editor: EditPage instance (object) +$content: content of the edit box, as a Content object +&$error: error message to return +$summary: Edit summary for page + 'EditFormPreloadText': Allows population of the edit form when creating new pages &$text: Text to preload with @@ -811,7 +848,7 @@ pages $editPage: EditPage object 'EditPage::attemptSave': called before an article is -saved, that is before WikiPage::doEdit() is called +saved, that is before WikiPage::doEditContent() is called $editpage_Obj: the current EditPage object 'EditPage::importFormData': allow extensions to read additional data @@ -863,14 +900,28 @@ $title: title of page being edited &$msg: localization message name, overridable. Default is either 'copyrightwarning' or 'copyrightwarning2' 'EditPageGetDiffText': Allow modifying the wikitext that will be used in -"Show changes" +"Show changes". DEPRECATED. Use EditPageGetDiffContent instead. +Note that it is preferrable to implement diff handling for different data types using the ContentHandler facility. +$editPage: EditPage object +&$newtext: wikitext that will be used as "your version" + +'EditPageGetDiffContent': Allow modifying the wikitext that will be used in +"Show changes". +Note that it is preferrable to implement diff handling for different data types using the ContentHandler facility. $editPage: EditPage object &$newtext: wikitext that will be used as "your version" -'EditPageGetPreviewText': Allow modifying the wikitext that will be previewed +'EditPageGetPreviewText': Allow modifying the wikitext that will be previewed. +DEPRECATED. Use EditPageGetPreviewContent instead. +Note that it is preferrable to implement previews for different data types using the COntentHandler facility. $editPage: EditPage object &$toparse: wikitext that will be parsed +'EditPageGetPreviewContent': Allow modifying the wikitext that will be previewed. +Note that it is preferrable to implement previews for different data types using the COntentHandler facility. +$editPage: EditPage object +&$content: Content object to be previewed (may be replaced by hook function) + 'EditPageNoSuchSection': When a section edit request is given for an non-existent section &$editpage: The current EditPage object &$res: the HTML of the error text @@ -1139,6 +1190,10 @@ $reader: XMLReader object $revisionInfo: Array of information Return false to stop further processing of the tag +'InfoAction': When building information to display on the action=info page +$context: IContextSource object +&$pageInfo: Array of information + 'InitializeArticleMaybeRedirect': MediaWiki check to see if title is a redirect $title: Title object ($wgTitle) $request: WebRequest @@ -1477,12 +1532,45 @@ $categories: associative array, keys are category names, values are category $links: array, intended to hold the result. Must be an associative array with category types as keys and arrays of HTML links as values. +'PageContentInsertComplete': After a new article is created +$article: WikiPage created +$user: User creating the article +$content: New content as a Content object +$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 WikiPage::doEditContent() +$revision: New Revision of the article + 'PageContentLanguage': allows changing the language in which the content of a page is written. Defaults to the wiki content language ($wgContLang). $title: Title object &$pageLang: the page content language (either an object or a language code) $wgLang: the user language +'PageContentSave': before an article is saved. +$article: the WikiPage (object) being saved +$user: the user (object) saving the article +$content: the new article content, as a Content object +$summary: the article summary (comment) +$isminor: minor flag +$iswatch: watch flag +$section: section # + +'PageContentSaveComplete': After an article has been updated +$article: WikiPage modified +$user: User performing the modification +$content: New content, as a Content object +$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 WikiPage::doEditContent() +$revision: New Revision of the article +$status: Status object about to be returned by doEditContent() +$baseRevId: the rev ID (or false) this edit was based on + 'PageHistoryBeforeList': When a history page list is about to be constructed. $article: the article that the history is loading for @@ -1730,7 +1818,8 @@ $title : Current Title object being displayed in search results. 'ShowMissingArticle': Called when generating the output for a non-existent page $article: The article object corresponding to the page -'ShowRawCssJs': Customise the output of raw CSS and JavaScript in page views +'ShowRawCssJs': Customise the output of raw CSS and JavaScript in page views. +DEPRECATED, use the ContentHandler facility to handle CSS and JavaScript! $text: Text being shown $title: Title of the custom script/stylesheet page $output: Current OutputPage object @@ -2349,6 +2438,14 @@ One, and only one hook should set this, and return false. &$opts: Options to use for the query &$join: Join conditions +'WikiPageDeletionUpdates': manipulate the list of DataUpdates to be applied when + a page is deleted. Called in WikiPage::getDeletionUpdates(). + Note that updates specific to a content model should be provided by the + respective Content's getDeletionUpdates() method. +$page: the WikiPage +$content: the Content to generate updates for +&$updates: the array of DataUpdate objects. Hook function may want to add to it. + 'wfShellWikiCmd': Called when generating a shell-escaped command line string to run a MediaWiki cli script. &$script: MediaWiki cli script path diff --git a/docs/memcached.txt b/docs/memcached.txt index 3872edc830..971a611a0d 100644 --- a/docs/memcached.txt +++ b/docs/memcached.txt @@ -198,7 +198,7 @@ Revision text: expriry: $wgRevisionCacheExpiry Sessions: - controlled by: $wgSessionsInMemcached + controlled by: $wgSessionsInObjectCache key: $wgBDname:session:$id ex: wikidb:session:38d7c5b8d3bfc51egf40c69bc40f8be3 stores: $SESSION, useful when using a multi-sever wiki diff --git a/includes/Action.php b/includes/Action.php index 51922251c0..19552bc2e4 100644 --- a/includes/Action.php +++ b/includes/Action.php @@ -272,7 +272,7 @@ abstract class Action { * must throw subclasses of ErrorPageError * * @param $user User: the user to check, or null to use the context user - * @throws ErrorPageError + * @throws UserBlockedError|ReadOnlyError|PermissionsError * @return bool True on success */ protected function checkCanExecute( User $user ) { @@ -546,6 +546,7 @@ abstract class FormlessAction extends Action { * forms, they probably won't have any data, but some (eg rollback) may do * @param $data Array values that would normally be in the GET request * @param $captureErrors Bool whether to catch exceptions and just return false + * @throws ErrorPageError * @return Bool whether execution was successful */ public function execute( array $data = null, $captureErrors = true ) { diff --git a/includes/Article.php b/includes/Article.php index 76e566bd99..0eb0c68b08 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -57,10 +57,17 @@ class Article extends Page { public $mParserOptions; /** - * Content of the revision we are working on + * Text of the revision we are working on * @var string $mContent */ - var $mContent; // !< + var $mContent; // !< #BC cruft + + /** + * Content of the revision we are working on + * @var Content + * @since 1.21 + */ + var $mContentObject; // !< /** * Is the content ($mContent) already loaded? @@ -231,9 +238,32 @@ class Article extends Page { * This function has side effects! Do not use this function if you * only want the real revision text if any. * + * @deprecated in 1.21; use WikiPage::getContent() instead + * * @return string Return the text of this revision */ public function getContent() { + ContentHandler::deprecated( __METHOD__, '1.21' ); + $content = $this->getContentObject(); + return ContentHandler::getContentText( $content ); + } + + /** + * Returns a Content object representing the pages effective display content, + * not necessarily the revision's content! + * + * Note that getContent/loadContent do not follow redirects anymore. + * If you need to fetch redirectable content easily, try + * the shortcut in WikiPage::getRedirectTarget() + * + * This function has side effects! Do not use this function if you + * only want the real revision text if any. + * + * @return Content Return the content of this revision + * + * @since 1.21 + */ + protected function getContentObject() { wfProfileIn( __METHOD__ ); if ( $this->mPage->getID() === 0 ) { @@ -244,18 +274,20 @@ class Article extends Page { if ( $text === false ) { $text = ''; } + + $content = ContentHandler::makeContent( $text, $this->getTitle() ); } else { $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon'; - $text = wfMessage( $message )->text(); + $content = new MessageContent( $message, null, 'parsemag' ); } wfProfileOut( __METHOD__ ); - return $text; + return $content; } else { - $this->fetchContent(); + $this->fetchContentObject(); wfProfileOut( __METHOD__ ); - return $this->mContent; + return $this->mContentObject; } } @@ -336,22 +368,61 @@ class Article extends Page { * Get text of an article from database * Does *NOT* follow redirects. * + * @protected + * @note this is really internal functionality that should really NOT be used by other functions. For accessing + * article content, use the WikiPage class, especially WikiBase::getContent(). However, a lot of legacy code + * uses this method to retrieve page text from the database, so the function has to remain public for now. + * * @return mixed string containing article contents, or false if null + * @deprecated in 1.21, use WikiPage::getContent() instead */ - function fetchContent() { - if ( $this->mContentLoaded ) { + function fetchContent() { #BC cruft! + ContentHandler::deprecated( __METHOD__, '1.21' ); + + if ( $this->mContentLoaded && $this->mContent ) { return $this->mContent; } wfProfileIn( __METHOD__ ); + $content = $this->fetchContentObject(); + + $this->mContent = ContentHandler::getContentText( $content ); #@todo: get rid of mContent everywhere! + ContentHandler::runLegacyHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); + + wfProfileOut( __METHOD__ ); + + return $this->mContent; + } + + + /** + * Get text content object + * Does *NOT* follow redirects. + * TODO: when is this null? + * + * @note code that wants to retrieve page content from the database should use WikiPage::getContent(). + * + * @return Content|null + * + * @since 1.21 + */ + protected function fetchContentObject() { + if ( $this->mContentLoaded ) { + return $this->mContentObject; + } + + wfProfileIn( __METHOD__ ); + $this->mContentLoaded = true; + $this->mContent = null; $oldid = $this->getOldID(); # Pre-fill content with error message so that if something # fails we'll have something telling us what we intended. - $this->mContent = wfMessage( 'missing-revision', $oldid )->plain(); + //XXX: this isn't page content but a UI message. horrible. + $this->mContentObject = new MessageContent( 'missing-revision', array( $oldid ), array() ) ; if ( $oldid ) { # $this->mRevision might already be fetched by getOldIDFromRequest() @@ -371,6 +442,7 @@ class Article extends Page { } $this->mRevision = $this->mPage->getRevision(); + if ( !$this->mRevision ) { wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" ); wfProfileOut( __METHOD__ ); @@ -380,14 +452,14 @@ class Article extends Page { // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks. // We should instead work with the Revision object when we need it... - $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed + $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER, $this->getContext()->getUser() ); // Loads if user is allowed $this->mRevIdFetched = $this->mRevision->getId(); - wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); + wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) ); wfProfileOut( __METHOD__ ); - return $this->mContent; + return $this->mContentObject; } /** @@ -420,7 +492,7 @@ class Article extends Page { * @return Revision|null */ public function getRevisionFetched() { - $this->fetchContent(); + $this->fetchContentObject(); return $this->mRevision; } @@ -580,7 +652,7 @@ class Article extends Page { break; case 3: # This will set $this->mRevision if needed - $this->fetchContent(); + $this->fetchContentObject(); # Are we looking at an old revision if ( $oldid && $this->mRevision ) { @@ -604,18 +676,25 @@ class Article extends Page { wfDebug( __METHOD__ . ": showing CSS/JS source\n" ); $this->showCssOrJsPage(); $outputDone = true; - } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) { + } elseif( !wfRunHooks( 'ArticleContentViewCustom', + array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) { + + # Allow extensions do their own custom view for certain pages + $outputDone = true; + } elseif( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', + array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) { + # Allow extensions do their own custom view for certain pages $outputDone = true; } else { - $text = $this->getContent(); - $rt = Title::newFromRedirectArray( $text ); + $content = $this->getContentObject(); + $rt = $content->getRedirectChain(); if ( $rt ) { wfDebug( __METHOD__ . ": showing redirect=no page\n" ); # Viewing a redirect page (e.g. with parameter redirect=no) $outputPage->addHTML( $this->viewRedirect( $rt ) ); # Parse just to get categories, displaytitle, etc. - $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions ); + $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid, $parserOptions, false ); $outputPage->addParserOutputNoText( $this->mParserOutput ); $outputDone = true; } @@ -625,8 +704,9 @@ class Article extends Page { # Run the parse, protected by a pool counter wfDebug( __METHOD__ . ": doing uncached parse\n" ); + // @todo: shouldn't we be passing $this->getPage() to PoolWorkArticleView instead of plain $this? $poolArticleView = new PoolWorkArticleView( $this, $parserOptions, - $this->getRevIdFetched(), $useParserCache, $this->getContent() ); + $this->getRevIdFetched(), $useParserCache, $this->getContentObject(), $this->getContext() ); if ( !$poolArticleView->execute() ) { $error = $poolArticleView->getError(); @@ -708,6 +788,8 @@ class Article extends Page { /** * Show a diff page according to current request variables. For use within * Article::view() only, other callers should use the DifferenceEngine class. + * + * @todo: make protected */ public function showDiffPage() { $request = $this->getContext()->getRequest(); @@ -719,7 +801,17 @@ class Article extends Page { $unhide = $request->getInt( 'unhide' ) == 1; $oldid = $this->getOldID(); - $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide ); + $rev = $this->getRevisionFetched(); + + if ( !$rev ) { + $this->getContext()->getOutput()->setPageTitle( wfMessage( 'errorpagetitle' ) ); + $this->getContext()->getOutput()->addWikiMsg( 'difference-missing-revision', $oldid, 1 ); + return; + } + + $contentHandler = $rev->getContentHandler(); + $de = $contentHandler->createDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide ); + // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; $de->showDiffPage( $diffOnly ); @@ -736,23 +828,24 @@ class Article extends Page { * * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these * page views. + * + * @param bool $showCacheHint whether to show a message telling the user to clear the browser cache (default: true). */ - protected function showCssOrJsPage() { - $dir = $this->getContext()->getLanguage()->getDir(); - $lang = $this->getContext()->getLanguage()->getCode(); - + protected function showCssOrJsPage( $showCacheHint = true ) { $outputPage = $this->getContext()->getOutput(); - $outputPage->wrapWikiMsg( "
\n$1\n
", - 'clearyourcache' ); + + if ( $showCacheHint ) { + $dir = $this->getContext()->getLanguage()->getDir(); + $lang = $this->getContext()->getLanguage()->getCode(); + + $outputPage->wrapWikiMsg( "
\n$1\n
", + 'clearyourcache' ); + } // Give hooks a chance to customise the output - if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) { - // Wrap the whole lot in a
 and don't parse
-			$m = array();
-			preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
-			$outputPage->addHTML( "
\n" );
-			$outputPage->addHTML( htmlspecialchars( $this->mContent ) );
-			$outputPage->addHTML( "\n
\n" ); + if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) { + $po = $this->mContentObject->getParserOutput( $this->getTitle() ); + $outputPage->addHTML( $po->getText() ); } } @@ -1377,7 +1470,13 @@ class Article extends Page { // Generate deletion reason $hasHistory = false; if ( !$reason ) { - $reason = $this->generateReason( $hasHistory ); + try { + $reason = $this->generateReason( $hasHistory ); + } catch ( MWException $e ) { + # if a page is horribly broken, we still want to be able to delete it. so be lenient about errors here. + wfDebug("Error while building auto delete summary: $e"); + $reason = ''; + } } // If the page has a history, insert a warning @@ -1617,6 +1716,8 @@ class Article extends Page { * @return ParserOutput or false if the given revsion ID is not found */ public function getParserOutput( $oldid = null, User $user = null ) { + //XXX: bypasses mParserOptions and thus setParserOptions() + if ( $user === null ) { $parserOptions = $this->getParserOptions(); } else { @@ -1626,6 +1727,21 @@ class Article extends Page { return $this->mPage->getParserOutput( $parserOptions, $oldid ); } + /** + * Override the ParserOptions used to render the primary article wikitext. + * + * @param ParserOptions $options + * @throws MWException if the parser options where already initialized. + */ + public function setParserOptions( ParserOptions $options ) { + if ( $this->mParserOptions ) { + throw new MWException( "can't change parser options after they have already been set" ); + } + + // clone, so if $options is modified later, it doesn't confuse the parser cache. + $this->mParserOptions = clone $options; + } + /** * Get parser options suitable for rendering the primary article wikitext * @return ParserOptions @@ -1845,7 +1961,13 @@ class Article extends Page { * @return bool */ public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) { - return $this->mPage->updateRestrictions( $limit, $reason, $cascade, $expiry ); + return $this->mPage->doUpdateRestrictions( + $limit, + $expiry, + $cascade, + $reason, + $this->getContext()->getUser() + ); } /** @@ -1892,7 +2014,9 @@ class Article extends Page { * @return mixed */ public function generateReason( &$hasHistory ) { - return $this->mPage->getAutoDeleteReason( $hasHistory ); + $title = $this->mPage->getTitle(); + $handler = ContentHandler::getForTitle( $title ); + return $handler->getAutoDeleteReason( $title, $hasHistory ); } // ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** // @@ -1930,6 +2054,7 @@ class Article extends Page { * @param $newtext * @param $flags * @return string + * @deprecated since 1.21, use ContentHandler::getAutosummary() instead */ public static function getAutosummary( $oldtext, $newtext, $flags ) { return WikiPage::getAutosummary( $oldtext, $newtext, $flags ); diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 706e1c83e2..1cf66348d6 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -246,6 +246,7 @@ $wgAutoloadLocalClasses = array( 'StubUserLang' => 'includes/StubObject.php', 'TablePager' => 'includes/Pager.php', 'MWTimestamp' => 'includes/Timestamp.php', + 'TimestampException' => 'includes/Timestamp.php', 'Title' => 'includes/Title.php', 'TitleArray' => 'includes/TitleArray.php', 'TitleArrayFromResult' => 'includes/TitleArray.php', @@ -253,6 +254,7 @@ $wgAutoloadLocalClasses = array( 'UnlistedSpecialPage' => 'includes/SpecialPage.php', 'UploadSourceAdapter' => 'includes/Import.php', 'UppercaseCollation' => 'includes/Collation.php', + 'Uri' => 'includes/Uri.php', 'User' => 'includes/User.php', 'UserArray' => 'includes/UserArray.php', 'UserArrayFromResult' => 'includes/UserArray.php', @@ -288,6 +290,20 @@ $wgAutoloadLocalClasses = array( 'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php', 'ZipDirectoryReaderError' => 'includes/ZipDirectoryReader.php', + # content handler + 'AbstractContent' => 'includes/content/AbstractContent.php', + 'ContentHandler' => 'includes/content/ContentHandler.php', + 'Content' => 'includes/content/Content.php', + 'CssContentHandler' => 'includes/content/CssContentHandler.php', + 'CssContent' => 'includes/content/CssContent.php', + 'JavaScriptContentHandler' => 'includes/content/JavaScriptContentHandler.php', + 'JavaScriptContent' => 'includes/content/JavaScriptContent.php', + 'MessageContent' => 'includes/content/MessageContent.php', + 'TextContentHandler' => 'includes/content/TextContentHandler.php', + 'TextContent' => 'includes/content/TextContent.php', + 'WikitextContentHandler' => 'includes/content/WikitextContentHandler.php', + 'WikitextContent' => 'includes/content/WikitextContent.php', + # includes/actions 'CachedAction' => 'includes/actions/CachedAction.php', 'CreditsAction' => 'includes/actions/CreditsAction.php', @@ -330,6 +346,7 @@ $wgAutoloadLocalClasses = array( 'ApiFormatDump' => 'includes/api/ApiFormatDump.php', 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', 'ApiFormatJson' => 'includes/api/ApiFormatJson.php', + 'ApiFormatNone' => 'includes/api/ApiFormatNone.php', 'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php', 'ApiFormatRaw' => 'includes/api/ApiFormatRaw.php', 'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php', @@ -382,6 +399,7 @@ $wgAutoloadLocalClasses = array( 'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php', 'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php', 'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php', + 'ApiQueryORM' => 'includes/api/ApiQueryORM.php', 'ApiQueryPageProps' => 'includes/api/ApiQueryPageProps.php', 'ApiQueryProtectedTitles' => 'includes/api/ApiQueryProtectedTitles.php', 'ApiQueryQueryPage' => 'includes/api/ApiQueryQueryPage.php', @@ -447,7 +465,6 @@ $wgAutoloadLocalClasses = array( 'Blob' => 'includes/db/DatabaseUtility.php', 'ChronologyProtector' => 'includes/db/LBFactory.php', 'CloneDatabase' => 'includes/db/CloneDatabase.php', - 'Database' => 'includes/db/DatabaseMysql.php', 'DatabaseBase' => 'includes/db/Database.php', 'DatabaseIbm_db2' => 'includes/db/DatabaseIbm_db2.php', 'DatabaseMssql' => 'includes/db/DatabaseMssql.php', @@ -639,6 +656,9 @@ $wgAutoloadLocalClasses = array( 'EmaillingJob' => 'includes/job/EmaillingJob.php', 'EnotifNotifyJob' => 'includes/job/EnotifNotifyJob.php', 'Job' => 'includes/job/Job.php', + 'JobQueue' => 'includes/job/JobQueue.php', + 'JobQueueDB' => 'includes/job/JobQueueDB.php', + 'JobQueueGroup' => 'includes/job/JobQueueGroup.php', 'RefreshLinksJob' => 'includes/job/RefreshLinksJob.php', 'RefreshLinksJob2' => 'includes/job/RefreshLinksJob.php', 'UploadFromUrlJob' => 'includes/job/UploadFromUrlJob.php', @@ -858,6 +878,15 @@ $wgAutoloadLocalClasses = array( 'SqliteSearchResultSet' => 'includes/search/SearchSqlite.php', 'SqlSearchResultSet' => 'includes/search/SearchEngine.php', + # includes/site + 'MediaWikiSite' => 'includes/site/MediaWikiSite.php', + 'Site' => 'includes/site/Site.php', + 'SiteArray' => 'includes/site/SiteArray.php', + 'SiteList' => 'includes/site/SiteList.php', + 'SiteObject' => 'includes/site/SiteObject.php', + 'Sites' => 'includes/site/Sites.php', + 'SitesTable' => 'includes/site/SitesTable.php', + # includes/specials 'ActiveUsersPager' => 'includes/specials/SpecialActiveusers.php', 'AllmessagesTablePager' => 'includes/specials/SpecialAllmessages.php', @@ -1007,7 +1036,12 @@ $wgAutoloadLocalClasses = array( 'FakeConverter' => 'languages/Language.php', 'Language' => 'languages/Language.php', 'LanguageConverter' => 'languages/LanguageConverter.php', + 'CLDRPluralRuleConverter' => 'languages/utils/CLDRPluralRuleEvaluator.php', + 'CLDRPluralRuleConverter_Expression' => 'languages/utils/CLDRPluralRuleEvaluator.php', + 'CLDRPluralRuleConverter_Fragment' => 'languages/utils/CLDRPluralRuleEvaluator.php', + 'CLDRPluralRuleConverter_Operator' => 'languages/utils/CLDRPluralRuleEvaluator.php', 'CLDRPluralRuleEvaluator' => 'languages/utils/CLDRPluralRuleEvaluator.php', + 'CLDRPluralRuleEvaluator_Range' => 'languages/utils/CLDRPluralRuleEvaluator.php', 'CLDRPluralRuleError' => 'languages/utils/CLDRPluralRuleEvaluator.php', # maintenance @@ -1021,6 +1055,7 @@ $wgAutoloadLocalClasses = array( 'FixExtLinksProtocolRelative' => 'maintenance/fixExtLinksProtocolRelative.php', 'PopulateCategory' => 'maintenance/populateCategory.php', 'PopulateImageSha1' => 'maintenance/populateImageSha1.php', + 'PopulateFilearchiveSha1' => 'maintenance/populateFilearchiveSha1.php', 'PopulateLogSearch' => 'maintenance/populateLogSearch.php', 'PopulateLogUsertext' => 'maintenance/populateLogUsertext.php', 'PopulateParentId' => 'maintenance/populateParentId.php', @@ -1057,12 +1092,24 @@ $wgAutoloadLocalClasses = array( 'TestFileIterator' => 'tests/testHelpers.inc', 'TestRecorder' => 'tests/testHelpers.inc', + # tests/phpunit + 'DummyContentHandlerForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php', + 'DummyContentForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php', + 'JavascriptContentTest' => 'tests/phpunit/includes/JavascriptContentTest.php', + 'RevisionStorageTest' => 'tests/phpunit/includes/RevisionStorageTest.php', + 'TextContentTest' => 'tests/phpunit/includes/TextContentTest.php', + 'WikiPageTest' => 'tests/phpunit/includes/WikiPageTest.php', + # tests/phpunit/includes 'GenericArrayObjectTest' => 'tests/phpunit/includes/libs/GenericArrayObjectTest.php', # tests/phpunit/includes/db 'ORMRowTest' => 'tests/phpunit/includes/db/ORMRowTest.php', + # tests/phpunit/includes/site + 'SiteObjectTest' => 'tests/phpunit/includes/site/SiteObjectTest.php', + 'TestSites' => 'tests/phpunit/includes/site/TestSites.php', + # tests/parser 'ParserTest' => 'tests/parser/parserTest.inc', 'ParserTestParserHook' => 'tests/parser/parserTestsParserHook.php', diff --git a/includes/Autopromote.php b/includes/Autopromote.php index 9c77855d74..d7ed2f904b 100644 --- a/includes/Autopromote.php +++ b/includes/Autopromote.php @@ -157,6 +157,7 @@ class Autopromote { * * @param $cond Array: A condition, which must not contain other conditions * @param $user User The user to check the condition against + * @throws MWException * @return bool Whether the condition is true for the user */ private static function checkCondition( $cond, User $user ) { diff --git a/includes/BacklinkCache.php b/includes/BacklinkCache.php index d2055dd5c5..ba8691b292 100644 --- a/includes/BacklinkCache.php +++ b/includes/BacklinkCache.php @@ -217,6 +217,7 @@ class BacklinkCache { /** * Get the field name prefix for a given table * @param $table String + * @throws MWException * @return null|string */ protected function getPrefix( $table ) { @@ -245,6 +246,7 @@ class BacklinkCache { * Get the SQL condition array for selecting backlinks, with a join * on the page table. * @param $table String + * @throws MWException * @return array|null */ protected function getConditions( $table ) { @@ -381,6 +383,7 @@ class BacklinkCache { * Partition a DB result with backlinks in it into batches * @param $res ResultWrapper database result * @param $batchSize integer + * @throws MWException * @return array @see */ protected function partitionResult( $res, $batchSize ) { diff --git a/includes/Block.php b/includes/Block.php index 732699dce6..86b4d13d36 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -231,6 +231,7 @@ class Block { * 3) An autoblock on the given IP * @param $vagueTarget User|String also search for blocks affecting this target. Doesn't * make any sense to use TYPE_AUTO / TYPE_ID here. Leave blank to skip IP lookups. + * @throws MWException * @return Bool whether a relevant block was found */ protected function newLoad( $vagueTarget = null ) { @@ -426,6 +427,7 @@ class Block { /** * Delete the row from the IP blocks table. * + * @throws MWException * @return Boolean */ public function delete() { @@ -780,6 +782,7 @@ class Block { /** * Get the IP address at the start of the range in Hex form + * @throws MWException * @return String IP in Hex form */ public function getRangeStart() { @@ -797,6 +800,7 @@ class Block { /** * Get the IP address at the start of the range in Hex form + * @throws MWException * @return String IP in Hex form */ public function getRangeEnd() { diff --git a/includes/Category.php b/includes/Category.php index b7b12e8a31..ffd7bb8bda 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -44,6 +44,7 @@ class Category { /** * Set up all member variables using a database query. + * @throws MWException * @return bool True on success, false on failure. */ protected function initialize() { diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php index 3bb2bc9b1c..3d66b74a26 100644 --- a/includes/CategoryViewer.php +++ b/includes/CategoryViewer.php @@ -613,6 +613,7 @@ class CategoryViewer extends ContextSource { * * @param Title $title: The title (usually $this->title) * @param String $section: Which section + * @throws MWException * @return Title */ private function addFragmentToTitle( $title, $section ) { diff --git a/includes/Cdb_PHP.php b/includes/Cdb_PHP.php index 02be65f3b3..f58e07e1d5 100644 --- a/includes/Cdb_PHP.php +++ b/includes/Cdb_PHP.php @@ -126,6 +126,7 @@ class CdbReader_PHP extends CdbReader { /** * @param $fileName string + * @throws MWException */ function __construct( $fileName ) { $this->fileName = $fileName; @@ -179,7 +180,7 @@ class CdbReader_PHP extends CdbReader { protected function read( $length, $pos ) { if ( fseek( $this->handle, $pos ) == -1 ) { // This can easily happen if the internal pointers are incorrect - throw new MWException( + throw new MWException( 'Seek failed, file "' . $this->fileName . '" may be corrupted.' ); } @@ -198,12 +199,13 @@ class CdbReader_PHP extends CdbReader { /** * Unpack an unsigned integer and throw an exception if it needs more than 31 bits * @param $s - * @return + * @throws MWException + * @return mixed */ protected function unpack31( $s ) { $data = unpack( 'V', $s ); if ( $data[1] > 0x7fffffff ) { - throw new MWException( + throw new MWException( 'Error in CDB file "' . $this->fileName . '", integer too big.' ); } return $data[1]; @@ -475,7 +477,7 @@ class CdbWriter_PHP extends CdbWriter { /** * Clean up the temp file and throw an exception - * + * * @param $msg string * @throws MWException */ diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php index a97cb03482..18f425ae5c 100644 --- a/includes/ChangeTags.php +++ b/includes/ChangeTags.php @@ -81,6 +81,7 @@ class ChangeTags { * @param $log_id int: log_id of the change to add the tags to * @param $params String: params to put in the ct_params field of tabel 'change_tag' * + * @throws MWException * @return bool: false if no changes are made, otherwise true * * @exception MWException when $rc_id, $rev_id and $log_id are all null @@ -164,10 +165,9 @@ class ChangeTags { * @param $conds String|Array: conditions used in query, see DatabaseBase::select * @param $join_conds Array: join conditions, see DatabaseBase::select * @param $options Array: options, see Database::select - * @param $filter_tag String: tag to select on - * - * @exception MWException when unable to determine appropriate JOIN condition for tagging + * @param bool|string $filter_tag Tag to select on * + * @throws MWException When unable to determine appropriate JOIN condition for tagging */ static function modifyDisplayQuery( &$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag = false ) { diff --git a/includes/Collation.php b/includes/Collation.php index ad2b94b177..3cc7902855 100644 --- a/includes/Collation.php +++ b/includes/Collation.php @@ -152,10 +152,10 @@ class IcuCollation extends Collation { /** * Unified CJK blocks. * - * The same definition of a CJK block must be used for both Collation and - * generateCollationData.php. These blocks are omitted from the first - * letter data, as an optimisation measure and because the default UCA table - * is pretty useless for sorting Chinese text anyway. Japanese and Korean + * The same definition of a CJK block must be used for both Collation and + * generateCollationData.php. These blocks are omitted from the first + * letter data, as an optimisation measure and because the default UCA table + * is pretty useless for sorting Chinese text anyway. Japanese and Korean * blocks are not included here, because they are smaller and more useful. */ static $cjkBlocks = array( @@ -180,7 +180,7 @@ class IcuCollation extends Collation { function __construct( $locale ) { if ( !extension_loaded( 'intl' ) ) { - throw new MWException( 'An ICU collation was requested, ' . + throw new MWException( 'An ICU collation was requested, ' . 'but the intl extension is not available.' ); } $this->locale = $locale; @@ -218,8 +218,8 @@ class IcuCollation extends Collation { // Check for CJK $firstChar = mb_substr( $string, 0, 1, 'UTF-8' ); - if ( ord( $firstChar ) > 0x7f - && self::isCjk( utf8ToCodepoint( $firstChar ) ) ) + if ( ord( $firstChar ) > 0x7f + && self::isCjk( utf8ToCodepoint( $firstChar ) ) ) { return $firstChar; } @@ -265,9 +265,9 @@ class IcuCollation extends Collation { // Sort the letters. // // It's impossible to have the precompiled data file properly sorted, - // because the sort order changes depending on ICU version. If the - // array is not properly sorted, the binary search will return random - // results. + // because the sort order changes depending on ICU version. If the + // array is not properly sorted, the binary search will return random + // results. // // We also take this opportunity to remove primary collisions. $letterMap = array(); @@ -320,7 +320,7 @@ class IcuCollation extends Collation { } /** - * Do a binary search, and return the index of the largest item that sorts + * Do a binary search, and return the index of the largest item that sorts * less than or equal to the target value. * * @param $valueCallback array A function to call to get the value with @@ -335,8 +335,12 @@ class IcuCollation extends Collation { * sorts before all items. */ function findLowerBound( $valueCallback, $valueCount, $comparisonCallback, $target ) { + if ( $valueCount === 0 ) { + return false; + } + $min = 0; - $max = $valueCount - 1; + $max = $valueCount; do { $mid = $min + ( ( $max - $min ) >> 1 ); $item = call_user_func( $valueCallback, $mid ); @@ -351,12 +355,15 @@ class IcuCollation extends Collation { } } while ( $min < $max - 1 ); - if ( $min == 0 && $max == 0 && $comparison > 0 ) { - // Before the first item - return false; - } else { - return $min; + if ( $min == 0 ) { + $item = call_user_func( $valueCallback, $min ); + $comparison = call_user_func( $comparisonCallback, $target, $item ); + if ( $comparison < 0 ) { + // Before the first item + return false; + } } + return $min; } static function isCjk( $codepoint ) { diff --git a/includes/ConfEditor.php b/includes/ConfEditor.php index b68fc76206..d304e65ee2 100644 --- a/includes/ConfEditor.php +++ b/includes/ConfEditor.php @@ -159,6 +159,7 @@ class ConfEditor { * insert * Insert a new element at the start of the array. * + * @throws MWException * @return string */ public function edit( $ops ) { @@ -392,6 +393,8 @@ class ConfEditor { * Finds the source byte region which you would want to delete, if $pathName * was to be deleted. Includes the leading spaces and tabs, the trailing line * break, and any comments in between. + * @param $pathName + * @throws MWException * @return array */ function findDeletionRegion( $pathName ) { @@ -450,6 +453,8 @@ class ConfEditor { * or semicolon. * * The end position is the past-the-end (end + 1) value as per convention. + * @param $pathName + * @throws MWException * @return array */ function findValueRegion( $pathName ) { diff --git a/includes/Cookie.php b/includes/Cookie.php index 7984d63e34..1b86f5da83 100644 --- a/includes/Cookie.php +++ b/includes/Cookie.php @@ -40,14 +40,15 @@ class Cookie { /** * Sets a cookie. Used before a request to set up any individual - * cookies. Used internally after a request to parse the + * cookies. Used internally after a request to parse the * Set-Cookie headers. * * @param $value String: the value of the cookie * @param $attr Array: possible key/values: - * expires A date string - * path The path this cookie is used on - * domain Domain this cookie is used on + * expires A date string + * path The path this cookie is used on + * domain Domain this cookie is used on + * @throws MWException */ public function set( $value, $attr ) { $this->value = $value; diff --git a/includes/CryptRand.php b/includes/CryptRand.php index 858eebf205..fcf6a3966b 100644 --- a/includes/CryptRand.php +++ b/includes/CryptRand.php @@ -391,7 +391,7 @@ class MWCryptRand { // We hash the random state with more salt to avoid the state from leaking // out and being used to predict the /randomness/ that follows. if ( strlen( $buffer ) < $bytes ) { - wfDebug( __METHOD__ . ": Falling back to using a pseudo random state to generate randomness.\n" ); + wfDebug( __METHOD__ . ": Falling back to using a pseudo random state to generate randomness.\n" ); } while ( strlen( $buffer ) < $bytes ) { wfProfileIn( __METHOD__ . '-fallback' ); diff --git a/includes/DataUpdate.php b/includes/DataUpdate.php index 377b64c0cf..088bb7e774 100644 --- a/includes/DataUpdate.php +++ b/includes/DataUpdate.php @@ -76,6 +76,7 @@ abstract class DataUpdate implements DeferrableUpdate { * * @static * @param $updates array a list of DataUpdate instances + * @throws Exception|null */ public static function runUpdates( $updates ) { if ( empty( $updates ) ) return; # nothing to do diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 8de981c6c5..cea63e07a3 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -530,6 +530,13 @@ $wgAllowAsyncCopyUploads = false; */ $wgCopyUploadsDomains = array(); +/** + * Enable copy uploads from Special:Upload. $wgAllowCopyUploads must also be + * true. If $wgAllowCopyUploads is true, but this is false, you will only be + * able to perform copy uploads from the API or extensions (e.g. UploadWizard). + */ +$wgCopyUploadsFromSpecialUpload = false; + /** * Proxy to use for copy upload requests. * @since 1.20 @@ -738,6 +745,19 @@ $wgMediaHandlers = array( 'image/x-djvu' => 'DjVuHandler', // compat ); +/** + * Plugins for page content model handling. + * Each entry in the array maps a model id to a class name. + * + * @since 1.21 + */ +$wgContentHandlers = array( + CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // the usual case + CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', // dumb version, no syntax highlighting + CONTENT_MODEL_CSS => 'CssContentHandler', // dumb version, no syntax highlighting + CONTENT_MODEL_TEXT => 'TextContentHandler', // plain text, for use by extensions etc +); + /** * Resizing can be done using PHP's internal image libraries or using * ImageMagick or another third-party converter, e.g. GraphicMagick. @@ -1076,6 +1096,16 @@ $wgThumbUpright = 0.75; */ $wgDirectoryMode = 0777; +/** + * Generate and use thumbnails suitable for screens with 1.5 and 2.0 pixel densities. + * + * This means a 320x240 use of an image on the wiki will also generate 480x360 and 640x480 + * thumbnails, output via data-src-1-5 and data-src-2-0. Runtime JavaScript switches the + * images in after loading the original low-resolution versions depending on the reported + * window.devicePixelRatio. + */ +$wgResponsiveImages = true; + /** * @name DJVU settings * @{ @@ -1377,10 +1407,14 @@ $wgAllDBsAreLocalhost = false; * $wgSharedTables may be customized with a list of tables to share in the shared * datbase. However it is advised to limit what tables you do share as many of * MediaWiki's tables may have side effects if you try to share them. - * EXPERIMENTAL * * $wgSharedPrefix is the table prefix for the shared database. It defaults to * $wgDBprefix. + * + * @deprecated In new code, use the $wiki parameter to wfGetLB() to access + * remote databases. Using wfGetLB() allows the shared database to reside on + * separate servers to the wiki's own database, with suitable configuration + * of $wgLBFactoryConf. */ $wgSharedDB = null; @@ -1758,7 +1792,7 @@ $wgDBAhandler = 'db3'; /** * Deprecated alias for $wgSessionsInObjectCache. * - * @deprecated Use $wgSessionsInObjectCache + * @deprecated since 1.20; Use $wgSessionsInObjectCache */ $wgSessionsInMemcached = false; @@ -2031,6 +2065,27 @@ $wgSquidServersNoPurge = array(); /** Maximum number of titles to purge in any one client operation */ $wgMaxSquidPurgeTitles = 400; +/** + * Whether to use a Host header in purge requests sent to the proxy servers + * configured in $wgSquidServers. Set this to false to support Squid + * configured in forward-proxy mode. + * + * If this is set to true, a Host header will be sent, and only the path + * component of the URL will appear on the request line, as if the request + * were a non-proxy HTTP 1.1 request. Varnish only supports this style of + * request. Squid supports this style of request only if reverse-proxy mode + * (http_port ... accel) is enabled. + * + * If this is set to false, no Host header will be sent, and the absolute URL + * will be sent in the request line, as is the standard for an HTTP proxy + * request in both HTTP 1.0 and 1.1. This style of request is not supported + * by Varnish, but is supported by Squid in either configuration (forward or + * reverse). + * + * @since 1.21 + */ +$wgSquidPurgeUseHostHeader = true; + /** * Routing configuration for HTCP multicast purging. Add elements here to * enable HTCP and determine which purges are sent where. If set to an empty @@ -2073,13 +2128,13 @@ $wgHTCPMulticastRouting = array(); * setting is ignored. If $wgHTCPMulticastRouting is not set and this setting * is, it is used to populate $wgHTCPMulticastRouting. * - * @deprecated in favor of $wgHTCPMulticastRouting + * @deprecated since 1.20 in favor of $wgHTCPMulticastRouting */ $wgHTCPMulticastAddress = false; /** * HTCP multicast port. - * @deprecated in favor of $wgHTCPMulticastRouting + * @deprecated since 1.20 in favor of $wgHTCPMulticastRouting * @see $wgHTCPMulticastAddress */ $wgHTCPPort = 4827; @@ -3594,13 +3649,6 @@ $wgDefaultUserOptions = array( 'wllimit' => 250, ); -/** - * Whether or not to allow and use real name fields. - * @deprecated since 1.16, use $wgHiddenPrefs[] = 'realname' below to disable real - * names - */ -$wgAllowRealName = true; - /** An array of preferences to not show for the user */ $wgHiddenPrefs = array(); @@ -4288,7 +4336,9 @@ $wgSecretKey = false; */ $wgProxyList = array(); -/** deprecated */ +/** + * @deprecated since 1.14 + */ $wgProxyKey = false; /** @} */ # end of proxy scanner settings @@ -5381,6 +5431,14 @@ $wgJobClasses = array( */ $wgJobTypesExcludedFromDefaultQueue = array(); +/** + * Map of job types to configuration arrays. + * These settings should be global to all wikis. + */ +$wgJobTypeConf = array( + 'default' => array( 'class' => 'JobQueueDB' ), +); + /** * Additional functions to be performed with updateSpecialPages. * Expensive Querypages are already updated. @@ -5911,6 +5969,7 @@ $wgAPIModules = array(); $wgAPIMetaModules = array(); $wgAPIPropModules = array(); $wgAPIListModules = array(); +$wgAPIGeneratorModules = array(); /** * Maximum amount of rows to scan in a DB query in the API @@ -6230,6 +6289,57 @@ $wgSeleniumConfigFile = null; $wgDBtestuser = ''; //db user that has permission to create and drop the test databases only $wgDBtestpassword = ''; +/** + * Associative array mapping namespace IDs to the name of the content model pages in that namespace should have by + * default (use the CONTENT_MODEL_XXX constants). If no special content type is defined for a given namespace, + * pages in that namespace will use the CONTENT_MODEL_WIKITEXT (except for the special case of JS and CS pages). + * + * @since 1.21 + */ +$wgNamespaceContentModels = array(); + +/** + * How to react if a plain text version of a non-text Content object is requested using ContentHandler::getContentText(): + * + * * 'ignore': return null + * * 'fail': throw an MWException + * * 'serialize': serialize to default format + * + * @since 1.21 + */ +$wgContentHandlerTextFallback = 'ignore'; + +/** + * Set to false to disable use of the database fields introduced by the ContentHandler facility. + * This way, the ContentHandler facility can be used without any additional information in the database. + * A page's content model is then derived solely from the page's title. This however means that changing + * a page's default model (e.g. using $wgNamespaceContentModels) will break the page and/or make the content + * inaccessible. This also means that pages can not be moved to a title that would default to a different + * content model. + * + * Overall, with $wgContentHandlerUseDB = false, no database updates are needed, but content handling + * is less robust and less flexible. + * + * @since 1.21 + */ +$wgContentHandlerUseDB = false; + +/** + * Determines which types of text are parsed as wikitext. This does not imply that these kinds + * of texts are also rendered as wikitext, it only means that links, magic words, etc will have + * the effect on the database they would have on a wikitext page. + * + * @todo: On the long run, it would be nice to put categories etc into a separate structure, + * or at least parse only the contents of comments in the scripts. + * + * @since 1.21 + */ +$wgTextModelsToParse = array( + CONTENT_MODEL_WIKITEXT, // Just for completeness, wikitext will always be parsed. + CONTENT_MODEL_JAVASCRIPT, // Make categories etc work, people put them into comments. + CONTENT_MODEL_CSS, // Make categories etc work, people put them into comments. +); + /** * Whether the user must enter their password to change their e-mail address * @@ -6237,6 +6347,14 @@ $wgDBtestpassword = ''; */ $wgRequirePasswordforEmailChange = true; +/** + * Register handlers for specific types of sites. + * + * @since 1.20 + */ +$wgSiteTypes = array(); +$wgSiteTypes['mediawiki'] = 'MediaWikiSite'; + /** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker diff --git a/includes/DeferredUpdates.php b/includes/DeferredUpdates.php index b4989a6997..716e65c8c3 100644 --- a/includes/DeferredUpdates.php +++ b/includes/DeferredUpdates.php @@ -92,7 +92,7 @@ class DeferredUpdates { $update->doUpdate(); if ( $doCommit && $dbw->trxLevel() ) { - $dbw->commit( __METHOD__ ); + $dbw->commit( __METHOD__, 'flush' ); } } catch ( MWException $e ) { // We don't want exceptions thrown during deferred updates to diff --git a/includes/Defines.php b/includes/Defines.php index be9f981602..882f318245 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -39,7 +39,7 @@ define( 'MW_SPECIALPAGE_VERSION', 2 ); define( 'DBO_DEBUG', 1 ); define( 'DBO_NOBUFFER', 2 ); define( 'DBO_IGNORE', 4 ); -define( 'DBO_TRX', 8 ); +define( 'DBO_TRX', 8 ); // automatically start transaction on first query define( 'DBO_DEFAULT', 16 ); define( 'DBO_PERSISTENT', 32 ); define( 'DBO_SYSDBA', 64 ); //for oracle maintenance @@ -171,11 +171,12 @@ define( 'MW_DATE_ISO', 'ISO 8601' ); /**@{ * RecentChange type identifiers */ -define( 'RC_EDIT', 0); -define( 'RC_NEW', 1); -define( 'RC_MOVE', 2); // obsolete -define( 'RC_LOG', 3); -define( 'RC_MOVE_OVER_REDIRECT', 4); // obsolete +define( 'RC_EDIT', 0 ); +define( 'RC_NEW', 1 ); +define( 'RC_MOVE', 2 ); // obsolete +define( 'RC_LOG', 3 ); +define( 'RC_MOVE_OVER_REDIRECT', 4 ); // obsolete +define( 'RC_EXTERNAL', 5 ); /**@}*/ /**@{ @@ -213,6 +214,7 @@ require_once __DIR__.'/normal/UtfNormalDefines.php'; define( 'MW_SUPPORTS_EDITFILTERMERGED', 1 ); define( 'MW_SUPPORTS_PARSERFIRSTCALLINIT', 1 ); define( 'MW_SUPPORTS_LOCALISATIONCACHE', 1 ); +define( 'MW_SUPPORTS_CONTENTHANDLER', 1 ); /**@}*/ /** Support for $wgResourceModules */ @@ -261,7 +263,7 @@ define( 'APCOND_BLOCKED', 8 ); define( 'APCOND_ISBOT', 9 ); /**@}*/ -/** +/** @{ * Protocol constants for wfExpandUrl() */ define( 'PROTO_HTTP', 'http://' ); @@ -270,3 +272,35 @@ define( 'PROTO_RELATIVE', '//' ); define( 'PROTO_CURRENT', null ); define( 'PROTO_CANONICAL', 1 ); define( 'PROTO_INTERNAL', 2 ); +/**@}*/ + +/**@{ + * Content model ids, used by Content and ContentHandler. + * These IDs will be exposed in the API and XML dumps. + * + * Extensions that define their own content model IDs should take + * care to avoid conflicts. Using the extension name as a prefix is recommended, + * for example 'myextension-somecontent'. + */ +define( 'CONTENT_MODEL_WIKITEXT', 'wikitext' ); +define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' ); +define( 'CONTENT_MODEL_CSS', 'css' ); +define( 'CONTENT_MODEL_TEXT', 'text' ); +/**@}*/ + +/**@{ + * Content formats, used by Content and ContentHandler. + * These should be MIME types, and will be exposed in the API and XML dumps. + * + * Extensions are free to use the below formats, or define their own. + * It is recommended to stick with the conventions for MIME types. + */ +define( 'CONTENT_FORMAT_WIKITEXT', 'text/x-wiki' ); // wikitext +define( 'CONTENT_FORMAT_JAVASCRIPT', 'text/javascript' ); // for js pages +define( 'CONTENT_FORMAT_CSS', 'text/css' ); // for css pages +define( 'CONTENT_FORMAT_TEXT', 'text/plain' ); // for future use, e.g. with some plain-html messages. +define( 'CONTENT_FORMAT_HTML', 'text/html' ); // for future use, e.g. with some plain-html messages. +define( 'CONTENT_FORMAT_SERIALIZED', 'application/vnd.php.serialized' ); // for future use with the api and for extensions +define( 'CONTENT_FORMAT_JSON', 'application/json' ); // for future use with the api, and for use by extensions +define( 'CONTENT_FORMAT_XML', 'application/xml' ); // for future use with the api, and for use by extensions +/**@}*/ diff --git a/includes/EditPage.php b/includes/EditPage.php index 39227198c2..17335e49bf 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -155,6 +155,11 @@ class EditPage { */ const AS_IMAGE_REDIRECT_LOGGED = 234; + /** + * Status: can't parse content + */ + const AS_PARSE_ERROR = 240; + /** * HTML id and name for the beginning of the edit form. */ @@ -214,6 +219,7 @@ class EditPage { var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false; var $edittime = '', $section = '', $sectiontitle = '', $starttime = ''; var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true; + var $contentModel = null, $contentFormat = null; # Placeholders for text injection by hooks (must be HTML) # extensions should take care to _append_ to the present value @@ -225,7 +231,7 @@ class EditPage { public $editFormTextBottom = ''; public $editFormTextAfterContent = ''; public $previewTextAfterContent = ''; - public $mPreloadText = ''; + public $mPreloadContent = null; /* $didSave should be set to true whenever an article was succesfully altered. */ public $didSave = false; @@ -233,12 +239,24 @@ class EditPage { public $suppressIntro = false; + /** + * Set to true to allow editing of non-text content types. + * + * @var bool + */ + public $allowNonTextContent = false; + /** * @param $article Article */ public function __construct( Article $article ) { $this->mArticle = $article; $this->mTitle = $article->getTitle(); + + $this->contentModel = $this->mTitle->getContentModel(); + + $handler = ContentHandler::getForModelID( $this->contentModel ); + $this->contentFormat = $handler->getDefaultFormat(); } /** @@ -267,7 +285,7 @@ class EditPage { /** * Get the context title object. - * If not set, $wgTitle will be returned. This behavior might changed in + * If not set, $wgTitle will be returned. This behavior might change in * the future to return $this->mTitle instead. * * @return Title object @@ -363,7 +381,6 @@ class EditPage { $this->isCssSubpage = $this->mTitle->isCssSubpage(); $this->isJsSubpage = $this->mTitle->isJsSubpage(); $this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage(); - $this->isNew = !$this->mTitle->exists() || $this->section == 'new'; # Show applicable editing introductions if ( $this->formtype == 'initial' || $this->firsttime ) { @@ -392,14 +409,10 @@ class EditPage { wfProfileOut( __METHOD__ ); return; } - - if ( !$this->mTitle->getArticleID() ) { + if ( !$this->mTitle->getArticleID() ) wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) ); - } - else { + else wfRunHooks( 'EditFormInitialText', array( $this ) ); - } - } $this->showEditForm(); @@ -442,6 +455,7 @@ class EditPage { * @since 1.19 * @param $permErrors Array of permissions errors, as returned by * Title::getUserPermissionsErrors(). + * @throws PermissionsError */ protected function displayPermissionsError( array $permErrors ) { global $wgRequest, $wgOut; @@ -454,10 +468,10 @@ class EditPage { return; } - $content = $this->getContent(); + $content = $this->getContentObject(); # Use the normal message if there's nothing to display - if ( $this->firsttime && $content === '' ) { + if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) { $action = $this->mTitle->exists() ? 'edit' : ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' ); throw new PermissionsError( $action, $permErrors ); @@ -471,13 +485,14 @@ class EditPage { # If the user made changes, preserve them when showing the markup # (This happens when a user is blocked during edit, for instance) if ( !$this->firsttime ) { - $content = $this->textbox1; + $text = $this->textbox1; $wgOut->addWikiMsg( 'viewyourtext' ); } else { + $text = $this->toEditText( $content ); $wgOut->addWikiMsg( 'viewsourcetext' ); } - $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) ); + $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) ); $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ), Linker::formatTemplates( $this->getTemplates() ) ) ); @@ -572,12 +587,13 @@ class EditPage { * @param $request WebRequest */ function importFormData( &$request ) { - global $wgLang, $wgUser; + global $wgContLang, $wgUser; wfProfileIn( __METHOD__ ); # Section edit can come from either the form or a link $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) ); + $this->isNew = !$this->mTitle->exists() || $this->section == 'new'; if ( $request->wasPosted() ) { # These fields need to be checked for encoding. @@ -590,15 +606,13 @@ class EditPage { // modified by subclasses wfProfileIn( get_class( $this ) . "::importContentFormData" ); $textbox1 = $this->importContentFormData( $request ); - if ( isset( $textbox1 ) ) { + if ( isset( $textbox1 ) ) $this->textbox1 = $textbox1; - } - wfProfileOut( get_class( $this ) . "::importContentFormData" ); } # Truncate for whole multibyte characters - $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 255 ); + $this->summary = $wgContLang->truncate( $request->getText( 'wpSummary' ), 255 ); # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for @@ -610,7 +624,7 @@ class EditPage { # currently doing double duty as both edit summary and section title. Right now this # is just to allow API edits to work around this limitation, but this should be # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312). - $this->sectiontitle = $wgLang->truncate( $request->getText( 'wpSectionTitle' ), 255 ); + $this->sectiontitle = $wgContLang->truncate( $request->getText( 'wpSectionTitle' ), 255 ); $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle ); $this->edittime = $request->getVal( 'wpEdittime' ); @@ -717,10 +731,17 @@ class EditPage { } } + $this->oldid = $request->getInt( 'oldid' ); + $this->bot = $request->getBool( 'bot', true ); $this->nosummary = $request->getBool( 'nosummary' ); - $this->oldid = $request->getInt( 'oldid' ); + $content_handler = ContentHandler::getForTitle( $this->mTitle ); + $this->contentModel = $request->getText( 'model', $content_handler->getModelID() ); #may be overridden by revision + $this->contentFormat = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision + + #TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed + #TODO: check if the desired content model supports the given content format! $this->live = $request->getCheck( 'live' ); $this->editintro = $request->getText( 'editintro', @@ -753,7 +774,13 @@ class EditPage { function initialiseForm() { global $wgUser; $this->edittime = $this->mArticle->getTimestamp(); - $this->textbox1 = $this->getContent( false ); + + $content = $this->getContentObject( false ); #TODO: track content object?! + if ( $content === false ) { + return false; + } + $this->textbox1 = $this->toEditText( $content ); + // activate checkboxes if user wants them to be always active # Sort out the "watch" checkbox if ( $wgUser->getOption( 'watchdefault' ) ) { @@ -779,36 +806,65 @@ class EditPage { /** * Fetch initial editing page content. * - * @param $def_text string + * @param $def_text string|bool * @return mixed string on success, $def_text for invalid sections * @private + * @deprecated since 1.21, get WikiPage::getContent() instead. + */ + function getContent( $def_text = false ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + if ( $def_text !== null && $def_text !== false && $def_text !== '' ) { + $def_content = $this->toEditContent( $def_text ); + } else { + $def_content = false; + } + + $content = $this->getContentObject( $def_content ); + + // Note: EditPage should only be used with text based content anyway. + return $this->toEditText( $content ); + } + + /** + * @param Content|null $def_content The default value to return + * + * @return mixed Content on success, $def_content for invalid sections + * + * @since 1.21 */ - function getContent( $def_text = '' ) { - global $wgOut, $wgRequest, $wgParser; + protected function getContentObject( $def_content = null ) { + global $wgOut, $wgRequest; wfProfileIn( __METHOD__ ); - $text = false; + $content = false; // For message page not locally set, use the i18n message. // For other non-existent articles, use preload text if any. if ( !$this->mTitle->exists() || $this->section == 'new' ) { if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) { # If this is a system message, get the default text. - $text = $this->mTitle->getDefaultMessageText(); + $msg = $this->mTitle->getDefaultMessageText(); + + $content = $this->toEditContent( $msg ); } - if ( $text === false ) { + if ( $content === false ) { # If requested, preload some text. $preload = $wgRequest->getVal( 'preload', // Custom preload text for new sections $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' ); - $text = $this->getPreloadedText( $preload ); + + $content = $this->getPreloadedContent( $preload ); } // For existing pages, get text based on "undo" or section parameters. } else { if ( $this->section != '' ) { // Get section edit text (returns $def_text for invalid sections) - $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text ); + $orig = $this->getOriginalContent(); + $content = $orig ? $orig->getSection( $this->section ) : null; + + if ( !$content ) $content = $def_content; } else { $undoafter = $wgRequest->getInt( 'undoafter' ); $undo = $wgRequest->getInt( 'undo' ); @@ -824,15 +880,16 @@ class EditPage { # Sanity check, make sure it's the right page, # the revisions exist and they were not deleted. - # Otherwise, $text will be left as-is. + # Otherwise, $content will be left as-is. if ( !is_null( $undorev ) && !is_null( $oldrev ) && $undorev->getPage() == $oldrev->getPage() && $undorev->getPage() == $this->mTitle->getArticleID() && !$undorev->isDeleted( Revision::DELETED_TEXT ) && !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { - $text = $this->mArticle->getUndoText( $undorev, $oldrev ); - if ( $text === false ) { + $content = $this->mArticle->getUndoContent( $undorev, $oldrev ); + + if ( $content === false ) { # Warn the user that something went wrong $undoMsg = 'failure'; } else { @@ -865,14 +922,14 @@ class EditPage { wfMessage( 'undo-' . $undoMsg )->plain() . '', true, /* interface */true ); } - if ( $text === false ) { - $text = $this->getOriginalContent(); + if ( $content === false ) { + $content = $this->getOriginalContent(); } } } wfProfileOut( __METHOD__ ); - return $text; + return $content; } /** @@ -883,47 +940,77 @@ class EditPage { * to the original text of the edit. * * This difers from Article::getContent() that when a missing revision is - * encountered the result will be an empty string and not the + * encountered the result will be null and not the * 'missing-revision' message. * * @since 1.19 - * @return string + * @return Content|null */ private function getOriginalContent() { if ( $this->section == 'new' ) { - return $this->getCurrentText(); + return $this->getCurrentContent(); } $revision = $this->mArticle->getRevisionFetched(); if ( $revision === null ) { - return ''; + if ( !$this->contentModel ) $this->contentModel = $this->getTitle()->getContentModel(); + $handler = ContentHandler::getForModelID( $this->contentModel ); + + return $handler->makeEmptyContent(); } - return $this->mArticle->getContent(); + $content = $revision->getContent(); + return $content; } /** - * Get the actual text of the page. This is basically similar to - * WikiPage::getRawText() except that when the page doesn't exist an empty - * string is returned instead of false. + * Get the current content of the page. This is basically similar to + * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty + * content object is returned instead of null. * - * @since 1.19 - * @return string + * @since 1.21 + * @return Content */ - private function getCurrentText() { - $text = $this->mArticle->getRawText(); - if ( $text === false ) { - return ''; + protected function getCurrentContent() { + $rev = $this->mArticle->getRevision(); + $content = $rev ? $rev->getContent( Revision::RAW ) : null; + + if ( $content === false || $content === null ) { + if ( !$this->contentModel ) $this->contentModel = $this->getTitle()->getContentModel(); + $handler = ContentHandler::getForModelID( $this->contentModel ); + + return $handler->makeEmptyContent(); } else { - return $text; + # nasty side-effect, but needed for consistency + $this->contentModel = $rev->getContentModel(); + $this->contentFormat = $rev->getContentFormat(); + + return $content; } } + /** * Use this method before edit() to preload some text into the edit box * * @param $text string + * @deprecated since 1.21, use setPreloadedContent() instead. */ public function setPreloadedText( $text ) { - $this->mPreloadText = $text; + ContentHandler::deprecated( __METHOD__, "1.21" ); + + $content = $this->toEditContent( $text ); + + $this->setPreloadedContent( $content ); + } + + /** + * Use this method before edit() to preload some content into the edit box + * + * @param $content Content + * + * @since 1.21 + */ + public function setPreloadedContent( Content $content ) { + $this->mPreloadContent = $content; } /** @@ -931,23 +1018,47 @@ class EditPage { * an earlier setPreloadText() or by loading the given page. * * @param $preload String: representing the title to preload from. + * * @return String + * + * @deprecated since 1.21, use getPreloadedContent() instead */ protected function getPreloadedText( $preload ) { - global $wgUser, $wgParser; + ContentHandler::deprecated( __METHOD__, "1.21" ); + + $content = $this->getPreloadedContent( $preload ); + $text = $this->toEditText( $content ); + + return $text; + } - if ( !empty( $this->mPreloadText ) ) { - return $this->mPreloadText; + /** + * Get the contents to be preloaded into the box, either set by + * an earlier setPreloadText() or by loading the given page. + * + * @param $preload String: representing the title to preload from. + * + * @return Content + * + * @since 1.21 + */ + protected function getPreloadedContent( $preload ) { + global $wgUser; + + if ( !empty( $this->mPreloadContent ) ) { + return $this->mPreloadContent; } + $handler = ContentHandler::getForTitle( $this->getTitle() ); + if ( $preload === '' ) { - return ''; + return $handler->makeEmptyContent(); } $title = Title::newFromText( $preload ); # Check for existence to avoid getting MediaWiki:Noarticletext if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) { - return ''; + return $handler->makeEmptyContent(); } $page = WikiPage::factory( $title ); @@ -955,13 +1066,19 @@ class EditPage { $title = $page->getRedirectTarget(); # Same as before if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) { - return ''; + return $handler->makeEmptyContent(); } $page = WikiPage::factory( $title ); } $parserOptions = ParserOptions::newFromUser( $wgUser ); - return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions ); + $content = $page->getContent( Revision::RAW ); + + if ( !$content ) { + return $handler->makeEmptyContent(); + } + + return $content->preloadTransform( $title, $parserOptions ); } /** @@ -981,6 +1098,7 @@ class EditPage { /** * Attempt submission + * @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError * @return bool false if output is done, true if the rest of the form should be displayed */ function attemptSave() { @@ -1009,6 +1127,10 @@ class EditPage { case self::AS_HOOK_ERROR: return false; + case self::AS_PARSE_ERROR: + $wgOut->addWikiText( '
' . $status->getWikiText() . '
'); + return true; + case self::AS_SUCCESS_NEW_ARTICLE: $query = $resultDetails['redirect'] ? 'redirect=no' : ''; $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : ''; @@ -1101,9 +1223,19 @@ class EditPage { return $status; } + try { + # Construct Content object + $textbox_content = $this->toEditContent( $this->textbox1 ); + } catch (MWContentSerializationException $ex) { + $status->fatal( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() ); + $status->value = self::AS_PARSE_ERROR; + wfProfileOut( __METHOD__ ); + return $status; + } + # Check image redirect if ( $this->mTitle->getNamespace() == NS_FILE && - Title::newFromRedirect( $this->textbox1 ) instanceof Title && + $textbox_content->isRedirect() && !$wgUser->isAllowed( 'upload' ) ) { $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; $status->setResult( false, $code ); @@ -1245,13 +1377,13 @@ class EditPage { return $status; } - $text = $this->textbox1; + $content = $textbox_content; + $result['sectionanchor'] = ''; if ( $this->section == 'new' ) { if ( $this->sectiontitle !== '' ) { // Insert the section title above the content. - $text = wfMessage( 'newsectionheaderdefaultlevel' )->rawParams( $this->sectiontitle ) - ->inContentLanguage()->text() . "\n\n" . $text; + $content = $content->addSectionHeader( $this->sectiontitle ); // Jump to the new section $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); @@ -1261,30 +1393,32 @@ class EditPage { // passed. if ( $this->summary === '' ) { $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); - $this->summary = wfMessage( 'newsectionsummary' ) - ->rawParams( $cleanSectionTitle )->inContentLanguage()->text(); + $this->summary = wfMessage( 'newsectionsummary', $cleanSectionTitle ) + ->inContentLanguage()->text() ; } } elseif ( $this->summary !== '' ) { // Insert the section title above the content. - $text = wfMessage( 'newsectionheaderdefaultlevel' )->rawParams( $this->summary ) - ->inContentLanguage()->text() . "\n\n" . $text; + $content = $content->addSectionHeader( $this->summary ); // Jump to the new section $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); // Create a link to the new section from the edit summary. $cleanSummary = $wgParser->stripSectionName( $this->summary ); - $this->summary = wfMessage( 'newsectionsummary' ) - ->rawParams( $cleanSummary )->inContentLanguage()->text(); + $this->summary = wfMessage( 'newsectionsummary', $cleanSummary ) + ->inContentLanguage()->text(); } } $status->value = self::AS_SUCCESS_NEW_ARTICLE; - } else { + } else { # not $new # Article exists. Check for edit conflict. + + $this->mArticle->clear(); # Force reload of dates, etc. $timestamp = $this->mArticle->getTimestamp(); + wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" ); if ( $timestamp != $this->edittime ) { @@ -1301,7 +1435,8 @@ class EditPage { $this->isConflict = false; wfDebug( __METHOD__ . ": conflict suppressed; new section\n" ); } - } elseif ( $this->section == '' && Revision::userWasLastToEdit( DB_MASTER, $this->mTitle->getArticleID(), $wgUser->getId(), $this->edittime ) ) { + } elseif ( $this->section == '' && Revision::userWasLastToEdit( DB_MASTER, $this->mTitle->getArticleID(), + $wgUser->getId(), $this->edittime ) ) { # Suppress edit conflict with self, except for section edits where merging is required. wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); $this->isConflict = false; @@ -1316,26 +1451,32 @@ class EditPage { $sectionTitle = $this->summary; } + $content = null; + if ( $this->isConflict ) { - wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime ); + wfDebug( __METHOD__ . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'" + . " (article time '{$timestamp}')\n" ); + + $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime ); } else { - wfDebug( __METHOD__ . ": getting section '$this->section'\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle ); + wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" ); + $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle ); } - if ( is_null( $text ) ) { + + if ( is_null( $content ) ) { wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); $this->isConflict = true; - $text = $this->textbox1; // do not try to merge here! + $content = $textbox_content; // do not try to merge here! } elseif ( $this->isConflict ) { # Attempt merge - if ( $this->mergeChangesInto( $text ) ) { + if ( $this->mergeChangesIntoContent( $textbox_content ) ) { // Successful merge! Maybe we should tell the user the good news? $this->isConflict = false; + $content = $textbox_content; wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" ); } else { $this->section = ''; - $this->textbox1 = $text; + #$this->textbox1 = $text; #redundant, nothing to do here? wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" ); } } @@ -1347,7 +1488,10 @@ class EditPage { } // Run post-section-merge edit filter - if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) { + $hook_args = array( $this, $content, &$this->hookError, $this->summary ); + + if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged', $hook_args ) + || !wfRunHooks( 'EditFilterMergedContent', $hook_args ) ) { # Error messages etc. could be handled within the hook... $status->fatal( 'hookaborted' ); $status->value = self::AS_HOOK_ERROR; @@ -1363,8 +1507,8 @@ class EditPage { # Handle the user preference to force summaries here, but not for null edits if ( $this->section != 'new' && !$this->allowBlankSummary - && $this->getOriginalContent() != $text - && !Title::newFromRedirect( $text ) ) # check if it's not a redirect + && !$content->equals( $this->getOriginalContent() ) + && !$content->isRedirect() ) # check if it's not a redirect { if ( md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; @@ -1405,16 +1549,16 @@ class EditPage { // passed. if ( $this->summary === '' ) { $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); - $this->summary = wfMessage( 'newsectionsummary' ) - ->rawParams( $cleanSectionTitle )->inContentLanguage()->text(); + $this->summary = wfMessage( 'newsectionsummary', $cleanSectionTitle ) + ->inContentLanguage()->text(); } } elseif ( $this->summary !== '' ) { $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); # This is a new section, so create a link to the new section # in the revision summary. $cleanSummary = $wgParser->stripSectionName( $this->summary ); - $this->summary = wfMessage( 'newsectionsummary' ) - ->rawParams( $cleanSummary )->inContentLanguage()->text(); + $this->summary = wfMessage( 'newsectionsummary', $cleanSummary ) + ->inContentLanguage()->text(); } } elseif ( $this->section != '' ) { # Try to get a section anchor from the section source, redirect to edited section if header found @@ -1434,14 +1578,14 @@ class EditPage { // 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->textbox1 = $this->toEditText( $content ); $this->section = ''; $status->value = self::AS_SUCCESS_UPDATE; } // Check for length errors again now that the section is merged in - $this->kblength = (int)( strlen( $text ) / 1024 ); + $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 ); if ( $this->kblength > $wgMaxArticleSize ) { $this->tooBig = true; $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED ); @@ -1454,11 +1598,12 @@ class EditPage { ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) | ( $bot ? EDIT_FORCE_BOT : 0 ); - $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags ); + $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, + false, null, $this->contentFormat ); if ( $doEditStatus->isOK() ) { - $result['redirect'] = Title::newFromRedirect( $text ) !== null; - $this->updateWatchlist(); + $result['redirect'] = $content->isRedirect(); + $this->commitWatch(); wfProfileOut( __METHOD__ ); return $status; } else { @@ -1479,27 +1624,19 @@ class EditPage { } /** - * Register the change of watch status + * Commit the change of watch status */ - protected function updateWatchlist() { + protected function commitWatch() { global $wgUser; - if ( $wgUser->isLoggedIn() && $this->watchthis != $wgUser->isWatched( $this->mTitle ) ) { - $fname = __METHOD__; - $title = $this->mTitle; - $watch = $this->watchthis; - - // Do this in its own transaction to reduce contention... $dbw = wfGetDB( DB_MASTER ); - $dbw->onTransactionIdle( function() use ( $dbw, $title, $watch, $wgUser, $fname ) { - $dbw->begin( $fname ); - if ( $watch ) { - WatchAction::doWatch( $title, $wgUser ); - } else { - WatchAction::doUnwatch( $title, $wgUser ); - } - $dbw->commit( $fname ); - } ); + $dbw->begin( __METHOD__ ); + if ( $this->watchthis ) { + WatchAction::doWatch( $this->mTitle, $wgUser ); + } else { + WatchAction::doUnwatch( $this->mTitle, $wgUser ); + } + $dbw->commit( __METHOD__ ); } } @@ -1510,31 +1647,59 @@ class EditPage { * @param $editText string * * @return bool + * @deprecated since 1.21, use mergeChangesIntoContent() instead + */ + function mergeChangesInto( &$editText ){ + ContentHandler::deprecated( __METHOD__, "1.21" ); + + $editContent = $this->toEditContent( $editText ); + + $ok = $this->mergeChangesIntoContent( $editContent ); + + if ( $ok ) { + $editText = $this->toEditText( $editContent ); + return true; + } + return false; + } + + /** + * @private + * @todo document + * + * @param $editContent + * @return bool + * @since since 1.WD */ - function mergeChangesInto( &$editText ) { + private function mergeChangesIntoContent( &$editContent ){ wfProfileIn( __METHOD__ ); $db = wfGetDB( DB_MASTER ); // This is the revision the editor started from $baseRevision = $this->getBaseRevision(); - if ( is_null( $baseRevision ) ) { + $baseContent = $baseRevision ? $baseRevision->getContent() : null; + + if ( is_null( $baseContent ) ) { wfProfileOut( __METHOD__ ); return false; } - $baseText = $baseRevision->getText(); // The current state, we want to merge updates into it $currentRevision = Revision::loadFromTitle( $db, $this->mTitle ); - if ( is_null( $currentRevision ) ) { + $currentContent = $currentRevision ? $currentRevision->getContent() : null; + + if ( is_null( $currentContent ) ) { wfProfileOut( __METHOD__ ); return false; } - $currentText = $currentRevision->getText(); - $result = ''; - if ( wfMerge( $baseText, $editText, $currentText, $result ) ) { - $editText = $result; + $handler = ContentHandler::getForModelID( $baseContent->getModel() ); + + $result = $handler->merge3( $baseContent, $editContent, $currentContent ); + + if ( $result ) { + $editContent = $result; wfProfileOut( __METHOD__ ); return true; } else { @@ -1605,7 +1770,7 @@ class EditPage { $wgOut->addModules( 'mediawiki.action.edit' ); if ( $wgUser->getOption( 'uselivepreview', false ) ) { - $wgOut->addModules( 'mediawiki.action.edit.preview' ); + $wgOut->addModules( 'mediawiki.legacy.preview' ); } // Bug #19334: textarea jumps when editing articles in IE8 $wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' ); @@ -1704,10 +1869,13 @@ class EditPage { # Give a notice if the user is editing a deleted/moved page... if ( !$this->mTitle->exists() ) { LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle, - '', array( 'lim' => 10, - 'conds' => array( "log_action != 'revision'" ), - 'showIfEmpty' => false, - 'msgKey' => array( 'recreate-moveddeleted-warn' ) ) + '', + array( + 'lim' => 10, + 'conds' => array( "log_action != 'revision'" ), + 'showIfEmpty' => false, + 'msgKey' => array( 'recreate-moveddeleted-warn' ) + ) ); } } @@ -1725,17 +1893,77 @@ class EditPage { // Added using template syntax, to take 's into account. $wgOut->addWikiTextTitleTidy( '{{:' . $title->getFullText() . '}}', $this->mTitle ); return true; - } else { - return false; } - } else { - return false; } + return false; + } + + /** + * Gets an editable textual representation of $content. + * The textual representation can be turned by into a Content object by the + * toEditContent() method. + * + * If $content is null or false or a string, $content is returned unchanged. + * + * If the given Content object is not of a type that can be edited using the text base EditPage, + * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual + * content. + * + * @param Content|null|false|string $content + * @return String the editable text form of the content. + * + * @throws MWException if $content is not an instance of TextContent and $this->allowNonTextContent is not true. + */ + protected function toEditText( $content ) { + if ( $content === null || $content === false ) { + return $content; + } + + if ( is_string( $content ) ) { + return $content; + } + + if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) { + throw new MWException( "This content model can not be edited as text: " + . ContentHandler::getLocalizedName( $content->getModel() ) ); + } + + return $content->serialize( $this->contentFormat ); + } + + /** + * Turns the given text into a Content object by unserializing it. + * + * If the resulting Content object is not of a type that can be edited using the text base EditPage, + * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual + * content. + * + * @param String|null|bool $text Text to unserialize + * @return Content The content object created from $text. If $text was false or null, false resp. null will be + * returned instead. + * + * @throws MWException if unserializing the text results in a Content object that is not an instance of TextContent + * and $this->allowNonTextContent is not true. + */ + protected function toEditContent( $text ) { + if ( $text === false || $text === null ) { + return $text; + } + + $content = ContentHandler::makeContent( $text, $this->getTitle(), + $this->contentModel, $this->contentFormat ); + + if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) { + throw new MWException( "This content model can not be edited as text: " + . ContentHandler::getLocalizedName( $content->getModel() ) ); + } + + return $content; } /** * Send the edit form and related headers to $wgOut - * @param $formCallback Callback that takes an OutputPage parameter; will be called + * @param $formCallback Callback|null that takes an OutputPage parameter; will be called * during form output near the top, for captchas and the like. */ function showEditForm( $formCallback = null ) { @@ -1781,6 +2009,8 @@ class EditPage { } } + //@todo: add EditForm plugin interface and use it here! + // search for textarea1 and textares2, and allow EditForm to override all uses. $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID, 'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ), 'enctype' => 'multipart/form-data' ) ) ); @@ -1845,6 +2075,9 @@ class EditPage { $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) ); + $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) ); + $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) ); + if ( $this->section == 'new' ) { $this->showSummaryInput( true, $this->summary ); $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) ); @@ -1862,7 +2095,9 @@ class EditPage { // resolved between page source edits and custom ui edits using the // custom edit ui. $this->textbox2 = $this->textbox1; - $this->textbox1 = $this->getCurrentText(); + + $content = $this->getCurrentContent(); + $this->textbox1 = $this->toEditText( $content ); $this->showTextbox1(); } else { @@ -1888,7 +2123,13 @@ class EditPage { Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) ); if ( $this->isConflict ) { - $this->showConflict(); + try { + $this->showConflict(); + } catch ( MWContentSerializationException $ex ) { + // this can't really happen, but be nice if it does. + $msg = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() ); + $wgOut->addWikiText( '
' . $msg->text() . '
'); + } } $wgOut->addHTML( $this->editFormTextBottom . "\n\n" ); @@ -1962,7 +2203,7 @@ class EditPage { if ( $this->section != '' && $this->section != 'new' ) { if ( !$this->summary && !$this->preview && !$this->diff ) { - $sectionTitle = self::extractSectionTitle( $this->textbox1 ); + $sectionTitle = self::extractSectionTitle( $this->textbox1 ); //FIXME: use Content object if ( $sectionTitle !== false ) { $this->summary = "/* $sectionTitle */ "; } @@ -2028,13 +2269,10 @@ class EditPage { $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) ); } if ( $this->formtype !== 'preview' ) { - if ( $this->isCssSubpage ) { + if ( $this->isCssSubpage ) $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'usercssyoucanpreview' ) ); - } - - if ( $this->isJsSubpage ) { + if ( $this->isJsSubpage ) $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'userjsyoucanpreview' ) ); - } } } } @@ -2165,16 +2403,14 @@ class EditPage { * @return String */ protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) { - if ( !$summary || ( !$this->preview && !$this->diff ) ) { + if ( !$summary || ( !$this->preview && !$this->diff ) ) return ""; - } global $wgParser; - if ( $isSubjectPreview ) { + if ( $isSubjectPreview ) $summary = wfMessage( 'newsectionsummary', $wgParser->stripSectionName( $summary ) ) ->inContentLanguage()->text(); - } $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview'; @@ -2193,9 +2429,8 @@ class EditPage { HTML ); - if ( !$this->checkUnicodeCompliantBrowser() ) { + if ( !$this->checkUnicodeCompliantBrowser() ) $wgOut->addHTML( Html::hidden( 'safemode', '1' ) ); - } } protected function showFormAfterText() { @@ -2275,10 +2510,10 @@ HTML $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) ); } - protected function showTextbox( $content, $name, $customAttribs = array() ) { + protected function showTextbox( $text, $name, $customAttribs = array() ) { global $wgOut, $wgUser; - $wikitext = $this->safeUnicodeOutput( $content ); + $wikitext = $this->safeUnicodeOutput( $text ); if ( strval( $wikitext ) !== '' ) { // Ensure there's a newline at the end, otherwise adding lines // is awkward. @@ -2311,9 +2546,8 @@ HTML $attribs = array( 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ); - if ( $this->formtype != 'preview' ) { + if ( $this->formtype != 'preview' ) $attribs['style'] = 'display: none;'; - } $wgOut->addHTML( Xml::openElement( 'div', $attribs ) ); @@ -2324,7 +2558,12 @@ HTML $wgOut->addHTML( '' ); if ( $this->formtype == 'diff' ) { - $this->showDiff(); + try { + $this->showDiff(); + } catch ( MWContentSerializationException $ex ) { + $msg = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() ); + $wgOut->addWikiText( '
' . $msg->text() . '
'); + } } } @@ -2356,7 +2595,7 @@ HTML * save and then make a comparison. */ function showDiff() { - global $wgUser, $wgContLang, $wgParser, $wgOut; + global $wgUser, $wgContLang, $wgOut; $oldtitlemsg = 'currentrev'; # if message does not exist, show diff against the preloaded default @@ -2364,24 +2603,43 @@ HTML $oldtext = $this->mTitle->getDefaultMessageText(); if( $oldtext !== false ) { $oldtitlemsg = 'defaultmessagetext'; + $oldContent = $this->toEditContent( $oldtext ); + } else { + $oldContent = null; } } else { - $oldtext = $this->mArticle->getRawText(); + $oldContent = $this->getOriginalContent(); } - $newtext = $this->mArticle->replaceSection( - $this->section, $this->textbox1, $this->summary, $this->edittime ); - wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) ); + $textboxContent = $this->toEditContent( $this->textbox1 ); + + $newContent = $this->mArticle->replaceSectionContent( + $this->section, $textboxContent, + $this->summary, $this->edittime ); - $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang ); - $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts ); + if ( $newContent ) { + ContentHandler::runLegacyHooks( 'EditPageGetDiffText', array( $this, &$newContent ) ); + wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) ); - if ( $oldtext !== false || $newtext != '' ) { + $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang ); + $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts ); + } + + if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) { $oldtitle = wfMessage( $oldtitlemsg )->parse(); $newtitle = wfMessage( 'yourtext' )->parse(); - $de = new DifferenceEngine( $this->mArticle->getContext() ); - $de->setText( $oldtext, $newtext ); + if ( !$oldContent ) { + $oldContent = $newContent->getContentHandler()->makeEmptyContent(); + } + + if ( !$newContent ) { + $newContent = $oldContent->getContentHandler()->makeEmptyContent(); + } + + $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() ); + $de->setContent( $oldContent, $newContent ); + $difftext = $de->getDiff( $oldtitle, $newtitle ); $de->showDiffStyle(); } else { @@ -2498,8 +2756,12 @@ HTML if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) { $wgOut->wrapWikiMsg( '

$1

', "yourdiff" ); - $de = new DifferenceEngine( $this->mArticle->getContext() ); - $de->setText( $this->textbox2, $this->textbox1 ); + $content1 = $this->toEditContent( $this->textbox1 ); + $content2 = $this->toEditContent( $this->textbox2 ); + + $handler = ContentHandler::getForModelID( $this->contentModel ); + $de = $handler->createDifferenceEngine( $this->mArticle->getContext() ); + $de->setContent( $content2, $content1 ); $de->showDiff( wfMessage( 'yourtext' )->parse(), wfMessage( 'storedversion' )->text() @@ -2590,23 +2852,21 @@ HTML ); // Quick paranoid permission checks... if ( is_object( $data ) ) { - if ( $data->log_deleted & LogPage::DELETED_USER ) { + if ( $data->log_deleted & LogPage::DELETED_USER ) $data->user_name = wfMessage( 'rev-deleted-user' )->escaped(); - } - - if ( $data->log_deleted & LogPage::DELETED_COMMENT ) { + if ( $data->log_deleted & LogPage::DELETED_COMMENT ) $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped(); - } } return $data; } /** * Get the rendered text for previewing. + * @throws MWException * @return string */ function getPreviewText() { - global $wgOut, $wgUser, $wgParser, $wgRawHtml, $wgLang; + global $wgOut, $wgUser, $wgRawHtml, $wgLang; wfProfileIn( __METHOD__ ); @@ -2625,82 +2885,94 @@ HTML return $parsedNote; } - if ( $this->mTriedSave && !$this->mTokenOk ) { - if ( $this->mTokenOkExceptSuffix ) { - $note = wfMessage( 'token_suffix_mismatch' )->plain(); - } else { - $note = wfMessage( 'session_fail_preview' )->plain(); - } - } elseif ( $this->incompleteForm ) { - $note = wfMessage( 'edit_form_incomplete' )->plain(); - } else { - $note = wfMessage( 'previewnote' )->plain() . - ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]'; - } + $note = ''; - $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() ); + try { + $content = $this->toEditContent( $this->textbox1 ); - $parserOptions->setEditSection( false ); - $parserOptions->setIsPreview( true ); - $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' ); + if ( $this->mTriedSave && !$this->mTokenOk ) { + if ( $this->mTokenOkExceptSuffix ) { + $note = wfMessage( 'token_suffix_mismatch' )->plain() ; - # don't parse non-wikitext pages, show message about preview - if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) { - if ( $this->mTitle->isCssJsSubpage() ) { - $level = 'user'; - } elseif ( $this->mTitle->isCssOrJsPage() ) { - $level = 'site'; - } else { - $level = false; - } - - # Used messages to make sure grep find them: - # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview - $class = 'mw-code'; - if ( $level ) { - if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) { - $previewtext = "
\n" . wfMessage( "{$level}csspreview" )->text() . "\n
"; - $class .= " mw-css"; - } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) { - $previewtext = "
\n" . wfMessage( "{$level}jspreview" )->text() . "\n
"; - $class .= " mw-js"; } else { - throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' ); + $note = wfMessage( 'session_fail_preview' )->plain() ; } - $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions ); - $previewHTML = $parserOutput->getText(); + } elseif ( $this->incompleteForm ) { + $note = wfMessage( 'edit_form_incomplete' )->plain() ; } else { - $previewHTML = ''; + $note = wfMessage( 'previewnote' )->plain() . + ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]'; } - $previewHTML .= "
\n" . htmlspecialchars( $this->textbox1 ) . "\n
\n"; - } else { - $toparse = $this->textbox1; + $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() ); + $parserOptions->setEditSection( false ); + $parserOptions->setIsPreview( true ); + $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' ); - # If we're adding a comment, we need to show the - # summary as the headline - if ( $this->section == "new" && $this->summary != "" ) { - $toparse = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )->inContentLanguage()->text() . "\n\n" . $toparse; - } + # don't parse non-wikitext pages, show message about preview + if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) { + if( $this->mTitle->isCssJsSubpage() ) { + $level = 'user'; + } elseif( $this->mTitle->isCssOrJsPage() ) { + $level = 'site'; + } else { + $level = false; + } - wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) ); + if ( $content->getModel() == CONTENT_MODEL_CSS ) { + $format = 'css'; + } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) { + $format = 'js'; + } else { + $format = false; + } - $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions ); - $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions ); + # Used messages to make sure grep find them: + # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview + if( $level && $format ) { + $note = "
" . wfMessage( "{$level}{$format}preview" )->text() . "
"; + } else { + $note = wfMessage( 'previewnote' )->text() ; + } + } else { + $note = wfMessage( 'previewnote' )->text() ; + } - $rt = Title::newFromRedirectArray( $this->textbox1 ); + $rt = $content->getRedirectChain(); if ( $rt ) { $previewHTML = $this->mArticle->viewRedirect( $rt, false ); } else { - $previewHTML = $parserOutput->getText(); - } - $this->mParserOutput = $parserOutput; - $wgOut->addParserOutputNoText( $parserOutput ); + # If we're adding a comment, we need to show the + # summary as the headline + if ( $this->section === "new" && $this->summary !== "" ) { + $content = $content->addSectionHeader( $this->summary ); + } + + $hook_args = array( $this, &$content ); + ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args ); + wfRunHooks( 'EditPageGetPreviewContent', $hook_args ); - if ( count( $parserOutput->getWarnings() ) ) { - $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); + $parserOptions->enableLimitReport(); + + # For CSS/JS pages, we should have called the ShowRawCssJs hook here. + # But it's now deprecated, so never mind + + $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions ); + $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions ); + + $previewHTML = $parserOutput->getText(); + $this->mParserOutput = $parserOutput; + $wgOut->addParserOutputNoText( $parserOutput ); + + if ( count( $parserOutput->getWarnings() ) ) { + $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); + } } + } catch ( MWContentSerializationException $ex ) { + $m = wfMessage('content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() ); + $note .= "\n\n" . $m->parse(); + $previewHTML = ''; } if ( $this->isConflict ) { @@ -3092,7 +3364,7 @@ HTML /** * Produce the stock "your edit contains spam" page * - * @param $match string Text which triggered one or more filters + * @param $match string|bool Text which triggered one or more filters * @deprecated since 1.17 Use method spamPageWithContent() instead */ static function spamPage( $match = false ) { diff --git a/includes/Export.php b/includes/Export.php index c2409de98f..e2b01b5397 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -63,7 +63,7 @@ class WikiExporter { * @return string */ public static function schemaVersion() { - return "0.7"; + return "0.8"; } /** @@ -498,7 +498,7 @@ class XmlDumpWriter { '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", + "http://www.mediawiki.org/xml/export-$ver.xsd", #TODO: how do we get a new version up there? 'version' => $ver, 'xml:lang' => $wgLanguageCode ), null ) . @@ -657,12 +657,6 @@ class XmlDumpWriter { $out .= " " . Xml::elementClean( 'comment', array(), strval( $row->rev_comment ) ) . "\n"; } - if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) { - $out .= " " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n"; - } else { - $out .= " \n"; - } - $text = ''; if ( $row->rev_deleted & Revision::DELETED_TEXT ) { $out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n"; @@ -679,6 +673,34 @@ class XmlDumpWriter { "" ) . "\n"; } + if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) { + $out .= " " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n"; + } else { + $out .= " \n"; + } + + if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model ) ) { + $content_model = strval( $row->rev_content_model ); + } else { + // probably using $wgContentHandlerUseDB = false; + // @todo: test! + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $content_model = ContentHandler::getDefaultModelFor( $title ); + } + + $out .= " " . Xml::element('model', null, strval( $content_model ) ) . "\n"; + + if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) { + $content_format = strval( $row->rev_content_format ); + } else { + // probably using $wgContentHandlerUseDB = false; + // @todo: test! + $content_handler = ContentHandler::getForModelID( $content_model ); + $content_format = $content_handler->getDefaultFormat(); + } + + $out .= " " . Xml::element('format', null, strval( $content_format ) ) . "\n"; + wfRunHooks( 'XmlDumpWriterWriteRevision', array( &$this, &$out, $row, $text ) ); $out .= " \n"; @@ -1325,6 +1347,7 @@ class DumpNamespaceFilter extends DumpFilter { /** * @param $sink DumpOutput * @param $param + * @throws MWException */ function __construct( &$sink, $param ) { parent::__construct( $sink ); diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php index 26e456c261..1b7c29dbbb 100644 --- a/includes/ExternalStore.php +++ b/includes/ExternalStore.php @@ -130,8 +130,8 @@ class ExternalStore { * * @param $data String * @param $storageParams Array: associative array of parameters for the ExternalStore object. - * @throws DBConnectionError|DBQueryError|MWException - * @return string The URL of the stored data item, or false on error + * @throws MWException|DBConnectionError|DBQueryError + * @return string|bool The URL of the stored data item, or false on error */ public static function insertToDefault( $data, $storageParams = array() ) { global $wgDefaultExternalStore; diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php index 4f35394086..37b1b9335d 100644 --- a/includes/ExternalStoreDB.php +++ b/includes/ExternalStoreDB.php @@ -100,8 +100,8 @@ class ExternalStoreDB { */ function fetchFromURL( $url ) { $path = explode( '/', $url ); - $cluster = $path[2]; - $id = $path[3]; + $cluster = $path[2]; + $id = $path[3]; if ( isset( $path[4] ) ) { $itemID = $path[4]; } else { diff --git a/includes/ExternalUser.php b/includes/ExternalUser.php index 9a01deb70b..23944a5d01 100644 --- a/includes/ExternalUser.php +++ b/includes/ExternalUser.php @@ -288,7 +288,7 @@ abstract class ExternalUser { 'eu_external_id' => $this->getId() ), __METHOD__ ); } - + /** * Check whether this external user id is already linked with * a local user. @@ -305,5 +305,5 @@ abstract class ExternalUser { ? User::newFromId( $row->eu_local_id ) : null; } - + } diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php index 11b2675d87..82c6e4a0ea 100644 --- a/includes/FeedUtils.php +++ b/includes/FeedUtils.php @@ -87,7 +87,7 @@ class FeedUtils { ($row->rc_deleted & Revision::DELETED_COMMENT) ? wfMessage('rev-deleted-comment')->escaped() : $row->rc_comment, - $actiontext + $actiontext ); } @@ -138,13 +138,23 @@ class FeedUtils { $diffText = ''; // Don't bother generating the diff if we won't be able to show it if ( $wgFeedDiffCutoff > 0 ) { - $de = new DifferenceEngine( $title, $oldid, $newid ); - $diffText = $de->getDiff( - wfMessage( 'previousrevision' )->text(), // hack - wfMessage( 'revisionasof', - $wgLang->timeanddate( $timestamp ), - $wgLang->date( $timestamp ), - $wgLang->time( $timestamp ) )->text() ); + $rev = Revision::newFromId( $oldid ); + + if ( !$rev ) { + $diffText = false; + } else { + $context = clone RequestContext::getMain(); + $context->setTitle( $title ); + + $contentHandler = $rev->getContentHandler(); + $de = $contentHandler->createDifferenceEngine( $context, $oldid, $newid ); + $diffText = $de->getDiff( + wfMessage( 'previousrevision' )->text(), // hack + wfMessage( 'revisionasof', + $wgLang->timeanddate( $timestamp ), + $wgLang->date( $timestamp ), + $wgLang->time( $timestamp ) )->text() ); + } } if ( $wgFeedDiffCutoff <= 0 || ( strlen( $diffText ) > $wgFeedDiffCutoff ) ) { @@ -162,16 +172,36 @@ class FeedUtils { } else { $rev = Revision::newFromId( $newid ); if( $wgFeedDiffCutoff <= 0 || is_null( $rev ) ) { - $newtext = ''; + $newContent = ContentHandler::getForTitle( $title )->makeEmptyContent(); + } else { + $newContent = $rev->getContent(); + } + + if ( $newContent instanceof TextContent ) { + // only textual content has a "source view". + $text = $newContent->getNativeData(); + + if ( $wgFeedDiffCutoff <= 0 || strlen( $text ) > $wgFeedDiffCutoff ) { + $html = null; + } else { + $html = nl2br( htmlspecialchars( $text ) ); + } } else { - $newtext = $rev->getText(); + //XXX: we could get an HTML representation of the content via getParserOutput, but that may + // contain JS magic and generally may not be suitable for inclusion in a feed. + // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. + //Compare also ApiFeedContributions::feedItemDesc + $html = null; } - if ( $wgFeedDiffCutoff <= 0 || strlen( $newtext ) > $wgFeedDiffCutoff ) { + + if ( $html === null ) { + // Omit large new page diffs, bug 29110 + // Also use diff link for non-textual content $diffText = self::getDiffLink( $title, $newid ); } else { $diffText = '

' . wfMessage( 'newpage' )->text() . '

' . - '
' . nl2br( htmlspecialchars( $newtext ) ) . '
'; + '
' . $html . '
'; } } $completeText .= $diffText; diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index 7e616b2493..e2fcd9cb15 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -144,6 +144,7 @@ class FileDeleteForm { * @param $reason String: reason of the deletion * @param $suppress Boolean: whether to mark all deleted versions as restricted * @param $user User object performing the request + * @throws MWException * @return bool|Status */ public static function doDelete( &$title, &$file, &$oldimage, $reason, $suppress, User $user = null ) { diff --git a/includes/FormOptions.php b/includes/FormOptions.php index 33bbd86a6f..1cfe88e891 100644 --- a/includes/FormOptions.php +++ b/includes/FormOptions.php @@ -22,7 +22,7 @@ * * @file * @author Niklas Laxström - * @author Antoine Musso + * @author Antoine Musso */ /** @@ -83,6 +83,7 @@ class FormOptions implements ArrayAccess { * which will be assumed as INT if the data is an integer. * * @param $data Mixed: value to guess type for + * @throws MWException * @exception MWException Unsupported datatype * @return int Type constant */ @@ -105,6 +106,7 @@ class FormOptions implements ArrayAccess { * * @param $name String: option name * @param $strict Boolean: throw an exception when the option does not exist (default false) + * @throws MWException * @return Boolean: true if option exist, false otherwise */ public function validateName( $name, $strict = false ) { @@ -205,11 +207,12 @@ class FormOptions implements ArrayAccess { /** * Validate and set an option integer value - * The value will be altered to fit in the range. + * The value will be altered to fit in the range. * * @param $name String: option name * @param $min Int: minimum value * @param $max Int: maximum value + * @throws MWException * @exception MWException Option is not of type int * @return null */ diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 17e3f4d5a0..3de25e716d 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -169,6 +169,7 @@ function wfArrayLookup( $a, $b ) { * @param $value Mixed * @param $default Mixed * @param $changed Array to alter + * @throws MWException */ function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) { if ( is_null( $changed ) ) { @@ -1110,6 +1111,7 @@ function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) { * * @param $text String * @param $file String filename + * @throws MWException */ function wfErrorLog( $text, $file ) { if ( substr( $file, 0, 4 ) == 'udp:' ) { @@ -1719,6 +1721,7 @@ function wfEmptyMsg( $key ) { * but now throws an exception instead, with similar results. * * @param $msg String: message shown when dying. + * @throws MWException */ function wfDebugDieBacktrace( $msg = '' ) { throw new MWException( $msg ); @@ -2512,6 +2515,7 @@ function wfTempDir() { * @param $dir String: full path to directory to create * @param $mode Integer: chmod value to use, default is $wgDirectoryMode * @param $caller String: optional caller param for debugging. + * @throws MWException * @return bool */ function wfMkdirParents( $dir, $mode = null, $caller = null ) { @@ -3022,6 +3026,7 @@ function wfDiff( $before, $after, $params = '-u' ) { * * @param $req_ver Mixed: the version to check, can be a string, an integer, or * a float + * @throws MWException */ function wfUsePHP( $req_ver ) { $php_ver = PHP_VERSION; @@ -3043,6 +3048,7 @@ function wfUsePHP( $req_ver ) { * * @param $req_ver Mixed: the version to check, can be a string, an integer, or * a float + * @throws MWException */ function wfUseMW( $req_ver ) { global $wgVersion; @@ -3551,16 +3557,6 @@ function wfBoolToStr( $value ) { return $value ? 'true' : 'false'; } -/** - * Load an extension messages file - * - * @deprecated since 1.16, warnings in 1.18, remove in 1.20 - * @codeCoverageIgnore - */ -function wfLoadExtensionMessages() { - wfDeprecated( __FUNCTION__, '1.16' ); -} - /** * Get a platform-independent path to the null file, e.g. /dev/null * diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index 5c00b9f6e7..ef24b62b26 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -248,6 +248,7 @@ class HTMLForm extends ContextSource { * Set format in which to display the form * @param $format String the name of the format to use, must be one of * $this->availableDisplayFormats + * @throws MWException * @since 1.20 * @return HTMLForm $this for chaining calls (since 1.20) */ @@ -279,6 +280,7 @@ class HTMLForm extends ContextSource { * Initialise a new Object for the field * @param $fieldname string * @param $descriptor string input Descriptor, as described above + * @throws MWException * @return HTMLFormField subclass */ static function loadInputFromParameters( $fieldname, $descriptor ) { @@ -312,6 +314,7 @@ class HTMLForm extends ContextSource { * @attention When doing method chaining, that should be the very last * method call before displayForm(). * + * @throws MWException * @return HTMLForm $this for chaining calls (since 1.20) */ function prepareForm() { @@ -375,9 +378,10 @@ class HTMLForm extends ContextSource { /** * Validate all the fields, and call the submision callback * function if everything is kosher. + * @throws MWException * @return Mixed Bool true == Successful submission, Bool false - * == No submission attempted, anything else == Error to - * display. + * == No submission attempted, anything else == Error to + * display. */ function trySubmit() { # Check for validation @@ -1177,6 +1181,7 @@ abstract class HTMLFormField { /** * Initialise the object * @param $params array Associative Array. See HTMLForm doc for syntax. + * @throws MWException */ function __construct( $params ) { $this->mParams = $params; @@ -1321,7 +1326,6 @@ abstract class HTMLFormField { public function getRaw( $value ) { list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); $inputHtml = $this->getInputHTML( $value ); - $fieldType = get_class( $this ); $helptext = $this->getHelpTextHtmlRaw( $this->getHelpText() ); $cellAttributes = array(); $label = $this->getLabelHtml( $cellAttributes ); diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index bb8ec5e3dd..05c27feacb 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -19,10 +19,10 @@ * * @file */ - + /** - * Base class for general text storage via the "object" flag in old_flags, or - * two-part external storage URLs. Used for represent efficient concatenated + * Base class for general text storage via the "object" flag in old_flags, or + * two-part external storage URLs. Used for represent efficient concatenated * storage, and migration-related pointer objects. */ interface HistoryBlob @@ -178,7 +178,7 @@ class ConcatenatedGzipHistoryBlob implements HistoryBlob * @return bool */ public function isHappy() { - return $this->mSize < $this->mMaxSize + return $this->mSize < $this->mMaxSize && count( $this->mItems ) < $this->mMaxCount; } } @@ -341,12 +341,12 @@ class DiffHistoryBlob implements HistoryBlob { /** Total uncompressed size */ var $mSize = 0; - /** - * Array of diffs. If a diff D from A to B is notated D = B - A, and Z is + /** + * Array of diffs. If a diff D from A to B is notated D = B - A, and Z is * an empty string: * * { item[map[i]] - item[map[i-1]] where i > 0 - * diff[i] = { + * diff[i] = { * { item[map[i]] - Z where i = 0 */ var $mDiffs; @@ -379,7 +379,7 @@ class DiffHistoryBlob implements HistoryBlob { * The maximum number of text items before the object becomes sad */ var $mMaxCount = 100; - + /** Constants from xdiff.h */ const XDL_BDOP_INS = 1; const XDL_BDOP_CPY = 2; @@ -433,7 +433,7 @@ class DiffHistoryBlob implements HistoryBlob { * @throws MWException */ function compress() { - if ( !function_exists( 'xdiff_string_rabdiff' ) ){ + if ( !function_exists( 'xdiff_string_rabdiff' ) ){ throw new MWException( "Need xdiff 1.5+ support to write DiffHistoryBlob\n" ); } if ( isset( $this->mDiffs ) ) { @@ -534,7 +534,7 @@ class DiffHistoryBlob implements HistoryBlob { # Pure PHP implementation $header = unpack( 'Vofp/Vcsize', substr( $diff, 0, 8 ) ); - + # Check the checksum if hash/mhash is available $ofp = $this->xdiffAdler32( $base ); if ( $ofp !== false && $ofp !== substr( $diff, 0, 4 ) ) { @@ -545,7 +545,7 @@ class DiffHistoryBlob implements HistoryBlob { wfDebug( __METHOD__. ": incorrect base length\n" ); return false; } - + $p = 8; $out = ''; while ( $p < strlen( $diff ) ) { @@ -579,7 +579,7 @@ class DiffHistoryBlob implements HistoryBlob { } /** - * Compute a binary "Adler-32" checksum as defined by LibXDiff, i.e. with + * Compute a binary "Adler-32" checksum as defined by LibXDiff, i.e. with * the bytes backwards and initialised with 0 instead of 1. See bug 34428. * * Returns false if no hashing library is available @@ -589,8 +589,8 @@ class DiffHistoryBlob implements HistoryBlob { if ( $init === null ) { $init = str_repeat( "\xf0", 205 ) . "\xee" . str_repeat( "\xf0", 67 ) . "\x02"; } - // The real Adler-32 checksum of $init is zero, so it initialises the - // state to zero, as it is at the start of LibXDiff's checksum + // The real Adler-32 checksum of $init is zero, so it initialises the + // state to zero, as it is at the start of LibXDiff's checksum // algorithm. Appending the subject string then simulates LibXDiff. if ( function_exists( 'hash' ) ) { $hash = hash( 'adler32', $init . $s, true ); @@ -664,7 +664,7 @@ class DiffHistoryBlob implements HistoryBlob { if ( isset( $info['base'] ) ) { // Old format $this->mDiffMap = range( 0, count( $this->mDiffs ) - 1 ); - array_unshift( $this->mDiffs, + array_unshift( $this->mDiffs, pack( 'VVCV', 0, 0, self::XDL_BDOP_INSB, strlen( $info['base'] ) ) . $info['base'] ); } else { @@ -687,7 +687,7 @@ class DiffHistoryBlob implements HistoryBlob { * @return bool */ function isHappy() { - return $this->mSize < $this->mMaxSize + return $this->mSize < $this->mMaxSize && count( $this->mItems ) < $this->mMaxCount; } diff --git a/includes/Hooks.php b/includes/Hooks.php index bc39f2fc25..c9c06793f8 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -23,23 +23,48 @@ * @file */ +/** + * @since 1.18 + */ class MWHookException extends MWException {} /** * Hooks class. * * Used to supersede $wgHooks, because globals are EVIL. + * + * @since 1.18 */ class Hooks { protected static $handlers = array(); + /** + * Clears hooks registered via Hooks::register(). Does not touch $wgHooks. + * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined. + * + * @since 1.21 + * + * @param $name String: the name of the hook to clear. + * + * @throws MWException if not in testing mode. + */ + public static function clear( $name ) { + if ( !defined( 'MW_PHPUNIT_TEST' ) ) { + throw new MWException( 'can not reset hooks in operation.' ); + } + + unset( self::$handlers[$name] ); + } + + /** * Attach an event handler to a given hook * - * @param $name Mixed: name of hook + * @since 1.18 + * + * @param $name String: name of hook * @param $callback Mixed: callback function to attach - * @return void */ public static function register( $name, $callback ) { if( !isset( self::$handlers[$name] ) ) { @@ -51,69 +76,81 @@ class Hooks { /** * Returns true if a hook has a function registered to it. + * The function may have been registered either via Hooks::register or in $wgHooks. + * + * @since 1.18 * - * @param $name Mixed: name of hook - * @return Boolean: true if a hook has a function registered to it + * @param $name String: name of hook + * @return Boolean: true if the hook has a function registered to it */ public static function isRegistered( $name ) { - if( !isset( self::$handlers[$name] ) ) { - self::$handlers[$name] = array(); - } + global $wgHooks; - return ( count( self::$handlers[$name] ) != 0 ); + return !empty( $wgHooks[$name] ) || !empty( self::$handlers[$name] ); } /** * Returns an array of all the event functions attached to a hook + * This combines functions registered via Hooks::register and with $wgHooks. + * @since 1.18 + * + * @throws MWException + * @throws FatalError + * @param $name String: name of the hook * - * @param $name Mixed: name of the hook * @return array */ public static function getHandlers( $name ) { - if( !isset( self::$handlers[$name] ) ) { + global $wgHooks; + + // Return quickly in the most common case + if ( empty( self::$handlers[$name] ) && empty( $wgHooks[$name] ) ) { return array(); } - return self::$handlers[$name]; + if ( !is_array( self::$handlers ) ) { + throw new MWException( "Local hooks array is not an array!\n" ); + } + + if ( !is_array( $wgHooks ) ) { + throw new MWException( "Global hooks array is not an array!\n" ); + } + + if ( empty( Hooks::$handlers[$name] ) ) { + $hooks = $wgHooks[$name]; + } elseif ( empty( $wgHooks[$name] ) ) { + $hooks = Hooks::$handlers[$name]; + } else { + // so they are both not empty... + $hooks = array_merge( Hooks::$handlers[$name], $wgHooks[$name] ); + } + + if ( !is_array( $hooks ) ) { + throw new MWException( "Hooks array for event '$name' is not an array!\n" ); + } + + return $hooks; } /** * Call hook functions defined in Hooks::register * - * 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. - * * @param $event String: event name - * @param $args Array: parameters passed to hook functions + * @param $args Array: parameters passed to hook functions + * * @return Boolean True if no handler aborted the hook */ public static function run( $event, $args = array() ) { global $wgHooks; // Return quickly in the most common case - if ( !isset( self::$handlers[$event] ) && !isset( $wgHooks[$event] ) ) { + if ( empty( self::$handlers[$event] ) && empty( $wgHooks[$event] ) ) { return true; } - if ( !is_array( self::$handlers ) ) { - throw new MWException( "Local hooks array is not an array!\n" ); - } - - if ( !is_array( $wgHooks ) ) { - throw new MWException( "Global hooks array is not an array!\n" ); - } - - $new_handlers = (array) self::$handlers; - $old_handlers = (array) $wgHooks; - - $hook_array = array_merge( $new_handlers, $old_handlers ); + $hooks = self::getHandlers( $event ); - if ( !is_array( $hook_array[$event] ) ) { - throw new MWException( "Hooks array for event '$event' is not an array!\n" ); - } - - foreach ( $hook_array[$event] as $index => $hook ) { + foreach ( $hooks as $hook ) { $object = null; $method = null; $func = null; @@ -131,7 +168,7 @@ class Hooks { if ( count( $hook ) < 1 ) { throw new MWException( 'Empty array in hooks for ' . $event . "\n" ); } elseif ( is_object( $hook[0] ) ) { - $object = $hook_array[$event][$index][0]; + $object = $hook[0]; if ( $object instanceof Closure ) { $closure = true; if ( count( $hook ) > 1 ) { @@ -161,7 +198,7 @@ class Hooks { } elseif ( is_string( $hook ) ) { # functions look like strings, too $func = $hook; } elseif ( is_object( $hook ) ) { - $object = $hook_array[$event][$index]; + $object = $hook; if ( $object instanceof Closure ) { $closure = true; } else { @@ -259,8 +296,11 @@ class Hooks { /** * This REALLY should be protected... but it's public for compatibility * + * @since 1.18 + * * @param $errno int Unused * @param $errstr String: error message + * @throws MWHookException * @return Boolean: false */ public static function hookErrorHandler( $errno, $errstr ) { diff --git a/includes/Html.php b/includes/Html.php index dfd081f25f..5be67ab6d9 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -480,7 +480,7 @@ class Html { 'class', // html4, html5 'accesskey', // as of html5, multiple space-separated values allowed // html4-spec doesn't document rel= as space-separated - // but has been used like that and is now documented as such + // but has been used like that and is now documented as such // in the html5-spec. 'rel', ); @@ -493,7 +493,6 @@ class Html { // values. Implode/explode to get those into the main array as well. if ( is_array( $value ) ) { // If input wasn't an array, we can skip this step - $newValue = array(); foreach ( $value as $k => $v ) { if ( is_string( $v ) ) { @@ -576,7 +575,6 @@ class Html { # @todo FIXME: Is this really true? $map['<'] = '<'; } - $ret .= " $key=$quote" . strtr( $value, $map ) . $quote; } } @@ -753,7 +751,7 @@ class Html { * - name: [optional], default: 'namespace' * @return string HTML code to select a namespace. */ - public static function namespaceSelector( Array $params = array(), Array $selectAttribs = array() ) { + public static function namespaceSelector( array $params = array(), array $selectAttribs = array() ) { global $wgContLang; ksort( $selectAttribs ); @@ -796,10 +794,12 @@ class Html { if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { continue; } - if ( $nsId === 0 ) { + if ( $nsId === NS_MAIN ) { // For other namespaces use use the namespace prefix as label, but for // main we don't use "" but the user message descripting it (e.g. "(Main)" or "(Article)") $nsName = wfMessage( 'blanknamespace' )->text(); + } elseif ( is_int( $nsId ) ) { + $nsName = $wgContLang->convertNamespace( $nsId ); } $optionsHtml[] = Html::element( 'option', array( @@ -810,6 +810,14 @@ class Html { ); } + if ( !array_key_exists( 'id', $selectAttribs ) ) { + $selectAttribs['id'] = 'namespace'; + } + + if ( !array_key_exists( 'name', $selectAttribs ) ) { + $selectAttribs['name'] = 'namespace'; + } + $ret = ''; if ( isset( $params['label'] ) ) { $ret .= Html::element( @@ -932,4 +940,22 @@ class Html { return $s; } + + /** + * Generate a srcset attribute value from an array mapping pixel densities + * to URLs. Note that srcset supports width and height values as well, which + * are not used here. + * + * @param array $urls + * @return string + */ + static function srcSet( $urls ) { + $candidates = array(); + foreach( $urls as $density => $url ) { + // Image candidate syntax per current whatwg live spec, 2012-09-23: + // http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content-1.html#attr-img-srcset + $candidates[] = "{$url} {$density}x"; + } + return implode( ", ", $candidates ); + } } diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index 8453e62cc9..32f77dcfab 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -265,6 +265,7 @@ class MWHttpRequest { * Generate a new request object * @param $url String: url to use * @param $options Array: (optional) extra params to pass (see Http::request()) + * @throws MWException * @return CurlHttpRequest|PhpHttpRequest * @see MWHttpRequest::__construct */ @@ -393,6 +394,7 @@ class MWHttpRequest { * will be aborted. * * @param $callback Callback + * @throws MWException */ public function setCallback( $callback ) { if ( !is_callable( $callback ) ) { diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 6f1b1a15d4..316e1c95b1 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -157,7 +157,9 @@ class ImagePage extends Article { $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content', 'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(), 'class' => 'mw-content-'.$pageLang->getDir() ) ) ); + parent::view(); + $out->addHTML( Xml::closeElement( 'div' ) ); } else { # Just need to set the right headers @@ -272,18 +274,18 @@ class ImagePage extends Article { } /** - * Overloading Article's getContent method. + * Overloading Article's getContentObject method. * * Omit noarticletext if sharedupload; text will be fetched from the * shared upload server if possible. * @return string */ - public function getContent() { + public function getContentObject() { $this->loadFile(); if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) { - return ''; + return null; } - return parent::getContent(); + return parent::getContentObject(); } protected function openShowImage() { diff --git a/includes/Import.php b/includes/Import.php index 11f379522d..71498ac72e 100644 --- a/includes/Import.php +++ b/includes/Import.php @@ -429,6 +429,7 @@ class WikiImporter { /** * Primary entry point + * @throws MWException * @return bool */ public function doImport() { @@ -611,7 +612,7 @@ class WikiImporter { $this->debug( "Enter revision handler" ); $revisionInfo = array(); - $normalFields = array( 'id', 'timestamp', 'comment', 'minor', 'text' ); + $normalFields = array( 'id', 'timestamp', 'comment', 'minor', 'model', 'format', 'text' ); $skip = false; @@ -656,6 +657,12 @@ class WikiImporter { if ( isset( $revisionInfo['text'] ) ) { $revision->setText( $revisionInfo['text'] ); } + if ( isset( $revisionInfo['model'] ) ) { + $revision->setModel( $revisionInfo['model'] ); + } + if ( isset( $revisionInfo['format'] ) ) { + $revision->setFormat( $revisionInfo['format'] ); + } $revision->setTitle( $pageInfo['_title'] ); if ( isset( $revisionInfo['timestamp'] ) ) { @@ -1008,7 +1015,10 @@ class WikiRevision { var $timestamp = "20010115000000"; var $user = 0; var $user_text = ""; + var $model = null; + var $format = null; var $text = ""; + var $content = null; var $comment = ""; var $minor = false; var $type = ""; @@ -1064,6 +1074,20 @@ class WikiRevision { $this->user_text = $ip; } + /** + * @param $model + */ + function setModel( $model ) { + $this->model = $model; + } + + /** + * @param $format + */ + function setFormat( $format ) { + $this->format = $format; + } + /** * @param $text */ @@ -1187,11 +1211,54 @@ class WikiRevision { /** * @return string + * + * @deprecated Since 1.21, use getContent() instead. */ function getText() { + ContentHandler::deprecated( __METHOD__, '1.21' ); + return $this->text; } + /** + * @return Content + */ + function getContent() { + if ( is_null( $this->content ) ) { + $this->content = + ContentHandler::makeContent( + $this->text, + $this->getTitle(), + $this->getModel(), + $this->getFormat() + ); + } + + return $this->content; + } + + /** + * @return String + */ + function getModel() { + if ( is_null( $this->model ) ) { + $this->model = $this->getTitle()->getContentModel(); + } + + return $this->model; + } + + /** + * @return String + */ + function getFormat() { + if ( is_null( $this->model ) ) { + $this->format = ContentHandler::getForTitle( $this->getTitle() )->getDefaultFormat(); + } + + return $this->format; + } + /** * @return string */ @@ -1331,7 +1398,9 @@ class WikiRevision { # Insert the row $revision = new Revision( array( 'page' => $pageId, - 'text' => $this->getText(), + 'content_model' => $this->getModel(), + 'content_format' => $this->getFormat(), + 'text' => $this->getContent()->serialize( $this->getFormat() ), //XXX: just set 'content' => $this->getContent()? 'comment' => $this->getComment(), 'user' => $userId, 'user_text' => $userText, diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php index 214f49591f..c2f6b1ebee 100644 --- a/includes/LinkFilter.php +++ b/includes/LinkFilter.php @@ -23,7 +23,7 @@ /** * Some functions to help implement an external link filter for spam control. - * + * * @todo implement the filter. Currently these are just some functions to help * maintenance/cleanupSpam.php remove links to a single specified domain. The * next thing is to implement functions for checking a given page against a big @@ -34,13 +34,22 @@ class LinkFilter { /** - * Check whether $text contains a link to $filterEntry + * Check whether $content contains a link to $filterEntry * - * @param $text String: text to check + * @param $content Content: content to check * @param $filterEntry String: domainparts, see makeRegex() for more details * @return Integer: 0 if no match or 1 if there's at least one match */ - static function matchEntry( $text, $filterEntry ) { + static function matchEntry( Content $content, $filterEntry ) { + if ( !( $content instanceof TextContent ) ) { + //TODO: handle other types of content too. + // Maybe create ContentHandler::matchFilter( LinkFilter ). + // Think about a common base class for LinkFilter and MagicWord. + return 0; + } + + $text = $content->getNativeData(); + $regex = LinkFilter::makeRegex( $filterEntry ); return preg_match( $regex, $text ); } @@ -110,17 +119,17 @@ class LinkFilter { // Reverse the labels in the hostname, convert to lower case // For emails reverse domainpart only if ( $prot == 'mailto:' && strpos($host, '@') ) { - // complete email adress + // complete email adress $mailparts = explode( '@', $host ); $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); $host = $domainpart . '@' . $mailparts[0]; $like = array( "$prot$host", $db->anyString() ); } elseif ( $prot == 'mailto:' ) { // domainpart of email adress only. do not add '.' - $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) ); - $like = array( "$prot$host", $db->anyString() ); + $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) ); + $like = array( "$prot$host", $db->anyString() ); } else { - $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) ); + $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) ); if ( substr( $host, -1, 1 ) !== '.' ) { $host .= '.'; } diff --git a/includes/Linker.php b/includes/Linker.php index 97b03be800..28c59855d7 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -448,6 +448,7 @@ class Linker { * @param $context IContextSource context to use to get the messages * @param $namespace int Namespace number * @param $title string Text of the title, without the namespace part + * @return string */ public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) { global $wgContLang; @@ -675,6 +676,7 @@ class Linker { if ( !$thumb ) { $s = self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true ); } else { + self::processResponsiveImages( $file, $thumb, $hp ); $params = array( 'alt' => $fp['alt'], 'title' => $fp['title'], @@ -795,6 +797,7 @@ class Linker { $hp['width'] = isset( $fp['upright'] ) ? 130 : 180; } $thumb = false; + $noscale = false; if ( !$exists ) { $outerWidth = $hp['width'] + 2; @@ -813,6 +816,7 @@ class Linker { } elseif ( isset( $fp['framed'] ) ) { // Use image dimensions, don't scale $thumb = $file->getUnscaledThumb( $hp ); + $noscale = true; } else { # 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 @@ -846,6 +850,9 @@ class Linker { $s .= wfMessage( 'thumbnail_error', '' )->escaped(); $zoomIcon = ''; } else { + if ( !$noscale ) { + self::processResponsiveImages( $file, $thumb, $hp ); + } $params = array( 'alt' => $fp['alt'], 'title' => $fp['title'], @@ -872,6 +879,37 @@ class Linker { return str_replace( "\n", ' ', $s ); } + /** + * Process responsive images: add 1.5x and 2x subimages to the thumbnail, where + * applicable. + * + * @param File $file + * @param MediaOutput $thumb + * @param array $hp image parameters + */ + protected static function processResponsiveImages( $file, $thumb, $hp ) { + global $wgResponsiveImages; + if ( $wgResponsiveImages ) { + $hp15 = $hp; + $hp15['width'] = round( $hp['width'] * 1.5 ); + $hp20 = $hp; + $hp20['width'] = $hp['width'] * 2; + if ( isset( $hp['height'] ) ) { + $hp15['height'] = round( $hp['height'] * 1.5 ); + $hp20['height'] = $hp['height'] * 2; + } + + $thumb15 = $file->transform( $hp15 ); + $thumb20 = $file->transform( $hp20 ); + if ( $thumb15->url !== $thumb->url ) { + $thumb->responsiveUrls['1.5'] = $thumb15->url; + } + if ( $thumb20->url !== $thumb->url ) { + $thumb->responsiveUrls['2'] = $thumb20->url; + } + } + } + /** * Make a "broken" link to an image * @@ -1075,8 +1113,11 @@ class Linker { // check if the user has an edit $attribs = array(); if ( $redContribsWhenNoEdits ) { - $count = !is_null( $edits ) ? $edits : User::edits( $userId ); - if ( $count == 0 ) { + if ( intval( $edits ) === 0 && $edits !== 0 ) { + $user = User::newFromId( $userId ); + $edits = $user->getEditCount(); + } + if ( $edits === 0 ) { $attribs['class'] = 'new'; } } @@ -1723,7 +1764,7 @@ class Linker { */ public static function buildRollbackLink( $rev, IContextSource $context = null ) { global $wgShowRollbackEditCount, $wgMiserMode; - + // To config which pages are effected by miser mode $disableRollbackEditCountSpecialPage = array( 'Recentchanges', 'Watchlist' ); @@ -2080,7 +2121,7 @@ class Linker { */ static function makeBrokenLink( $title, $text = '', $query = '', $trail = '' ) { wfDeprecated( __METHOD__, '1.16' ); - + $nt = Title::newFromText( $title ); if ( $nt instanceof Title ) { return self::makeBrokenLinkObj( $nt, $text, $query, $trail ); @@ -2109,7 +2150,7 @@ class Linker { */ static function makeLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { # wfDeprecated( __METHOD__, '1.16' ); // See r105985 and it's revert. Somewhere still used. - + wfProfileIn( __METHOD__ ); $query = wfCgiToArray( $query ); list( $inside, $trail ) = self::splitTrail( $trail ); @@ -2143,7 +2184,7 @@ class Linker { $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) { # wfDeprecated( __METHOD__, '1.16' ); // See r105985 and it's revert. Somewhere still used. - + wfProfileIn( __METHOD__ ); if ( $text == '' ) { @@ -2179,7 +2220,7 @@ class Linker { */ static function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) { wfDeprecated( __METHOD__, '1.16' ); - + wfProfileIn( __METHOD__ ); list( $inside, $trail ) = self::splitTrail( $trail ); @@ -2211,7 +2252,7 @@ class Linker { */ static function makeColouredLinkObj( $nt, $colour, $text = '', $query = '', $trail = '', $prefix = '' ) { wfDeprecated( __METHOD__, '1.16' ); - + if ( $colour != '' ) { $style = self::getInternalLinkAttributesObj( $nt, $text, $colour ); } else { diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php index 87db4d60da..fd1fefb831 100644 --- a/includes/LinksUpdate.php +++ b/includes/LinksUpdate.php @@ -49,6 +49,7 @@ class LinksUpdate extends SqlDataUpdate { * @param $title Title of the page we're updating * @param $parserOutput ParserOutput: output from a full parse of this page * @param $recursive Boolean: queue jobs for recursive updates? + * @throws MWException */ function __construct( $title, $parserOutput, $recursive = true ) { parent::__construct( false ); // no implicit transaction @@ -71,6 +72,7 @@ class LinksUpdate extends SqlDataUpdate { } $this->mParserOutput = $parserOutput; + $this->mLinks = $parserOutput->getLinks(); $this->mImages = $parserOutput->getImages(); $this->mTemplates = $parserOutput->getTemplates(); @@ -828,6 +830,10 @@ class LinksDeletionUpdate extends SqlDataUpdate { parent::__construct( false ); // no implicit transaction $this->mPage = $page; + + if ( !$page->exists() ) { + throw new MWException( "Page ID not known, perhaps the page doesn't exist?" ); + } } /** @@ -879,4 +885,16 @@ class LinksDeletionUpdate extends SqlDataUpdate { __METHOD__ ); } } + + /** + * Update all the appropriate counts in the category table. + * @param $added array associative array of category name => sort key + * @param $deleted array associative array of category name => sort key + */ + function updateCategoryCounts( $added, $deleted ) { + $a = WikiPage::factory( $this->mTitle ); + $a->updateCategoryCounts( + array_keys( $added ), array_keys( $deleted ) + ); + } } diff --git a/includes/LocalisationCache.php b/includes/LocalisationCache.php index d8e5d3a368..e88c240837 100644 --- a/includes/LocalisationCache.php +++ b/includes/LocalisationCache.php @@ -168,6 +168,7 @@ class LocalisationCache { * for $wgLocalisationCacheConf. * * @param $conf Array + * @throws MWException */ function __construct( $conf ) { global $wgCacheDirectory; @@ -404,6 +405,7 @@ class LocalisationCache { /** * Initialise a language in this object. Rebuild the cache if necessary. * @param $code + * @throws MWException */ protected function initLanguage( $code ) { if ( isset( $this->initialisedLangs[$code] ) ) { @@ -474,6 +476,7 @@ class LocalisationCache { * Read a PHP file containing localisation data. * @param $_fileName * @param $_fileType + * @throws MWException * @return array */ protected function readPHPFile( $_fileName, $_fileType ) { @@ -659,6 +662,7 @@ class LocalisationCache { * Load localisation data for a given language for both core and extensions * and save it to the persistent cache store and the process cache * @param $code + * @throws MWException */ public function recache( $code ) { global $wgExtensionMessagesFiles; diff --git a/includes/Message.php b/includes/Message.php index 9d09f00c7d..824f1771fe 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -202,6 +202,11 @@ class Message { */ protected $title = null; + /** + * Content object representing the message + */ + protected $content = null; + /** * @var string */ @@ -332,6 +337,7 @@ class Message { * turned off. * @since 1.17 * @param $lang Mixed: language code or Language object. + * @throws MWException * @return Message: $this */ public function inLanguage( $lang ) { @@ -404,6 +410,18 @@ class Message { return $this; } + /** + * Returns the message as a Content object. + * @return Content + */ + public function content() { + if ( !$this->content ) { + $this->content = new MessageContent( $this->key ); + } + + return $this->content; + } + /** * Returns the message parsed from wikitext to HTML. * @since 1.17 @@ -420,6 +438,15 @@ class Message { return '<' . $key . '>'; } + # Replace $* with a list of parameters for &uselang=qqx. + if ( strpos( $string, '$*' ) !== false ) { + $paramlist = ''; + if ( $this->parameters !== array() ) { + $paramlist = ': $' . implode( ', $', range( 1, count( $this->parameters ) ) ); + } + $string = str_replace( '$*', $paramlist, $string ); + } + # Replace parameters before text parsing $string = $this->replaceParameters( $string, 'before' ); @@ -618,6 +645,7 @@ class Message { /** * Wrapper for what ever method we use to get message contents * @since 1.17 + * @throws MWException * @return string */ protected function fetchMessage() { diff --git a/includes/MessageBlobStore.php b/includes/MessageBlobStore.php index 34014e1b31..09561bd78a 100644 --- a/includes/MessageBlobStore.php +++ b/includes/MessageBlobStore.php @@ -140,7 +140,7 @@ class MessageBlobStore { // Save the old and new blobs for later $oldBlob = $row->mr_blob; $newBlob = self::generateMessageBlob( $module, $lang ); - + $newRow = array( 'mr_resource' => $name, 'mr_lang' => $lang, @@ -311,6 +311,7 @@ class MessageBlobStore { * @param $resourceLoader ResourceLoader object * @param $modules Array of module names * @param $lang String: language code + * @throws MWException * @return array Array mapping module names to blobs */ private static function getFromDB( ResourceLoader $resourceLoader, $modules, $lang ) { diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index 1873e7bfef..88383d8f38 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -25,17 +25,17 @@ * This is used as a fallback to mime.types files. * An extensive list of well known mime types is provided by * the file mime.types in the includes directory. - * + * * This list concatenated with mime.types is used to create a mime <-> ext * map. Each line contains a mime type followed by a space separated list of - * extensions. If multiple extensions for a single mime type exist or if + * extensions. If multiple extensions for a single mime type exist or if * multiple mime types exist for a single extension then in most cases * MediaWiki assumes that the first extension following the mime type is the * canonical extension, and the first time a mime type appears for a certain * extension is considered the canonical mime type. - * + * * (Note that appending $wgMimeTypeFile to the end of MM_WELL_KNOWN_MIME_TYPES - * sucks because you can't redefine canonical types. This could be fixed by + * sucks because you can't redefine canonical types. This could be fixed by * appending MM_WELL_KNOWN_MIME_TYPES behind $wgMimeTypeFile, but who knows * what will break? In practice this probably isn't a problem anyway -- Bryan) */ @@ -70,7 +70,7 @@ image/x-bmp bmp image/gif gif image/jpeg jpeg jpg jpe image/png png -image/svg+xml svg +image/svg+xml svg image/svg svg image/tiff tiff tif image/vnd.djvu djvu @@ -352,11 +352,11 @@ class MimeMagic { return self::$instance; } - /** - * Returns a list of file extensions for a given mime type as a space + /** + * Returns a list of file extensions for a given mime type as a space * separated string or null if the mime type was unrecognized. Resolves * mime type aliases. - * + * * @param $mime string * @return string|null */ @@ -379,10 +379,10 @@ class MimeMagic { return null; } - /** - * Returns a list of mime types for a given file extension as a space + /** + * Returns a list of mime types for a given file extension as a space * separated string or null if the extension was unrecognized. - * + * * @param $ext string * @return string|null */ @@ -393,10 +393,10 @@ class MimeMagic { return $r; } - /** + /** * Returns a single mime type for a given file extension or null if unknown. * This is always the first type from the list returned by getTypesForExtension($ext). - * + * * @param $ext string * @return string|null */ @@ -414,11 +414,11 @@ class MimeMagic { } - /** - * Tests if the extension matches the given mime type. Returns true if a - * match was found, null if the mime type is unknown, and false if the + /** + * Tests if the extension matches the given mime type. 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. - * + * * @param $extension string * @param $mime string * @return bool|null @@ -436,12 +436,12 @@ class MimeMagic { return in_array( $extension, $ext ); } - /** - * Returns true if the mime type is known to represent an image format + /** + * Returns true if the mime type is known to represent an image format * supported by the PHP GD library. * * @param $mime string - * + * * @return bool */ public function isPHPImageType( $mime ) { @@ -489,19 +489,19 @@ class MimeMagic { return in_array( strtolower( $extension ), $types ); } - /** + /** * Improves a mime type using the file extension. Some file formats are very generic, - * so their mime type is not very meaningful. A more useful mime type can be derived - * by looking at the file extension. Typically, this method would be called on the + * so their mime type is not very meaningful. A more useful mime type can be derived + * by looking at the file extension. Typically, this method would be called on the * result of guessMimeType(). - * + * * Currently, this method does the following: * * If $mime is "unknown/unknown" and isRecognizableExtension( $ext ) returns false, - * return the result of guessTypesForExtension($ext). + * return the result of guessTypesForExtension($ext). * * If $mime is "application/x-opc+zip" and isMatchingExtension( $ext, $mime ) - * gives true, return the result of guessTypesForExtension($ext). + * gives true, return the result of guessTypesForExtension($ext). * * @param $mime String: the mime type, typically guessed from a file's content. * @param $ext String: the file extension, as taken from the file name @@ -511,10 +511,10 @@ class MimeMagic { public function improveTypeFromExtension( $mime, $ext ) { if ( $mime === 'unknown/unknown' ) { if ( $this->isRecognizableExtension( $ext ) ) { - wfDebug( __METHOD__. ': refusing to guess mime type for .' . + wfDebug( __METHOD__. ': refusing to guess mime type for .' . "$ext file, we should have recognized it\n" ); } else { - // Not something we can detect, so simply + // Not something we can detect, so simply // trust the file extension $mime = $this->guessTypesForExtension( $ext ); } @@ -525,7 +525,7 @@ class MimeMagic { // find the proper mime type for that file extension $mime = $this->guessTypesForExtension( $ext ); } else { - wfDebug( __METHOD__. ": refusing to guess better type for $mime file, " . + wfDebug( __METHOD__. ": refusing to guess better type for $mime file, " . ".$ext is not a known OPC extension.\n" ); $mime = 'application/zip'; } @@ -539,16 +539,16 @@ class MimeMagic { return $mime; } - /** - * 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, as well as ZIP + /** + * 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, as well as ZIP * based formats like OPC/ODF files). * * @param $file String: the file to check * @param $ext Mixed: the file extension, or true (default) to extract it from the filename. - * Set it to false to ignore the extension. DEPRECATED! Set to false, use + * Set it to false to ignore the extension. DEPRECATED! Set to false, use * improveTypeFromExtension($mime, $ext) later to improve mime type. * * @return string the mime type of $file @@ -587,7 +587,7 @@ class MimeMagic { // @todo FIXME: Shouldn't this be rb? $f = fopen( $file, 'rt' ); wfRestoreWarnings(); - + if( !$f ) { return 'unknown/unknown'; } @@ -750,7 +750,7 @@ class MimeMagic { return false; } - + /** * Detect application-specific file type of a given ZIP file from its * header data. Currently works for OpenDocument and OpenXML types... @@ -759,7 +759,7 @@ class MimeMagic { * @param $header String: some reasonably-sized chunk of file header * @param $tail String: the tail of the file * @param $ext Mixed: the file extension, or true to extract it from the filename. - * Set it to false (default) to ignore the extension. DEPRECATED! Set to false, + * Set it to false (default) to ignore the extension. DEPRECATED! Set to false, * use improveTypeFromExtension($mime, $ext) later to improve mime type. * * @return string @@ -800,8 +800,8 @@ class MimeMagic { wfDebug( __METHOD__.": detected $mime from ZIP archive\n" ); } elseif ( preg_match( $openxmlRegex, substr( $header, 30 ) ) ) { $mime = "application/x-opc+zip"; - # TODO: remove the block below, as soon as improveTypeFromExtension is used everywhere - if ( $ext !== true && $ext !== false ) { + # TODO: remove the block below, as soon as improveTypeFromExtension is used everywhere + if ( $ext !== true && $ext !== false ) { /** This is the mode used by getPropsFromPath * These mime's are stored in the database, where we don't really want * x-opc+zip, because we use it only for internal purposes @@ -815,12 +815,12 @@ class MimeMagic { } } wfDebug( __METHOD__.": detected an Open Packaging Conventions archive: $mime\n" ); - } elseif ( substr( $header, 0, 8 ) == "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" && + } elseif ( substr( $header, 0, 8 ) == "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" && ($headerpos = strpos( $tail, "PK\x03\x04" ) ) !== false && preg_match( $openxmlRegex, substr( $tail, $headerpos + 30 ) ) ) { if ( substr( $header, 512, 4) == "\xEC\xA5\xC1\x00" ) { $mime = "application/msword"; - } + } switch( substr( $header, 512, 6) ) { case "\xEC\xA5\xC1\x00\x0E\x00": case "\xEC\xA5\xC1\x00\x1C\x00": @@ -850,20 +850,20 @@ class MimeMagic { return $mime; } - /** - * Internal mime type detection. 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 $ext is not false, the mime + /** + * Internal mime type detection. 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 $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, + * + * 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 $file String: the file to check * @param $ext Mixed: the file extension, or true (default) to extract it from the filename. - * Set it to false to ignore the extension. DEPRECATED! Set to false, use + * Set it to false to ignore the extension. DEPRECATED! Set to false, use * improveTypeFromExtension($mime, $ext) later to improve mime type. * * @return string the mime type of $file @@ -1037,7 +1037,7 @@ class MimeMagic { 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 * distinguish them from mime types. @@ -1047,7 +1047,7 @@ class MimeMagic { * @return int|string */ function findMediaType( $extMime ) { - if ( strpos( $extMime, '.' ) === 0 ) { + if ( strpos( $extMime, '.' ) === 0 ) { // If it's an extension, look up the mime types $m = $this->getTypesForExtension( substr( $extMime, 1 ) ); if ( !$m ) { @@ -1076,7 +1076,7 @@ class MimeMagic { } /** - * Get the MIME types that various versions of Internet Explorer would + * Get the MIME types that various versions of Internet Explorer would * detect from a chunk of the content. * * @param $fileName String: the file name (unused at present) diff --git a/includes/Namespace.php b/includes/Namespace.php index 2e2b8d6188..e8d5632d88 100644 --- a/includes/Namespace.php +++ b/includes/Namespace.php @@ -48,6 +48,7 @@ class MWNamespace { * @param $index * @param $method * + * @throws MWException * @return bool */ private static function isMethodValidFor( $index, $method ) { @@ -209,12 +210,14 @@ class MWNamespace { * Returns array of all defined namespaces with their canonical * (English) names. * + * @param bool $rebuild rebuild namespace list (default = false). Used for testing. + * * @return array * @since 1.17 */ - public static function getCanonicalNamespaces() { + public static function getCanonicalNamespaces( $rebuild = false ) { static $namespaces = null; - if ( $namespaces === null ) { + if ( $namespaces === null || $rebuild ) { global $wgExtraNamespaces, $wgCanonicalNamespaceNames; $namespaces = array( NS_MAIN => '' ) + $wgCanonicalNamespaceNames; if ( is_array( $wgExtraNamespaces ) ) { diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php index 46a43f6338..78435e415b 100644 --- a/includes/OutputHandler.php +++ b/includes/OutputHandler.php @@ -22,9 +22,9 @@ /** * Standard output handler for use with ob_start - * + * * @param $s string - * + * * @return string */ function wfOutputHandler( $s ) { @@ -85,7 +85,7 @@ function wfRequestExtension() { /** * Handler that compresses data with gzip if allowed by the Accept header. * Unlike ob_gzhandler, it works for HEAD requests too. - * + * * @param $s string * * @return string diff --git a/includes/OutputPage.php b/includes/OutputPage.php index a2e26709bb..3578568651 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -255,7 +255,7 @@ class OutputPage extends ContextSource { function __construct( IContextSource $context = null ) { if ( $context === null ) { # Extensions should use `new RequestContext` instead of `new OutputPage` now. - wfDeprecated( __METHOD__ ); + wfDeprecated( __METHOD__, '1.18' ); } else { $this->setContext( $context ); } @@ -1570,9 +1570,10 @@ class OutputPage extends ContextSource { * @param $interface Boolean: use interface language ($wgLang instead of * $wgContLang) while parsing language sensitive magic * words like GRAMMAR and PLURAL. This also disables - * LanguageConverter. + * LanguageConverter. * @param $language Language object: target language object, will override * $interface + * @throws MWException * @return String: HTML */ public function parse( $text, $linestart = true, $interface = false, $language = null ) { @@ -1902,7 +1903,7 @@ class OutputPage extends ContextSource { * @deprecated since 1.18 Use HttpStatus::getMessage() instead. */ public static function getStatusMessage( $code ) { - wfDeprecated( __METHOD__ ); + wfDeprecated( __METHOD__, '1.18' ); return HttpStatus::getMessage( $code ); } @@ -1993,7 +1994,9 @@ class OutputPage extends ContextSource { wfRunHooks( 'AfterFinalPageOutput', array( $this ) ); $this->sendCacheControl(); + ob_end_flush(); + wfProfileOut( __METHOD__ ); } @@ -2056,7 +2059,7 @@ class OutputPage extends ContextSource { $this->prepareErrorPage( $title ); if ( $msg instanceof Message ){ - $this->addHTML( $msg->parse() ); + $this->addHTML( $msg->parseAsBlock() ); } else { $this->addWikiMsgArray( $msg, $params ); } @@ -2071,8 +2074,6 @@ class OutputPage extends ContextSource { * @param $action String: action that was denied or null if unknown */ public function showPermissionsErrorPage( $errors, $action = null ) { - global $wgGroupPermissions; - // For some action (read, edit, create and upload), display a "login to do this action" // error if all of the following conditions are met: // 1. the user is not logged in @@ -2081,8 +2082,8 @@ class OutputPage extends ContextSource { if ( in_array( $action, array( 'read', 'edit', 'createpage', 'createtalk', 'upload' ) ) && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] ) && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' ) - && ( ( isset( $wgGroupPermissions['user'][$action] ) && $wgGroupPermissions['user'][$action] ) - || ( isset( $wgGroupPermissions['autoconfirmed'][$action] ) && $wgGroupPermissions['autoconfirmed'][$action] ) ) + && ( User::groupHasPermission( 'user', $action ) + || User::groupHasPermission( 'autoconfirmed', $action ) ) ) { $displayReturnto = null; @@ -2155,6 +2156,7 @@ class OutputPage extends ContextSource { * Display an error page noting that a given permission bit is required. * @deprecated since 1.18, just throw the exception directly * @param $permission String: key required + * @throws PermissionsError */ public function permissionRequired( $permission ) { throw new PermissionsError( $permission ); @@ -2225,6 +2227,7 @@ class OutputPage extends ContextSource { * @param $protected Boolean: is this a permissions error? * @param $reasons Array: list of reasons for this error, as returned by Title::getUserPermissionsErrors(). * @param $action String: action that was denied or null if unknown + * @throws ReadOnlyError */ public function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) { $this->setRobotPolicy( 'noindex,nofollow' ); @@ -2459,7 +2462,7 @@ $templates */ private function addDefaultModules() { global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax, - $wgAjaxWatch; + $wgAjaxWatch, $wgResponsiveImages; // Add base resources $this->addModules( array( @@ -2500,6 +2503,11 @@ $templates if ( $this->isArticle() && $this->getUser()->getOption( 'editondblclick' ) ) { $this->addModules( 'mediawiki.action.view.dblClickEdit' ); } + + // Support for high-density display images + if ( $wgResponsiveImages ) { + $this->addModules( 'mediawiki.hidpi' ); + } } /** @@ -2912,7 +2920,7 @@ $templates * @return array */ public function getJSVars() { - global $wgUseAjax, $wgContLang; + global $wgContLang; $latestRevID = 0; $pageID = 0; @@ -3565,7 +3573,6 @@ $templates $msgSpecs = array_values( $msgSpecs ); $s = $wrap; foreach ( $msgSpecs as $n => $spec ) { - $options = array(); if ( is_array( $spec ) ) { $args = $spec; $name = array_shift( $args ); diff --git a/includes/PHPVersionError.php b/includes/PHPVersionError.php index dad71f82f5..2aed2afbdb 100644 --- a/includes/PHPVersionError.php +++ b/includes/PHPVersionError.php @@ -38,7 +38,7 @@ * version are hardcoded here */ function wfPHPVersionError( $type ){ - $mwVersion = '1.20'; + $mwVersion = '1.21'; $phpVersion = PHP_VERSION; $message = "MediaWiki $mwVersion requires at least PHP version 5.3.2, you are using PHP $phpVersion."; if( $type == 'index.php' ) { diff --git a/includes/Preferences.php b/includes/Preferences.php index 216ba48c8f..65a0d0291b 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -879,7 +879,7 @@ class Preferences { global $wgUseRCPatrol, $wgEnableAPI, $wgRCMaxAge; $watchlistdaysMax = ceil( $wgRCMaxAge / ( 3600 * 24 ) ); - + ## Watchlist ##################################### $defaultPreferences['watchlistdays'] = array( 'type' => 'float', diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index ce0e36b044..9643ba7a30 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -63,7 +63,7 @@ class ProtectionForm { $this->mArticle = $article; $this->mTitle = $article->getTitle(); $this->mApplicableTypes = $this->mTitle->getRestrictionTypes(); - + // Check if the form should be disabled. // If it is, the form will be available in read-only to show levels. $this->mPermErrors = $this->mTitle->getUserPermissionsErrors( 'protect', $wgUser ); @@ -286,12 +286,10 @@ class ProtectionForm { # They shouldn't be able to do this anyway, but just to make sure, ensure that cascading restrictions aren't being applied # to a semi-protected page. - global $wgGroupPermissions; - $edit_restriction = isset( $this->mRestrictions['edit'] ) ? $this->mRestrictions['edit'] : ''; $this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' ); if ($this->mCascade && ($edit_restriction != 'protect') && - !(isset($wgGroupPermissions[$edit_restriction]['protect']) && $wgGroupPermissions[$edit_restriction]['protect'] ) ) + !User::groupHasPermission( $edit_restriction, 'protect' ) ) $this->mCascade = false; $status = $this->mArticle->doUpdateRestrictions( $this->mRestrictions, $expiry, $this->mCascade, $reasonstr, $wgUser ); @@ -600,11 +598,11 @@ class ProtectionForm { } function buildCleanupScript() { - global $wgRestrictionLevels, $wgGroupPermissions, $wgOut; + global $wgRestrictionLevels, $wgOut; $cascadeableLevels = array(); foreach( $wgRestrictionLevels as $key ) { - if ( ( isset( $wgGroupPermissions[$key]['protect'] ) && $wgGroupPermissions[$key]['protect'] ) + if ( User::groupHasPermission( $key, 'protect' ) || $key == 'protect' ) { $cascadeableLevels[] = $key; diff --git a/includes/QueryPage.php b/includes/QueryPage.php index ac559dc5fc..1f21584080 100644 --- a/includes/QueryPage.php +++ b/includes/QueryPage.php @@ -151,6 +151,7 @@ abstract class QueryPage extends SpecialPage { /** * For back-compat, subclasses may return a raw SQL query here, as a string. * This is stronly deprecated; getQueryInfo() should be overridden instead. + * @throws MWException * @return string */ function getSQL() { diff --git a/includes/Revision.php b/includes/Revision.php index 20cc8f5862..984da69206 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -25,6 +25,10 @@ */ class Revision implements IDBAccessObject { protected $mId; + + /** + * @var int|null + */ protected $mPage; protected $mUserText; protected $mOrigUserText; @@ -38,8 +42,24 @@ class Revision implements IDBAccessObject { protected $mComment; protected $mText; protected $mTextRow; + + /** + * @var null|Title + */ protected $mTitle; protected $mCurrent; + protected $mContentModel; + protected $mContentFormat; + + /** + * @var Content + */ + protected $mContent; + + /** + * @var null|ContentHandler + */ + protected $mContentHandler; // Revision deletion constants const DELETED_TEXT = 1; @@ -83,7 +103,7 @@ class Revision implements IDBAccessObject { * @param $flags Integer Bitfield (optional) * @return Revision or null */ - public static function newFromTitle( $title, $id = 0, $flags = null ) { + public static function newFromTitle( $title, $id = 0, $flags = 0 ) { $conds = array( 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDBkey() @@ -94,8 +114,6 @@ class Revision implements IDBAccessObject { } else { // Use a join to get the latest revision $conds[] = 'rev_id=page_latest'; - // Callers assume this will be up-to-date - $flags = is_int( $flags ) ? $flags : self::READ_LATEST; // b/c } return self::newFromConds( $conds, (int)$flags ); } @@ -114,15 +132,13 @@ class Revision implements IDBAccessObject { * @param $flags Integer Bitfield (optional) * @return Revision or null */ - public static function newFromPageId( $pageId, $revId = 0, $flags = null ) { + public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) { $conds = array( 'page_id' => $pageId ); if ( $revId ) { $conds['rev_id'] = $revId; } else { // Use a join to get the latest revision $conds[] = 'rev_id = page_latest'; - // Callers assume this will be up-to-date - $flags = is_int( $flags ) ? $flags : self::READ_LATEST; // b/c } return self::newFromConds( $conds, (int)$flags ); } @@ -135,9 +151,12 @@ class Revision implements IDBAccessObject { * @param $row * @param $overrides array * + * @throws MWException * @return Revision */ public static function newFromArchiveRow( $row, $overrides = array() ) { + global $wgContentHandlerUseDB; + $attribs = $overrides + array( 'page' => isset( $row->ar_page_id ) ? $row->ar_page_id : null, 'id' => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null, @@ -150,7 +169,15 @@ class Revision implements IDBAccessObject { 'deleted' => $row->ar_deleted, 'len' => $row->ar_len, 'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null, + 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null, + 'content_format' => isset( $row->ar_content_format ) ? $row->ar_content_format : null, ); + + if ( !$wgContentHandlerUseDB ) { + unset( $attribs['content_model'] ); + unset( $attribs['content_format'] ); + } + if ( isset( $row->ar_text ) && !$row->ar_text_id ) { // Pre-1.5 ar_text row $attribs['text'] = self::getRevisionText( $row, 'ar_' ); @@ -358,7 +385,9 @@ class Revision implements IDBAccessObject { * @return array */ public static function selectFields() { - return array( + global $wgContentHandlerUseDB; + + $fields = array( 'rev_id', 'rev_page', 'rev_text_id', @@ -370,8 +399,15 @@ class Revision implements IDBAccessObject { 'rev_deleted', 'rev_len', 'rev_parent_id', - 'rev_sha1' + 'rev_sha1', ); + + if ( $wgContentHandlerUseDB ) { + $fields[] = 'rev_content_format'; + $fields[] = 'rev_content_model'; + } + + return $fields; } /** @@ -436,6 +472,7 @@ class Revision implements IDBAccessObject { * Constructor * * @param $row Mixed: either a database row or an array + * @throws MWException * @access private */ function __construct( $row ) { @@ -475,6 +512,18 @@ class Revision implements IDBAccessObject { $this->mTitle = null; } + if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) { + $this->mContentModel = null; # determine on demand if needed + } else { + $this->mContentModel = strval( $row->rev_content_model ); + } + + if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) { + $this->mContentFormat = null; # determine on demand if needed + } else { + $this->mContentFormat = strval( $row->rev_content_format ); + } + // Lazy extraction... $this->mText = null; if( isset( $row->old_text ) ) { @@ -496,6 +545,21 @@ class Revision implements IDBAccessObject { // Build a new revision to be saved... global $wgUser; // ugh + + # if we have a content object, use it to set the model and type + if ( !empty( $row['content'] ) ) { + //@todo: when is that set? test with external store setup! check out insertOn() [dk] + if ( !empty( $row['text_id'] ) ) { + throw new MWException( "Text already stored in external store (id {$row['text_id']}), " + . "can't serialize content object" ); + } + + $row['content_model'] = $row['content']->getModel(); + # note: mContentFormat is initializes later accordingly + # note: content is serialized later in this method! + # also set text to null? + } + $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; @@ -508,21 +572,63 @@ class Revision implements IDBAccessObject { $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null; $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null; + $this->mContentModel = isset( $row['content_model'] ) ? strval( $row['content_model'] ) : null; + $this->mContentFormat = isset( $row['content_format'] ) ? strval( $row['content_format'] ) : 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->mTextRow = null; - $this->mTitle = null; # Load on demand if needed + $this->mTitle = isset( $row['title'] ) ? $row['title'] : null; + + // if we have a Content object, override mText and mContentModel + if ( !empty( $row['content'] ) ) { + if ( !( $row['content'] instanceof Content ) ) { + throw new MWException( '`content` field must contain a Content object.' ); + } + + $handler = $this->getContentHandler(); + $this->mContent = $row['content']; + + $this->mContentModel = $this->mContent->getModel(); + $this->mContentHandler = null; + + $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() ); + } elseif ( !is_null( $this->mText ) ) { + $handler = $this->getContentHandler(); + $this->mContent = $handler->unserializeContent( $this->mText ); + } + + // if we have a Title object, override mPage. Useful for testing and convenience. + if ( isset( $row['title'] ) ) { + $this->mTitle = $row['title']; + $this->mPage = $this->mTitle->getArticleID(); + } else { + $this->mTitle = null; // Load on demand if needed + } + + // @todo: XXX: really? we are about to create a revision. it will usually then be the current one. $this->mCurrent = false; - # If we still have no length, see it we have the text to figure it out + + // If we still have no length, see it we have the text to figure it out if ( !$this->mSize ) { - $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText ); + if ( !is_null( $this->mContent ) ) { + $this->mSize = $this->mContent->getSize(); + } else { + #NOTE: this should never happen if we have either text or content object! + $this->mSize = null; + } } - # Same for sha1 + + // Same for sha1 if ( $this->mSha1 === null ) { $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText ); } + + // force lazy init + $this->getContentModel(); + $this->getContentFormat(); } else { throw new MWException( 'Revision constructor passed invalid row format.' ); } @@ -595,7 +701,7 @@ class Revision implements IDBAccessObject { if( isset( $this->mTitle ) ) { return $this->mTitle; } - if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL + if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted. $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( array( 'page', 'revision' ), @@ -607,6 +713,11 @@ class Revision implements IDBAccessObject { $this->mTitle = Title::newFromRow( $row ); } } + + if ( !$this->mTitle && !is_null( $this->mPage ) && $this->mPage > 0 ) { + $this->mTitle = Title::newFromID( $this->mPage ); + } + return $this->mTitle; } @@ -789,15 +900,39 @@ class Revision implements IDBAccessObject { * Revision::RAW get the text regardless of permissions * @param $user User object to check for, only if FOR_THIS_USER is passed * to the $audience parameter + * + * @deprecated in 1.21, use getContent() instead + * @todo: replace usage in core * @return String */ public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + $content = $this->getContent( $audience, $user ); + return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable + } + + /** + * Fetch revision content if it's available to the specified audience. + * If the specified audience does not have the ability to view this + * revision, null will be returned. + * + * @param $audience Integer: one of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * @param $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter + * @since 1.21 + * @return Content|null + */ + public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) { if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) { - return ''; + return null; } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) { - return ''; + return null; } else { - return $this->getRawText(); + return $this->getContentInternal(); } } @@ -816,15 +951,109 @@ class Revision implements IDBAccessObject { * Fetch revision text without regard for view restrictions * * @return String + * + * @deprecated since 1.21. Instead, use Revision::getContent( Revision::RAW ) + * or Revision::getSerializedData() as appropriate. */ public function getRawText() { - if( is_null( $this->mText ) ) { - // Revision text is immutable. Load on demand: - $this->mText = $this->loadText(); - } + ContentHandler::deprecated( __METHOD__, "1.21" ); + return $this->getText( self::RAW ); + } + + /** + * Fetch original serialized data without regard for view restrictions + * + * @since 1.21 + * @return String + */ + public function getSerializedData() { return $this->mText; } + /** + * Gets the content object for the revision + * + * @since 1.21 + * @return Content + */ + protected function getContentInternal() { + if( is_null( $this->mContent ) ) { + // Revision is immutable. Load on demand: + + $handler = $this->getContentHandler(); + $format = $this->getContentFormat(); + + if( is_null( $this->mText ) ) { + // Load text on demand: + $this->mText = $this->loadText(); + } + + $this->mContent = is_null( $this->mText ) ? null : $handler->unserializeContent( $this->mText, $format ); + } + + return $this->mContent->copy(); // NOTE: copy() will return $this for immutable content objects + } + + /** + * Returns the content model for this revision. + * + * If no content model was stored in the database, $this->getTitle()->getContentModel() is + * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT + * is used as a last resort. + * + * @return String the content model id associated with this revision, see the CONTENT_MODEL_XXX constants. + **/ + public function getContentModel() { + if ( !$this->mContentModel ) { + $title = $this->getTitle(); + $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT ); + + assert( !empty( $this->mContentModel ) ); + } + + return $this->mContentModel; + } + + /** + * Returns the content format for this revision. + * + * If no content format was stored in the database, the default format for this + * revision's content model is returned. + * + * @return String the content format id associated with this revision, see the CONTENT_FORMAT_XXX constants. + **/ + public function getContentFormat() { + if ( !$this->mContentFormat ) { + $handler = $this->getContentHandler(); + $this->mContentFormat = $handler->getDefaultFormat(); + + assert( !empty( $this->mContentFormat ) ); + } + + return $this->mContentFormat; + } + + /** + * Returns the content handler appropriate for this revision's content model. + * + * @throws MWException + * @return ContentHandler + */ + public function getContentHandler() { + if ( !$this->mContentHandler ) { + $model = $this->getContentModel(); + $this->mContentHandler = ContentHandler::getForModelID( $model ); + + $format = $this->getContentFormat(); + + if ( !$this->mContentHandler->isSupportedFormat( $format ) ) { + throw new MWException( "Oops, the content format $format is not supported for this content model, $model" ); + } + } + + return $this->mContentHandler; + } + /** * @return String */ @@ -1004,13 +1233,16 @@ class Revision implements IDBAccessObject { * number on success and dies horribly on failure. * * @param $dbw DatabaseBase: (master connection) + * @throws MWException * @return Integer */ public function insertOn( $dbw ) { - global $wgDefaultExternalStore; + global $wgDefaultExternalStore, $wgContentHandlerUseDB; wfProfileIn( __METHOD__ ); + $this->checkContentModel(); + $data = $this->mText; $flags = self::compressRevisionText( $data ); @@ -1046,27 +1278,47 @@ class Revision implements IDBAccessObject { $rev_id = isset( $this->mId ) ? $this->mId : $dbw->nextSequenceValue( 'revision_rev_id_seq' ); - $dbw->insert( 'revision', - array( - 'rev_id' => $rev_id, - 'rev_page' => $this->mPage, - 'rev_text_id' => $this->mTextId, - 'rev_comment' => $this->mComment, - 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0, - 'rev_user' => $this->mUser, - 'rev_user_text' => $this->mUserText, - 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ), - 'rev_deleted' => $this->mDeleted, - 'rev_len' => $this->mSize, - 'rev_parent_id' => is_null( $this->mParentId ) - ? $this->getPreviousRevisionId( $dbw ) - : $this->mParentId, - 'rev_sha1' => is_null( $this->mSha1 ) - ? self::base36Sha1( $this->mText ) - : $this->mSha1 - ), __METHOD__ + $row = array( + 'rev_id' => $rev_id, + 'rev_page' => $this->mPage, + 'rev_text_id' => $this->mTextId, + 'rev_comment' => $this->mComment, + 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0, + 'rev_user' => $this->mUser, + 'rev_user_text' => $this->mUserText, + 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ), + 'rev_deleted' => $this->mDeleted, + 'rev_len' => $this->mSize, + 'rev_parent_id' => is_null( $this->mParentId ) + ? $this->getPreviousRevisionId( $dbw ) + : $this->mParentId, + 'rev_sha1' => is_null( $this->mSha1 ) + ? Revision::base36Sha1( $this->mText ) + : $this->mSha1, ); + if ( $wgContentHandlerUseDB ) { + //NOTE: Store null for the default model and format, to save space. + //XXX: Makes the DB sensitive to changed defaults. Make this behaviour optional? Only in miser mode? + + $model = $this->getContentModel(); + $format = $this->getContentFormat(); + + $title = $this->getTitle(); + + if ( $title === null ) { + throw new MWException( "Insufficient information to determine the title of the revision's page!" ); + } + + $defaultModel = ContentHandler::getDefaultModelFor( $title ); + $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat(); + + $row[ 'rev_content_model' ] = ( $model === $defaultModel ) ? null : $model; + $row[ 'rev_content_format' ] = ( $format === $defaultFormat ) ? null : $format; + } + + $dbw->insert( 'revision', $row, __METHOD__ ); + $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId(); wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) ); @@ -1075,6 +1327,52 @@ class Revision implements IDBAccessObject { return $this->mId; } + protected function checkContentModel() { + global $wgContentHandlerUseDB; + + $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted. + + $model = $this->getContentModel(); + $format = $this->getContentFormat(); + $handler = $this->getContentHandler(); + + if ( !$handler->isSupportedFormat( $format ) ) { + $t = $title->getPrefixedDBkey(); + + throw new MWException( "Can't use format $format with content model $model on $t" ); + } + + if ( !$wgContentHandlerUseDB && $title ) { + // if $wgContentHandlerUseDB is not set, all revisions must use the default content model and format. + + $defaultModel = ContentHandler::getDefaultModelFor( $title ); + $defaultHandler = ContentHandler::getForModelID( $defaultModel ); + $defaultFormat = $defaultHandler->getDefaultFormat(); + + if ( $this->getContentModel() != $defaultModel ) { + $t = $title->getPrefixedDBkey(); + + throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: " + . "model is $model , default for $t is $defaultModel" ); + } + + if ( $this->getContentFormat() != $defaultFormat ) { + $t = $title->getPrefixedDBkey(); + + throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: " + . "format is $format, default for $t is $defaultFormat" ); + } + } + + $content = $this->getContent( Revision::RAW ); + + if ( !$content->isValid() ) { + $t = $title->getPrefixedDBkey(); + + throw new MWException( "Content of $t is not valid! Content model is $model" ); + } + } + /** * Get the base 36 SHA-1 value for a string of text * @param $text String @@ -1159,12 +1457,21 @@ class Revision implements IDBAccessObject { * @return Revision|null on error */ public static function newNullRevision( $dbw, $pageId, $summary, $minor ) { + global $wgContentHandlerUseDB; + wfProfileIn( __METHOD__ ); + $fields = array( 'page_latest', 'page_namespace', 'page_title', + 'rev_text_id', 'rev_len', 'rev_sha1' ); + + if ( $wgContentHandlerUseDB ) { + $fields[] = 'rev_content_model'; + $fields[] = 'rev_content_format'; + } + $current = $dbw->selectRow( array( 'page', 'revision' ), - array( 'page_latest', 'page_namespace', 'page_title', - 'rev_text_id', 'rev_len', 'rev_sha1' ), + $fields, array( 'page_id' => $pageId, 'page_latest=rev_id', @@ -1172,7 +1479,7 @@ class Revision implements IDBAccessObject { __METHOD__ ); if( $current ) { - $revision = new Revision( array( + $row = array( 'page' => $pageId, 'comment' => $summary, 'minor_edit' => $minor, @@ -1180,7 +1487,14 @@ class Revision implements IDBAccessObject { 'parent_id' => $current->page_latest, 'len' => $current->rev_len, 'sha1' => $current->rev_sha1 - ) ); + ); + + if ( $wgContentHandlerUseDB ) { + $row[ 'content_model' ] = $current->rev_content_model; + $row[ 'content_format' ] = $current->rev_content_format; + } + + $revision = new Revision( $row ); $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) ); } else { $revision = null; @@ -1328,4 +1642,4 @@ class Revision implements IDBAccessObject { } return true; } -} \ No newline at end of file +} diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index 6358540c6f..8919f10ad6 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -1183,6 +1183,7 @@ class Sanitizer { * attribs regex matches. * * @param $set Array + * @throws MWException * @return String */ private static function getTagAttributeCallback( $set ) { diff --git a/includes/SeleniumWebSettings.php b/includes/SeleniumWebSettings.php index 7b98568d74..0e4decfbe2 100644 --- a/includes/SeleniumWebSettings.php +++ b/includes/SeleniumWebSettings.php @@ -38,7 +38,7 @@ $cookieName = $cookiePrefix . 'Selenium'; // this is a fallback SQL file $testSqlFile = false; $testImageZip = false; - + // if we find a request parameter containing the test name, set a cookie with the test name if ( isset( $_GET['setupTestSuite'] ) ) { $setupTestSuiteName = $_GET['setupTestSuite']; @@ -62,7 +62,7 @@ if ( isset( $_GET['setupTestSuite'] ) ) { true ); } - + $testIncludes = array(); // array containing all the includes needed for this test $testGlobalConfigs = array(); // an array containg all the global configs needed for this test $testResourceFiles = array(); // an array containing all the resource files needed for this test @@ -72,11 +72,11 @@ if ( isset( $_GET['setupTestSuite'] ) ) { if ( isset( $testResourceFiles['images'] ) ) { $testImageZip = $testResourceFiles['images']; } - + if ( isset( $testResourceFiles['db'] ) ) { $testSqlFile = $testResourceFiles['db']; $testResourceName = getTestResourceNameFromTestSuiteName( $setupTestSuiteName ); - + switchToTestResources( $testResourceName, false ); // false means do not switch database yet setupTestResources( $testResourceName, $testSqlFile, $testImageZip ); } @@ -86,7 +86,7 @@ if ( isset( $_GET['setupTestSuite'] ) ) { if ( isset( $_GET['clearTestSuite'] ) ) { $testSuiteName = getTestSuiteNameFromCookie( $cookieName ); - $expire = time() - 600; + $expire = time() - 600; setcookie( $cookieName, '', @@ -96,22 +96,22 @@ if ( isset( $_GET['clearTestSuite'] ) ) { $wgCookieSecure, true ); - + $testResourceName = getTestResourceNameFromTestSuiteName( $testSuiteName ); teardownTestResources( $testResourceName ); } // if a cookie is found, run the appropriate callback to get the config params. -if ( isset( $_COOKIE[$cookieName] ) ) { +if ( isset( $_COOKIE[$cookieName] ) ) { $testSuiteName = getTestSuiteNameFromCookie( $cookieName ); if ( !isset( $wgSeleniumTestConfigs[$testSuiteName] ) ) { return; } - + $testIncludes = array(); // array containing all the includes needed for this test $testGlobalConfigs = array(); // an array containg all the global configs needed for this test $testResourceFiles = array(); // an array containing all the resource files needed for this test - $callback = $wgSeleniumTestConfigs[$testSuiteName]; + $callback = $wgSeleniumTestConfigs[$testSuiteName]; call_user_func_array( $callback, array( &$testIncludes, &$testGlobalConfigs, &$testResourceFiles)); if ( isset( $testResourceFiles['db'] ) ) { @@ -123,9 +123,8 @@ if ( isset( $_COOKIE[$cookieName] ) ) { require_once( $file ); } foreach ( $testGlobalConfigs as $key => $value ) { - if ( is_array( $value ) ) { + if ( is_array( $value ) ) { $GLOBALS[$key] = array_merge( $GLOBALS[$key], $value ); - } else { $GLOBALS[$key] = $value; } @@ -166,7 +165,7 @@ function setupTestResources( $testResourceName, $testSqlFile, $testImageZip ) { if ( $testResourceName == '' ) { die( 'Cannot identify a test the resources should be installed for.' ); } - + // create tables $dbw = wfGetDB( DB_MASTER ); $dbw->query( 'DROP DATABASE IF EXISTS ' . $testResourceName ); diff --git a/includes/Setup.php b/includes/Setup.php index 5c5d7d141f..83ca516f06 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -317,12 +317,6 @@ if ( $wgUseFileCache || $wgUseSquid ) { $wgDebugToolbar = false; } -# $wgAllowRealName and $wgAllowUserSkin were removed in 1.16 -# in favor of $wgHiddenPrefs, handle b/c here -if ( !$wgAllowRealName ) { - $wgHiddenPrefs[] = 'realname'; -} - # Doesn't make sense to have if disabled. if ( !$wgEnotifMinorEdits ) { $wgHiddenPrefs[] = 'enotifminoredits'; diff --git a/includes/Skin.php b/includes/Skin.php index 9bee8a2735..24d48bc44c 100644 --- a/includes/Skin.php +++ b/includes/Skin.php @@ -1536,6 +1536,7 @@ abstract class Skin extends ContextSource { * * @param $fname String Name of called method * @param $args Array Arguments to the method + * @throws MWException * @return mixed */ function __call( $fname, $args ) { diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php index f8f29bc86d..5a32d47138 100644 --- a/includes/SkinTemplate.php +++ b/includes/SkinTemplate.php @@ -503,6 +503,7 @@ class SkinTemplate extends Skin { * By default it is capitalized. * * @param $name string Language name, e.g. "English" or "español" + * @return string * @private */ function formatLanguageName( $name ) { @@ -1443,6 +1444,7 @@ abstract class BaseTemplate extends QuickTemplate { } if ( isset( $this->data['nav_urls']['info'] ) && $this->data['nav_urls']['info'] ) { $toolbox['info'] = $this->data['nav_urls']['info']; + $toolbox['info']['id'] = 't-info'; } wfRunHooks( 'BaseTemplateToolbox', array( &$this, &$toolbox ) ); diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index 2e5e02b0fc..a72c1af566 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -105,19 +105,6 @@ class SpecialPage { return SpecialPageFactory::resolveAlias( $alias ); } - /** - * 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 $page SpecialPage - * @deprecated since 1.7, warnings in 1.17, might be removed in 1.20 - */ - static function addPage( &$page ) { - wfDeprecated( __METHOD__, '1.7' ); - SpecialPageFactory::getList()->{$page->mName} = $page; - } - /** * Add a page to a certain display group for Special:SpecialPages * @@ -267,6 +254,7 @@ class SpecialPage { * * @param $name String * @param $subpage String|Bool subpage string, or false to not use a subpage + * @throws MWException * @return Title object */ public static function getTitleFor( $name, $subpage = false ) { @@ -363,6 +351,7 @@ class SpecialPage { * * @param $fName String Name of called method * @param $a Array Arguments to the method + * @throws MWException * @deprecated since 1.17, call parent::__construct() */ public function __call( $fName, $a ) { @@ -532,9 +521,8 @@ class SpecialPage { * pages? */ public function isRestricted() { - global $wgGroupPermissions; // DWIM: If all anons can do something, then it is not restricted - return $this->mRestriction != '' && empty( $wgGroupPermissions['*'][$this->mRestriction] ); + return $this->mRestriction != '' && User::groupHasPermission( '*', $this->mRestriction ); } /** @@ -945,8 +933,8 @@ abstract class FormSpecialPage extends SpecialPage { * Called from execute() to check if the given user can perform this action. * Failures here must throw subclasses of ErrorPageError. * @param $user User + * @throws UserBlockedError * @return Bool true - * @throws ErrorPageError */ protected function checkExecutePermissions( User $user ) { $this->checkPermissions(); diff --git a/includes/SqlDataUpdate.php b/includes/SqlDataUpdate.php index 52c9be00d4..d0ead9ad8b 100644 --- a/includes/SqlDataUpdate.php +++ b/includes/SqlDataUpdate.php @@ -95,7 +95,7 @@ abstract class SqlDataUpdate extends DataUpdate { * Abort the database transaction started via beginTransaction (if any). */ public function abortTransaction() { - if ( $this->mHasTransaction ) { + if ( $this->mHasTransaction ) { //XXX: actually... maybe always? $this->mDb->rollback( get_class( $this ) . '::abortTransaction' ); $this->mHasTransaction = false; } @@ -108,8 +108,8 @@ abstract class SqlDataUpdate extends DataUpdate { * @param $namespace Integer * @param $dbkeys Array */ - protected function invalidatePages( $namespace, Array $dbkeys ) { - if ( !count( $dbkeys ) ) { + protected function invalidatePages( $namespace, array $dbkeys ) { + if ( $dbkeys === array() ) { return; } @@ -127,10 +127,12 @@ abstract class SqlDataUpdate extends DataUpdate { 'page_touched < ' . $this->mDb->addQuotes( $now ) ), __METHOD__ ); + foreach ( $res as $row ) { $ids[] = $row->page_id; } - if ( !count( $ids ) ) { + + if ( $ids === array() ) { return; } diff --git a/includes/SquidPurgeClient.php b/includes/SquidPurgeClient.php index 8eb0f6bfc8..7d75f2ca9a 100644 --- a/includes/SquidPurgeClient.php +++ b/includes/SquidPurgeClient.php @@ -21,9 +21,9 @@ */ /** - * An HTTP 1.0 client built for the purposes of purging Squid and Varnish. - * Uses asynchronous I/O, allowing purges to be done in a highly parallel - * manner. + * An HTTP 1.0 client built for the purposes of purging Squid and Varnish. + * Uses asynchronous I/O, allowing purges to be done in a highly parallel + * manner. * * Could be replaced by curl_multi_exec() or some such. */ @@ -123,7 +123,7 @@ class SquidPurgeClient { return array( $socket ); } - /** + /** * Get the host's IP address. * Does not support IPv6 at present due to the lack of a convenient interface in PHP. */ @@ -176,11 +176,32 @@ class SquidPurgeClient { * @param $url string */ public function queuePurge( $url ) { + global $wgSquidPurgeUseHostHeader; $url = SquidUpdate::expand( str_replace( "\n", '', $url ) ); - $this->requests[] = "PURGE $url HTTP/1.0\r\n" . - "Connection: Keep-Alive\r\n" . - "Proxy-Connection: Keep-Alive\r\n" . - "User-Agent: " . Http::userAgent() . ' ' . __CLASS__ . "\r\n\r\n"; + $request = array(); + if ( $wgSquidPurgeUseHostHeader ) { + $url = wfParseUrl( $url ); + $host = $url['host']; + if ( isset( $url['port'] ) && strlen( $url['port'] ) > 0 ) { + $host .= ":" . $url['port']; + } + $path = $url['path']; + if ( isset( $url['query'] ) && is_string( $url['query'] ) ) { + $path = wfAppendQuery( $path, $url['query'] ); + } + $request[] = "PURGE $path HTTP/1.1"; + $request[] = "Host: $host"; + } else { + $request[] = "PURGE $url HTTP/1.0"; + } + $request[] = "Connection: Keep-Alive"; + $request[] = "Proxy-Connection: Keep-Alive"; + $request[] = "User-Agent: " . Http::userAgent() . ' ' . __CLASS__; + // Two ''s to create \r\n\r\n + $request[] = ''; + $request[] = ''; + + $this->requests[] = implode( "\r\n", $request ); if ( $this->currentRequestIndex === null ) { $this->nextRequest(); } @@ -408,7 +429,7 @@ class SquidPurgeClientPool { $numReady = socket_select( $readSockets, $writeSockets, $exceptSockets, $timeout ); wfRestoreWarnings(); if ( $numReady === false ) { - wfDebugLog( 'squid', __METHOD__.': Error in stream_select: ' . + wfDebugLog( 'squid', __METHOD__.': Error in stream_select: ' . socket_strerror( socket_last_error() ) . "\n" ); break; } diff --git a/includes/Status.php b/includes/Status.php index 10dfb516b8..763c95cd2c 100644 --- a/includes/Status.php +++ b/includes/Status.php @@ -225,6 +225,15 @@ class Status { } } + /** + * Get the error message as HTML. This is done by parsing the wikitext error + * message. + */ + public function getHTML( $shortContext = false, $longContext = false ) { + $text = $this->getWikiText( $shortContext, $longContext ); + return MessageCache::singleton()->transform( $text, true ); + } + /** * Return an array with the wikitext for each item in the array. * @param $errors Array diff --git a/includes/StreamFile.php b/includes/StreamFile.php index 95c69a20b6..b0e6c12ec2 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -35,6 +35,7 @@ class StreamFile { * @param $fname string Full name and path of the file to stream * @param $headers array Any additional headers to send * @param $sendErrors bool Send error messages if errors occur (like 404) + * @throws MWException * @return bool Success */ public static function stream( $fname, $headers = array(), $sendErrors = true ) { diff --git a/includes/StringUtils.php b/includes/StringUtils.php index 43275a660f..fba31ea976 100644 --- a/includes/StringUtils.php +++ b/includes/StringUtils.php @@ -424,7 +424,7 @@ class ReplacementArray { /** * An iterator which works exactly like: - * + * * foreach ( explode( $delim, $s ) as $element ) { * ... * } diff --git a/includes/StubObject.php b/includes/StubObject.php index 615bcb5f97..cc0adb743d 100644 --- a/includes/StubObject.php +++ b/includes/StubObject.php @@ -108,6 +108,7 @@ class StubObject { * @param $name String: name of the method called in this object. * @param $level Integer: level to go in the stact trace to get the function * who called this function. + * @throws MWException */ function _unstub( $name = '_unstub', $level = 2 ) { static $recursionLevel = 0; diff --git a/includes/Timestamp.php b/includes/Timestamp.php index a5844b6722..56ce46c1ac 100644 --- a/includes/Timestamp.php +++ b/includes/Timestamp.php @@ -196,7 +196,7 @@ class MWTimestamp { * * @since 1.20 * - * @return string Formatted timestamp + * @return Message Formatted timestamp */ public function getHumanTimestamp() { $then = $this->getTimestamp( TS_UNIX ); diff --git a/includes/Title.php b/includes/Title.php index 3573198b2c..c3e37a0f2b 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -65,6 +65,7 @@ class Title { var $mFragment; // /< Title fragment (i.e. the bit after the #) var $mArticleID = -1; // /< Article ID, fetched from the link cache on demand var $mLatestID = false; // /< ID of most recent revision + var $mContentModel = false; // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants private $mEstimateRevisions; // /< Estimated number of revisions; null of not loaded var $mRestrictions = array(); // /< Array of groups allowed to edit this article var $mOldRestrictions = false; @@ -199,6 +200,27 @@ class Title { } } + /** + * Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries. + * Uses $wgContentHandlerUseDB to determine whether to include page_content_model. + * + * @return array + */ + protected static function getSelectFields() { + global $wgContentHandlerUseDB; + + $fields = array( + 'page_namespace', 'page_title', 'page_id', + 'page_len', 'page_is_redirect', 'page_latest', + ); + + if ( $wgContentHandlerUseDB ) { + $fields[] = 'page_content_model'; + } + + return $fields; + } + /** * Create a new Title from an article ID * @@ -210,10 +232,7 @@ class Title { $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); $row = $db->selectRow( 'page', - array( - 'page_namespace', 'page_title', 'page_id', - 'page_len', 'page_is_redirect', 'page_latest', - ), + self::getSelectFields(), array( 'page_id' => $id ), __METHOD__ ); @@ -239,10 +258,7 @@ class Title { $res = $dbr->select( 'page', - array( - 'page_namespace', 'page_title', 'page_id', - 'page_len', 'page_is_redirect', 'page_latest', - ), + self::getSelectFields(), array( 'page_id' => $ids ), __METHOD__ ); @@ -282,11 +298,16 @@ class Title { $this->mRedirect = (bool)$row->page_is_redirect; if ( isset( $row->page_latest ) ) $this->mLatestID = (int)$row->page_latest; + if ( isset( $row->page_content_model ) ) + $this->mContentModel = strval( $row->page_content_model ); + else + $this->mContentModel = false; # initialized lazily in getContentModel() } else { // page not found $this->mArticleID = 0; $this->mLength = 0; $this->mRedirect = false; $this->mLatestID = 0; + $this->mContentModel = false; # initialized lazily in getContentModel() } } @@ -312,6 +333,7 @@ class Title { $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; $t->mUrlform = wfUrlencode( $t->mDbkeyform ); $t->mTextform = str_replace( '_', ' ', $title ); + $t->mContentModel = false; # initialized lazily in getContentModel() return $t; } @@ -362,9 +384,13 @@ class Title { * * @param $text String: Text with possible redirect * @return Title: The corresponding Title + * @deprecated since 1.21, use Content::getRedirectTarget instead. */ public static function newFromRedirect( $text ) { - return self::newFromRedirectInternal( $text ); + ContentHandler::deprecated( __METHOD__, '1.21' ); + + $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); + return $content->getRedirectTarget(); } /** @@ -375,10 +401,13 @@ class Title { * * @param $text String Text with possible redirect * @return Title + * @deprecated since 1.21, use Content::getUltimateRedirectTarget instead. */ public static function newFromRedirectRecurse( $text ) { - $titles = self::newFromRedirectArray( $text ); - return $titles ? array_pop( $titles ) : null; + ContentHandler::deprecated( __METHOD__, '1.21' ); + + $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); + return $content->getUltimateRedirectTarget(); } /** @@ -389,71 +418,13 @@ class Title { * * @param $text String Text with possible redirect * @return Array of Titles, with the destination last + * @deprecated since 1.21, use Content::getRedirectChain instead. */ public static function newFromRedirectArray( $text ) { - global $wgMaxRedirects; - $title = self::newFromRedirectInternal( $text ); - if ( is_null( $title ) ) { - return null; - } - // recursive check to follow double redirects - $recurse = $wgMaxRedirects; - $titles = array( $title ); - while ( --$recurse > 0 ) { - if ( $title->isRedirect() ) { - $page = WikiPage::factory( $title ); - $newtitle = $page->getRedirectTarget(); - } else { - break; - } - // Redirects to some special pages are not permitted - if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) { - // the new title passes the checks, so make that our current title so that further recursion can be checked - $title = $newtitle; - $titles[] = $newtitle; - } else { - break; - } - } - return $titles; - } + ContentHandler::deprecated( __METHOD__, '1.21' ); - /** - * Really extract the redirect destination - * Do not call this function directly, use one of the newFromRedirect* functions above - * - * @param $text String Text with possible redirect - * @return Title - */ - protected static function newFromRedirectInternal( $text ) { - global $wgMaxRedirects; - if ( $wgMaxRedirects < 1 ) { - //redirects are disabled, so quit early - return null; - } - $redir = MagicWord::get( 'redirect' ); - $text = trim( $text ); - if ( $redir->matchStartAndRemove( $text ) ) { - // Extract the first link and see if it's usable - // Ensure that it really does come directly after #REDIRECT - // Some older redirects included a colon, so don't freak about that! - $m = array(); - if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) { - // Strip preceding colon used to "escape" categories, etc. - // and URL-decode links - if ( strpos( $m[1], '%' ) !== false ) { - // Match behavior of inline link parsing here; - $m[1] = rawurldecode( ltrim( $m[1], ':' ) ); - } - $title = Title::newFromText( $m[1] ); - // If the title is a redirect to bad special pages or is invalid, return null - if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) { - return null; - } - return $title; - } - } - return null; + $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); + return $content->getRedirectChain(); } /** @@ -701,6 +672,38 @@ class Title { return $this->mNamespace; } + /** + * Get the page's content model id, see the CONTENT_MODEL_XXX constants. + * + * @return String: Content model id + */ + public function getContentModel() { + if ( !$this->mContentModel ) { + $linkCache = LinkCache::singleton(); + $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' ); + } + + if ( !$this->mContentModel ) { + $this->mContentModel = ContentHandler::getDefaultModelFor( $this ); + } + + if( !$this->mContentModel ) { + throw new MWException( "failed to determin content model!" ); + } + + return $this->mContentModel; + } + + /** + * Convenience method for checking a title's content model name + * + * @param String $id The content model ID (use the CONTENT_MODEL_XXX constants). + * @return Boolean true if $this->getContentModel() == $id + */ + public function hasContentModel( $id ) { + return $this->getContentModel() == $id; + } + /** * Get the namespace text * @@ -934,6 +937,8 @@ class Title { * @return Bool */ public function isConversionTable() { + //@todo: ConversionTable should become a separate content model. + return $this->getNamespace() == NS_MEDIAWIKI && strpos( $this->getText(), 'Conversiontable/' ) === 0; } @@ -944,22 +949,31 @@ class Title { * @return Bool */ public function isWikitextPage() { - $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage(); - wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) ); - return $retval; + return $this->hasContentModel( CONTENT_MODEL_WIKITEXT ); } /** - * Could this page contain custom CSS or JavaScript, based - * on the title? + * Could this page contain custom CSS or JavaScript for the global UI. + * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS + * or CONTENT_MODEL_JAVASCRIPT. + * + * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() for that! + * + * Note that this method should not return true for pages that contain and show "inactive" CSS or JS. * * @return Bool */ public function isCssOrJsPage() { - $retval = $this->mNamespace == NS_MEDIAWIKI - && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0; - wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) ); - return $retval; + $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace + && ( $this->hasContentModel( CONTENT_MODEL_CSS ) + || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); + + #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure + # hook funktions can force this method to return true even outside the mediawiki namespace. + + wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) ); + + return $isCssOrJsPage; } /** @@ -967,7 +981,9 @@ class Title { * @return Bool */ public function isCssJsSubpage() { - return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) ); + return ( NS_USER == $this->mNamespace && $this->isSubpage() + && ( $this->hasContentModel( CONTENT_MODEL_CSS ) + || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) ); } /** @@ -990,7 +1006,8 @@ class Title { * @return Bool */ public function isCssSubpage() { - return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) ); + return ( NS_USER == $this->mNamespace && $this->isSubpage() + && $this->hasContentModel( CONTENT_MODEL_CSS ) ); } /** @@ -999,7 +1016,8 @@ class Title { * @return Bool */ public function isJsSubpage() { - return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) ); + return ( NS_USER == $this->mNamespace && $this->isSubpage() + && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); } /** @@ -1723,15 +1741,8 @@ class Title { if ( !$user->isAllowed( 'move' ) ) { // User can't move anything - global $wgGroupPermissions; - $userCanMove = false; - if ( isset( $wgGroupPermissions['user']['move'] ) ) { - $userCanMove = $wgGroupPermissions['user']['move']; - } - $autoconfirmedCanMove = false; - if ( isset( $wgGroupPermissions['autoconfirmed']['move'] ) ) { - $autoconfirmedCanMove = $wgGroupPermissions['autoconfirmed']['move']; - } + $userCanMove = User::groupHasPermission( 'user', 'move' ); + $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' ); if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { // custom message if logged-in users without any special rights can move $errors[] = array( 'movenologintext' ); @@ -2072,13 +2083,13 @@ class Title { * @return Array list of errors */ private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { - global $wgWhitelistRead, $wgGroupPermissions, $wgRevokePermissions; + global $wgWhitelistRead, $wgRevokePermissions; static $useShortcut = null; # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below if ( is_null( $useShortcut ) ) { $useShortcut = true; - if ( empty( $wgGroupPermissions['*']['read'] ) ) { + if ( !User::groupHasPermission( '*', 'read' ) ) { # Not a public wiki, so no shortcut $useShortcut = false; } elseif ( !empty( $wgRevokePermissions ) ) { @@ -2908,8 +2919,16 @@ class Title { if ( !$this->getArticleID( $flags ) ) { return $this->mRedirect = false; } + $linkCache = LinkCache::singleton(); - $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' ); + $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' ); + if ( $cached === null ) { + // TODO: check the assumption that the cache actually knows about this title + // and handle this, such as get the title from the database. + // See https://bugzilla.wikimedia.org/show_bug.cgi?id=37209 + } + + $this->mRedirect = (bool)$cached; return $this->mRedirect; } @@ -2930,7 +2949,14 @@ class Title { return $this->mLength = 0; } $linkCache = LinkCache::singleton(); - $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) ); + $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' ); + if ( $cached === null ) { # check the assumption that the cache actually knows about this title + # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209 + # as a stop gap, perhaps log this, but don't throw an exception? + throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() ); + } + + $this->mLength = intval( $cached ); return $this->mLength; } @@ -2950,7 +2976,14 @@ class Title { return $this->mLatestID = 0; } $linkCache = LinkCache::singleton(); - $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) ); + $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' ); + if ( $cached === null ) { # check the assumption that the cache actually knows about this title + # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209 + # as a stop gap, perhaps log this, but don't throw an exception? + throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() ); + } + + $this->mLatestID = intval( $cached ); return $this->mLatestID; } @@ -2979,6 +3012,7 @@ class Title { $this->mRedirect = null; $this->mLength = -1; $this->mLatestID = false; + $this->mContentModel = false; $this->mEstimateRevisions = null; } @@ -3217,7 +3251,7 @@ class Title { $res = $db->select( array( 'page', $table ), - array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), + self::getSelectFields(), array( "{$prefix}_from=page_id", "{$prefix}_namespace" => $this->getNamespace(), @@ -3267,6 +3301,8 @@ class Title { * @return Array of Title objects linking here */ public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { + global $wgContentHandlerUseDB; + $id = $this->getArticleID(); # If the page doesn't exist; there can't be any link from this page @@ -3283,9 +3319,12 @@ class Title { $namespaceFiled = "{$prefix}_namespace"; $titleField = "{$prefix}_title"; + $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ); + if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model'; + $res = $db->select( array( $table, 'page' ), - array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), + $fields, array( "{$prefix}_from" => $id ), __METHOD__, $options, @@ -3417,7 +3456,7 @@ class Title { * @return Mixed True on success, getUserPermissionsErrors()-like array on failure */ public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { - global $wgUser; + global $wgUser, $wgContentHandlerUseDB; $errors = array(); if ( !$nt ) { @@ -3450,6 +3489,15 @@ class Title { $errors[] = array( 'badarticleerror' ); } + // Content model checks + if ( !$wgContentHandlerUseDB && + $this->getContentModel() !== $nt->getContentModel() ) { + // can't move a page if that would change the page's content model + $errors[] = array( 'bad-target-model', + ContentHandler::getLocalizedName( $this->getContentModel() ), + ContentHandler::getLocalizedName( $nt->getContentModel() ) ); + } + // Image-specific checks if ( $this->getNamespace() == NS_FILE ) { $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) ); @@ -3676,7 +3724,14 @@ class Title { $logType = 'move'; } - $redirectSuppressed = !$createRedirect; + if ( $createRedirect ) { + $contentHandler = ContentHandler::getForTitle( $this ); + $redirectContent = $contentHandler->makeRedirectContent( $nt ); + + // NOTE: If this page's content model does not support redirects, $redirectContent will be null. + } else { + $redirectContent = null; + } $logEntry = new ManualLogEntry( 'move', $logType ); $logEntry->setPerformer( $wgUser ); @@ -3684,7 +3739,7 @@ class Title { $logEntry->setComment( $reason ); $logEntry->setParameters( array( '4::target' => $nt->getPrefixedText(), - '5::noredir' => $redirectSuppressed ? '1': '0', + '5::noredir' => $redirectContent ? '0': '1', ) ); $formatter = LogFormatter::newFromEntry( $logEntry ); @@ -3719,7 +3774,8 @@ class Title { if ( !is_object( $nullRevision ) ) { throw new MWException( 'No valid null revision produced in ' . __METHOD__ ); } - $nullRevId = $nullRevision->insertOn( $dbw ); + + $nullRevision->insertOn( $dbw ); # Change the name of the target page: $dbw->update( 'page', @@ -3746,18 +3802,16 @@ class Title { } # Recreate the redirect, this time in the other direction. - if ( $redirectSuppressed ) { + if ( !$redirectContent ) { WikiPage::onArticleDelete( $this ); } else { - $mwRedir = MagicWord::get( 'redirect' ); - $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; $redirectArticle = WikiPage::factory( $this ); $newid = $redirectArticle->insertOn( $dbw ); if ( $newid ) { // sanity $redirectRevision = new Revision( array( 'page' => $newid, 'comment' => $comment, - 'text' => $redirectText ) ); + 'content' => $redirectContent ) ); $redirectRevision->insertOn( $dbw ); $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); @@ -3853,10 +3907,16 @@ class Title { * @return Bool */ public function isSingleRevRedirect() { + global $wgContentHandlerUseDB; + $dbw = wfGetDB( DB_MASTER ); + # Is it a redirect? + $fields = array( 'page_is_redirect', 'page_latest', 'page_id' ); + if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model'; + $row = $dbw->selectRow( 'page', - array( 'page_is_redirect', 'page_latest', 'page_id' ), + $fields, $this->pageCond(), __METHOD__, array( 'FOR UPDATE' ) @@ -3865,6 +3925,7 @@ class Title { $this->mArticleID = $row ? intval( $row->page_id ) : 0; $this->mRedirect = $row ? (bool)$row->page_is_redirect : false; $this->mLatestID = $row ? intval( $row->page_latest ) : false; + $this->mContentModel = $row && isset( $row->page_content_model ) ? strval( $row->page_content_model ) : false; if ( !$this->mRedirect ) { return false; } @@ -3909,24 +3970,25 @@ class Title { if( !is_object( $rev ) ){ return false; } - $text = $rev->getText(); + $content = $rev->getContent(); # Does the redirect point to the source? # Or is it a broken self-redirect, usually caused by namespace collisions? - $m = array(); - if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) { - $redirTitle = Title::newFromText( $m[1] ); - if ( !is_object( $redirTitle ) || - ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() && - $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) { + $redirTitle = $content->getRedirectTarget(); + + if ( $redirTitle ) { + if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() && + $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) { wfDebug( __METHOD__ . ": redirect points to other page\n" ); return false; + } else { + return true; } } else { - # Fail safe - wfDebug( __METHOD__ . ": failsafe\n" ); + # Fail safe (not a redirect after all. strange.) + wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() . + " is a redirect, but it doesn't contain a valid redirect.\n" ); return false; } - return true; } /** @@ -4625,17 +4687,13 @@ class Title { if ( $this->isSpecialPage() ) { // special pages are in the user language return $wgLang; - } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) { - // css/js should always be LTR and is, in fact, English - return wfGetLangObj( 'en' ); - } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) { - // Parse mediawiki messages with correct target language - list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $this->getText() ); - return wfGetLangObj( $lang ); } - global $wgContLang; - // If nothing special, it should be in the wiki content language - $pageLang = $wgContLang; + + //TODO: use the LinkCache to cache this! Note that this may depend on user settings, so the cache should be only per-request. + //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language! + $contentHandler = ContentHandler::getForTitle( $this ); + $pageLang = $contentHandler->getPageLanguage( $this ); + // Hook at the end because we don't want to override the above stuff wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) ); return wfGetLangObj( $pageLang ); @@ -4650,19 +4708,23 @@ class Title { * @return Language */ public function getPageViewLanguage() { - $pageLang = $this->getPageLanguage(); - // If this is nothing special (so the content is converted when viewed) - if ( !$this->isSpecialPage() - && !$this->isCssOrJsPage() && !$this->isCssJsSubpage() - && $this->getNamespace() !== NS_MEDIAWIKI - ) { + global $wgLang; + + if ( $this->isSpecialPage() ) { // If the user chooses a variant, the content is actually // in a language whose code is the variant code. - $variant = $pageLang->getPreferredVariant(); - if ( $pageLang->getCode() !== $variant ) { - $pageLang = Language::factory( $variant ); + $variant = $wgLang->getPreferredVariant(); + if ( $wgLang->getCode() !== $variant ) { + return Language::factory( $variant ); } + + return $wgLang; } + + //NOTE: can't be cached persistently, depends on user settings + //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language! + $contentHandler = ContentHandler::getForTitle( $this ); + $pageLang = $contentHandler->getPageViewLanguage( $this ); return $pageLang; } } diff --git a/includes/User.php b/includes/User.php index 8216914266..a197077011 100644 --- a/includes/User.php +++ b/includes/User.php @@ -765,6 +765,7 @@ class User { * - 'usable' Valid for batch processes and login * - 'creatable' Valid for batch processes, login and account creation * + * @throws MWException * @return bool|string */ public static function getCanonicalName( $name, $validate = 'valid' ) { @@ -816,39 +817,16 @@ class User { /** * Count the number of edits of a user - * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas * * @param $uid Int User ID to check * @return Int the user's edit count + * + * @deprecated since 1.21 in favour of User::getEditCount */ public static function edits( $uid ) { - wfProfileIn( __METHOD__ ); - $dbr = wfGetDB( DB_SLAVE ); - // check if the user_editcount field has been initialized - $field = $dbr->selectField( - 'user', 'user_editcount', - array( 'user_id' => $uid ), - __METHOD__ - ); - - if( $field === null ) { // it has not been initialized. do so. - $dbw = wfGetDB( DB_MASTER ); - $count = $dbr->selectField( - 'revision', 'count(*)', - array( 'rev_user' => $uid ), - __METHOD__ - ); - $dbw->update( - 'user', - array( 'user_editcount' => $count ), - array( 'user_id' => $uid ), - __METHOD__ - ); - } else { - $count = $field; - } - wfProfileOut( __METHOD__ ); - return $count; + wfDeprecated( __METHOD__, '1.21' ); + $user = self::newFromId( $uid ); + return $user->getEditCount(); } /** @@ -892,7 +870,7 @@ class User { if( $loggedOut !== null ) { $this->mTouched = wfTimestamp( TS_MW, $loggedOut ); } else { - $this->mTouched = '0'; # Allow any pages to be cached + $this->mTouched = '1'; # Allow any pages to be cached } $this->mToken = null; // Don't run cryptographic functions till we need a token @@ -1190,7 +1168,9 @@ class User { } /** - * Clear various cached data stored in this object. + * Clear various cached data stored in this object. The cache of the user table + * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given. + * * @param $reloadFrom bool|String Reload user and user_groups table data from a * given source. May be "name", "id", "defaults", "session", or false for * no reload. @@ -1204,6 +1184,8 @@ class User { $this->mEffectiveGroups = null; $this->mImplicitGroups = null; $this->mOptions = null; + $this->mOptionsLoaded = false; + $this->mEditCount = null; if ( $reloadFrom ) { $this->mLoadedItems = array(); @@ -1222,9 +1204,8 @@ class User { $defOpt = $wgDefaultUserOptions; # default language setting - $variant = $wgContLang->getDefaultVariant(); - $defOpt['variant'] = $variant; - $defOpt['language'] = $variant; + $defOpt['variant'] = $wgContLang->getCode(); + $defOpt['language'] = $wgContLang->getCode(); foreach( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) { $defOpt['searchNs'.$nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] ); } @@ -2465,7 +2446,21 @@ class User { if( $this->getId() ) { if ( !isset( $this->mEditCount ) ) { /* Populate the count, if it has not been populated yet */ - $this->mEditCount = User::edits( $this->mId ); + wfProfileIn( __METHOD__ ); + $dbr = wfGetDB( DB_SLAVE ); + // check if the user_editcount field has been initialized + $count = $dbr->selectField( + 'user', 'user_editcount', + array( 'user_id' => $this->mId ), + __METHOD__ + ); + + if( $count === null ) { + // it has not been initialized. do so. + $count = $this->initEditCount(); + } + wfProfileOut( __METHOD__ ); + $this->mEditCount = $count; } return $this->mEditCount; } else { @@ -3007,7 +3002,29 @@ class User { } /** - * Add this existing user object to the database + * Add this existing user object to the database. If the user already + * exists, a fatal status object is returned, and the user object is + * initialised with the data from the database. + * + * Previously, this function generated a DB error due to a key conflict + * if the user already existed. Many extension callers use this function + * in code along the lines of: + * + * $user = User::newFromName( $name ); + * if ( !$user->isLoggedIn() ) { + * $user->addToDatabase(); + * } + * // do something with $user... + * + * However, this was vulnerable to a race condition (bug 16020). By + * initialising the user object if the user exists, we aim to support this + * calling sequence as far as possible. + * + * Note that if the user exists, this function will acquire a write lock, + * so it is still advisable to make the call conditional on isLoggedIn(), + * and to commit the transaction after calling. + * + * @return Status */ public function addToDatabase() { $this->load(); @@ -3030,14 +3047,31 @@ class User { 'user_registration' => $dbw->timestamp( $this->mRegistration ), 'user_editcount' => 0, 'user_touched' => $dbw->timestamp( $this->mTouched ), - ), __METHOD__ + ), __METHOD__, + array( 'IGNORE' ) ); + if ( !$dbw->affectedRows() ) { + $this->mId = $dbw->selectField( 'user', 'user_id', + array( 'user_name' => $this->mName ), __METHOD__ ); + $loaded = false; + if ( $this->mId ) { + if ( $this->loadFromDatabase() ) { + $loaded = true; + } + } + if ( !$loaded ) { + throw new MWException( __METHOD__. ": hit a key conflict attempting " . + "to insert a user row, but then it doesn't exist when we select it!" ); + } + return Status::newFatal( 'userexists' ); + } $this->mId = $dbw->insertId(); // Clear instance cache other than user table data, which is already accurate $this->clearInstanceCache(); $this->saveOptions(); + return Status::newGood(); } /** @@ -3639,14 +3673,27 @@ class User { public static function getGroupsWithPermission( $role ) { global $wgGroupPermissions; $allowedGroups = array(); - foreach ( $wgGroupPermissions as $group => $rights ) { - if ( isset( $rights[$role] ) && $rights[$role] ) { + foreach ( array_keys( $wgGroupPermissions ) as $group ) { + if ( self::groupHasPermission( $group, $role ) ) { $allowedGroups[] = $group; } } return $allowedGroups; } + /** + * Check, if the given group has the given permission + * + * @param $group String Group to check + * @param $role String Role to check + * @return bool + */ + public static function groupHasPermission( $group, $role ) { + global $wgGroupPermissions, $wgRevokePermissions; + return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role] + && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] ); + } + /** * Get the localized descriptive name for a group, if it exists * @@ -3885,43 +3932,63 @@ class User { public function incEditCount() { if( !$this->isAnon() ) { $dbw = wfGetDB( DB_MASTER ); - $dbw->update( 'user', + $dbw->update( + 'user', array( 'user_editcount=user_editcount+1' ), array( 'user_id' => $this->getId() ), - __METHOD__ ); + __METHOD__ + ); // Lazy initialization check... if( $dbw->affectedRows() == 0 ) { - // Pull from a slave to be less cruel to servers - // Accuracy isn't the point anyway here - $dbr = wfGetDB( DB_SLAVE ); - $count = $dbr->selectField( 'revision', - 'COUNT(rev_user)', - array( 'rev_user' => $this->getId() ), - __METHOD__ ); - // Now here's a goddamn hack... + $dbr = wfGetDB( DB_SLAVE ); if( $dbr !== $dbw ) { // If we actually have a slave server, the count is // at least one behind because the current transaction // has not been committed and replicated. - $count++; + $this->initEditCount( 1 ); } else { // But if DB_SLAVE is selecting the master, then the // count we just read includes the revision that was // just added in the working transaction. + $this->initEditCount(); } - - $dbw->update( 'user', - array( 'user_editcount' => $count ), - array( 'user_id' => $this->getId() ), - __METHOD__ ); } } // edit count in user cache too $this->invalidateCache(); } + /** + * Initialize user_editcount from data out of the revision table + * + * @param $add Integer Edits to add to the count from the revision table + * @return Integer Number of edits + */ + protected function initEditCount( $add = 0 ) { + // Pull from a slave to be less cruel to servers + // Accuracy isn't the point anyway here + $dbr = wfGetDB( DB_SLAVE ); + $count = $dbr->selectField( + 'revision', + 'COUNT(rev_user)', + array( 'rev_user' => $this->getId() ), + __METHOD__ + ); + $count = $count + $add; + + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( + 'user', + array( 'user_editcount' => $count ), + array( 'user_id' => $this->getId() ), + __METHOD__ + ); + + return $count; + } + /** * Get the description of a given right * @@ -4064,12 +4131,28 @@ class User { * @todo document */ protected function loadOptions() { + global $wgContLang; + $this->load(); - if ( $this->mOptionsLoaded || !$this->getId() ) + + if ( $this->mOptionsLoaded ) { return; + } $this->mOptions = self::getDefaultOptions(); + if ( !$this->getId() ) { + // For unlogged-in users, load language/variant options from request. + // There's no need to do it for logged-in users: they can set preferences, + // and handling of page content is done by $pageLang->getPreferredVariant() and such, + // so don't override user's choice (especially when the user chooses site default). + $variant = $wgContLang->getDefaultVariant(); + $this->mOptions['variant'] = $variant; + $this->mOptions['language'] = $variant; + $this->mOptionsLoaded = true; + return; + } + // Maybe load from the object if ( !is_null( $this->mOptionOverrides ) ) { wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" ); diff --git a/includes/UserMailer.php b/includes/UserMailer.php index 5d5ed8552c..9830f69578 100644 --- a/includes/UserMailer.php +++ b/includes/UserMailer.php @@ -152,6 +152,7 @@ class UserMailer { * @param $body String: email's text. * @param $replyto MailAddress: optional reply-to email (default: null). * @param $contentType String: optional custom Content-Type (default: text/plain; charset=UTF-8) + * @throws MWException * @return Status object */ public static function send( $to, $from, $subject, $body, $replyto = null, $contentType = 'text/plain; charset=UTF-8' ) { diff --git a/includes/WebRequest.php b/includes/WebRequest.php index 2cc6338b96..7005416410 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -380,7 +380,6 @@ class WebRequest { return $ret; } - /** * Unset an arbitrary value from our get/post data. * @@ -620,6 +619,7 @@ class WebRequest { * Return the path and query string portion of the request URI. * This will be suitable for use as a relative link in HTML output. * + * @throws MWException * @return String */ public function getRequestURL() { @@ -907,6 +907,7 @@ class WebRequest { * false if an error message has been shown and the request should be aborted. * * @param $extWhitelist array + * @throws HttpError * @return bool */ public function checkUrlExtension( $extWhitelist = array() ) { @@ -1056,9 +1057,10 @@ HTML; /** * Work out the IP address based on various globals * For trusted proxies, use the XFF client IP (first of the chain) - * + * * @since 1.19 * + * @throws MWException * @return string */ public function getIP() { @@ -1238,6 +1240,7 @@ class FauxRequest extends WebRequest { * fake GET/POST values * @param $wasPosted Bool: whether to treat the data as POST * @param $session Mixed: session array or null + * @throws MWException */ public function __construct( $data = array(), $wasPosted = false, $session = null ) { if( is_array( $data ) ) { diff --git a/includes/Wiki.php b/includes/Wiki.php index e1d84d4559..7a6b37d9fd 100644 --- a/includes/Wiki.php +++ b/includes/Wiki.php @@ -169,6 +169,7 @@ class MediaWiki { * - special pages * - normal pages * + * @throws MWException|PermissionsError|BadTitleError|HttpError * @return void */ private function performRequest() { @@ -177,7 +178,7 @@ class MediaWiki { wfProfileIn( __METHOD__ ); $request = $this->context->getRequest(); - $title = $this->context->getTitle(); + $requestTitle = $title = $this->context->getTitle(); $output = $this->context->getOutput(); $user = $this->context->getUser(); @@ -301,7 +302,7 @@ class MediaWiki { global $wgArticle; $wgArticle = new DeprecatedGlobal( 'wgArticle', $article, '1.18' ); - $this->performAction( $article ); + $this->performAction( $article, $requestTitle ); } elseif ( is_string( $article ) ) { $output->redirect( $article ); } else { @@ -395,8 +396,9 @@ class MediaWiki { * Perform one of the "standard" actions * * @param $page Page + * @param $requestTitle The original title, before any redirects were applied */ - private function performAction( Page $page ) { + private function performAction( Page $page, Title $requestTitle ) { global $wgUseSquid, $wgSquidMaxage; wfProfileIn( __METHOD__ ); @@ -419,7 +421,7 @@ class MediaWiki { if ( $action instanceof Action ) { # Let Squid cache things if we can purge them. if ( $wgUseSquid && - in_array( $request->getFullRequestURL(), $title->getSquidURLs() ) + in_array( $request->getFullRequestURL(), $requestTitle->getSquidURLs() ) ) { $output->setSquidMaxage( $wgSquidMaxage ); } @@ -595,28 +597,51 @@ class MediaWiki { if ( $wgJobRunRate <= 0 || wfReadOnly() ) { return; } + if ( $wgJobRunRate < 1 ) { $max = mt_getrandmax(); if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) { - return; + return; // the higher $wgJobRunRate, the less likely we return here } $n = 1; } else { $n = intval( $wgJobRunRate ); } - while ( $n-- && false != ( $job = Job::pop() ) ) { - $output = $job->toString() . "\n"; - $t = - microtime( true ); - $success = $job->run(); - $t += microtime( true ); - $t = round( $t * 1000 ); - if ( !$success ) { - $output .= "Error: " . $job->getLastError() . ", Time: $t ms\n"; - } else { - $output .= "Success, Time: $t ms\n"; + $group = JobQueueGroup::singleton(); + $types = $group->getDefaultQueueTypes(); + shuffle( $types ); // avoid starvation + + // Scan the queues for a job N times... + do { + $jobFound = false; // found a job in any queue? + // Find a queue with a job on it and run it... + foreach ( $types as $i => $type ) { + $queue = $group->get( $type ); + if ( $queue->isEmpty() ) { + unset( $types[$i] ); // don't keep checking this queue + continue; + } + $job = $queue->pop(); + if ( $job ) { + $jobFound = true; + $output = $job->toString() . "\n"; + $t = - microtime( true ); + $success = $job->run(); + $queue->ack( $job ); // done + $t += microtime( true ); + $t = round( $t * 1000 ); + if ( !$success ) { + $output .= "Error: " . $job->getLastError() . ", Time: $t ms\n"; + } else { + $output .= "Success, Time: $t ms\n"; + } + wfDebugLog( 'jobqueue', $output ); + break; + } else { + unset( $types[$i] ); // don't keep checking this queue + } } - wfDebugLog( 'jobqueue', $output ); - } + } while ( --$n && $jobFound ); } } diff --git a/includes/WikiFilePage.php b/includes/WikiFilePage.php index 9fb1522d73..0114cce902 100644 --- a/includes/WikiFilePage.php +++ b/includes/WikiFilePage.php @@ -41,7 +41,9 @@ class WikiFilePage extends WikiPage { } public function getActionOverrides() { - return array( 'revert' => 'RevertFileAction' ); + $overrides = parent::getActionOverrides(); + $overrides[ 'revert' ] = 'RevertFileAction'; + return $overrides; } /** @@ -103,13 +105,12 @@ class WikiFilePage extends WikiPage { } /** - * @param bool $text * @return bool */ - public function isRedirect( $text = false ) { + public function isRedirect( ) { $this->loadFile(); if ( $this->mFile->isLocal() ) { - return parent::isRedirect( $text ); + return parent::isRedirect(); } return (bool)$this->mFile->getRedirected(); diff --git a/includes/WikiPage.php b/includes/WikiPage.php index 5a3a9e6a04..bdec273134 100644 --- a/includes/WikiPage.php +++ b/includes/WikiPage.php @@ -187,7 +187,21 @@ class WikiPage extends Page implements IDBAccessObject { * @return Array */ public function getActionOverrides() { - return array(); + $content_handler = $this->getContentHandler(); + return $content_handler->getActionOverrides(); + } + + /** + * Returns the ContentHandler instance to be used to deal with the content of this WikiPage. + * + * Shorthand for ContentHandler::getForModelID( $this->getContentModel() ); + * + * @return ContentHandler + * + * @since 1.21 + */ + public function getContentHandler() { + return ContentHandler::getForModelID( $this->getContentModel() ); } /** @@ -231,7 +245,9 @@ class WikiPage extends Page implements IDBAccessObject { * @return array */ public static function selectFields() { - return array( + global $wgContentHandlerUseDB; + + $fields = array( 'page_id', 'page_namespace', 'page_title', @@ -244,6 +260,12 @@ class WikiPage extends Page implements IDBAccessObject { 'page_latest', 'page_len', ); + + if ( $wgContentHandlerUseDB ) { + $fields[] = 'page_content_model'; + } + + return $fields; } /** @@ -418,21 +440,42 @@ class WikiPage extends Page implements IDBAccessObject { } /** - * Tests if the article text represents a redirect + * Tests if the article content represents a redirect * - * @param $text mixed string containing article contents, or boolean * @return bool */ - public function isRedirect( $text = false ) { - if ( $text === false ) { - if ( !$this->mDataLoaded ) { - $this->loadPageData(); - } + public function isRedirect( ) { + $content = $this->getContent(); + if ( !$content ) return false; - return (bool)$this->mIsRedirect; - } else { - return Title::newFromRedirect( $text ) !== null; + return $content->isRedirect(); + } + + /** + * Returns the page's content model id (see the CONTENT_MODEL_XXX constants). + * + * Will use the revisions actual content model if the page exists, + * and the page's default if the page doesn't exist yet. + * + * @return String + * + * @since 1.21 + */ + public function getContentModel() { + if ( $this->exists() ) { + # look at the revision's actual content model + $rev = $this->getRevision(); + + if ( $rev !== null ) { + return $rev->getContentModel(); + } else { + $title = $this->mTitle->getPrefixedDBkey(); + wfWarn( "Page $title exists but has no (visible) revisions!" ); + } } + + # use the default model for this page + return $this->mTitle->getContentModel(); } /** @@ -554,6 +597,27 @@ class WikiPage extends Page implements IDBAccessObject { return null; } + /** + * Get the content of the current revision. No side-effects... + * + * @param $audience Integer: one of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * @param $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter + * @return Content|null The content of the current revision + * + * @since 1.21 + */ + public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) { + $this->loadLastEdit(); + if ( $this->mLastRevision ) { + return $this->mLastRevision->getContent( $audience ); + } + return null; + } + /** * Get the text of the current revision. No side-effects... * @@ -563,9 +627,12 @@ class WikiPage extends Page implements IDBAccessObject { * Revision::RAW get the text regardless of permissions * @param $user User object to check for, only if FOR_THIS_USER is passed * to the $audience parameter - * @return String|bool The text of the current revision. False on failure + * @return String|false The text of the current revision + * @deprecated as of 1.21, getContent() should be used instead. */ - public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { + public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { #@todo: deprecated, replace usage! + ContentHandler::deprecated( __METHOD__, '1.21' ); + $this->loadLastEdit(); if ( $this->mLastRevision ) { return $this->mLastRevision->getText( $audience, $user ); @@ -577,13 +644,12 @@ class WikiPage extends Page implements IDBAccessObject { * Get the text of the current revision. No side-effects... * * @return String|bool The text of the current revision. False on failure + * @deprecated as of 1.21, getContent() should be used instead. */ public function getRawText() { - $this->loadLastEdit(); - if ( $this->mLastRevision ) { - return $this->mLastRevision->getRawText(); - } - return false; + ContentHandler::deprecated( __METHOD__, '1.21' ); + + return $this->getText( Revision::RAW ); } /** @@ -733,32 +799,34 @@ class WikiPage extends Page implements IDBAccessObject { return false; } - $text = $editInfo ? $editInfo->pst : false; + if ( $editInfo ) { + $content = $editInfo->pstContent; + } else { + $content = $this->getContent(); + } - if ( $this->isRedirect( $text ) ) { + if ( !$content || $content->isRedirect( ) ) { return false; } - switch ( $wgArticleCountMethod ) { - case 'any': - return true; - case 'comma': - if ( $text === false ) { - $text = $this->getRawText(); - } - return strpos( $text, ',' ) !== false; - case 'link': + $hasLinks = null; + + if ( $wgArticleCountMethod === 'link' ) { + # nasty special case to avoid re-parsing to detect links + if ( $editInfo ) { // ParserOutput::getLinks() is a 2D array of page links, so // to be really correct we would need to recurse in the array // but the main array should only have items in it if there are // links. - return (bool)count( $editInfo->output->getLinks() ); + $hasLinks = (bool)count( $editInfo->output->getLinks() ); } else { - return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, + $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, array( 'pl_from' => $this->getId() ), __METHOD__ ); } } + + return $content->isCountable( $hasLinks ); } /** @@ -804,7 +872,8 @@ class WikiPage extends Page implements IDBAccessObject { */ public function insertRedirect() { // recurse through to only get the final target - $retval = Title::newFromRedirectRecurse( $this->getRawText() ); + $content = $this->getContent(); + $retval = $content ? $content->getUltimateRedirectTarget() : null; if ( !$retval ) { return null; } @@ -1000,7 +1069,7 @@ class WikiPage extends Page implements IDBAccessObject { && $parserOptions->getStubThreshold() == 0 && $this->mTitle->exists() && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() ) - && $this->mTitle->isWikitextPage(); + && $this->getContentHandler()->isParserCacheSupported(); } /** @@ -1011,6 +1080,7 @@ class WikiPage extends Page implements IDBAccessObject { * @param $parserOptions ParserOptions to use for the parse operation * @param $oldid Revision ID to get the text from, passing null or 0 will * get the current revision (default value) + * * @return ParserOutput or false if the revision was not found */ public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) { @@ -1088,8 +1158,16 @@ class WikiPage extends Page implements IDBAccessObject { } if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + //@todo: move this logic to MessageCache + if ( $this->mTitle->exists() ) { - $text = $this->getRawText(); + // NOTE: use transclusion text for messages. + // This is consistent with MessageCache::getMsgFromNamespace() + + $content = $this->getContent(); + $text = $content === null ? null : $content->getWikitextForTransclusion(); + + if ( $text === null ) $text = false; } else { $text = false; } @@ -1154,11 +1232,13 @@ class WikiPage extends Page implements IDBAccessObject { * @private */ public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { + global $wgContentHandlerUseDB; + wfProfileIn( __METHOD__ ); - $text = $revision->getText(); - $len = strlen( $text ); - $rt = Title::newFromRedirectRecurse( $text ); + $content = $revision->getContent(); + $len = $content->getSize(); + $rt = $content->getUltimateRedirectTarget(); $conditions = array( 'page_id' => $this->getId() ); @@ -1168,14 +1248,20 @@ class WikiPage extends Page implements IDBAccessObject { } $now = wfTimestampNow(); + $row = array( /* SET */ + 'page_latest' => $revision->getId(), + 'page_touched' => $dbw->timestamp( $now ), + 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, + 'page_is_redirect' => $rt !== null ? 1 : 0, + 'page_len' => $len, + ); + + if ( $wgContentHandlerUseDB ) { + $row[ 'page_content_model' ] = $revision->getContentModel(); + } + $dbw->update( 'page', - array( /* SET */ - 'page_latest' => $revision->getId(), - 'page_touched' => $dbw->timestamp( $now ), - 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, - 'page_is_redirect' => $rt !== null ? 1 : 0, - 'page_len' => $len, - ), + $row, $conditions, __METHOD__ ); @@ -1187,7 +1273,8 @@ class WikiPage extends Page implements IDBAccessObject { $this->mLatest = $revision->getId(); $this->mIsRedirect = (bool)$rt; # Update the LinkCache. - LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest ); + LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, + $this->mLatest, $revision->getContentModel() ); } wfProfileOut( __METHOD__ ); @@ -1270,6 +1357,21 @@ class WikiPage extends Page implements IDBAccessObject { return $ret; } + /** + * Get the content that needs to be saved in order to undo all revisions + * between $undo and $undoafter. Revisions must belong to the same page, + * must exist and must not be deleted + * @param $undo Revision + * @param $undoafter Revision Must be an earlier revision than $undo + * @return mixed string on success, false on failure + * @since 1.21 + * Before we had the Content object, this was done in getUndoText + */ + public function getUndoContent( Revision $undo, Revision $undoafter = null ) { + $handler = $undo->getContentHandler(); + return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter ); + } + /** * Get the text that needs to be saved in order to undo all revisions * between $undo and $undoafter. Revisions must belong to the same page, @@ -1277,27 +1379,29 @@ class WikiPage extends Page implements IDBAccessObject { * @param $undo Revision * @param $undoafter Revision Must be an earlier revision than $undo * @return mixed string on success, false on failure + * @deprecated since 1.21: use ContentHandler::getUndoContent() instead. */ public function getUndoText( Revision $undo, Revision $undoafter = null ) { - $cur_text = $this->getRawText(); - if ( $cur_text === false ) { - return false; // no page - } - $undo_text = $undo->getText(); - $undoafter_text = $undoafter->getText(); + ContentHandler::deprecated( __METHOD__, '1.21' ); - if ( $cur_text == $undo_text ) { - # No use doing a merge if it's just a straight revert. - return $undoafter_text; - } + $this->loadLastEdit(); - $undone_text = ''; + if ( $this->mLastRevision ) { + if ( is_null( $undoafter ) ) { + $undoafter = $undo->getPrevious(); + } - if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) { - return false; + $handler = $this->getContentHandler(); + $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter ); + + if ( !$undone ) { + return false; + } else { + return ContentHandler::getContentText( $undone ); + } } - return $undone_text; + return false; } /** @@ -1305,18 +1409,67 @@ class WikiPage extends Page implements IDBAccessObject { * @param $text String: new text of the section * @param $sectionTitle String: new section's subject, only if $section is 'new' * @param $edittime String: revision timestamp or null to use the current revision - * @return string Complete article text, or null if error + * @return String new complete article text, or null if error + * + * @deprecated since 1.21, use replaceSectionContent() instead */ public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + if ( strval( $section ) == '' ) { //NOTE: keep condition in sync with condition in replaceSectionContent! + // Whole-page edit; let the whole text through + return $text; + } + + if ( !$this->supportsSections() ) { + throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() ); + } + + # could even make section title, but that's not required. + $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); + + $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime ); + + return ContentHandler::getContentText( $newContent ); + } + + /** + * Returns true iff this page's content model supports sections. + * + * @return boolean whether sections are supported. + * + * @todo: the skin should check this and not offer section functionality if sections are not supported. + * @todo: the EditPage should check this and not offer section functionality if sections are not supported. + */ + public function supportsSections() { + return $this->getContentHandler()->supportsSections(); + } + + /** + * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...) + * @param $content Content: new content of the section + * @param $sectionTitle String: new section's subject, only if $section is 'new' + * @param $edittime String: revision timestamp or null to use the current revision + * + * @return Content new complete article content, or null if error + * + * @since 1.21 + */ + public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) { wfProfileIn( __METHOD__ ); if ( strval( $section ) == '' ) { // Whole-page edit; let the whole text through + $newContent = $sectionContent; } else { + if ( !$this->supportsSections() ) { + throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() ); + } + // Bug 30711: always use current version when adding a new section if ( is_null( $edittime ) || $section == 'new' ) { - $oldtext = $this->getRawText(); - if ( $oldtext === false ) { + $oldContent = $this->getContent(); + if ( ! $oldContent ) { wfDebug( __METHOD__ . ": no page text\n" ); wfProfileOut( __METHOD__ ); return null; @@ -1332,28 +1485,14 @@ class WikiPage extends Page implements IDBAccessObject { return null; } - $oldtext = $rev->getText(); + $oldContent = $rev->getContent(); } - if ( $section == 'new' ) { - # Inserting a new section - $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' ) - ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : ''; - if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) { - $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 ); - } + $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle ); } wfProfileOut( __METHOD__ ); - return $text; + return $newContent; } /** @@ -1419,8 +1558,66 @@ class WikiPage extends Page implements IDBAccessObject { * revision: The revision object for the inserted revision, or null * * Compatibility note: this function previously returned a boolean value indicating success/failure + * + * @deprecated since 1.21: use doEditContent() instead. */ public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + + return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user ); + } + + /** + * Change an existing article or create a new article. Updates RC and all necessary caches, + * optionally via the deferred update array. + * + * @param $content Content: new content + * @param $summary String: edit summary + * @param $flags Integer 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 an + * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an + * edit-already-exists error will be returned. These two conditions are also possible with + * auto-detection due to MediaWiki's performance-optimised locking strategy. + * + * @param $baseRevId the revision ID this edit was based off, if any + * @param $user User the user doing the edit + * @param $serialisation_format String: format for storing the content in the database + * + * @return Status object. Possible errors: + * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't set the fatal flag of $status + * edit-gone-missing: In update mode, but the article didn't exist + * edit-conflict: In update mode, the article changed unexpectedly + * edit-no-change: Warning that the text was the same as before + * edit-already-exists: In creation mode, but the article already exists + * + * Extensions may define additional errors. + * + * $return->value will contain an associative array with members as follows: + * new: Boolean indicating if the function attempted to create a new article + * revision: The revision object for the inserted revision, or null + * + * @since 1.21 + */ + public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, + User $user = null, $serialisation_format = null ) { global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; # Low-level sanity check @@ -1430,6 +1627,13 @@ class WikiPage extends Page implements IDBAccessObject { wfProfileIn( __METHOD__ ); + if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) { + wfProfileOut( __METHOD__ ); + return Status::newFatal( 'content-not-allowed-here', + ContentHandler::getLocalizedName( $content->getModel() ), + $this->getTitle()->getPrefixedText() ); + } + $user = is_null( $user ) ? $wgUser : $user; $status = Status::newGood( array() ); @@ -1440,10 +1644,14 @@ class WikiPage extends Page implements IDBAccessObject { $flags = $this->checkFlags( $flags ); - if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary, - $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) ) - { - wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" ); + # handle hook + $hook_args = array( &$this, &$user, &$content, &$summary, + $flags & EDIT_MINOR, null, null, &$flags, &$status ); + + if ( !wfRunHooks( 'PageContentSave', $hook_args ) + || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) { + + wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" ); if ( $status->isOK() ) { $status->fatal( 'edit-hook-aborted' ); @@ -1457,20 +1665,25 @@ class WikiPage extends Page implements IDBAccessObject { $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); $bot = $flags & EDIT_FORCE_BOT; - $oldtext = $this->getRawText(); // current revision - $oldsize = strlen( $oldtext ); + $old_content = $this->getContent( Revision::RAW ); // current revision's content + + $oldsize = $old_content ? $old_content->getSize() : 0; $oldid = $this->getLatest(); $oldIsRedirect = $this->isRedirect(); $oldcountable = $this->isCountable(); + $handler = $content->getContentHandler(); + # Provide autosummaries if one is not provided and autosummaries are enabled. if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { - $summary = self::getAutosummary( $oldtext, $text, $flags ); + if ( !$old_content ) $old_content = null; + $summary = $handler->getAutosummary( $old_content, $content, $flags ); } - $editInfo = $this->prepareTextForEdit( $text, null, $user ); - $text = $editInfo->pst; - $newsize = strlen( $text ); + $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format ); + $serialized = $editInfo->pst; + $content = $editInfo->pstContent; + $newsize = $content->getSize(); $dbw = wfGetDB( DB_MASTER ); $now = wfTimestampNow(); @@ -1487,7 +1700,7 @@ class WikiPage extends Page implements IDBAccessObject { wfProfileOut( __METHOD__ ); return $status; - } elseif ( $oldtext === false ) { + } elseif ( !$old_content ) { # Sanity check for bug 37225 wfProfileOut( __METHOD__ ); throw new MWException( "Could not find text for current revision {$oldid}." ); @@ -1497,20 +1710,35 @@ class WikiPage extends Page implements IDBAccessObject { 'page' => $this->getId(), 'comment' => $summary, 'minor_edit' => $isminor, - 'text' => $text, + 'text' => $serialized, + 'len' => $newsize, 'parent_id' => $oldid, 'user' => $user->getId(), 'user_text' => $user->getName(), - 'timestamp' => $now - ) ); - # Bug 37225: use accessor to get the text as Revision may trim it. - # After trimming, the text may be a duplicate of the current text. - $text = $revision->getText(); // sanity; EditPage should trim already + 'timestamp' => $now, + 'content_model' => $content->getModel(), + 'content_format' => $serialisation_format, + ) ); #XXX: pass content object?! - $changed = ( strcmp( $text, $oldtext ) != 0 ); + $changed = !$content->equals( $old_content ); if ( $changed ) { + if ( !$content->isValid() ) { + throw new MWException( "New content failed validity check!" ); + } + $dbw->begin( __METHOD__ ); + + $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); + $status->merge( $prepStatus ); + + if ( !$status->isOK() ) { + $dbw->rollback(); + + wfProfileOut( __METHOD__ ); + return $status; + } + $revisionId = $revision->insertOn( $dbw ); # Update page @@ -1558,8 +1786,14 @@ class WikiPage extends Page implements IDBAccessObject { } # Update links tables, site stats, etc. - $this->doEditUpdates( $revision, $user, array( 'changed' => $changed, - 'oldcountable' => $oldcountable ) ); + $this->doEditUpdates( + $revision, + $user, + array( + 'changed' => $changed, + 'oldcountable' => $oldcountable + ) + ); if ( !$changed ) { $status->warning( 'edit-no-change' ); @@ -1574,6 +1808,18 @@ class WikiPage extends Page implements IDBAccessObject { $dbw->begin( __METHOD__ ); + $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); + $status->merge( $prepStatus ); + + if ( !$status->isOK() ) { + $dbw->rollback(); + + wfProfileOut( __METHOD__ ); + return $status; + } + + $status->merge( $prepStatus ); + # Add the page record; stake our claim on this title! # This will return false if the article already exists $newid = $this->insertOn( $dbw ); @@ -1591,15 +1837,18 @@ class WikiPage extends Page implements IDBAccessObject { 'page' => $newid, 'comment' => $summary, 'minor_edit' => $isminor, - 'text' => $text, + 'text' => $serialized, + 'len' => $newsize, 'user' => $user->getId(), 'user_text' => $user->getName(), - 'timestamp' => $now + 'timestamp' => $now, + 'content_model' => $content->getModel(), + 'content_format' => $serialisation_format, ) ); $revisionId = $revision->insertOn( $dbw ); # Bug 37225: use accessor to get the text as Revision may trim it - $text = $revision->getText(); // sanity; EditPage should trim already + $content = $revision->getContent(); // sanity; get normalized version # Update the page record with revision data $this->updateRevisionOn( $dbw, $revision, 0 ); @@ -1613,7 +1862,7 @@ class WikiPage extends Page implements IDBAccessObject { $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); # Add RC row to the DB $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, - '', strlen( $text ), $revisionId, $patrolled ); + '', $content->getSize(), $revisionId, $patrolled ); # Log auto-patrolled edits if ( $patrolled ) { @@ -1626,8 +1875,11 @@ class WikiPage extends Page implements IDBAccessObject { # Update links, etc. $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); - wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); + $hook_args = array( &$this, &$user, $content, $summary, + $flags & EDIT_MINOR, null, null, &$flags, $revision ); + + ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args ); + wfRunHooks( 'PageContentInsertComplete', $hook_args ); } # Do updates right now unless deferral was requested @@ -1638,8 +1890,11 @@ class WikiPage extends Page implements IDBAccessObject { // Return the new revision (or null) to the caller $status->value['revision'] = $revision; - wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) ); + $hook_args = array( &$this, &$user, $content, $summary, + $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ); + + ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args ); + wfRunHooks( 'PageContentSaveComplete', $hook_args ); # Promote user to any groups they meet the criteria for $user->addAutopromoteOnceGroups( 'onEdit' ); @@ -1651,6 +1906,8 @@ class WikiPage extends Page implements IDBAccessObject { /** * Get parser options suitable for rendering the primary article wikitext * + * @see ContentHandler::makeParserOptions + * * @param IContextSource|User|string $context One of the following: * - IContextSource: Use the User and the Language of the provided * context @@ -1661,38 +1918,52 @@ class WikiPage extends Page implements IDBAccessObject { * @return ParserOptions */ public function makeParserOptions( $context ) { - global $wgContLang; - - if ( $context instanceof IContextSource ) { - $options = ParserOptions::newFromContext( $context ); - } elseif ( $context instanceof User ) { // settings per user (even anons) - $options = ParserOptions::newFromUser( $context ); - } else { // canonical settings - $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); - } + $options = $this->getContentHandler()->makeParserOptions( $context ); if ( $this->getTitle()->isConversionTable() ) { + //@todo: ConversionTable should become a separate content model, so we don't need special cases like this one. $options->disableContentConversion(); } - $options->enableLimitReport(); // show inclusion/loop reports - $options->setTidy( true ); // fix bad HTML - return $options; } /** * Prepare text which is about to be saved. * Returns a stdclass with source, pst and output members - * @return bool|object + * + * @deprecated in 1.21: use prepareContentForEdit instead. */ public function prepareTextForEdit( $text, $revid = null, User $user = null ) { + ContentHandler::deprecated( __METHOD__, '1.21' ); + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + return $this->prepareContentForEdit( $content, $revid , $user ); + } + + /** + * Prepare content which is about to be saved. + * Returns a stdclass with source, pst and output members + * + * @param \Content $content + * @param null $revid + * @param null|\User $user + * @param null $serialization_format + * + * @return bool|object + * + * @since 1.21 + */ + public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) { global $wgParser, $wgContLang, $wgUser; $user = is_null( $user ) ? $wgUser : $user; - // @TODO fixme: check $user->getId() here??? + //XXX: check $user->getId() here??? + if ( $this->mPreparedEdit - && $this->mPreparedEdit->newText == $text + && $this->mPreparedEdit->newContent + && $this->mPreparedEdit->newContent->equals( $content ) && $this->mPreparedEdit->revid == $revid + && $this->mPreparedEdit->format == $serialization_format + #XXX: also check $user here? ) { // Already prepared return $this->mPreparedEdit; @@ -1703,11 +1974,21 @@ class WikiPage extends Page implements IDBAccessObject { $edit = (object)array(); $edit->revid = $revid; - $edit->newText = $text; - $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); + + $edit->pstContent = $content->preSaveTransform( $this->mTitle, $user, $popts ); + $edit->pst = $edit->pstContent->serialize( $serialization_format ); #XXX: do we need this?? + $edit->format = $serialization_format; + $edit->popts = $this->makeParserOptions( 'canonical' ); - $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid ); - $edit->oldText = $this->getRawText(); + + $edit->output = $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts ); + + $edit->newContent = $content; + $edit->oldContent = $this->getContent( Revision::RAW ); + + #NOTE: B/C for hooks! don't use these fields! + $edit->newText = ContentHandler::getContentText( $edit->newContent ); + $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : ''; $this->mPreparedEdit = $edit; @@ -1720,7 +2001,6 @@ class WikiPage extends Page implements IDBAccessObject { * Purges pages that include this page if the text was changed here. * Every 100th edit, prune the recent changes table. * - * @private * @param $revision Revision object * @param $user User object that did the revision * @param $options Array of options, following indexes are used: @@ -1737,13 +2017,13 @@ class WikiPage extends Page implements IDBAccessObject { wfProfileIn( __METHOD__ ); $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null ); - $text = $revision->getText(); + $content = $revision->getContent(); # Parse the text # Be careful not to double-PST: $text is usually already PST-ed once if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); - $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user ); + $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user ); } else { wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); $editInfo = $this->mPreparedEdit; @@ -1756,7 +2036,7 @@ class WikiPage extends Page implements IDBAccessObject { } # Update the links tables and other secondary data - $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle ); + $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, true, $editInfo->output ); DataUpdate::runUpdates( $updates ); wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); @@ -1801,7 +2081,8 @@ class WikiPage extends Page implements IDBAccessObject { } DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) ); - DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) ); + DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) ); + #@TODO: let the search engine decide what to do with the content object # If this is another user's talk page, update newtalk. # Don't do this if $options['changed'] = false (null-edits) nor if @@ -1827,7 +2108,11 @@ class WikiPage extends Page implements IDBAccessObject { } if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - MessageCache::singleton()->replace( $shortTitle, $text ); + #XXX: could skip pseudo-messages like js/css here, based on content model. + $msgtext = $content->getWikitextForTransclusion(); + if ( $msgtext === false || $msgtext === null ) $msgtext = ''; + + MessageCache::singleton()->replace( $shortTitle, $msgtext ); } if( $options['created'] ) { @@ -1848,17 +2133,40 @@ class WikiPage extends Page implements IDBAccessObject { * @param $user User The relevant user * @param $comment String: comment submitted * @param $minor Boolean: whereas it's a minor modification + * + * @deprecated since 1.21, use doEditContent() instead. */ public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) { + ContentHandler::deprecated( __METHOD__, "1.21" ); + + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + return $this->doQuickEditContent( $content, $user, $comment , $minor ); + } + + /** + * Edit an article without doing all that other stuff + * The article must already exist; link tables etc + * are not updated, caches are not flushed. + * + * @param $content Content: content submitted + * @param $user User The relevant user + * @param $comment String: comment submitted + * @param $serialisation_format String: format for storing the content in the database + * @param $minor Boolean: whereas it's a minor modification + */ + public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) { wfProfileIn( __METHOD__ ); + $serialized = $content->serialize( $serialisation_format ); + $dbw = wfGetDB( DB_MASTER ); $revision = new Revision( array( 'page' => $this->getId(), - 'text' => $text, + 'text' => $serialized, + 'length' => $content->getSize(), 'comment' => $comment, 'minor_edit' => $minor ? 1 : 0, - ) ); + ) ); #XXX: set the content object? $revision->insertOn( $dbw ); $this->updateRevisionOn( $dbw, $revision ); @@ -2152,7 +2460,7 @@ class WikiPage extends Page implements IDBAccessObject { public function doDeleteArticleReal( $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null ) { - global $wgUser; + global $wgUser, $wgContentHandlerUseDB; wfDebug( __METHOD__ . "\n" ); @@ -2193,6 +2501,9 @@ class WikiPage extends Page implements IDBAccessObject { $bitfield = 'rev_deleted'; } + // we need to remember the old content so we can use it to generate all deletion updates. + $content = $this->getContent( Revision::RAW ); + $dbw = wfGetDB( DB_MASTER ); $dbw->begin( __METHOD__ ); // For now, shunt the revision data into the archive table. @@ -2205,25 +2516,34 @@ class WikiPage extends Page implements IDBAccessObject { // // In the future, we may keep revisions and mark them with // the rev_deleted field, which is reserved for this purpose. + + $row = array( + 'ar_namespace' => 'page_namespace', + 'ar_title' => 'page_title', + 'ar_comment' => 'rev_comment', + 'ar_user' => 'rev_user', + 'ar_user_text' => 'rev_user_text', + 'ar_timestamp' => 'rev_timestamp', + 'ar_minor_edit' => 'rev_minor_edit', + 'ar_rev_id' => 'rev_id', + 'ar_parent_id' => 'rev_parent_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', + 'ar_deleted' => $bitfield, + 'ar_sha1' => 'rev_sha1', + ); + + if ( $wgContentHandlerUseDB ) { + $row[ 'ar_content_model' ] = 'rev_content_model'; + $row[ 'ar_content_format' ] = 'rev_content_format'; + } + $dbw->insertSelect( 'archive', array( 'page', 'revision' ), + $row, array( - 'ar_namespace' => 'page_namespace', - 'ar_title' => 'page_title', - 'ar_comment' => 'rev_comment', - 'ar_user' => 'rev_user', - 'ar_user_text' => 'rev_user_text', - 'ar_timestamp' => 'rev_timestamp', - 'ar_minor_edit' => 'rev_minor_edit', - 'ar_rev_id' => 'rev_id', - 'ar_parent_id' => 'rev_parent_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', - 'ar_deleted' => $bitfield, - 'ar_sha1' => 'rev_sha1' - ), array( 'page_id' => $id, 'page_id = rev_page' ), __METHOD__ @@ -2239,7 +2559,7 @@ class WikiPage extends Page implements IDBAccessObject { return $status; } - $this->doDeleteUpdates( $id ); + $this->doDeleteUpdates( $id, $content ); # Log the deletion, if the page was suppressed, log it at Oversight instead $logtype = $suppress ? 'suppress' : 'delete'; @@ -2255,7 +2575,7 @@ class WikiPage extends Page implements IDBAccessObject { $dbw->commit( __METHOD__ ); } - wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id ) ); + wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) ); $status->value = $logid; return $status; } @@ -2264,13 +2584,15 @@ class WikiPage extends Page implements IDBAccessObject { * Do some database updates after deletion * * @param $id Int: page_id value of the page being deleted (B/C, currently unused) + * @param $content Content: optional page content to be used when determining the required updates. + * This may be needed because $this->getContent() may already return null when the page proper was deleted. */ - public function doDeleteUpdates( $id ) { + public function doDeleteUpdates( $id, Content $content = null ) { # update site status DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) ); # remove secondary indexes, etc - $updates = $this->getDeletionUpdates( ); + $updates = $this->getDeletionUpdates( $content ); DataUpdate::runUpdates( $updates ); # Clear caches @@ -2283,16 +2605,6 @@ class WikiPage extends Page implements IDBAccessObject { $this->mTitle->resetArticleID( 0 ); } - public function getDeletionUpdates() { - $updates = array( - new LinksDeletionUpdate( $this ), - ); - - //@todo: make a hook to add update objects - //NOTE: deletion updates will be determined by the ContentHandler in the future - return $updates; - } - /** * Roll back the most recent consecutive set of edits to a page * from the same user; fails if there are no eligible edits to @@ -2465,7 +2777,12 @@ class WikiPage extends Page implements IDBAccessObject { } # Actually store the edit - $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser ); + $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser ); + + if ( !$status->isOK() ) { + return $status->getErrorsArray(); + } + if ( !empty( $status->value['revision'] ) ) { $revId = $status->value['revision']->getId(); } else { @@ -2611,60 +2928,23 @@ class WikiPage extends Page implements IDBAccessObject { /** * Return an applicable autosummary if one exists for the given edit. - * @param $oldtext String: the previous text of the page. - * @param $newtext String: The submitted text of the page. + * @param $oldtext String|null: the previous text of the page. + * @param $newtext String|null: The submitted text of the page. * @param $flags Int bitmask: a bitmask of flags submitted for the edit. * @return string An appropriate autosummary, or an empty string. + * + * @deprecated since 1.21, use ContentHandler::getAutosummary() instead */ public static function getAutosummary( $oldtext, $newtext, $flags ) { - global $wgContLang; - - # Decide what kind of autosummary is needed. - - # Redirect autosummaries - $ot = Title::newFromRedirect( $oldtext ); - $rt = Title::newFromRedirect( $newtext ); - - if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) { - $truncatedtext = $wgContLang->truncate( - str_replace( "\n", ' ', $newtext ), - max( 0, 255 - - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() ) - - strlen( $rt->getFullText() ) - ) ); - return wfMessage( 'autoredircomment', $rt->getFullText() ) - ->rawParams( $truncatedtext )->inContentLanguage()->text(); - } - - # New page autosummaries - if ( $flags & EDIT_NEW && strlen( $newtext ) ) { - # If they're making a new article, give its text, truncated, in the summary. - - $truncatedtext = $wgContLang->truncate( - str_replace( "\n", ' ', $newtext ), - max( 0, 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ) ); - - return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext ) - ->inContentLanguage()->text(); - } + # NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't. - # Blanking autosummaries - if ( $oldtext != '' && $newtext == '' ) { - return wfMessage( 'autosumm-blank' )->inContentLanguage()->text(); - } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) { - # Removing more than 90% of the article + ContentHandler::deprecated( __METHOD__, '1.21' ); - $truncatedtext = $wgContLang->truncate( - $newtext, - max( 0, 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ) ); + $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT ); + $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext ); + $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext ); - return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext ) - ->inContentLanguage()->text(); - } - - # If we reach this point, there's no applicable autosummary for our case, so our - # autosummary is empty. - return ''; + return $handler->getAutosummary( $oldContent, $newContent, $flags ); } /** @@ -2675,95 +2955,7 @@ class WikiPage extends Page implements IDBAccessObject { * if no revision occurred */ public function getAutoDeleteReason( &$hasHistory ) { - global $wgContLang; - - // Get the last revision - $rev = $this->getRevision(); - - if ( is_null( $rev ) ) { - return false; - } - - // Get the article's contents - $contents = $rev->getText(); - $blank = false; - - // If the page is blank, use the text from the previous revision, - // which can only be blank if there's a move/import/protect dummy revision involved - if ( $contents == '' ) { - $prev = $rev->getPrevious(); - - if ( $prev ) { - $contents = $prev->getText(); - $blank = true; - } - } - - $dbw = wfGetDB( DB_MASTER ); - - // Find out if there was only one contributor - // Only scan the last 20 revisions - $res = $dbw->select( 'revision', 'rev_user_text', - array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ), - __METHOD__, - array( 'LIMIT' => 20 ) - ); - - if ( $res === false ) { - // This page has no revisions, which is very weird - return false; - } - - $hasHistory = ( $res->numRows() > 1 ); - $row = $dbw->fetchObject( $res ); - - if ( $row ) { // $row is false if the only contributor is hidden - $onlyAuthor = $row->rev_user_text; - // Try to find a second contributor - foreach ( $res as $row ) { - if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999 - $onlyAuthor = false; - break; - } - } - } else { - $onlyAuthor = false; - } - - // Generate the summary with a '$1' placeholder - if ( $blank ) { - // The current revision is blank and the one before is also - // blank. It's just not our lucky day - $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text(); - } else { - if ( $onlyAuthor ) { - $reason = wfMessage( - 'excontentauthor', - '$1', - $onlyAuthor - )->inContentLanguage()->text(); - } else { - $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text(); - } - } - - if ( $reason == '-' ) { - // Allow these UI messages to be blanked out cleanly - return ''; - } - - // Replace newlines with spaces to prevent uglyness - $contents = preg_replace( "/[\n\r]/", ' ', $contents ); - // Calculate the maximum amount of chars to get - // Max content length = max comment length - length of the comment (excl. $1) - $maxLength = 255 - ( strlen( $reason ) - 2 ); - $contents = $wgContLang->truncate( $contents, $maxLength ); - // Remove possible unfinished links - $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents ); - // Now replace the '$1' placeholder - $reason = str_replace( '$1', $contents, $reason ); - - return $reason; + return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory ); } /** @@ -3005,6 +3197,31 @@ class WikiPage extends Page implements IDBAccessObject { global $wgUser; return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid ); } + + /** + * Returns a list of updates to be performed when this page is deleted. The updates should remove any information + * about this page from secondary data stores such as links tables. + * + * @param Content|null $content optional Content object for determining the necessary updates + * @return Array an array of DataUpdates objects + */ + public function getDeletionUpdates( Content $content = null ) { + if ( !$content ) { + // load content object, which may be used to determine the necessary updates + // XXX: the content may not be needed to determine the updates, then this would be overhead. + $content = $this->getContent( Revision::RAW ); + } + + if ( !$content ) { + $updates = array(); + } else { + $updates = $content->getDeletionUpdates( $this ); + } + + wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) ); + return $updates; + } + } class PoolWorkArticleView extends PoolCounterWork { @@ -3030,9 +3247,9 @@ class PoolWorkArticleView extends PoolCounterWork { private $parserOptions; /** - * @var string|null + * @var Content|null */ - private $text; + private $content = null; /** * @var ParserOutput|bool @@ -3056,14 +3273,20 @@ class PoolWorkArticleView extends PoolCounterWork { * @param $revid Integer: ID of the revision being parsed * @param $useParserCache Boolean: whether to use the parser cache * @param $parserOptions parserOptions to use for the parse operation - * @param $text String: text to parse or null to load it + * @param $content Content|String: content to parse or null to load it; may also be given as a wikitext string, for BC */ - function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) { + function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) { + if ( is_string($content) ) { #BC: old style call + $modelId = $page->getRevision()->getContentModel(); + $format = $page->getRevision()->getContentFormat(); + $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format ); + } + $this->page = $page; $this->revid = $revid; $this->cacheable = $useParserCache; $this->parserOptions = $parserOptions; - $this->text = $text; + $this->content = $content; $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions ); parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid ); } @@ -3099,25 +3322,35 @@ class PoolWorkArticleView extends PoolCounterWork { * @return bool */ function doWork() { - global $wgParser, $wgUseFileCache; + global $wgUseFileCache; + + // @todo: several of the methods called on $this->page are not declared in Page, but present + // in WikiPage and delegated by Article. $isCurrent = $this->revid === $this->page->getLatest(); - if ( $this->text !== null ) { - $text = $this->text; + if ( $this->content !== null ) { + $content = $this->content; } elseif ( $isCurrent ) { - $text = $this->page->getRawText(); + #XXX: why use RAW audience here, and PUBLIC (default) below? + $content = $this->page->getContent( Revision::RAW ); } else { $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid ); + if ( $rev === null ) { - return false; + $content = null; + } else { + #XXX: why use PUBLIC audience here (default), and RAW above? + $content = $rev->getContent(); } - $text = $rev->getText(); + } + + if ( $content === null ) { + return false; } $time = - microtime( true ); - $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(), - $this->parserOptions, true, true, $this->revid ); + $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions ); $time += microtime( true ); # Timing hack @@ -3186,3 +3419,4 @@ class PoolWorkArticleView extends PoolCounterWork { return false; } } + diff --git a/includes/Xml.php b/includes/Xml.php index 4a55d5e7ce..2f8ba0fe39 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -59,6 +59,7 @@ class Xml { * The values are passed to Sanitizer::encodeAttribute. * Return null if no attributes given. * @param $attribs Array of attributes for an XML element + * @throws MWException * @return null|string */ public static function expandAttributes( $attribs ) { @@ -207,7 +208,7 @@ class Xml { /** * Construct a language selector appropriate for use in a form or preferences - * + * * @param string $selected The language code of the selected language * @param boolean $customisedOnly If true only languages which have some content are listed * @param string $inLanguage The ISO code of the language to display the select list in (optional) diff --git a/includes/ZipDirectoryReader.php b/includes/ZipDirectoryReader.php index 0e84583f58..ccdf2bb110 100644 --- a/includes/ZipDirectoryReader.php +++ b/includes/ZipDirectoryReader.php @@ -596,6 +596,7 @@ class ZipDirectoryReader { * * @param $offset int The offset into the string at which to start unpacking. * + * @throws MWException * @return array Unpacked associative array. Note that large integers in the input * may be represented as floating point numbers in the return value, so * the use of weak comparison is advised. @@ -622,7 +623,6 @@ class ZipDirectoryReader { } else { // Unsigned little-endian integer $length = intval( $type ); - $bytes = substr( $string, $pos, $length ); // Calculate the value. Use an algorithm which automatically // upgrades the value to floating point if necessary. diff --git a/includes/actions/CreditsAction.php b/includes/actions/CreditsAction.php index defd93e480..d0bc22cb5d 100644 --- a/includes/actions/CreditsAction.php +++ b/includes/actions/CreditsAction.php @@ -29,12 +29,204 @@ class CreditsAction extends FormlessAction { return 'credits'; } + protected function getDescription() { + return $this->msg( 'creditspage' )->escaped(); + } + /** * This is largely cadged from PageHistory::history * * @return String HTML */ public function onView() { - $this->getOutput()->redirect( $this->getTitle()->getLocalURL( "action=info" ) ); + wfProfileIn( __METHOD__ ); + + if ( $this->page->getID() == 0 ) { + $s = $this->msg( 'nocredits' )->parse(); + } else { + $s = $this->getCredits( -1 ); + } + + wfProfileOut( __METHOD__ ); + + return Html::rawElement( 'div', array( 'id' => 'mw-credits' ), $s ); + } + + /** + * Get a list of contributors + * + * @param $cnt Int: maximum list of contributors to show + * @param $showIfMax Bool: whether to contributors if there more than $cnt + * @return String: html + */ + public function getCredits( $cnt, $showIfMax = true ) { + wfProfileIn( __METHOD__ ); + $s = ''; + + if ( $cnt != 0 ) { + $s = $this->getAuthor( $this->page ); + if ( $cnt > 1 || $cnt < 0 ) { + $s .= ' ' . $this->getContributors( $cnt - 1, $showIfMax ); + } + } + + wfProfileOut( __METHOD__ ); + return $s; + } + + /** + * Get the last author with the last modification time + * @param $article Article object + * @return String HTML + */ + protected function getAuthor( Page $article ) { + $user = User::newFromName( $article->getUserText(), false ); + + $timestamp = $article->getTimestamp(); + if ( $timestamp ) { + $lang = $this->getLanguage(); + $d = $lang->date( $article->getTimestamp(), true ); + $t = $lang->time( $article->getTimestamp(), true ); + } else { + $d = ''; + $t = ''; + } + return $this->msg( 'lastmodifiedatby', $d, $t )->rawParams( + $this->userLink( $user ) )->params( $user->getName() )->escaped(); + } + + /** + * Get a list of contributors of $article + * @param $cnt Int: maximum list of contributors to show + * @param $showIfMax Bool: whether to contributors if there more than $cnt + * @return String: html + */ + protected function getContributors( $cnt, $showIfMax ) { + global $wgHiddenPrefs; + + $contributors = $this->page->getContributors(); + + $others_link = false; + + # Hmm... too many to fit! + if ( $cnt > 0 && $contributors->count() > $cnt ) { + $others_link = $this->othersLink(); + if ( !$showIfMax ) + return $this->msg( 'othercontribs' )->rawParams( + $others_link )->params( $contributors->count() )->escaped(); + } + + $real_names = array(); + $user_names = array(); + $anon_ips = array(); + + # Sift for real versus user names + foreach ( $contributors as $user ) { + $cnt--; + if ( $user->isLoggedIn() ) { + $link = $this->link( $user ); + if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) { + $real_names[] = $link; + } else { + $user_names[] = $link; + } + } else { + $anon_ips[] = $this->link( $user ); + } + + if ( $cnt == 0 ) { + break; + } + } + + $lang = $this->getLanguage(); + + if ( count( $real_names ) ) { + $real = $lang->listToText( $real_names ); + } else { + $real = false; + } + + # "ThisSite user(s) A, B and C" + if ( count( $user_names ) ) { + $user = $this->msg( 'siteusers' )->rawParams( $lang->listToText( $user_names ) )->params( + count( $user_names ) )->escaped(); + } else { + $user = false; + } + + if ( count( $anon_ips ) ) { + $anon = $this->msg( 'anonusers' )->rawParams( $lang->listToText( $anon_ips ) )->params( + count( $anon_ips ) )->escaped(); + } else { + $anon = false; + } + + # This is the big list, all mooshed together. We sift for blank strings + $fulllist = array(); + foreach ( array( $real, $user, $anon, $others_link ) as $s ) { + if ( $s !== false ) { + array_push( $fulllist, $s ); + } + } + + $count = count( $fulllist ); + # "Based on work by ..." + return $count + ? $this->msg( 'othercontribs' )->rawParams( + $lang->listToText( $fulllist ) )->params( $count )->escaped() + : ''; + } + + /** + * Get a link to $user's user page + * @param $user User object + * @return String: html + */ + protected function link( User $user ) { + global $wgHiddenPrefs; + if ( !in_array( 'realname', $wgHiddenPrefs ) && !$user->isAnon() ) { + $real = $user->getRealName(); + } else { + $real = false; + } + + $page = $user->isAnon() + ? SpecialPage::getTitleFor( 'Contributions', $user->getName() ) + : $user->getUserPage(); + + return Linker::link( $page, htmlspecialchars( $real ? $real : $user->getName() ) ); + } + + /** + * Get a link to $user's user page + * @param $user User object + * @return String: html + */ + protected function userLink( User $user ) { + $link = $this->link( $user ); + if ( $user->isAnon() ) { + return $this->msg( 'anonuser' )->rawParams( $link )->parse(); + } else { + global $wgHiddenPrefs; + if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) { + return $link; + } else { + return $this->msg( 'siteuser' )->rawParams( $link )->params( $user->getName() )->escaped(); + } + } + } + + /** + * Get a link to action=credits of $article page + * @return String: HTML link + */ + protected function othersLink() { + return Linker::linkKnown( + $this->getTitle(), + $this->msg( 'others' )->escaped(), + array(), + array( 'action' => 'credits' ) + ); } } diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index dcd6fe5505..61de3b6328 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -158,8 +158,12 @@ class HistoryAction extends FormlessAction { } else { $conds = array(); } - $checkDeleted = Xml::checkLabel( $this->msg( 'history-show-deleted' )->text(), + if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { + $checkDeleted = Xml::checkLabel( $this->msg( 'history-show-deleted' )->text(), 'deleted', 'mw-show-deleted-only', $request->getBool( 'deleted' ) ) . "\n"; + } else { + $checkDeleted = ''; + } // Add the general form $action = htmlspecialchars( $wgScript ); @@ -572,7 +576,7 @@ class HistoryPager extends ReverseChronologicalPager { } elseif ( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) { // If revision was hidden from sysops, disable the link if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) { - $cdel = Linker::revDeleteLinkDisabled( false ); + $del = Linker::revDeleteLinkDisabled( false ); // Otherwise, show the link... } else { $query = array( 'type' => 'revision', diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index cb04ec52f3..510f4ef48c 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -56,7 +56,116 @@ class InfoAction extends FormlessAction { * @return string Page information that will be added to the output */ public function onView() { - global $wgContLang, $wgDisableCounters, $wgRCMaxAge; + $content = ''; + + // Validate revision + $oldid = $this->page->getOldID(); + if ( $oldid ) { + $revision = $this->page->getRevisionFetched(); + + // Revision is missing + if ( $revision === null ) { + return $this->msg( 'missing-revision', $oldid )->parse(); + } + + // Revision is not current + if ( !$revision->isCurrent() ) { + return $this->msg( 'pageinfo-not-current' )->plain(); + } + } + + // Page header + if ( !$this->msg( 'pageinfo-header' )->isDisabled() ) { + $content .= $this->msg( 'pageinfo-header' )->parse(); + } + + // Hide "This page is a member of # hidden categories" explanation + $content .= Html::element( 'style', array(), + '.mw-hiddenCategoriesExplanation { display: none; }' ); + + // Hide "Templates used on this page" explanation + $content .= Html::element( 'style', array(), + '.mw-templatesUsedExplanation { display: none; }' ); + + // Get page information + $pageInfo = $this->pageInfo(); + + // Allow extensions to add additional information + wfRunHooks( 'InfoAction', array( $this->getContext(), &$pageInfo ) ); + + // Render page information + foreach ( $pageInfo as $header => $infoTable ) { + $content .= $this->makeHeader( $this->msg( "pageinfo-${header}" )->escaped() ); + $table = ''; + foreach ( $infoTable as $infoRow ) { + $name = ( $infoRow[0] instanceof Message ) ? $infoRow[0]->escaped() : $infoRow[0]; + $value = ( $infoRow[1] instanceof Message ) ? $infoRow[1]->escaped() : $infoRow[1]; + $table = $this->addRow( $table, $name, $value ); + } + $content = $this->addTable( $content, $table ); + } + + // Page footer + if ( !$this->msg( 'pageinfo-footer' )->isDisabled() ) { + $content .= $this->msg( 'pageinfo-footer' )->parse(); + } + + // Page credits + /*if ( $this->page->exists() ) { + $content .= Html::rawElement( 'div', array( 'id' => 'mw-credits' ), $this->getContributors() ); + }*/ + + return $content; + } + + /** + * Creates a header that can be added to the output. + * + * @param $header The header text. + * @return string The HTML. + */ + protected function makeHeader( $header ) { + global $wgParser; + $spanAttribs = array( 'class' => 'mw-headline', 'id' => $wgParser->guessSectionNameFromWikiText( $header ) ); + return Html::rawElement( 'h2', array(), Html::element( 'span', $spanAttribs, $header ) ); + } + + /** + * Adds a row to a table that will be added to the content. + * + * @param $table string The table that will be added to the content + * @param $name string The name of the row + * @param $value string The value of the row + * @return string The table with the row added + */ + protected function addRow( $table, $name, $value ) { + return $table . Html::rawElement( 'tr', array(), + Html::rawElement( 'td', array( 'style' => 'vertical-align: top;' ), $name ) . + Html::rawElement( 'td', array(), $value ) + ); + } + + /** + * Adds a table to the content that will be added to the output. + * + * @param $content string The content that will be added to the output + * @param $table string The table + * @return string The content with the table added + */ + protected function addTable( $content, $table ) { + return $content . Html::rawElement( 'table', array( 'class' => 'wikitable mw-page-info' ), + $table ); + } + + /** + * Returns page information in an easily-manipulated format. Array keys are used so extensions + * may add additional information in arbitrary positions. Array values are arrays with one + * element to be rendered as a header, arrays with two elements to be rendered as a table row. + * + * @return array + */ + protected function pageInfo() { + global $wgContLang, $wgRCMaxAge; $user = $this->getUser(); $lang = $this->getLanguage(); @@ -64,8 +173,7 @@ class InfoAction extends FormlessAction { $id = $title->getArticleID(); // Get page information that would be too "expensive" to retrieve by normal means - $userCanViewUnwatchedPages = $user->isAllowed( 'unwatchedpages' ); - $pageInfo = self::pageCountInfo( $title, $userCanViewUnwatchedPages, $wgDisableCounters ); + $pageCounts = self::pageCounts( $title, $user ); // Get page properties $dbr = wfGetDB( DB_SLAVE ); @@ -81,21 +189,9 @@ class InfoAction extends FormlessAction { $pageProperties[$row->pp_propname] = $row->pp_value; } - $content = ''; - $table = ''; - - // Header - if ( !$this->msg( 'pageinfo-header' )->isDisabled() ) { - $content .= $this->msg( 'pageinfo-header' )->parse(); - } - - // Credits - if ( $title->exists() ) { - $content .= Html::rawElement( 'div', array( 'id' => 'mw-credits' ), $this->getContributors() ); - } - // Basic information - $content .= $this->makeHeader( $this->msg( 'pageinfo-header-basic' )->plain() ); + $pageInfo = array(); + $pageInfo['header-basic'] = array(); // Display title $displayTitle = $title->getPrefixedText(); @@ -103,8 +199,24 @@ class InfoAction extends FormlessAction { $displayTitle = $pageProperties['displaytitle']; } - $table = $this->addRow( $table, - $this->msg( 'pageinfo-display-title' )->escaped(), $displayTitle ); + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-display-title' ), $displayTitle + ); + + // Is it a redirect? If so, where to? + if ( $title->isRedirect() ) { + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-redirectsto' ), + Linker::link( $this->page->getRedirectTarget() ) . + $this->msg( 'word-separator' )->text() . + $this->msg( 'parentheses', Linker::link( + $this->page->getRedirectTarget(), + $this->msg( 'pageinfo-redirectsto-info' )->escaped(), + array(), + array( 'action' => 'info' ) + ) )->text() + ); + } // Default sort key $sortKey = $title->getCategorySortKey(); @@ -112,16 +224,15 @@ class InfoAction extends FormlessAction { $sortKey = $pageProperties['defaultsort']; } - $table = $this->addRow( $table, - $this->msg( 'pageinfo-default-sort' )->escaped(), $sortKey ); + $pageInfo['header-basic'][] = array( $this->msg( 'pageinfo-default-sort' ), $sortKey ); // Page length (in bytes) - $table = $this->addRow( $table, - $this->msg( 'pageinfo-length' )->escaped(), $lang->formatNum( $title->getLength() ) ); + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-length' ), $lang->formatNum( $title->getLength() ) + ); - // Page ID (number not localised, as it's a database ID.) - $table = $this->addRow( $table, - $this->msg( 'pageinfo-article-id' )->escaped(), $id ); + // Page ID (number not localised, as it's a database ID) + $pageInfo['header-basic'][] = array( $this->msg( 'pageinfo-article-id' ), $id ); // Search engine status $pOutput = new ParserOutput(); @@ -131,27 +242,27 @@ class InfoAction extends FormlessAction { // Use robot policy logic $policy = $this->page->getRobotPolicy( 'view', $pOutput ); - $table = $this->addRow( $table, - $this->msg( 'pageinfo-robot-policy' )->escaped(), - $this->msg( "pageinfo-robot-${policy['index']}" )->escaped() + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-robot-policy' ), $this->msg( "pageinfo-robot-${policy['index']}" ) ); - if ( !$wgDisableCounters ) { + if ( isset( $pageCounts['views'] ) ) { // Number of views - $table = $this->addRow( $table, - $this->msg( 'pageinfo-views' )->escaped(), $lang->formatNum( $pageInfo['views'] ) + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-views' ), $lang->formatNum( $pageCounts['views'] ) ); } - if ( $userCanViewUnwatchedPages ) { + if ( isset( $pageCounts['watchers'] ) ) { // Number of page watchers - $table = $this->addRow( $table, - $this->msg( 'pageinfo-watchers' )->escaped(), $lang->formatNum( $pageInfo['watchers'] ) ); + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-watchers' ), $lang->formatNum( $pageCounts['watchers'] ) + ); } // Redirects to this page $whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() ); - $table = $this->addRow( $table, + $pageInfo['header-basic'][] = array( Linker::link( $whatLinksHere, $this->msg( 'pageinfo-redirects-name' )->escaped(), @@ -159,26 +270,56 @@ class InfoAction extends FormlessAction { array( 'hidelinks' => 1, 'hidetrans' => 1 ) ), $this->msg( 'pageinfo-redirects-value' ) - ->numParams( count( $title->getRedirectsHere() ) )->escaped() + ->numParams( count( $title->getRedirectsHere() ) ) ); + // Is it counted as a content page? + if ( $this->page->isCountable() ) { + $pageInfo['header-basic'][] = array( + $this->msg( 'pageinfo-contentpage' ), + $this->msg( 'pageinfo-contentpage-yes' ) + ); + } + // Subpages of this page, if subpages are enabled for the current NS if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) { $prefixIndex = SpecialPage::getTitleFor( 'Prefixindex', $title->getPrefixedText() . '/' ); - $table = $this->addRow( $table, + $pageInfo['header-basic'][] = array( Linker::link( $prefixIndex, $this->msg( 'pageinfo-subpages-name' )->escaped() ), $this->msg( 'pageinfo-subpages-value' ) ->numParams( - $pageInfo['subpages']['total'], - $pageInfo['subpages']['redirects'], - $pageInfo['subpages']['nonredirects'] )->escaped() + $pageCounts['subpages']['total'], + $pageCounts['subpages']['redirects'], + $pageCounts['subpages']['nonredirects'] ) ); } // Page protection - $content = $this->addTable( $content, $table ); - $content .= $this->makeHeader( $this->msg( 'pageinfo-header-restrictions' )->plain() ); - $table = ''; + $pageInfo['header-restrictions'] = array(); + + // Is this page effected by the cascading protection of something which includes it? + if ( $title->isCascadeProtected() ) { + $cascadingFrom = ''; + $sources = $title->getCascadeProtectionSources(); // Array deferencing is in PHP 5.4 :( + + foreach ( $sources[0] as $sourceTitle ) { + $cascadingFrom .= Html::rawElement( 'li', array(), Linker::linkKnown( $sourceTitle ) ); + } + + $cascadingFrom = Html::rawElement( 'ul', array(), $cascadingFrom ); + $pageInfo['header-restrictions'][] = array( + $this->msg( 'pageinfo-protect-cascading-from' ), + $cascadingFrom + ); + } + + // Is out protection set to cascade to other pages? + if ( $title->areRestrictionsCascading() ) { + $pageInfo['header-restrictions'][] = array( + $this->msg( 'pageinfo-protect-cascading' ), + $this->msg( 'pageinfo-protect-cascading-yes' ) + ); + } // Page protection foreach ( $title->getRestrictionTypes() as $restrictionType ) { @@ -198,28 +339,29 @@ class InfoAction extends FormlessAction { } } - $table = $this->addRow( $table, - $this->msg( "restriction-$restrictionType" )->plain(), - $message + $pageInfo['header-restrictions'][] = array( + $this->msg( "restriction-$restrictionType" ), $message ); } + if ( !$this->page->exists() ) { + return $pageInfo; + } + // Edit history - $content = $this->addTable( $content, $table ); - $content .= $this->makeHeader( $this->msg( 'pageinfo-header-edits' )->plain() ); - $table = ''; + $pageInfo['header-edits'] = array(); $firstRev = $this->page->getOldestRevision(); // Page creator - $table = $this->addRow( $table, - $this->msg( 'pageinfo-firstuser' )->escaped(), - Linker::userLink( $firstRev->getUser( Revision::FOR_THIS_USER, $user ), $firstRev->getUserText( Revision::FOR_THIS_USER, $user ) ) + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-firstuser' ), + Linker::revUserTools( $firstRev ) ); // Date of page creation - $table = $this->addRow( $table, - $this->msg( 'pageinfo-firsttime' )->escaped(), + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-firsttime' ), Linker::linkKnown( $title, $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ), @@ -229,14 +371,14 @@ class InfoAction extends FormlessAction { ); // Latest editor - $table = $this->addRow( $table, - $this->msg( 'pageinfo-lastuser' )->escaped(), - Linker::userLink( $this->page->getUser( Revision::FOR_THIS_USER, $user ), $this->page->getUserText( Revision::FOR_THIS_USER, $user ) ) + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-lastuser' ), + Linker::revUserTools( $this->page->getRevision() ) ); // Date of latest edit - $table = $this->addRow( $table, - $this->msg( 'pageinfo-lasttime' )->escaped(), + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-lasttime' ), Linker::linkKnown( $title, $lang->userTimeAndDate( $this->page->getTimestamp(), $user ), @@ -246,28 +388,26 @@ class InfoAction extends FormlessAction { ); // Total number of edits - $table = $this->addRow( $table, - $this->msg( 'pageinfo-edits' )->escaped(), $lang->formatNum( $pageInfo['edits'] ) + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-edits' ), $lang->formatNum( $pageCounts['edits'] ) ); // Total number of distinct authors - $table = $this->addRow( $table, - $this->msg( 'pageinfo-authors' )->escaped(), $lang->formatNum( $pageInfo['authors'] ) + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-authors' ), $lang->formatNum( $pageCounts['authors'] ) ); // Recent number of edits (within past 30 days) - $table = $this->addRow( $table, - $this->msg( 'pageinfo-recent-edits', $lang->formatDuration( $wgRCMaxAge ) )->escaped(), - $lang->formatNum( $pageInfo['recent_edits'] ) + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-recent-edits', $lang->formatDuration( $wgRCMaxAge ) ), + $lang->formatNum( $pageCounts['recent_edits'] ) ); // Recent number of distinct authors - $table = $this->addRow( $table, - $this->msg( 'pageinfo-recent-authors' )->escaped(), $lang->formatNum( $pageInfo['recent_authors'] ) + $pageInfo['header-edits'][] = array( + $this->msg( 'pageinfo-recent-authors' ), $lang->formatNum( $pageCounts['recent_authors'] ) ); - $content = $this->addTable( $content, $table ); - // Array of MagicWord objects $magicWords = MagicWord::getDoubleUnderscoreArray(); @@ -292,76 +432,47 @@ class InfoAction extends FormlessAction { || count( $hiddenCategories ) > 0 || count( $transcludedTemplates ) > 0 ) { // Page properties - $content .= $this->makeHeader( $this->msg( 'pageinfo-header-properties' )->plain() ); - $table = ''; + $pageInfo['header-properties'] = array(); // Magic words if ( count( $listItems ) > 0 ) { - $table = $this->addRow( $table, - $this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) )->escaped(), + $pageInfo['header-properties'][] = array( + $this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) ), $localizedList ); } - // Hide "This page is a member of # hidden categories explanation - $content .= Html::element( 'style', array(), - '.mw-hiddenCategoriesExplanation { display: none; }' ); - // Hidden categories if ( count( $hiddenCategories ) > 0 ) { - $table = $this->addRow( $table, + $pageInfo['header-properties'][] = array( $this->msg( 'pageinfo-hidden-categories' ) - ->numParams( count( $hiddenCategories ) )->escaped(), + ->numParams( count( $hiddenCategories ) ), Linker::formatHiddenCategories( $hiddenCategories ) ); } - // Hide "Templates used on this page:" explanation - $content .= Html::element( 'style', array(), - '.mw-templatesUsedExplanation { display: none; }' ); - // Transcluded templates if ( count( $transcludedTemplates ) > 0 ) { - $table = $this->addRow( $table, + $pageInfo['header-properties'][] = array( $this->msg( 'pageinfo-templates' ) - ->numParams( count( $transcludedTemplates ) )->escaped(), + ->numParams( count( $transcludedTemplates ) ), Linker::formatTemplates( $transcludedTemplates ) ); } - - $content = $this->addTable( $content, $table ); - } - - // Footer - if ( !$this->msg( 'pageinfo-footer' )->isDisabled() ) { - $content .= $this->msg( 'pageinfo-footer' )->parse(); } - return $content; + return $pageInfo; } /** - * Creates a header that can be added to the output. - * - * @param $header The header text. - * @return string The HTML. - */ - public static function makeHeader( $header ) { - global $wgParser; - $spanAttribs = array( 'class' => 'mw-headline', 'id' => $wgParser->guessSectionNameFromWikiText( $header ) ); - return Html::rawElement( 'h2', array(), Html::element( 'span', $spanAttribs, $header ) ); - } - - /** - * Returns page information that would be too "expensive" to retrieve by normal means. + * Returns page counts that would be too "expensive" to retrieve by normal means. * * @param $title Title object - * @param $canViewUnwatched bool - * @param $disableCounter bool + * @param $user User object * @return array */ - public static function pageCountInfo( $title, $canViewUnwatched, $disableCounter ) { - global $wgRCMaxAge; + protected static function pageCounts( $title, $user ) { + global $wgRCMaxAge, $wgDisableCounters; wfProfileIn( __METHOD__ ); $id = $title->getArticleID(); @@ -369,7 +480,7 @@ class InfoAction extends FormlessAction { $dbr = wfGetDB( DB_SLAVE ); $result = array(); - if ( !$disableCounter ) { + if ( !$wgDisableCounters ) { // Number of views $views = (int) $dbr->selectField( 'page', @@ -380,7 +491,7 @@ class InfoAction extends FormlessAction { $result['views'] = $views; } - if ( $canViewUnwatched ) { + if ( $user->isAllowed( 'unwatchedpages' ) ) { // Number of page watchers $watchers = (int) $dbr->selectField( 'watchlist', @@ -470,42 +581,6 @@ class InfoAction extends FormlessAction { return $result; } - /** - * Adds a row to a table that will be added to the content. - * - * @param $table string The table that will be added to the content - * @param $name string The name of the row - * @param $value string The value of the row - * @return string The table with the row added - */ - protected function addRow( $table, $name, $value ) { - return $table . Html::rawElement( 'tr', array(), - Html::rawElement( 'td', array( 'style' => 'vertical-align: top;' ), $name ) . - Html::rawElement( 'td', array(), $value ) - ); - } - - /** - * Adds a table to the content that will be added to the output. - * - * @param $content string The content that will be added to the output - * @param $table string The table - * @return string The content with the table added - */ - protected function addTable( $content, $table ) { - return $content . Html::rawElement( 'table', array( 'class' => 'wikitable mw-page-info' ), - $table ); - } - - /** - * Returns the description that goes below the

tag. - * - * @return string - */ - protected function getDescription() { - return ''; - } - /** * Returns the name that goes in the

page title. * @@ -576,4 +651,13 @@ class InfoAction extends FormlessAction { $lang->listToText( $fulllist ) )->params( $count )->escaped() : ''; } + + /** + * Returns the description that goes below the

tag. + * + * @return string + */ + protected function getDescription() { + return ''; + } } diff --git a/includes/actions/RawAction.php b/includes/actions/RawAction.php index 174ca3f86c..71cb397dd7 100644 --- a/includes/actions/RawAction.php +++ b/includes/actions/RawAction.php @@ -46,7 +46,7 @@ class RawAction extends FormlessAction { } function onView() { - global $wgGroupPermissions, $wgSquidMaxage, $wgForcedRawSMaxage, $wgJsMimeType; + global $wgSquidMaxage, $wgForcedRawSMaxage, $wgJsMimeType; $this->getOutput()->disable(); $request = $this->getRequest(); @@ -91,7 +91,7 @@ class RawAction extends FormlessAction { $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' ); # Output may contain user-specific data; # vary generated content for open sessions on private wikis - $privateCache = !$wgGroupPermissions['*']['read'] && ( $smaxage == 0 || session_id() != '' ); + $privateCache = !User::groupHasPermission( '*', 'read' ) && ( $smaxage == 0 || session_id() != '' ); # allow the client to cache this for 24 hours $mode = $privateCache ? 'private' : 'public'; $response->header( 'Cache-Control: ' . $mode . ', s-maxage=' . $smaxage . ', max-age=' . $maxage ); @@ -148,10 +148,29 @@ class RawAction extends FormlessAction { $request->response()->header( "Last-modified: $lastmod" ); // Public-only due to cache headers - $text = $rev->getText(); - $section = $request->getIntOrNull( 'section' ); - if ( $section !== null ) { - $text = $wgParser->getSection( $text, $section ); + $content = $rev->getContent(); + + if ( $content === null ) { + // revision not found (or suppressed) + $text = false; + } elseif ( !$content instanceof TextContent ) { + // non-text content + wfHttpError( 415, "Unsupported Media Type", "The requested page uses the content model `" + . $content->getModel() . "` which is not supported via this interface." ); + die(); + } else { + // want a section? + $section = $request->getIntOrNull( 'section' ); + if ( $section !== null ) { + $content = $content->getSection( $section ); + } + + if ( $content === null || $content === false ) { + // section not found (or section not supported, e.g. for JS and CSS) + $text = false; + } else { + $text = $content->getNativeData(); + } } } } diff --git a/includes/actions/RevertAction.php b/includes/actions/RevertAction.php index 774343842a..a5fc4e1734 100644 --- a/includes/actions/RevertAction.php +++ b/includes/actions/RevertAction.php @@ -115,7 +115,7 @@ class RevertFileAction extends FormAction { $source = $this->page->getFile()->getArchiveVirtualUrl( $this->getRequest()->getText( 'oldimage' ) ); $comment = $data['comment']; // TODO: Preserve file properties from database instead of reloading from file - return $this->page->getFile()->upload( $source, $comment, $comment ); + return $this->page->getFile()->upload( $source, $comment, $comment, 0, false, false, $this->getUser() ); } public function onSuccess() { @@ -124,7 +124,7 @@ class RevertFileAction extends FormAction { $lang = $this->getLanguage(); $userDate = $lang->userDate( $timestamp, $user ); $userTime = $lang->userTime( $timestamp, $user ); - + $this->getOutput()->addWikiMsg( 'filerevert-success', $this->getTitle()->getText(), $userDate, $userTime, wfExpandUrl( $this->page->getFile()->getArchiveUrl( $this->getRequest()->getText( 'oldimage' ) ), @@ -136,7 +136,7 @@ class RevertFileAction extends FormAction { protected function getPageTitle() { return $this->msg( 'filerevert', $this->getTitle()->getText() ); } - + protected function getDescription() { $this->getOutput()->addBacklinkSubtitle( $this->getTitle() ); return ''; diff --git a/includes/actions/RollbackAction.php b/includes/actions/RollbackAction.php index 0d9a902727..81bad9da0b 100644 --- a/includes/actions/RollbackAction.php +++ b/includes/actions/RollbackAction.php @@ -71,45 +71,32 @@ class RollbackAction extends FormlessAction { return; } - # Display permissions errors before read-only message -- there's no - # point in misleading the user into thinking the inability to rollback - # is only temporary. - if ( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) { - # array_diff is completely broken for arrays of arrays, sigh. - # Remove any 'readonlytext' error manually. - $out = array(); - foreach ( $result as $error ) { - if ( $error != array( 'readonlytext' ) ) { - $out [] = $error; - } - } - throw new PermissionsError( 'rollback', $out ); - } + #NOTE: Permission errors already handled by Action::checkExecute. if ( $result == array( array( 'readonlytext' ) ) ) { throw new ReadOnlyError; } + #XXX: Would be nice if ErrorPageError could take multiple errors, and/or a status object. + # Right now, we only show the first error + foreach ( $result as $error ) { + throw new ErrorPageError( 'rollbackfailed', $error[0], array_slice( $error, 1 ) ); + } + $current = $details['current']; $target = $details['target']; $newId = $details['newid']; $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) ); $this->getOutput()->setRobotPolicy( 'noindex,nofollow' ); - if ( $current->getUserText() === '' ) { - $old = $this->msg( 'rev-deleted-user' )->escaped(); - } else { - $old = Linker::userLink( $current->getUser(), $current->getUserText() ) - . Linker::userToolLinks( $current->getUser(), $current->getUserText() ); - } - - $new = Linker::userLink( $target->getUser(), $target->getUserText() ) - . Linker::userToolLinks( $target->getUser(), $target->getUserText() ); + $old = Linker::revUserTools( $current ); + $new = Linker::revUserTools( $target ); $this->getOutput()->addHTML( $this->msg( 'rollback-success' )->rawParams( $old, $new )->parseAsBlock() ); $this->getOutput()->returnToMain( false, $this->getTitle() ); if ( !$request->getBool( 'hidediff', false ) && !$this->getUser()->getBoolOption( 'norollbackdiff', false ) ) { - $de = new DifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true ); + $contentHandler = $current->getContentHandler(); + $de = $contentHandler->createDifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true ); $de->showDiff( '', '' ); } } diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index ed72b29bd7..67412598a8 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -35,7 +35,15 @@ class ApiComparePages extends ApiBase { $rev1 = $this->revisionOrTitleOrId( $params['fromrev'], $params['fromtitle'], $params['fromid'] ); $rev2 = $this->revisionOrTitleOrId( $params['torev'], $params['totitle'], $params['toid'] ); - $de = new DifferenceEngine( $this->getContext(), + $revision = Revision::newFromId( $rev1 ); + + if ( !$revision ) { + $this->dieUsage( 'The diff cannot be retrieved, ' . + 'one revision does not exist or you do not have permission to view it.', 'baddiff' ); + } + + $contentHandler = $revision->getContentHandler(); + $de = $contentHandler->createDifferenceEngine( $this->getContext(), $rev1, $rev2, null, // rcid diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index 2d36f19aa3..964e0ae40b 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -61,6 +61,9 @@ class ApiDelete extends ApiBase { $status = self::delete( $pageObj, $user, $params['token'], $reason ); } + if ( is_array( $status ) ) { + $this->dieUsageMsg( $status[0] ); + } if ( !$status->isGood() ) { $errors = $status->getErrorsArray(); $this->dieUsageMsg( $errors[0] ); // We don't care about multiple errors, just report one of them @@ -98,11 +101,11 @@ class ApiDelete extends ApiBase { /** * We have our own delete() function, since Article.php's implementation is split in two phases * - * @param $page WikiPage object to work on + * @param $page Page|WikiPage object to work on * @param $user User doing the action - * @param $token String: delete token (same as edit token) - * @param $reason String: reason for the deletion. Autogenerated if NULL - * @return Status + * @param $token String delete token (same as edit token) + * @param $reason String|null reason for the deletion. Autogenerated if NULL + * @return Status|array */ public static function delete( Page $page, User $user, $token, &$reason = null ) { $title = $page->getTitle(); @@ -128,13 +131,13 @@ class ApiDelete extends ApiBase { } /** - * @param $page WikiPage object to work on + * @param $page WikiPage|Page object to work on * @param $user User doing the action * @param $token * @param $oldimage * @param $reason * @param $suppress bool - * @return Status + * @return Status|array */ public static function deleteFile( Page $page, User $user, $token, $oldimage, &$reason = null, $suppress = false ) { $title = $page->getTitle(); @@ -161,7 +164,7 @@ class ApiDelete extends ApiBase { if ( is_null( $reason ) ) { // Log and RC don't like null reasons $reason = ''; } - return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress ); + return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress, $user ); } public function mustBePosted() { diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index 0963fe7cf2..ca62bc6930 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -54,17 +54,37 @@ class ApiEditPage extends ApiBase { $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); } + if ( !isset( $params['contentmodel'] ) || $params['contentmodel'] == '' ) { + $contentHandler = $pageObj->getContentHandler(); + } else { + $contentHandler = ContentHandler::getForModelID( $params['contentmodel'] ); + } + + // @todo ask handler whether direct editing is supported at all! make allowFlatEdit() method or some such + + if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) { + $params['contentformat'] = $contentHandler->getDefaultFormat(); + } + + $contentFormat = $params['contentformat']; + + if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) { + $name = $titleObj->getPrefixedDBkey(); + $model = $contentHandler->getModelID(); + + $this->dieUsage( "The requested format $contentFormat is not supported for content model ". + " $model used by $name", 'badformat' ); + } + $apiResult = $this->getResult(); if ( $params['redirect'] ) { if ( $titleObj->isRedirect() ) { $oldTitle = $titleObj; - $titles = Title::newFromRedirectArray( - Revision::newFromTitle( - $oldTitle, false, Revision::READ_LATEST - )->getText( Revision::FOR_THIS_USER ) - ); + $titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST ) + ->getContent( Revision::FOR_THIS_USER, $user ) + ->getRedirectChain(); // array_shift( $titles ); $redirValues = array(); @@ -103,31 +123,61 @@ class ApiEditPage extends ApiBase { $this->dieUsageMsg( $errors[0] ); } - $articleObj = Article::newFromTitle( $titleObj, $this->getContext() ); - $toMD5 = $params['text']; if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) ) { - // For non-existent pages, Article::getContent() - // returns an interface message rather than '' - // We do want getContent()'s behavior for non-existent - // MediaWiki: pages, though - if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI ) { - $content = ''; - } else { - $content = $articleObj->getContent(); + $content = $pageObj->getContent(); + + if ( !$content ) { + if ( $titleObj->getNamespace() == NS_MEDIAWIKI ) { + # If this is a MediaWiki:x message, then load the messages + # and return the message value for x. + $text = $titleObj->getDefaultMessageText(); + if ( $text === false ) { + $text = ''; + } + + try { + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + } catch ( MWContentSerializationException $ex ) { + $this->dieUsage( $ex->getMessage(), 'parseerror' ); + return; + } + } else { + # Otherwise, make a new empty content. + $content = $contentHandler->makeEmptyContent(); + } + } + + // @todo: Add support for appending/prepending to the Content interface + + if ( !( $content instanceof TextContent ) ) { + $mode = $contentHandler->getModelID(); + $this->dieUsage( "Can't append to pages using content model $mode", 'appendnotsupported' ); } if ( !is_null( $params['section'] ) ) { + if ( !$contentHandler->supportsSections() ) { + $modelName = $contentHandler->getModelID(); + $this->dieUsage( "Sections are not supported for this content model: $modelName.", 'sectionsnotsupported' ); + } + // Process the content for section edits - global $wgParser; $section = intval( $params['section'] ); - $content = $wgParser->getSection( $content, $section, false ); - if ( $content === false ) { + $content = $content->getSection( $section ); + + if ( !$content ) { $this->dieUsage( "There is no section {$section}.", 'nosuchsection' ); } } - $params['text'] = $params['prependtext'] . $content . $params['appendtext']; + + if ( !$content ) { + $text = ''; + } else { + $text = $content->serialize( $contentFormat ); + } + + $params['text'] = $params['prependtext'] . $text . $params['appendtext']; $toMD5 = $params['prependtext'] . $params['appendtext']; } @@ -151,18 +201,21 @@ class ApiEditPage extends ApiBase { $this->dieUsageMsg( array( 'nosuchrevid', $params['undoafter'] ) ); } - if ( $undoRev->getPage() != $articleObj->getID() ) { + if ( $undoRev->getPage() != $pageObj->getID() ) { $this->dieUsageMsg( array( 'revwrongpage', $undoRev->getID(), $titleObj->getPrefixedText() ) ); } - if ( $undoafterRev->getPage() != $articleObj->getID() ) { + if ( $undoafterRev->getPage() != $pageObj->getID() ) { $this->dieUsageMsg( array( 'revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText() ) ); } - $newtext = $articleObj->getUndoText( $undoRev, $undoafterRev ); - if ( $newtext === false ) { + $newContent = $contentHandler->getUndoContent( $pageObj->getRevision(), $undoRev, $undoafterRev ); + + if ( !$newContent ) { $this->dieUsageMsg( 'undo-failure' ); } - $params['text'] = $newtext; + + $params['text'] = $newContent->serialize( $params['contentformat'] ); + // If no summary was given and we only undid one rev, // use an autosummary if ( is_null( $params['summary'] ) && $titleObj->getNextRevisionID( $undoafterRev->getID() ) == $params['undo'] ) { @@ -179,6 +232,8 @@ class ApiEditPage extends ApiBase { // That interface kind of sucks, but it's workable $requestArray = array( 'wpTextbox1' => $params['text'], + 'format' => $contentFormat, + 'model' => $contentHandler->getModelID(), 'wpEditToken' => $params['token'], 'wpIgnoreBlankSummary' => '' ); @@ -196,7 +251,7 @@ class ApiEditPage extends ApiBase { if ( !is_null( $params['basetimestamp'] ) && $params['basetimestamp'] != '' ) { $requestArray['wpEdittime'] = wfTimestamp( TS_MW, $params['basetimestamp'] ); } else { - $requestArray['wpEdittime'] = $articleObj->getTimestamp(); + $requestArray['wpEdittime'] = $pageObj->getTimestamp(); } if ( !is_null( $params['starttimestamp'] ) && $params['starttimestamp'] != '' ) { @@ -244,7 +299,12 @@ class ApiEditPage extends ApiBase { // TODO: Make them not or check if they still do $wgTitle = $titleObj; - $ep = new EditPage( $articleObj ); + $articleObject = new Article( $titleObj ); + $ep = new EditPage( $articleObject ); + + // allow editing of non-textual content. + $ep->allowNonTextContent = true; + $ep->setContextTitle( $titleObj ); $ep->importFormData( $req ); @@ -262,7 +322,7 @@ class ApiEditPage extends ApiBase { } // Do the actual save - $oldRevId = $articleObj->getRevIdFetched(); + $oldRevId = $articleObject->getRevIdFetched(); $result = null; // Fake $wgRequest for some hooks inside EditPage // @todo FIXME: This interface SUCKS @@ -278,6 +338,9 @@ class ApiEditPage extends ApiBase { case EditPage::AS_HOOK_ERROR_EXPECTED: $this->dieUsageMsg( 'hookaborted' ); + case EditPage::AS_PARSE_ERROR: + $this->dieUsage( $status->getMessage(), 'parseerror' ); + case EditPage::AS_IMAGE_REDIRECT_ANON: $this->dieUsageMsg( 'noimageredirect-anon' ); @@ -329,14 +392,15 @@ class ApiEditPage extends ApiBase { $r['result'] = 'Success'; $r['pageid'] = intval( $titleObj->getArticleID() ); $r['title'] = $titleObj->getPrefixedText(); - $newRevId = $articleObj->getLatest(); + $r['contentmodel'] = $titleObj->getContentModel(); + $newRevId = $articleObject->getLatest(); if ( $newRevId == $oldRevId ) { $r['nochange'] = ''; } else { $r['oldrevid'] = intval( $oldRevId ); $r['newrevid'] = intval( $newRevId ); $r['newtimestamp'] = wfTimestamp( TS_ISO_8601, - $articleObj->getTimestamp() ); + $pageObj->getTimestamp() ); } break; @@ -380,6 +444,7 @@ class ApiEditPage extends ApiBase { array( 'undo-failure' ), array( 'hashcheckfailed' ), array( 'hookaborted' ), + array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ), array( 'noimageredirect-anon' ), array( 'noimageredirect-logged' ), array( 'spamdetected', 'spam' ), @@ -397,6 +462,13 @@ class ApiEditPage extends ApiBase { array( 'unknownerror', 'retval' ), array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ), array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ), + array( 'code' => 'sectionsnotsupported', 'info' => 'Sections are not supported for this type of page.' ), + array( 'code' => 'editnotsupported', 'info' => 'Editing of this type of page is not supported using ' + . 'the text based edit API.' ), + array( 'code' => 'appendnotsupported', 'info' => 'This type of page can not be edited by appending ' + . 'or prepending text.' ), + array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied to ' + . 'the page\'s content model' ), array( 'customcssprotected' ), array( 'customjsprotected' ), ) @@ -460,6 +532,12 @@ class ApiEditPage extends ApiBase { ApiBase::PARAM_TYPE => 'boolean', ApiBase::PARAM_DFLT => false, ), + 'contentformat' => array( + ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), + ), + 'contentmodel' => array( + ApiBase::PARAM_TYPE => ContentHandler::getContentModels(), + ) ); } @@ -498,6 +576,8 @@ class ApiEditPage extends ApiBase { 'undo' => "Undo this revision. Overrides {$p}text, {$p}prependtext and {$p}appendtext", 'undoafter' => 'Undo all revisions from undo to this one. If not set, just undo one revision', 'redirect' => 'Automatically resolve redirects', + 'contentformat' => 'Content serialization format used for the input text', + 'contentmodel' => 'Content model of the new content', ); } diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 1cf760aea9..fb6a06ffcb 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -130,10 +130,22 @@ class ApiFeedContributions extends ApiBase { protected function feedItemDesc( $revision ) { if( $revision ) { $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text(); + $content = $revision->getContent(); + + if ( $content instanceof TextContent ) { + // only textual content has a "source view". + $html = nl2br( htmlspecialchars( $content->getNativeData() ) ); + } else { + //XXX: we could get an HTML representation of the content via getParserOutput, but that may + // contain JS magic and generally may not be suitable for inclusion in a feed. + // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. + //Compare also FeedUtils::formatDiffRow. + $html = ''; + } + return '

' . htmlspecialchars( $revision->getUserText() ) . $msg . htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) . - "

\n
\n
" . - nl2br( htmlspecialchars( $revision->getText() ) ) . "
"; + "

\n
\n
" . $html . "
"; } return ''; } diff --git a/includes/api/ApiFileRevert.php b/includes/api/ApiFileRevert.php index 83d078d2af..092b0036f2 100644 --- a/includes/api/ApiFileRevert.php +++ b/includes/api/ApiFileRevert.php @@ -50,7 +50,7 @@ class ApiFileRevert extends ApiBase { $this->checkPermissions( $this->getUser() ); $sourceUrl = $this->file->getArchiveVirtualUrl( $this->archiveName ); - $status = $this->file->upload( $sourceUrl, $this->params['comment'], $this->params['comment'] ); + $status = $this->file->upload( $sourceUrl, $this->params['comment'], $this->params['comment'], 0, false, false, $this->getUser() ); if ( $status->isGood() ) { $result = array( 'result' => 'Success' ); diff --git a/includes/api/ApiFormatNone.php b/includes/api/ApiFormatNone.php new file mode 100644 index 0000000000..31c90e101a --- /dev/null +++ b/includes/api/ApiFormatNone.php @@ -0,0 +1,51 @@ +@gmail.com + * + * 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. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * API Serialized PHP output formatter + * @ingroup API + */ +class ApiFormatNone extends ApiFormatBase { + + public function __construct( $main, $format ) { + parent::__construct( $main, $format ); + } + + public function getMimeType() { + return 'text/plain'; + } + + public function execute() { + } + + public function getDescription() { + return 'Output nothing' . parent::getDescription(); + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 87a287b31a..3aa51b7967 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -105,6 +105,7 @@ class ApiMain extends ApiBase { 'dbgfm' => 'ApiFormatDbg', 'dump' => 'ApiFormatDump', 'dumpfm' => 'ApiFormatDump', + 'none' => 'ApiFormatNone', ); /** @@ -839,7 +840,7 @@ class ApiMain extends ApiBase { protected function logRequest( $time ) { $request = $this->getRequest(); $milliseconds = $time === null ? '?' : round( $time * 1000 ); - $s = 'API' . + $s = 'API' . ' ' . $request->getMethod() . ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . ' ' . $request->getIP() . @@ -896,7 +897,7 @@ class ApiMain extends ApiBase { */ public function getCheck( $name ) { $this->mParamsUsed[$name] = true; - return $this->getRequest()->getCheck( $name ); + return $this->getRequest()->getCheck( $name ); } /** diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 2fcdc38824..1a55d5a16b 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -26,7 +26,15 @@ * @ingroup API */ class ApiParse extends ApiBase { - private $section, $text, $pstText = null; + + /** @var String $section */ + private $section = null; + + /** @var Content $content */ + private $content = null; + + /** @var Content $pstContent */ + private $pstContent = null; public function __construct( $main, $action ) { parent::__construct( $main, $action ); @@ -44,6 +52,9 @@ class ApiParse extends ApiBase { $pageid = $params['pageid']; $oldid = $params['oldid']; + $model = $params['contentmodel']; + $format = $params['contentformat']; + if ( !is_null( $page ) && ( !is_null( $text ) || $title != 'API' ) ) { $this->dieUsage( 'The page parameter cannot be used together with the text and title parameters', 'params' ); } @@ -93,17 +104,17 @@ class ApiParse extends ApiBase { // If for some reason the "oldid" is actually the current revision, it may be cached if ( $rev->isCurrent() ) { // May get from/save to parser cache - $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid, - isset( $prop['wikitext'] ) ) ; + $p_result = $this->getParsedContent( $pageObj, $popts, + $pageid, isset( $prop['wikitext'] ) ) ; } else { // This is an old revision, so get the text differently - $this->text = $rev->getText( Revision::FOR_THIS_USER, $this->getUser() ); + $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); if ( $this->section !== false ) { - $this->text = $this->getSectionText( $this->text, 'r' . $rev->getId() ); + $this->content = $this->getSectionContent( $this->content, 'r' . $rev->getId() ); } // Should we save old revision parses to the parser cache? - $p_result = $wgParser->parse( $this->text, $titleObj, $popts ); + $p_result = $this->content->getParserOutput( $titleObj, $rev->getId(), $popts ); } } else { // Not $oldid, but $pageid or $page if ( $params['redirects'] ) { @@ -146,15 +157,10 @@ class ApiParse extends ApiBase { $popts->enableLimitReport( !$params['disablepp'] ); // Potentially cached - $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid, - isset( $prop['wikitext'] ) ) ; + $p_result = $this->getParsedContent( $pageObj, $popts, $pageid, + isset( $prop['wikitext'] ) ) ; } } else { // Not $oldid, $pageid, $page. Hence based on $text - - if ( is_null( $text ) ) { - $this->dieUsage( 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params' ); - } - $this->text = $text; $titleObj = Title::newFromText( $title ); if ( !$titleObj ) { $this->dieUsageMsg( array( 'invalidtitle', $title ) ); @@ -165,27 +171,42 @@ class ApiParse extends ApiBase { $popts = $pageObj->makeParserOptions( $this->getContext() ); $popts->enableLimitReport( !$params['disablepp'] ); + if ( is_null( $text ) ) { + $this->dieUsage( 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params' ); + } + + try { + $this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format ); + } catch ( MWContentSerializationException $ex ) { + $this->dieUsage( $ex->getMessage(), 'parseerror' ); + } + if ( $this->section !== false ) { - $this->text = $this->getSectionText( $this->text, $titleObj->getText() ); + $this->content = $this->getSectionContent( $this->content, $titleObj->getText() ); } if ( $params['pst'] || $params['onlypst'] ) { - $this->pstText = $wgParser->preSaveTransform( $this->text, $titleObj, $this->getUser(), $popts ); + $this->pstContent = $this->content->preSaveTransform( $titleObj, $this->getUser(), $popts ); } if ( $params['onlypst'] ) { // Build a result and bail out $result_array = array(); $result_array['text'] = array(); - $result->setContent( $result_array['text'], $this->pstText ); + $result->setContent( $result_array['text'], $this->pstContent->serialize( $format ) ); if ( isset( $prop['wikitext'] ) ) { $result_array['wikitext'] = array(); - $result->setContent( $result_array['wikitext'], $this->text ); + $result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) ); } $result->addValue( null, $this->getModuleName(), $result_array ); return; } + // Not cached (save or load) - $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts ); + if ( $params['pst'] ) { + $p_result = $this->pstContent->getParserOutput( $titleObj, null, $popts ); + } else { + $p_result = $this->content->getParserOutput( $titleObj, null, $popts ); + } } $result_array = array(); @@ -275,10 +296,10 @@ class ApiParse extends ApiBase { if ( isset( $prop['wikitext'] ) ) { $result_array['wikitext'] = array(); - $result->setContent( $result_array['wikitext'], $this->text ); - if ( !is_null( $this->pstText ) ) { + $result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) ); + if ( !is_null( $this->pstContent ) ) { $result_array['psttext'] = array(); - $result->setContent( $result_array['psttext'], $this->pstText ); + $result->setContent( $result_array['psttext'], $this->pstContent->serialize( $format ) ); } } if ( isset( $prop['properties'] ) ) { @@ -286,8 +307,12 @@ class ApiParse extends ApiBase { } if ( $params['generatexml'] ) { + if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) { + $this->dieUsage( "generatexml is only supported for wikitext content", "notwikitext" ); + } + $wgParser->startExternalParse( $titleObj, $popts, OT_PREPROCESS ); - $dom = $wgParser->preprocessToDom( $this->text ); + $dom = $wgParser->preprocessToDom( $this->content->getNativeData() ); if ( is_callable( array( $dom, 'saveXML' ) ) ) { $xml = $dom->saveXML(); } else { @@ -325,15 +350,16 @@ class ApiParse extends ApiBase { * @param $getWikitext Bool * @return ParserOutput */ - private function getParsedSectionOrText( $page, $popts, $pageId = null, $getWikitext = false ) { - global $wgParser; + private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) { + $this->content = $page->getContent( Revision::RAW ); //XXX: really raw? if ( $this->section !== false ) { - $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId ) - ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText() ); + $this->content = $this->getSectionContent( + $this->content, + !is_null( $pageId ) ? 'page id ' . $pageId : $page->getTitle()->getText() ); // Not cached (save or load) - return $wgParser->parse( $this->text, $page->getTitle(), $popts ); + return $this->content->getParserOutput( $page->getTitle(), null, $popts ); } else { // Try the parser cache first // getParserOutput will save to Parser cache if able @@ -342,20 +368,23 @@ class ApiParse extends ApiBase { $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' ); } if ( $getWikitext ) { - $this->text = $page->getRawText(); + $this->content = $page->getContent( Revision::RAW ); } return $pout; } } - private function getSectionText( $text, $what ) { - global $wgParser; + private function getSectionContent( Content $content, $what ) { // Not cached (save or load) - $text = $wgParser->getSection( $text, $this->section, false ); - if ( $text === false ) { + $section = $content->getSection( $this->section ); + if ( $section === false ) { $this->dieUsage( "There is no section {$this->section} in " . $what, 'nosuchsection' ); } - return $text; + if ( $section === null ) { + $this->dieUsage( "Sections are not supported by " . $what, 'nosuchsection' ); + $section = false; + } + return $section; } private function formatLangLinks( $links ) { @@ -548,6 +577,12 @@ class ApiParse extends ApiBase { 'section' => null, 'disablepp' => false, 'generatexml' => false, + 'contentformat' => array( + ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), + ), + 'contentmodel' => array( + ApiBase::PARAM_TYPE => ContentHandler::getContentModels(), + ) ); } @@ -593,6 +628,8 @@ class ApiParse extends ApiBase { 'section' => 'Only retrieve the content of this section number', 'disablepp' => 'Disable the PP Report from the parser output', 'generatexml' => 'Generate XML parse tree', + 'contentformat' => 'Content serialization format used for the input text', + 'contentmodel' => 'Content model of the new content', ); } @@ -613,6 +650,8 @@ class ApiParse extends ApiBase { array( 'code' => 'nosuchsection', 'info' => 'There is no section sectionnumber in page' ), array( 'nosuchpageid' ), array( 'invalidtitle', 'title' ), + array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ), + array( 'code' => 'notwikitext', 'info' => 'The requested operation is only supported on wikitext content.' ), ) ); } diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php index 9fedaf1bdd..dbfa89cbcf 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -86,14 +86,16 @@ class ApiPurge extends ApiBase { if( $forceLinkUpdate ) { if ( !$user->pingLimiter() ) { - global $wgParser, $wgEnableParserCache; + global $wgEnableParserCache; $popts = $page->makeParserOptions( 'canonical' ); - $p_result = $wgParser->parse( $page->getRawText(), $title, $popts, - true, true, $page->getLatest() ); + + # Parse content; note that HTML generation is only needed if we want to cache the result. + $content = $page->getContent( Revision::RAW ); + $p_result = $content->getParserOutput( $title, $page->getLatest(), $popts, $wgEnableParserCache ); # Update the links tables - $updates = $p_result->getSecondaryDataUpdates( $title ); + $updates = $content->getSecondaryDataUpdates( $title, null, true, $p_result ); DataUpdate::runUpdates( $updates ); $r['linkupdate'] = ''; diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 64399b2f3a..dff7524d88 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -46,6 +46,10 @@ class ApiQuery extends ApiBase { private $params, $redirects, $convertTitles, $iwUrl; + /** + * List of Api Query prop modules + * @var array + */ private $mQueryPropModules = array( 'categories' => 'ApiQueryCategories', 'categoryinfo' => 'ApiQueryCategoryInfo', @@ -63,6 +67,10 @@ class ApiQuery extends ApiBase { 'templates' => 'ApiQueryLinks', ); + /** + * List of Api Query list modules + * @var array + */ private $mQueryListModules = array( 'allcategories' => 'ApiQueryAllCategories', 'allimages' => 'ApiQueryAllImages', @@ -92,16 +100,52 @@ class ApiQuery extends ApiBase { 'watchlistraw' => 'ApiQueryWatchlistRaw', ); + /** + * List of Api Query meta modules + * @var array + */ private $mQueryMetaModules = array( 'allmessages' => 'ApiQueryAllMessages', 'siteinfo' => 'ApiQuerySiteinfo', 'userinfo' => 'ApiQueryUserInfo', ); + /** + * List of Api Query generator modules + * Defined in code, rather than being derived at runtime, + * due to performance reasons + * @var array + */ + private $mQueryGenerators = array( + 'allcategories' => 'ApiQueryAllCategories', + 'allimages' => 'ApiQueryAllImages', + 'alllinks' => 'ApiQueryAllLinks', + 'allpages' => 'ApiQueryAllPages', + 'backlinks' => 'ApiQueryBacklinks', + 'categories' => 'ApiQueryCategories', + 'categorymembers' => 'ApiQueryCategoryMembers', + 'duplicatefiles' => 'ApiQueryDuplicateFiles', + 'embeddedin' => 'ApiQueryBacklinks', + 'exturlusage' => 'ApiQueryExtLinksUsage', + 'images' => 'ApiQueryImages', + 'imageusage' => 'ApiQueryBacklinks', + 'iwbacklinks' => 'ApiQueryIWBacklinks', + 'langbacklinks' => 'ApiQueryLangBacklinks', + 'links' => 'ApiQueryLinks', + 'protectedtitles' => 'ApiQueryProtectedTitles', + 'querypage' => 'ApiQueryQueryPage', + 'random' => 'ApiQueryRandom', + 'recentchanges' => 'ApiQueryRecentChanges', + 'search' => 'ApiQuerySearch', + 'templates' => 'ApiQueryLinks', + 'watchlist' => 'ApiQueryWatchlist', + 'watchlistraw' => 'ApiQueryWatchlistRaw', + ); + private $mSlaveDB = null; private $mNamedDB = array(); - protected $mAllowedGenerators = array(); + protected $mAllowedGenerators; /** * @param $main ApiMain @@ -111,32 +155,16 @@ class ApiQuery extends ApiBase { parent::__construct( $main, $action ); // Allow custom modules to be added in LocalSettings.php - global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules, - $wgMemc, $wgAPICacheHelpTimeout; + global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules, $wgAPIGeneratorModules; self::appendUserModules( $this->mQueryPropModules, $wgAPIPropModules ); self::appendUserModules( $this->mQueryListModules, $wgAPIListModules ); self::appendUserModules( $this->mQueryMetaModules, $wgAPIMetaModules ); + self::appendUserModules( $this->mQueryGenerators, $wgAPIGeneratorModules ); $this->mPropModuleNames = array_keys( $this->mQueryPropModules ); $this->mListModuleNames = array_keys( $this->mQueryListModules ); $this->mMetaModuleNames = array_keys( $this->mQueryMetaModules ); - - // Get array of query generators from cache if present - $key = wfMemcKey( 'apiquerygenerators', SpecialVersion::getVersion( 'nodb' ) ); - - if ( $wgAPICacheHelpTimeout > 0 ) { - $cached = $wgMemc->get( $key ); - if ( $cached ) { - $this->mAllowedGenerators = $cached; - return; - } - } - $this->makeGeneratorList( $this->mQueryPropModules ); - $this->makeGeneratorList( $this->mQueryListModules ); - - if ( $wgAPICacheHelpTimeout > 0 ) { - $wgMemc->set( $key, $this->mAllowedGenerators, $wgAPICacheHelpTimeout ); - } + $this->mAllowedGenerators = array_keys( $this->mQueryGenerators ); } /** @@ -196,10 +224,18 @@ class ApiQuery extends ApiBase { * Get the array mapping module names to class names * @return array array(modulename => classname) */ - function getModules() { + public function getModules() { return array_merge( $this->mQueryPropModules, $this->mQueryListModules, $this->mQueryMetaModules ); } + /** + * Get the generators array mapping module names to class names + * @return array array(modulename => classname) + */ + public function getGenerators() { + return $this->mQueryGenerators; + } + /** * Get whether the specified module is a prop, list or a meta query module * @param $moduleName string Name of the module to find type for @@ -680,19 +716,6 @@ class ApiQuery extends ApiBase { return implode( "\n", $moduleDescriptions ); } - /** - * Adds any classes that are a subclass of ApiQueryGeneratorBase - * to the allowed generator list - * @param $moduleList array() - */ - private function makeGeneratorList( $moduleList ) { - foreach( $moduleList as $moduleName => $moduleClass ) { - if ( is_subclass_of( $moduleClass, 'ApiQueryGeneratorBase' ) ) { - $this->mAllowedGenerators[] = $moduleName; - } - } - } - /** * Override to add extra parameters from PageSet * @return string diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php index 4f4c77f0e7..c2beaec6a7 100644 --- a/includes/api/ApiQueryAllCategories.php +++ b/includes/api/ApiQueryAllCategories.php @@ -81,7 +81,6 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { } else { $this->addWhereRange( 'cat_pages', 'older', $max, $min); } - if ( isset( $params['prefix'] ) ) { $this->addWhere( 'cat_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index a5486ef4a0..dbca1d96db 100644 --- a/includes/api/ApiQueryFilearchive.php +++ b/includes/api/ApiQueryFilearchive.php @@ -64,7 +64,7 @@ class ApiQueryFilearchive extends ApiQueryBase { $this->addTables( 'filearchive' ); $this->addFields( array( 'fa_name', 'fa_deleted' ) ); - $this->addFieldsIf( 'fa_storage_key', $fld_sha1 ); + $this->addFieldsIf( 'fa_sha1', $fld_sha1 ); $this->addFieldsIf( 'fa_timestamp', $fld_timestamp ); $this->addFieldsIf( array( 'fa_user', 'fa_user_text' ), $fld_user ); $this->addFieldsIf( array( 'fa_height', 'fa_width', 'fa_size' ), $fld_dimensions || $fld_size ); @@ -101,11 +101,6 @@ class ApiQueryFilearchive extends ApiQueryBase { $sha1Set = isset( $params['sha1'] ); $sha1base36Set = isset( $params['sha1base36'] ); if ( $sha1Set || $sha1base36Set ) { - global $wgMiserMode; - if ( $wgMiserMode ) { - $this->dieUsage( 'Search by hash disabled in Miser Mode', 'hashsearchdisabled' ); - } - $sha1 = false; if ( $sha1Set ) { if ( !$this->validateSha1Hash( $params['sha1'] ) ) { @@ -119,7 +114,7 @@ class ApiQueryFilearchive extends ApiQueryBase { $sha1 = $params['sha1base36']; } if ( $sha1 ) { - $this->addWhere( 'fa_storage_key ' . $db->buildLike( "{$sha1}.", $db->anyString() ) ); + $this->addWhereFld( 'fa_sha1', $sha1 ); } } @@ -155,7 +150,7 @@ class ApiQueryFilearchive extends ApiQueryBase { self::addTitleInfo( $file, $title ); if ( $fld_sha1 ) { - $file['sha1'] = wfBaseConvert( LocalRepo::getHashFromKey( $row->fa_storage_key ), 36, 16, 40 ); + $file['sha1'] = wfBaseConvert( $row->fa_sha1, 36, 16, 40 ); } if ( $fld_timestamp ) { $file['timestamp'] = wfTimestamp( TS_ISO_8601, $row->fa_timestamp ); @@ -276,8 +271,8 @@ class ApiQueryFilearchive extends ApiQueryBase { 'prefix' => 'Search for all image titles that begin with this value', 'dir' => 'The direction in which to list', 'limit' => 'How many images to return in total', - 'sha1' => "SHA1 hash of image. Overrides {$this->getModulePrefix()}sha1base36. Disabled in Miser Mode", - 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki). Disabled in Miser Mode', + 'sha1' => "SHA1 hash of image. Overrides {$this->getModulePrefix()}sha1base36", + 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki)', 'prop' => array( 'What image information to get:', ' sha1 - Adds SHA-1 hash for the image', diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index ee55fb5476..de0261413f 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -172,7 +172,7 @@ class ApiQueryImageInfo extends ApiQueryBase { $data = $this->getResultData(); foreach ( $data['query']['pages'] as $pageid => $arr ) { - if ( !isset( $arr['imagerepository'] ) ) { + if ( is_array( $arr ) && !isset( $arr['imagerepository'] ) ) { $result->addValue( array( 'query', 'pages', $pageid ), 'imagerepository', '' diff --git a/includes/api/ApiQueryORM.php b/includes/api/ApiQueryORM.php new file mode 100644 index 0000000000..3b18d8a92d --- /dev/null +++ b/includes/api/ApiQueryORM.php @@ -0,0 +1,264 @@ + + */ +abstract class ApiQueryORM extends ApiQueryBase { + + /** + * Returns an instance of the IORMTable table being queried. + * + * @since 1.21 + * + * @return IORMTable + */ + protected abstract function getTable(); + + /** + * Returns the name of the individual rows. + * For example: page, user, contest, campaign, etc. + * This is used to appropriately name elements in XML. + * Deriving classes typically override this method. + * + * @since 1.21 + * + * @return string + */ + protected function getRowName() { + return 'item'; + } + + /** + * Returns the name of the list of rows. + * For example: pages, users, contests, campaigns, etc. + * This is used to appropriately name nodes in the output. + * Deriving classes typically override this method. + * + * @since 1.21 + * + * @return string + */ + protected function getListName() { + return 'items'; + } + + /** + * Returns the path to where the items results should be added in the result. + * + * @since 1.21 + * + * @return null|string|array + */ + protected function getResultPath() { + return null; + } + + /** + * Get the parameters, find out what the conditions for the query are, + * run it, and add the results. + * + * @since 1.21 + */ + public function execute() { + $params = $this->getParams(); + + if ( !in_array( 'id', $params['props'] ) ) { + $params['props'][] = 'id'; + } + + $results = $this->getResults( $params, $this->getConditions( $params ) ); + $this->addResults( $params, $results ); + } + + /** + * Get the request parameters, handle the * value for the props param + * and remove all params set to null (ie those that are not actually provided). + * + * @since 1.21 + * + * @return array + */ + protected function getParams() { + return array_filter( + $this->extractRequestParams(), + function( $prop ) { + return isset( $prop ); + } + ); + } + + /** + * Get the conditions for the query. These will be provided as + * regular parameters, together with limit, props, continue, + * and possibly others which we need to get rid off. + * + * @since 1.21 + * + * @param array $params + * + * @return array + */ + protected function getConditions( array $params ) { + $conditions = array(); + $fields = $this->getTable()->getFields(); + + foreach ( $params as $name => $value ) { + if ( array_key_exists( $name, $fields ) ) { + $conditions[$name] = $value; + } + } + + return $conditions; + } + + /** + * Get the actual results. + * + * @since 1.21 + * + * @param array $params + * @param array $conditions + * + * @return ORMResult + */ + protected function getResults( array $params, array $conditions ) { + return $this->getTable()->select( + $params['props'], + $conditions, + array( + 'LIMIT' => $params['limit'] + 1, + 'ORDER BY' => $this->getTable()->getPrefixedField( 'id' ) . ' ASC', + ), + __METHOD__ + ); + } + + /** + * Serialize the results and add them to the result object. + * + * @since 1.21 + * + * @param array $params + * @param ORMResult $results + */ + protected function addResults( array $params, ORMResult $results ) { + $serializedResults = array(); + $count = 0; + + foreach ( $results as /* IORMRow */ $result ) { + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', $result->getId() ); + break; + } + + $serializedResults[] = $this->formatRow( $result, $params ); + } + + $this->setIndexedTagNames( $serializedResults ); + $this->addSerializedResults( $serializedResults ); + } + + /** + * Formats a row to it's desired output format. + * + * @since 1.21 + * + * @param IORMRow $result + * @param array $params + * + * @return mixed + */ + protected function formatRow( IORMRow $result, array $params ) { + return $result->toArray( $params['props'] ); + } + + /** + * Set the tag names for formats such as XML. + * + * @since 1.21 + * + * @param array $serializedResults + */ + protected function setIndexedTagNames( array &$serializedResults ) { + $this->getResult()->setIndexedTagName( $serializedResults, $this->getRowName() ); + } + + /** + * Add the serialized results to the result object. + * + * @since 1.21 + * + * @param array $serializedResults + */ + protected function addSerializedResults( array $serializedResults ) { + $this->getResult()->addValue( + $this->getResultPath(), + $this->getListName(), + $serializedResults + ); + } + + /** + * @see ApiBase::getAllowedParams() + * @return array + */ + public function getAllowedParams() { + $params = array ( + 'props' => array( + ApiBase::PARAM_TYPE => $this->getTable()->getFieldNames(), + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_REQUIRED => true, + ), + 'limit' => array( + ApiBase::PARAM_DFLT => 20, + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MIN => 1, + ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, + ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 + ), + 'continue' => null, + ); + + return array_merge( $this->getTable()->getAPIParams(), $params ); + } + + /** + * @see ApiBase::getParamDescription() + * @return array + */ + public function getParamDescription() { + $descriptions = array ( + 'props' => 'Fields to query', + 'continue' => 'Offset number from where to continue the query', + 'limit' => 'Max amount of rows to return', + ); + + return array_merge( $this->getTable()->getFieldDescriptions(), $descriptions ); + } + +} diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 41dfc33a5f..881b79721b 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -34,15 +34,15 @@ class ApiQueryRevisions extends ApiQueryBase { private $diffto, $difftotext, $expandTemplates, $generateXML, $section, - $token, $parseContent; + $token, $parseContent, $contentFormat; public function __construct( $query, $moduleName ) { parent::__construct( $query, $moduleName, 'rv' ); } - private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false, + private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false, $fld_sha1 = false, $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false, - $fld_content = false, $fld_tags = false; + $fld_content = false, $fld_tags = false, $fld_contentmodel = false; private $tokenFunctions; @@ -155,10 +155,15 @@ class ApiQueryRevisions extends ApiQueryBase { $this->fld_parsedcomment = isset ( $prop['parsedcomment'] ); $this->fld_size = isset ( $prop['size'] ); $this->fld_sha1 = isset ( $prop['sha1'] ); + $this->fld_contentmodel = isset ( $prop['contentmodel'] ); $this->fld_userid = isset( $prop['userid'] ); $this->fld_user = isset ( $prop['user'] ); $this->token = $params['token']; + if ( !empty( $params['contentformat'] ) ) { + $this->contentFormat = $params['contentformat']; + } + // Possible indexes used $index = array(); @@ -442,6 +447,10 @@ class ApiQueryRevisions extends ApiQueryBase { } } + if ( $this->fld_contentmodel ) { + $vals['contentmodel'] = $revision->getContentModel(); + } + if ( $this->fld_comment || $this->fld_parsedcomment ) { if ( $revision->isDeleted( Revision::DELETED_COMMENT ) ) { $vals['commenthidden'] = ''; @@ -480,39 +489,79 @@ class ApiQueryRevisions extends ApiQueryBase { } } - $text = null; + $content = null; global $wgParser; if ( $this->fld_content || !is_null( $this->difftotext ) ) { - $text = $revision->getText(); + $content = $revision->getContent(); // Expand templates after getting section content because // template-added sections don't count and Parser::preprocess() // will have less input if ( $this->section !== false ) { - $text = $wgParser->getSection( $text, $this->section, false ); - if ( $text === false ) { + $content = $content->getSection( $this->section, false ); + if ( !$content ) { $this->dieUsage( "There is no section {$this->section} in r" . $revision->getId(), 'nosuchsection' ); } } } if ( $this->fld_content && !$revision->isDeleted( Revision::DELETED_TEXT ) ) { + $text = null; + if ( $this->generateXML ) { - $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS ); - $dom = $wgParser->preprocessToDom( $text ); - if ( is_callable( array( $dom, 'saveXML' ) ) ) { - $xml = $dom->saveXML(); + if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { + $t = $content->getNativeData(); # note: don't set $text + + $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS ); + $dom = $wgParser->preprocessToDom( $t ); + if ( is_callable( array( $dom, 'saveXML' ) ) ) { + $xml = $dom->saveXML(); + } else { + $xml = $dom->__toString(); + } + $vals['parsetree'] = $xml; } else { - $xml = $dom->__toString(); + $this->setWarning( "Conversion to XML is supported for wikitext only, " . + $title->getPrefixedDBkey() . + " uses content model " . $content->getModel() . ")" ); } - $vals['parsetree'] = $xml; - } + if ( $this->expandTemplates && !$this->parseContent ) { - $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) ); + #XXX: implement template expansion for all content types in ContentHandler? + if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { + $text = $content->getNativeData(); + + $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) ); + } else { + $this->setWarning( "Template expansion is supported for wikitext only, " . + $title->getPrefixedDBkey() . + " uses content model " . $content->getModel() . ")" ); + + $text = false; + } } if ( $this->parseContent ) { - $text = $wgParser->parse( $text, $title, ParserOptions::newFromContext( $this->getContext() ) )->getText(); + $po = $content->getParserOutput( $title, $revision->getId(), ParserOptions::newFromContext( $this->getContext() ) ); + $text = $po->getText(); + } + + if ( $text === null ) { + $format = $this->contentFormat ? $this->contentFormat : $content->getDefaultFormat(); + + if ( !$content->isSupportedFormat( $format ) ) { + $model = $content->getModel(); + $name = $title->getPrefixedDBkey(); + + $this->dieUsage( "The requested format {$this->contentFormat} is not supported ". + "for content model $model used by $name", 'badformat' ); + } + + $text = $content->serialize( $format ); + $vals['contentformat'] = $format; + } + + if ( $text !== false ) { + ApiResult::setContent( $vals, $text ); } - ApiResult::setContent( $vals, $text ); } elseif ( $this->fld_content ) { $vals['texthidden'] = ''; } @@ -524,11 +573,26 @@ class ApiQueryRevisions extends ApiQueryBase { $vals['diff'] = array(); $context = new DerivativeContext( $this->getContext() ); $context->setTitle( $title ); + $handler = $revision->getContentHandler(); + if ( !is_null( $this->difftotext ) ) { - $engine = new DifferenceEngine( $context ); - $engine->setText( $text, $this->difftotext ); + $model = $title->getContentModel(); + + if ( $this->contentFormat + && !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat ) ) { + + $name = $title->getPrefixedDBkey(); + + $this->dieUsage( "The requested format {$this->contentFormat} is not supported for ". + "content model $model used by $name", 'badformat' ); + } + + $difftocontent = ContentHandler::makeContent( $this->difftotext, $title, $model, $this->contentFormat ); + + $engine = $handler->createDifferenceEngine( $context ); + $engine->setContent( $content, $difftocontent ); } else { - $engine = new DifferenceEngine( $context, $revision->getID(), $this->diffto ); + $engine = $handler->createDifferenceEngine( $context, $revision->getID(), $this->diffto ); $vals['diff']['from'] = $engine->getOldid(); $vals['diff']['to'] = $engine->getNewid(); } @@ -568,6 +632,7 @@ class ApiQueryRevisions extends ApiQueryBase { 'userid', 'size', 'sha1', + 'contentmodel', 'comment', 'parsedcomment', 'content', @@ -617,6 +682,10 @@ class ApiQueryRevisions extends ApiQueryBase { 'continue' => null, 'diffto' => null, 'difftotext' => null, + 'contentformat' => array( + ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), + ApiBase::PARAM_DFLT => null + ), ); } @@ -632,6 +701,7 @@ class ApiQueryRevisions extends ApiQueryBase { ' userid - User id of revision creator', ' size - Length (bytes) of the revision', ' sha1 - SHA-1 (base 16) of the revision', + ' contentmodel - Content model id', ' comment - Comment by the user for revision', ' parsedcomment - Parsed comment by the user for the revision', ' content - Text of the revision', @@ -656,6 +726,7 @@ class ApiQueryRevisions extends ApiQueryBase { 'difftotext' => array( 'Text to diff each revision to. Only diffs a limited number of revisions.', "Overrides {$p}diffto. If {$p}section is set, only that section will be diffed against this text" ), 'tag' => 'Only list revisions tagged with this tag', + 'contentformat' => 'Serialization format used for difftotext and expected for output of content', ); } @@ -733,13 +804,18 @@ class ApiQueryRevisions extends ApiQueryBase { public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), array( array( 'nosuchrevid', 'diffto' ), - array( 'code' => 'revids', 'info' => 'The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).' ), - array( 'code' => 'multpages', 'info' => 'titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start and end parameters may only be used on a single page.' ), + array( 'code' => 'revids', 'info' => 'The revids= parameter may not be used with the list options ' + . '(limit, startid, endid, dirNewer, start, end).' ), + array( 'code' => 'multpages', 'info' => 'titles, pageids or a generator was used to supply multiple pages, ' + . ' but the limit, startid, endid, dirNewer, user, excludeuser, ' + . 'start and end parameters may only be used on a single page.' ), array( 'code' => 'diffto', 'info' => 'rvdiffto must be set to a non-negative number, "prev", "next" or "cur"' ), array( 'code' => 'badparams', 'info' => 'start and startid cannot be used together' ), array( 'code' => 'badparams', 'info' => 'end and endid cannot be used together' ), array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' ), array( 'code' => 'nosuchsection', 'info' => 'There is no section section in rID' ), + array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied ' + . ' to the page\'s content model' ), ) ); } diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index c9962517e2..2ee86411c4 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -61,7 +61,13 @@ class ApiUndelete extends ApiBase { } $pa = new PageArchive( $titleObj ); - $retval = $pa->undelete( ( isset( $params['timestamps'] ) ? $params['timestamps'] : array() ), $params['reason'] ); + $retval = $pa->undelete( + ( isset( $params['timestamps'] ) ? $params['timestamps'] : array() ), + $params['reason'], + array(), + false, + $this->getUser() + ); if ( !is_array( $retval ) ) { $this->dieUsageMsg( 'cannotundelete' ); } diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index 3a9b5c5642..6b8639c8e6 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -86,10 +86,13 @@ class ApiUpload extends ApiBase { if( $this->mParams['filesize'] > $maxSize ) { $this->dieUsage( 'The file you submitted was too large', 'file-too-large' ); } + if ( !$this->mUpload->getTitle() ) { + $this->dieUsage( 'Invalid file title supplied', 'internal-error' ); + } } else { $this->verifyUpload(); } - + // Check if the user has the rights to modify or overwrite the requested title // (This check is irrelevant if stashing is already requested, since the errors // can always be fixed by changing the title) @@ -99,7 +102,7 @@ class ApiUpload extends ApiBase { $this->dieRecoverableError( $permErrors[0], 'filename' ); } } - // Get the result based on the current upload context: + // Get the result based on the current upload context: $result = $this->getContextResult(); if ( $result['result'] === 'Success' ) { @@ -196,7 +199,7 @@ class ApiUpload extends ApiBase { return array(); } - // Check we added the last chunk: + // Check we added the last chunk: if( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) { $status = $this->mUpload->concatenateChunks(); @@ -222,7 +225,7 @@ class ApiUpload extends ApiBase { $result['offset'] = $this->mParams['offset'] + $chunkSize; return $result; } - + /** * Stash the file and return the file key * Also re-raises exceptions with slightly more informative message strings (useful for API) diff --git a/includes/cache/FileCacheBase.php b/includes/cache/FileCacheBase.php index c0c5609c6a..1a08d9f2cd 100644 --- a/includes/cache/FileCacheBase.php +++ b/includes/cache/FileCacheBase.php @@ -229,7 +229,7 @@ abstract class FileCacheBase { public function incrMissesRecent( WebRequest $request ) { global $wgMemc; if ( mt_rand( 0, self::MISS_FACTOR - 1 ) == 0 ) { - # Get a large IP range that should include the user even if that + # Get a large IP range that should include the user even if that # person's IP address changes $ip = $request->getIP(); if ( !IP::isValid( $ip ) ) { diff --git a/includes/cache/HTMLFileCache.php b/includes/cache/HTMLFileCache.php index 6bfeed32b1..fca071a120 100644 --- a/includes/cache/HTMLFileCache.php +++ b/includes/cache/HTMLFileCache.php @@ -33,6 +33,7 @@ class HTMLFileCache extends FileCacheBase { * Construct an ObjectFileCache from a Title and an action * @param $title Title|string Title object or prefixed DB key string * @param $action string + * @throws MWException * @return HTMLFileCache */ public static function newFromTitle( $title, $action ) { diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php index f759c0206d..623f545605 100644 --- a/includes/cache/LinkCache.php +++ b/includes/cache/LinkCache.php @@ -74,7 +74,7 @@ class LinkCache { * Get a field of a title object from cache. * If this link is not good, it will return NULL. * @param $title Title - * @param $field String: ('length','redirect','revision') + * @param $field String: ('length','redirect','revision','model') * @return mixed */ public function getGoodLinkFieldObj( $title, $field ) { @@ -102,14 +102,16 @@ class LinkCache { * @param $len Integer: text's length * @param $redir Integer: whether the page is a redirect * @param $revision Integer: latest revision's ID + * @param $model Integer: latest revision's content model ID */ - public function addGoodLinkObj( $id, $title, $len = -1, $redir = null, $revision = false ) { + public function addGoodLinkObj( $id, $title, $len = -1, $redir = null, $revision = false, $model = false ) { $dbkey = $title->getPrefixedDbKey(); $this->mGoodLinks[$dbkey] = intval( $id ); $this->mGoodLinkFields[$dbkey] = array( 'length' => intval( $len ), 'redirect' => intval( $redir ), - 'revision' => intval( $revision ) ); + 'revision' => intval( $revision ), + 'model' => intval( $model ) ); } /** @@ -117,7 +119,7 @@ class LinkCache { * @since 1.19 * @param $title Title * @param $row object which has the fields page_id, page_is_redirect, - * page_latest + * page_latest and page_content_model */ public function addGoodLinkObjFromRow( $title, $row ) { $dbkey = $title->getPrefixedDbKey(); @@ -126,6 +128,7 @@ class LinkCache { 'length' => intval( $row->page_len ), 'redirect' => intval( $row->page_is_redirect ), 'revision' => intval( $row->page_latest ), + 'model' => !empty( $row->page_content_model ) ? strval( $row->page_content_model ) : null, ); } @@ -178,7 +181,8 @@ class LinkCache { * @return Integer */ public function addLinkObj( $nt ) { - global $wgAntiLockFlags; + global $wgAntiLockFlags, $wgContentHandlerUseDB; + wfProfileIn( __METHOD__ ); $key = $nt->getPrefixedDBkey(); @@ -210,8 +214,10 @@ class LinkCache { $options = array(); } - $s = $db->selectRow( 'page', - array( 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), + $f = array( 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ); + if ( $wgContentHandlerUseDB ) $f[] = 'page_content_model'; + + $s = $db->selectRow( 'page', $f, array( 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ), __METHOD__, $options ); # Set fields... diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index b854a2ec36..e061101bc6 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -596,7 +596,7 @@ class MessageCache { * @param $key String: the message cache key * @param $useDB Boolean: get the message from the DB, false to use only * the localisation - * @param $langcode String: code of the language to get the message for, if + * @param bool|string $langcode Code of the language to get the message for, if * it is a valid code create a language for that language, * if it is a string but not a valid code then make a basic * language object, if it is a false boolean then use the @@ -607,6 +607,7 @@ class MessageCache { * @param $isFullKey Boolean: specifies whether $key is a two part key * "msg/lang". * + * @throws MWException * @return string|bool */ function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) { @@ -770,16 +771,32 @@ class MessageCache { Title::makeTitle( NS_MEDIAWIKI, $title ), false, Revision::READ_LATEST ); if ( $revision ) { - $message = $revision->getText(); - if ($message === false) { + $content = $revision->getContent(); + if ( !$content ) { // A possibly temporary loading failure. wfDebugLog( 'MessageCache', __METHOD__ . ": failed to load message page text for {$title} ($code)" ); + $message = null; // no negative caching } else { - $this->mCache[$code][$title] = ' ' . $message; - $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry ); + // XXX: Is this the right way to turn a Content object into a message? + // NOTE: $content is typically either WikitextContent, JavaScriptContent or CssContent. + // MessageContent is *not* used for storing messages, it's only used for wrapping them when needed. + $message = $content->getWikitextForTransclusion(); + + if ( $message === false || $message === null ) { + wfDebugLog( 'MessageCache', __METHOD__ . ": message content doesn't provide wikitext " + . "(content model: " . $content->getContentHandler() . ")" ); + + $message = false; // negative caching + } else { + $this->mCache[$code][$title] = ' ' . $message; + $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry ); + } } } else { - $message = false; + $message = false; // negative caching + } + + if ( $message === false ) { // negative caching $this->mCache[$code][$title] = '!NONEXISTENT'; $this->mMemc->set( $titleKey, '!NONEXISTENT', $this->mExpiry ); } diff --git a/includes/cache/SquidUpdate.php b/includes/cache/SquidUpdate.php index 423e388448..6b48fa4d14 100644 --- a/includes/cache/SquidUpdate.php +++ b/includes/cache/SquidUpdate.php @@ -129,6 +129,8 @@ class SquidUpdate { return; } + wfDebug( "Squid purge: " . implode( ' ', $urlArr ) . "\n" ); + if ( $wgHTCPMulticastRouting ) { SquidUpdate::HTCPPurge( $urlArr ); } @@ -249,7 +251,7 @@ class SquidUpdate { static function expand( $url ) { return wfExpandUrl( $url, PROTO_INTERNAL ); } - + /** * Find the HTCP routing rule to use for a given URL. * @param $url string URL to match @@ -264,5 +266,4 @@ class SquidUpdate { } return false; } - } diff --git a/includes/conf/Conf.php b/includes/conf/Conf.php index 93204ead73..22c621fe5a 100644 --- a/includes/conf/Conf.php +++ b/includes/conf/Conf.php @@ -97,6 +97,7 @@ abstract class Conf { * Initialize a new child class based on a configuration array * @param $conf Array of configuration settings, see $wgConfiguration * for details + * @throws MWException * @return Conf */ private static function newFromSettings( $conf ) { @@ -109,7 +110,8 @@ abstract class Conf { /** * Get the singleton if we don't want a specific wiki - * @param $wiki String An id for a remote wiki + * @param bool|string $wiki An id for a remote wiki + * @throws MWException * @return Conf child */ public static function load( $wiki = false ) { diff --git a/includes/conf/DatabaseConf.php b/includes/conf/DatabaseConf.php index e2e36cef2c..d8f644deec 100644 --- a/includes/conf/DatabaseConf.php +++ b/includes/conf/DatabaseConf.php @@ -39,7 +39,7 @@ class DatabaseConf extends Conf { * * @param $name * @param $value - * + * * @return bool */ protected function writeSetting( $name, $value ) { diff --git a/includes/content/AbstractContent.php b/includes/content/AbstractContent.php new file mode 100644 index 0000000000..495711ab05 --- /dev/null +++ b/includes/content/AbstractContent.php @@ -0,0 +1,414 @@ +model_id = $modelId; + } + + /** + * @see Content::getModel + * + * @since 1.21 + */ + public function getModel() { + return $this->model_id; + } + + /** + * Throws an MWException if $model_id is not the id of the content model + * supported by this Content object. + * + * @since 1.21 + * + * @param string $modelId The model to check + * + * @throws MWException + */ + protected function checkModelID( $modelId ) { + if ( $modelId !== $this->model_id ) { + throw new MWException( + "Bad content model: " . + "expected {$this->model_id} " . + "but got $modelId." + ); + } + } + + /** + * @see Content::getContentHandler + * + * @since 1.21 + */ + public function getContentHandler() { + return ContentHandler::getForContent( $this ); + } + + /** + * @see Content::getDefaultFormat + * + * @since 1.21 + */ + public function getDefaultFormat() { + return $this->getContentHandler()->getDefaultFormat(); + } + + /** + * @see Content::getSupportedFormats + * + * @since 1.21 + */ + public function getSupportedFormats() { + return $this->getContentHandler()->getSupportedFormats(); + } + + /** + * @see Content::isSupportedFormat + * + * @param string $format + * + * @since 1.21 + * + * @return boolean + */ + public function isSupportedFormat( $format ) { + if ( !$format ) { + return true; // this means "use the default" + } + + return $this->getContentHandler()->isSupportedFormat( $format ); + } + + /** + * Throws an MWException if $this->isSupportedFormat( $format ) does not + * return true. + * + * @since 1.21 + * + * @param string $format + * @throws MWException + */ + protected function checkFormat( $format ) { + if ( !$this->isSupportedFormat( $format ) ) { + throw new MWException( + "Format $format is not supported for content model " . + $this->getModel() + ); + } + } + + /** + * @see Content::serialize + * + * @param string|null $format + * + * @since 1.21 + * + * @return string + */ + public function serialize( $format = null ) { + return $this->getContentHandler()->serializeContent( $this, $format ); + } + + /** + * @see Content::isEmpty + * + * @since 1.21 + * + * @return boolean + */ + public function isEmpty() { + return $this->getSize() === 0; + } + + /** + * @see Content::isValid + * + * @since 1.21 + * + * @return boolean + */ + public function isValid() { + return true; + } + + /** + * @see Content::equals + * + * @since 1.21 + * + * @param Content|null $that + * + * @return boolean + */ + public function equals( Content $that = null ) { + if ( is_null( $that ) ) { + return false; + } + + if ( $that === $this ) { + return true; + } + + if ( $that->getModel() !== $this->getModel() ) { + return false; + } + + return $this->getNativeData() === $that->getNativeData(); + } + + + /** + * Returns a list of DataUpdate objects for recording information about this + * Content in some secondary data store. + * + * This default implementation calls + * $this->getParserOutput( $content, $title, null, null, false ), + * and then calls getSecondaryDataUpdates( $title, $recursive ) on the + * resulting ParserOutput object. + * + * Subclasses may override this to determine the secondary data updates more + * efficiently, preferrably without the need to generate a parser output object. + * + * @see Content::getSecondaryDataUpdates() + * + * @param $title Title The context for determining the necessary updates + * @param $old Content|null An optional Content object representing the + * previous content, i.e. the content being replaced by this Content + * object. + * @param $recursive boolean Whether to include recursive updates (default: + * false). + * @param $parserOutput ParserOutput|null Optional ParserOutput object. + * Provide if you have one handy, to avoid re-parsing of the content. + * + * @return Array. A list of DataUpdate objects for putting information + * about this content object somewhere. + * + * @since 1.21 + */ + public function getSecondaryDataUpdates( Title $title, + Content $old = null, + $recursive = true, ParserOutput $parserOutput = null + ) { + if ( !$parserOutput ) { + $parserOutput = $this->getParserOutput( $title, null, null, false ); + } + + return $parserOutput->getSecondaryDataUpdates( $title, $recursive ); + } + + + /** + * @see Content::getRedirectChain + * + * @since 1.21 + */ + public function getRedirectChain() { + global $wgMaxRedirects; + $title = $this->getRedirectTarget(); + if ( is_null( $title ) ) { + return null; + } + // recursive check to follow double redirects + $recurse = $wgMaxRedirects; + $titles = array( $title ); + while ( --$recurse > 0 ) { + if ( $title->isRedirect() ) { + $page = WikiPage::factory( $title ); + $newtitle = $page->getRedirectTarget(); + } else { + break; + } + // Redirects to some special pages are not permitted + if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) { + // The new title passes the checks, so make that our current + // title so that further recursion can be checked + $title = $newtitle; + $titles[] = $newtitle; + } else { + break; + } + } + return $titles; + } + + /** + * @see Content::getRedirectTarget + * + * @since 1.21 + */ + public function getRedirectTarget() { + return null; + } + + /** + * @see Content::getUltimateRedirectTarget + * @note: migrated here from Title::newFromRedirectRecurse + * + * @since 1.21 + */ + public function getUltimateRedirectTarget() { + $titles = $this->getRedirectChain(); + return $titles ? array_pop( $titles ) : null; + } + + /** + * @see Content::isRedirect + * + * @since 1.21 + * + * @return bool + */ + public function isRedirect() { + return $this->getRedirectTarget() !== null; + } + + /** + * @see Content::updateRedirect + * + * This default implementation always returns $this. + * + * @param Title $target + * + * @since 1.21 + * + * @return Content $this + */ + public function updateRedirect( Title $target ) { + return $this; + } + + /** + * @see Content::getSection + * + * @since 1.21 + */ + public function getSection( $sectionId ) { + return null; + } + + /** + * @see Content::replaceSection + * + * @since 1.21 + */ + public function replaceSection( $section, Content $with, $sectionTitle = '' ) { + return null; + } + + /** + * @see Content::preSaveTransform + * + * @since 1.21 + */ + public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) { + return $this; + } + + /** + * @see Content::addSectionHeader + * + * @since 1.21 + */ + public function addSectionHeader( $header ) { + return $this; + } + + /** + * @see Content::preloadTransform + * + * @since 1.21 + */ + public function preloadTransform( Title $title, ParserOptions $popts ) { + return $this; + } + + /** + * @see Content::prepareSave + * + * @since 1.21 + */ + public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user ) { + if ( $this->isValid() ) { + return Status::newGood(); + } else { + return Status::newFatal( "invalid-content-data" ); + } + } + + /** + * @see Content::getDeletionUpdates + * + * @since 1.21 + * + * @param $page \WikiPage the deleted page + * @param $parserOutput null|\ParserOutput optional parser output object + * for efficient access to meta-information about the content object. + * Provide if you have one handy. + * + * @return array A list of DataUpdate instances that will clean up the + * database after deletion. + */ + public function getDeletionUpdates( WikiPage $page, + ParserOutput $parserOutput = null ) + { + return array( + new LinksDeletionUpdate( $page ), + ); + } + + /** + * This default implementation always returns false. Subclasses may override this to supply matching logic. + * + * @see Content::matchMagicWord + * + * @since 1.21 + * + * @param MagicWord $word + * + * @return bool + */ + public function matchMagicWord( MagicWord $word ) { + return false; + } +} diff --git a/includes/content/Content.php b/includes/content/Content.php new file mode 100644 index 0000000000..d830dc7dc2 --- /dev/null +++ b/includes/content/Content.php @@ -0,0 +1,490 @@ +getContentHandler()->getDefaultFormat() + * + * @since 1.21 + * + * @return String + */ + public function getDefaultFormat(); + + /** + * Convenience method that returns the list of serialization formats + * supported for the content model that this Content object uses. + * + * Shorthand for $this->getContentHandler()->getSupportedFormats() + * + * @since 1.21 + * + * @return Array of supported serialization formats + */ + public function getSupportedFormats(); + + /** + * Returns true if $format is a supported serialization format for this + * Content object, false if it isn't. + * + * Note that this should always return true if $format is null, because null + * stands for the default serialization. + * + * Shorthand for $this->getContentHandler()->isSupportedFormat( $format ) + * + * @since 1.21 + * + * @param $format string The format to check + * @return bool Whether the format is supported + */ + public function isSupportedFormat( $format ); + + /** + * Convenience method for serializing this Content object. + * + * Shorthand for $this->getContentHandler()->serializeContent( $this, $format ) + * + * @since 1.21 + * + * @param $format null|string The desired serialization format (or null for + * the default format). + * @return string Serialized form of this Content object + */ + public function serialize( $format = null ); + + /** + * Returns true if this Content object represents empty content. + * + * @since 1.21 + * + * @return bool Whether this Content object is empty + */ + public function isEmpty(); + + /** + * Returns whether the content is valid. This is intended for local validity + * checks, not considering global consistency. + * + * Content needs to be valid before it can be saved. + * + * This default implementation always returns true. + * + * @since 1.21 + * + * @return boolean + */ + public function isValid(); + + /** + * Returns true if this Content objects is conceptually equivalent to the + * given Content object. + * + * Contract: + * + * - Will return false if $that is null. + * - Will return true if $that === $this. + * - Will return false if $that->getModel() != $this->getModel(). + * - Will return false if $that->getNativeData() is not equal to $this->getNativeData(), + * where the meaning of "equal" depends on the actual data model. + * + * Implementations should be careful to make equals() transitive and reflexive: + * + * - $a->equals( $b ) <=> $b->equals( $a ) + * - $a->equals( $b ) && $b->equals( $c ) ==> $a->equals( $c ) + * + * @since 1.21 + * + * @param $that Content The Content object to compare to + * @return bool True if this Content object is equal to $that, false otherwise. + */ + public function equals( Content $that = null ); + + /** + * Return a copy of this Content object. The following must be true for the + * object returned: + * + * if $copy = $original->copy() + * + * - get_class($original) === get_class($copy) + * - $original->getModel() === $copy->getModel() + * - $original->equals( $copy ) + * + * If and only if the Content object is immutable, the copy() method can and + * should return $this. That is, $copy === $original may be true, but only + * for immutable content objects. + * + * @since 1.21 + * + * @return Content. A copy of this object + */ + public function copy( ); + + /** + * Returns true if this content is countable as a "real" wiki page, provided + * that it's also in a countable location (e.g. a current revision in the + * main namespace). + * + * @since 1.21 + * + * @param $hasLinks Bool: If it is known whether this content contains + * links, provide this information here, to avoid redundant parsing to + * find out. + * @return boolean + */ + public function isCountable( $hasLinks = null ); + + + /** + * Parse the Content object and generate a ParserOutput from the result. + * $result->getText() can be used to obtain the generated HTML. If no HTML + * is needed, $generateHtml can be set to false; in that case, + * $result->getText() may return null. + * + * @param $title Title The page title to use as a context for rendering + * @param $revId null|int The revision being rendered (optional) + * @param $options null|ParserOptions Any parser options + * @param $generateHtml Boolean Whether to generate HTML (default: true). If false, + * the result of calling getText() on the ParserOutput object returned by + * this method is undefined. + * + * @since 1.21 + * + * @return ParserOutput + */ + public function getParserOutput( Title $title, + $revId = null, + ParserOptions $options = null, $generateHtml = true ); + # TODO: make RenderOutput and RenderOptions base classes + + /** + * Returns a list of DataUpdate objects for recording information about this + * Content in some secondary data store. If the optional second argument, + * $old, is given, the updates may model only the changes that need to be + * made to replace information about the old content with information about + * the new content. + * + * This default implementation calls + * $this->getParserOutput( $content, $title, null, null, false ), + * and then calls getSecondaryDataUpdates( $title, $recursive ) on the + * resulting ParserOutput object. + * + * Subclasses may implement this to determine the necessary updates more + * efficiently, or make use of information about the old content. + * + * @param $title Title The context for determining the necessary updates + * @param $old Content|null An optional Content object representing the + * previous content, i.e. the content being replaced by this Content + * object. + * @param $recursive boolean Whether to include recursive updates (default: + * false). + * @param $parserOutput ParserOutput|null Optional ParserOutput object. + * Provide if you have one handy, to avoid re-parsing of the content. + * + * @return Array. A list of DataUpdate objects for putting information + * about this content object somewhere. + * + * @since 1.21 + */ + public function getSecondaryDataUpdates( Title $title, + Content $old = null, + $recursive = true, ParserOutput $parserOutput = null + ); + + /** + * Construct the redirect destination from this content and return an + * array of Titles, or null if this content doesn't represent a redirect. + * The last element in the array is the final destination after all redirects + * have been resolved (up to $wgMaxRedirects times). + * + * @since 1.21 + * + * @return Array of Titles, with the destination last + */ + public function getRedirectChain(); + + /** + * Construct the redirect destination from this content and return a Title, + * or null if this content doesn't represent a redirect. + * This will only return the immediate redirect target, useful for + * the redirect table and other checks that don't need full recursion. + * + * @since 1.21 + * + * @return Title: The corresponding Title + */ + public function getRedirectTarget(); + + /** + * Construct the redirect destination from this content and return the + * Title, or null if this content doesn't represent a redirect. + * + * This will recurse down $wgMaxRedirects times or until a non-redirect + * target is hit in order to provide (hopefully) the Title of the final + * destination instead of another redirect. + * + * There is usually no need to override the default behaviour, subclasses that + * want to implement redirects should override getRedirectTarget(). + * + * @since 1.21 + * + * @return Title + */ + public function getUltimateRedirectTarget(); + + /** + * Returns whether this Content represents a redirect. + * Shorthand for getRedirectTarget() !== null. + * + * @since 1.21 + * + * @return bool + */ + public function isRedirect(); + + /** + * If this Content object is a redirect, this method updates the redirect target. + * Otherwise, it does nothing. + * + * @since 1.21 + * + * @param Title $target the new redirect target + * + * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect) + */ + public function updateRedirect( Title $target ); + + /** + * Returns the section with the given ID. + * + * @since 1.21 + * + * @param $sectionId string The section's ID, given as a numeric string. + * The ID "0" retrieves the section before the first heading, "1" the + * text between the first heading (included) and the second heading + * (excluded), etc. + * @return Content|Boolean|null The section, or false if no such section + * exist, or null if sections are not supported. + */ + public function getSection( $sectionId ); + + /** + * Replaces a section of the content and returns a Content object with the + * section replaced. + * + * @since 1.21 + * + * @param $section Empty/null/false or a section number (0, 1, 2, T1, T2...), or "new" + * @param $with Content: new content of the section + * @param $sectionTitle String: new section's subject, only if $section is 'new' + * @return string Complete article text, or null if error + */ + public function replaceSection( $section, Content $with, $sectionTitle = '' ); + + /** + * Returns a Content object with pre-save transformations applied (or this + * object if no transformations apply). + * + * @since 1.21 + * + * @param $title Title + * @param $user User + * @param $popts null|ParserOptions + * @return Content + */ + public function preSaveTransform( Title $title, User $user, ParserOptions $popts ); + + /** + * Returns a new WikitextContent object with the given section heading + * prepended, if supported. The default implementation just returns this + * Content object unmodified, ignoring the section header. + * + * @since 1.21 + * + * @param $header string + * @return Content + */ + public function addSectionHeader( $header ); + + /** + * Returns a Content object with preload transformations applied (or this + * object if no transformations apply). + * + * @since 1.21 + * + * @param $title Title + * @param $popts null|ParserOptions + * @return Content + */ + public function preloadTransform( Title $title, ParserOptions $popts ); + + /** + * Prepare Content for saving. Called before Content is saved by WikiPage::doEditContent() and in + * similar places. + * + * This may be used to check the content's consistency with global state. This function should + * NOT write any information to the database. + * + * Note that this method will usually be called inside the same transaction bracket that will be used + * to save the new revision. + * + * Note that this method is called before any update to the page table is performed. This means that + * $page may not yet know a page ID. + * + * @since 1.21 + * + * @param WikiPage $page The page to be saved. + * @param int $flags bitfield for use with EDIT_XXX constants, see WikiPage::doEditContent() + * @param int $baseRevId the ID of the current revision + * @param User $user + * + * @return Status A status object indicating whether the content was successfully prepared for saving. + * If the returned status indicates an error, a rollback will be performed and the + * transaction aborted. + * + * @see see WikiPage::doEditContent() + */ + public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user ); + + /** + * Returns a list of updates to perform when this content is deleted. + * The necessary updates may be taken from the Content object, or depend on + * the current state of the database. + * + * @since 1.21 + * + * @param $page \WikiPage the deleted page + * @param $parserOutput null|\ParserOutput optional parser output object + * for efficient access to meta-information about the content object. + * Provide if you have one handy. + * + * @return array A list of DataUpdate instances that will clean up the + * database after deletion. + */ + public function getDeletionUpdates( WikiPage $page, + ParserOutput $parserOutput = null ); + + /** + * Returns true if this Content object matches the given magic word. + * + * @since 1.21 + * + * @param MagicWord $word the magic word to match + * + * @return bool whether this Content object matches the given magic word. + */ + public function matchMagicWord( MagicWord $word ); + + # TODO: ImagePage and CategoryPage interfere with per-content action handlers + # TODO: nice&sane integration of GeSHi syntax highlighting + # [11:59] Hooks are ugly; make CodeHighlighter interface and a + # config to set the class which handles syntax highlighting + # [12:00] And default it to a DummyHighlighter +} diff --git a/includes/content/ContentHandler.php b/includes/content/ContentHandler.php new file mode 100644 index 0000000000..33c803eed2 --- /dev/null +++ b/includes/content/ContentHandler.php @@ -0,0 +1,1108 @@ +getNativeData(). + * + * If $content is not a TextContent object, the behavior of this method + * depends on the global $wgContentHandlerTextFallback: + * - If $wgContentHandlerTextFallback is 'fail' and $content is not a + * TextContent object, an MWException is thrown. + * - If $wgContentHandlerTextFallback is 'serialize' and $content is not a + * TextContent object, $content->serialize() is called to get a string + * form of the content. + * - If $wgContentHandlerTextFallback is 'ignore' and $content is not a + * TextContent object, this method returns null. + * - otherwise, the behaviour is undefined. + * + * @since 1.21 + * + * @static + * @param $content Content|null + * @return null|string the textual form of $content, if available + * @throws MWException if $content is not an instance of TextContent and + * $wgContentHandlerTextFallback was set to 'fail'. + */ + public static function getContentText( Content $content = null ) { + global $wgContentHandlerTextFallback; + + if ( is_null( $content ) ) { + return ''; + } + + if ( $content instanceof TextContent ) { + return $content->getNativeData(); + } + + wfDebugLog( 'ContentHandler', 'Accessing ' . $content->getModel() . ' content as text!' ); + + if ( $wgContentHandlerTextFallback == 'fail' ) { + throw new MWException( + "Attempt to get text from Content with model " . + $content->getModel() + ); + } + + if ( $wgContentHandlerTextFallback == 'serialize' ) { + return $content->serialize(); + } + + return null; + } + + /** + * Convenience function for creating a Content object from a given textual + * representation. + * + * $text will be deserialized into a Content object of the model specified + * by $modelId (or, if that is not given, $title->getContentModel()) using + * the given format. + * + * @since 1.21 + * + * @static + * + * @param $text string the textual representation, will be + * unserialized to create the Content object + * @param $title null|Title the title of the page this text belongs to. + * Required if $modelId is not provided. + * @param $modelId null|string the model to deserialize to. If not provided, + * $title->getContentModel() is used. + * @param $format null|string the format to use for deserialization. If not + * given, the model's default format is used. + * + * @return Content a Content object representing $text + * + * @throw MWException if $model or $format is not supported or if $text can + * not be unserialized using $format. + */ + public static function makeContent( $text, Title $title = null, + $modelId = null, $format = null ) + { + if ( is_null( $modelId ) ) { + if ( is_null( $title ) ) { + throw new MWException( "Must provide a Title object or a content model ID." ); + } + + $modelId = $title->getContentModel(); + } + + $handler = ContentHandler::getForModelID( $modelId ); + return $handler->unserializeContent( $text, $format ); + } + + /** + * Returns the name of the default content model to be used for the page + * with the given title. + * + * Note: There should rarely be need to call this method directly. + * To determine the actual content model for a given page, use + * Title::getContentModel(). + * + * Which model is to be used by default for the page is determined based + * on several factors: + * - The global setting $wgNamespaceContentModels specifies a content model + * per namespace. + * - The hook ContentHandlerDefaultModelFor may be used to override the page's default + * model. + * - Pages in NS_MEDIAWIKI and NS_USER default to the CSS or JavaScript + * model if they end in .js or .css, respectively. + * - Pages in NS_MEDIAWIKI default to the wikitext model otherwise. + * - The hook TitleIsCssOrJsPage may be used to force a page to use the CSS + * or JavaScript model. This is a compatibility feature. The ContentHandlerDefaultModelFor + * hook should be used instead if possible. + * - The hook TitleIsWikitextPage may be used to force a page to use the + * wikitext model. This is a compatibility feature. The ContentHandlerDefaultModelFor + * hook should be used instead if possible. + * + * If none of the above applies, the wikitext model is used. + * + * Note: this is used by, and may thus not use, Title::getContentModel() + * + * @since 1.21 + * + * @static + * @param $title Title + * @return null|string default model name for the page given by $title + */ + public static function getDefaultModelFor( Title $title ) { + global $wgNamespaceContentModels; + + // NOTE: this method must not rely on $title->getContentModel() directly or indirectly, + // because it is used to initialize the mContentModel member. + + $ns = $title->getNamespace(); + + $ext = false; + $m = null; + $model = null; + + if ( !empty( $wgNamespaceContentModels[ $ns ] ) ) { + $model = $wgNamespaceContentModels[ $ns ]; + } + + // Hook can determine default model + if ( !wfRunHooks( 'ContentHandlerDefaultModelFor', array( $title, &$model ) ) ) { + if ( !is_null( $model ) ) { + return $model; + } + } + + // Could this page contain custom CSS or JavaScript, based on the title? + $isCssOrJsPage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js)$!u', $title->getText(), $m ); + if ( $isCssOrJsPage ) { + $ext = $m[1]; + } + + // Hook can force JS/CSS + wfRunHooks( 'TitleIsCssOrJsPage', array( $title, &$isCssOrJsPage ) ); + + // Is this a .css subpage of a user page? + $isJsCssSubpage = NS_USER == $ns + && !$isCssOrJsPage + && preg_match( "/\\/.*\\.(js|css)$/", $title->getText(), $m ); + if ( $isJsCssSubpage ) { + $ext = $m[1]; + } + + // Is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook? + $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT; + $isWikitext = $isWikitext && !$isCssOrJsPage && !$isJsCssSubpage; + + // Hook can override $isWikitext + wfRunHooks( 'TitleIsWikitextPage', array( $title, &$isWikitext ) ); + + if ( !$isWikitext ) { + switch ( $ext ) { + case 'js': + return CONTENT_MODEL_JAVASCRIPT; + case 'css': + return CONTENT_MODEL_CSS; + default: + return is_null( $model ) ? CONTENT_MODEL_TEXT : $model; + } + } + + // We established that it must be wikitext + + return CONTENT_MODEL_WIKITEXT; + } + + /** + * Returns the appropriate ContentHandler singleton for the given title. + * + * @since 1.21 + * + * @static + * @param $title Title + * @return ContentHandler + */ + public static function getForTitle( Title $title ) { + $modelId = $title->getContentModel(); + return ContentHandler::getForModelID( $modelId ); + } + + /** + * Returns the appropriate ContentHandler singleton for the given Content + * object. + * + * @since 1.21 + * + * @static + * @param $content Content + * @return ContentHandler + */ + public static function getForContent( Content $content ) { + $modelId = $content->getModel(); + return ContentHandler::getForModelID( $modelId ); + } + + /** + * @var Array A Cache of ContentHandler instances by model id + */ + static $handlers; + + /** + * Returns the ContentHandler singleton for the given model ID. Use the + * CONTENT_MODEL_XXX constants to identify the desired content model. + * + * ContentHandler singletons are taken from the global $wgContentHandlers + * array. Keys in that array are model names, the values are either + * ContentHandler singleton objects, or strings specifying the appropriate + * subclass of ContentHandler. + * + * If a class name is encountered when looking up the singleton for a given + * model name, the class is instantiated and the class name is replaced by + * the resulting singleton in $wgContentHandlers. + * + * If no ContentHandler is defined for the desired $modelId, the + * ContentHandler may be provided by the ContentHandlerForModelID hook. + * If no ContentHandler can be determined, an MWException is raised. + * + * @since 1.21 + * + * @static + * @param $modelId String The ID of the content model for which to get a + * handler. Use CONTENT_MODEL_XXX constants. + * @return ContentHandler The ContentHandler singleton for handling the + * model given by $modelId + * @throws MWException if no handler is known for $modelId. + */ + public static function getForModelID( $modelId ) { + global $wgContentHandlers; + + if ( isset( ContentHandler::$handlers[$modelId] ) ) { + return ContentHandler::$handlers[$modelId]; + } + + if ( empty( $wgContentHandlers[$modelId] ) ) { + $handler = null; + + wfRunHooks( 'ContentHandlerForModelID', array( $modelId, &$handler ) ); + + if ( $handler === null ) { + throw new MWException( "No handler for model '$modelId'' registered in \$wgContentHandlers" ); + } + + if ( !( $handler instanceof ContentHandler ) ) { + throw new MWException( "ContentHandlerForModelID must supply a ContentHandler instance" ); + } + } else { + $class = $wgContentHandlers[$modelId]; + $handler = new $class( $modelId ); + + if ( !( $handler instanceof ContentHandler ) ) { + throw new MWException( "$class from \$wgContentHandlers is not compatible with ContentHandler" ); + } + } + + wfDebugLog( 'ContentHandler', 'Created handler for ' . $modelId + . ': ' . get_class( $handler ) ); + + ContentHandler::$handlers[$modelId] = $handler; + return ContentHandler::$handlers[$modelId]; + } + + /** + * Returns the localized name for a given content model. + * + * Model names are localized using system messages. Message keys + * have the form content-model-$name, where $name is getContentModelName( $id ). + * + * @static + * @param $name String The content model ID, as given by a CONTENT_MODEL_XXX + * constant or returned by Revision::getContentModel(). + * + * @return string The content format's localized name. + * @throws MWException if the model id isn't known. + */ + public static function getLocalizedName( $name ) { + $key = "content-model-$name"; + + $msg = wfMessage( $key ); + + return $msg->exists() ? $msg->plain() : $name; + } + + public static function getContentModels() { + global $wgContentHandlers; + + return array_keys( $wgContentHandlers ); + } + + public static function getAllContentFormats() { + global $wgContentHandlers; + + $formats = array(); + + foreach ( $wgContentHandlers as $model => $class ) { + $handler = ContentHandler::getForModelID( $model ); + $formats = array_merge( $formats, $handler->getSupportedFormats() ); + } + + $formats = array_unique( $formats ); + return $formats; + } + + // ------------------------------------------------------------------------ + + protected $mModelID; + protected $mSupportedFormats; + + /** + * Constructor, initializing the ContentHandler instance with its model ID + * and a list of supported formats. Values for the parameters are typically + * provided as literals by subclass's constructors. + * + * @param $modelId String (use CONTENT_MODEL_XXX constants). + * @param $formats array List for supported serialization formats + * (typically as MIME types) + */ + public function __construct( $modelId, $formats ) { + $this->mModelID = $modelId; + $this->mSupportedFormats = $formats; + + $this->mModelName = preg_replace( '/(Content)?Handler$/', '', get_class( $this ) ); + $this->mModelName = preg_replace( '/[_\\\\]/', '', $this->mModelName ); + $this->mModelName = strtolower( $this->mModelName ); + } + + /** + * Serializes a Content object of the type supported by this ContentHandler. + * + * @since 1.21 + * + * @abstract + * @param $content Content The Content object to serialize + * @param $format null|String The desired serialization format + * @return string Serialized form of the content + */ + public abstract function serializeContent( Content $content, $format = null ); + + /** + * Unserializes a Content object of the type supported by this ContentHandler. + * + * @since 1.21 + * + * @abstract + * @param $blob string serialized form of the content + * @param $format null|String the format used for serialization + * @return Content the Content object created by deserializing $blob + */ + public abstract function unserializeContent( $blob, $format = null ); + + /** + * Creates an empty Content object of the type supported by this + * ContentHandler. + * + * @since 1.21 + * + * @return Content + */ + public abstract function makeEmptyContent(); + + /** + * Creates a new Content object that acts as a redirect to the given page, + * or null of redirects are not supported by this content model. + * + * This default implementation always returns null. Subclasses supporting redirects + * must override this method. + * + * @since 1.21 + * + * @param Title $destination the page to redirect to. + * + * @return Content + */ + public function makeRedirectContent( Title $destination ) { + return null; + } + + /** + * Returns the model id that identifies the content model this + * ContentHandler can handle. Use with the CONTENT_MODEL_XXX constants. + * + * @since 1.21 + * + * @return String The model ID + */ + public function getModelID() { + return $this->mModelID; + } + + /** + * Throws an MWException if $model_id is not the ID of the content model + * supported by this ContentHandler. + * + * @since 1.21 + * + * @param String $model_id The model to check + * + * @throws MWException + */ + protected function checkModelID( $model_id ) { + if ( $model_id !== $this->mModelID ) { + throw new MWException( "Bad content model: " . + "expected {$this->mModelID} " . + "but got $model_id." ); + } + } + + /** + * Returns a list of serialization formats supported by the + * serializeContent() and unserializeContent() methods of this + * ContentHandler. + * + * @since 1.21 + * + * @return array of serialization formats as MIME type like strings + */ + public function getSupportedFormats() { + return $this->mSupportedFormats; + } + + /** + * The format used for serialization/deserialization by default by this + * ContentHandler. + * + * This default implementation will return the first element of the array + * of formats that was passed to the constructor. + * + * @since 1.21 + * + * @return string the name of the default serialization format as a MIME type + */ + public function getDefaultFormat() { + return $this->mSupportedFormats[0]; + } + + /** + * Returns true if $format is a serialization format supported by this + * ContentHandler, and false otherwise. + * + * Note that if $format is null, this method always returns true, because + * null means "use the default format". + * + * @since 1.21 + * + * @param $format string the serialization format to check + * @return bool + */ + public function isSupportedFormat( $format ) { + + if ( !$format ) { + return true; // this means "use the default" + } + + return in_array( $format, $this->mSupportedFormats ); + } + + /** + * Throws an MWException if isSupportedFormat( $format ) is not true. + * Convenient for checking whether a format provided as a parameter is + * actually supported. + * + * @param $format string the serialization format to check + * + * @throws MWException + */ + protected function checkFormat( $format ) { + if ( !$this->isSupportedFormat( $format ) ) { + throw new MWException( + "Format $format is not supported for content model " + . $this->getModelID() + ); + } + } + + /** + * Returns overrides for action handlers. + * Classes listed here will be used instead of the default one when + * (and only when) $wgActions[$action] === true. This allows subclasses + * to override the default action handlers. + * + * @since 1.21 + * + * @return Array + */ + public function getActionOverrides() { + return array(); + } + + /** + * Factory for creating an appropriate DifferenceEngine for this content model. + * + * @since 1.21 + * + * @param $context IContextSource context to use, anything else will be + * ignored + * @param $old Integer Old ID we want to show and diff with. + * @param $new int|string String either 'prev' or 'next'. + * @param $rcid Integer ??? FIXME (default 0) + * @param $refreshCache boolean If set, refreshes the diff cache + * @param $unhide boolean If set, allow viewing deleted revs + * + * @return DifferenceEngine + */ + public function createDifferenceEngine( IContextSource $context, + $old = 0, $new = 0, + $rcid = 0, # FIXME: use everywhere! + $refreshCache = false, $unhide = false + ) { + $this->checkModelID( $context->getTitle()->getContentModel() ); + + $diffEngineClass = $this->getDiffEngineClass(); + + return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide ); + } + + /** + * Get the language in which the content of the given page is written. + * + * This default implementation just returns $wgContLang (except for pages in the MediaWiki namespace) + * + * Note that the pages language is not cacheable, since it may in some cases depend on user settings. + * + * Also note that the page language may or may not depend on the actual content of the page, + * that is, this method may load the content in order to determine the language. + * + * @since 1.21 + * + * @param Title $title the page to determine the language for. + * @param Content|null $content the page's content, if you have it handy, to avoid reloading it. + * + * @return Language the page's language + */ + public function getPageLanguage( Title $title, Content $content = null ) { + global $wgContLang; + + if ( $title->getNamespace() == NS_MEDIAWIKI ) { + // Parse mediawiki messages with correct target language + list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() ); + return wfGetLangObj( $lang ); + } + + return $wgContLang; + } + + /** + * Get the language in which the content of this page is written when + * viewed by user. Defaults to $this->getPageLanguage(), but if the user + * specified a preferred variant, the variant will be used. + * + * This default implementation just returns $this->getPageLanguage( $title, $content ) unless + * the user specified a preferred variant. + * + * Note that the pages view language is not cacheable, since it depends on user settings. + * + * Also note that the page language may or may not depend on the actual content of the page, + * that is, this method may load the content in order to determine the language. + * + * @since 1.21 + * + * @param Title $title the page to determine the language for. + * @param Content|null $content the page's content, if you have it handy, to avoid reloading it. + * + * @return Language the page's language for viewing + */ + public function getPageViewLanguage( Title $title, Content $content = null ) { + $pageLang = $this->getPageLanguage( $title, $content ); + + if ( $title->getNamespace() !== NS_MEDIAWIKI ) { + // If the user chooses a variant, the content is actually + // in a language whose code is the variant code. + $variant = $pageLang->getPreferredVariant(); + if ( $pageLang->getCode() !== $variant ) { + $pageLang = Language::factory( $variant ); + } + } + + return $pageLang; + } + + /** + * Determines whether the content type handled by this ContentHandler + * can be used on the given page. + * + * This default implementation always returns true. + * Subclasses may override this to restrict the use of this content model to specific locations, + * typically based on the namespace or some other aspect of the title, such as a special suffix + * (e.g. ".svg" for SVG content). + * + * @param Title $title the page's title. + * + * @return bool true if content of this kind can be used on the given page, false otherwise. + */ + public function canBeUsedOn( Title $title ) { + return true; + } + + /** + * Returns the name of the diff engine to use. + * + * @since 1.21 + * + * @return string + */ + protected function getDiffEngineClass() { + return 'DifferenceEngine'; + } + + /** + * Attempts to merge differences between three versions. + * Returns a new Content object for a clean merge and false for failure or + * a conflict. + * + * This default implementation always returns false. + * + * @since 1.21 + * + * @param $oldContent Content|string String + * @param $myContent Content|string String + * @param $yourContent Content|string String + * + * @return Content|Bool + */ + public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) { + return false; + } + + /** + * Return an applicable auto-summary if one exists for the given edit. + * + * @since 1.21 + * + * @param $oldContent Content|null: the previous text of the page. + * @param $newContent Content|null: The submitted text of the page. + * @param $flags int Bit mask: a bit mask of flags submitted for the edit. + * + * @return string An appropriate auto-summary, or an empty string. + */ + public function getAutosummary( Content $oldContent = null, Content $newContent = null, $flags ) { + global $wgContLang; + + // Decide what kind of auto-summary is needed. + + // Redirect auto-summaries + + /** + * @var $ot Title + * @var $rt Title + */ + + $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null; + $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null; + + if ( is_object( $rt ) ) { + if ( !is_object( $ot ) + || !$rt->equals( $ot ) + || $ot->getFragment() != $rt->getFragment() ) + { + $truncatedtext = $newContent->getTextForSummary( + 250 + - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() ) + - strlen( $rt->getFullText() ) ); + + return wfMessage( 'autoredircomment', $rt->getFullText() ) + ->rawParams( $truncatedtext )->inContentLanguage()->text(); + } + } + + // New page auto-summaries + if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) { + // If they're making a new article, give its text, truncated, in + // the summary. + + $truncatedtext = $newContent->getTextForSummary( + 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ); + + return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext ) + ->inContentLanguage()->text(); + } + + // Blanking auto-summaries + if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) { + return wfMessage( 'autosumm-blank' )->inContentLanguage()->text(); + } elseif ( !empty( $oldContent ) + && $oldContent->getSize() > 10 * $newContent->getSize() + && $newContent->getSize() < 500 ) + { + // Removing more than 90% of the article + + $truncatedtext = $newContent->getTextForSummary( + 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ); + + return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext ) + ->inContentLanguage()->text(); + } + + // If we reach this point, there's no applicable auto-summary for our + // case, so our auto-summary is empty. + return ''; + } + + /** + * Auto-generates a deletion reason + * + * @since 1.21 + * + * @param $title Title: the page's title + * @param &$hasHistory Boolean: whether the page has a history + * @return mixed String containing deletion reason or empty string, or + * boolean false if no revision occurred + * + * @XXX &$hasHistory is extremely ugly, it's here because + * WikiPage::getAutoDeleteReason() and Article::getReason() + * have it / want it. + */ + public function getAutoDeleteReason( Title $title, &$hasHistory ) { + $dbw = wfGetDB( DB_MASTER ); + + // Get the last revision + $rev = Revision::newFromTitle( $title ); + + if ( is_null( $rev ) ) { + return false; + } + + // Get the article's contents + $content = $rev->getContent(); + $blank = false; + + $this->checkModelID( $content->getModel() ); + + // If the page is blank, use the text from the previous revision, + // which can only be blank if there's a move/import/protect dummy + // revision involved + if ( $content->getSize() == 0 ) { + $prev = $rev->getPrevious(); + + if ( $prev ) { + $content = $prev->getContent(); + $blank = true; + } + } + + // Find out if there was only one contributor + // Only scan the last 20 revisions + $res = $dbw->select( 'revision', 'rev_user_text', + array( + 'rev_page' => $title->getArticleID(), + $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' + ), + __METHOD__, + array( 'LIMIT' => 20 ) + ); + + if ( $res === false ) { + // This page has no revisions, which is very weird + return false; + } + + $hasHistory = ( $res->numRows() > 1 ); + $row = $dbw->fetchObject( $res ); + + if ( $row ) { // $row is false if the only contributor is hidden + $onlyAuthor = $row->rev_user_text; + // Try to find a second contributor + foreach ( $res as $row ) { + if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999 + $onlyAuthor = false; + break; + } + } + } else { + $onlyAuthor = false; + } + + // Generate the summary with a '$1' placeholder + if ( $blank ) { + // The current revision is blank and the one before is also + // blank. It's just not our lucky day + $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text(); + } else { + if ( $onlyAuthor ) { + $reason = wfMessage( + 'excontentauthor', + '$1', + $onlyAuthor + )->inContentLanguage()->text(); + } else { + $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text(); + } + } + + if ( $reason == '-' ) { + // Allow these UI messages to be blanked out cleanly + return ''; + } + + // Max content length = max comment length - length of the comment (excl. $1) + $text = $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ); + + // Now replace the '$1' placeholder + $reason = str_replace( '$1', $text, $reason ); + + return $reason; + } + + /** + * Get the Content object that needs to be saved in order to undo all revisions + * between $undo and $undoafter. Revisions must belong to the same page, + * must exist and must not be deleted. + * + * @since 1.21 + * + * @param $current Revision The current text + * @param $undo Revision The revision to undo + * @param $undoafter Revision Must be an earlier revision than $undo + * + * @return mixed String on success, false on failure + */ + public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) { + $cur_content = $current->getContent(); + + if ( empty( $cur_content ) ) { + return false; // no page + } + + $undo_content = $undo->getContent(); + $undoafter_content = $undoafter->getContent(); + + $this->checkModelID( $cur_content->getModel() ); + $this->checkModelID( $undo_content->getModel() ); + $this->checkModelID( $undoafter_content->getModel() ); + + if ( $cur_content->equals( $undo_content ) ) { + // No use doing a merge if it's just a straight revert. + return $undoafter_content; + } + + $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content ); + + return $undone_content; + } + + /** + * Get parser options suitable for rendering the primary article wikitext + * + * @param IContextSource|User|string $context One of the following: + * - IContextSource: Use the User and the Language of the provided + * context + * - User: Use the provided User object and $wgLang for the language, + * so use an IContextSource object if possible. + * - 'canonical': Canonical options (anonymous user with default + * preferences and content language). + * + * @param IContextSource|User|string $context + * + * @throws MWException + * @return ParserOptions + */ + public function makeParserOptions( $context ) { + global $wgContLang; + + if ( $context instanceof IContextSource ) { + $options = ParserOptions::newFromContext( $context ); + } elseif ( $context instanceof User ) { // settings per user (even anons) + $options = ParserOptions::newFromUser( $context ); + } elseif ( $context === 'canonical' ) { // canonical settings + $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); + } else { + throw new MWException( "Bad context for parser options: $context" ); + } + + $options->enableLimitReport(); // show inclusion/loop reports + $options->setTidy( true ); // fix bad HTML + + return $options; + } + + /** + * Returns true for content models that support caching using the + * ParserCache mechanism. See WikiPage::isParserCacheUsed(). + * + * @since 1.21 + * + * @return bool + */ + public function isParserCacheSupported() { + return false; + } + + /** + * Returns true if this content model supports sections. + * + * This default implementation returns false. + * + * @return boolean whether sections are supported. + */ + public function supportsSections() { + return false; + } + + /** + * Logs a deprecation warning, visible if $wgDevelopmentWarnings, but only if + * self::$enableDeprecationWarnings is set to true. + * + * @param String $func The name of the deprecated function + * @param string $version The version since the method is deprecated. Usually 1.21 + * for ContentHandler related stuff. + * @param String|bool $component: Component to which the function belongs. + * If false, it is assumed the function is in MediaWiki core. + * + * @see ContentHandler::$enableDeprecationWarnings + * @see wfDeprecated + */ + public static function deprecated( $func, $version, $component = false ) { + if ( self::$enableDeprecationWarnings ) { + wfDeprecated( $func, $version, $component, 3 ); + } + } + + /** + * Call a legacy hook that uses text instead of Content objects. + * Will log a warning when a matching hook function is registered. + * If the textual representation of the content is changed by the + * hook function, a new Content object is constructed from the new + * text. + * + * @param $event String: event name + * @param $args Array: parameters passed to hook functions + * @param $warn bool: whether to log a warning. + * Default to self::$enableDeprecationWarnings. + * May be set to false for testing. + * + * @return Boolean True if no handler aborted the hook + * + * @see ContentHandler::$enableDeprecationWarnings + */ + public static function runLegacyHooks( $event, $args = array(), + $warn = null ) { + + if ( $warn === null ) { + $warn = self::$enableDeprecationWarnings; + } + + if ( !Hooks::isRegistered( $event ) ) { + return true; // nothing to do here + } + + if ( $warn ) { + // Log information about which handlers are registered for the legacy hook, + // so we can find and fix them. + + $handlers = Hooks::getHandlers( $event ); + $handlerInfo = array(); + + wfSuppressWarnings(); + + foreach ( $handlers as $handler ) { + $info = ''; + + if ( is_array( $handler ) ) { + if ( is_object( $handler[0] ) ) { + $info = get_class( $handler[0] ); + } else { + $info = $handler[0]; + } + + if ( isset( $handler[1] ) ) { + $info .= '::' . $handler[1]; + } + } else if ( is_object( $handler ) ) { + $info = get_class( $handler[0] ); + $info .= '::on' . $event; + } else { + $info = $handler; + } + + $handlerInfo[] = $info; + } + + wfRestoreWarnings(); + + wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " . implode(', ', $handlerInfo), 2 ); + } + + // convert Content objects to text + $contentObjects = array(); + $contentTexts = array(); + + foreach ( $args as $k => $v ) { + if ( $v instanceof Content ) { + /* @var Content $v */ + + $contentObjects[$k] = $v; + + $v = $v->serialize(); + $contentTexts[ $k ] = $v; + $args[ $k ] = $v; + } + } + + // call the hook functions + $ok = wfRunHooks( $event, $args ); + + // see if the hook changed the text + foreach ( $contentTexts as $k => $orig ) { + /* @var Content $content */ + + $modified = $args[ $k ]; + $content = $contentObjects[$k]; + + if ( $modified !== $orig ) { + // text was changed, create updated Content object + $content = $content->getContentHandler()->unserializeContent( $modified ); + } + + $args[ $k ] = $content; + } + + return $ok; + } +} + diff --git a/includes/content/CssContent.php b/includes/content/CssContent.php new file mode 100644 index 0000000000..29527cfbd4 --- /dev/null +++ b/includes/content/CssContent.php @@ -0,0 +1,58 @@ +getNativeData(); + $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts ); + + return new CssContent( $pst ); + } + + + protected function getHtml( ) { + $html = ""; + $html .= "
\n";
+		$html .= $this->getHighlightHtml( );
+		$html .= "\n
\n"; + + return $html; + } +} diff --git a/includes/content/CssContentHandler.php b/includes/content/CssContentHandler.php new file mode 100644 index 0000000000..e2199c41b2 --- /dev/null +++ b/includes/content/CssContentHandler.php @@ -0,0 +1,43 @@ +checkFormat( $format ); + + return new CssContent( $text ); + } + + public function makeEmptyContent() { + return new CssContent( '' ); + } + + /** + * Returns the english language, because CSS is english, and should be handled as such. + * + * @return Language wfGetLangObj( 'en' ) + * + * @see ContentHandler::getPageLanguage() + */ + public function getPageLanguage( Title $title, Content $content = null ) { + return wfGetLangObj( 'en' ); + } + + /** + * Returns the english language, because CSS is english, and should be handled as such. + * + * @return Language wfGetLangObj( 'en' ) + * + * @see ContentHandler::getPageViewLanguage() + */ + public function getPageViewLanguage( Title $title, Content $content = null ) { + return wfGetLangObj( 'en' ); + } +} \ No newline at end of file diff --git a/includes/content/JavaScriptContent.php b/includes/content/JavaScriptContent.php new file mode 100644 index 0000000000..770f233620 --- /dev/null +++ b/includes/content/JavaScriptContent.php @@ -0,0 +1,60 @@ +getNativeData(); + $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts ); + + return new JavaScriptContent( $pst ); + } + + + protected function getHtml( ) { + $html = ""; + $html .= "
\n";
+		$html .= $this->getHighlightHtml( );
+		$html .= "\n
\n"; + + return $html; + } +} \ No newline at end of file diff --git a/includes/content/JavaScriptContentHandler.php b/includes/content/JavaScriptContentHandler.php new file mode 100644 index 0000000000..8b080bf5c8 --- /dev/null +++ b/includes/content/JavaScriptContentHandler.php @@ -0,0 +1,45 @@ +checkFormat( $format ); + + return new JavaScriptContent( $text ); + } + + public function makeEmptyContent() { + return new JavaScriptContent( '' ); + } + + /** + * Returns the english language, because JS is english, and should be handled as such. + * + * @return Language wfGetLangObj( 'en' ) + * + * @see ContentHandler::getPageLanguage() + */ + public function getPageLanguage( Title $title, Content $content = null ) { + return wfGetLangObj( 'en' ); + } + + /** + * Returns the english language, because CSS is english, and should be handled as such. + * + * @return Language wfGetLangObj( 'en' ) + * + * @see ContentHandler::getPageViewLanguage() + */ + public function getPageViewLanguage( Title $title, Content $content = null ) { + return wfGetLangObj( 'en' ); + } +} \ No newline at end of file diff --git a/includes/content/MessageContent.php b/includes/content/MessageContent.php new file mode 100644 index 0000000000..d38355d107 --- /dev/null +++ b/includes/content/MessageContent.php @@ -0,0 +1,152 @@ +mMessage = wfMessage( $msg ); + } else { + $this->mMessage = clone $msg; + } + + if ( $params ) { + $this->mMessage = $this->mMessage->params( $params ); + } + } + + /** + * Returns the message as rendered HTML + * + * @return string The message text, parsed into html + */ + public function getHtml() { + return $this->mMessage->parse(); + } + + /** + * Returns the message as rendered HTML + * + * @return string The message text, parsed into html + */ + public function getWikitext() { + return $this->mMessage->text(); + } + + /** + * Returns the message object, with any parameters already substituted. + * + * @return Message The message object. + */ + public function getNativeData() { + //NOTE: Message objects are mutable. Cloning here makes MessageContent immutable. + return clone $this->mMessage; + } + + /** + * @see Content::getTextForSearchIndex + */ + public function getTextForSearchIndex() { + return $this->mMessage->plain(); + } + + /** + * @see Content::getWikitextForTransclusion + */ + public function getWikitextForTransclusion() { + return $this->getWikitext(); + } + + /** + * @see Content::getTextForSummary + */ + public function getTextForSummary( $maxlength = 250 ) { + return substr( $this->mMessage->plain(), 0, $maxlength ); + } + + /** + * @see Content::getSize + * + * @return int + */ + public function getSize() { + return strlen( $this->mMessage->plain() ); + } + + /** + * @see Content::copy + * + * @return Content. A copy of this object + */ + public function copy() { + // MessageContent is immutable (because getNativeData() returns a clone of the Message object) + return $this; + } + + /** + * @see Content::isCountable + * + * @return bool false + */ + public function isCountable( $hasLinks = null ) { + return false; + } + + /** + * @see Content::getParserOutput + * + * @return ParserOutput + */ + public function getParserOutput( + Title $title, $revId = null, + ParserOptions $options = null, $generateHtml = true + ) { + + if ( $generateHtml ) { + $html = $this->getHtml(); + } else { + $html = ''; + } + + $po = new ParserOutput( $html ); + return $po; + } +} \ No newline at end of file diff --git a/includes/content/TextContent.php b/includes/content/TextContent.php new file mode 100644 index 0000000000..8e832ececa --- /dev/null +++ b/includes/content/TextContent.php @@ -0,0 +1,232 @@ +mText = $text; + } + + public function copy() { + return $this; # NOTE: this is ok since TextContent are immutable. + } + + public function getTextForSummary( $maxlength = 250 ) { + global $wgContLang; + + $text = $this->getNativeData(); + + $truncatedtext = $wgContLang->truncate( + preg_replace( "/[\n\r]/", ' ', $text ), + max( 0, $maxlength ) ); + + return $truncatedtext; + } + + /** + * returns the text's size in bytes. + * + * @return int The size + */ + public function getSize( ) { + $text = $this->getNativeData( ); + return strlen( $text ); + } + + /** + * Returns true if this content is not a redirect, and $wgArticleCountMethod + * is "any". + * + * @param $hasLinks Bool: if it is known whether this content contains links, + * provide this information here, to avoid redundant parsing to find out. + * + * @return bool True if the content is countable + */ + public function isCountable( $hasLinks = null ) { + global $wgArticleCountMethod; + + if ( $this->isRedirect( ) ) { + return false; + } + + if ( $wgArticleCountMethod === 'any' ) { + return true; + } + + return false; + } + + /** + * Returns the text represented by this Content object, as a string. + * + * @param the raw text + */ + public function getNativeData( ) { + $text = $this->mText; + return $text; + } + + /** + * Returns the text represented by this Content object, as a string. + * + * @param the raw text + */ + public function getTextForSearchIndex( ) { + return $this->getNativeData(); + } + + /** + * Returns the text represented by this Content object, as a string. + * + * @param the raw text + */ + public function getWikitextForTransclusion( ) { + return $this->getNativeData(); + } + + /** + * Returns a Content object with pre-save transformations applied. + * This implementation just trims trailing whitespace. + * + * @param $title Title + * @param $user User + * @param $popts ParserOptions + * @return Content + */ + public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) { + $text = $this->getNativeData(); + $pst = rtrim( $text ); + + return ( $text === $pst ) ? $this : new WikitextContent( $pst ); + } + + /** + * Diff this content object with another content object.. + * + * @since 1.21diff + * + * @param $that Content the other content object to compare this content object to + * @param $lang Language the language object to use for text segmentation. + * If not given, $wgContentLang is used. + * + * @return DiffResult a diff representing the changes that would have to be + * made to this content object to make it equal to $that. + */ + public function diff( Content $that, Language $lang = null ) { + global $wgContLang; + + $this->checkModelID( $that->getModel() ); + + # @todo: could implement this in DifferenceEngine and just delegate here? + + if ( !$lang ) $lang = $wgContLang; + + $otext = $this->getNativeData(); + $ntext = $this->getNativeData(); + + # Note: Use native PHP diff, external engines don't give us abstract output + $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); + $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); + + $diff = new Diff( $ota, $nta ); + return $diff; + } + + + /** + * Returns a generic ParserOutput object, wrapping the HTML returned by + * getHtml(). + * + * @param $title Title Context title for parsing + * @param $revId int|null Revision ID (for {{REVISIONID}}) + * @param $options ParserOptions|null Parser options + * @param $generateHtml bool Whether or not to generate HTML + * + * @return ParserOutput representing the HTML form of the text + */ + public function getParserOutput( Title $title, + $revId = null, + ParserOptions $options = null, $generateHtml = true + ) { + global $wgParser, $wgTextModelsToParse; + + if ( !$options ) { + //NOTE: use canonical options per default to produce cacheable output + $options = $this->getContentHandler()->makeParserOptions( 'canonical' ); + } + + if ( in_array( $this->getModel(), $wgTextModelsToParse ) ) { + // parse just to get links etc into the database + $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId ); + } else { + $po = new ParserOutput(); + } + + if ( $generateHtml ) { + $html = $this->getHtml(); + } else { + $html = ''; + } + + $po->setText( $html ); + return $po; + } + + /** + * Generates an HTML version of the content, for display. Used by + * getParserOutput() to construct a ParserOutput object. + * + * This default implementation just calls getHighlightHtml(). Content + * models that have another mapping to HTML (as is the case for markup + * languages like wikitext) should override this method to generate the + * appropriate HTML. + * + * @return string An HTML representation of the content + */ + protected function getHtml() { + return $this->getHighlightHtml(); + } + + /** + * Generates a syntax-highlighted version of the content, as HTML. + * Used by the default implementation of getHtml(). + * + * @return string an HTML representation of the content's markup + */ + protected function getHighlightHtml( ) { + # TODO: make Highlighter interface, use highlighter here, if available + return htmlspecialchars( $this->getNativeData() ); + } +} diff --git a/includes/content/TextContentHandler.php b/includes/content/TextContentHandler.php new file mode 100644 index 0000000000..9dff67edd4 --- /dev/null +++ b/includes/content/TextContentHandler.php @@ -0,0 +1,90 @@ +checkFormat( $format ); + return $content->getNativeData(); + } + + /** + * Attempts to merge differences between three versions. Returns a new + * Content object for a clean merge and false for failure or a conflict. + * + * All three Content objects passed as parameters must have the same + * content model. + * + * This text-based implementation uses wfMerge(). + * + * @param $oldContent \Content|string String + * @param $myContent \Content|string String + * @param $yourContent \Content|string String + * + * @return Content|Bool + */ + public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) { + $this->checkModelID( $oldContent->getModel() ); + $this->checkModelID( $myContent->getModel() ); + $this->checkModelID( $yourContent->getModel() ); + + $format = $this->getDefaultFormat(); + + $old = $this->serializeContent( $oldContent, $format ); + $mine = $this->serializeContent( $myContent, $format ); + $yours = $this->serializeContent( $yourContent, $format ); + + $ok = wfMerge( $old, $mine, $yours, $result ); + + if ( !$ok ) { + return false; + } + + if ( !$result ) { + return $this->makeEmptyContent(); + } + + $mergedContent = $this->unserializeContent( $result, $format ); + return $mergedContent; + } + + /** + * Unserializes a Content object of the type supported by this ContentHandler. + * + * @since 1.21 + * + * @param $text string serialized form of the content + * @param $format null|String the format used for serialization + * + * @return Content the TextContent object wrapping $text + */ + public function unserializeContent( $text, $format = null ) { + $this->checkFormat( $format ); + + return new TextContent( $text ); + } + + /** + * Creates an empty TextContent object. + * + * @since 1.21 + * + * @return Content + */ + public function makeEmptyContent() { + return new TextContent( '' ); + } +} \ No newline at end of file diff --git a/includes/content/WikitextContent.php b/includes/content/WikitextContent.php new file mode 100644 index 0000000000..8f1381fdce --- /dev/null +++ b/includes/content/WikitextContent.php @@ -0,0 +1,314 @@ +getNativeData(); + $sect = $wgParser->getSection( $text, $section, false ); + + if ( $sect === false ) { + return false; + } else { + return new WikitextContent( $sect ); + } + } + + /** + * @see Content::replaceSection() + */ + public function replaceSection( $section, Content $with, $sectionTitle = '' ) { + wfProfileIn( __METHOD__ ); + + $myModelId = $this->getModel(); + $sectionModelId = $with->getModel(); + + if ( $sectionModelId != $myModelId ) { + throw new MWException( "Incompatible content model for section: " . + "document uses $myModelId but " . + "section uses $sectionModelId." ); + } + + $oldtext = $this->getNativeData(); + $text = $with->getNativeData(); + + if ( $section === '' ) { + return $with; # XXX: copy first? + } if ( $section == 'new' ) { + # Inserting a new section + $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' ) + ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : ''; + if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) { + $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 ); + } + + $newContent = new WikitextContent( $text ); + + wfProfileOut( __METHOD__ ); + return $newContent; + } + + /** + * Returns a new WikitextContent object with the given section heading + * prepended. + * + * @param $header string + * @return Content + */ + public function addSectionHeader( $header ) { + $text = wfMessage( 'newsectionheaderdefaultlevel' ) + ->rawParams( $header )->inContentLanguage()->text(); + $text .= "\n\n"; + $text .= $this->getNativeData(); + + return new WikitextContent( $text ); + } + + /** + * Returns a Content object with pre-save transformations applied using + * Parser::preSaveTransform(). + * + * @param $title Title + * @param $user User + * @param $popts ParserOptions + * @return Content + */ + public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) { + global $wgParser; + + $text = $this->getNativeData(); + $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts ); + rtrim( $pst ); + + return ( $text === $pst ) ? $this : new WikitextContent( $pst ); + } + + /** + * Returns a Content object with preload transformations applied (or this + * object if no transformations apply). + * + * @param $title Title + * @param $popts ParserOptions + * @return Content + */ + public function preloadTransform( Title $title, ParserOptions $popts ) { + global $wgParser; + + $text = $this->getNativeData(); + $plt = $wgParser->getPreloadText( $text, $title, $popts ); + + return new WikitextContent( $plt ); + } + + /** + * Implement redirect extraction for wikitext. + * + * @return null|Title + * + * @note: migrated here from Title::newFromRedirectInternal() + * + * @see Content::getRedirectTarget + * @see AbstractContent::getRedirectTarget + */ + public function getRedirectTarget() { + global $wgMaxRedirects; + if ( $wgMaxRedirects < 1 ) { + // redirects are disabled, so quit early + return null; + } + $redir = MagicWord::get( 'redirect' ); + $text = trim( $this->getNativeData() ); + if ( $redir->matchStartAndRemove( $text ) ) { + // Extract the first link and see if it's usable + // Ensure that it really does come directly after #REDIRECT + // Some older redirects included a colon, so don't freak about that! + $m = array(); + if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) { + // Strip preceding colon used to "escape" categories, etc. + // and URL-decode links + if ( strpos( $m[1], '%' ) !== false ) { + // Match behavior of inline link parsing here; + $m[1] = rawurldecode( ltrim( $m[1], ':' ) ); + } + $title = Title::newFromText( $m[1] ); + // If the title is a redirect to bad special pages or is invalid, return null + if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) { + return null; + } + return $title; + } + } + return null; + } + + /** + * @see Content::updateRedirect() + * + * This implementation replaces the first link on the page with the given new target + * if this Content object is a redirect. Otherwise, this method returns $this. + * + * @since 1.21 + * + * @param Title $target + * + * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect) + */ + public function updateRedirect( Title $target ) { + if ( !$this->isRedirect() ) { + return $this; + } + + # Fix the text + # Remember that redirect pages can have categories, templates, etc., + # so the regex has to be fairly general + $newText = preg_replace( '/ \[ \[ [^\]]* \] \] /x', + '[[' . $target->getFullText() . ']]', + $this->getNativeData(), 1 ); + + return new WikitextContent( $newText ); + } + + /** + * Returns true if this content is not a redirect, and this content's text + * is countable according to the criteria defined by $wgArticleCountMethod. + * + * @param $hasLinks Bool if it is known whether this content contains + * links, provide this information here, to avoid redundant parsing to + * find out. + * @param $title null|\Title + * + * @internal param \IContextSource $context context for parsing if necessary + * + * @return bool True if the content is countable + */ + public function isCountable( $hasLinks = null, Title $title = null ) { + global $wgArticleCountMethod; + + if ( $this->isRedirect( ) ) { + return false; + } + + $text = $this->getNativeData(); + + switch ( $wgArticleCountMethod ) { + case 'any': + return true; + case 'comma': + return strpos( $text, ',' ) !== false; + case 'link': + if ( $hasLinks === null ) { # not known, find out + if ( !$title ) { + $context = RequestContext::getMain(); + $title = $context->getTitle(); + } + + $po = $this->getParserOutput( $title, null, null, false ); + $links = $po->getLinks(); + $hasLinks = !empty( $links ); + } + + return $hasLinks; + } + + return false; + } + + public function getTextForSummary( $maxlength = 250 ) { + $truncatedtext = parent::getTextForSummary( $maxlength ); + + # clean up unfinished links + # XXX: make this optional? wasn't there in autosummary, but required for + # deletion summary. + $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext ); + + return $truncatedtext; + } + + /** + * Returns a ParserOutput object resulting from parsing the content's text + * using $wgParser. + * + * @since 1.21 + * + * @param $content Content the content to render + * @param $title \Title + * @param $revId null + * @param $options null|ParserOptions + * @param $generateHtml bool + * + * @internal param \IContextSource|null $context + * @return ParserOutput representing the HTML form of the text + */ + public function getParserOutput( Title $title, + $revId = null, + ParserOptions $options = null, $generateHtml = true + ) { + global $wgParser; + + if ( !$options ) { + //NOTE: use canonical options per default to produce cacheable output + $options = $this->getContentHandler()->makeParserOptions( 'canonical' ); + } + + $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId ); + return $po; + } + + protected function getHtml() { + throw new MWException( + "getHtml() not implemented for wikitext. " + . "Use getParserOutput()->getText()." + ); + } + + /** + * @see Content::matchMagicWord() + * + * This implementation calls $word->match() on the this TextContent object's text. + * + * @param MagicWord $word + * + * @return bool whether this Content object matches the given magic word. + */ + public function matchMagicWord( MagicWord $word ) { + return $word->match( $this->getNativeData() ); + } +} diff --git a/includes/content/WikitextContentHandler.php b/includes/content/WikitextContentHandler.php new file mode 100644 index 0000000000..c6ac2ba879 --- /dev/null +++ b/includes/content/WikitextContentHandler.php @@ -0,0 +1,63 @@ +checkFormat( $format ); + + return new WikitextContent( $text ); + } + + /** + * @see ContentHandler::makeEmptyContent + * + * @return Content + */ + public function makeEmptyContent() { + return new WikitextContent( '' ); + } + + + /** + * Returns a WikitextContent object representing a redirect to the given destination page. + * + * @see ContentHandler::makeRedirectContent + * + * @param Title $destination the page to redirect to. + * + * @return Content + */ + public function makeRedirectContent( Title $destination ) { + $mwRedir = MagicWord::get( 'redirect' ); + $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $destination->getPrefixedText() . "]]\n"; + + return new WikitextContent( $redirectText ); + } + + /** + * Returns true because wikitext supports sections. + * + * @return boolean whether sections are supported. + */ + public function supportsSections() { + return true; + } + + /** + * Returns true, because wikitext supports caching using the + * ParserCache mechanism. + * + * @since 1.21 + * @return bool + */ + public function isParserCacheSupported() { + return true; + } +} \ No newline at end of file diff --git a/includes/context/ContextSource.php b/includes/context/ContextSource.php index 45bd6fffb4..d5a6d15ad9 100644 --- a/includes/context/ContextSource.php +++ b/includes/context/ContextSource.php @@ -165,6 +165,4 @@ abstract class ContextSource implements IContextSource { $args = func_get_args(); return call_user_func_array( array( $this->getContext(), 'msg' ), $args ); } - } - diff --git a/includes/context/DerivativeContext.php b/includes/context/DerivativeContext.php index 5adf3621b3..a4e327267f 100644 --- a/includes/context/DerivativeContext.php +++ b/includes/context/DerivativeContext.php @@ -222,6 +222,7 @@ class DerivativeContext extends ContextSource { * Set the Language object * * @param $l Mixed Language instance or language code + * @throws MWException * @since 1.19 */ public function setLanguage( $l ) { diff --git a/includes/context/RequestContext.php b/includes/context/RequestContext.php index 1ffbc08ce2..cd2bf556f8 100644 --- a/includes/context/RequestContext.php +++ b/includes/context/RequestContext.php @@ -148,6 +148,7 @@ class RequestContext implements IContextSource { * canUseWikiPage() to check whether this method can be called safely. * * @since 1.19 + * @throws MWException * @return WikiPage */ public function getWikiPage() { @@ -237,6 +238,7 @@ class RequestContext implements IContextSource { * Set the Language object * * @param $l Mixed Language instance or language code + * @throws MWException * @since 1.19 */ public function setLanguage( $l ) { @@ -305,7 +307,7 @@ class RequestContext implements IContextSource { public function getSkin() { if ( $this->skin === null ) { wfProfileIn( __METHOD__ . '-createskin' ); - + $skin = null; wfRunHooks( 'RequestContextCreateSkin', array( $this, &$skin ) ); @@ -395,4 +397,3 @@ class RequestContext implements IContextSource { } } - diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index 4e43642fae..4ff7913bd2 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -87,18 +87,17 @@ class CloneDatabase { * Clone the table structure */ public function cloneTableStructure() { - foreach( $this->tablesToClone as $tbl ) { # Clean up from previous aborted run. So that table escaping # works correctly across DB engines, we need to change the pre- # fix back and forth so tableName() works right. - + self::changePrefix( $this->oldTablePrefix ); $oldTableName = $this->db->tableName( $tbl, 'raw' ); - + self::changePrefix( $this->newTablePrefix ); $newTableName = $this->db->tableName( $tbl, 'raw' ); - + if( $this->dropCurrentTables && !in_array( $this->db->getType(), array( 'postgres', 'oracle' ) ) ) { $this->db->dropTable( $tbl, __METHOD__ ); wfDebug( __METHOD__." dropping {$newTableName}\n", true); @@ -108,9 +107,7 @@ class CloneDatabase { # Create new table wfDebug( __METHOD__." duplicating $oldTableName to $newTableName\n", true ); $this->db->duplicateTableStructure( $oldTableName, $newTableName, $this->useTemporaryTables ); - } - } /** diff --git a/includes/db/Database.php b/includes/db/Database.php index c082cc9811..48aac9dbd8 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -266,6 +266,14 @@ abstract class DatabaseBase implements DatabaseType { */ private $mTrxDoneWrites = false; + /** + * Record if the current transaction was started implicitly due to DBO_TRX being set. + * + * @var Bool + * @see DatabaseBase::mTrxLevel + */ + private $mTrxAutomatic = false; + # ------------------------------------------------------------------------------ # Accessors # ------------------------------------------------------------------------------ @@ -745,8 +753,14 @@ abstract class DatabaseBase implements DatabaseType { $this->mOpened = false; if ( $this->mConn ) { if ( $this->trxLevel() ) { - $this->commit( __METHOD__ ); + if ( !$this->mTrxAutomatic ) { + wfWarn( "Transaction still in progress (from {$this->mTrxFname}), " . + " performing implicit commit before closing connection!" ); + } + + $this->commit( __METHOD__, 'flush' ); } + $ret = $this->closeConnection(); $this->mConn = false; return $ret; @@ -764,6 +778,7 @@ abstract class DatabaseBase implements DatabaseType { /** * @param $error String: fallback error message, used if none is given by DB + * @throws DBConnectionError */ function reportConnectionError( $error = 'Unknown error' ) { $myError = $this->lastError(); @@ -813,9 +828,9 @@ abstract class DatabaseBase implements DatabaseType { * comment (you can use __METHOD__ or add some extra info) * @param $tempIgnore Boolean: Whether to avoid throwing an exception on errors... * maybe best to catch the exception instead? + * @throws MWException * @return boolean|ResultWrapper. 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 */ public function query( $sql, $fname = '', $tempIgnore = false ) { $isMaster = !is_null( $this->getLBInfo( 'master' ) ); @@ -869,6 +884,7 @@ abstract class DatabaseBase implements DatabaseType { wfDebug("Implicit transaction start.\n"); } $this->begin( __METHOD__ . " ($fname)" ); + $this->mTrxAutomatic = true; } } @@ -903,6 +919,7 @@ abstract class DatabaseBase implements DatabaseType { if ( false === $ret && $this->wasErrorReissuable() ) { # Transaction is gone, like it or not $this->mTrxLevel = 0; + $this->trxIdleCallbacks = array(); // cancel wfDebug( "Connection lost, reconnecting...\n" ); if ( $this->ping() ) { @@ -942,6 +959,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $sql String * @param $fname String * @param $tempIgnore Boolean + * @throws DBQueryError */ public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { # Ignore errors during error handling to avoid infinite recursion @@ -1028,6 +1046,7 @@ abstract class DatabaseBase implements DatabaseType { * while we're doing this. * * @param $matches Array + * @throws DBUnexpectedError * @return String */ protected function fillPreparedArg( $matches ) { @@ -1743,7 +1762,7 @@ abstract class DatabaseBase implements DatabaseType { /** * Makes an encoded list of strings from an array * @param $a Array containing the data - * @param $mode int Constant + * @param int $mode Constant * - LIST_COMMA: comma separated, no field names * - LIST_AND: ANDed WHERE clause (without the WHERE). See * the documentation for $conds in DatabaseBase::select(). @@ -1751,6 +1770,7 @@ abstract class DatabaseBase implements DatabaseType { * - LIST_SET: comma separated with field names, like a SET clause * - LIST_NAMES: comma separated field names * + * @throws MWException|DBUnexpectedError * @return string */ public function makeList( $a, $mode = LIST_COMMA ) { @@ -2452,6 +2472,7 @@ abstract class DatabaseBase implements DatabaseType { * ANDed together in the WHERE clause * @param $fname String: Calling function name (use __METHOD__) for * logs/profiling + * @throws DBUnexpectedError */ public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseBase::deleteJoin' ) @@ -2517,6 +2538,7 @@ abstract class DatabaseBase implements DatabaseType { * the format. Use $conds == "*" to delete all rows * @param $fname String name of the calling function * + * @throws DBUnexpectedError * @return bool */ public function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) { @@ -2615,6 +2637,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $limit Integer the SQL limit * @param $offset Integer|bool the SQL offset (default false) * + * @throws DBUnexpectedError * @return string */ public function limitResult( $sql, $limit, $offset = false ) { @@ -2904,8 +2927,8 @@ abstract class DatabaseBase implements DatabaseType { * Note that when the DBO_TRX flag is set (which is usually the case for web requests, but not for maintenance scripts), * any previous database query will have started a transaction automatically. * - * Nesting of transactions is not supported. Attempts to nest transactions will cause warnings if DBO_TRX is not set - * or the extsting transaction contained write operations. + * Nesting of transactions is not supported. Attempts to nest transactions will cause a warning, unless the current + * transaction was started automatically because of the DBO_TRX flag. * * @param $fname string */ @@ -2913,21 +2936,18 @@ abstract class DatabaseBase implements DatabaseType { global $wgDebugDBTransactions; if ( $this->mTrxLevel ) { // implicit commit - if ( $this->mTrxDoneWrites || ( $this->mFlags & DBO_TRX ) === 0 ) { - // In theory, we should always warn about nesting BEGIN statements. - // However, it is sometimes hard to avoid so we only warn if: - // - // a) the transaction has done writes. This gives warnings about bad transactions - // that could cause partial writes but not about read queries seeing more - // than one DB snapshot (when in REPEATABLE-READ) due to nested BEGINs. - // - // b) the DBO_TRX flag is not set. Explicit transactions should always be properly - // started and comitted. + if ( !$this->mTrxAutomatic ) { + // We want to warn about inadvertently nested begin/commit pairs, but not about auto-committing + // implicit transactions that were started by query() because DBO_TRX was set. + wfWarn( "$fname: Transaction already in progress (from {$this->mTrxFname}), " . " performing implicit commit!" ); } else { - if ( $wgDebugDBTransactions ) { - wfDebug( "$fname: Transaction already in progress (from {$this->mTrxFname}), " . + // if the transaction was automatic and has done write operations, + // log it if $wgDebugDBTransactions is enabled. + + if ( $this->mTrxDoneWrites && $wgDebugDBTransactions ) { + wfDebug( "$fname: Automatic transaction with writes in progress (from {$this->mTrxFname}), " . " performing implicit commit!\n" ); } } @@ -2939,6 +2959,7 @@ abstract class DatabaseBase implements DatabaseType { $this->doBegin( $fname ); $this->mTrxFname = $fname; $this->mTrxDoneWrites = false; + $this->mTrxAutomatic = false; } /** @@ -2959,11 +2980,26 @@ abstract class DatabaseBase implements DatabaseType { * Nesting of transactions is not supported. * * @param $fname string - */ - final public function commit( $fname = 'DatabaseBase::commit' ) { - if ( !$this->mTrxLevel ) { - wfWarn( "$fname: No transaction to commit, something got out of sync!" ); + * @param $flush String Flush flag, set to 'flush' to disable warnings about explicitly committing implicit + * transactions, or calling commit when no transaction is in progress. + * This will silently break any ongoing explicit transaction. Only set the flush flag if you are sure + * that it is safe to ignore these warnings in your context. + */ + final public function commit( $fname = 'DatabaseBase::commit', $flush = '' ) { + if ( $flush != 'flush' ) { + if ( !$this->mTrxLevel ) { + wfWarn( "$fname: No transaction to commit, something got out of sync!" ); + } elseif( $this->mTrxAutomatic ) { + wfWarn( "$fname: Explicit commit of implicit transaction. Something may be out of sync!" ); + } + } else { + if ( !$this->mTrxLevel ) { + return; // nothing to do + } elseif( !$this->mTrxAutomatic ) { + wfWarn( "$fname: Flushing an explicit transaction, getting out of sync!" ); + } } + $this->doCommit( $fname ); $this->runOnTransactionIdleCallbacks(); } @@ -3022,6 +3058,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $newName String: name of table to be created * @param $temporary Boolean: whether the new table should be temporary * @param $fname String: calling function name + * @throws MWException * @return Boolean: true if operation was successful */ public function duplicateTableStructure( $oldName, $newName, $temporary = false, @@ -3036,6 +3073,7 @@ abstract class DatabaseBase implements DatabaseType { * * @param $prefix string Only show tables with this prefix, e.g. mw_ * @param $fname String: calling function name + * @throws MWException */ function listTables( $prefix = null, $fname = 'DatabaseBase::listTables' ) { throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' ); @@ -3179,10 +3217,11 @@ abstract class DatabaseBase implements DatabaseType { * on object's error ignore settings). * * @param $filename String: File name to open - * @param $lineCallback Callback: Optional function called before reading each line - * @param $resultCallback Callback: Optional function called for each MySQL result - * @param $fname String: Calling function name or false if name should be + * @param bool|callable $lineCallback Optional function called before reading each line + * @param bool|callable $resultCallback Optional function called for each MySQL result + * @param bool|string $fname Calling function name or false if name should be * generated dynamically using $filename + * @throws MWException * @return bool|string */ public function sourceFile( diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php index f1f6dfca58..62c90d174e 100644 --- a/includes/db/DatabaseIbm_db2.php +++ b/includes/db/DatabaseIbm_db2.php @@ -496,6 +496,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * @param $user String * @param $password String * @param $dbName String: database name + * @throws DBConnectionError * @return DatabaseBase a fresh connection */ public function open( $server, $user, $password, $dbName ) { @@ -622,6 +623,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * The DBMS-dependent part of query() * @param $sql String: SQL query. + * @throws DBUnexpectedError * @return object Result object for fetch functions or false on failure */ protected function doQuery( $sql ) { @@ -854,6 +856,9 @@ class DatabaseIbm_db2 extends DatabaseBase { * LIST_SET - comma separated with field names, like a SET clause * LIST_NAMES - comma separated field names * LIST_SET_PREPARED - like LIST_SET, except with ? tokens as values + * @param array $a + * @param int $mode + * @throws DBUnexpectedError * @return string */ function makeList( $a, $mode = LIST_COMMA ) { @@ -891,7 +896,8 @@ class DatabaseIbm_db2 extends DatabaseBase { * * @param $sql string SQL query we will append the limit too * @param $limit integer the SQL limit - * @param $offset integer the SQL offset (default false) + * @param bool|int $offset SQL offset (default false) + * @throws DBUnexpectedError * @return string */ public function limitResult( $sql, $limit, $offset=false ) { @@ -1173,6 +1179,10 @@ class DatabaseIbm_db2 extends DatabaseBase { * DELETE query wrapper * * Use $conds == "*" to delete all rows + * @param array $table + * @param array|string $conds + * @param string $fname + * @throws DBUnexpectedError * @return bool|\ResultWrapper */ public function delete( $table, $conds, $fname = 'DatabaseIbm_db2::delete' ) { @@ -1247,6 +1257,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Frees memory associated with a statement resource * @param $res Object: statement resource to free + * @throws DBUnexpectedError * @return Boolean success or failure */ public function freeResult( $res ) { diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 914ab4089a..ff67f47273 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -61,6 +61,11 @@ class DatabaseMssql extends DatabaseBase { /** * Usually aborts on failure + * @param String $server + * @param String $user + * @param String $password + * @param String $dbName + * @throws DBConnectionError * @return bool|DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { @@ -380,6 +385,11 @@ class DatabaseMssql extends DatabaseBase { * * Usually aborts on failure * If errors are explicitly ignored, returns success + * @param String $table + * @param Array $arrToInsert + * @param string $fname + * @param array $options + * @throws DBQueryError * @return bool */ function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) { @@ -510,6 +520,14 @@ class DatabaseMssql extends DatabaseBase { * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes() * $conds may be "*" to copy the whole table * srcTable may be an array of tables. + * @param string $destTable + * @param array|string $srcTable + * @param array $varMap + * @param array $conds + * @param string $fname + * @param array $insertOptions + * @param array $selectOptions + * @throws DBQueryError * @return null|\ResultWrapper */ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect', @@ -720,6 +738,8 @@ class DatabaseMssql extends DatabaseBase { * Escapes a identifier for use inm SQL. * Throws an exception if it is invalid. * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx + * @param $identifier + * @throws MWException * @return string */ private function escapeIdentifier( $identifier ) { diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php index 7f389da904..b509302fc5 100644 --- a/includes/db/DatabaseMysql.php +++ b/includes/db/DatabaseMysql.php @@ -805,7 +805,8 @@ class DatabaseMysql extends DatabaseBase { * @param $delVar string * @param $joinVar string * @param $conds array|string - * @param $fname bool + * @param bool|string $fname bool + * @throws DBUnexpectedError * @return bool|ResultWrapper */ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseBase::deleteJoin' ) { @@ -951,13 +952,6 @@ class DatabaseMysql extends DatabaseBase { } -/** - * Legacy support: Database == DatabaseMysql - * - * @deprecated in 1.16 - */ -class Database extends DatabaseMysql {} - /** * Utility class. * @ingroup Database diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 7d8884fb6b..aa4da0fedf 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -241,6 +241,11 @@ class DatabaseOracle extends DatabaseBase { /** * Usually aborts on failure + * @param string $server + * @param string $user + * @param string $password + * @param string $dbName + * @throws DBConnectionError * @return DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index 457bf384a8..419488e595 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -325,6 +325,11 @@ class DatabasePostgres extends DatabaseBase { /** * Usually aborts on failure + * @param string $server + * @param string $user + * @param string $password + * @param string $dbName + * @throws DBConnectionError * @return DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index f1e553d736..1125d4f37a 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -79,11 +79,12 @@ class DatabaseSqlite extends DatabaseBase { /** Open an SQLite database and return a resource handle to it * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases * - * @param $server - * @param $user - * @param $pass - * @param $dbName + * @param string $server + * @param string $user + * @param string $pass + * @param string $dbName * + * @throws DBConnectionError * @return PDO */ function open( $server, $user, $pass, $dbName ) { @@ -103,6 +104,7 @@ class DatabaseSqlite extends DatabaseBase { * * @param $fileName string * + * @throws DBConnectionError * @return PDO|bool SQL connection or false if failed */ function openFile( $fileName ) { diff --git a/includes/db/IORMTable.php b/includes/db/IORMTable.php index 99413f99b9..9693789f02 100644 --- a/includes/db/IORMTable.php +++ b/includes/db/IORMTable.php @@ -298,6 +298,72 @@ interface IORMTable { */ public function setReadDb( $db ); + + /** + * Get the ID of the any foreign wiki to use as a target for database operations + * + * @since 1.20 + * + * @return String|bool The target wiki, in a form that LBFactory understands (or false if the local wiki is used) + */ + public function getTargetWiki(); + + /** + * Set the ID of the any foreign wiki to use as a target for database operations + * + * @param String|bool $wiki The target wiki, in a form that LBFactory understands (or false if the local wiki shall be used) + * + * @since 1.20 + */ + public function setTargetWiki( $wiki ); + + /** + * Get the database type used for read operations. + * This is to be used instead of wfGetDB. + * + * @see LoadBalancer::getConnection + * + * @since 1.20 + * + * @return DatabaseBase The database object + */ + public function getReadDbConnection(); + + /** + * Get the database type used for read operations. + * This is to be used instead of wfGetDB. + * + * @see LoadBalancer::getConnection + * + * @since 1.20 + * + * @return DatabaseBase The database object + */ + public function getWriteDbConnection(); + + /** + * Get the database type used for read operations. + * + * @see wfGetLB + * + * @since 1.20 + * + * @return LoadBalancer The database load balancer object + */ + public function getLoadBalancer(); + + /** + * Releases the lease on the given database connection. This is useful mainly + * for connections to a foreign wiki. It does nothing for connections to the local wiki. + * + * @see LoadBalancer::reuseConnection + * + * @param DatabaseBase $db the database + * + * @since 1.20 + */ + public function releaseConnection( DatabaseBase $db ); + /** * Update the records matching the provided conditions by * setting the fields that are keys in the $values param to diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php index 6008813ba1..9b468a7f7f 100644 --- a/includes/db/LBFactory_Multi.php +++ b/includes/db/LBFactory_Multi.php @@ -70,6 +70,7 @@ class LBFactory_Multi extends LBFactory { /** * @param $conf array + * @throws MWException */ function __construct( $conf ) { $this->chronProt = new ChronologyProtector; @@ -153,8 +154,9 @@ class LBFactory_Multi extends LBFactory { } /** - * @param $cluster - * @param $wiki + * @param String $cluster + * @param bool $wiki + * @throws MWException * @return LoadBalancer */ function newExternalLB( $cluster, $wiki = false ) { diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php index 195d4ec417..46d24fc0d4 100644 --- a/includes/db/LoadBalancer.php +++ b/includes/db/LoadBalancer.php @@ -41,6 +41,7 @@ class LoadBalancer { * servers Required. Array of server info structures. * masterWaitTimeout Replication lag wait timeout * loadMonitor Name of a class used to fetch server lag and load. + * @throws MWException */ function __construct( $params ) { if ( !isset( $params['servers'] ) ) { @@ -197,6 +198,7 @@ class LoadBalancer { * Side effect: opens connections to databases * @param $group bool * @param $wiki bool + * @throws MWException * @return bool|int|string */ function getReaderIndex( $group = false, $wiki = false ) { @@ -452,8 +454,9 @@ class LoadBalancer { * * @param $i Integer: server index * @param $groups Array: query groups - * @param $wiki String: wiki ID + * @param bool|string $wiki Wiki ID * + * @throws MWException * @return DatabaseBase */ public function &getConnection( $i, $groups = array(), $wiki = false ) { @@ -520,6 +523,7 @@ class LoadBalancer { * the same number of times as getConnection() to work. * * @param DatabaseBase $conn + * @throws MWException */ public function reuseConnection( $conn ) { $serverIndex = $conn->getLBInfo('serverIndex'); @@ -692,6 +696,7 @@ class LoadBalancer { * * @param $server * @param $dbNameOverride bool + * @throws MWException * @return DatabaseBase */ function reallyOpenConnection( $server, $dbNameOverride = false ) { @@ -902,7 +907,9 @@ class LoadBalancer { foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { foreach ( $conns3 as $conn ) { - $conn->commit( __METHOD__ ); + if ( $conn->trxLevel() ) { + $conn->commit( __METHOD__, 'flush' ); + } } } } @@ -920,7 +927,7 @@ class LoadBalancer { } foreach ( $conns2[$masterIndex] as $conn ) { if ( $conn->trxLevel() && $conn->doneWrites() ) { - $conn->commit( __METHOD__ ); + $conn->commit( __METHOD__, 'flush' ); } } } diff --git a/includes/db/ORMTable.php b/includes/db/ORMTable.php index a77074ffca..e3a3434005 100644 --- a/includes/db/ORMTable.php +++ b/includes/db/ORMTable.php @@ -47,7 +47,7 @@ abstract class ORMTable implements IORMTable { protected static $instanceCache = array(); /** - * The database connection to use for read operations. + * ID of the database connection to use for read operations. * Can be changed via @see setReadDb. * * @since 1.20 @@ -55,6 +55,15 @@ abstract class ORMTable implements IORMTable { */ protected $readDb = DB_SLAVE; + /** + * The ID of any foreign wiki to use as a target for database operations, + * or false to use the local wiki. + * + * @since 1.20 + * @var String|bool + */ + protected $wiki = false; + /** * Returns a list of default field values. * field name => field value @@ -145,13 +154,17 @@ abstract class ORMTable implements IORMTable { $fields = (array)$fields; } - return wfGetDB( $this->getReadDb() )->select( + $dbr = $this->getReadDbConnection(); + $result = $dbr->select( $this->getName(), $this->getPrefixedFields( $fields ), $this->getPrefixedValues( $conditions ), is_null( $functionName ) ? __METHOD__ : $functionName, $options ); + + $this->releaseConnection( $dbr ); + return $result; } /** @@ -241,15 +254,18 @@ abstract class ORMTable implements IORMTable { */ public function rawSelectRow( array $fields, array $conditions = array(), array $options = array(), $functionName = null ) { - $dbr = wfGetDB( $this->getReadDb() ); + $dbr = $this->getReadDbConnection(); - return $dbr->selectRow( + $result = $dbr->selectRow( $this->getName(), $fields, $conditions, is_null( $functionName ) ? __METHOD__ : $functionName, $options ); + + $this->releaseConnection( $dbr ); + return $result; } /** @@ -327,13 +343,18 @@ abstract class ORMTable implements IORMTable { * @return boolean Success indicator */ public function delete( array $conditions, $functionName = null ) { - return wfGetDB( DB_MASTER )->delete( + $dbw = $this->getWriteDbConnection(); + + $result = $dbw->delete( $this->getName(), $conditions === array() ? '*' : $this->getPrefixedValues( $conditions ), $functionName ) !== false; // DatabaseBase::delete does not always return true for success as documented... + + $this->releaseConnection( $dbw ); + return $result; } - + /** * Get API parameters for the fields supported by this object. * @@ -397,7 +418,7 @@ abstract class ORMTable implements IORMTable { } /** - * Get the database type used for read operations. + * Get the database ID used for read operations. * * @since 1.20 * @@ -408,7 +429,7 @@ abstract class ORMTable implements IORMTable { } /** - * Set the database type to use for read operations. + * Set the database ID to use for read operations, use DB_XXX constants or an index to the load balancer setup. * * @param integer $db * @@ -418,6 +439,86 @@ abstract class ORMTable implements IORMTable { $this->readDb = $db; } + /** + * Get the ID of the any foreign wiki to use as a target for database operations + * + * @since 1.20 + * + * @return String|bool The target wiki, in a form that LBFactory understands (or false if the local wiki is used) + */ + public function getTargetWiki() { + return $this->wiki; + } + + /** + * Set the ID of the any foreign wiki to use as a target for database operations + * + * @param String|bool $wiki The target wiki, in a form that LBFactory understands (or false if the local wiki shall be used) + * + * @since 1.20 + */ + public function setTargetWiki( $wiki ) { + $this->wiki = $wiki; + } + + /** + * Get the database type used for read operations. + * This is to be used instead of wfGetDB. + * + * @see LoadBalancer::getConnection + * + * @since 1.20 + * + * @return DatabaseBase The database object + */ + public function getReadDbConnection() { + return $this->getLoadBalancer()->getConnection( $this->getReadDb(), array(), $this->getTargetWiki() ); + } + + /** + * Get the database type used for read operations. + * This is to be used instead of wfGetDB. + * + * @see LoadBalancer::getConnection + * + * @since 1.20 + * + * @return DatabaseBase The database object + */ + public function getWriteDbConnection() { + return $this->getLoadBalancer()->getConnection( DB_MASTER, array(), $this->getTargetWiki() ); + } + + /** + * Get the database type used for read operations. + * + * @see wfGetLB + * + * @since 1.20 + * + * @return LoadBalancer The database load balancer object + */ + public function getLoadBalancer() { + return wfGetLB( $this->getTargetWiki() ); + } + + /** + * Releases the lease on the given database connection. This is useful mainly + * for connections to a foreign wiki. It does nothing for connections to the local wiki. + * + * @see LoadBalancer::reuseConnection + * + * @param DatabaseBase $db the database + * + * @since 1.20 + */ + public function releaseConnection( DatabaseBase $db ) { + if ( $this->wiki !== false ) { + // recycle connection to foreign wiki + $this->getLoadBalancer()->reuseConnection( $db ); + } + } + /** * Update the records matching the provided conditions by * setting the fields that are keys in the $values param to @@ -431,14 +532,17 @@ abstract class ORMTable implements IORMTable { * @return boolean Success indicator */ public function update( array $values, array $conditions = array() ) { - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->getWriteDbConnection(); - return $dbw->update( + $result = $dbw->update( $this->getName(), $this->getPrefixedValues( $values ), $this->getPrefixedValues( $conditions ), __METHOD__ ) !== false; // DatabaseBase::update does not always return true for success as documented... + + $this->releaseConnection( $dbw ); + return $result; } /** @@ -450,6 +554,7 @@ abstract class ORMTable implements IORMTable { * @param array $conditions */ public function updateSummaryFields( $summaryFields = null, array $conditions = array() ) { + $slave = $this->getReadDb(); $this->setReadDb( DB_MASTER ); /** @@ -461,7 +566,7 @@ abstract class ORMTable implements IORMTable { $item->save(); } - $this->setReadDb( DB_SLAVE ); + $this->setReadDb( $slave ); } /** diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index c7156fb2f1..f3dc5a3a00 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -38,7 +38,7 @@ class DifferenceEngine extends ContextSource { * @private */ var $mOldid, $mNewid; - var $mOldtext, $mNewtext; + var $mOldContent, $mNewContent; protected $mDiffLang; /** @@ -224,6 +224,10 @@ class DifferenceEngine extends ContextSource { # we'll use the application/x-external-editor interface to call # an external diff tool like kompare, kdiff3, etc. if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) { + //TODO: come up with a good solution for non-text content here. + // at least, the content format needs to be passed to the client somehow. + // Currently, action=raw will just fail for non-text content. + $urls = array( 'File' => array( 'Extension' => 'wiki', 'URL' => # This should be mOldPage, but it may not be set, see below. @@ -510,19 +514,21 @@ class DifferenceEngine extends ContextSource { $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() ); $out->setArticleFlag( true ); + // NOTE: only needed for B/C: custom rendering of JS/CSS via hook if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { // Stolen from Article::view --AG 2007-10-11 // Give hooks a chance to customise the output // @TODO: standardize this crap into one function - if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) { - // Wrap the whole lot in a
 and don't parse
-					$m = array();
-					preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
-					$out->addHTML( "
\n" );
-					$out->addHTML( htmlspecialchars( $this->mNewtext ) );
-					$out->addHTML( "\n
\n" ); + if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { + // NOTE: deprecated hook, B/C only + // use the content object's own rendering + $po = $this->mNewRev->getContent()->getParserOutput( $this->mNewRev->getTitle(), $this->mNewRev->getId() ); + $out->addHTML( $po->getText() ); } - } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) { + } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { + // Handled by extension + } elseif( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { + // NOTE: deprecated hook, B/C only // Handled by extension } else { // Normal page @@ -536,16 +542,21 @@ class DifferenceEngine extends ContextSource { $wikiPage = WikiPage::factory( $this->mNewPage ); } - $parserOptions = $wikiPage->makeParserOptions( $this->getContext() ); + $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev ); - if ( !$this->mNewRev->isCurrent() ) { - $parserOptions->setEditSection( false ); - } + # Also try to load it as a redirect + $rt = $this->mNewContent->getRedirectTarget(); - $parserOutput = $wikiPage->getParserOutput( $parserOptions, $this->mNewid ); + if ( $rt ) { + $article = Article::newFromTitle( $this->mNewPage, $this->getContext() ); + $out->addHTML( $article->viewRedirect( $rt ) ); - # WikiPage::getParserOutput() should not return false, but just in case - if( $parserOutput ) { + # WikiPage::getParserOutput() should not return false, but just in case + if ( $parserOutput ) { + # Show categories etc. + $out->addParserOutputNoText( $parserOutput ); + } + } else if ( $parserOutput ) { $out->addParserOutput( $parserOutput ); } } @@ -556,6 +567,17 @@ class DifferenceEngine extends ContextSource { wfProfileOut( __METHOD__ ); } + protected function getParserOutput( WikiPage $page, Revision $rev ) { + $parserOptions = $page->makeParserOptions( $this->getContext() ); + + if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( "edit" ) ) { + $parserOptions->setEditSection( false ); + } + + $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() ); + return $parserOutput; + } + /** * Get the diff text, send it to the OutputPage object * Returns false if the diff could not be generated, otherwise returns true @@ -652,7 +674,7 @@ class DifferenceEngine extends ContextSource { return false; } - $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); + $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent ); // Save to cache for 7 days if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) { @@ -689,14 +711,63 @@ class DifferenceEngine extends ContextSource { } } + /** + * Generate a diff, no caching. + * + * This implementation uses generateTextDiffBody() to generate a diff based on the default + * serialization of the given Content objects. This will fail if $old or $new are not + * instances of TextContent. + * + * Subclasses may override this to provide a different rendering for the diff, + * perhaps taking advantage of the content's native form. This is required for all content + * models that are not text based. + * + * @param $old Content: old content + * @param $new Content: new content + * + * @since 1.21 + * @throws MWException if $old or $new are not instances of TextContent. + */ + function generateContentDiffBody( Content $old, Content $new ) { + if ( !( $old instanceof TextContent ) ) { + throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " + . "override generateContentDiffBody to fix this." ); + } + + if ( !( $new instanceof TextContent ) ) { + throw new MWException( "Diff not implemented for " . get_class( $new ) . "; " + . "override generateContentDiffBody to fix this." ); + } + + $otext = $old->serialize(); + $ntext = $new->serialize(); + + return $this->generateTextDiffBody( $otext, $ntext ); + } + /** * Generate a diff, no caching * * @param $otext String: old text, must be already segmented * @param $ntext String: new text, must be already segmented - * @return bool|string + * @deprecated since 1.21, use generateContentDiffBody() instead! */ function generateDiffBody( $otext, $ntext ) { + ContentHandler::deprecated( __METHOD__, "1.21" ); + + return $this->generateTextDiffBody( $otext, $ntext ); + } + + /** + * Generate a diff, no caching + * + * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point. + * + * @param $otext String: old text, must be already segmented + * @param $ntext String: new text, must be already segmented + * @return bool|string + */ + function generateTextDiffBody( $otext, $ntext ) { global $wgExternalDiffEngine, $wgContLang; wfProfileIn( __METHOD__ ); @@ -859,7 +930,7 @@ class DifferenceEngine extends ContextSource { * the visibility of the revision and a link to edit the page. * @return String HTML fragment */ - private function getRevisionHeader( Revision $rev, $complete = '' ) { + protected function getRevisionHeader( Revision $rev, $complete = '' ) { $lang = $this->getLanguage(); $user = $this->getUser(); $revtimestamp = $rev->getTimestamp(); @@ -951,10 +1022,25 @@ class DifferenceEngine extends ContextSource { /** * Use specified text instead of loading from the database + * @deprecated since 1.21, use setContent() instead. */ function setText( $oldText, $newText ) { - $this->mOldtext = $oldText; - $this->mNewtext = $newText; + ContentHandler::deprecated( __METHOD__, "1.21" ); + + $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() ); + $newContent = ContentHandler::makeContent( $newText, $this->getTitle() ); + + $this->setContent( $oldContent, $newContent ); + } + + /** + * Use specified text instead of loading from the database + * @since 1.21 + */ + function setContent( Content $oldContent, Content $newContent ) { + $this->mOldContent = $oldContent; + $this->mNewContent = $newContent; + $this->mTextLoaded = 2; $this->mRevisionsLoaded = true; } @@ -1082,14 +1168,14 @@ class DifferenceEngine extends ContextSource { return false; } if ( $this->mOldRev ) { - $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER ); - if ( $this->mOldtext === false ) { + $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); + if ( $this->mOldContent === false ) { return false; } } if ( $this->mNewRev ) { - $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); - if ( $this->mNewtext === false ) { + $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); + if ( $this->mNewContent === false ) { return false; } } @@ -1110,7 +1196,7 @@ class DifferenceEngine extends ContextSource { if ( !$this->loadRevisionData() ) { return false; } - $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); + $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); return true; } } diff --git a/includes/extauth/MediaWiki.php b/includes/extauth/MediaWiki.php index 0a5efae6ee..c7f6a20417 100644 --- a/includes/extauth/MediaWiki.php +++ b/includes/extauth/MediaWiki.php @@ -36,15 +36,15 @@ * 'DBprefix' => '', * ); * - * All fields must be present. These mean the same things as $wgDBtype, - * $wgDBserver, etc. This implementation is quite crude; it could easily - * support multiple database servers, for instance, and memcached, and it - * probably has bugs. Kind of hard to reuse code when things might rely on who + * All fields must be present. These mean the same things as $wgDBtype, + * $wgDBserver, etc. This implementation is quite crude; it could easily + * support multiple database servers, for instance, and memcached, and it + * probably has bugs. Kind of hard to reuse code when things might rely on who * knows what configuration globals. * - * If either wiki uses the UserComparePasswords hook, password authentication - * might fail unexpectedly unless they both do the exact same validation. - * There may be other corner cases like this where this will fail, but it + * If either wiki uses the UserComparePasswords hook, password authentication + * might fail unexpectedly unless they both do the exact same validation. + * There may be other corner cases like this where this will fail, but it * should be unlikely. * * @ingroup ExternalUser @@ -62,8 +62,8 @@ class ExternalUser_MediaWiki extends ExternalUser { * @return bool */ protected function initFromName( $name ) { - # We might not need the 'usable' bit, but let's be safe. Theoretically - # this might return wrong results for old versions, but it's probably + # We might not need the 'usable' bit, but let's be safe. Theoretically + # this might return wrong results for old versions, but it's probably # good enough. $name = User::getCanonicalName( $name, 'usable' ); @@ -130,14 +130,14 @@ class ExternalUser_MediaWiki extends ExternalUser { } public function authenticate( $password ) { - # This might be wrong if anyone actually uses the UserComparePasswords hook + # This might be wrong if anyone actually uses the UserComparePasswords hook # (on either end), so don't use this if you those are incompatible. return User::comparePasswords( $this->mRow->user_password, $password, - $this->mRow->user_id ); + $this->mRow->user_id ); } public function getPref( $pref ) { - # @todo FIXME: Return other prefs too. Lots of global-riddled code that does + # @todo FIXME: Return other prefs too. Lots of global-riddled code that does # this normally. if ( $pref === 'emailaddress' && $this->row->user_email_authenticated !== null ) { diff --git a/includes/filebackend/FSFile.php b/includes/filebackend/FSFile.php index e07c99d42d..fda356e0a7 100644 --- a/includes/filebackend/FSFile.php +++ b/includes/filebackend/FSFile.php @@ -86,8 +86,8 @@ class FSFile { /** * Guess the MIME type from the file contents alone - * - * @return string + * + * @return string */ public function getMimeType() { return MimeMagic::singleton()->guessMimeType( $this->path, false ); @@ -211,7 +211,7 @@ class FSFile { /** * Get the final file extension from a file system path - * + * * @param $path string * @return string */ diff --git a/includes/filebackend/FSFileBackend.php b/includes/filebackend/FSFileBackend.php index 092291921f..dd43f82921 100644 --- a/includes/filebackend/FSFileBackend.php +++ b/includes/filebackend/FSFileBackend.php @@ -670,8 +670,8 @@ class FSFileBackend extends FileBackendStore { foreach ( $params['srcs'] as $src ) { $source = $this->resolveToFSPath( $src ); - if ( $source === null ) { - $fsFiles[$src] = null; // invalid path + if ( $source === null || !is_file( $source ) ) { + $fsFiles[$src] = null; // invalid path or file does not exist } else { $fsFiles[$src] = new FSFile( $source ); } @@ -700,7 +700,9 @@ class FSFileBackend extends FileBackendStore { } else { $tmpPath = $tmpFile->getPath(); // Copy the source file over the temp file + wfSuppressWarnings(); $ok = copy( $source, $tmpPath ); + wfRestoreWarnings(); if ( !$ok ) { $tmpFiles[$src] = null; } else { diff --git a/includes/filebackend/FileBackend.php b/includes/filebackend/FileBackend.php index c10d1057a5..0ef4cd0131 100644 --- a/includes/filebackend/FileBackend.php +++ b/includes/filebackend/FileBackend.php @@ -1073,6 +1073,17 @@ abstract class FileBackend { return "mwstore://{$this->name}"; } + /** + * Get the storage path for the given container for this backend + * + * @param $container string Container name + * @return string Storage path + * @since 1.21 + */ + final public function getContainerStoragePath( $container ) { + return $this->getRootStoragePath() . "/{$container}"; + } + /** * Get the file journal object for this backend * @@ -1177,6 +1188,7 @@ abstract class FileBackend { * * @param $type string One of (attachment, inline) * @param $filename string Suggested file name (should not contain slashes) + * @throws MWException * @return string * @since 1.20 */ diff --git a/includes/filebackend/FileBackendMultiWrite.php b/includes/filebackend/FileBackendMultiWrite.php index 7df09d180a..90292ee027 100644 --- a/includes/filebackend/FileBackendMultiWrite.php +++ b/includes/filebackend/FileBackendMultiWrite.php @@ -179,10 +179,11 @@ class FileBackendMultiWrite extends FileBackend { // Actually attempt the operation batch on the master backend... $masterStatus = $mbe->doOperations( $realOps, $opts ); $status->merge( $masterStatus ); - // Propagate the operations to the clone backends if there were no fatal errors. - // If $ops only had one operation, this might avoid backend inconsistencies. - // This also avoids inconsistency for expected errors (like "file already exists"). - if ( !count( $masterStatus->getErrorsArray() ) ) { + // Propagate the operations to the clone backends if there were no unexpected errors + // and if there were either no expected errors or if the 'force' option was used. + // However, if nothing succeeded at all, then don't replicate any of the operations. + // If $ops only had one operation, this might avoid backend sync inconsistencies. + if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) { foreach ( $this->backends as $index => $backend ) { if ( $index !== $this->masterIndex ) { // not done already $realOps = $this->substOpBatchPaths( $ops, $backend ); @@ -535,6 +536,7 @@ class FileBackendMultiWrite extends FileBackend { /** * @see FileBackend::fileExists() * @param $params array + * @return bool|null */ public function fileExists( array $params ) { $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); diff --git a/includes/filebackend/FileBackendStore.php b/includes/filebackend/FileBackendStore.php index a29816f068..5f562d2d8c 100644 --- a/includes/filebackend/FileBackendStore.php +++ b/includes/filebackend/FileBackendStore.php @@ -834,6 +834,14 @@ abstract class FileBackendStore extends FileBackend { $status = $this->doStreamFile( $params ); wfProfileOut( __METHOD__ . '-send-' . $this->name ); wfProfileOut( __METHOD__ . '-send' ); + if ( !$status->isOK() ) { + // Per bug 41113, nasty things can happen if bad cache entries get + // stuck in cache. It's also possible that this error can come up + // with simple race conditions. Clear out the stat cache to be safe. + $this->clearCache( array( $params['src'] ) ); + $this->deleteFileCache( $params['src'] ); + trigger_error( "Bad stat cache or race condition for file {$params['src']}." ); + } } else { $status->fatal( 'backend-fail-stream', $params['src'] ); } @@ -1007,6 +1015,7 @@ abstract class FileBackendStore extends FileBackend { * Get a list of storage paths to lock for a list of operations * Returns an array with 'sh' (shared) and 'ex' (exclusive) keys, * each corresponding to a list of storage paths to be locked. + * All returned paths are normalized. * * @param $performOps Array List of FileOp objects * @return Array ('sh' => list of paths, 'ex' => list of paths) @@ -1176,6 +1185,8 @@ abstract class FileBackendStore extends FileBackend { /** * @see FileBackendStore::executeOpHandlesInternal() + * @param array $fileOpHandles + * @throws MWException * @return Array List of corresponding Status objects */ protected function doExecuteOpHandlesInternal( array $fileOpHandles ) { @@ -1473,6 +1484,7 @@ abstract class FileBackendStore extends FileBackend { /** * Do a batch lookup from cache for container stats for all containers * used in a list of container names, storage paths, or FileOp objects. + * This loads the persistent cache values into the process cache. * * @param $items Array * @return void @@ -1529,7 +1541,7 @@ abstract class FileBackendStore extends FileBackend { /** * Get the cache key for a file path * - * @param $path string Storage path + * @param $path string Normalized storage path * @return string */ private function fileCacheKey( $path ) { @@ -1545,6 +1557,10 @@ abstract class FileBackendStore extends FileBackend { * @param $val mixed Information to cache */ final protected function setFileCache( $path, $val ) { + $path = FileBackend::normalizeStoragePath( $path ); + if ( $path === null ) { + return; // invalid storage path + } $this->memCache->add( $this->fileCacheKey( $path ), $val, 7*86400 ); } @@ -1555,6 +1571,10 @@ abstract class FileBackendStore extends FileBackend { * @param $path string Storage path */ final protected function deleteFileCache( $path ) { + $path = FileBackend::normalizeStoragePath( $path ); + if ( $path === null ) { + return; // invalid storage path + } if ( !$this->memCache->set( $this->fileCacheKey( $path ), 'PURGED', 300 ) ) { trigger_error( "Unable to delete stat cache for file $path." ); } @@ -1563,6 +1583,7 @@ abstract class FileBackendStore extends FileBackend { /** * Do a batch lookup from cache for file stats for all paths * used in a list of storage paths or FileOp objects. + * This loads the persistent cache values into the process cache. * * @param $items Array List of storage paths or FileOps * @return void @@ -1579,9 +1600,11 @@ abstract class FileBackendStore extends FileBackend { $paths = array_merge( $paths, $item->storagePathsRead() ); $paths = array_merge( $paths, $item->storagePathsChanged() ); } elseif ( self::isStoragePath( $item ) ) { - $paths[] = $item; + $paths[] = FileBackend::normalizeStoragePath( $item ); } } + // Get rid of any paths that failed normalization... + $paths = array_filter( $paths, 'strlen' ); // remove nulls // Get all the corresponding cache keys for paths... foreach ( $paths as $path ) { list( $cont, $rel, $s ) = $this->resolveStoragePath( $path ); diff --git a/includes/filebackend/SwiftFileBackend.php b/includes/filebackend/SwiftFileBackend.php index 1807734f2c..25c14f5560 100644 --- a/includes/filebackend/SwiftFileBackend.php +++ b/includes/filebackend/SwiftFileBackend.php @@ -1344,13 +1344,6 @@ class SwiftFileBackend extends FileBackendStore { return wfMemcKey( 'backend', $this->getName(), 'usercreds', $username ); } - /** - * @see FileBackendStore::doClearCache() - */ - protected function doClearCache( array $paths = null ) { - $this->connContainerCache->clear(); // clear container object cache - } - /** * Get a Swift container object, possibly from process cache. * Use $reCache if the file count or byte count is needed. diff --git a/includes/filebackend/TempFSFile.php b/includes/filebackend/TempFSFile.php index 5032bf682a..11e125c11f 100644 --- a/includes/filebackend/TempFSFile.php +++ b/includes/filebackend/TempFSFile.php @@ -82,30 +82,37 @@ class TempFSFile extends FSFile { * Clean up the temporary file only after an object goes out of scope * * @param $object Object - * @return void + * @return TempFSFile This object */ public function bind( $object ) { if ( is_object( $object ) ) { + if ( !isset( $object->tempFSFileReferences ) ) { + // Init first since $object might use __get() and return only a copy variable + $object->tempFSFileReferences = array(); + } $object->tempFSFileReferences[] = $this; } + return $this; } /** * Set flag to not clean up after the temporary file * - * @return void + * @return TempFSFile This object */ public function preserve() { $this->canDelete = false; + return $this; } /** * Set flag clean up after the temporary file * - * @return void + * @return TempFSFile This object */ public function autocollect() { $this->canDelete = true; + return $this; } /** diff --git a/includes/filebackend/filejournal/DBFileJournal.php b/includes/filebackend/filejournal/DBFileJournal.php index f6268c258f..34f3e53d50 100644 --- a/includes/filebackend/filejournal/DBFileJournal.php +++ b/includes/filebackend/filejournal/DBFileJournal.php @@ -83,6 +83,19 @@ class DBFileJournal extends FileJournal { return $status; } + /** + * @see FileJournal::doGetCurrentPosition() + * @return integer|false + */ + protected function doGetCurrentPosition() { + $dbw = $this->getMasterDB(); + + return $dbw->selectField( 'filejournal', 'MAX(fj_id)', + array( 'fj_backend' => $this->backend ), + __METHOD__ + ); + } + /** * @see FileJournal::doGetChangeEntries() * @return Array diff --git a/includes/filebackend/filejournal/FileJournal.php b/includes/filebackend/filejournal/FileJournal.php index ce029bbeaf..3bc0df74f5 100644 --- a/includes/filebackend/filejournal/FileJournal.php +++ b/includes/filebackend/filejournal/FileJournal.php @@ -110,6 +110,21 @@ abstract class FileJournal { */ abstract protected function doLogChangeBatch( array $entries, $batchId ); + /** + * Get the position ID of the latest journal entry + * + * @return integer|false + */ + final public function getCurrentPosition() { + return $this->doGetCurrentPosition(); + } + + /** + * @see FileJournal::getCurrentPosition() + * @return integer|false + */ + abstract protected function doGetCurrentPosition(); + /** * Get an array of file change log entries. * A starting change ID and/or limit can be specified. @@ -169,7 +184,7 @@ abstract class FileJournal { */ class NullFileJournal extends FileJournal { /** - * @see FileJournal::logChangeBatch() + * @see FileJournal::doLogChangeBatch() * @param $entries array * @param $batchId string * @return Status @@ -178,6 +193,14 @@ class NullFileJournal extends FileJournal { return Status::newGood(); } + /** + * @see FileJournal::doGetCurrentPosition() + * @return integer|false + */ + protected function doGetCurrentPosition() { + return false; + } + /** * @see FileJournal::doGetChangeEntries() * @return Array @@ -187,7 +210,7 @@ class NullFileJournal extends FileJournal { } /** - * @see FileJournal::purgeOldLogs() + * @see FileJournal::doPurgeOldLogs() * @return Status */ protected function doPurgeOldLogs() { diff --git a/includes/filebackend/lockmanager/MemcLockManager.php b/includes/filebackend/lockmanager/MemcLockManager.php index 57c0463d4c..26a5e2da8e 100644 --- a/includes/filebackend/lockmanager/MemcLockManager.php +++ b/includes/filebackend/lockmanager/MemcLockManager.php @@ -64,6 +64,7 @@ class MemcLockManager extends QuorumLockManager { * - wikiId : Wiki ID string that all resources are relative to. [optional] * * @param Array $config + * @throws MWException */ public function __construct( array $config ) { parent::__construct( $config ); diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php index 9c8d85dc2a..635cb95efe 100644 --- a/includes/filerepo/FSRepo.php +++ b/includes/filerepo/FSRepo.php @@ -24,9 +24,9 @@ /** * A repository for files accessible via the local filesystem. * Does not support database access or registration. - * + * * This is a mostly a legacy class. New uses should not be added. - * + * * @ingroup FileRepo * @deprecated since 1.19 */ diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index 5f24fedc13..e8aa5a6335 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -1318,6 +1318,7 @@ class FileRepo { * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg * * @param $key string + * @throws MWException * @return string */ public function getDeletedHashPath( $key ) { diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index 4b206c3d0e..18659852d2 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -86,7 +86,7 @@ class ForeignDBRepo extends LocalRepo { /** * Get a key on the primary cache for this repository. - * Returns false if the repository's cache is not accessible at this site. + * Returns false if the repository's cache is not accessible at this site. * The parameters are the parts of the key, as for wfMemcKey(). * @return bool|mixed */ diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php index bd76fce7fe..7951fb1383 100644 --- a/includes/filerepo/ForeignDBViaLBRepo.php +++ b/includes/filerepo/ForeignDBViaLBRepo.php @@ -61,7 +61,7 @@ class ForeignDBViaLBRepo extends LocalRepo { /** * Get a key on the primary cache for this repository. - * Returns false if the repository's cache is not accessible at this site. + * Returns false if the repository's cache is not accessible at this site. * The parameters are the parts of the key, as for wfMemcKey(). * @return bool|string */ diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index 0954422d4f..118e9810c3 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -238,7 +238,7 @@ class LocalRepo extends FileRepo { __METHOD__, array( 'ORDER BY' => 'img_name' ) ); - + $result = array(); foreach ( $res as $row ) { $result[] = $this->newFileFromRow( $row ); @@ -299,7 +299,7 @@ class LocalRepo extends FileRepo { /** * Get a key on the primary cache for this repository. - * Returns false if the repository's cache is not accessible at this site. + * Returns false if the repository's cache is not accessible at this site. * The parameters are the parts of the key, as for wfMemcKey(). * * @return string diff --git a/includes/filerepo/README b/includes/filerepo/README index 885a1deddd..d3aea9f0c7 100644 --- a/includes/filerepo/README +++ b/includes/filerepo/README @@ -39,22 +39,3 @@ LocalRepo.php. LocalRepo provides only file access, and LocalFile provides database access and higher-level functions such as cache management. Tim Starling, June 2007 - -Structure: - -File defines an abstract class File. - ForeignAPIFile extends File. - LocalFile extends File. - ForeignDBFile extends LocalFile - Image extends LocalFile - UnregisteredLocalFile extends File. - UploadStashFile extends UnregisteredLocalFile. -FileRepo defines an abstract class FileRepo. - ForeignAPIRepo extends FileRepo - FSRepo extends FileRepo - LocalRepo extends FSRepo - ForeignDBRepo extends LocalRepo - ForeignDBViaLBRepo extends LocalRepo - NullRepo extends FileRepo - -Russ Nelson, March 2011 diff --git a/includes/filerepo/file/ArchivedFile.php b/includes/filerepo/file/ArchivedFile.php index c5a0bd1b48..694623b627 100644 --- a/includes/filerepo/file/ArchivedFile.php +++ b/includes/filerepo/file/ArchivedFile.php @@ -47,6 +47,7 @@ class ArchivedFile { $timestamp, # time of upload $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) $deleted, # Bitfield akin to rev_deleted + $sha1, # sha1 hash of file content $pageCount, $archive_name; @@ -87,6 +88,7 @@ class ArchivedFile { $this->deleted = 0; $this->dataLoaded = false; $this->exists = false; + $this->sha1 = ''; if( $title instanceof Title ) { $this->title = File::normalizeTitle( $title, 'exception' ); @@ -108,6 +110,7 @@ class ArchivedFile { /** * Loads a file object from the filearchive table + * @throws MWException * @return bool|null True on success or null */ public function load() { @@ -152,7 +155,8 @@ class ArchivedFile { 'fa_user', 'fa_user_text', 'fa_timestamp', - 'fa_deleted' ), + 'fa_deleted', + 'fa_sha1' ), $conds, __METHOD__, array( 'ORDER BY' => 'fa_timestamp DESC' ) ); @@ -164,23 +168,7 @@ class ArchivedFile { $row = $ret->fetchObject(); // initialize fields for filestore image object - $this->id = intval($row->fa_id); - $this->name = $row->fa_name; - $this->archive_name = $row->fa_archive_name; - $this->group = $row->fa_storage_group; - $this->key = $row->fa_storage_key; - $this->size = $row->fa_size; - $this->bits = $row->fa_bits; - $this->width = $row->fa_width; - $this->height = $row->fa_height; - $this->metadata = $row->fa_metadata; - $this->mime = "$row->fa_major_mime/$row->fa_minor_mime"; - $this->media_type = $row->fa_media_type; - $this->description = $row->fa_description; - $this->user = $row->fa_user; - $this->user_text = $row->fa_user_text; - $this->timestamp = $row->fa_timestamp; - $this->deleted = $row->fa_deleted; + $this->loadFromRow( $row ); } else { throw new MWException( 'This title does not correspond to an image page.' ); } @@ -199,28 +187,42 @@ class ArchivedFile { */ public static function newFromRow( $row ) { $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) ); - - $file->id = intval($row->fa_id); - $file->name = $row->fa_name; - $file->archive_name = $row->fa_archive_name; - $file->group = $row->fa_storage_group; - $file->key = $row->fa_storage_key; - $file->size = $row->fa_size; - $file->bits = $row->fa_bits; - $file->width = $row->fa_width; - $file->height = $row->fa_height; - $file->metadata = $row->fa_metadata; - $file->mime = "$row->fa_major_mime/$row->fa_minor_mime"; - $file->media_type = $row->fa_media_type; - $file->description = $row->fa_description; - $file->user = $row->fa_user; - $file->user_text = $row->fa_user_text; - $file->timestamp = $row->fa_timestamp; - $file->deleted = $row->fa_deleted; - + $file->loadFromRow( $row ); return $file; } + /** + * Load ArchivedFile object fields from a DB row. + * + * @param $row Object database row + * @since 1.21 + */ + public function loadFromRow( $row ) { + $this->id = intval($row->fa_id); + $this->name = $row->fa_name; + $this->archive_name = $row->fa_archive_name; + $this->group = $row->fa_storage_group; + $this->key = $row->fa_storage_key; + $this->size = $row->fa_size; + $this->bits = $row->fa_bits; + $this->width = $row->fa_width; + $this->height = $row->fa_height; + $this->metadata = $row->fa_metadata; + $this->mime = "$row->fa_major_mime/$row->fa_minor_mime"; + $this->media_type = $row->fa_media_type; + $this->description = $row->fa_description; + $this->user = $row->fa_user; + $this->user_text = $row->fa_user_text; + $this->timestamp = $row->fa_timestamp; + $this->deleted = $row->fa_deleted; + if( isset( $row->fa_sha1 ) ) { + $this->sha1 = $row->fa_sha1; + } else { + // old row, populate from key + $this->sha1 = LocalRepo::getHashFromKey( $this->key ); + } + } + /** * Return the associated title object * @@ -380,6 +382,17 @@ class ArchivedFile { return wfTimestamp( TS_MW, $this->timestamp ); } + /** + * Get the SHA-1 base 36 hash of the file + * + * @return string + * @since 1.21 + */ + function getSha1() { + $this->load(); + return $this->sha1; + } + /** * Return the user ID of the uploader. * diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index bbabe8415d..caa93a42c6 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -1186,8 +1186,9 @@ class LocalFile extends File { } else { # New file; create the description page. # There's already a log entry, so don't make a second RC entry - # Squid and file cache for the description page are purged by doEdit. - $status = $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user ); + # Squid and file cache for the description page are purged by doEditContent. + $content = ContentHandler::makeContent( $pageText, $descTitle ); + $status = $wikiPage->doEditContent( $content, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user ); if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction $dbw->begin(); @@ -1475,9 +1476,9 @@ class LocalFile extends File { global $wgParser; $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL ); if ( !$revision ) return false; - $text = $revision->getText(); - if ( !$text ) return false; - $pout = $wgParser->parse( $text, $this->title, new ParserOptions() ); + $content = $revision->getContent(); + if ( !$content ) return false; + $pout = $content->getParserOutput( $this->title, null, new ParserOptions() ); return $pout->getText(); } @@ -1773,7 +1774,8 @@ class LocalFileDeleteBatch { 'fa_description' => 'img_description', 'fa_user' => 'img_user', 'fa_user_text' => 'img_user_text', - 'fa_timestamp' => 'img_timestamp' + 'fa_timestamp' => 'img_timestamp', + 'fa_sha1' => 'img_sha1', ), $where, __METHOD__ ); } @@ -1805,6 +1807,7 @@ class LocalFileDeleteBatch { 'fa_user' => 'oi_user', 'fa_user_text' => 'oi_user_text', 'fa_timestamp' => 'oi_timestamp', + 'fa_sha1' => 'oi_sha1', ), $where, __METHOD__ ); } } @@ -2037,7 +2040,12 @@ class LocalFileRestoreBatch { $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key; $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel; - $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) ); + if( isset( $row->fa_sha1 ) ) { + $sha1 = $row->fa_sha1; + } else { + // old row, populate from key + $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key ); + } # Fix leading zero if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) { diff --git a/includes/installer/DatabaseUpdater.php b/includes/installer/DatabaseUpdater.php index ff0a99e9ff..7223003180 100644 --- a/includes/installer/DatabaseUpdater.php +++ b/includes/installer/DatabaseUpdater.php @@ -54,12 +54,17 @@ abstract class DatabaseUpdater { protected $shared = false; + /** + * Scripts to run after database update + * Should be a subclass of LoggedUpdateMaintenance + */ protected $postDatabaseUpdateMaintenance = array( 'DeleteDefaultMessages', 'PopulateRevisionLength', 'PopulateRevisionSha1', 'PopulateImageSha1', 'FixExtLinksProtocolRelative', + 'PopulateFilearchiveSha1', ); /** @@ -177,7 +182,7 @@ abstract class DatabaseUpdater { * Note that callback functions will receive this object as * first parameter. */ - public function addExtensionUpdate( Array $update ) { + public function addExtensionUpdate( array $update ) { $this->extensionUpdates[] = $update; } @@ -254,6 +259,8 @@ abstract class DatabaseUpdater { /** * Add a maintenance script to be run after the database updates are complete. * + * Script should subclass LoggedUpdateMaintenance + * * @since 1.19 * * @param $class string Name of a Maintenance subclass diff --git a/includes/installer/Ibm_db2Updater.php b/includes/installer/Ibm_db2Updater.php index f7d5a1eed2..805ff0ff66 100644 --- a/includes/installer/Ibm_db2Updater.php +++ b/includes/installer/Ibm_db2Updater.php @@ -77,7 +77,7 @@ class Ibm_db2Updater extends DatabaseUpdater { array( 'modifyField', 'user_properties', 'up_property', 'patch-up_property.sql' ), array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ), array( 'addTable', 'user_former_groups', 'patch-user_former_groups.sql'), - array( 'doRebuildLocalisationCache' ), + array( 'doRebuildLocalisationCache' ), // 1.19 array( 'addIndex', 'logging', 'type_action', 'patch-logging-type-action-index.sql'), @@ -89,6 +89,11 @@ class Ibm_db2Updater extends DatabaseUpdater { array( 'addTable', 'config', 'patch-config.sql' ), // 1.21 + array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ), + array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ), + array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ), + array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ), + array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ), ); } } diff --git a/includes/installer/Installer.i18n.php b/includes/installer/Installer.i18n.php index 8849ac02e6..c60fbb014f 100644 --- a/includes/installer/Installer.i18n.php +++ b/includes/installer/Installer.i18n.php @@ -567,11 +567,13 @@ When that has been done, you can '''[$2 enter your wiki]'''.", * @author Kghbln * @author McDutchie * @author Mormegil + * @author Nemo bis * @author Nike * @author Platonides * @author Purodha * @author Raymond * @author SPQRobin + * @author Shirayuki * @author Siebrand * @author Umherirrender */ @@ -596,6 +598,8 @@ $messages['qqq'] = array( 'config-sidebar' => 'Maximum width for words is 24 characters. Only visible part of the translation counts to this limit.', 'config-env-php' => 'Parameters: * $1 is the version of PHP that has been installed.', + 'config-unicode-pure-php-warning' => 'PECL is the name of a group producing standard pieces of software for PHP, and intl is the name of their library handling some aspects of internationalization.', + 'config-unicode-update-warning' => "ICU is a body producing standard software tools for support of Unicode and other internationalization aspects. This message warns the system administrator installing MediaWiki that the server's software is not up-to-date and MediaWiki will have problems handling some characters.", 'config-no-db' => 'Do not translate: ./configure --with-mysql.
Do not translate: php5-mysql. @@ -603,6 +607,8 @@ Do not translate: php5-mysql. Parameters: * $1 is comma separated list of database types supported by MediaWiki.', 'config-no-fts3' => 'A "[[:wikipedia:Front and back ends|backend]]" is a system or component that ordinary users don\'t interact with directly and don\'t need to know about, and that is responsible for a distinct task or service - for example, a storage back-end is a generic system for storing data which other applications can use. Possible alternatives for back-end are "system" or "service", or (depending on context and language) even leave it untranslated.', + 'config-pcre' => 'PCRE is an initialism for "Perl-compatible regular expression". Perl is programming language whose [[:w:regular expression|regular expression]] syntax is popular and used in other languages using a library called PCRE.', + 'config-pcre-no-utf8' => "PCRE is a name of a programmers' library for supporting regular expressions. It can probably be translated without change.", 'config-memory-raised' => 'Parameters: * $1 is the configured memory_limit. * $2 is the value to which memory_limit was raised.', @@ -640,10 +646,13 @@ Add dir="ltr" to the for right-to-left languages.', 'config-connection-error' => '$1 is the external error from the database, such as "DB connection error: Access denied for user \'dba\'@\'localhost\' (using password: YES) (localhost)." If you\'re translating this message to a right-to-left language, consider writing
$1.
. (When the bidi features for HTML5 will be implemented in the browsers, it will probably be a good idea to write it as
$1.
.)', + 'config-invalid-schema' => '*$1 - schema name', 'config-sqlite-dir-unwritable' => 'webserver refers to a software like Apache or Lighttpd.', 'config-can-upgrade' => 'Parameters: * $1 - Version or Revision indicator.', 'config-show-table-status' => '{{doc-important|"SHOW TABLE STATUS" is a MySQL command. Do not translate this.}}', + 'config-db-web-account-same' => 'checkbox label', + 'config-db-web-create' => 'checkbox label', 'config-ns-generic' => '{{Identical|Project}}', 'config-admin-name' => '{{Identical|Your name}}', 'config-admin-password' => '{{Identical|Password}}', @@ -660,20 +669,109 @@ If you\'re translating this message to a right-to-left language, consider writin This message refers to a block of HTML being embedded into the installer page. It comes from the Creative Commons Web site. The block is in the English language. It is a scripted license chooser. When an individual license has been selected, it asks you to klick "proceed" so as to return to the MediaWiki installer page.', 'config-extensions' => '{{Identical|Extension}}', 'config-install-step-done' => '{{Identical|Done}}', + 'config-install-database' => '*{{msg-mw|Config-install-database}} +*{{msg-mw|Config-install-tables}} +*{{msg-mw|Config-install-schema}} +*{{msg-mw|Config-install-user}} +*{{msg-mw|Config-install-interwiki}} +*{{msg-mw|Config-install-stats}} +*{{msg-mw|Config-install-keys}} +*{{msg-mw|Config-install-sysop}} +*{{msg-mw|Config-install-mainpage}}', + 'config-install-schema' => '*{{msg-mw|Config-install-database}} +*{{msg-mw|Config-install-tables}} +*{{msg-mw|Config-install-schema}} +*{{msg-mw|Config-install-user}} +*{{msg-mw|Config-install-interwiki}} +*{{msg-mw|Config-install-stats}} +*{{msg-mw|Config-install-keys}} +*{{msg-mw|Config-install-sysop}} +*{{msg-mw|Config-install-mainpage}}', 'config-install-pg-schema-failed' => 'Parameters: * $1 = database user name (usernames in the database are unrelated to wiki user names) * $2 =', - 'config-install-user' => 'Message indicates that the user is being created', + 'config-install-user' => 'Message indicates that the user is being created + +See also: +*{{msg-mw|Config-install-database}} +*{{msg-mw|Config-install-tables}} +*{{msg-mw|Config-install-schema}} +*{{msg-mw|Config-install-user}} +*{{msg-mw|Config-install-interwiki}} +*{{msg-mw|Config-install-stats}} +*{{msg-mw|Config-install-keys}} +*{{msg-mw|Config-install-sysop}} +*{{msg-mw|Config-install-mainpage}}', 'config-install-user-grant-failed' => 'Parameters: * $1 is the database username for which granting rights failed * $2 is the error message', - 'config-install-tables' => 'Message indicates that the tables are being created', - 'config-install-interwiki' => 'Message indicates that the interwikitables are being populated', + 'config-install-tables' => 'Message indicates that the tables are being created + +See also: +*{{msg-mw|Config-install-database}} +*{{msg-mw|Config-install-tables}} +*{{msg-mw|Config-install-schema}} +*{{msg-mw|Config-install-user}} +*{{msg-mw|Config-install-interwiki}} +*{{msg-mw|Config-install-stats}} +*{{msg-mw|Config-install-keys}} +*{{msg-mw|Config-install-sysop}} +*{{msg-mw|Config-install-mainpage}}', + 'config-install-interwiki' => 'Message indicates that the interwikitables are being populated + +See also: +*{{msg-mw|Config-install-database}} +*{{msg-mw|Config-install-tables}} +*{{msg-mw|Config-install-schema}} +*{{msg-mw|Config-install-user}} +*{{msg-mw|Config-install-interwiki}} +*{{msg-mw|Config-install-stats}} +*{{msg-mw|Config-install-keys}} +*{{msg-mw|Config-install-sysop}} +*{{msg-mw|Config-install-mainpage}}', + 'config-install-stats' => '*{{msg-mw|Config-install-database}} +*{{msg-mw|Config-install-tables}} +*{{msg-mw|Config-install-schema}} +*{{msg-mw|Config-install-user}} +*{{msg-mw|Config-install-interwiki}} +*{{msg-mw|Config-install-stats}} +*{{msg-mw|Config-install-keys}} +*{{msg-mw|Config-install-sysop}} +*{{msg-mw|Config-install-mainpage}}', + 'config-install-keys' => '*{{msg-mw|Config-install-database}} +*{{msg-mw|Config-install-tables}} +*{{msg-mw|Config-install-schema}} +*{{msg-mw|Config-install-user}} +*{{msg-mw|Config-install-interwiki}} +*{{msg-mw|Config-install-stats}} +*{{msg-mw|Config-install-keys}} +*{{msg-mw|Config-install-sysop}} +*{{msg-mw|Config-install-mainpage}}', 'config-insecure-keys' => 'Parameters: * $1 - A list of names of the secret keys that were generated. * $2 - the number of items in the list $1, to be used with PLURAL.', - 'config-install-sysop' => 'Message indicates that the administrator user account is being created', + 'config-install-sysop' => 'Message indicates that the administrator user account is being created + +See also: +*{{msg-mw|Config-install-database}} +*{{msg-mw|Config-install-tables}} +*{{msg-mw|Config-install-schema}} +*{{msg-mw|Config-install-user}} +*{{msg-mw|Config-install-interwiki}} +*{{msg-mw|Config-install-stats}} +*{{msg-mw|Config-install-keys}} +*{{msg-mw|Config-install-sysop}} +*{{msg-mw|Config-install-mainpage}}', 'config-install-subscribe-fail' => '{{doc-important|"mediawiki-announce" is the name of a mailing list and should not be translated.}}', + 'config-install-mainpage' => '*{{msg-mw|Config-install-database}} +*{{msg-mw|Config-install-tables}} +*{{msg-mw|Config-install-schema}} +*{{msg-mw|Config-install-user}} +*{{msg-mw|Config-install-interwiki}} +*{{msg-mw|Config-install-stats}} +*{{msg-mw|Config-install-keys}} +*{{msg-mw|Config-install-sysop}} +*{{msg-mw|Config-install-mainpage}}', 'config-install-done' => 'Parameters: * $1 is the URL to LocalSettings download * $2 is a link to the wiki. @@ -9508,43 +9606,45 @@ I seguenti collegamenti sono in lingua inglese: * @author 青子守歌 */ $messages['ja'] = array( - 'config-desc' => 'MediaWikiのインストーラー', - 'config-title' => 'MediaWiki $1のインストール', + 'config-desc' => 'MediaWiki のインストーラー', + 'config-title' => 'MediaWiki $1 のインストール', 'config-information' => '情報', - 'config-localsettings-upgrade' => 'LocalSettings.phpファイルが検出されました。 -アップグレードするため、ボックス中の$wgUpgradeKeyの値を入力してください。 -LocalSettings.phpの中にそれはあるでしょう。', - 'config-localsettings-key' => 'アップグレードキー:', + 'config-localsettings-upgrade' => 'ファイル LocalSettings.php を検出しました。 +インストールされているものをアップグレードするには、$wgUpgradeKey の値を以下の欄に入力してください。 +この値は LocalSettings.php 内にあります。', + 'config-localsettings-cli-upgrade' => 'ファイル LocalSettings.php を検出しました。 +インストールされているものをアップグレードするには、update.php を実行してください', + 'config-localsettings-key' => 'アップグレード キー:', 'config-localsettings-badkey' => '与えられたキーが間違っています', 'config-upgrade-key-missing' => 'MediaWikiの既存インストールを検出しました。 インストールをアップグレードするために、次の行をLocalSettings.phpの末尾に挿入してください: $1', - 'config-localsettings-incomplete' => '現在のLocalSettings.phpは不完全であるようです。 -変数$1が設定されていません。 -LocalSettings.phpを変更してこの変数を設定して、『{{int:Config-continue}}』を押してください。', - 'config-session-error' => 'セッションの開始エラー:$1', + 'config-localsettings-incomplete' => '既存の LocalSettings.php の内容は不完全のようです。 +変数 $1 が設定されていません。 +LocalSettings.php 内でこの変数を設定して、「{{int:Config-continue}}」をクリックしてください。', + 'config-session-error' => 'セッションの開始エラー: $1', 'config-session-expired' => 'セッションの有効期限が切れたようです。 セッションの有効期間は$1に設定されています。 php.iniのsession.gc_maxlifetimeを設定することで、この問題を改善できます。 インストール作業を再起動させてください。', 'config-no-session' => 'セッションのデータが消失しました! php.iniを確認し、session.save_pathが適切なディレクトリに設定されていることを確認してください。', - 'config-your-language' => 'あなたの言語:', + 'config-your-language' => 'あなたの言語:', 'config-your-language-help' => 'インストール作業に使用する言語を選択してください。', - 'config-wiki-language' => 'ウィキの言語:', + 'config-wiki-language' => 'ウィキの言語:', 'config-wiki-language-help' => 'ウィキで主に書き込まれる言語を選択してください。', - 'config-back' => '←戻る', + 'config-back' => '← 戻る', 'config-continue' => '続行 →', 'config-page-language' => '言語', - 'config-page-welcome' => 'MediaWikiへようこそ!', + 'config-page-welcome' => 'MediaWiki へようこそ!', 'config-page-dbconnect' => 'データベースに接続', 'config-page-upgrade' => '既存のインストールを更新', 'config-page-dbsettings' => 'データベースの設定', 'config-page-name' => '名前', 'config-page-options' => 'オプション', 'config-page-install' => 'インストール', - 'config-page-complete' => '完了!', + 'config-page-complete' => '完了!', 'config-page-restart' => 'インストールを再起動', 'config-page-readme' => 'お読みください', 'config-page-releasenotes' => 'リリースノート', @@ -9587,12 +9687,12 @@ MediaWikiのインストールはできません。', 高トラフィックのサイトを運営する場合は、[//www.mediawiki.org/wiki/Unicode_normalization_considerations Unicode正規化に関するページ]をお読みください。", 'config-unicode-update-warning' => "'''警告''':インストールされているバージョンのUnicode正規化ラッパーは、[http://site.icu-project.org/ ICUプロジェクト]のライブラリの古いバージョンを使用しています。 Unicodeを少しでも利用する可能性があるなら、[//www.mediawiki.org/wiki/Unicode_normalization_considerations アップグレード]する必要があります。", - 'config-no-db' => '適切なデータベースドライバが見つかりませんでした!PHPにデータベースドライバをインストールする必要があります。 -次の種類のデータベースが使用できます: $1 + 'config-no-db' => '適切なデータベース ドライバーが見つかりませんでした! PHP にデータベース ドライバーをインストールする必要があります。 +以下の種類のデータベースに対応しています: $1 -共有サーバを使用している場合は、サーバの管理者に適切なデータベースドライバのインストールを依頼してください。 -PHPを自分でコンパイルした場合は、たとえば./configure --with-mysqlを実行して、データベースクライアントを使用可能に再設定してください。 -DebianまたはUbuntuのパッケージからPHPをインストールした場合は、php5-mysqlモジュールもインストールする必要があります。', +共有サーバーを使用している場合は、適切なデータベース ドライバーのインストールを、サーバーの管理者に依頼してください。 +PHP を自分でコンパイルした場合は、例えば ./configure --with-mysql を実行して、データベース クライアントを使用できるように再設定してください。 +Debian または Ubuntu のパッケージから PHP をインストールした場合は、php5-mysql モジュールもインストールする必要があります。', 'config-no-fts3' => "'''警告''':SQLiteは[//sqlite.org/fts3.html FTS3]モジュールなしでコンパイルされており、検索機能はこのバックエンドで利用不可能になります。", 'config-register-globals' => "'''警告:PHPの[http://php.net/register_globals register_globals]オプションが有効になっています。''' '''可能なら無効化してください。''' @@ -9616,18 +9716,18 @@ MediaWikiは、このモジュールの関数を必要としているため、 Mandrakeを実行している場合、php-xmlパッケージをインストールしてください。', 'config-pcre' => 'PCREをサポートしているモジュールが不足しているようです。 MediaWikiは、Perl互換の正規表現関数の動作が必要です。', - 'config-pcre-no-utf8' => "'''致命的エラー''': PHPのPCREがPCRE_UTF8サポート無しでコンパイルされています。 -MediaWikiにはUTF-8サポートの関数が必要です。", + 'config-pcre-no-utf8' => "'''致命的エラー''': PHP の PCRE が PCRE_UTF8 対応なしでコンパイルされているようです。 +MediaWiki を正しく動作させるには、UTF-8 対応が必要です。", 'config-memory-raised' => 'PHPのmemory_limitは$1で、$2に引き上げられました。', 'config-memory-bad' => "'''警告:'''PHPのmemory_limitは$1です。 これは、非常に遅い可能性があります。 インストールが失敗するかもしれません!", - 'config-xcache' => '[http://xcache.lighttpd.net/ XCache]がインストール済み', - 'config-apc' => '[http://www.php.net/apc APC]がインストール済み', - 'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache]がインストール済み', - 'config-no-cache' => "'''警告:'''[http://www.php.net/apc APC]、[http://xcache.lighttpd.net/ XCache]あるいは[http://www.iis.net/download/WinCacheForPhp WinCache]のいずれも見つかりませんでした。 + 'config-xcache' => '[http://xcache.lighttpd.net/ XCache] がインストール済み', + 'config-apc' => '[http://www.php.net/apc APC] がインストール済み', + 'config-wincache' => '[http://www.iis.net/download/WinCacheForPhp WinCache] がインストール済み', + 'config-no-cache' => "'''警告:'''[http://www.php.net/apc APC]、[http://xcache.lighttpd.net/ XCache]、[http://www.iis.net/download/WinCacheForPhp WinCache] のいずれも見つかりませんでした。 オブジェクトのキャッシュは有効化されません。", - 'config-diff3-bad' => 'GNU diff3が見つかりません。', + 'config-diff3-bad' => 'GNU diff3 が見つかりません。', 'config-imagemagick' => 'ImageMagickが見つかりました:$1。 アップロードが有効なら、画像のサムネイルが利用できます。', 'config-gd' => 'GD画像ライブラリが内蔵されていることが確認されました。 @@ -9637,9 +9737,9 @@ MediaWikiにはUTF-8サポートの関数が必要です。", 'config-no-uri' => "'''エラー:'''現在のURIを決定できませんでした。 インストールは中止されました。", 'config-using-server' => 'サーバー名「$1」を使用しています。', - 'config-using-uri' => 'サーバーURL「$1$2」を使用しています。', - 'config-uploads-not-safe' => "'''警告:'''アップロードの既定ディレクトリ$1が、任意のスクリプト実行に関して脆弱性があります。 -MediaWikiはアップロードされたファイルのセキュリティ上の脅威を確認しますが、アップロードを有効化するまえに、[//www.mediawiki.org/wiki/Manual:Security#Upload_security このセキュリティ上の脆弱性を閉じる]ことが強く推奨されます。", + 'config-using-uri' => 'サーバー URL「$1$2」を使用しています。', + 'config-uploads-not-safe' => "'''警告:'''アップロードの既定ディレクトリ $1 に、任意のスクリプト実行に関する脆弱性があります。 +MediaWiki はアップロードされたファイルのセキュリティ上の脅威を確認しますが、アップロードを有効化する前に、[//www.mediawiki.org/wiki/Manual:Security#Upload_security このセキュリティ上の脆弱性を解決する]ことを強く推奨します。", 'config-brokenlibxml' => 'このシステムで使われているPHPとlibxml2のバージョンのこの組み合わせにはバグがあります。具体的には、MediaWikiやその他のウェブアプリケーションでhiddenデータが破損する可能性があります。 PHPを5.2.9かそれ以降のバージョンに、libxml2を2.7.3かそれ以降のバージョンにアップグレードしてください([//bugs.php.net/bug.php?id=45996 PHPでのバグ情報])。 インストールを終了します。', @@ -9647,8 +9747,8 @@ PHPを5.2.9かそれ以降のバージョンに、libxml2を2.7.3かそれ以降 PHP5.3.2以降に更新するか、この([//bugs.php.net/bug.php?id=50394 PHPに提出されたバグ])を修正するためにPHP5.3.0へ戻してください。 インストールは中止されました。', 'config-suhosin-max-value-length' => 'Suhosin がインストールされており、GETパラメータの長さを $1 バイトに制限しています。MediaWiki の ResourceLoader コンポーネントはこの制限を回避しますが、パフォーマンスは低下します。可能な限り、php.ini で suhosin.get.max_value_length を 1024 以上に設定し、同じ値を LocalSettings.php 中で $wgResourceLoaderMaxQueryLength に設定してください。', - 'config-db-type' => 'データベースの種類:', - 'config-db-host' => 'データベースのホスト:', + 'config-db-type' => 'データベースの種類:', + 'config-db-host' => 'データベースのホスト:', 'config-db-host-help' => '異なるサーバー上にデータベースサーバーがある場合、ホスト名またはIPアドレスをここに入力してください。 もし、共有されたウェブホスティングを使用している場合、ホスティングプロバイダーは正確なホスト名を解説しているはずです。 @@ -9656,18 +9756,18 @@ PHP5.3.2以降に更新するか、この([//bugs.php.net/bug.php?id=50394 PHP WindowsでMySQLを使用している場合に、「localhost」は、サーバー名としてはうまく働かないでしょう。もしそのような場合は、ローカルIPアドレスとして「127.0.0.1」を試してみてください。 PostgreSQLを使用している場合、UNIXソケットで接続するにはこの欄を空欄のままにしてください。', - 'config-db-host-oracle' => 'データベースTNS:', + 'config-db-host-oracle' => 'データベース TNS:', 'config-db-host-oracle-help' => '有効な[http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm ローカル接続名]を入力してください。tnsnames.oraファイルは、このインストールに対して表示されてなければなりません、
もしクライアントライブラリ10gもしくはそれ以上を使用している場合、メソッドの名前を[http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm 簡易接続]で利用できます。', - 'config-db-wiki-settings' => 'このウィキを識別', - 'config-db-name' => 'データベース名:', + 'config-db-wiki-settings' => 'このウィキの識別情報', + 'config-db-name' => 'データベース名:', 'config-db-name-help' => 'このウィキを識別する名前を入力してください。 空白を含めることはできません。 共有ウェブホストを利用している場合、ホスティングプロバイダーが特定の使用可能なデータベース名を提供するか、あるいは管理パネルからデータベースを作成できるようにしているでしょう。', - 'config-db-name-oracle' => 'データベースのスキーマ:', - 'config-db-install-account' => 'インストールのための利用者アカウント', - 'config-db-username' => 'データベースの利用者名:', - 'config-db-password' => 'データベースのパスワード:', + 'config-db-name-oracle' => 'データベースのスキーマ:', + 'config-db-install-account' => 'インストールで使用する利用者アカウント', + 'config-db-username' => 'データベースのユーザー名:', + 'config-db-password' => 'データベースのパスワード:', 'config-db-password-empty' => '新しいデータベースの利用者名 $1 のパスワードを入力してください。 パスワードを設定しないでユーザを作ることもできるかもしれませんが、安全ではありません。', 'config-db-install-username' => 'インストール中にデータベースに接続するために使うユーザ名を入力してください。これは MediaWiki アカウントのユーザ名 (利用者名) のことではありません。あなたのデータベースでのユーザ名です。', @@ -9677,62 +9777,63 @@ PostgreSQLを使用している場合、UNIXソケットで接続するにはこ 'config-db-wiki-account' => 'インストール作業終了後の利用者アカウント', 'config-db-wiki-help' => '通常のウィキ操作中にデータベースへの接続する時に利用する利用者名とパスワードを入力してください。 アカウントが存在せず、インストールのアカウントに十分な権限がある場合は、この利用者アカウントは、ウィキを操作する上で最小限の権限を持った状態で作成されます。', - 'config-db-prefix' => 'データベーステーブルの接頭辞:', + 'config-db-prefix' => 'データベース テーブルの接頭辞:', 'config-db-prefix-help' => 'データベースを複数のウィキ間、あるいはMediaWikiと他のウェブアプリケーションで共有する必要がある場合、衝突を避けるために、すべてのテーブル名に接頭辞を付ける必要があります。 空白は使用できません。 このフィールドは、通常は空のままです。', 'config-db-charset' => 'データベースの文字セット', - 'config-charset-mysql5-binary' => 'MySQL 4.1/5.0バイナリ', + 'config-charset-mysql5-binary' => 'MySQL 4.1/5.0 バイナリ', 'config-charset-mysql5' => 'MySQL 4.1/5.0 UTF-8', - 'config-charset-mysql4' => 'MySQL 4.0 下位互換UTF-8', - 'config-charset-help' => "'''警告:'''MySQL 4.1+で'''下位互換UTF-8'''を使用し、その後mysqldumpでデータベースをバックアップすると、すべての非ASCII文字が破壊され、不可逆的にバップアップが壊れるかもしれません。 - -'''バイナリー形式'''では、MediaWikiは、UTF-8テキストを、データベースのバイナリーフィールドに格納します。 -これは、MySQLのUTF-8形式より効率的で、Unicode文字の全範囲を利用できるようになります。 -'''UTF-8形式'''では、MySQLは、データ内でどの文字集合を使用しているかを知っていて、それに対して適切な提示と変換をするでしょうが、 -[//ja.wikipedia.org/wiki/%E5%9F%BA%E6%9C%AC%E5%A4%9A%E8%A8%80%E8%AA%9E%E9%9D%A2 基本多言語面]の外にある文字を格納できるようにはなりません。", - 'config-mysql-old' => 'MySQLの$1以降が要求されています。あなたの所有のものは$2です。', - 'config-db-port' => 'データベースポート:', - 'config-db-schema' => 'メディアウィキの図式', - 'config-db-schema-help' => '上の図式は常に正確です。 -必要である場合のみ、変更してください。', - 'config-sqlite-dir' => 'SQLiteのデータディレクトリ:', - 'config-sqlite-dir-help' => "SQLiteは単一のファイル内にすべてのデータを保持しています。 - -指定したディレクトリは、インストール時にウェブサーバーが書き込める必要があります。 - -このディレクトリはウェブからアクセス'''不可能'''である必要があります。これがPHPファイルがある場所に配置できない理由です。 - -インストーラーは同時に.htaccessファイルに書き込みます。しかし、これが失敗しても誰かがあなたの生のデータベースにアクセスすることが可能となるでしょう。データベースは生のデータ(メールアドレス、パスワードのハッシュ値)の他、削除された版、その他、ウィキ上の制限されているデータを含んでいます。 - -例えば/var/lib/mediawiki/yourwikiのように、別の場所にデータベースを配置することを検討してください。", - 'config-oracle-def-ts' => '既定のテーブル領域:', - 'config-oracle-temp-ts' => '一時的なテーブル領域:', + 'config-charset-mysql4' => 'MySQL 4.0 後方互換 UTF-8', + 'config-charset-help' => "'''警告:''' MySQL 4.1+ で'''後方互換 UTF-8''' を使用している状態で、mysqldump でデータベースをバックアップすると、すべての非 ASCII 文字が破壊されてしまい、バックアップが不可逆的に破損してしまいます! + +'''バイナリ モード'''では、MediaWiki は、UTF-8 テキストをデータベースのバイナリ フィールドに格納します。 +これは、MySQL の UTF-8 モードより効率的で、Unicode 文字の全範囲を利用できるようになります。 +'''UTF-8 モード'''では、MySQL は、データ内で使用している文字集合を知っているため、適切に表現や変換ができますが、 +[//ja.wikipedia.org/wiki/%E5%9F%BA%E6%9C%AC%E5%A4%9A%E8%A8%80%E8%AA%9E%E9%9D%A2 基本多言語面]の外にある文字を格納できません。", + 'config-mysql-old' => 'MySQL $1 以降が必要です。ご使用中の MySQL は $2 です。', + 'config-db-port' => 'データベースのポート:', + 'config-db-schema' => 'MediaWiki のスキーマ:', + 'config-db-schema-help' => '通常はこのスキーマで問題ありません。 +必要な場合のみ変更してください。', + 'config-sqlite-dir' => 'SQLite データ ディレクトリ:', + 'config-sqlite-dir-help' => "SQLite は単一のファイル内にすべてのデータを格納しています。 + +指定したディレクトリは、インストール時にウェブ サーバーが書き込めるようにしておく必要があります。 + +このディレクトリはウェブからアクセス'''不可能'''である必要があります。PHP ファイルがある場所には配置できないのはこのためです。 + +インストーラーは .htaccess ファイルにも書き込みます。しかし、これが失敗した場合は、誰かが生のデータベースにアクセスできてしまいます。 +データベースは、生のデータ (メールアドレス、パスワードのハッシュ値) の他、削除された版、その他ウィキ上の制限されているデータを含んでいます。 + +例えば /var/lib/mediawiki/yourwiki のように、別の場所にデータベースを配置することを検討してください。", + 'config-oracle-def-ts' => '既定のテーブル領域:', + 'config-oracle-temp-ts' => '一時的なテーブル領域:', 'config-type-mysql' => 'MySQL', 'config-type-postgres' => 'PostgreSQL', 'config-type-sqlite' => 'SQLite', 'config-type-oracle' => 'Oracle', 'config-type-ibm_db2' => 'IBM DB2', - 'config-support-info' => 'メディアウィキは次のようなデータベースシステムをサポートする: + 'config-support-info' => 'MediaWiki は以下のデータベース システムに対応しています: $1 -もし、データベースシステムが不可視であるならば、以下のようにリスト化されたものを使用してみてください。可能なサポートの指示に従ってください。', +使用しようとしているデータベース システムが下記の一覧にない場合は、上記リンク先の手順に従ってインストールしてください。', 'config-support-mysql' => '* $1はMediaWikiの主要な対象で、もっともサポートされています([http://www.php.net/manual/en/mysql.installation.php MySQLのサポート下でPHPをコンパイルする方法])', 'config-support-postgres' => '* $1は、MySQLの代替として、人気のあるオープンソースデータベースシステムです([http://www.php.net/manual/en/pgsql.installation.php PostgreSQLのサポート下でPHPをコンパイルする方法])', 'config-support-sqlite' => '* $1は、良くサポートされている、軽量データベースシステムです。([http://www.php.net/manual/en/pdo.installation.php SQLiteのサポート下でPHPをコンパイルする方法]、PDOを使用)', 'config-support-oracle' => '* $1は商業企業のデータベースです。([http://www.php.net/manual/en/oci8.installation.php OCI8サポートなPHPをコンパイルする方法])', 'config-support-ibm_db2' => '* $1 は商業企業のデータベースです。', - 'config-header-mysql' => 'MySQLの設定', - 'config-header-postgres' => 'PostgreSQLの設定', - 'config-header-sqlite' => 'SQLiteの設定', - 'config-header-oracle' => 'Oracleの設定', - 'config-header-ibm_db2' => 'IBM DB2の設定', + 'config-header-mysql' => 'MySQL の設定', + 'config-header-postgres' => 'PostgreSQL の設定', + 'config-header-sqlite' => 'SQLite の設定', + 'config-header-oracle' => 'Oracle の設定', + 'config-header-ibm_db2' => 'IBM DB2 の設定', 'config-invalid-db-type' => '無効なデータベースの種類', - 'config-missing-db-name' => '「データベース名」を入力する必要があります', - 'config-missing-db-host' => '「データベースのホスト」を入力する必要があります', - 'config-missing-db-server-oracle' => '「データベースTNS」に値を入力する必要があります', + 'config-missing-db-name' => '「データベース名」を入力してください', + 'config-missing-db-host' => '「データベースのホスト」を入力してください', + 'config-missing-db-server-oracle' => '「データベース TNS」の値を入力してください', 'config-invalid-db-server-oracle' => '「$1」は無効なデータベース TNS です。 アスキー英字(a-z、A-Z)、数字(0-9)、アンダーバー(_)、ドット(.)のみを使用してください。', 'config-invalid-db-name' => '「$1」は無効なデータベース名です。 @@ -9742,43 +9843,43 @@ $1 'config-connection-error' => '$1。 以下のホスト名、ユーザ名、パスワードをチェックして、再度試してみてください。', - 'config-invalid-schema' => 'メディアウィキ「$1」における無効な図式です。 -アスキー英字(a-z、A-Z)、数字(0-9)、下線(_)のみを使用してください。', - 'config-postgres-old' => 'PostgreSQLの$1あるいはそれ以降が必要で、いまのバージョンは$2です。', + 'config-invalid-schema' => '「$1」は MediaWiki のスキーマとして無効です。 +ASCII の英数字 (a-z、A-Z、0-9)、下線 (_) のみを使用してください。', + 'config-postgres-old' => 'PostgreSQL $1 以降が必要です。ご使用中の PostgreSQL は $2 です。', 'config-sqlite-name-help' => 'あなたのウェキと同一性のある名前を選んでください。 空白およびハイフンは使用しないでください。 SQLiteのデータファイル名として使用されます。', - 'config-sqlite-parent-unwritable-group' => 'データディレクトリ$1を作成できません。親ディレクトリ$2は、ウェブサーバーから書き込みできませんでした。 + 'config-sqlite-parent-unwritable-group' => 'データ ディレクトリ $1 を作成できません。ウェブ サーバーは親ディレクトリ $2 に書き込めませんでした。 -インストール機能は、実行しているウェブサーバーのユーザーを特定しました。 -続行するには、$3ディレクトリを書き込み可能にしてください。 -UnixあるいはLinux上では、以下を実行してください: +インストーラーは、ウェブ サーバーの実行ユーザーを特定しました。 +続行するには、ディレクトリ $3 に書き込めるようにしてください。 +Unix または Linux であれば、以下を実行してください:
cd $2
 mkdir $3
 chgrp $4 $3
 chmod g+w $3
', - 'config-sqlite-parent-unwritable-nogroup' => 'データディレクトリ$1を作成できません。親ディレクトリ$2は、ウェブサーバから書き込みできませんでした。 + 'config-sqlite-parent-unwritable-nogroup' => 'データ ディレクトリ $1 を作成できません。ウェブ サーバーは、親ディレクトリ $2 に書き込めませんでした。 -インストール機能は、実行しているウェブサーバのユーザーを特定できませんでした。 -続行するには、$3ディレクトリを、ウェブサーバ(と他のユーザ!)からグローバルに書き込めるようにしてください。 -UnixあるいはLinux上では、以下を実行してください: +インストーラーは、ウェブ サーバーの実行ユーザーを特定できませんでした。 +続行するには、ディレクトリ $3 に、ウェブ サーバー (と、あらゆる人々!) がグローバルに書き込めるようにしてください。 +Unix または Linux では、以下を実行してください:
cd $2
 mkdir $3
 chmod a+w $3
', - 'config-sqlite-mkdir-error' => 'データディレクトリ「$1」の作成中にエラーが発生しました。 + 'config-sqlite-mkdir-error' => 'データ ディレクトリ「$1」を作成する際にエラーが発生しました。 場所を確認してから、再度試してください。', 'config-sqlite-dir-unwritable' => 'ディレクトリ「$1」に書き込めません。 -ウェブサーバーが書き込めるようにパーミッションを変更してから、度試してください。', +ウェブ サーバーが書き込めるようにパーミッションを変更してから、再度試してください。', 'config-sqlite-connection-error' => '$1。 -下記のデータディレクトリとデータベース名を確認してから、再度試してみてください。', - 'config-sqlite-readonly' => 'ファイル$1は書き込み不能です。', - 'config-sqlite-cant-create-db' => 'データベースファイル$1を作成できませんでした。', - 'config-sqlite-fts3-downgrade' => 'PHPはFTS3のサポート、テーブルのダウングレードが無効です。', - 'config-can-upgrade' => 'このデータベースにはメディアウィキテーブルが存在します。 -それらをメディアウィキ$1にアップグレードするために「続行」をクリックしてください。', +データ ディレクトリおよびデータベース名を確認してから、再度試してください。', + 'config-sqlite-readonly' => 'ファイル $1 に書き込めません。', + 'config-sqlite-cant-create-db' => 'データベース ファイル $1 を作成できませんでした。', + 'config-sqlite-fts3-downgrade' => 'PHP が FTS3 に対応していないため、テーブルをダウングレードしています', + 'config-can-upgrade' => 'このデータベースには MediaWiki テーブルがあります。 +これらのテーブルを MediaWiki $1 にアップグレードするには、「続行」をクリックしてください。', 'config-upgrade-done' => "更新は完了しました。 [$1 ウィキを使い始める]ことができます。 @@ -9792,12 +9893,12 @@ chmod a+w $3
', 'config-show-table-status' => 'SHOW TABLE STATUSクエリーが失敗しました!', 'config-unknown-collation' => "'''警告:''' データベースは認識されない照合を使用しています。", 'config-db-web-account' => 'ウェブアクセスのためのデータベースアカウント', - 'config-db-web-help' => 'ウィキの元来の操作中、ウェブサーバーがデーターベースサーバーに接続できるように、ユーザ名とパスワードを選択してください。', - 'config-db-web-account-same' => 'インストールのために同じアカウントを使用してください', - 'config-db-web-create' => '既に存在していないのであれば、アカウントを作成してください', + 'config-db-web-help' => 'ウィキの通常の操作の際に、ウェブ サーバーがデータベース サーバーに接続できるように、ユーザー名とパスワードを指定してください。', + 'config-db-web-account-same' => 'インストール作業と同じアカウントを使用する', + 'config-db-web-create' => 'アカウントが存在しない場合は作成する', 'config-db-web-no-create-privs' => 'あなたがインストールのために定義したアカウントは、アカウント作成のための特権としては不充分です。 あなたがここで指定したアカウントは既に存在している必要があります。', - 'config-mysql-engine' => 'ストレージエンジン:', + 'config-mysql-engine' => 'ストレージ エンジン:', 'config-mysql-innodb' => 'InnoDB', 'config-mysql-myisam' => 'MyISAM', 'config-mysql-engine-help' => "'''InnoDB'''は、並行処理のサポートに優れているので、ほとんどの場合において最良の選択肢です。 @@ -9807,29 +9908,29 @@ chmod a+w $3

', 'config-mysql-charset' => 'データベースの文字セット:', 'config-mysql-binary' => 'バイナリ', 'config-mysql-utf8' => 'UTF-8', - 'config-mysql-charset-help' => "'''バイナリー形式'''では、MediaWikiは、UTF-8テキストを、データベースのバイナリーフィールドに格納します。 -これは、MySQLのUTF-8形式より効率的で、Unicode文字の全範囲を利用できるようになります。 + 'config-mysql-charset-help' => "'''バイナリ モード'''では、MediaWiki は、UTF-8 テキストをデータベースのバイナリ フィールドに格納します。 +これは、MySQL の UTF-8 モードより効率的で、Unicode 文字の全範囲を利用できるようになります。 -'''UTF-8形式'''では、MySQLは、データ内でどの文字集合を使用しているかを知っていて、それに対して適切な提示と変換をするでしょうが、 -[//ja.wikipedia.org/wiki/%E5%9F%BA%E6%9C%AC%E5%A4%9A%E8%A8%80%E8%AA%9E%E9%9D%A2 基本多言語面]の外にある文字を格納できるようにはなりません。", - 'config-site-name' => 'ウィキの名前:', +'''UTF-8 モード'''では、MySQL は、データ内で使用している文字集合を知っているため、適切に表現や変換ができますが、 +[//ja.wikipedia.org/wiki/%E5%9F%BA%E6%9C%AC%E5%A4%9A%E8%A8%80%E8%AA%9E%E9%9D%A2 基本多言語面]の外にある文字を格納できません。", + 'config-site-name' => 'ウィキ名:', 'config-site-name-help' => 'この事象はブラウザーのタイトルバーと他のさまざまな場所に現れる。', 'config-site-name-blank' => 'サイト名を入力してください。', - 'config-project-namespace' => 'プロジェクト名前空間:', + 'config-project-namespace' => 'プロジェクト名前空間:', 'config-ns-generic' => 'プロジェクト', - 'config-ns-site-name' => 'ウィキ名と同じ:$1', - 'config-ns-other' => 'その他(指定してください)', + 'config-ns-site-name' => 'ウィキ名と同じ: $1', + 'config-ns-other' => 'その他 (指定してください)', 'config-ns-other-default' => 'マイウィキ', 'config-project-namespace-help' => "ウィキペディアの例に従い、多くのウィキは、コンテンツのページとは分離したポリシーページを「'''プロジェクトの名前空間'''」に持っています。 この名前空間のページのタイトルはすべて、ある接頭辞で始まります。それをここで指定することができます。 -この接頭辞はウィキの名前に由来するのが伝統的ですが、「#」や「:」のような区切り記号を含めることはできません。", +この接頭辞はウィキの名前に由来するのが伝統的ですが、「#」や「:」のような区切り文字を含めることはできません。", 'config-ns-invalid' => '"$1"のように指定された名前空間は無効です。 違うプロジェクト名前空間を指定してください。', 'config-admin-box' => '管理アカウント', - 'config-admin-name' => '名前:', - 'config-admin-password' => 'パスワード:', - 'config-admin-password-confirm' => 'パスワードの再入力:', - 'config-admin-help' => 'ここにあなたの希望するユーザ名を入力してください(例えば"Joe Bloggs"など)。 + 'config-admin-name' => '名前:', + 'config-admin-password' => 'パスワード:', + 'config-admin-password-confirm' => 'パスワードの再入力:', + 'config-admin-help' => '希望するユーザー名をここに入力してください (例: "Joe Bloggs")。 この名前でこのウィキにログインすることになります。', 'config-admin-name-blank' => '管理者のユーザ名を入力してください。', 'config-admin-name-invalid' => '指定されたユーザ名 "$1" は無効です。 @@ -9837,7 +9938,7 @@ chmod a+w $3', 'config-admin-password-blank' => '管理者アカウントのパスワードを入力してください。', 'config-admin-password-same' => 'ユーザ名と同じパスワードは使えません。', 'config-admin-password-mismatch' => '入力された2つのパスワードが一致しません。', - 'config-admin-email' => 'メールアドレス:', + 'config-admin-email' => 'メールアドレス:', 'config-admin-email-help' => 'メールアドレスを入力してください。他の利用者からのメールの受け取り、パスワードのリセット、ウォッチリストに登録したページの更新通知に使用します。空欄のままにすることもできます。', 'config-admin-error-user' => '"$1"という名前の管理者を作成する際に内部エラーが発生しました。', 'config-admin-error-password' => '管理者"$1"のパスワードを設定する際に内部エラーが発生しました:
$2
', @@ -9848,7 +9949,7 @@ chmod a+w $3', 残りの設定を飛ばして、今すぐにウィキをインストールできます。', 'config-optional-continue' => '私にもっと質問してください。', 'config-optional-skip' => 'もう飽きてしまったので、とにかくウィキをインストールしてください。', - 'config-profile' => '正しいプロフィールのユーザ:', + 'config-profile' => '利用者権限のプロファイル:', 'config-profile-wiki' => '伝統的なウィキ', 'config-profile-no-anon' => 'アカウントの作成が必要', 'config-profile-fishbowl' => '承認された編集者のみ', @@ -9871,7 +9972,7 @@ MediaWikiでは、最近の更新を確認し、神経質な、もしくは悪 'config-license-cc-by-sa' => 'クリエイティブ・コモンズ 表示-継承', 'config-license-cc-by' => 'クリエイティブ・コモンズ 表示', 'config-license-cc-by-nc-sa' => 'クリエイティブ・コモンズ 表示-非営利-継承', - 'config-license-gfdl' => 'GNUフリー文書利用許諾契約書1.3以降', + 'config-license-gfdl' => 'GNU フリー文書利用許諾契約書 1.3 以降', 'config-license-pd' => 'パブリック・ドメイン', 'config-license-cc-choose' => 'その他のクリエイティブ・コモンズ・ライセンスを選択する', 'config-license-help' => "多くの公開ウィキでは、すべての寄稿物が[http://freedomdefined.org/Definition フリーライセンス]の元に置かれています。 @@ -9897,40 +9998,40 @@ GFDL は有効なライセンスですが、内容を理解するのは困難で 'config-email-auth-help' => "この選択肢を有効にすると、利用者がメールアドレスを設定あるいは変更したときに送信されるリンクにより、そのアドレスを確認しなければならなくなります。 認証済みのアドレスだけが、他の利用者からのメールや、変更通知のメールを受け取ることができます。 公開ウィキでは、メール機能による潜在的な不正利用の防止のため、この選択肢を設定することが'''推奨'''されます。", - 'config-email-sender' => '返信メールアドレス:', + 'config-email-sender' => '返信先メールアドレス:', 'config-email-sender-help' => '送信メールで返信先として使用するメールアドレスを入力してください。 このアドレスは、宛先不明の場合の通知の宛先になります。 多くのメールサーバーでは、少なくともドメイン名部分は有効である必要があります。', 'config-upload-settings' => '画像およびファイルのアップロード', 'config-upload-enable' => 'ファイルのアップロードを有効にする', - 'config-upload-help' => 'ファイルのアップロードは潜在的にあなたのサーバにセキュリティ上の危険をさらします。 -更なる情報のために、マニュアルの[//www.mediawiki.org/wiki/Manual:Security security section] を読むことをすすめます。 + 'config-upload-help' => 'ファイルのアップロードは、あなたのサーバーをセキュリティ上の潜在的な危険に晒します。 +この詳細は、マニュアルの [//www.mediawiki.org/wiki/Manual:Security security section] をお読みください。 -ファイルをアップロードできるようにするために、メディアウィキのルートディレクトリ下のimagesサブディレクトリのモードを変更します。そうすることで、ウェブサーバーはそこに書き込めるようになります。 +ファイルのアップロードを有効にするには、MediaWiki のルート ディレクトリ内の images サブ ディレクトリのモードを変更します。これにより、ウェブ サーバーがそこに書き込めるようになります。 そして、このオプションを有効にしてください。', 'config-upload-deleted' => '削除されたファイルのためのディレクトリ:', 'config-upload-deleted-help' => '削除されるファイルを保存するためのディレクトリを選択してください。 これがウェブからアクセスできないことが理想です。', - 'config-logo' => 'ロゴのURL:', + 'config-logo' => 'ロゴ のURL:', 'config-logo-help' => 'MediaWikiの既定の外装では、サイドバー上部に135x160ピクセルのロゴ用の余白があります。 適切なサイズの画像をアップロードして、そのURLをここに入力してください。 ロゴが不要の場合は、このボックスを空白のままにしてください。', - 'config-instantcommons' => 'InstantCommons機能を有効にする', + 'config-instantcommons' => 'Instant Commons 機能を有効にする', 'config-instantcommons-help' => '[//www.mediawiki.org/wiki/InstantCommons InstantCommons]は、[//commons.wikimedia.org/ ウィキメディア・コモンズ]のサイトで見つかった画像や音声、その他のメディアをウィキ上で利用することができるようになる機能です。 これを有効化するには、MediaWikiはインターネットに接続できなければなりません。 -ウィキメディアコモンズ以外のウィキを同じように設定する方法など、この機能に関する詳細な情報は、[//mediawiki.org/wiki/Manual:$wgForeignFileRepos マニュアル]をご覧ください。', +ウィキメディア・コモンズ以外のウィキを同じように設定する方法など、この機能に関する詳細な情報は、[//mediawiki.org/wiki/Manual:$wgForeignFileRepos マニュアル]をご覧ください。', 'config-cc-error' => 'クリエイティブ・コモンズ・ライセンスの選択器から結果が得られませんでした。 ライセンスの名前を手動で入力してください。', 'config-cc-again' => 'もう一度選択してください...', - 'config-cc-not-chosen' => 'あなたの求めるクリエイティブコモンズのライセンスを選んで、"続行"をクリックしてください。', + 'config-cc-not-chosen' => '希望するクリエイティブ・コモンズのライセンスを選択して、「続行」をクリックしてください。', 'config-advanced-settings' => '高度な設定', 'config-cache-options' => 'オブジェクトのキャッシュの設定:', - 'config-cache-help' => 'オブジェクトのキャッシュは、使用したデータを頻繁にキャッシングすることによって、メディアウィキのスピード改善に使用されます。 -中〜大サイトにおいては、これを有効にするために大変望ましいことです。また小さなサイトにおいても同様な利点をもたらすと考えられます。', + 'config-cache-help' => 'オブジェクトのキャッシュを使用すると、頻繁に使用するデータをキャッシュするため MediaWiki の動作速度を改善できます。 +中〜大規模サイトではこれを有効にすることを強くお勧めします。小規模サイトでも同様に効果があります。', 'config-cache-none' => 'キャッシングしない(機能は取り払われます、しかもより大きなウィキサイト上でスピードの問題が発生します)', - 'config-cache-accel' => 'PHPオブジェクトキャッシング(APC、XCacheあるいはWinCache)', + 'config-cache-accel' => 'PHP オブジェクト キャッシュ (APC、XCache、WinCache のいずれか)', 'config-cache-memcached' => 'Memcachedを使用(追加の設定が必要です)', 'config-memcached-servers' => 'メモリをキャッシュされたサーバ:', 'config-memcached-help' => 'Memcachedを使用するIPアドレスの一覧。 @@ -9943,31 +10044,37 @@ GFDL は有効なライセンスですが、内容を理解するのは困難で これらは更に多くの設定を要求するかもしれませんが、今これらを有効にすることができます。', 'config-install-alreadydone' => "'''警告:''' 既にMediaWikiがインストール済みで、再びインストールし直そうとしています。 次のページへ進んでください。", - 'config-install-begin' => '「{{int:config-continue}}」を押すと、MediaWikiのインストールを開始することができます。 -変更したい設定があれば、「{{int:Config-back}}」を押してください。', + 'config-install-begin' => '「{{int:config-continue}}」を押すと、MediaWiki のインストールを開始できます。 +変更したい設定がある場合は、「{{int:Config-back}}」を押してください。', 'config-install-step-done' => '実行', 'config-install-step-failed' => '失敗した', 'config-install-extensions' => '拡張機能を含む', 'config-install-database' => 'データベースの構築', 'config-install-schema' => 'スキーマの作成', 'config-install-pg-schema-not-exist' => 'PostgreSQL スキーマがありません。', - 'config-install-pg-schema-failed' => 'テーブルの作成に失敗した。 -ユーザ"$1"が図式"$2"に書き込みができるようにしてください。', + 'config-install-pg-schema-failed' => 'テーブルの作成に失敗しました。 +利用者「$1」がスキーマ「$2」に書き込めるようにしてください。', 'config-install-pg-commit' => '変更を送信', - 'config-install-user' => 'データベースユーザを作成する', - 'config-install-user-grant-failed' => 'ユーザー「$1」に許可を与えることに失敗しました。:$2', + 'config-install-user' => 'データベースユーザーの作成', + 'config-install-user-alreadyexists' => 'ユーザー「$1」は既に存在します', + 'config-install-user-create-failed' => 'ユーザー「$1」の作成に失敗しました: $2', + 'config-install-user-grant-failed' => 'ユーザー「$1」に許可を与えることに失敗しました: $2', + 'config-install-user-missing' => '指定したユーザー「$1」は存在しません。', + 'config-install-user-missing-create' => '指定したユーザー「$1」は存在しません。 +アカウントを作成する場合は、下の「アカウント作成」をクリックしてください。', 'config-install-tables' => 'テーブルの作成', 'config-install-tables-exist' => "'''警告''':MediaWikiテーブルは既に存在するようです。 作成を飛ばします。", - 'config-install-tables-failed' => "'''エラー''':テーブルの作成が、次のエラーにより失敗しました:$1", - 'config-install-interwiki' => '既定のウィキ間テーブルを導入しています', - 'config-install-interwiki-list' => 'ファイルinterwiki.listを見つけることができませんでした。', + 'config-install-tables-failed' => "'''エラー''': テーブルの作成が、以下のエラーにより失敗しました: $1", + 'config-install-interwiki' => '既定のウィキ間テーブルの導入', + 'config-install-interwiki-list' => 'ファイル interwiki.list から読み取れませんでした。', 'config-install-interwiki-exists' => "'''警告''':ウィキ間テーブルは既に登録されているようです。 既定のテーブルを無視します。", - 'config-install-keys' => '秘密鍵を生成する', - 'config-install-sysop' => '管理者のユーザーアカウントを作成する', - 'config-install-mainpage' => '既定の接続でメインページを作成', - 'config-install-mainpage-failed' => 'メインページを挿入できませんでした:$1', + 'config-install-stats' => '統計情報の初期化', + 'config-install-keys' => '秘密鍵の生成', + 'config-install-sysop' => '管理者のアカウントの作成', + 'config-install-mainpage' => 'メインページを既定の内容で作成', + 'config-install-mainpage-failed' => 'メインページを挿入できませんでした: $1', 'config-install-done' => "'''おめでとうございます!''' MediaWikiのインストールに成功しました。 @@ -9983,7 +10090,7 @@ $3 '''注意''': もし、これを今しなければ、つまり、このファイルをダウンロードせずインストールを終了した場合、この生成された設定ファイルは利用されません。 それを完了すれば、'''[$2 ウィキに入る]'''ことができます。", - 'config-download-localsettings' => 'LocalSettings.phpをダウンロード', + 'config-download-localsettings' => 'LocalSettings.php をダウンロード', 'config-help' => 'ヘルプ', 'mainpagetext' => "'''MediaWiki のインストールに成功しました。'''", 'mainpagedocfooter' => 'ウィキソフトウェアの使い方に関する情報は[//meta.wikimedia.org/wiki/Help:Contents 利用者案内]を参照してください。 @@ -16281,6 +16388,7 @@ $messages['sk'] = array( /** Slovenian (slovenščina) * @author Dbc334 + * @author Eleassar */ $messages['sl'] = array( 'config-desc' => 'Namestitveni program za MediaWiki', @@ -16452,7 +16560,7 @@ Sedaj lahko preskočite preostalo konfiguriranje in zdaj namestite wiki.', 'config-profile-no-anon' => 'Zahtevano je ustvarjanje računa', 'config-profile-fishbowl' => 'Samo pooblaščeni urejevalci', 'config-profile-private' => 'Zasebni wiki', - 'config-license' => 'Avtorske pravice in dovoljenje:', + 'config-license' => 'Avtorske pravice in licenca:', 'config-license-none' => 'Brez noge dovoljenja', 'config-license-cc-by-sa' => 'Creative Commons Priznanje avtorstva-Deljenje pod enakimi pogoji', 'config-license-cc-by' => 'Creative Commons Priznanje avtorstva', @@ -16474,7 +16582,7 @@ Najbolje je, da mapa ni dostopna preko spleta.', 'config-cc-error' => 'Izbirnik dovoljenja Creative Commons ni vrnil nobenih rezultatov. Vnesite ime dovoljenja ročno.', 'config-cc-again' => 'Izberi ponovno ...', - 'config-cc-not-chosen' => 'Izberite dovoljenje Creative Commons, ki ga želite dodati, in kliknite »proceed«.', + 'config-cc-not-chosen' => 'Izberite licenco Creative Commons, ki jo želite uporabiti, in kliknite »proceed«.', 'config-advanced-settings' => 'Napredna konfiguracija', 'config-cache-accel' => 'Predpomnjenje predmetov PHP (APC, XCache ali WinCache)', 'config-cache-memcached' => 'Uporabi Memcached (zahteva dodatno namestitev in konfiguracijo)', diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php index ac5dbd747c..c673f6fe9f 100644 --- a/includes/installer/Installer.php +++ b/includes/installer/Installer.php @@ -1594,13 +1594,16 @@ abstract class Installer { $status = Status::newGood(); try { $page = WikiPage::factory( Title::newMainPage() ); - $page->doEdit( wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" . - wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text(), + $content = new WikitextContent ( + wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" . + wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text() + ); + + $page->doEditContent( $content, '', EDIT_NEW, false, - User::newFromName( 'MediaWiki default' ) - ); + User::newFromName( 'MediaWiki default' ) ); } catch (MWException $e) { //using raw, because $wgShowExceptionDetails can not be set yet $status->fatal( 'config-install-mainpage-failed', $e->getMessage() ); diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php index 98e13866eb..82de913618 100644 --- a/includes/installer/MysqlUpdater.php +++ b/includes/installer/MysqlUpdater.php @@ -216,8 +216,16 @@ class MysqlUpdater extends DatabaseUpdater { array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ), // 1.21 + array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ), + array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ), + array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ), + array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ), + array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ), array( 'dropField', 'site_stats', 'ss_admins', 'patch-drop-ss_admins.sql' ), array( 'dropField', 'recentchanges', 'rc_moved_to_title', 'patch-rc_moved.sql' ), + array( 'addTable', 'sites', 'patch-sites.sql' ), + array( 'addField', 'filearchive', 'fa_sha1', 'patch-fa_sha1.sql' ), + array( 'addField', 'job', 'job_token', 'patch-job_token.sql' ), ); } diff --git a/includes/installer/OracleInstaller.php b/includes/installer/OracleInstaller.php index 72ec800d55..845816e566 100644 --- a/includes/installer/OracleInstaller.php +++ b/includes/installer/OracleInstaller.php @@ -40,7 +40,7 @@ class OracleInstaller extends DatabaseInstaller { protected $internalDefaults = array( '_OracleDefTS' => 'USERS', '_OracleTempTS' => 'TEMP', - '_InstallUser' => 'SYSDBA', + '_InstallUser' => 'SYSTEM', ); public $minimumVersion = '9.0.1'; // 9iR1 diff --git a/includes/installer/OracleUpdater.php b/includes/installer/OracleUpdater.php index d81cf06b74..f946d59a96 100644 --- a/includes/installer/OracleUpdater.php +++ b/includes/installer/OracleUpdater.php @@ -70,8 +70,16 @@ class OracleUpdater extends DatabaseUpdater { array( 'addTable', 'config', 'patch-config.sql' ), array( 'addIndex', 'ipblocks', 'i05', 'patch-ipblocks_i05_index.sql' ), array( 'addIndex', 'revision', 'i05', 'patch-revision_i05_index.sql' ), - - // 1.21 + array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ), + + //1.21 + array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ), + array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ), + array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ), + array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ), + array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ), + array( 'dropField', 'site_stats', 'ss_admins', 'patch-ss_admins.sql' ), + array( 'dropField', 'recentchanges', 'rc_moved_to_title', 'patch-rc_moved.sql' ), // KEEP THIS AT THE BOTTOM!! array( 'doRebuildDuplicateFunction' ), diff --git a/includes/installer/PostgresInstaller.php b/includes/installer/PostgresInstaller.php index 3ac2b3a8d2..882ec5323a 100644 --- a/includes/installer/PostgresInstaller.php +++ b/includes/installer/PostgresInstaller.php @@ -190,6 +190,7 @@ class PostgresInstaller extends DatabaseInstaller { * other similar objects in the new DB. * - create-tables: A connection with a role suitable for creating tables. * + * @throws MWException * @return Status object. On success, a connection object will be in the * value member. */ diff --git a/includes/installer/PostgresUpdater.php b/includes/installer/PostgresUpdater.php index 499a2d6671..2942c0b7f7 100644 --- a/includes/installer/PostgresUpdater.php +++ b/includes/installer/PostgresUpdater.php @@ -101,6 +101,8 @@ class PostgresUpdater extends DatabaseUpdater { array( 'addPgField', 'archive', 'ar_len', 'INTEGER' ), array( 'addPgField', 'archive', 'ar_page_id', 'INTEGER' ), array( 'addPgField', 'archive', 'ar_parent_id', 'INTEGER' ), + array( 'addPgField', 'archive', 'ar_content_model', 'TEXT' ), + array( 'addPgField', 'archive', 'ar_content_format', 'TEXT' ), array( 'addPgField', 'categorylinks', 'cl_sortkey_prefix', "TEXT NOT NULL DEFAULT ''"), array( 'addPgField', 'categorylinks', 'cl_collation', "TEXT NOT NULL DEFAULT 0"), array( 'addPgField', 'categorylinks', 'cl_type', "TEXT NOT NULL DEFAULT 'page'"), @@ -114,6 +116,7 @@ class PostgresUpdater extends DatabaseUpdater { array( 'addPgField', 'ipblocks', 'ipb_enable_autoblock', 'SMALLINT NOT NULL DEFAULT 1' ), array( 'addPgField', 'ipblocks', 'ipb_parent_block_id', 'INTEGER DEFAULT NULL REFERENCES ipblocks(ipb_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED' ), array( 'addPgField', 'filearchive', 'fa_deleted', 'SMALLINT NOT NULL DEFAULT 0' ), + array( 'addPgField', 'filearchive', 'fa_sha1', "TEXT NOT NULL DEFAULT ''" ), array( 'addPgField', 'logging', 'log_deleted', 'SMALLINT NOT NULL DEFAULT 0' ), array( 'addPgField', 'logging', 'log_id', "INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('logging_log_id_seq')" ), array( 'addPgField', 'logging', 'log_params', 'TEXT' ), @@ -125,6 +128,7 @@ class PostgresUpdater extends DatabaseUpdater { array( 'addPgField', 'oldimage', 'oi_metadata', "BYTEA NOT NULL DEFAULT ''" ), array( 'addPgField', 'oldimage', 'oi_minor_mime', "TEXT NOT NULL DEFAULT 'unknown'" ), array( 'addPgField', 'oldimage', 'oi_sha1', "TEXT NOT NULL DEFAULT ''" ), + array( 'addPgField', 'page', 'page_content_model', 'TEXT' ), array( 'addPgField', 'page_restrictions', 'pr_id', "INTEGER NOT NULL UNIQUE DEFAULT nextval('page_restrictions_pr_id_seq')" ), array( 'addPgField', 'profiling', 'pf_memory', 'NUMERIC(18,10) NOT NULL DEFAULT 0' ), array( 'addPgField', 'recentchanges', 'rc_deleted', 'SMALLINT NOT NULL DEFAULT 0' ), @@ -139,6 +143,8 @@ class PostgresUpdater extends DatabaseUpdater { array( 'addPgField', 'revision', 'rev_deleted', 'SMALLINT NOT NULL DEFAULT 0' ), array( 'addPgField', 'revision', 'rev_len', 'INTEGER' ), array( 'addPgField', 'revision', 'rev_parent_id', 'INTEGER DEFAULT NULL' ), + array( 'addPgField', 'revision', 'rev_content_model', 'TEXT' ), + array( 'addPgField', 'revision', 'rev_content_format', 'TEXT' ), array( 'addPgField', 'site_stats', 'ss_active_users', "INTEGER DEFAULT '-1'" ), array( 'addPgField', 'user_newtalk', 'user_last_timestamp', 'TIMESTAMPTZ' ), array( 'addPgField', 'logging', 'log_user_text', "TEXT NOT NULL DEFAULT ''" ), @@ -222,6 +228,7 @@ class PostgresUpdater extends DatabaseUpdater { array( 'addPgIndex', 'logging', 'logging_page_id_time', '(log_page,log_timestamp)' ), array( 'addPgIndex', 'iwlinks', 'iwl_prefix_title_from', '(iwl_prefix, iwl_title, iwl_from)' ), array( 'addPgIndex', 'job', 'job_timestamp_idx', '(job_timestamp)' ), + array( 'addPgIndex', 'filearchive', 'fa_sha1', '(fa_sha1)' ), array( 'checkIndex', 'pagelink_unique', array( array('pl_from', 'int4_ops', 'btree', 0), diff --git a/includes/installer/SqliteUpdater.php b/includes/installer/SqliteUpdater.php index 95a61c19b2..c3f7a81674 100644 --- a/includes/installer/SqliteUpdater.php +++ b/includes/installer/SqliteUpdater.php @@ -95,8 +95,17 @@ class SqliteUpdater extends DatabaseUpdater { array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ), // 1.21 - array( 'dropField', 'site_stats', 'ss_admins', 'patch-drop-ss_admins.sql' ), + array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ), + array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ), + array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ), + array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ), + array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ), + + array( 'dropField', 'site_stats', 'ss_admins', 'patch-drop-ss_admins.sql' ), array( 'dropField', 'recentchanges', 'rc_moved_to_title', 'patch-rc_moved.sql' ), + array( 'addTable', 'sites', 'patch-sites.sql' ), + array( 'addField', 'filearchive', 'fa_sha1', 'patch-fa_sha1.sql' ), + array( 'addField', 'job', 'job_token', 'patch-job_token.sql' ), ); } diff --git a/includes/job/DoubleRedirectJob.php b/includes/job/DoubleRedirectJob.php index f9c4b0fff2..b1b96b62ab 100644 --- a/includes/job/DoubleRedirectJob.php +++ b/includes/job/DoubleRedirectJob.php @@ -93,8 +93,8 @@ class DoubleRedirectJob extends Job { wfDebug( __METHOD__.": target redirect already deleted, ignoring\n" ); return true; } - $text = $targetRev->getText(); - $currentDest = Title::newFromRedirect( $text ); + $content = $targetRev->getContent(); + $currentDest = $content->getRedirectTarget(); if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) { wfDebug( __METHOD__.": Redirect has changed since the job was queued\n" ); return true; @@ -102,7 +102,7 @@ class DoubleRedirectJob extends Job { # Check for a suppression tag (used e.g. in periodically archived discussions) $mw = MagicWord::get( 'staticredirect' ); - if ( $mw->match( $text ) ) { + if ( $content->matchMagicWord( $mw ) ) { wfDebug( __METHOD__.": skipping: suppressed with __STATICREDIRECT__\n" ); return true; } @@ -124,14 +124,10 @@ class DoubleRedirectJob extends Job { $currentDest->getFragment(), $newTitle->getInterwiki() ); # Fix the text - # Remember that redirect pages can have categories, templates, etc., - # so the regex has to be fairly general - $newText = preg_replace( '/ \[ \[ [^\]]* \] \] /x', - '[[' . $newTitle->getFullText() . ']]', - $text, 1 ); - - if ( $newText === $text ) { - $this->setLastError( 'Text unchanged???' ); + $newContent = $content->updateRedirect( $newTitle ); + + if ( $newContent->equals( $content ) ) { + $this->setLastError( 'Content unchanged???' ); return false; } @@ -143,7 +139,7 @@ class DoubleRedirectJob extends Job { $reason = wfMessage( 'double-redirect-fixed-' . $this->reason, $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText() )->inContentLanguage()->text(); - $article->doEdit( $newText, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC, false, $this->getUser() ); + $article->doEditContent( $newContent, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC, false, $this->getUser() ); $wgUser = $oldUser; return true; diff --git a/includes/job/Job.php b/includes/job/Job.php index 270671e768..0d2803e1bf 100644 --- a/includes/job/Job.php +++ b/includes/job/Job.php @@ -23,11 +23,11 @@ /** * Class to both describe a background job and handle jobs. + * The queue aspects of this class are now deprecated. * * @ingroup JobQueue */ abstract class Job { - /** * @var Title */ @@ -47,172 +47,12 @@ abstract class Job { * Run the job * @return boolean success */ - abstract function run(); + abstract public function run(); /*------------------------------------------------------------------------- * Static functions *------------------------------------------------------------------------*/ - /** - * Pop a job of a certain type. This tries less hard than pop() to - * actually find a job; it may be adversely affected by concurrent job - * runners. - * - * @param $type string - * - * @return Job - */ - static function pop_type( $type ) { - wfProfilein( __METHOD__ ); - - $dbw = wfGetDB( DB_MASTER ); - - $dbw->begin( __METHOD__ ); - - $row = $dbw->selectRow( - 'job', - '*', - array( 'job_cmd' => $type ), - __METHOD__, - array( 'LIMIT' => 1, 'FOR UPDATE' ) - ); - - if ( $row === false ) { - $dbw->commit( __METHOD__ ); - wfProfileOut( __METHOD__ ); - return false; - } - - /* Ensure we "own" this row */ - $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); - $affected = $dbw->affectedRows(); - $dbw->commit( __METHOD__ ); - - if ( $affected == 0 ) { - wfProfileOut( __METHOD__ ); - return false; - } - - wfIncrStats( 'job-pop' ); - $namespace = $row->job_namespace; - $dbkey = $row->job_title; - $title = Title::makeTitleSafe( $namespace, $dbkey ); - $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), - $row->job_id ); - - $job->removeDuplicates(); - - wfProfileOut( __METHOD__ ); - return $job; - } - - /** - * Pop a job off the front of the queue - * - * @param $offset Integer: Number of jobs to skip - * @return Job or false if there's no jobs - */ - static function pop( $offset = 0 ) { - wfProfileIn( __METHOD__ ); - - $dbr = wfGetDB( DB_SLAVE ); - - /* Get a job from the slave, start with an offset, - scan full set afterwards, avoid hitting purged rows - - NB: If random fetch previously was used, offset - will always be ahead of few entries - */ - - $conditions = self::defaultQueueConditions(); - - $offset = intval( $offset ); - $options = array( 'ORDER BY' => 'job_id', 'USE INDEX' => 'PRIMARY' ); - - $row = $dbr->selectRow( 'job', '*', - array_merge( $conditions, array( "job_id >= $offset" ) ), - __METHOD__, - $options - ); - - // Refetching without offset is needed as some of job IDs could have had delayed commits - // and have lower IDs than jobs already executed, blame concurrency :) - // - if ( $row === false ) { - if ( $offset != 0 ) { - $row = $dbr->selectRow( 'job', '*', $conditions, __METHOD__, $options ); - } - - if ( $row === false ) { - wfProfileOut( __METHOD__ ); - return false; - } - } - - // Try to delete it from the master - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin( __METHOD__ ); - $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); - $affected = $dbw->affectedRows(); - $dbw->commit( __METHOD__ ); - - if ( !$affected ) { - $dbw->begin( __METHOD__ ); - - // Failed, someone else beat us to it - // Try getting a random row - $row = $dbw->selectRow( 'job', array( 'minjob' => 'MIN(job_id)', - 'maxjob' => 'MAX(job_id)' ), '1=1', __METHOD__ ); - if ( $row === false || is_null( $row->minjob ) || is_null( $row->maxjob ) ) { - // No jobs to get - $dbw->rollback( __METHOD__ ); - wfProfileOut( __METHOD__ ); - return false; - } - // Get the random row - $row = $dbw->selectRow( 'job', '*', - 'job_id >= ' . mt_rand( $row->minjob, $row->maxjob ), __METHOD__ ); - if ( $row === false ) { - // Random job gone before we got the chance to select it - // Give up - $dbw->rollback( __METHOD__ ); - wfProfileOut( __METHOD__ ); - return false; - } - // Delete the random row - $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); - $affected = $dbw->affectedRows(); - $dbw->commit( __METHOD__ ); - - if ( !$affected ) { - // Random job gone before we exclusively deleted it - // Give up - wfProfileOut( __METHOD__ ); - return false; - } - } - - // If execution got to here, there's a row in $row that has been deleted from the database - // by this thread. Hence the concurrent pop was successful. - wfIncrStats( 'job-pop' ); - $namespace = $row->job_namespace; - $dbkey = $row->job_title; - $title = Title::makeTitleSafe( $namespace, $dbkey ); - - if ( is_null( $title ) ) { - wfProfileOut( __METHOD__ ); - return false; - } - - $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id ); - - // Remove any duplicates it may have later in the queue - $job->removeDuplicates(); - - wfProfileOut( __METHOD__ ); - return $job; - } - /** * Create the appropriate object to handle a specific job * @@ -223,7 +63,7 @@ abstract class Job { * @throws MWException * @return Job */ - static function factory( $command, Title $title, $params = false, $id = 0 ) { + public static function factory( $command, Title $title, $params = false, $id = 0 ) { global $wgJobClasses; if( isset( $wgJobClasses[$command] ) ) { $class = $wgJobClasses[$command]; @@ -232,30 +72,6 @@ abstract class Job { throw new MWException( "Invalid job command `{$command}`" ); } - /** - * @param $params - * @return string - */ - static function makeBlob( $params ) { - if ( $params !== false ) { - return serialize( $params ); - } else { - return ''; - } - } - - /** - * @param $blob - * @return bool|mixed - */ - static function extractBlob( $blob ) { - if ( (string)$blob !== '' ) { - return unserialize( $blob ); - } else { - return false; - } - } - /** * Batch-insert a group of jobs into the queue. * This will be wrapped in a transaction with a forced commit. @@ -264,33 +80,10 @@ abstract class Job { * removed later on, when the first one is popped. * * @param $jobs array of Job objects + * @deprecated 1.21 */ - static function batchInsert( $jobs ) { - if ( !count( $jobs ) ) { - return; - } - $dbw = wfGetDB( DB_MASTER ); - $rows = array(); - - /** - * @var $job Job - */ - foreach ( $jobs as $job ) { - $rows[] = $job->insertFields(); - if ( count( $rows ) >= 50 ) { - # Do a small transaction to avoid slave lag - $dbw->begin( __METHOD__ ); - $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' ); - $dbw->commit( __METHOD__ ); - $rows = array(); - } - } - if ( $rows ) { // last chunk - $dbw->begin( __METHOD__ ); - $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' ); - $dbw->commit( __METHOD__ ); - } - wfIncrStats( 'job-insert', count( $jobs ) ); + public static function batchInsert( $jobs ) { + return JobQueueGroup::singleton()->push( $jobs ); } /** @@ -301,45 +94,34 @@ abstract class Job { * large batches of jobs can cause slave lag. * * @param $jobs array of Job objects + * @deprecated 1.21 */ - static function safeBatchInsert( $jobs ) { - if ( !count( $jobs ) ) { - return; - } - $dbw = wfGetDB( DB_MASTER ); - $rows = array(); - foreach ( $jobs as $job ) { - $rows[] = $job->insertFields(); - if ( count( $rows ) >= 500 ) { - $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' ); - $rows = array(); - } - } - if ( $rows ) { // last chunk - $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' ); - } - wfIncrStats( 'job-insert', count( $jobs ) ); + public static function safeBatchInsert( $jobs ) { + return JobQueueGroup::singleton()->push( $jobs, JobQueue::QoS_Atomic ); } - /** - * SQL conditions to apply on most JobQueue queries + * Pop a job of a certain type. This tries less hard than pop() to + * actually find a job; it may be adversely affected by concurrent job + * runners. * - * Whenever we exclude jobs types from the default queue, we want to make - * sure that queries to the job queue actually ignore them. + * @param $type string + * @return Job + * @deprecated 1.21 + */ + public static function pop_type( $type ) { + return JobQueueGroup::singleton()->get( $type )->pop(); + } + + /** + * Pop a job off the front of the queue. + * This is subject to $wgJobTypesExcludedFromDefaultQueue. * - * @return array SQL conditions suitable for Database:: methods + * @return Job or false if there's no jobs + * @deprecated 1.21 */ - static function defaultQueueConditions( ) { - global $wgJobTypesExcludedFromDefaultQueue; - $conditions = array(); - if ( count( $wgJobTypesExcludedFromDefaultQueue ) > 0 ) { - $dbr = wfGetDB( DB_SLAVE ); - foreach ( $wgJobTypesExcludedFromDefaultQueue as $cmdType ) { - $conditions[] = "job_cmd != " . $dbr->addQuotes( $cmdType ); - } - } - return $conditions; + public static function pop() { + return JobQueueGroup::singleton()->pop(); } /*------------------------------------------------------------------------- @@ -352,77 +134,63 @@ abstract class Job { * @param $params array|bool * @param $id int */ - function __construct( $command, $title, $params = false, $id = 0 ) { + public function __construct( $command, $title, $params = false, $id = 0 ) { $this->command = $command; $this->title = $title; $this->params = $params; $this->id = $id; - // A bit of premature generalisation - // Oh well, the whole class is premature generalisation really - $this->removeDuplicates = true; + $this->removeDuplicates = false; // expensive jobs may set this to true } /** - * Insert a single job into the queue. - * @return bool true on success + * @return integer May be 0 for jobs stored outside the DB */ - function insert() { - $fields = $this->insertFields(); + public function getId() { + return $this->id; + } - $dbw = wfGetDB( DB_MASTER ); + /** + * @return string + */ + public function getType() { + return $this->command; + } - if ( $this->removeDuplicates ) { - $res = $dbw->select( 'job', array( '1' ), $fields, __METHOD__ ); - if ( $dbw->numRows( $res ) ) { - return true; - } - } - wfIncrStats( 'job-insert' ); - return $dbw->insert( 'job', $fields, __METHOD__ ); + /** + * @return Title + */ + public function getTitle() { + return $this->title; } /** * @return array */ - protected function insertFields() { - $dbw = wfGetDB( DB_MASTER ); - return array( - 'job_id' => $dbw->nextSequenceValue( 'job_job_id_seq' ), - 'job_cmd' => $this->command, - 'job_namespace' => $this->title->getNamespace(), - 'job_title' => $this->title->getDBkey(), - 'job_timestamp' => $dbw->timestamp(), - 'job_params' => Job::makeBlob( $this->params ) - ); + public function getParams() { + return $this->params; } /** - * Remove jobs in the job queue which are duplicates of this job. - * This is deadlock-prone and so starts its own transaction. + * @return bool */ - function removeDuplicates() { - if ( !$this->removeDuplicates ) { - return; - } + public function ignoreDuplicates() { + return $this->removeDuplicates; + } - $fields = $this->insertFields(); - unset( $fields['job_id'] ); - unset( $fields['job_timestamp'] ); - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin( __METHOD__ ); - $dbw->delete( 'job', $fields, __METHOD__ ); - $affected = $dbw->affectedRows(); - $dbw->commit( __METHOD__ ); - if ( $affected ) { - wfIncrStats( 'job-dup-delete', $affected ); - } + /** + * Insert a single job into the queue. + * @return bool true on success + * @deprecated 1.21 + */ + public function insert() { + return JobQueueGroup::singleton()->push( $this ); } /** * @return string */ - function toString() { + public function toString() { $paramString = ''; if ( $this->params ) { foreach ( $this->params as $key => $value ) { @@ -448,7 +216,7 @@ abstract class Job { $this->error = $error; } - function getLastError() { + public function getLastError() { return $this->error; } } diff --git a/includes/job/JobQueue.php b/includes/job/JobQueue.php new file mode 100644 index 0000000000..6409cffc41 --- /dev/null +++ b/includes/job/JobQueue.php @@ -0,0 +1,185 @@ +wiki = $params['wiki']; + $this->type = $params['type']; + } + + /** + * Get a job queue object of the specified type. + * $params includes: + * class : what job class to use (determines job type) + * wiki : wiki ID of the wiki the jobs are for (defaults to current wiki) + * type : The name of the job types this queue handles + * + * @param $params array + * @return JobQueue + * @throws MWException + */ + final public static function factory( array $params ) { + $class = $params['class']; + if ( !MWInit::classExists( $class ) ) { + throw new MWException( "Invalid job queue class '$class'." ); + } + $obj = new $class( $params ); + if ( !( $obj instanceof self ) ) { + throw new MWException( "Class '$class' is not a " . __CLASS__ . " class." ); + } + return $obj; + } + + /** + * @return string Wiki ID + */ + final public function getWiki() { + return $this->wiki; + } + + /** + * @return string Job type that this queue handles + */ + final public function getType() { + return $this->type; + } + + /** + * @return bool Quickly check if the queue is empty + */ + final public function isEmpty() { + wfProfileIn( __METHOD__ ); + $res = $this->doIsEmpty(); + wfProfileOut( __METHOD__ ); + return $res; + } + + /** + * @see JobQueue::isEmpty() + * @return bool + */ + abstract protected function doIsEmpty(); + + /** + * Push a batch of jobs into the queue + * + * @param $jobs array List of Jobs + * @param $flags integer Bitfield (supports JobQueue::QoS_Atomic) + * @return bool + */ + final public function batchPush( array $jobs, $flags = 0 ) { + foreach ( $jobs as $job ) { + if ( $job->getType() !== $this->type ) { + throw new MWException( "Got '{$job->getType()}' job; expected '{$this->type}'." ); + } + } + wfProfileIn( __METHOD__ ); + $ok = $this->doBatchPush( $jobs, $flags ); + if ( $ok ) { + wfIncrStats( 'job-insert', count( $jobs ) ); + } + wfProfileOut( __METHOD__ ); + return $ok; + } + + /** + * @see JobQueue::batchPush() + * @return bool + */ + abstract protected function doBatchPush( array $jobs, $flags ); + + /** + * Pop a job off of the queue + * + * @return Job|bool Returns false on failure + */ + final public function pop() { + wfProfileIn( __METHOD__ ); + $job = $this->doPop(); + if ( $job ) { + wfIncrStats( 'job-pop' ); + } + wfProfileOut( __METHOD__ ); + return $job; + } + + /** + * @see JobQueue::pop() + * @return Job + */ + abstract protected function doPop(); + + /** + * Acknowledge that a job was completed + * + * @param $job Job + * @return bool + */ + final public function ack( Job $job ) { + if ( $job->getType() !== $this->type ) { + throw new MWException( "Got '{$job->getType()}' job; expected '{$this->type}'." ); + } + wfProfileIn( __METHOD__ ); + $ok = $this->doAck( $job ); + wfProfileOut( __METHOD__ ); + return $ok; + } + + /** + * @see JobQueue::ack() + * @return bool + */ + abstract protected function doAck( Job $job ); + + /** + * Wait for any slaves or backup servers to catch up + * + * @return void + */ + final public function waitForBackups() { + wfProfileIn( __METHOD__ ); + $this->doWaitForBackups(); + wfProfileOut( __METHOD__ ); + } + + /** + * @see JobQueue::waitForBackups() + * @return void + */ + protected function doWaitForBackups() {} +} diff --git a/includes/job/JobQueueDB.php b/includes/job/JobQueueDB.php new file mode 100644 index 0000000000..bea4a6f69a --- /dev/null +++ b/includes/job/JobQueueDB.php @@ -0,0 +1,318 @@ +getEmptinessCacheKey(); + + $isEmpty = $wgMemc->get( $key ); + if ( $isEmpty === 'true' ) { + return true; + } elseif ( $isEmpty === 'false' ) { + return false; + } + + $found = $this->getSlaveDB()->selectField( + 'job', '1', array( 'job_cmd' => $this->type ), __METHOD__ + ); + + $wgMemc->add( $key, $found ? 'false' : 'true', self::CACHE_TTL ); + } + + /** + * @see JobQueue::doBatchPush() + * @return bool + */ + protected function doBatchPush( array $jobs, $flags ) { + if ( count( $jobs ) ) { + $dbw = $this->getMasterDB(); + + $rows = array(); + foreach ( $jobs as $job ) { + $rows[] = $this->insertFields( $job ); + } + $atomic = ( $flags & self::QoS_Atomic ); + $key = $this->getEmptinessCacheKey(); + $ttl = self::CACHE_TTL; + + $dbw->onTransactionIdle( function() use ( $dbw, $rows, $atomic, $key, $ttl ) { + global $wgMemc; + + $autoTrx = $dbw->getFlag( DBO_TRX ); // automatic begin() enabled? + if ( $atomic ) { + $dbw->begin(); // wrap all the job additions in one transaction + } else { + $dbw->clearFlag( DBO_TRX ); // make each query its own transaction + } + try { + foreach ( array_chunk( $rows, 50 ) as $rowBatch ) { // avoid slave lag + $dbw->insert( 'job', $rowBatch, __METHOD__ ); + } + } catch ( DBError $e ) { + if ( $atomic ) { + $dbw->rollback(); + } else { + $dbw->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin() + } + throw $e; + } + if ( $atomic ) { + $dbw->commit(); + } else { + $dbw->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin() + } + + $wgMemc->set( $key, 'false', $ttl ); + } ); + } + + return true; + } + + /** + * @see JobQueue::doPop() + * @return Job|bool + */ + protected function doPop() { + global $wgMemc; + + $uuid = wfRandomString( 32 ); // pop attempt + + $dbw = $this->getMasterDB(); + $dbw->commit( __METHOD__, 'flush' ); // flush existing transaction + + $job = false; // job popped off + $autoTrx = $dbw->getFlag( DBO_TRX ); // automatic begin() enabled? + $dbw->clearFlag( DBO_TRX ); // make each query its own transaction + try { + do { // retry when our row is invalid or deleted as a duplicate + $row = false; // row claimed + $rand = mt_rand( 0, self::MAX_JOB_RANDOM ); // encourage concurrent UPDATEs + $gte = (bool)mt_rand( 0, 1 ); // find rows with rand before/after $rand + // Try to reserve a DB row... + if ( $this->claim( $uuid, $rand, $gte ) || $this->claim( $uuid, $rand, !$gte ) ) { + // Fetch any row that we just reserved... + $row = $dbw->selectRow( 'job', '*', + array( 'job_cmd' => $this->type, 'job_token' => $uuid ), __METHOD__ ); + // Check if another process deleted it as a duplicate + if ( !$row ) { + wfDebugLog( 'JobQueueDB', "Row deleted as duplicate by another process." ); + continue; // try again + } + // Get the job object from the row... + $title = Title::makeTitleSafe( $row->job_namespace, $row->job_title ); + if ( !$title ) { + $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); + wfDebugLog( 'JobQueueDB', "Row has invalid title '{$row->job_title}'." ); + continue; // try again + } + $job = Job::factory( $row->job_cmd, $title, + self::extractBlob( $row->job_params ), $row->job_id ); + // Delete any *other* duplicate jobs in the queue... + if ( $job->ignoreDuplicates() && strlen( $row->job_sha1 ) ) { + $dbw->delete( 'job', + array( 'job_sha1' => $row->job_sha1, + "job_id != {$dbw->addQuotes( $row->job_id )}" ), + __METHOD__ + ); + } + } else { + $wgMemc->set( $this->getEmptinessCacheKey(), 'true', self::CACHE_TTL ); + } + break; // done + } while( true ); + } catch ( DBError $e ) { + $dbw->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin() + throw $e; + } + $dbw->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin() + + return $job; + } + + /** + * Reserve a row with a single UPDATE without holding row locks over RTTs... + * @param $uuid string 32 char hex string + * @param $rand integer Random unsigned integer (31 bits) + * @param $gte bool Search for job_random >= $random (otherwise job_random <= $random) + * @return integer Number of affected rows + */ + protected function claim( $uuid, $rand, $gte ) { + $dbw = $this->getMasterDB(); + $dir = $gte ? 'ASC' : 'DESC'; + $ineq = $gte ? '>=' : '<='; + if ( $dbw->getType() === 'mysql' ) { + // Per http://bugs.mysql.com/bug.php?id=6980, we can't use subqueries on the + // same table being changed in an UPDATE query in MySQL (gives Error: 1093). + // Oracle and Postgre have no such limitation. However, MySQL offers an + // alternative here by supporting ORDER BY + LIMIT for UPDATE queries. + // The DB wrapper functions do not support this, so it's done manually. + $dbw->query( "UPDATE {$dbw->tableName( 'job' )} + SET + job_token = {$dbw->addQuotes( $uuid ) }, + job_token_timestamp = {$dbw->addQuotes( $dbw->timestamp() )} + WHERE ( + job_cmd = {$dbw->addQuotes( $this->type )} + AND job_token = {$dbw->addQuotes( '' )} + AND job_random {$ineq} {$dbw->addQuotes( $rand )} + ) ORDER BY job_random {$dir} LIMIT 1", + __METHOD__ + ); + } else { + // Use a subquery to find the job, within an UPDATE to claim it. + // This uses as much of the DB wrapper functions as possible. + $dbw->update( 'job', + array( 'job_token' => $uuid, 'job_token_timestamp' => $dbw->timestamp() ), + array( 'job_id = (' . + $dbw->selectSQLText( 'job', 'job_id', + array( + 'job_cmd' => $this->type, + 'job_token' => '', + "job_random {$ineq} {$dbw->addQuotes( $rand )}" ), + __METHOD__, + array( 'ORDER BY' => "job_random {$dir}", 'LIMIT' => 1 ) ) . + ')' + ), + __METHOD__ + ); + } + return $dbw->affectedRows(); + } + + /** + * @see JobQueue::doAck() + * @return Job|bool + */ + protected function doAck( Job $job ) { + $dbw = $this->getMasterDB(); + if ( $dbw->trxLevel() ) { + wfWarn( "Attempted to ack a job in a transaction; committing first." ); + $dbw->commit(); // push existing transaction + } + + $autoTrx = $dbw->getFlag( DBO_TRX ); // automatic begin() enabled? + $dbw->clearFlag( DBO_TRX ); // make each query its own transaction + try { + // Delete a row with a single DELETE without holding row locks over RTTs... + $dbw->delete( 'job', array( 'job_cmd' => $this->type, 'job_id' => $job->getId() ) ); + } catch ( Exception $e ) { + $dbw->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin() + throw $e; + } + $dbw->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin() + + return true; + } + + /** + * @see JobQueue::doWaitForBackups() + * @return void + */ + protected function doWaitForBackups() { + wfWaitForSlaves(); + } + + /** + * @return DatabaseBase + */ + protected function getSlaveDB() { + return wfGetDB( DB_SLAVE, array(), $this->wiki ); + } + + /** + * @return DatabaseBase + */ + protected function getMasterDB() { + return wfGetDB( DB_MASTER, array(), $this->wiki ); + } + + /** + * @param $job Job + * @return array + */ + protected function insertFields( Job $job ) { + // Rows that describe the nature of the job + $descFields = array( + 'job_cmd' => $job->getType(), + 'job_namespace' => $job->getTitle()->getNamespace(), + 'job_title' => $job->getTitle()->getDBkey(), + 'job_params' => self::makeBlob( $job->getParams() ), + ); + // Additional job metadata + $dbw = $this->getMasterDB(); + $metaFields = array( + 'job_id' => $dbw->nextSequenceValue( 'job_job_id_seq' ), + 'job_timestamp' => $dbw->timestamp(), + 'job_sha1' => wfBaseConvert( sha1( serialize( $descFields ) ), 16, 36, 32 ), + 'job_random' => mt_rand( 0, self::MAX_JOB_RANDOM ) + ); + return ( $descFields + $metaFields ); + } + + /** + * @return string + */ + private function getEmptinessCacheKey() { + list( $db, $prefix ) = wfSplitWikiID( $this->wiki ); + return wfForeignMemcKey( $db, $prefix, 'jobqueue', $this->type, 'isempty' ); + } + + /** + * @param $params + * @return string + */ + protected static function makeBlob( $params ) { + if ( $params !== false ) { + return serialize( $params ); + } else { + return ''; + } + } + + /** + * @param $blob + * @return bool|mixed + */ + protected static function extractBlob( $blob ) { + if ( (string)$blob !== '' ) { + return unserialize( $blob ); + } else { + return false; + } + } +} diff --git a/includes/job/JobQueueGroup.php b/includes/job/JobQueueGroup.php new file mode 100644 index 0000000000..69bcf011be --- /dev/null +++ b/includes/job/JobQueueGroup.php @@ -0,0 +1,160 @@ +wiki = $wiki; + } + + /** + * @param $wiki string Wiki ID + * @return JobQueueGroup + */ + public static function singleton( $wiki = false ) { + $wiki = ( $wiki === false ) ? wfWikiID() : $wiki; + if ( !isset( self::$instances[$wiki] ) ) { + self::$instances[$wiki] = new self( $wiki ); + } + return self::$instances[$wiki]; + } + + /** + * @param $type string + * @return JobQueue Job queue object for a given queue type + */ + public function get( $type ) { + global $wgJobTypeConf; + + $conf = false; + if ( isset( $wgJobTypeConf[$type] ) ) { + $conf = $wgJobTypeConf[$type]; + } else { + $conf = $wgJobTypeConf['default']; + } + + return JobQueue::factory( array( + 'class' => $conf['class'], + 'wiki' => $this->wiki, + 'type' => $type, + ) ); + } + + /** + * Insert jobs into the respective queues of with the belong + * + * @param $jobs Job|array A single Job or a list of Jobs + * @return bool + */ + public function push( $jobs ) { + $jobs = is_array( $jobs ) ? $jobs : array( $jobs ); + + $jobsByType = array(); // (job type => list of jobs) + foreach ( $jobs as $job ) { + if ( $job instanceof Job ) { + $jobsByType[$job->getType()][] = $job; + } else { + throw new MWException( "Attempted to push a non-Job object into a queue." ); + } + } + + $ok = true; + foreach ( $jobsByType as $type => $jobs ) { + if ( !$this->get( $type )->batchPush( $jobs ) ) { + $ok = false; + } + } + + return $ok; + } + + /** + * Pop a job off one of the job queues + * + * @param $type integer JobQueueGroup::TYPE_* constant + * @return Job|bool Returns false on failure + */ + public function pop( $type = self::TYPE_DEFAULT ) { + $types = ( $type == self::TYPE_DEFAULT ) + ? $this->getDefaultQueueTypes() + : $this->getQueueTypes(); + shuffle( $types ); // avoid starvation + + foreach ( $types as $type ) { // for each queue... + $job = $this->get( $type )->pop(); + if ( $job ) { + return $job; // found + } + } + + return false; // no jobs found + } + + /** + * Acknowledge that a job was completed + * + * @param $job Job + * @return bool + */ + public function ack( Job $job ) { + return $this->get( $job->getType() )->ack( $job ); + } + + /** + * Get the list of queue types + * + * @return array List of strings + */ + public function getQueueTypes() { + global $wgJobClasses; + + return array_keys( $wgJobClasses ); + } + + /** + * Get the list of default queue types + * + * @return array List of strings + */ + public function getDefaultQueueTypes() { + global $wgJobTypesExcludedFromDefaultQueue; + + return array_diff( $this->getQueueTypes(), $wgJobTypesExcludedFromDefaultQueue ); + } +} diff --git a/includes/job/RefreshLinksJob.php b/includes/job/RefreshLinksJob.php index b23951c6f5..a29f29fe64 100644 --- a/includes/job/RefreshLinksJob.php +++ b/includes/job/RefreshLinksJob.php @@ -27,9 +27,9 @@ * @ingroup JobQueue */ class RefreshLinksJob extends Job { - function __construct( $title, $params = '', $id = 0 ) { parent::__construct( 'refreshLinks', $title, $params, $id ); + $this->removeDuplicates = true; // job is expensive } /** @@ -70,16 +70,16 @@ class RefreshLinksJob extends Job { } public static function runForTitleInternal( Title $title, Revision $revision, $fname ) { - global $wgParser, $wgContLang; + global $wgContLang; wfProfileIn( $fname . '-parse' ); $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); - $parserOutput = $wgParser->parse( - $revision->getText(), $title, $options, true, true, $revision->getId() ); + $content = $revision->getContent(); + $parserOutput = $content->getParserOutput( $title, $revision->getId(), $options, false ); wfProfileOut( $fname . '-parse' ); wfProfileIn( $fname . '-update' ); - $updates = $parserOutput->getSecondaryDataUpdates( $title, false ); + $updates = $content->getSecondaryDataUpdates( $title, null, false, $parserOutput ); DataUpdate::runUpdates( $updates ); wfProfileOut( $fname . '-update' ); } diff --git a/includes/libs/JavaScriptMinifier.php b/includes/libs/JavaScriptMinifier.php index 0b4be9ae74..db5326c7cf 100644 --- a/includes/libs/JavaScriptMinifier.php +++ b/includes/libs/JavaScriptMinifier.php @@ -59,7 +59,7 @@ class JavaScriptMinifier { const TYPE_DO = 15; // keywords: case, var, finally, else, do, try const TYPE_FUNC = 16; // keywords: function const TYPE_LITERAL = 17; // all literals, identifiers and unrecognised tokens - + // Sanity limit to avoid excessive memory usage const STACK_LIMIT = 1000; @@ -385,7 +385,7 @@ class JavaScriptMinifier { self::TYPE_LITERAL => true ) ); - + // Rules for when newlines should be inserted if // $statementsOnOwnLine is enabled. // $newlineBefore is checked before switching state, @@ -514,7 +514,7 @@ class JavaScriptMinifier { return self::parseError($s, $end, 'Number with several E' ); } $end++; - + // + sign is optional; - sign is required. $end += strspn( $s, '-+', $end ); $len = strspn( $s, '0123456789', $end ); @@ -564,13 +564,13 @@ class JavaScriptMinifier { $out .= ' '; $lineLength++; } - + $out .= $token; $lineLength += $end - $pos; // += strlen( $token ) $last = $s[$end - 1]; $pos = $end; $newlineFound = false; - + // Output a newline after the token if required // This is checked before AND after switching state $newlineAdded = false; @@ -589,7 +589,7 @@ class JavaScriptMinifier { } elseif( isset( $goto[$state][$type] ) ) { $state = $goto[$state][$type]; } - + // Check for newline insertion again if ( $statementsOnOwnLine && !$newlineAdded && isset( $newlineAfter[$state][$type] ) ) { $out .= "\n"; @@ -598,7 +598,7 @@ class JavaScriptMinifier { } return $out; } - + static function parseError($fullJavascript, $position, $errorMsg) { // TODO: Handle the error: trigger_error, throw exception, return false... return false; diff --git a/includes/logging/LogEntry.php b/includes/logging/LogEntry.php index 37560d8055..2ca525e159 100644 --- a/includes/logging/LogEntry.php +++ b/includes/logging/LogEntry.php @@ -335,9 +335,9 @@ class ManualLogEntry extends LogEntryBase { /** * Constructor. - * + * * @since 1.19 - * + * * @param string $type * @param string $subtype */ @@ -357,9 +357,9 @@ class ManualLogEntry extends LogEntryBase { * '4:color' => 'blue', * 'animal' => 'dog' * ); - * + * * @since 1.19 - * + * * @param $parameters array Associative array */ public function setParameters( $parameters ) { @@ -368,9 +368,9 @@ class ManualLogEntry extends LogEntryBase { /** * Set the user that performed the action being logged. - * + * * @since 1.19 - * + * * @param User $performer */ public function setPerformer( User $performer ) { @@ -379,9 +379,9 @@ class ManualLogEntry extends LogEntryBase { /** * Set the title of the object changed. - * + * * @since 1.19 - * + * * @param Title $target */ public function setTarget( Title $target ) { @@ -390,9 +390,9 @@ class ManualLogEntry extends LogEntryBase { /** * Set the timestamp of when the logged action took place. - * + * * @since 1.19 - * + * * @param string $timestamp */ public function setTimestamp( $timestamp ) { @@ -401,9 +401,9 @@ class ManualLogEntry extends LogEntryBase { /** * Set a comment associated with the action being logged. - * + * * @since 1.19 - * + * * @param string $comment */ public function setComment( $comment ) { @@ -412,9 +412,9 @@ class ManualLogEntry extends LogEntryBase { /** * TODO: document - * + * * @since 1.19 - * + * * @param integer $deleted */ public function setDeleted( $deleted ) { diff --git a/includes/logging/LogFormatter.php b/includes/logging/LogFormatter.php index 7586bb6548..7d94a30bd4 100644 --- a/includes/logging/LogFormatter.php +++ b/includes/logging/LogFormatter.php @@ -429,6 +429,7 @@ class LogFormatter { * value in consideration. * @param $title Title the page * @param $parameters array query parameters + * @throws MWException * @return String */ protected function makePageLink( Title $title = null, $parameters = array() ) { diff --git a/includes/logging/LogPage.php b/includes/logging/LogPage.php index d96a5ea51a..90393ea8d6 100644 --- a/includes/logging/LogPage.php +++ b/includes/logging/LogPage.php @@ -220,7 +220,7 @@ class LogPage { } /** - * Generate text for a log entry. + * Generate text for a log entry. * Only LogFormatter should call this function. * * @param $type String: log type diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index 99ac854ba1..ca9b6363cb 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -623,7 +623,8 @@ class BitmapHandler extends ImageHandler { * to filter down to users. * * @param $path string The file path - * @param $scene string The scene specification, or false if there is none + * @param bool|string $scene The scene specification, or false if there is none + * @throws MWException * @return string */ function escapeMagickInput( $path, $scene = false ) { @@ -654,7 +655,8 @@ class BitmapHandler extends ImageHandler { * helper function for escapeMagickInput() and escapeMagickOutput(). * * @param $path string The file path - * @param $scene string The scene specification, or false if there is none + * @param bool|string $scene The scene specification, or false if there is none + * @throws MWException * @return string */ protected function escapeMagickPath( $path, $scene = false ) { diff --git a/includes/media/BitmapMetadataHandler.php b/includes/media/BitmapMetadataHandler.php index 0a195547eb..2b04f78b53 100644 --- a/includes/media/BitmapMetadataHandler.php +++ b/includes/media/BitmapMetadataHandler.php @@ -240,7 +240,7 @@ class BitmapMetadataHandler { unset( $baseArray['comment'] ); unset( $baseArray['xmp'] ); - + $baseArray['metadata'] = $meta->getMetadataArray(); $baseArray['metadata']['_MW_GIF_VERSION'] = GIFMetadataExtractor::VERSION; return $baseArray; @@ -257,6 +257,7 @@ class BitmapMetadataHandler { * * The various exceptions this throws are caught later. * @param $filename String + * @throws MWException * @return Array The metadata. */ static public function Tiff ( $filename ) { diff --git a/includes/media/DjVuImage.php b/includes/media/DjVuImage.php index 6aef562b12..e2fcaa932c 100644 --- a/includes/media/DjVuImage.php +++ b/includes/media/DjVuImage.php @@ -228,7 +228,7 @@ class DjVuImage { function retrieveMetaData() { global $wgDjvuToXML, $wgDjvuDump, $wgDjvuTxt; wfProfileIn( __METHOD__ ); - + if ( isset( $wgDjvuDump ) ) { # djvudump is faster as of version 3.5 # http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583 @@ -247,7 +247,7 @@ class DjVuImage { $xml = null; } # Text layer - if ( isset( $wgDjvuTxt ) ) { + if ( isset( $wgDjvuTxt ) ) { wfProfileIn( 'djvutxt' ); $cmd = wfEscapeShellArg( $wgDjvuTxt ) . ' --detail=page ' . wfEscapeShellArg( $this->mFilename ) ; wfDebug( __METHOD__.": $cmd\n" ); @@ -260,7 +260,7 @@ class DjVuImage { $reg = << # Text to match is composed of atoms of either: - \\\\. # - any escaped character + \\\\. # - any escaped character | # - any character different from " and \ [^"\\\\]+ )*?) diff --git a/includes/media/Exif.php b/includes/media/Exif.php index 784a6018c0..bdacbc86d9 100644 --- a/includes/media/Exif.php +++ b/includes/media/Exif.php @@ -104,6 +104,7 @@ class Exif { * * @param $file String: filename. * @param $byteOrder String Type of byte ordering either 'BE' (Big Endian) or 'LE' (Little Endian). Default ''. + * @throws MWException * @todo FIXME: The following are broke: * SubjectArea. Need to test the more obscure tags. * @@ -388,7 +389,7 @@ class Exif { $this->charCodeString( 'UserComment' ); $this->charCodeString( 'GPSProcessingMethod'); $this->charCodeString( 'GPSAreaInformation' ); - + //ComponentsConfiguration should really be an array instead of a string... //This turns a string of binary numbers into an array of numbers. @@ -401,7 +402,7 @@ class Exif { $ccVals['_type'] = 'ol'; //this is for formatting later. $this->mFilteredExifData['ComponentsConfiguration'] = $ccVals; } - + //GPSVersion(ID) is treated as the wrong type by php exif support. //Go through each byte turning it into a version string. //For example: "\x02\x02\x00\x00" -> "2.2.0.0" @@ -450,8 +451,7 @@ class Exif { } $charCode = substr( $this->mFilteredExifData[$prop], 0, 8); $val = substr( $this->mFilteredExifData[$prop], 8); - - + switch ($charCode) { case "\x4A\x49\x53\x00\x00\x00\x00\x00": //JIS @@ -480,7 +480,7 @@ class Exif { wfRestoreWarnings(); } } - + //trim and check to make sure not only whitespace. $val = trim($val); if ( strlen( $val ) === 0 ) { @@ -748,10 +748,10 @@ class Exif { return false; } if( $count > 1 ) { - foreach( $val as $v ) { + foreach( $val as $v ) { if( !$this->validate( $section, $tag, $v, true ) ) { - return false; - } + return false; + } } return true; } diff --git a/includes/media/FormatMetadata.php b/includes/media/FormatMetadata.php index 843c1fa22d..f2710f7e80 100644 --- a/includes/media/FormatMetadata.php +++ b/includes/media/FormatMetadata.php @@ -1361,7 +1361,7 @@ class FormatExif { * @param $meta array */ function FormatExif( $meta ) { - wfDeprecated(__METHOD__); + wfDeprecated( __METHOD__, '1.18' ); $this->meta = $meta; } diff --git a/includes/media/GIF.php b/includes/media/GIF.php index 84b9b8ca7d..da8fc6f87b 100644 --- a/includes/media/GIF.php +++ b/includes/media/GIF.php @@ -29,7 +29,7 @@ class GIFHandler extends BitmapHandler { const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata. - + function getMetadata( $image, $filename ) { try { $parsedGIFMetadata = BitmapMetadataHandler::GIF( $filename ); @@ -143,7 +143,7 @@ class GIFHandler extends BitmapHandler { wfSuppressWarnings(); $metadata = unserialize($image->getMetadata()); wfRestoreWarnings(); - + if (!$metadata || $metadata['frameCount'] <= 1) { return $original; } @@ -151,19 +151,19 @@ class GIFHandler extends BitmapHandler { /* Preserve original image info string, but strip the last char ')' so we can add even more */ $info = array(); $info[] = $original; - + if ( $metadata['looped'] ) { $info[] = wfMessage( 'file-info-gif-looped' )->parse(); } - + if ( $metadata['frameCount'] > 1 ) { $info[] = wfMessage( 'file-info-gif-frames' )->numParams( $metadata['frameCount'] )->parse(); } - + if ( $metadata['duration'] ) { $info[] = $wgLang->formatTimePeriod( $metadata['duration'] ); } - + return $wgLang->commaList( $info ); } } diff --git a/includes/media/GIFMetadataExtractor.php b/includes/media/GIFMetadataExtractor.php index 5fc5c1a717..7a162c3fcb 100644 --- a/includes/media/GIFMetadataExtractor.php +++ b/includes/media/GIFMetadataExtractor.php @@ -58,7 +58,7 @@ class GIFMetadataExtractor { $isLooped = false; $xmp = ""; $comment = array(); - + if ( !$filename ) { throw new Exception( "No file name specified" ); } elseif ( !file_exists( $filename ) || is_dir( $filename ) ) { @@ -107,7 +107,7 @@ class GIFMetadataExtractor { ## Read GCT self::readGCT( $fh, $bpp ); fread( $fh, 1 ); - self::skipBlock( $fh ); + self::skipBlock( $fh ); } elseif ( $buf == self::$gif_extension_sep ) { $buf = fread( $fh, 1 ); if ( strlen( $buf ) < 1 ) throw new Exception( "Ran out of input" ); @@ -182,23 +182,22 @@ class GIFMetadataExtractor { // NETSCAPE2.0 (application name for animated gif) if ( $data == 'NETSCAPE2.0' ) { - $data = fread( $fh, 2 ); // Block length and introduction, should be 03 01 if ($data != "\x03\x01") { throw new Exception( "Expected \x03\x01, got $data" ); } - + // Unsigned little-endian integer, loop count or zero for "forever" $loopData = fread( $fh, 2 ); if ( strlen( $loopData ) < 2 ) throw new Exception( "Ran out of input" ); $loopData = unpack( 'v', $loopData ); $loopCount = $loopData[1]; - + if ($loopCount != 1) { $isLooped = true; } - + // Read out terminator byte fread( $fh, 1 ); } elseif ( $data == 'XMP DataXMP' ) { @@ -260,6 +259,7 @@ class GIFMetadataExtractor { /** * @param $data + * @throws Exception * @return int */ static function decodeBPP( $data ) { @@ -276,7 +276,7 @@ class GIFMetadataExtractor { /** * @param $fh - * @return + * @throws Exception */ static function skipBlock( $fh ) { while ( !feof( $fh ) ) { @@ -290,6 +290,7 @@ class GIFMetadataExtractor { fread( $fh, $block_len ); } } + /** * Read a block. In the GIF format, a block is made up of * several sub-blocks. Each sub block starts with one byte @@ -301,6 +302,7 @@ class GIFMetadataExtractor { * sub-blocks in the returned value. Normally this is false, * except XMP is weird and does a hack where you need to keep * these length bytes. + * @throws Exception * @return string The data. */ static function readBlock( $fh, $includeLengths = false ) { diff --git a/includes/media/JpegMetadataExtractor.php b/includes/media/JpegMetadataExtractor.php index 8d7e43b905..60fd2a0f7b 100644 --- a/includes/media/JpegMetadataExtractor.php +++ b/includes/media/JpegMetadataExtractor.php @@ -129,7 +129,7 @@ class JpegMetadataExtractor { // whatever... $segments["XMP"] = substr( $temp, 29 ); wfDebug( __METHOD__ . ' Found XMP section with wrong app identifier ' - . "Using anyways.\n" ); + . "Using anyways.\n" ); } elseif ( substr( $temp, 0, 6 ) === "Exif\0\0" ) { // Just need to find out what the byte order is. // because php's exif plugin sucks... @@ -165,10 +165,11 @@ class JpegMetadataExtractor { } /** - * Helper function for jpegSegmentSplitter - * @param &$fh FileHandle for jpeg file - * @return string data content of segment. - */ + * Helper function for jpegSegmentSplitter + * @param &$fh FileHandle for jpeg file + * @throws MWException + * @return string data content of segment. + */ private static function jpegExtractMarker( &$fh ) { $size = wfUnpack( "nint", fread( $fh, 2 ), 2 ); if ( $size['int'] <= 2 ) { diff --git a/includes/media/MediaTransformOutput.php b/includes/media/MediaTransformOutput.php index 773824cb60..97a2d1d136 100644 --- a/includes/media/MediaTransformOutput.php +++ b/includes/media/MediaTransformOutput.php @@ -33,6 +33,13 @@ abstract class MediaTransformOutput { var $file; var $width, $height, $url, $page, $path; + + /** + * @var array Associative array mapping optional supplementary image files + * from pixel density (eg 1.5 or 2) to additional URLs. + */ + public $responsiveUrls = array(); + protected $storagePath = false; /** @@ -189,7 +196,10 @@ abstract class MediaTransformOutput { * @return array */ public function getDescLinkAttribs( $title = null, $params = '' ) { - $query = $this->page ? ( 'page=' . urlencode( $this->page ) ) : ''; + $query = ''; + if ( $this->page && $this->page !== 1 ) { + $query = 'page=' . urlencode( $this->page ); + } if( $params ) { $query .= $query ? '&'.$params : $params; } @@ -281,6 +291,7 @@ class ThumbnailImage extends MediaTransformOutput { * For images, desc-link and file-link are implemented as a click-through. For * sounds and videos, they may be displayed in other ways. * + * @throws MWException * @return string */ function toHtml( $options = array() ) { @@ -323,7 +334,7 @@ class ThumbnailImage extends MediaTransformOutput { 'alt' => $alt, 'src' => $this->url, 'width' => $this->width, - 'height' => $this->height, + 'height' => $this->height ); if ( !empty( $options['valign'] ) ) { $attribs['style'] = "vertical-align: {$options['valign']}"; @@ -331,6 +342,11 @@ class ThumbnailImage extends MediaTransformOutput { if ( !empty( $options['img-class'] ) ) { $attribs['class'] = $options['img-class']; } + + // Additional densities for responsive images, if specified. + if ( !empty( $this->responsiveUrls ) ) { + $attribs['srcset'] = Html::srcSet( $this->responsiveUrls ); + } return $this->linkWrap( $linkAttribs, Xml::element( 'img', $attribs ) ); } diff --git a/includes/media/PNG.php b/includes/media/PNG.php index 1b329e57b5..a23821f4fb 100644 --- a/includes/media/PNG.php +++ b/includes/media/PNG.php @@ -88,11 +88,11 @@ class PNGHandler extends BitmapHandler { function canAnimateThumbnail( $image ) { return false; } - + function getMetadataType( $image ) { return 'parsed-png'; } - + function isMetadataValid( $image, $metadata ) { if ( $metadata === self::BROKEN_FILE ) { @@ -134,21 +134,21 @@ class PNGHandler extends BitmapHandler { $info = array(); $info[] = $original; - + if ( $metadata['loopCount'] == 0 ) { $info[] = wfMessage( 'file-info-png-looped' )->parse(); } elseif ( $metadata['loopCount'] > 1 ) { $info[] = wfMessage( 'file-info-png-repeat' )->numParams( $metadata['loopCount'] )->parse(); } - + if ( $metadata['frameCount'] > 0 ) { $info[] = wfMessage( 'file-info-png-frames' )->numParams( $metadata['frameCount'] )->parse(); } - + if ( $metadata['duration'] ) { $info[] = $wgLang->formatTimePeriod( $metadata['duration'] ); } - + return $wgLang->commaList( $info ); } diff --git a/includes/media/PNGMetadataExtractor.php b/includes/media/PNGMetadataExtractor.php index 9dcde40694..55f087ad2e 100644 --- a/includes/media/PNGMetadataExtractor.php +++ b/includes/media/PNGMetadataExtractor.php @@ -124,7 +124,7 @@ class PNGMetadataExtractor { case 0: $colorType = 'greyscale'; break; - case 2: + case 2: $colorType = 'truecolour'; break; case 3: diff --git a/includes/media/SVG.php b/includes/media/SVG.php index 55fa5547bf..75d474c0ab 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -135,14 +135,15 @@ class SvgHandler extends ImageHandler { } /** - * Transform an SVG file to PNG - * This function can be called outside of thumbnail contexts - * @param string $srcPath - * @param string $dstPath - * @param string $width - * @param string $height - * @return bool|MediaTransformError - */ + * Transform an SVG file to PNG + * This function can be called outside of thumbnail contexts + * @param string $srcPath + * @param string $dstPath + * @param string $width + * @param string $height + * @throws MWException + * @return bool|MediaTransformError + */ public function rasterize( $srcPath, $dstPath, $width, $height ) { global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath; $err = false; diff --git a/includes/media/SVGMetadataExtractor.php b/includes/media/SVGMetadataExtractor.php index 851fe428a6..456c0166c2 100644 --- a/includes/media/SVGMetadataExtractor.php +++ b/includes/media/SVGMetadataExtractor.php @@ -52,6 +52,7 @@ class SVGReader { * * Creates an SVGReader drawing from the source provided * @param $source String: URI from which to read + * @throws MWException|Exception */ function __construct( $source ) { global $wgSVGMetadataCutoff; @@ -74,9 +75,9 @@ class SVGReader { $this->reader->open( $source, null, LIBXML_NOERROR | LIBXML_NOWARNING ); } - // Expand entities, since Adobe Illustrator uses them for xmlns - // attributes (bug 31719). Note that libxml2 has some protection - // against large recursive entity expansions so this is not as + // Expand entities, since Adobe Illustrator uses them for xmlns + // attributes (bug 31719). Note that libxml2 has some protection + // against large recursive entity expansions so this is not as // insecure as it might appear to be. $this->reader->setParserProperty( XMLReader::SUBST_ENTITIES, true ); @@ -113,6 +114,7 @@ class SVGReader { /** * Read the SVG + * @throws MWException * @return bool */ public function read() { @@ -196,6 +198,7 @@ class SVGReader { * Read an XML snippet from an element * * @param String $metafield that we will fill with the result + * @throws MWException */ private function readXml( $metafield=null ) { $this->debug ( "Read top level metadata" ); diff --git a/includes/media/Tiff.php b/includes/media/Tiff.php index d95c907400..55dff77b07 100644 --- a/includes/media/Tiff.php +++ b/includes/media/Tiff.php @@ -70,8 +70,9 @@ class TiffHandler extends ExifBitmapHandler { } /** - * @param $image - * @param $filename + * @param File $image + * @param string $filename + * @throws MWException * @return string */ function getMetadata( $image, $filename ) { diff --git a/includes/media/XMP.php b/includes/media/XMP.php index 36660b3dcc..c5743d7286 100644 --- a/includes/media/XMP.php +++ b/includes/media/XMP.php @@ -232,17 +232,18 @@ class XMPReader { } /** - * Main function to call to parse XMP. Use getResults to - * get results. - * - * Also catches any errors during processing, writes them to - * debug log, blanks result array and returns false. - * - * @param $content String: XMP data - * @param $allOfIt Boolean: If this is all the data (true) or if its split up (false). Default true - * @param $reset Boolean: does xml parser need to be reset. Default false - * @return Boolean success. - */ + * Main function to call to parse XMP. Use getResults to + * get results. + * + * Also catches any errors during processing, writes them to + * debug log, blanks result array and returns false. + * + * @param $content String: XMP data + * @param $allOfIt Boolean: If this is all the data (true) or if its split up (false). Default true + * @param $reset Boolean: does xml parser need to be reset. Default false + * @throws MWException + * @return Boolean success. + */ public function parse( $content, $allOfIt = true, $reset = false ) { if ( $reset ) { $this->resetXMLParser(); @@ -463,22 +464,23 @@ class XMPReader { } /** - * Hit a closing element in MODE_STRUCT, MODE_SEQ, MODE_BAG - * generally means we've finished processing a nested structure. - * resets some internal variables to indicate that. - * - * Note this means we hit the closing element not the "". - * - * @par For example, when processing: - * @code{,xml} - * 64 - * - * @endcode - * - * This method is called when we hit the "" tag. - * - * @param $elm String namespace . space . tag name. - */ + * Hit a closing element in MODE_STRUCT, MODE_SEQ, MODE_BAG + * generally means we've finished processing a nested structure. + * resets some internal variables to indicate that. + * + * Note this means we hit the closing element not the "". + * + * @par For example, when processing: + * @code{,xml} + * 64 + * + * @endcode + * + * This method is called when we hit the "" tag. + * + * @param $elm String namespace . space . tag name. + * @throws MWException + */ private function endElementNested( $elm ) { /* cur item must be the same as $elm, unless if in MODE_STRUCT @@ -528,23 +530,24 @@ class XMPReader { } /** - * Hit a closing element in MODE_LI (either rdf:Seq, or rdf:Bag ) - * Add information about what type of element this is. - * - * Note we still have to hit the outer "" - * - * @par For example, when processing: - * @code{,xml} - * 64 - * - * @endcode - * - * This method is called when we hit the "". - * (For comparison, we call endElementModeSimple when we - * hit the "") - * - * @param $elm String namespace . ' ' . element name - */ + * Hit a closing element in MODE_LI (either rdf:Seq, or rdf:Bag ) + * Add information about what type of element this is. + * + * Note we still have to hit the outer "" + * + * @par For example, when processing: + * @code{,xml} + * 64 + * + * @endcode + * + * This method is called when we hit the "". + * (For comparison, we call endElementModeSimple when we + * hit the "") + * + * @param $elm String namespace . ' ' . element name + * @throws MWException + */ private function endElementModeLi( $elm ) { list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 ); @@ -599,17 +602,18 @@ class XMPReader { } /** - * Handler for hitting a closing element. - * - * generally just calls a helper function depending on what - * mode we're in. - * - * Ignores the outer wrapping elements that are optional in - * xmp and have no meaning. - * - * @param $parser XMLParser - * @param $elm String namespace . ' ' . element name - */ + * Handler for hitting a closing element. + * + * generally just calls a helper function depending on what + * mode we're in. + * + * Ignores the outer wrapping elements that are optional in + * xmp and have no meaning. + * + * @param $parser XMLParser + * @param $elm String namespace . ' ' . element name + * @throws MWException + */ function endElement( $parser, $elm ) { if ( $elm === ( self::NS_RDF . ' RDF' ) || $elm === 'adobe:ns:meta/ xmpmeta' @@ -759,22 +763,23 @@ class XMPReader { } /** - * Handle an opening element when in MODE_SIMPLE - * - * This should not happen often. This is for if a simple element - * already opened has a child element. Could happen for a - * qualified element. - * - * For example: - * 0/10 - * Bar - * - * - * This method is called when processing the element - * - * @param $elm String namespace and tag names separated by space. - * @param $attribs Array Attributes of the element. - */ + * Handle an opening element when in MODE_SIMPLE + * + * This should not happen often. This is for if a simple element + * already opened has a child element. Could happen for a + * qualified element. + * + * For example: + * 0/10 + * Bar + * + * + * This method is called when processing the element + * + * @param $elm String namespace and tag names separated by space. + * @param $attribs Array Attributes of the element. + * @throws MWException + */ private function startElementModeSimple( $elm, $attribs ) { if ( $elm === self::NS_RDF . ' Description' ) { // If this value has qualifiers @@ -824,16 +829,17 @@ class XMPReader { } /** - * Starting an element when in MODE_INITIAL - * This usually happens when we hit an element inside - * the outer rdf:Description - * - * This is generally where most properties start. - * - * @param $ns String Namespace - * @param $tag String tag name (without namespace prefix) - * @param $attribs Array array of attributes - */ + * Starting an element when in MODE_INITIAL + * This usually happens when we hit an element inside + * the outer rdf:Description + * + * This is generally where most properties start. + * + * @param $ns String Namespace + * @param $tag String tag name (without namespace prefix) + * @param $attribs Array array of attributes + * @throws MWException + */ private function startElementModeInitial( $ns, $tag, $attribs ) { if ( $ns !== self::NS_RDF ) { @@ -877,23 +883,24 @@ class XMPReader { } /** - * Hit an opening element when in a Struct (MODE_STRUCT) - * This is generally for fields of a compound property. - * - * Example of a struct (abbreviated; flash has more properties): - * - * True - * 1 - * - * or: - * - * True - * 1 - * - * @param $ns String namespace - * @param $tag String tag name (no ns) - * @param $attribs Array array of attribs w/ values. - */ + * Hit an opening element when in a Struct (MODE_STRUCT) + * This is generally for fields of a compound property. + * + * Example of a struct (abbreviated; flash has more properties): + * + * True + * 1 + * + * or: + * + * True + * 1 + * + * @param $ns String namespace + * @param $tag String tag name (no ns) + * @param $attribs Array array of attribs w/ values. + * @throws MWException + */ private function startElementModeStruct( $ns, $tag, $attribs ) { if ( $ns !== self::NS_RDF ) { @@ -1015,14 +1022,15 @@ class XMPReader { } /** - * Hits an opening element. - * Generally just calls a helper based on what MODE we're in. - * Also does some initial set up for the wrapper element - * - * @param $parser XMLParser - * @param $elm String namespace "" element - * @param $attribs Array attribute name => value - */ + * Hits an opening element. + * Generally just calls a helper based on what MODE we're in. + * Also does some initial set up for the wrapper element + * + * @param $parser XMLParser + * @param $elm String namespace "" element + * @param $attribs Array attribute name => value + * @throws MWException + */ function startElement( $parser, $elm, $attribs ) { if ( $elm === self::NS_RDF . ' RDF' @@ -1100,19 +1108,20 @@ class XMPReader { } /** - * Process attributes. - * Simple values can be stored as either a tag or attribute - * - * Often the initial "" tag just has all the simple - * properties as attributes. - * - * @par Example: - * @code - * - * @endcode - * - * @param $attribs Array attribute=>value array. - */ + * Process attributes. + * Simple values can be stored as either a tag or attribute + * + * Often the initial "" tag just has all the simple + * properties as attributes. + * + * @par Example: + * @code + * + * @endcode + * + * @param $attribs Array attribute=>value array. + * @throws MWException + */ private function doAttribs( $attribs ) { // first check for rdf:parseType attribute, as that can change diff --git a/includes/media/XMPInfo.php b/includes/media/XMPInfo.php index 83b8a102d0..aa411dfdf9 100644 --- a/includes/media/XMPInfo.php +++ b/includes/media/XMPInfo.php @@ -669,7 +669,7 @@ class XMPInfo { * 'validate' => 'validateClosed', * 'choices' => array( '1' => true, '2' => true ), * ), - */ + */ ), 'http://ns.adobe.com/exif/1.0/aux/' => array( 'Lens' => array( diff --git a/includes/media/XMPValidate.php b/includes/media/XMPValidate.php index 5ce3c00bde..e3fd59fdad 100644 --- a/includes/media/XMPValidate.php +++ b/includes/media/XMPValidate.php @@ -153,7 +153,7 @@ class XMPValidate { //check if its in a numeric range $inRange = false; - if ( isset( $info['rangeLow'] ) + if ( isset( $info['rangeLow'] ) && isset( $info['rangeHigh'] ) && is_numeric( $val ) && ( intval( $val ) <= $info['rangeHigh'] ) @@ -342,7 +342,7 @@ class XMPValidate { } $m = array(); - if ( preg_match( + if ( preg_match( '/(\d{1,3}),(\d{1,2}),(\d{1,2})([NWSE])/D', $val, $m ) ) { @@ -354,7 +354,7 @@ class XMPValidate { } $val = $coord; return; - } elseif ( preg_match( + } elseif ( preg_match( '/(\d{1,3}),(\d{1,2}(?:.\d*)?)([NWSE])/D', $val, $m ) ) { @@ -367,7 +367,7 @@ class XMPValidate { return; } else { - wfDebugLog( 'XMP', __METHOD__ + wfDebugLog( 'XMP', __METHOD__ . " Expected GPSCoordinate, but got $val." ); $val = null; return; diff --git a/includes/normal/Utf8CaseGenerate.php b/includes/normal/Utf8CaseGenerate.php index 368d0bcdff..d9deb3c36f 100644 --- a/includes/normal/Utf8CaseGenerate.php +++ b/includes/normal/Utf8CaseGenerate.php @@ -49,7 +49,7 @@ while( false !== ($line = fgets( $in ) ) ) { $name = $columns[1]; $simpleUpper = $columns[12]; $simpleLower = $columns[13]; - + $source = codepointToUtf8( hexdec( $codepoint ) ); if( $simpleUpper ) { $wikiUpperChars[$source] = codepointToUtf8( hexdec( $simpleUpper ) ); diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php index 08f85bd342..64d96187d5 100644 --- a/includes/normal/UtfNormal.php +++ b/includes/normal/UtfNormal.php @@ -765,7 +765,7 @@ class UtfNormal { * @param $string String The string * @return String String with the character codes replaced. */ - private static function replaceForNativeNormalize( $string ) { + private static function replaceForNativeNormalize( $string ) { $string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', UTF8_REPLACEMENT, diff --git a/includes/normal/UtfNormalBench.php b/includes/normal/UtfNormalBench.php index 944c443530..6642844e2b 100644 --- a/includes/normal/UtfNormalBench.php +++ b/includes/normal/UtfNormalBench.php @@ -19,7 +19,7 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html - * + * * @file * @ingroup UtfNormal */ diff --git a/includes/normal/UtfNormalDefines.php b/includes/normal/UtfNormalDefines.php index 5142a4143b..64624b8a14 100644 --- a/includes/normal/UtfNormalDefines.php +++ b/includes/normal/UtfNormalDefines.php @@ -2,7 +2,7 @@ /** * Some constant definitions for the unicode normalization module. * - * Note: these constants must all be resolvable at compile time by HipHop, + * Note: these constants must all be resolvable at compile time by HipHop, * since this file will not be executed during request startup for a compiled * MediaWiki. * diff --git a/includes/normal/UtfNormalGenerate.php b/includes/normal/UtfNormalGenerate.php index e4c1138ea3..11d06d4ccd 100644 --- a/includes/normal/UtfNormalGenerate.php +++ b/includes/normal/UtfNormalGenerate.php @@ -177,7 +177,7 @@ if( $out ) { * * @file */ - + UtfNormal::\$utfCombiningClass = unserialize( '$serCombining' ); UtfNormal::\$utfCanonicalComp = unserialize( '$serComp' ); UtfNormal::\$utfCanonicalDecomp = unserialize( '$serCanon' ); diff --git a/includes/objectcache/EhcacheBagOStuff.php b/includes/objectcache/EhcacheBagOStuff.php index f86cf1575a..60d0645c90 100644 --- a/includes/objectcache/EhcacheBagOStuff.php +++ b/includes/objectcache/EhcacheBagOStuff.php @@ -28,13 +28,14 @@ * @ingroup Cache */ class EhcacheBagOStuff extends BagOStuff { - var $servers, $cacheName, $connectTimeout, $timeout, $curlOptions, + var $servers, $cacheName, $connectTimeout, $timeout, $curlOptions, $requestData, $requestDataPos; - + var $curls = array(); /** * @param $params array + * @throws MWException */ function __construct( $params ) { if ( !defined( 'CURLOPT_TIMEOUT_MS' ) ) { @@ -48,7 +49,7 @@ class EhcacheBagOStuff extends BagOStuff { } $this->servers = $params['servers']; $this->cacheName = isset( $params['cache'] ) ? $params['cache'] : 'mw'; - $this->connectTimeout = isset( $params['connectTimeout'] ) + $this->connectTimeout = isset( $params['connectTimeout'] ) ? $params['connectTimeout'] : 1; $this->timeout = isset( $params['timeout'] ) ? $params['timeout'] : 1; $this->curlOptions = array( @@ -76,7 +77,7 @@ class EhcacheBagOStuff extends BagOStuff { if ( $response['http_code'] >= 300 ) { wfDebug( __METHOD__.": GET failure, got HTTP {$response['http_code']}\n" ); wfProfileOut( __METHOD__ ); - return false; + return false; } $body = $response['body']; $type = $response['content_type']; @@ -202,9 +203,9 @@ class EhcacheBagOStuff extends BagOStuff { * @return int */ protected function attemptPut( $key, $data, $type, $ttl ) { - // In initial benchmarking, it was 30 times faster to use CURLOPT_POST + // In initial benchmarking, it was 30 times faster to use CURLOPT_POST // than CURLOPT_UPLOAD with CURLOPT_READFUNCTION. This was because - // CURLOPT_UPLOAD was pushing the request headers first, then waiting + // CURLOPT_UPLOAD was pushing the request headers first, then waiting // for an ACK packet, then sending the data, whereas CURLOPT_POST just // sends the headers and the data in a single send(). $response = $this->doItemRequest( $key, @@ -231,7 +232,7 @@ class EhcacheBagOStuff extends BagOStuff { */ protected function createCache( $key ) { wfDebug( __METHOD__.": creating cache for $key\n" ); - $response = $this->doCacheRequest( $key, + $response = $this->doCacheRequest( $key, array( CURLOPT_POST => 1, CURLOPT_CUSTOMREQUEST => 'PUT', @@ -279,7 +280,7 @@ class EhcacheBagOStuff extends BagOStuff { if ( array_diff_key( $curlOptions, $this->curlOptions ) ) { // var_dump( array_diff_key( $curlOptions, $this->curlOptions ) ); throw new MWException( __METHOD__.": to prevent options set in one doRequest() " . - "call from affecting subsequent doRequest() calls, only options listed " . + "call from affecting subsequent doRequest() calls, only options listed " . "in \$this->curlOptions may be specified in the \$curlOptions parameter." ); } $curlOptions += $this->curlOptions; diff --git a/includes/objectcache/MemcachedBagOStuff.php b/includes/objectcache/MemcachedBagOStuff.php index 813c2727c4..643d2e94bf 100644 --- a/includes/objectcache/MemcachedBagOStuff.php +++ b/includes/objectcache/MemcachedBagOStuff.php @@ -101,7 +101,7 @@ class MemcachedBagOStuff extends BagOStuff { * @return Mixed */ public function replace( $key, $value, $exptime = 0 ) { - return $this->client->replace( $this->encodeKey( $key ), $value, + return $this->client->replace( $this->encodeKey( $key ), $value, $this->fixExpiry( $exptime ) ); } diff --git a/includes/objectcache/MemcachedClient.php b/includes/objectcache/MemcachedClient.php index 536ba6ea07..9ac8ad87ce 100644 --- a/includes/objectcache/MemcachedClient.php +++ b/includes/objectcache/MemcachedClient.php @@ -897,7 +897,7 @@ class MWMemcached { return false; } if ( substr( $data, -2 ) !== "\r\n" ) { - $this->_handle_error( $sock, + $this->_handle_error( $sock, 'line ending missing from data block from $1' ); return false; } @@ -1096,7 +1096,7 @@ class MWMemcached { } /** - * Read the specified number of bytes from a stream. If there is an error, + * Read the specified number of bytes from a stream. If there is an error, * mark the socket dead. * * @param $sock The socket @@ -1137,7 +1137,7 @@ class MWMemcached { function _fgets( $sock ) { $result = fgets( $sock ); // fgets() may return a partial line if there is a select timeout after - // a successful recv(), so we have to check for a timeout even if we + // a successful recv(), so we have to check for a timeout even if we // got a string response. $data = stream_get_meta_data( $sock ); if ( $data['timed_out'] ) { diff --git a/includes/objectcache/MemcachedPhpBagOStuff.php b/includes/objectcache/MemcachedPhpBagOStuff.php index a46dc71628..5a9ee508b9 100644 --- a/includes/objectcache/MemcachedPhpBagOStuff.php +++ b/includes/objectcache/MemcachedPhpBagOStuff.php @@ -81,7 +81,7 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff { public function unlock( $key ) { return $this->client->unlock( $this->encodeKey( $key ) ); } - + /** * @param $key string * @param $value int diff --git a/includes/objectcache/MultiWriteBagOStuff.php b/includes/objectcache/MultiWriteBagOStuff.php index e496ddd8ab..2f37c23b27 100644 --- a/includes/objectcache/MultiWriteBagOStuff.php +++ b/includes/objectcache/MultiWriteBagOStuff.php @@ -22,8 +22,8 @@ */ /** - * A cache class that replicates all writes to multiple child caches. Reads - * are implemented by reading from the caches in the order they are given in + * A cache class that replicates all writes to multiple child caches. Reads + * are implemented by reading from the caches in the order they are given in * the configuration until a cache gives a positive result. * * @ingroup Cache diff --git a/includes/objectcache/ObjectCache.php b/includes/objectcache/ObjectCache.php index 9b360f3234..83b6016401 100644 --- a/includes/objectcache/ObjectCache.php +++ b/includes/objectcache/ObjectCache.php @@ -139,8 +139,8 @@ class ObjectCache { /** * Factory function that creates a memcached client object. * - * This always uses the PHP client, since the PECL client has a different - * hashing scheme and a different interpretation of the flags bitfield, so + * This always uses the PHP client, since the PECL client has a different + * hashing scheme and a different interpretation of the flags bitfield, so * switching between the two clients randomly would be disasterous. * * @param $params array diff --git a/includes/objectcache/RedisBagOStuff.php b/includes/objectcache/RedisBagOStuff.php index d5de044607..40784f547c 100644 --- a/includes/objectcache/RedisBagOStuff.php +++ b/includes/objectcache/RedisBagOStuff.php @@ -321,6 +321,8 @@ class RedisBagOStuff extends BagOStuff { * Get a connection to the server with the specified name. Connections * are cached, and failures are persistent to avoid multiple timeouts. * + * @param $server + * @throws MWException * @return Redis object, or false on failure */ protected function getConnectionToServer( $server ) { diff --git a/includes/parser/CoreTagHooks.php b/includes/parser/CoreTagHooks.php index 296be66f5b..6505183958 100644 --- a/includes/parser/CoreTagHooks.php +++ b/includes/parser/CoreTagHooks.php @@ -72,6 +72,7 @@ class CoreTagHooks { * @param $content string * @param $attributes array * @param $parser Parser + * @throws MWException * @return array */ static function html( $content, $attributes, $parser ) { diff --git a/includes/parser/DateFormatter.php b/includes/parser/DateFormatter.php index 2917b4a740..0f22675e28 100644 --- a/includes/parser/DateFormatter.php +++ b/includes/parser/DateFormatter.php @@ -200,7 +200,7 @@ class DateFormatter { $linked = true; if ( isset( $this->mLinked ) ) $linked = $this->mLinked; - + $bits = array(); $key = $this->keys[$this->mSource]; for ( $p=0; $p < strlen($key); $p++ ) { @@ -219,7 +219,7 @@ class DateFormatter { */ function formatDate( $bits, $link = true ) { $format = $this->targets[$this->mTarget]; - + if (!$link) { // strip piped links $format = preg_replace( '/\[\[[^|]+\|([^\]]+)\]\]/', '$1', $format ); diff --git a/includes/parser/LinkHolderArray.php b/includes/parser/LinkHolderArray.php index d9356b48be..49160e8a71 100644 --- a/includes/parser/LinkHolderArray.php +++ b/includes/parser/LinkHolderArray.php @@ -45,7 +45,7 @@ class LinkHolderArray { /** * Don't serialize the parent object, it is big, and not needed when it is - * a parameter to mergeForeign(), which is the only application of + * a parameter to mergeForeign(), which is the only application of * serializing at present. * * Compact the titles, only serialize the text form. @@ -103,9 +103,9 @@ class LinkHolderArray { } /** - * Merge a LinkHolderArray from another parser instance into this one. The - * keys will not be preserved. Any text which went with the old - * LinkHolderArray and needs to work with the new one should be passed in + * Merge a LinkHolderArray from another parser instance into this one. The + * keys will not be preserved. Any text which went with the old + * LinkHolderArray and needs to work with the new one should be passed in * the $texts array. The strings in this array will have their link holders * converted for use in the destination link holder. The resulting array of * strings will be returned. @@ -126,7 +126,7 @@ class LinkHolderArray { $maxId = $newKey > $maxId ? $newKey : $maxId; } } - $texts = preg_replace_callback( '/()/', + $texts = preg_replace_callback( '/()/', array( $this, 'mergeForeignCallback' ), $texts ); # Renumber interwiki links @@ -135,7 +135,7 @@ class LinkHolderArray { $this->interwikis[$newKey] = $entry; $maxId = $newKey > $maxId ? $newKey : $maxId; } - $texts = preg_replace_callback( '/()/', + $texts = preg_replace_callback( '/()/', array( $this, 'mergeForeignCallback' ), $texts ); # Set the parent link ID to be beyond the highest used ID @@ -159,8 +159,8 @@ class LinkHolderArray { # Internal links $pos = 0; while ( $pos < strlen( $text ) ) { - if ( !preg_match( '//', - $text, $m, PREG_OFFSET_CAPTURE, $pos ) ) + if ( !preg_match( '//', + $text, $m, PREG_OFFSET_CAPTURE, $pos ) ) { break; } diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index 9a094c8b76..8671665ffc 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -200,6 +200,13 @@ class Parser { */ var $mUniqPrefix; + /** + * @var Array with the language name of each language link (i.e. the + * interwiki prefix) in the key, value arbitrary. Used to avoid sending + * duplicate language links to the ParserOutput. + */ + var $mLangLinkLanguages; + /** * Constructor * @@ -282,6 +289,7 @@ class Parser { $this->mRevisionId = $this->mRevisionUser = null; $this->mVarCache = array(); $this->mUser = null; + $this->mLangLinkLanguages = array(); /** * Prefix for temporary replacement strings for the multipass parser. @@ -745,6 +753,7 @@ class Parser { * * @since 1.19 * + * @throws MWException * @return Language|null */ public function getTargetLanguage() { @@ -1523,6 +1532,7 @@ class Parser { * * @param $text string * + * @throws MWException * @return string */ function replaceExternalLinks( $text ) { @@ -1537,6 +1547,7 @@ class Parser { $i = 0; while ( $imOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) { - $this->mOutput->addLanguageLink( $nt->getFullText() ); + // XXX: the above check prevents links to sites with identifiers that are not language codes + + # Bug 24502: filter duplicates + if ( !isset( $this->mLangLinkLanguages[$iw] ) ) { + $this->mLangLinkLanguages[$iw] = true; + $this->mOutput->addLanguageLink( $nt->getFullText() ); + } + $s = rtrim( $s . $prefix ); $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail; wfProfileOut( __METHOD__."-interwiki" ); @@ -2436,6 +2456,7 @@ class Parser { * @param $str String the string to split * @param &$before String set to everything before the ':' * @param &$after String set to everything after the ':' + * @throws MWException * @return String the position of the ':', or false if none found */ function findColonNoLinks( $str, &$before, &$after ) { @@ -2600,8 +2621,9 @@ class Parser { * @private * * @param $index integer - * @param $frame PPFrame + * @param bool|\PPFrame $frame * + * @throws MWException * @return string */ function getVariableValue( $index, $frame = false ) { @@ -3110,6 +3132,7 @@ class Parser { * $piece['parts']: the parameter array * $piece['lineStart']: whether the brace was at the start of a line * @param $frame PPFrame The current frame, contains template arguments + * @throws MWException * @return String: the text of the template * @private */ @@ -3593,7 +3616,13 @@ class Parser { } if ( $rev ) { - $text = $rev->getText(); + $content = $rev->getContent(); + $text = $content->getWikitextForTransclusion(); + + if ( $text === false || $text === null ) { + $text = false; + break; + } } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) { global $wgContLang; $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage(); @@ -3601,16 +3630,17 @@ class Parser { $text = false; break; } + $content = $message->content(); $text = $message->plain(); } else { break; } - if ( $text === false ) { + if ( !$content ) { break; } # Redirect? $finalTitle = $title; - $title = Title::newFromRedirect( $text ); + $title = $content->getRedirectTarget(); } return array( 'text' => $text, @@ -3780,6 +3810,7 @@ class Parser { * noClose Original text did not have a close tag * @param $frame PPFrame * + * @throws MWException * @return string */ function extensionSubstitution( $params, $frame ) { @@ -4677,6 +4708,7 @@ class Parser { * * @param $tag Mixed: the tag to use, e.g. 'hook' for "" * @param $callback Mixed: the callback function (and object) to use for the tag + * @throws MWException * @return Mixed|null The old value of the mTagHooks array associated with the hook */ public function setHook( $tag, $callback ) { @@ -4707,6 +4739,7 @@ class Parser { * * @param $tag Mixed: the tag to use, e.g. 'hook' for "" * @param $callback Mixed: the callback function (and object) to use for the tag + * @throws MWException * @return Mixed|null The old value of the mTagHooks array associated with the hook */ function setTransparentTagHook( $tag, $callback ) { @@ -4769,6 +4802,7 @@ class Parser { * Please read the documentation in includes/parser/Preprocessor.php for more information * about the methods available in PPFrame and PPNode. * + * @throws MWException * @return string|callback The old callback function for this name, if any */ public function setFunctionHook( $id, $callback, $flags = 0 ) { @@ -4816,6 +4850,10 @@ class Parser { * Create a tag function, e.g. "some stuff". * Unlike tag hooks, tag functions are parsed at preprocessor level. * Unlike parser functions, their content is not preprocessed. + * @param $tag + * @param $callback + * @param $flags + * @throws MWException * @return null */ function setFunctionTagHook( $tag, $callback, $flags ) { @@ -5779,6 +5817,7 @@ class Parser { * check whether it is still valid, by calling isValidHalfParsedText(). * * @param $data array Serialized data + * @throws MWException * @return String */ function unserializeHalfParsedText( $data ) { diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php index 6a4ef0c552..d41962127a 100644 --- a/includes/parser/ParserCache.php +++ b/includes/parser/ParserCache.php @@ -48,6 +48,7 @@ class ParserCache { * May be a memcached client or a BagOStuff derivative. * * @param $memCached Object + * @throws MWException */ protected function __construct( $memCached ) { if ( !$memCached ) { @@ -200,8 +201,8 @@ class ParserCache { wfDebug( "ParserOutput cache found.\n" ); - // The edit section preference may not be the appropiate one in - // the ParserOutput, as we are not storing it in the parsercache + // The edit section preference may not be the appropiate one in + // the ParserOutput, as we are not storing it in the parsercache // key. Force it here. See bug 31445. $value->setEditSectionTokens( $popts->getEditSection() ); diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index 009b18a13e..064182e7f8 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -29,67 +29,67 @@ * @ingroup Parser */ class ParserOptions { - + /** * Use DateFormatter to format dates */ var $mUseDynamicDates; - + /** * Interlanguage links are removed and returned in an array */ var $mInterwikiMagic; - + /** * Allow external images inline? */ var $mAllowExternalImages; - + /** * If not, any exception? */ var $mAllowExternalImagesFrom; - + /** * If not or it doesn't match, should we check an on-wiki whitelist? */ var $mEnableImageWhitelist; - + /** * Date format index */ var $mDateFormat = null; - + /** * Create "edit section" links? */ var $mEditSection = true; - + /** * Allow inclusion of special pages? */ var $mAllowSpecialInclusion; - + /** * Use tidy to cleanup output HTML? */ var $mTidy = false; - + /** * Which lang to call for PLURAL and GRAMMAR */ var $mInterfaceMessage = false; - + /** * Overrides $mInterfaceMessage with arbitrary language */ var $mTargetLanguage = null; - + /** * Maximum size of template expansions, in bytes */ var $mMaxIncludeSize; - + /** * Maximum number of nodes touched by PPFrame::expand() */ @@ -99,56 +99,56 @@ class ParserOptions { * Maximum number of nodes generated by Preprocessor::preprocessToObj() */ var $mMaxGeneratedPPNodeCount; - + /** * Maximum recursion depth in PPFrame::expand() */ var $mMaxPPExpandDepth; - + /** * Maximum recursion depth for templates within templates */ var $mMaxTemplateDepth; - + /** * Maximum number of calls per parse to expensive parser functions */ var $mExpensiveParserFunctionLimit; - + /** * Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS */ var $mRemoveComments = true; - + /** * Callback for template fetching. Used as first argument to call_user_func(). */ var $mTemplateCallback = array( 'Parser', 'statelessFetchTemplate' ); - + /** * Enable limit report in an HTML comment on output */ var $mEnableLimitReport = false; - + /** * Timestamp used for {{CURRENTDAY}} etc. */ var $mTimestamp; - + /** * Target attribute for external links */ var $mExternalLinkTarget; - + /** - * Clean up signature texts? + * Clean up signature texts? * * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures * 2) Substitute all transclusions */ var $mCleanSignatures; - + /** * Transform wiki markup when saving the page? */ @@ -168,43 +168,43 @@ class ParserOptions { * Automatically number headings? */ var $mNumberHeadings; - + /** * User math preference (as integer). Not used (1.19) */ var $mMath; - + /** * Thumb size preferred by the user. */ var $mThumbSize; - + /** * Maximum article size of an article to be marked as "stub" */ private $mStubThreshold; - + /** * Language object of the User language. */ var $mUserLang; /** - * @var User + * @var User * Stored user object */ var $mUser; - + /** * Parsing the page for a "preview" operation? */ var $mIsPreview = false; - + /** * Parsing the page for a "preview" operation on a single section? */ var $mIsSectionPreview = false; - + /** * Parsing the printable version of the page? */ @@ -415,8 +415,8 @@ class ParserOptions { return new ParserOptions( $context->getUser(), $context->getLanguage() ); } - /** - * Get user options + /** + * Get user options * * @param $user User object * @param $lang Language object diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php index be629d378a..b6bcf63264 100644 --- a/includes/parser/ParserOutput.php +++ b/includes/parser/ParserOutput.php @@ -50,7 +50,7 @@ class ParserOutput extends CacheTime { $mTimestamp; # Timestamp of the revision private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change. private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys) - private $mSecondaryDataUpdates = array(); # List of instances of SecondaryDataObject(), used to cause some information extracted from the page in a custom place. + private $mSecondaryDataUpdates = array(); # List of DataUpdate, used to save info from the page somewhere else. const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)())#'; @@ -75,6 +75,8 @@ class ParserOutput extends CacheTime { /** * callback used by getText to replace editsection tokens * @private + * @param $m + * @throws MWException * @return mixed */ function replaceEditSectionLinksCallback( $m ) { @@ -399,9 +401,13 @@ class ParserOutput extends CacheTime { * extracted from the page's content, including a LinksUpdate object for all links stored in * this ParserOutput object. * + * @note: Avoid using this method directly, use ContentHandler::getSecondaryDataUpdates() instead! The content + * handler may provide additional update objects. + * * @since 1.20 * - * @param $title Title of the page we're updating. If not given, a title object will be created based on $this->getTitleText() + * @param $title Title The title of the page we're updating. If not given, a title object will be created + * based on $this->getTitleText() * @param $recursive Boolean: queue jobs for recursive updates? * * @return Array. An array of instances of DataUpdate diff --git a/includes/parser/Parser_LinkHooks.php b/includes/parser/Parser_LinkHooks.php index 6bcc324d58..e4f5d1262a 100644 --- a/includes/parser/Parser_LinkHooks.php +++ b/includes/parser/Parser_LinkHooks.php @@ -32,7 +32,7 @@ class Parser_LinkHooks extends Parser { * can automatically discard old data. */ const VERSION = '1.6.4'; - + # Flags for Parser::setLinkHook # Also available as global constants from Defines.php const SLH_PATTERN = 1; @@ -84,11 +84,11 @@ class Parser_LinkHooks extends Parser { * Create a link hook, e.g. [[Namepsace:...|display}} * The callback function should have the form: * function myLinkCallback( $parser, $holders, $markers, - * Title $title, $titleText, &$sortText = null, &$leadingColon = false ) { ... } + * Title $title, $titleText, &$sortText = null, &$leadingColon = false ) { ... } * * Or with SLH_PATTERN: * function myLinkCallback( $parser, $holders, $markers, ) - * &$titleText, &$sortText = null, &$leadingColon = false ) { ... } + * &$titleText, &$sortText = null, &$leadingColon = false ) { ... } * * The callback may either return a number of different possible values: * String) Text result of the link @@ -100,6 +100,7 @@ class Parser_LinkHooks extends Parser { * @param $flags Integer: a combination of the following flags: * SLH_PATTERN Use a regex link pattern rather than a namespace * + * @throws MWException * @return callback|null The old callback function for this name, if any */ public function setLinkHook( $ns, $callback, $flags = 0 ) { @@ -111,7 +112,7 @@ class Parser_LinkHooks extends Parser { $this->mLinkHooks[$ns] = array( $callback, $flags ); return $oldVal; } - + /** * Get all registered link hook identifiers * @@ -120,9 +121,11 @@ class Parser_LinkHooks extends Parser { function getLinkHooks() { return array_keys( $this->mLinkHooks ); } - + /** * Process [[ ]] wikilinks + * @param $s + * @throws MWException * @return LinkHolderArray * * @private @@ -144,7 +147,7 @@ class Parser_LinkHooks extends Parser { } $holders = new LinkHolderArray( $this ); - + if( is_null( $this->mTitle ) ) { wfProfileOut( __METHOD__ ); wfProfileOut( __METHOD__.'-setup' ); @@ -152,7 +155,7 @@ class Parser_LinkHooks extends Parser { } wfProfileOut( __METHOD__.'-setup' ); - + $offset = 0; $offsetStack = array(); $markers = new LinkMarkerReplacer( $this, $holders, array( &$this, 'replaceInternalLinksCallback' ) ); @@ -178,7 +181,7 @@ class Parser_LinkHooks extends Parser { $startBracketOffset = array_pop($offsetStack); # Just to clean up the code, lets place offsets on the outer ends $endBracketOffset += 2; - + # Only do logic if we actually have a opening bracket for this if( isset($startBracketOffset) ) { # Extract text inside the link @@ -203,22 +206,20 @@ class Parser_LinkHooks extends Parser { # ToDO: Some LinkHooks use patterns rather than namespaces # these need to be tested at this point here } - } # Bump our offset to after our current bracket $offset = $bracketOffset+2; } - - + # Now expand our tree wfProfileIn( __METHOD__.'-expand' ); $s = $markers->expand( $s ); wfProfileOut( __METHOD__.'-expand' ); - + wfProfileOut( __METHOD__ ); return $holders; } - + function replaceInternalLinksCallback( $parser, $holders, $markers, $titleText, $paramText ) { wfProfileIn( __METHOD__ ); $wt = isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]"; @@ -230,16 +231,16 @@ class Parser_LinkHooks extends Parser { wfProfileOut( __METHOD__ ); return $wt; } - + # Make subpage if necessary if( $this->areSubpagesAllowed() ) { $titleText = $this->maybeDoSubpageLink( $titleText, $paramText ); } - + # Check for a leading colon and strip it if it is there $leadingColon = $titleText[0] == ':'; if( $leadingColon ) $titleText = substr( $titleText, 1 ); - + wfProfileOut( __METHOD__."-misc" ); # Make title object wfProfileIn( __METHOD__."-title" ); @@ -251,7 +252,7 @@ class Parser_LinkHooks extends Parser { } $ns = $title->getNamespace(); wfProfileOut( __METHOD__."-title" ); - + # Default for Namespaces is a default link # ToDo: Default for patterns is plain wikitext $return = true; @@ -270,7 +271,7 @@ class Parser_LinkHooks extends Parser { } if( $return === true ) { # True (treat as plain link) was returned, call the defaultLinkHook - $return = CoreLinkFunctions::defaultLinkHook( $parser, $holders, $markers, $title, + $return = CoreLinkFunctions::defaultLinkHook( $parser, $holders, $markers, $title, $titleText, $paramText, $leadingColon ); } if( $return === false ) { @@ -282,13 +283,13 @@ class Parser_LinkHooks extends Parser { wfProfileOut( __METHOD__ ); return $return; } - + } class LinkMarkerReplacer { - + protected $markers, $nextId, $parser, $holders, $callback; - + function __construct( $parser, $holders, $callback ) { $this->nextId = 0; $this->markers = array(); @@ -296,21 +297,21 @@ class LinkMarkerReplacer { $this->holders = $holders; $this->callback = $callback; } - + function addMarker($titleText, $paramText) { $id = $this->nextId++; $this->markers[$id] = array( $titleText, $paramText ); return ""; } - + function findMarker( $string ) { return (bool) preg_match('//', $string ); } - + function expand( $string ) { return StringUtils::delimiterReplaceCallback( "", array( &$this, 'callback' ), $string ); } - + function callback( $m ) { $id = intval($m[1]); if( !array_key_exists($id, $this->markers) ) return $m[0]; @@ -320,5 +321,4 @@ class LinkMarkerReplacer { array_unshift( $args, $this->parser ); return call_user_func_array( $this->callback, $args ); } - } diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php index 34de0ba541..b2dd7db1a1 100644 --- a/includes/parser/Preprocessor_DOM.php +++ b/includes/parser/Preprocessor_DOM.php @@ -126,6 +126,7 @@ class Preprocessor_DOM implements Preprocessor { * cache may be implemented at a later date which takes further advantage of these strict * dependency requirements. * + * @throws MWException * @return PPNode_DOM */ function preprocessToObj( $text, $flags = 0 ) { @@ -164,7 +165,7 @@ class Preprocessor_DOM implements Preprocessor { } // Fail if the number of elements exceeds acceptable limits - // Do not attempt to generate the DOM + // Do not attempt to generate the DOM $this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' ); $max = $this->parser->mOptions->getMaxGeneratedPPNodeCount(); if ( $this->parser->mGeneratedPPNodeCount > $max ) { @@ -1673,6 +1674,7 @@ class PPNode_DOM implements PPNode { * - index String index * - value PPNode value * + * @throws MWException * @return array */ function splitArg() { @@ -1694,6 +1696,7 @@ class PPNode_DOM implements PPNode { * Split an "" node into an associative array containing name, attr, inner and close * All values in the resulting array are PPNodes. Inner and close are optional. * + * @throws MWException * @return array */ function splitExt() { @@ -1719,6 +1722,7 @@ class PPNode_DOM implements PPNode { /** * Split a "" node + * @throws MWException * @return array */ function splitHeading() { diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php index 4f04c86502..a4e408e99d 100644 --- a/includes/parser/Preprocessor_Hash.php +++ b/includes/parser/Preprocessor_Hash.php @@ -105,6 +105,7 @@ class Preprocessor_Hash implements Preprocessor { * cache may be implemented at a later date which takes further advantage of these strict * dependency requirements. * + * @throws MWException * @return PPNode_Hash_Tree */ function preprocessToObj( $text, $flags = 0 ) { @@ -884,9 +885,11 @@ class PPFrame_Hash implements PPFrame { * Create a new child frame * $args is optionally a multi-root PPNode or array containing the template arguments * - * @param $args PPNode_Hash_Array|array + * @param array|bool|\PPNode_Hash_Array $args PPNode_Hash_Array|array * @param $title Title|bool * + * @param int $indexOffset + * @throws MWException * @return PPTemplateFrame_Hash */ function newChild( $args = false, $title = false, $indexOffset = 0 ) { @@ -1609,6 +1612,7 @@ class PPNode_Hash_Tree implements PPNode { * - index String index * - value PPNode value * + * @throws MWException * @return array */ function splitArg() { @@ -1642,6 +1646,7 @@ class PPNode_Hash_Tree implements PPNode { * Split an "" node into an associative array containing name, attr, inner and close * All values in the resulting array are PPNodes. Inner and close are optional. * + * @throws MWException * @return array */ function splitExt() { @@ -1669,6 +1674,7 @@ class PPNode_Hash_Tree implements PPNode { /** * Split an "" node * + * @throws MWException * @return array */ function splitHeading() { @@ -1695,6 +1701,7 @@ class PPNode_Hash_Tree implements PPNode { /** * Split a "