From 2ae7d6b58035f038717d725efd493c4870337599 Mon Sep 17 00:00:00 2001 From: "James D. Forrester" Date: Mon, 12 Feb 2018 16:20:05 -0800 Subject: [PATCH] Add protection for User: JSON pages in the same manner as JS & CSS ones Also recognise MediaWiki: JSON pages (with the existing protection of the editinterface right). Bug: T76554 Change-Id: Idba166d82ee6dd507d7345c9bdbefc8ca78ed7b4 --- RELEASE-NOTES-1.31 | 2 + includes/DefaultSettings.php | 4 ++ includes/EditPage.php | 27 ++++++-- includes/Title.php | 60 ++++++++++++----- includes/actions/RawAction.php | 15 +++-- includes/user/User.php | 2 + languages/i18n/en.json | 18 +++-- languages/i18n/qqq.json | 24 +++++-- tests/phpunit/includes/TitleMethodsTest.php | 14 ++++ .../phpunit/includes/TitlePermissionTest.php | 66 ++++++++++++++++++- 10 files changed, 192 insertions(+), 40 deletions(-) diff --git a/RELEASE-NOTES-1.31 b/RELEASE-NOTES-1.31 index e0bacb35d1..75d7b7ad78 100644 --- a/RELEASE-NOTES-1.31 +++ b/RELEASE-NOTES-1.31 @@ -33,6 +33,8 @@ production. was configured with 'any'. === New features in 1.31 === +* (T76554) User sub-pages named ….json are now protected in the same way that ….js + and ….css pages are, so that configuration options can safely be placed there. * Wikimedia\Rdbms\IDatabase->select() and similar methods now support joins with parentheses for grouping. * As a first pass in standardizing dialog boxes across the MediaWiki product, diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index f473b3e7f7..81d3c35d29 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -5150,6 +5150,7 @@ $wgGroupPermissions['user']['reupload'] = true; $wgGroupPermissions['user']['reupload-shared'] = true; $wgGroupPermissions['user']['minoredit'] = true; $wgGroupPermissions['user']['editmyusercss'] = true; +$wgGroupPermissions['user']['editmyuserjson'] = true; $wgGroupPermissions['user']['editmyuserjs'] = true; $wgGroupPermissions['user']['purge'] = true; $wgGroupPermissions['user']['sendemail'] = true; @@ -5185,6 +5186,7 @@ $wgGroupPermissions['sysop']['deletedtext'] = true; $wgGroupPermissions['sysop']['undelete'] = true; $wgGroupPermissions['sysop']['editinterface'] = true; $wgGroupPermissions['sysop']['editusercss'] = true; +$wgGroupPermissions['sysop']['edituserjson'] = true; $wgGroupPermissions['sysop']['edituserjs'] = true; $wgGroupPermissions['sysop']['import'] = true; $wgGroupPermissions['sysop']['importupload'] = true; @@ -5816,6 +5818,7 @@ $wgGrantPermissions['editprotected']['editprotected'] = true; // FIXME: Rename editmycssjs to editmyconfig $wgGrantPermissions['editmycssjs'] = $wgGrantPermissions['editpage']; $wgGrantPermissions['editmycssjs']['editmyusercss'] = true; +$wgGrantPermissions['editmycssjs']['editmyuserjson'] = true; $wgGrantPermissions['editmycssjs']['editmyuserjs'] = true; $wgGrantPermissions['editmyoptions']['editmyoptions'] = true; @@ -5823,6 +5826,7 @@ $wgGrantPermissions['editmyoptions']['editmyoptions'] = true; $wgGrantPermissions['editinterface'] = $wgGrantPermissions['editpage']; $wgGrantPermissions['editinterface']['editinterface'] = true; $wgGrantPermissions['editinterface']['editusercss'] = true; +$wgGrantPermissions['editinterface']['edituserjson'] = true; $wgGrantPermissions['editinterface']['edituserjs'] = true; $wgGrantPermissions['createeditmovepage'] = $wgGrantPermissions['editpage']; diff --git a/includes/EditPage.php b/includes/EditPage.php index 27671bcebf..a1d9ae82d5 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -2473,9 +2473,11 @@ ERROR; if ( $namespace == NS_MEDIAWIKI ) { # Show a warning if editing an interface message $out->wrapWikiMsg( "
\n$1\n
", 'editinginterface' ); - # If this is a default message (but not css or js), + # If this is a default message (but not css, json, or js), # show a hint that it is translatable on translatewiki.net - if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS ) + if ( + !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS ) + && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON ) && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) { $defaultMessageText = $this->mTitle->getDefaultMessageText(); @@ -3095,10 +3097,12 @@ ERROR; } if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) { $isUserCssConfig = $this->mTitle->isUserCssConfigPage(); + $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage(); + $isUserJsConfig = $this->mTitle->isUserJsConfigPage(); $warning = $isUserCssConfig ? 'usercssispublic' - : 'userjsispublic'; + : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' ); $out->wrapWikiMsg( '
$1
', $warning ); @@ -3109,9 +3113,12 @@ ERROR; "
\n$1\n
", [ 'usercssyoucanpreview' ] ); - } - - if ( $this->mTitle->isJsSubpage() && $config->get( 'AllowUserJs' ) ) { + } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) { + $out->wrapWikiMsg( + "
\n$1\n
", + [ 'userjsonyoucanpreview' ] + ); + } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) { $out->wrapWikiMsg( "
\n$1\n
", [ 'userjsyoucanpreview' ] @@ -3848,6 +3855,11 @@ ERROR; if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) { $format = false; } + } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) { + $format = 'json'; + if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) { + $format = false; + } } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) { $format = 'js'; if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) { @@ -3858,7 +3870,8 @@ ERROR; } # Used messages to make sure grep find them: - # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview + # Messages: usercsspreview, userjsonpreview, userjspreview, + # sitecsspreview, sitejsonpreview, sitejspreview if ( $level && $format ) { $note = "
" . $this->context->msg( "{$level}{$format}preview" )->text() . diff --git a/includes/Title.php b/includes/Title.php index 8dda01f464..58e688589f 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -1278,15 +1278,15 @@ class Title implements LinkTarget { } /** - * 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. + * Could this MediaWiki namespace page contain custom CSS, JSON, or JavaScript for the + * global UI. This is generally true for pages in the MediaWiki namespace having + * CONTENT_MODEL_CSS, CONTENT_MODEL_JSON, or CONTENT_MODEL_JAVASCRIPT. * - * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() + * This method does *not* return true for per-user JS/JSON/CSS. Use isUserConfigPage() * for that! * - * Note that this method should not return true for pages that contain and - * show "inactive" CSS or JS. + * Note that this method should not return true for pages that contain and show + * "inactive" CSS, JSON, or JS. * * @return bool * @since 1.31 @@ -1296,6 +1296,7 @@ class Title implements LinkTarget { NS_MEDIAWIKI == $this->mNamespace && ( $this->hasContentModel( CONTENT_MODEL_CSS ) + || $this->hasContentModel( CONTENT_MODEL_JSON ) || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) ); @@ -1303,7 +1304,7 @@ class Title implements LinkTarget { /** * @return bool - * @deprecated Since 1.31; use ::isSiteConfigPage() instead + * @deprecated Since 1.31; use ::isSiteConfigPage() instead (which also checks for JSON pages) */ public function isCssOrJsPage() { wfDeprecated( __METHOD__, '1.31' ); @@ -1313,7 +1314,7 @@ class Title implements LinkTarget { } /** - * Is this a "config" (.css or .js) sub-page of a user page? + * Is this a "config" (.css, .json, or .js) sub-page of a user page? * * @return bool * @since 1.31 @@ -1324,6 +1325,7 @@ class Title implements LinkTarget { && $this->isSubpage() && ( $this->hasContentModel( CONTENT_MODEL_CSS ) + || $this->hasContentModel( CONTENT_MODEL_JSON ) || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) ); @@ -1331,7 +1333,7 @@ class Title implements LinkTarget { /** * @return bool - * @deprecated Since 1.31; use ::isUserConfigPage() instead + * @deprecated Since 1.31; use ::isUserConfigPage() instead (which also checks for JSON pages) */ public function isCssJsSubpage() { wfDeprecated( __METHOD__, '1.31' ); @@ -1341,9 +1343,9 @@ class Title implements LinkTarget { } /** - * Trim down a .css or .js subpage title to get the corresponding skin name + * Trim down a .css, .json, or .js subpage title to get the corresponding skin name * - * @return string Containing skin name from .css or .js subpage title + * @return string Containing skin name from .css, .json, or .js subpage title * @since 1.31 */ public function getSkinFromConfigSubpage() { @@ -1351,14 +1353,14 @@ class Title implements LinkTarget { $subpage = $subpage[count( $subpage ) - 1]; $lastdot = strrpos( $subpage, '.' ); if ( $lastdot === false ) { - return $subpage; # Never happens: only called for names ending in '.css' or '.js' + return $subpage; # Never happens: only called for names ending in '.css'/'.json'/'.js' } return substr( $subpage, 0, $lastdot ); } /** * @deprecated Since 1.31; use ::getSkinFromConfigSubpage() instead - * @return string Containing skin name from .css or .js subpage title + * @return string Containing skin name from .css, .json, or .js subpage title */ public function getSkinFromCssJsSubpage() { wfDeprecated( __METHOD__, '1.31' ); @@ -1389,7 +1391,21 @@ class Title implements LinkTarget { } /** - * Is this a .js subpage of a user page? + * Is this a JSON "config" sub-page of a user page? + * + * @return bool + * @since 1.31 + */ + public function isUserJsonConfigPage() { + return ( + NS_USER == $this->mNamespace + && $this->isSubpage() + && $this->hasContentModel( CONTENT_MODEL_JSON ) + ); + } + + /** + * Is this a JS "config" sub-page of a user page? * * @return bool * @since 1.31 @@ -2302,7 +2318,7 @@ class Title implements LinkTarget { } /** - * Check CSS/JS sub-page permissions + * Check CSS/JSON/JS sub-page permissions * * @param string $action The action to check * @param User $user User to check @@ -2313,7 +2329,7 @@ class Title implements LinkTarget { * @return array List of errors */ private function checkUserConfigPermissions( $action, $user, $errors, $rigor, $short ) { - # Protect css/js subpages of user pages + # Protect css/json/js subpages of user pages # XXX: this might be better using restrictions if ( $action != 'patrol' ) { @@ -2323,6 +2339,11 @@ class Title implements LinkTarget { && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) { $errors[] = [ 'mycustomcssprotected', $action ]; + } elseif ( + $this->isUserJsonConfigPage() + && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' ) + ) { + $errors[] = [ 'mycustomjsonprotected', $action ]; } elseif ( $this->isUserJsConfigPage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) @@ -2335,6 +2356,11 @@ class Title implements LinkTarget { && !$user->isAllowed( 'editusercss' ) ) { $errors[] = [ 'customcssprotected', $action ]; + } elseif ( + $this->isUserJsonConfigPage() + && !$user->isAllowed( 'edituserjson' ) + ) { + $errors[] = [ 'customjsonprotected', $action ]; } elseif ( $this->isUserJsConfigPage() && !$user->isAllowed( 'edituserjs' ) @@ -3810,6 +3836,8 @@ class Title implements LinkTarget { // If we are looking at a css/js user subpage, purge the action=raw. if ( $this->isUserJsConfigPage() ) { $urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' ); + } elseif ( $this->isUserJsonConfigPage() ) { + $urls[] = $this->getInternalURL( 'action=raw&ctype=application/json' ); } elseif ( $this->isUserCssConfigPage() ) { $urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' ); } diff --git a/includes/actions/RawAction.php b/includes/actions/RawAction.php index 625a9bb928..812f9623ff 100644 --- a/includes/actions/RawAction.php +++ b/includes/actions/RawAction.php @@ -64,10 +64,14 @@ class RawAction extends FormlessAction { $maxage = $request->getInt( 'maxage', $config->get( 'SquidMaxage' ) ); $smaxage = $request->getIntOrNull( 'smaxage' ); if ( $smaxage === null ) { - if ( $contentType == 'text/css' || $contentType == 'text/javascript' ) { - // CSS/JS raw content has its own CDN max age configuration. - // Note: Title::getCdnUrls() includes action=raw for css/js pages, - // so if using the canonical url, this will get HTCP purges. + if ( + $contentType == 'text/css' || + $contentType == 'application/json' || + $contentType == 'text/javascript' + ) { + // CSS/JSON/JS raw content has its own CDN max age configuration. + // Note: Title::getCdnUrls() includes action=raw for css/json/js + // pages, so if using the canonical url, this will get HTCP purges. $smaxage = intval( $config->get( 'ForcedRawSMaxage' ) ); } else { // No CDN cache for anything else @@ -161,7 +165,7 @@ class RawAction extends FormlessAction { } if ( $content === null || $content === false ) { - // section not found (or section not supported, e.g. for JS and CSS) + // section not found (or section not supported, e.g. for JS, JSON, and CSS) $text = false; } else { $text = $content->getNativeData(); @@ -239,6 +243,7 @@ class RawAction extends FormlessAction { 'text/x-wiki', 'text/javascript', 'text/css', + // FIXME: Should we still allow Zope editing? External editing feature was dropped 'application/x-zope-edit', 'application/json' ]; diff --git a/includes/user/User.php b/includes/user/User.php index 9777148b38..9ef880bfd4 100644 --- a/includes/user/User.php +++ b/includes/user/User.php @@ -150,10 +150,12 @@ class User implements IDBAccessObject, UserIdentity { 'editmyoptions', 'editmyprivateinfo', 'editmyusercss', + 'editmyuserjson', 'editmyuserjs', 'editmywatchlist', 'editsemiprotected', 'editusercss', + 'edituserjson', 'edituserjs', 'hideuser', 'import', diff --git a/languages/i18n/en.json b/languages/i18n/en.json index a9466f190e..9036396d0c 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -363,8 +363,10 @@ "cascadeprotected": "This page has been protected from editing because it is transcluded in the following {{PLURAL:$1|page, which is|pages, which are}} protected with the \"cascading\" option turned on:\n$2", "namespaceprotected": "You do not have permission to edit pages in the $1 namespace.", "customcssprotected": "You do not have permission to edit this CSS page because it contains another user's personal settings.", + "customjsonprotected": "You do not have permission to edit this JSON page because it contains another user's personal settings.", "customjsprotected": "You do not have permission to edit this JavaScript page because it contains another user's personal settings.", "mycustomcssprotected": "You do not have permission to edit this CSS page.", + "mycustomjsonprotected": "You do not have permission to edit this JSON page.", "mycustomjsprotected": "You do not have permission to edit this JavaScript page.", "myprivateinfoprotected": "You do not have permission to edit your private information.", "mypreferencesprotected": "You do not have permission to edit your preferences.", @@ -683,12 +685,15 @@ "blocked-notice-logextract": "This user is currently blocked.\nThe latest block log entry is provided below for reference:", "clearyourcache": "Note: After saving, you may have to bypass your browser's cache to see the changes.\n* Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)\n* Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)\n* Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5\n* Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.", "usercssyoucanpreview": "Tip: Use the \"{{int:showpreview}}\" button to test your new CSS before saving.", + "userjsonyoucanpreview": "Tip: Use the \"{{int:showpreview}}\" button to test your new JSON before saving.", "userjsyoucanpreview": "Tip: Use the \"{{int:showpreview}}\" button to test your new JavaScript before saving.", "usercsspreview": "Remember that you are only previewing your user CSS.\nIt has not yet been saved!", + "userjsonpreview": "Remember that you are only testing/previewing your user JSON config.\nIt has not yet been saved!", "userjspreview": "Remember that you are only testing/previewing your user JavaScript.\nIt has not yet been saved!", "sitecsspreview": "Remember that you are only previewing this CSS.\nIt has not yet been saved!", + "sitejsonpreview": "Remember that you are only previewing this JSON config.\nIt has not yet been saved!", "sitejspreview": "Remember that you are only previewing this JavaScript code.\nIt has not yet been saved!", - "userinvalidconfigtitle": "Warning: There is no skin \"$1\".\nCustom .css and .js pages use a lowercase title, e.g. {{ns:user}}:Foo/vector.css as opposed to {{ns:user}}:Foo/Vector.css.", + "userinvalidconfigtitle": "Warning: There is no skin \"$1\".\nCustom .css, .json, and .js pages use a lowercase title, e.g. {{ns:user}}:Foo/vector.css as opposed to {{ns:user}}:Foo/Vector.css.", "updated": "(Updated)", "note": "Note:", "previewnote": "Remember that this is only a preview.\nYour changes have not yet been saved!", @@ -1079,8 +1084,9 @@ "default": "default", "prefs-files": "Files", "prefs-custom-css": "Custom CSS", + "prefs-custom-json": "Custom JSON", "prefs-custom-js": "Custom JavaScript", - "prefs-common-config": "Shared CSS/JavaScript for all skins:", + "prefs-common-config": "Shared CSS/JSON/JavaScript for all skins:", "prefs-reset-intro": "You can use this page to reset your preferences to the site defaults.\nThis cannot be undone.", "prefs-emailconfirm-label": "Email confirmation:", "youremail": "Email:", @@ -1225,8 +1231,10 @@ "right-editcontentmodel": "Edit the content model of a page", "right-editinterface": "Edit the user interface", "right-editusercss": "Edit other users' CSS files", + "right-edituserjson": "Edit other users' JSON files", "right-edituserjs": "Edit other users' JavaScript files", "right-editmyusercss": "Edit your own user CSS files", + "right-editmyuserjson": "Edit your own user JSON files", "right-editmyuserjs": "Edit your own user JavaScript files", "right-viewmywatchlist": "View your own watchlist", "right-editmywatchlist": "Edit your own watchlist. Note some actions will still add pages even without this right.", @@ -1266,8 +1274,8 @@ "grant-createaccount": "Create accounts", "grant-createeditmovepage": "Create, edit, and move pages", "grant-delete": "Delete pages, revisions, and log entries", - "grant-editinterface": "Edit the MediaWiki namespace and user CSS/JavaScript", - "grant-editmycssjs": "Edit your user CSS/JavaScript", + "grant-editinterface": "Edit the MediaWiki namespace and user CSS/JSON/JavaScript", + "grant-editmycssjs": "Edit your user CSS/JSON/JavaScript", "grant-editmyoptions": "Edit your user preferences", "grant-editmywatchlist": "Edit your watchlist", "grant-editpage": "Edit existing pages", @@ -2984,6 +2992,7 @@ "group-bot.css": "/* CSS placed here will affect bots only */", "group-sysop.css": "/* CSS placed here will affect sysops only */", "group-bureaucrat.css": "/* CSS placed here will affect bureaucrats only */", + "common.json": "/* Any JSON here will be loaded for all users on every page load. */", "common.js": "/* Any JavaScript here will be loaded for all users on every page load. */", "group-autoconfirmed.js": "/* Any JavaScript here will be loaded for autoconfirmed users only */", "group-user.js": "/* Any JavaScript here will be loaded for registered users only */", @@ -4433,6 +4442,7 @@ "unlinkaccounts-success": "The account was unlinked.", "authenticationdatachange-ignored": "The authentication data change was not handled. Maybe no provider was configured?", "userjsispublic": "Please note: JavaScript subpages should not contain confidential data as they are viewable by other users.", + "userjsonispublic": "Please note: JSON subpages should not contain confidential data as they are viewable by other users.", "usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users.", "restrictionsfield-badip": "Invalid IP address or range: $1", "restrictionsfield-label": "Allowed IP ranges:", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index a4ccfbb0d8..f2ec8e9ec5 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -560,8 +560,10 @@ "cascadeprotected": "Parameters:\n* $1 - number of cascade-protected pages, used for PLURAL\n* $2 - list of cascade-protected pages\n* $3 - (Unused) the action the user attempted to perform", "namespaceprotected": "Parameters:\n* $1 - namespace name\n* $2 - (Unused) the action the user attempted to perform", "customcssprotected": "Used as error message. Parameters:\n* $1 - (Unused) the action the user attempted to perform", + "customjsonprotected": "Used as error message. Parameters:\n* $1 - (Unused) the action the user attempted to perform", "customjsprotected": "Used as error message. Parameters:\n* $1 - (Unused) the action the user attempted to perform", "mycustomcssprotected": "Used as error message. Parameters:\n* $1 - (Unused) the action the user attempted to perform", + "mycustomjsonprotected": "Used as error message. Parameters:\n* $1 - (Unused) the action the user attempted to perform", "mycustomjsprotected": "Used as error message. Parameters:\n* $1 - (Unused) the action the user attempted to perform", "myprivateinfoprotected": "Used as error message.", "mypreferencesprotected": "Used as error message.", @@ -878,12 +880,15 @@ "userpage-userdoesnotexist": "Error message displayed when trying to edit or create a page or a subpage that belongs to a user who is not registered on the wiki.\n\nParameters:\n* $1 - a username\n{{Identical|Userdoesnotexist}}", "userpage-userdoesnotexist-view": "Shown in user pages of non-existing users. See for example [{{canonicalurl:User:Foo}} User:Foo].\n\nParameters:\n* $1 - a username\n{{Identical|Userdoesnotexist}}", "blocked-notice-logextract": "{{gender}}\nParameters:\n* $1 - (Optional) the name of the blocked user. Can be used for GENDER.", - "clearyourcache": "Text at the top of .js/.css pages.\n\nWhen translating browser function names, check how they are translated in the localized versions of these web browsers in your language. If a browser is not translated to it, use English or another language in which browsers are most commonly used by the speakers of your language.", - "usercssyoucanpreview": "Text displayed on every CSS page.\n\nSee also:\n* {{msg-mw|Userjsyoucanpreview}}\n* {{msg-mw|Showpreview}}", - "userjsyoucanpreview": "Text displayed on every JavaScript page.\n\nSee also:\n* {{msg-mw|Usercssyoucanpreview}}\n* {{msg-mw|Showpreview}}", + "clearyourcache": "Text at the top of .js/.json/.css pages.\n\nWhen translating browser function names, check how they are translated in the localized versions of these web browsers in your language. If a browser is not translated to it, use English or another language in which browsers are most commonly used by the speakers of your language.", + "usercssyoucanpreview": "Text displayed on every CSS page.\n\nSee also:\n* {{msg-mw|Userjsyoucanpreview}}\n* {{msg-mw|Userjsonyoucanpreview}}\n* {{msg-mw|Showpreview}}", + "userjsonyoucanpreview": "Text displayed on every JSON page.\n\nSee also:\n* {{msg-mw|Usercssyoucanpreview}}\n* {{msg-mw|Userjsyoucanpreview}}\n* {{msg-mw|Showpreview}}", + "userjsyoucanpreview": "Text displayed on every JavaScript page.\n\nSee also:\n* {{msg-mw|Userjsonyoucanpreview}}\n* {{msg-mw|Usercssyoucanpreview}}\n* {{msg-mw|Showpreview}}", "usercsspreview": "Text displayed on preview of every user .css subpage.\n\nSee also:\n* {{msg-mw|Sitecsspreview}}", + "userjsonpreview": "Text displayed on preview of every user .json subpage", "userjspreview": "Text displayed on preview of every user .js subpage", "sitecsspreview": "Text displayed on preview of .css pages in MediaWiki namespace.\n\nSee also:\n* {{msg-mw|Usercsspreview}}", + "sitejsonpreview": "Text displayed on preview of .json pages in MediaWiki namespace", "sitejspreview": "Text displayed on preview of .js pages in MediaWiki namespace", "userinvalidconfigtitle": "Parameters:\n* $1 - skin name", "updated": "{{Identical|Updated}}", @@ -951,9 +956,9 @@ "addsection-preload": "{{notranslate}}", "addsection-editintro": "{{notranslate}}", "defaultmessagetext": "Caption above the default message text shown on the left-hand side of a diff displayed after clicking \"Show changes\" when creating a new page in the MediaWiki: namespace", - "content-failed-to-parse": "Error message indicating that the page's content can not be saved because it is syntactically invalid. This may occurr for content types using serialization or a strict markup syntax.\n\nParameters:\n* $1 – content model, any one of the following messages:\n** {{msg-mw|Content-model-wikitext}}\n** {{msg-mw|Content-model-javascript}}\n** {{msg-mw|Content-model-css}}\n** {{msg-mw|Content-model-text}}\n* $2 – content format as MIME type (e.g. text/css)\n* $3 – specific error message", + "content-failed-to-parse": "Error message indicating that the page's content can not be saved because it is syntactically invalid. This may occurr for content types using serialization or a strict markup syntax.\n\nParameters:\n* $1 – content model, any one of the following messages:\n** {{msg-mw|Content-model-wikitext}}\n** {{msg-mw|Content-model-javascript}}\n** {{msg-mw|Content-model-css}}\n** {{msg-mw|Content-model-json}}\n** {{msg-mw|Content-model-text}}\n* $2 – content format as MIME type (e.g. text/css)\n* $3 – specific error message", "invalid-content-data": "Error message indicating that the page's content can not be saved because it is invalid. This may occurr for content types with internal consistency constraints.", - "content-not-allowed-here": "Error message indicating that the desired content model is not supported in given localtion.\n* $1 - the human readable name of the content model: {{msg-mw|Content-model-wikitext}}, {{msg-mw|Content-model-javascript}}, {{msg-mw|Content-model-css}} or {{msg-mw|Content-model-text}}\n* $2 - the title of the page in question", + "content-not-allowed-here": "Error message indicating that the desired content model is not supported in given localtion.\n* $1 - the human readable name of the content model: {{msg-mw|Content-model-wikitext}}, {{msg-mw|Content-model-javascript}}, {{msg-mw|Content-model-json}}, {{msg-mw|Content-model-css}} or {{msg-mw|Content-model-text}}\n* $2 - the title of the page in question", "editwarning-warning": "Uses {{msg-mw|Prefs-editing}}", "editpage-invalidcontentmodel-title": "Title of error page shown when using an unrecognized content model on EditPage", "editpage-invalidcontentmodel-text": "Error message shown when using an unrecognized content model on EditPage. $1 is the user's invalid input", @@ -1276,6 +1281,7 @@ "default": "{{Identical|Default}}", "prefs-files": "Title of a tab in [[Special:Preferences]].\n{{Identical|File}}", "prefs-custom-css": "visible on [[Special:Preferences]] -[Skins].\n{{Identical|Custom CSS}}", + "prefs-custom-json": "visible on [[Special:Preferences]] -[Skins].\n{{Identical|Custom JSON}}", "prefs-custom-js": "visible on [[Special:Preferences]] -[Skins].\n{{Identical|Custom JavaScript}}", "prefs-common-config": "Used as label in [[Special:Preferences#mw-prefsection-rendering|preferences]], tab \"Appearance\", section \"Skin\".\n\nSee also:\n* {{msg-mw|Globalcssjs-custom-css-js}}", "prefs-reset-intro": "Used in [[Special:Preferences/reset]].", @@ -1422,8 +1428,10 @@ "right-editcontentmodel": "{{doc-right|editcontentmodel}}", "right-editinterface": "{{doc-right|editinterface}}", "right-editusercss": "{{doc-right|editusercss}}\nSee also:\n* {{msg-mw|Right-editmyusercss}}", + "right-edituserjson": "{{doc-right|edituserjson}}\nSee also:\n* {{msg-mw|Right-editmyuserjson}}", "right-edituserjs": "{{doc-right|edituserjs}}\nSee also:\n* {{msg-mw|Right-editmyuserjs}}", "right-editmyusercss": "{{doc-right|editmyusercss}}\nSee also:\n* {{msg-mw|Right-editusercss}}", + "right-editmyuserjson": "{{doc-right|editmyuserjson}}\nSee also:\n* {{msg-mw|Right-edituserjson}}", "right-editmyuserjs": "{{doc-right|editmyuserjs}}\nSee also:\n* {{msg-mw|Right-edituserjs}}", "right-viewmywatchlist": "{{doc-right|viewmywatchlist}}", "right-editmywatchlist": "{{doc-right|editmywatchlist}}", @@ -3181,6 +3189,7 @@ "group-bot.css": "{{doc-group|bot|css}}", "group-sysop.css": "{{doc-group|sysop|css}}", "group-bureaucrat.css": "{{doc-group|bureaucrat|css}}", + "common.json": "{{optional}}\nJSON for all users.", "common.js": "{{optional}}\nJS for all users.", "group-autoconfirmed.js": "{{doc-group|autoconfirmed|js}}", "group-user.js": "{{doc-group|user|js}}", @@ -4629,8 +4638,9 @@ "unlinkaccounts": "Title of the special page [[Special:UnlinkAccounts]] which allows the user to remove linked remote accounts.", "unlinkaccounts-success": "Account unlinking form success message", "authenticationdatachange-ignored": "Shown when authentication data change was unsuccessful due to configuration problems.\n\nCf. e.g. {{msg-mw|Passwordreset-ignored}}.", - "userjsispublic": "A reminder to users that Javascript subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .js. See also {{msg-mw|usercssispublic}}.", - "usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}}", + "userjsispublic": "A reminder to users that Javascript subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .js. See also {{msg-mw|usercssispublic}} and {{msg-mw|userjsonispublic}}.", + "userjsonispublic": "A reminder to users that JSON subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .json. See also {{msg-mw|userjsispublic}} and {{msg-mw|usercssispublic}}", + "usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}} and {{msg-mw|userjsonispublic}}", "restrictionsfield-badip": "An error message shown when one entered an invalid IP address or range in a restrictions field (such as Special:BotPassword). $1 is the IP address.", "restrictionsfield-label": "Field label shown for restriction fields (e.g. on Special:BotPassword).", "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword).", diff --git a/tests/phpunit/includes/TitleMethodsTest.php b/tests/phpunit/includes/TitleMethodsTest.php index 9ae84d920b..4032b3a120 100644 --- a/tests/phpunit/includes/TitleMethodsTest.php +++ b/tests/phpunit/includes/TitleMethodsTest.php @@ -172,15 +172,19 @@ class TitleMethodsTest extends MediaWikiLangTestCase { [ 'User:Foo', false ], [ 'User:Foo.js', false ], [ 'User:Foo/bar.js', false ], + [ 'User:Foo/bar.json', false ], [ 'User:Foo/bar.css', false ], [ 'User:Foo/bar.JS', false ], + [ 'User:Foo/bar.JSON', false ], [ 'User:Foo/bar.CSS', false ], [ 'User talk:Foo/bar.css', false ], [ 'User:Foo/bar.js.xxx', false ], [ 'User:Foo/bar.xxx', false ], [ 'MediaWiki:Foo.js', true ], + [ 'MediaWiki:Foo.json', true ], [ 'MediaWiki:Foo.css', true ], [ 'MediaWiki:Foo.JS', false ], + [ 'MediaWiki:Foo.JSON', false ], [ 'MediaWiki:Foo.CSS', false ], [ 'MediaWiki:Foo/bar.css', true ], [ 'MediaWiki:Foo.css.xxx', false ], @@ -207,14 +211,18 @@ class TitleMethodsTest extends MediaWikiLangTestCase { [ 'User:Foo.js', false ], [ 'User:Foo/bar.js', true ], [ 'User:Foo/bar.JS', false ], + [ 'User:Foo/bar.json', true ], + [ 'User:Foo/bar.JSON', false ], [ 'User:Foo/bar.css', true ], [ 'User:Foo/bar.CSS', false ], [ 'User talk:Foo/bar.css', false ], [ 'User:Foo/bar.js.xxx', false ], [ 'User:Foo/bar.xxx', false ], [ 'MediaWiki:Foo.js', false ], + [ 'MediaWiki:Foo.json', false ], [ 'MediaWiki:Foo.css', false ], [ 'MediaWiki:Foo.JS', false ], + [ 'MediaWiki:Foo.JSON', false ], [ 'MediaWiki:Foo.CSS', false ], [ 'MediaWiki:Foo.css.xxx', false ], [ 'TEST-JS:Foo', false ], @@ -237,8 +245,10 @@ class TitleMethodsTest extends MediaWikiLangTestCase { [ 'Help:Foo.css', false ], [ 'User:Foo', false ], [ 'User:Foo.js', false ], + [ 'User:Foo.json', false ], [ 'User:Foo.css', false ], [ 'User:Foo/bar.js', false ], + [ 'User:Foo/bar.json', false ], [ 'User:Foo/bar.css', true ], ]; } @@ -283,15 +293,19 @@ class TitleMethodsTest extends MediaWikiLangTestCase { [ 'User:Foo', true ], [ 'User:Foo.js', true ], [ 'User:Foo/bar.js', false ], + [ 'User:Foo/bar.json', false ], [ 'User:Foo/bar.css', false ], [ 'User talk:Foo/bar.css', true ], [ 'User:Foo/bar.js.xxx', true ], [ 'User:Foo/bar.xxx', true ], [ 'MediaWiki:Foo.js', false ], [ 'User:Foo/bar.JS', true ], + [ 'User:Foo/bar.JSON', true ], [ 'User:Foo/bar.CSS', true ], + [ 'MediaWiki:Foo.json', false ], [ 'MediaWiki:Foo.css', false ], [ 'MediaWiki:Foo.JS', true ], + [ 'MediaWiki:Foo.JSON', true ], [ 'MediaWiki:Foo.CSS', true ], [ 'MediaWiki:Foo.css.xxx', true ], [ 'TEST-JS:Foo', false ], diff --git a/tests/phpunit/includes/TitlePermissionTest.php b/tests/phpunit/includes/TitlePermissionTest.php index 7dfb7357f5..4e342447c4 100644 --- a/tests/phpunit/includes/TitlePermissionTest.php +++ b/tests/phpunit/includes/TitlePermissionTest.php @@ -453,14 +453,38 @@ class TitlePermissionTest extends MediaWikiLangTestCase { $this->runConfigEditPermissions( [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ] ] ); } + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers Title::checkUserConfigPermissions + */ + public function testJsonConfigEditPermissions() { + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->userName . '/test.json' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ] + ); + } + /** * @todo This test method should be split up into separate test methods and * data providers @@ -475,8 +499,10 @@ class TitlePermissionTest extends MediaWikiLangTestCase { [ [ 'badaccess-group0' ] ], [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ] ); } @@ -493,14 +519,38 @@ class TitlePermissionTest extends MediaWikiLangTestCase { $this->runConfigEditPermissions( [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ], [ [ 'badaccess-group0' ] ] ); } + /** + * @todo This test method should be split up into separate test methods and + * data providers + * @covers Title::checkUserConfigPermissions + */ + public function testOtherJsonConfigEditPermissions() { + $this->setUser( $this->userName ); + + $this->setTitle( NS_USER, $this->altUserName . '/test.json' ); + $this->runConfigEditPermissions( + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ] + ); + } + /** * @todo This test method should be split up into separate test methods and * data providers @@ -513,10 +563,12 @@ class TitlePermissionTest extends MediaWikiLangTestCase { $this->runConfigEditPermissions( [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], + [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ], [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ] ); } @@ -533,9 +585,11 @@ class TitlePermissionTest extends MediaWikiLangTestCase { $this->runConfigEditPermissions( [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ] ], [ [ 'badaccess-group0' ] ], [ [ 'badaccess-group0' ] ], + [ [ 'badaccess-group0' ] ], [ [ 'badaccess-group0' ] ], [ [ 'badaccess-group0' ] ] ); @@ -544,8 +598,10 @@ class TitlePermissionTest extends MediaWikiLangTestCase { protected function runConfigEditPermissions( $resultNone, $resultMyCss, + $resultMyJson, $resultMyJs, $resultUserCss, + $resultUserJson, $resultUserJs ) { $this->setUserPerm( '' ); @@ -556,6 +612,10 @@ class TitlePermissionTest extends MediaWikiLangTestCase { $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); $this->assertEquals( $resultMyCss, $result ); + $this->setUserPerm( 'editmyuserjson' ); + $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); + $this->assertEquals( $resultMyJson, $result ); + $this->setUserPerm( 'editmyuserjs' ); $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); $this->assertEquals( $resultMyJs, $result ); @@ -564,11 +624,15 @@ class TitlePermissionTest extends MediaWikiLangTestCase { $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); $this->assertEquals( $resultUserCss, $result ); + $this->setUserPerm( 'edituserjson' ); + $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); + $this->assertEquals( $resultUserJson, $result ); + $this->setUserPerm( 'edituserjs' ); $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); $this->assertEquals( $resultUserJs, $result ); - $this->setUserPerm( [ 'edituserjs', 'editusercss' ] ); + $this->setUserPerm( [ 'edituserjs', 'edituserjson', 'editusercss' ] ); $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user ); $this->assertEquals( [ [ 'badaccess-group0' ] ], $result ); } -- 2.20.1