Goodbye Gadget/Gadget_definition namespaces!

== What ==

* Remove the empty Gadget and Gadget_definition namespaces.
* Remove the "gadgets-definition-edit" user right.
* Remove need for custom namespace permissions that previously
  had to extend editsitejs to apply to NS_GADGET.

== Why ==

Simplify the (unused) "GadgetDefinitionNamespaceRepo" backend for
Gadgets 2.0 by making it less radically different from the status quo.

The experimental 2.0 branch will now make use of the "gadget definition"
content model via "MediaWiki:Gadgets/<id>.json" pages, instead of
through a dedicated namespace.

When I first worked the Gadgets 2.0 branch, content models
were not a thing in MediaWiki, and interface-admin wasn't a thing yet
either. Now that we have per-page permissions and per-page content
models, we don't really need a separate namespace.

This follows the principle of least surprise, and fits well with other
interface admin and site configuration tools such as:
- Citoid, MediaWiki:Citoid-template-type-map.json,
- VisualEditor, MediaWiki:Visualeditor-template-tools-definition.json,
- AbuseFilter, MediaWiki:BlockedExternalDomains.json,
- the upcoming "Community Config" initiative.

If/when we develop the SpecialPage GUI for editing gadget definitions,
this can save its data to these pages the same as it would in
any other namespace. Similar to how Special:BlockedExternalDomains
operates on MediaWiki:BlockedExternalDomains.json.

See also bf1d6b3e93 (I6ffd5e9467), which recently removed the
gadgets-edit user right in favour of the editsite{css,js,json} rights.

Change-Id: I5b04ab251552e839087d0a8a6923d205adc7f771
This commit is contained in:
Timo Tijhof 2023-12-05 17:28:45 -06:00
parent 64c44829b9
commit fce6fdfb20
17 changed files with 147 additions and 620 deletions

View file

@ -1,442 +0,0 @@
<?php
$namespaceNames = [];
// For wikis without Gadgets installed.
if ( !defined( 'NS_GADGET' ) ) {
define( 'NS_GADGET', 2300 );
define( 'NS_GADGET_TALK', 2301 );
define( 'NS_GADGET_DEFINITION', 2302 );
define( 'NS_GADGET_DEFINITION_TALK', 2303 );
}
$namespaceNames['alt'] = [
NS_GADGET => 'Гаджет',
NS_GADGET_TALK => аджетти_шӱӱжери',
NS_GADGET_DEFINITION => аджетти_аайлары',
NS_GADGET_DEFINITION_TALK => аджеттиҥ_аайларын_шӱӱжери',
];
$namespaceNames['an'] = [
NS_GADGET => 'Accesorio',
NS_GADGET_TALK => 'Descusión_accesorio',
NS_GADGET_DEFINITION => 'Accesorio_definición',
NS_GADGET_DEFINITION_TALK => 'Descusión_definición_accesorio',
];
$namespaceNames['anp'] = [
NS_GADGET => 'गैजेट',
NS_GADGET_TALK => 'गैजेट_वार्ता',
NS_GADGET_DEFINITION => 'गैजेट_परिभाषा',
NS_GADGET_DEFINITION_TALK => 'गैजेट_परिभाषा_वार्ता',
];
$namespaceNames['ar'] = [
NS_GADGET => 'إضافة',
NS_GADGET_TALK => 'نقاش_الإضافة',
NS_GADGET_DEFINITION => 'تعريف_الإضافة',
NS_GADGET_DEFINITION_TALK => 'نقاش_تعريف_الإضافة',
];
$namespaceNames['ast'] = [
NS_GADGET => 'Accesoriu',
NS_GADGET_TALK => 'Accesoriu_alderique',
NS_GADGET_DEFINITION => 'Accesoriu_definición',
NS_GADGET_DEFINITION_TALK => 'Accesoriu_definición_alderique',
];
$namespaceNames['atj'] = [
NS_GADGET => 'Gadget',
NS_GADGET_TALK => 'Ka_ici_aimihitonaniwok_gadget',
NS_GADGET_DEFINITION => 'Tipatcitcikan_e_icinakok_gadget',
NS_GADGET_DEFINITION_TALK => 'Ka_ici_aimihitonaniwok_tipatcitcikan_gadget_otci',
];
$namespaceNames['av'] = [
NS_GADGET => 'Гаджет',
NS_GADGET_TALK => аджеталъул_бахӀс',
NS_GADGET_DEFINITION => аджеталъул_баян_чӀезаби',
NS_GADGET_DEFINITION_TALK => аджеталъул_баян_чӀезабиялъул_бахӀс',
];
$namespaceNames['az'] = [
NS_GADGET => 'Qadcet',
NS_GADGET_TALK => 'Qadcet müzakirəsi',
];
$namespaceNames['azb'] = [
NS_GADGET => 'آلت',
NS_GADGET_TALK => 'آلت_دانیشیغی',
NS_GADGET_DEFINITION => 'آلت_آچیقلاماسی',
NS_GADGET_DEFINITION_TALK => 'آلت_آچیقلاماسی_دانیشیغی',
];
$namespaceNames['ba'] = [
NS_GADGET => 'Гаджет',
NS_GADGET_TALK => аджет_буйынсаекерләшеү',
NS_GADGET_DEFINITION => аджет_билдәһе',
NS_GADGET_DEFINITION_TALK => аджет_билдәһе_буйынсаекерләшеү',
];
$namespaceNames['bgn'] = [
NS_GADGET => 'وسیلهان',
NS_GADGET_TALK => 'وسیلهان_ئی_گپ',
NS_GADGET_DEFINITION => 'وسیلهانی_شرح',
NS_GADGET_DEFINITION_TALK => 'وسیلهانی_شرح_ئی_گپ',
];
$namespaceNames['bn'] = [
NS_GADGET => 'গ্যাজেট',
NS_GADGET_TALK => 'গ্যাজেট_আলোচনা',
NS_GADGET_DEFINITION => 'গ্যাজেট_সংজ্ঞা',
NS_GADGET_DEFINITION_TALK => 'গ্যাজেট_সংজ্ঞার_আলোচনা',
];
$namespaceNames['ce'] = [
NS_GADGET => 'Гаджет',
NS_GADGET_TALK => аджет_йийцар',
NS_GADGET_DEFINITION => аджет_къастор',
NS_GADGET_DEFINITION_TALK => аджет_къастор_дийцар',
];
$namespaceNames['ckb'] = [
NS_GADGET => 'ئامراز',
NS_GADGET_TALK => 'وتووێژی_ئامراز',
NS_GADGET_DEFINITION => 'پێناسهی_ئامراز',
NS_GADGET_DEFINITION_TALK => 'وتووێژی_پێناسهی_ئامراز',
];
$namespaceNames['cs'] = [
NS_GADGET => 'Udělátko',
NS_GADGET_TALK => 'Diskuse_k_udělátku',
NS_GADGET_DEFINITION => 'Definice_udělátka',
NS_GADGET_DEFINITION_TALK => 'Diskuse_k_definici_udělátka',
];
$namespaceNames['de'] = [
NS_GADGET => 'Gadget',
NS_GADGET_TALK => 'Gadget_Diskussion',
NS_GADGET_DEFINITION => 'Gadget-Definition',
NS_GADGET_DEFINITION_TALK => 'Gadget-Definition_Diskussion',
];
$namespaceNames['din'] = [
NS_GADGET => 'Muluuitet',
NS_GADGET_TALK => 'Jam_wɛ̈t_ë_muluuitet',
NS_GADGET_DEFINITION => 'Wɛ̈tdic_ë_muluuitet',
NS_GADGET_DEFINITION_TALK => 'Jam_wɛ̈t_ë_wɛ̈tdic_ë_muluuitet',
];
$namespaceNames['diq'] = [
NS_GADGET => 'Halet',
NS_GADGET_TALK => 'Halet_vaten',
NS_GADGET_DEFINITION => 'Halet_şınasnayış',
NS_GADGET_DEFINITION_TALK => 'Halet_şınasnayış_vaten',
];
$namespaceNames['dty'] = [
NS_GADGET => 'ग्याजेट',
NS_GADGET_TALK => 'ग्याजेट_कुरणि',
NS_GADGET_DEFINITION => 'ग्याजेट_परिभाषा',
NS_GADGET_DEFINITION_TALK => 'ग्याजेट_परिभाषा_कुरणि',
];
$namespaceNames['en'] = [
NS_GADGET => 'Gadget',
NS_GADGET_TALK => 'Gadget_talk',
NS_GADGET_DEFINITION => 'Gadget_definition',
NS_GADGET_DEFINITION_TALK => 'Gadget_definition_talk',
];
$namespaceNames['es'] = [
NS_GADGET => 'Accesorio',
NS_GADGET_TALK => 'Accesorio_discusión',
NS_GADGET_DEFINITION => 'Accesorio_definición',
NS_GADGET_DEFINITION_TALK => 'Accesorio_definición_discusión',
];
$namespaceNames['et'] = [
NS_GADGET => 'Tööriist',
NS_GADGET_TALK => 'Tööriista_arutelu',
NS_GADGET_DEFINITION => 'Tööriista_määratlus',
NS_GADGET_DEFINITION_TALK => 'Tööriista_määratluse_arutelu',
];
$namespaceNames['eu'] = [
NS_GADGET => 'Gadget',
NS_GADGET_TALK => 'Gadget_eztabaida',
NS_GADGET_DEFINITION => 'Gadget_definizio',
NS_GADGET_DEFINITION_TALK => 'Gadget_definizio_eztabaida',
];
$namespaceNames['fa'] = [
NS_GADGET => 'ابزار',
NS_GADGET_TALK => 'بحث_ابزار',
NS_GADGET_DEFINITION => 'توضیحات_ابزار',
NS_GADGET_DEFINITION_TALK => 'بحث_توضیحات_ابزار',
];
$namespaceNames['fi'] = [
NS_GADGET => 'Pienoisohjelma',
NS_GADGET_TALK => 'Keskustelu_pienoisohjelmasta',
NS_GADGET_DEFINITION => 'Pienoisohjelman_määritys',
NS_GADGET_DEFINITION_TALK => 'Keskustelu_pienoisohjelman_määrityksestä',
];
$namespaceNames['fr'] = [
NS_GADGET => 'Gadget',
NS_GADGET_TALK => 'Discussion_gadget',
NS_GADGET_DEFINITION => 'Définition_de_gadget',
NS_GADGET_DEFINITION_TALK => 'Discussion_définition_de_gadget',
];
$namespaceNames['he'] = [
NS_GADGET => 'גאדג\'ט',
NS_GADGET_TALK => יחת_גאדג\'ט',
NS_GADGET_DEFINITION => 'הגדרת_גאדג\'ט',
NS_GADGET_DEFINITION_TALK => יחת_הגדרת_גאדג\'ט',
];
$namespaceNames['hi'] = [
NS_GADGET => 'गैजेट',
NS_GADGET_TALK => 'गैजेट वार्ता',
NS_GADGET_DEFINITION => 'गैजेट परिभाषा',
NS_GADGET_DEFINITION_TALK => 'गैजेट परिभाषा वार्ता',
];
$namespaceNames['inh'] = [
NS_GADGET => 'Гаджет',
NS_GADGET_TALK => аджет_ювцар',
NS_GADGET_DEFINITION => аджета_къоастадар',
NS_GADGET_DEFINITION_TALK => аджета_къоастадарувцар',
];
$namespaceNames['is'] = [
NS_GADGET => 'Smától',
NS_GADGET_TALK => 'Smátólaspjall',
NS_GADGET_DEFINITION => 'Smátóla_skilgreining',
NS_GADGET_DEFINITION_TALK => 'Smátóla_skilgreiningarspjall',
];
$namespaceNames['it'] = [
NS_GADGET => 'Accessorio',
NS_GADGET_TALK => 'Discussioni_accessorio',
NS_GADGET_DEFINITION => 'Definizione_accessorio',
NS_GADGET_DEFINITION_TALK => 'Discussioni_definizione_accessorio',
];
$namespaceNames['ko'] = [
NS_GADGET => '소도구',
NS_GADGET_TALK => '소도구토론',
NS_GADGET_DEFINITION => '소도구정의',
NS_GADGET_DEFINITION_TALK => '소도구정의토론',
];
$namespaceNames['ks-arab'] = [
NS_GADGET => 'آلہٕ',
NS_GADGET_TALK => 'آلہٕ_کَتھ',
NS_GADGET_DEFINITION => 'آلہٕ_تَعارُف',
NS_GADGET_DEFINITION_TALK => 'آلہٕ_تَعارُف_کَتھ',
];
$namespaceNames['ky'] = [
NS_GADGET => 'Гаджет',
NS_GADGET_TALK => аджетти_талкуулоо',
NS_GADGET_DEFINITION => аджеттин_түшүндүрмөсү',
NS_GADGET_DEFINITION_TALK => аджеттин_түшүндүрмөсүн_талкуулоо',
];
$namespaceNames['lfn'] = [
NS_GADGET => 'Macineta',
NS_GADGET_TALK => 'Macineta_Discute',
NS_GADGET_DEFINITION => 'Defini_de_macineta',
NS_GADGET_DEFINITION_TALK => 'Defini_de_macineta_Discute',
];
$namespaceNames['lrc'] = [
NS_GADGET => 'گأجئت',
NS_GADGET_TALK => 'چأک_چئنە_گأجئت',
NS_GADGET_DEFINITION => 'توضییا_گأجئت',
NS_GADGET_DEFINITION_TALK => 'چأک_چئنە_توضییا_گأجئت',
];
$namespaceNames['ms'] = [
NS_GADGET => 'Alat',
NS_GADGET_TALK => 'Perbincangan_alat',
NS_GADGET_DEFINITION => 'Penerangan_alat',
NS_GADGET_DEFINITION_TALK => 'Perbincangan_penerangan_alat',
];
$namespaceNames['ms-arab'] = [
NS_GADGET => 'الت',
NS_GADGET_TALK => 'ڤربينچڠن_الت',
NS_GADGET_DEFINITION => 'ڤنرڠن_الت',
NS_GADGET_DEFINITION_TALK => 'ڤربينچڠن_ڤنرڠن_الت',
];
$namespaceNames['mnw'] = [
NS_GADGET => 'ကိရိယာ',
NS_GADGET_TALK => 'ကိရိယာ_ဓရီုကျာ',
NS_GADGET_DEFINITION => 'ကိရိယာ_ပွံက်အဓိပ္ပါယ်',
NS_GADGET_DEFINITION_TALK => 'ကိရိယာ_ပွံက်အဓိပ္ပါယ်_ဓရီုကျာ',
];
$namespaceNames['mwl'] = [
NS_GADGET => 'Gadget',
NS_GADGET_TALK => 'Cumbersa_gadget',
NS_GADGET_DEFINITION => 'Defeniçon_gadget',
NS_GADGET_DEFINITION_TALK => 'Cumbersa_defeniçon_gadget',
];
$namespaceNames['mzn'] = [
NS_GADGET => 'گجت',
NS_GADGET_TALK => 'گجت_گپ',
NS_GADGET_DEFINITION => 'گجت_توضیحات',
NS_GADGET_DEFINITION_TALK => 'گجت_توضیحات_گپ',
];
$namespaceNames['my'] = [
NS_GADGET => 'ကိရိယာငယ်',
NS_GADGET_TALK => 'ကိရိယာငယ်_ဆွေးနွေးချက်',
NS_GADGET_DEFINITION => 'ကိရိယာငယ်_အဓိပ္ပာယ်',
NS_GADGET_DEFINITION_TALK => 'ကိရိယာငယ်_အဓိပ္ပာယ်_ဆွေးနွေးချက်',
];
$namespaceNames['nap'] = [
NS_GADGET => 'Pazziella',
NS_GADGET_TALK => 'Pazziella_chiàcchiera',
NS_GADGET_DEFINITION => 'Pazziella_definizzione',
NS_GADGET_DEFINITION_TALK => 'Pazziella_definizzione_chiàcchiera',
];
$namespaceNames['nl'] = [
NS_GADGET => 'Uitbreiding',
NS_GADGET_TALK => 'Overleg_uitbreiding',
NS_GADGET_DEFINITION => 'Uitbreidingsdefinitie',
NS_GADGET_DEFINITION_TALK => 'Overleg_uitbreidingsdefinitie',
];
$namespaceNames['or'] = [
NS_GADGET => 'ଗ୍ୟାଜେଟ',
NS_GADGET_TALK => 'ଗ୍ୟାଜେଟ_ଆଲୋଚନା',
NS_GADGET_DEFINITION => 'ଗ୍ୟାଜେଟ_ସଂଜ୍ଞା',
NS_GADGET_DEFINITION_TALK => 'ଗ୍ୟାଜେଟ_ସଂଜ୍ଞା_ଆଲୋଚନା',
];
$namespaceNames['pa'] = [
NS_GADGET => 'ਗੈਜਟ',
NS_GADGET_TALK => 'ਗੈਜਟ_ਗੱਲ-ਬਾਤ',
NS_GADGET_DEFINITION => 'ਗੈਜਟ_ਪਰਿਭਾਸ਼ਾ',
NS_GADGET_DEFINITION_TALK => 'ਗੈਜਟ_ਪਰਿਭਾਸ਼ਾ_ਗੱਲ-ਬਾਤ',
];
$namespaceNames['pcm'] = [
NS_GADGET => 'Gajet',
NS_GADGET_TALK => 'Gajet_tok_abaut_am',
NS_GADGET_DEFINITION => 'Gajet_definishon',
NS_GADGET_DEFINITION_TALK => 'Gajet_definishon_tok_abaut_am',
];
$namespaceNames['pl'] = [
NS_GADGET => 'Gadżet',
NS_GADGET_TALK => 'Dyskusja_gadżetu',
NS_GADGET_DEFINITION => 'Definicja_gadżetu',
NS_GADGET_DEFINITION_TALK => 'Dyskusja_definicji_gadżetu',
];
$namespaceNames['pnb'] = [
NS_GADGET => 'آلہ',
NS_GADGET_TALK => 'آلہ_گل_بات',
NS_GADGET_DEFINITION => 'آلہ_تعریف',
NS_GADGET_DEFINITION_TALK => 'آلہ_تعریف_گل_بات',
];
$namespaceNames['ro'] = [
NS_GADGET => 'Gadget',
NS_GADGET_TALK => 'Discuție_Gadget',
NS_GADGET_DEFINITION => 'Definiție_gadget',
NS_GADGET_DEFINITION_TALK => 'Discuție_Definiție_gadget',
];
$namespaceNames['ru'] = [
NS_GADGET => 'Гаджет',
NS_GADGET_TALK => 'Обсуждение_гаджета',
NS_GADGET_DEFINITION => 'Определение_гаджета',
NS_GADGET_DEFINITION_TALK => 'Обсуждение_определения_гаджета',
];
$namespaceNames['sat'] = [
NS_GADGET => 'ᱥᱟᱢᱟᱱᱚᱢ',
NS_GADGET_TALK => 'ᱥᱟᱢᱟᱱᱚᱢ_ᱜᱟᱞᱢᱟᱨᱟᱣ',
NS_GADGET_DEFINITION => 'ᱥᱟᱢᱟᱱᱚᱢ_ᱢᱮᱱᱮᱛᱮᱫ',
NS_GADGET_DEFINITION_TALK => 'ᱥᱟᱢᱟᱱᱚᱢ_ᱢᱮᱱᱮᱛᱮᱫ_ᱜᱟᱞᱢᱟᱨᱟᱣ',
];
$namespaceNames['scn'] = [
NS_GADGET => 'Accissoriu',
NS_GADGET_TALK => 'Discussioni_accissoriu',
NS_GADGET_DEFINITION => 'Difinizzioni_accissoriu',
NS_GADGET_DEFINITION_TALK => 'Discussioni_difinizzioni_accissoriu',
];
$namespaceNames['sd'] = [
NS_GADGET => 'گيجيٽ',
NS_GADGET_TALK => 'گيجيٽ_بحث',
NS_GADGET_DEFINITION => 'گيجيٽ_وصف',
NS_GADGET_DEFINITION_TALK => 'گيجيٽ_وصف_بحث',
];
$namespaceNames['shn'] = [
NS_GADGET => 'ၶိူင်ႈပိတ်းပွတ်း',
NS_GADGET_TALK => 'ဢုပ်ႇၵုမ်_ၶိူင်ႈပိတ်းပွတ်း',
NS_GADGET_DEFINITION => 'ပိုတ်ႇတီႈပွင်ႇ_ၶိူင်ႈပိတ်းပွတ်း',
NS_GADGET_DEFINITION_TALK => 'ဢုပ်ႇၵုမ်_ပိုတ်ႇတီႈပွင်ႇ_ၶိူင်ႈပိတ်းပွတ်း',
];
$namespaceNames['sl'] = [
NS_GADGET => 'Pripomoček',
NS_GADGET_TALK => 'Pogovor_o_pripomočku',
NS_GADGET_DEFINITION => 'Opredelitev_pripomočka',
NS_GADGET_DEFINITION_TALK => 'Pogovor_o_opredelitvi_pripomočka',
];
$namespaceNames['sr-ec'] = [
NS_GADGET => 'Справица',
NS_GADGET_TALK => 'Разговор_о_справици',
NS_GADGET_DEFINITION => ефиниција_справице',
NS_GADGET_DEFINITION_TALK => 'Разговор_оефиницији_справице',
];
$namespaceNames['sr-el'] = [
NS_GADGET => 'Spravica',
NS_GADGET_TALK => 'Razgovor_o_spravici',
NS_GADGET_DEFINITION => 'Definicija_spravice',
NS_GADGET_DEFINITION_TALK => 'Razgovor_o_definiciji_spravice',
];
$namespaceNames['ti'] = [
NS_GADGET => 'መሳርሒ',
NS_GADGET_TALK => 'ምይይጥ_መሳርሒ',
NS_GADGET_DEFINITION => 'መብርሂ_መሳርሒ',
NS_GADGET_DEFINITION_TALK => 'ምይይጥ_መብርሂ_መሳርሒ',
];
$namespaceNames['ur'] = [
NS_GADGET => 'آلہ',
NS_GADGET_TALK => 'تبادلۂ_خیال_آلہ',
NS_GADGET_DEFINITION => 'تعریف_آلہ',
NS_GADGET_DEFINITION_TALK => 'تبادلۂ_خیال_تعریف_آلہ',
];
$namespaceNames['uz'] = [
NS_GADGET => 'Gadjet',
NS_GADGET_TALK => 'Gadjet_munozarasi',
NS_GADGET_DEFINITION => 'Gadjet_taʼrifi',
NS_GADGET_DEFINITION_TALK => 'Gadjet_taʼrifi_munozarasi',
];
$namespaceNames['vi'] = [
NS_GADGET => 'Tiện_ích',
NS_GADGET_TALK => 'Thảo_luận_Tiện_ích',
NS_GADGET_DEFINITION => 'Định_nghĩa_tiện_ích',
NS_GADGET_DEFINITION_TALK => 'Thảo_luận_Định_nghĩa_tiện_ích',
];

View file

@ -11,38 +11,9 @@
"MediaWiki": ">= 1.42"
},
"type": "other",
"namespaces": [
{
"id": 2300,
"constant": "NS_GADGET",
"name": "Gadget",
"capitallinkoverride": false
},
{
"id": 2301,
"constant": "NS_GADGET_TALK",
"name": "Gadget_talk"
},
{
"id": 2302,
"constant": "NS_GADGET_DEFINITION",
"name": "Gadget_definition",
"protection": "gadgets-definition-edit",
"capitallinkoverride": false,
"defaultcontentmodel": "GadgetDefinition"
},
{
"id": 2303,
"constant": "NS_GADGET_DEFINITION_TALK",
"name": "Gadget_definition_talk"
}
],
"ContentHandlers": {
"GadgetDefinition": "MediaWiki\\Extension\\Gadgets\\Content\\GadgetDefinitionContentHandler"
},
"AvailableRights": [
"gadgets-definition-edit"
],
"SpecialPages": {
"Gadgets": {
"class": "MediaWiki\\Extension\\Gadgets\\Special\\SpecialGadgets",
@ -78,8 +49,7 @@
]
},
"ExtensionMessagesFiles": {
"GadgetsAlias": "Gadgets.alias.php",
"GadgetsNamespaces": "Gadgets.namespaces.php"
"GadgetsAlias": "Gadgets.alias.php"
},
"RawHtmlMessages": [
"gadgets-definition"

View file

@ -59,9 +59,5 @@
"gadgets-validate-invalidtitle": "Page title \"$1\" is invalid",
"gadgets-validate-unknownpages": "Contains one or more pages without .js, .css or .json suffix. They would not be used.",
"gadgets-validate-nopage": "Page \"$1\" does not exist.",
"right-gadgets-definition-edit": "Edit gadget definitions",
"action-gadgets-definition-edit": "edit this gadget definition",
"gadgets-wrong-contentmodel": "Pages in {{ns:2302}} namespace must be of GadgetDefinition content model.",
"gadgets-supports-urlload": "This gadget supports loading via URL with <code>?withgadget</code> query parameter.",
"gadgets-protected": "You do not have permission to edit pages in {{ns:2300}} namespace."
"gadgets-supports-urlload": "This gadget supports loading via URL with <code>?withgadget</code> query parameter."
}

View file

@ -74,9 +74,5 @@
"gadgets-validate-invalidtitle": "Warning message to indicate that the page title is invalid. Parameters:\n* $1 - page name",
"gadgets-validate-unknownpages": "Warning message to indicate that a gadget contains pages without .js, .css or .json suffix, which are not recognised.",
"gadgets-validate-nopage": "Warning message to indicate the script/style/json page does not exist. Parameters:\n* $1 - page name",
"right-gadgets-definition-edit": "{{doc-right|gadgets-definition-edit}}",
"action-gadgets-definition-edit": "{{doc-action|gadgets-definition-edit}}",
"gadgets-wrong-contentmodel": "Error message shown while trying to change the content model of a page in gadget definition namespace to other than GadgetDefinition.",
"gadgets-supports-urlload": "Used in [[Special:Gadgets]], if the gadget supports ?withgadget query parameter.",
"gadgets-protected": "Error message shown while trying to edit any non-CSS/JS/JSON pages in gadget namespace, if user doesn't have permission"
"gadgets-supports-urlload": "Used in [[Special:Gadgets]], if the gadget supports ?withgadget query parameter."
}

View file

@ -12,8 +12,12 @@ use MediaWiki\Title\Title;
class CodeEditorHooks implements CodeEditorGetPageLanguageHook {
/**
* Set the CodeEditor language for Gadget definition pages. It already
* knows the language for Gadget: namespace pages.
* Set the CodeEditor language for GadgetDefinition pages.
*
* The CodeEditor extension sets the default syntax highlight language based
* on the content model (not page title), so while gadget definitions have ".json"
* page titles, the fact that we use a more specific subclass as content model,
* means we must explicitly opt-in to JSON syntax highlighting.
*
* @param Title $title
* @param string|null &$lang

View file

@ -24,7 +24,9 @@ use Content;
use FormatJson;
use JsonContentHandler;
use MediaWiki\Content\Renderer\ContentParseParams;
use MediaWiki\Extension\Gadgets\MediaWikiGadgetsJsonRepo;
use MediaWiki\Linker\Linker;
use MediaWiki\MediaWikiServices;
use MediaWiki\Parser\ParserOutput;
use MediaWiki\Title\Title;
@ -38,7 +40,7 @@ class GadgetDefinitionContentHandler extends JsonContentHandler {
* @return bool
*/
public function canBeUsedOn( Title $title ) {
return $title->inNamespace( NS_GADGET_DEFINITION );
return MediaWikiGadgetsJsonRepo::isGadgetDefinitionTitle( $title );
}
/** @inheritDoc */
@ -102,23 +104,28 @@ class GadgetDefinitionContentHandler extends JsonContentHandler {
if ( $data !== null ) {
if ( isset( $data->module->pages ) ) {
foreach ( $data->module->pages as &$page ) {
$title = Title::makeTitleSafe( NS_GADGET, $page );
$title = Title::makeTitleSafe( NS_MEDIAWIKI, "Gadget-$page" );
$this->makeLink( $parserOutput, $page, $title );
}
}
$gadgetRepo = MediaWikiServices::getInstance()->getService( 'GadgetsRepo' );
if ( isset( $data->module->dependencies ) ) {
foreach ( $data->module->dependencies as &$dep ) {
if ( str_starts_with( $dep, 'ext.gadget.' ) ) {
$gadgetId = explode( 'ext.gadget.', $dep )[ 1 ];
$title = Title::makeTitleSafe( NS_GADGET_DEFINITION, $gadgetId );
$this->makeLink( $parserOutput, $dep, $title );
$title = $gadgetRepo->getGadgetDefinitionTitle( $gadgetId );
if ( $title ) {
$this->makeLink( $parserOutput, $dep, $title );
}
}
}
}
if ( isset( $data->module->peers ) ) {
foreach ( $data->module->peers as &$peer ) {
$title = Title::makeTitleSafe( NS_GADGET_DEFINITION, $peer );
$this->makeLink( $parserOutput, $peer, $title );
$title = $gadgetRepo->getGadgetDefinitionTitle( $peer );
if ( $title ) {
$this->makeLink( $parserOutput, $peer, $title );
}
}
}
if ( isset( $data->module->messages ) ) {

View file

@ -119,7 +119,7 @@ class Gadget {
*/
public static function serializeDefinition( string $id, array $data ): array {
$prefixGadgetNs = static function ( $page ) {
return 'Gadget:' . $page;
return GadgetRepo::RESOURCE_TITLE_PREFIX . $page;
};
return [
'category' => $data['settings']['category'],

View file

@ -15,10 +15,8 @@ abstract class GadgetRepo {
*/
private static $instance;
/**
* @var string
*/
protected $titlePrefix;
/** @internal */
public const RESOURCE_TITLE_PREFIX = 'MediaWiki:Gadget-';
/**
* Get the ids of the gadgets provided by this repository
@ -82,17 +80,17 @@ abstract class GadgetRepo {
}
/**
* Get the script file name without the "MediaWiki:Gadget-" or "Gadget:" prefix.
* This name is used by the client-side require() so that require("Data.json") resolves
* to either "MediaWiki:Gadget-Data.json" or "Gadget:Data.json" depending on the
* $wgGadgetsRepo configuration, enabling easy migration between the configuration modes.
* Get the page name without "MediaWiki:Gadget-" prefix.
*
* This name is used by `mw.loader.require()` so that `require("./example.json")` resolves
* to `MediaWiki:Gadget-example.json`.
*
* @param string $titleText
* @return string
*/
public function titleWithoutPrefix( string $titleText ): string {
$numReplaces = 1; // there will only one occurrence of the prefix
return str_replace( $this->titlePrefix, '', $titleText, $numReplaces );
return str_replace( self::RESOURCE_TITLE_PREFIX, '', $titleText, $numReplaces );
}
/**

View file

@ -362,40 +362,22 @@ class Hooks implements
$status->merge( $validateStatus );
return false;
}
} else {
$title = $context->getTitle();
if ( $title->inNamespace( NS_GADGET_DEFINITION ) ) {
$status->merge( Status::newFatal( "gadgets-wrong-contentmodel" ) );
return false;
}
}
return true;
}
/**
* Mark the Title as having a content model of javascript or css for pages
* in the Gadget namespace based on their file extension
* Create "MediaWiki:Gadgets/<id>.json" pages with GadgetDefinitionContent
*
* @param Title $title
* @param string &$model
* @return bool
*/
public function onContentHandlerDefaultModelFor( $title, &$model ) {
if ( $title->inNamespace( NS_GADGET ) ) {
preg_match( '!\.(css|js|json)$!u', $title->getText(), $ext );
$ext = $ext[1] ?? '';
switch ( $ext ) {
case 'js':
$model = 'javascript';
return false;
case 'css':
$model = 'css';
return false;
case 'json':
$model = 'json';
return false;
}
if ( MediaWikiGadgetsJsonRepo::isGadgetDefinitionTitle( $title ) ) {
$model = 'GadgetDefinition';
return false;
}
return true;
@ -430,36 +412,13 @@ class Hooks implements
* @return bool|void
*/
public function onGetUserPermissionsErrors( $title, $user, $action, &$result ) {
if ( $action !== 'edit' || !$title->inNamespace( NS_GADGET ) ) {
return true;
if ( $action === 'edit'
&& MediaWikiGadgetsJsonRepo::isGadgetDefinitionTitle( $title )
) {
if ( !$user->isAllowed( 'editsitejs' ) ) {
$result = ApiMessage::create( wfMessage( 'sitejsprotected' ), 'sitejsprotected' );
return false;
}
}
switch ( $title->getContentModel() ) {
case CONTENT_MODEL_JSON:
if ( !$user->isAllowed( 'editsitejson' ) ) {
$result = ApiMessage::create( wfMessage( 'sitejsonprotected' ), 'sitejsonprotected' );
return false;
}
break;
case CONTENT_MODEL_CSS:
if ( !$user->isAllowed( 'editsitecss' ) ) {
$result = ApiMessage::create( wfMessage( 'sitecssprotected' ), 'sitecssprotected' );
return false;
}
break;
case CONTENT_MODEL_JAVASCRIPT:
if ( !$user->isAllowed( 'editsitejs' ) ) {
$result = ApiMessage::create( wfMessage( 'sitejsprotected' ), 'sitejsprotected' );
return false;
}
break;
default:
// For any other pages in the namespace
if ( !$user->isAllowed( 'editsitejs' ) ) {
$result = ApiMessage::create( wfMessage( 'gadgets-protected' ), 'permissiondenied' );
return false;
}
break;
}
return true;
}
}

View file

@ -40,9 +40,6 @@ class MediaWikiGadgetsDefinitionRepo extends GadgetRepo {
/** @var array|null */
private $definitions;
/** @var string */
protected $titlePrefix = 'MediaWiki:Gadget-';
private WANObjectCache $wanCache;
private RevisionLookup $revLookup;
@ -300,7 +297,7 @@ class MediaWikiGadgetsDefinitionRepo extends GadgetRepo {
}
foreach ( preg_split( '/\s*\|\s*/', $pages, -1, PREG_SPLIT_NO_EMPTY ) as $page ) {
$info['pages'][] = $this->titlePrefix . trim( $page );
$info['pages'][] = self::RESOURCE_TITLE_PREFIX . trim( $page );
}
return new Gadget( $info );

View file

@ -10,23 +10,23 @@ use MediaWiki\Revision\SlotRecord;
use MediaWiki\Title\Title;
use WANObjectCache;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IExpression;
use Wikimedia\Rdbms\LikeValue;
/**
* GadgetRepo implementation where each gadget has a page in
* the Gadget definition namespace, and scripts and styles are
* located in the Gadget namespace.
* Gadgets repo powered by `MediaWiki:Gadgets/<id>.json` pages.
*
* Each gadget has its own gadget definition page, using GadgetDefinitionContent.
*/
class GadgetDefinitionNamespaceRepo extends GadgetRepo {
class MediaWikiGadgetsJsonRepo extends GadgetRepo {
/**
* How long in seconds the list of gadget ids and
* individual gadgets should be cached for (1 day)
*/
private const CACHE_TTL = 86400;
/**
* @var string
*/
protected $titlePrefix = 'Gadget:';
public const DEF_PREFIX = 'Gadgets/';
public const DEF_SUFFIX = '.json';
/**
* @var WANObjectCache
@ -52,7 +52,7 @@ class GadgetDefinitionNamespaceRepo extends GadgetRepo {
$key = $this->getGadgetIdsKey();
$fname = __METHOD__;
return $this->wanCache->getWithSetCallback(
$titles = $this->wanCache->getWithSetCallback(
$key,
self::CACHE_TTL,
static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
@ -62,25 +62,44 @@ class GadgetDefinitionNamespaceRepo extends GadgetRepo {
return $dbr->newSelectQueryBuilder()
->select( 'page_title' )
->from( 'page' )
->where( [ 'page_namespace' => NS_GADGET_DEFINITION ] )
->where( [
'page_namespace' => NS_MEDIAWIKI,
'page_content_model' => 'GadgetDefinition',
$dbr->expr(
'page_title',
IExpression::LIKE,
new LikeValue( self::DEF_PREFIX, $dbr->anyString(), self::DEF_SUFFIX )
)
] )
->caller( $fname )
->fetchFieldValues();
},
[
'checkKeys' => [ $key ],
'pcTTL' => WANObjectCache::TTL_PROC_SHORT,
// Bump when changing the database query.
'version' => 2,
'lockTSE' => 30
]
);
$ids = [];
foreach ( $titles as $title ) {
$id = self::getGadgetId( $title );
if ( $id !== '' ) {
$ids[] = $id;
}
}
return $ids;
}
/**
* @inheritDoc
*/
public function handlePageUpdate( LinkTarget $target ): void {
if ( $target->inNamespace( NS_GADGET_DEFINITION ) ) {
if ( $this->isGadgetDefinitionTitle( $target ) ) {
$this->purgeGadgetIdsList();
$this->purgeGadgetEntry( $target->getText() );
$this->purgeGadgetEntry( self::getGadgetId( $target->getText() ) );
}
}
@ -91,11 +110,39 @@ class GadgetDefinitionNamespaceRepo extends GadgetRepo {
$this->wanCache->touchCheckKey( $this->getGadgetIdsKey() );
}
/**
* @param string $title Gadget definition page title
* @return string Gadget ID
*/
private static function getGadgetId( string $title ): string {
if ( !str_starts_with( $title, self::DEF_PREFIX ) || !str_ends_with( $title, self::DEF_SUFFIX ) ) {
throw new InvalidArgumentException( 'Invalid definition page title' );
}
return substr( $title, strlen( self::DEF_PREFIX ), -strlen( self::DEF_SUFFIX ) );
}
/**
* @param LinkTarget $target
* @return bool
*/
public static function isGadgetDefinitionTitle( LinkTarget $target ): bool {
if ( !$target->inNamespace( NS_MEDIAWIKI ) ) {
return false;
}
$title = $target->getText();
try {
self::getGadgetId( $title );
return true;
} catch ( InvalidArgumentException $e ) {
return false;
}
}
/**
* @inheritDoc
*/
public function getGadgetDefinitionTitle( string $id ): ?Title {
return Title::makeTitleSafe( NS_GADGET_DEFINITION, $id );
return Title::makeTitleSafe( NS_MEDIAWIKI, self::DEF_PREFIX . $id . self::DEF_SUFFIX );
}
/**
@ -110,7 +157,7 @@ class GadgetDefinitionNamespaceRepo extends GadgetRepo {
self::CACHE_TTL,
function ( $old, &$ttl, array &$setOpts ) use ( $id ) {
$setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
$title = Title::makeTitleSafe( NS_GADGET_DEFINITION, $id );
$title = $this->getGadgetDefinitionTitle( $id );
if ( !$title ) {
$ttl = WANObjectCache::TTL_UNCACHEABLE;
return null;
@ -143,7 +190,7 @@ class GadgetDefinitionNamespaceRepo extends GadgetRepo {
);
if ( $gadget === null ) {
throw new InvalidArgumentException( "No gadget registered for '$id'" );
throw new InvalidArgumentException( "Unknown gadget $id" );
}
return new Gadget( $gadget );
@ -162,7 +209,7 @@ class GadgetDefinitionNamespaceRepo extends GadgetRepo {
* @return string
*/
private function getGadgetIdsKey() {
return $this->wanCache->makeKey( 'gadgets', 'namespace', 'ids' );
return $this->wanCache->makeKey( 'gadgets-jsonrepo-ids' );
}
/**
@ -170,7 +217,6 @@ class GadgetDefinitionNamespaceRepo extends GadgetRepo {
* @return string
*/
private function getGadgetCacheKey( $id ) {
return $this->wanCache->makeKey(
'gadgets', 'object', md5( $id ), Gadget::GADGET_CLASS_VERSION );
return $this->wanCache->makeKey( 'gadgets-object', $id, Gadget::GADGET_CLASS_VERSION );
}
}

View file

@ -1,8 +1,8 @@
<?php
use MediaWiki\Extension\Gadgets\GadgetDefinitionNamespaceRepo;
use MediaWiki\Extension\Gadgets\GadgetRepo;
use MediaWiki\Extension\Gadgets\MediaWikiGadgetsDefinitionRepo;
use MediaWiki\Extension\Gadgets\MediaWikiGadgetsJsonRepo;
use MediaWiki\MediaWikiServices;
return [
@ -13,7 +13,7 @@ return [
case 'definition':
return new MediaWikiGadgetsDefinitionRepo( $wanCache, $revisionLookup );
case 'json':
return new GadgetDefinitionNamespaceRepo( $wanCache, $revisionLookup );
return new MediaWikiGadgetsJsonRepo( $wanCache, $revisionLookup );
default:
throw new InvalidArgumentException( 'Unexpected value for $wgGadgetsRepo' );
}

View file

@ -102,7 +102,8 @@ class SpecialGadgetUsage extends QueryPage {
'fields' => [
'title' => 'up_property',
'value' => 'COUNT(*)',
'namespace' => NS_GADGET
// Required field, but unused
'namespace' => NS_MEDIAWIKI
],
'conds' => $conds,
'options' => [

View file

@ -99,7 +99,7 @@ class SpecialGadgets extends SpecialPage {
$listOpen = false;
$editDefinitionMessage = $this->getUser()->isAllowed( 'gadgets-definition-edit' )
$editDefinitionMessage = $this->getUser()->isAllowed( 'editsitejs' )
? 'edit'
: 'viewsource';
$editInterfaceMessage = $this->getUser()->isAllowed( 'editinterface' )

View file

@ -15,7 +15,7 @@ use MediaWikiIntegrationTestCase;
class GadgetDefinitionContentHandlerTest extends MediaWikiIntegrationTestCase {
public function testHandler() {
$status = $this->editPage( 'Gadget definition:X1', '{}' );
$status = $this->editPage( 'MediaWiki:Gadgets/X1.json', '{}' );
/** @var RevisionRecord $rev */
$rev = $status->getValue()['revision-record'];
$revText = $rev->getContent( SlotRecord::MAIN )->serialize();

View file

@ -1,40 +0,0 @@
<?php
use MediaWiki\Extension\Gadgets\GadgetDefinitionNamespaceRepo;
/**
* @group Gadgets
* @group Database
*/
class GadgetDefinitionNamespaceRepoTest extends MediaWikiIntegrationTestCase {
/**
* @covers \MediaWiki\Extension\Gadgets\GadgetDefinitionNamespaceRepo
*/
public function testGetGadget() {
$this->editPage( 'Gadget definition:Test',
'{"module":{"pages":["test.js"]}, "settings":{"default":true}}' );
$services = $this->getServiceContainer();
$repo = new GadgetDefinitionNamespaceRepo( $services->getMainWANObjectCache(), $services->getRevisionLookup() );
$gadget = $repo->getGadget( 'Test' );
$this->assertTrue( $gadget->isOnByDefault() );
$this->assertArrayEquals( [ "Gadget:test.js" ], $gadget->getScripts() );
}
/**
* @covers \MediaWiki\Extension\Gadgets\GadgetDefinitionNamespaceRepo
*/
public function testGetGadgetIds() {
$this->editPage( 'Gadget definition:X1',
'{"module":{"pages":["Gadget:test.js"]}, "settings":{"default":true}}' );
$this->editPage( 'Gadget definition:X2',
'{"module":{"pages":["Gadget:test.js"]}, "settings":{"default":true}}' );
$services = $this->getServiceContainer();
$wanCache = $services->getMainWANObjectCache();
$repo = new GadgetDefinitionNamespaceRepo( $wanCache, $services->getRevisionLookup() );
$wanCache->clearProcessCache();
$this->assertArrayEquals( [ 'X1', 'X2' ], $repo->getGadgetIds() );
}
}

View file

@ -0,0 +1,35 @@
<?php
use MediaWiki\Extension\Gadgets\MediaWikiGadgetsJsonRepo;
/**
* @covers \MediaWiki\Extension\Gadgets\MediaWikiGadgetsJsonRepo
* @group Gadgets
* @group Database
*/
class MediaWikiGadgetsJsonRepoTest extends MediaWikiIntegrationTestCase {
public function testGetGadget() {
$this->editPage( 'MediaWiki:Gadgets/test.json',
'{"module":{"pages":["test.js"]}, "settings":{"default":true}}' );
$services = $this->getServiceContainer();
$repo = new MediaWikiGadgetsJsonRepo( $services->getMainWANObjectCache(), $services->getRevisionLookup() );
$gadget = $repo->getGadget( 'test' );
$this->assertTrue( $gadget->isOnByDefault() );
$this->assertArrayEquals( [ "MediaWiki:Gadget-test.js" ], $gadget->getScripts() );
}
public function testGetGadgetIds() {
$this->editPage( 'MediaWiki:Gadgets/X1.json',
'{"module":{"pages":["MediaWiki:Gadget-test.js"]}, "settings":{"default":true}}' );
$this->editPage( 'MediaWiki:Gadgets/X2.json',
'{"module":{"pages":["MediaWiki:Gadget-test.js"]}, "settings":{"default":true}}' );
$services = $this->getServiceContainer();
$wanCache = $services->getMainWANObjectCache();
$repo = new MediaWikiGadgetsJsonRepo( $wanCache, $services->getRevisionLookup() );
$wanCache->clearProcessCache();
$this->assertArrayEquals( [ 'X1', 'X2' ], $repo->getGadgetIds() );
}
}