* Ed Sanders
* Edward Chernenko
* Edward Z. Yang
+* Eddie Greiner-Petter
* Elisabeth Bauer
* Elliott Eggleston
* Elvis Stansvik
* Updated cssjanus from v1.1.2 to 1.1.3.
* Updated psr/log from v1.0.0 to v1.0.2.
* Update Moment.js from v2.8.4 to v2.15.0.
+* Updated oyejorge/less.php from v1.7.0.10 to v1.7.0.13.
==== New external libraries ====
'PackedImageGallery' => __DIR__ . '/includes/gallery/PackedImageGallery.php',
'PackedOverlayImageGallery' => __DIR__ . '/includes/gallery/PackedOverlayImageGallery.php',
'Page' => __DIR__ . '/includes/page/Page.php',
- 'PageArchive' => __DIR__ . '/includes/page/PageAcrhive.php',
+ 'PageArchive' => __DIR__ . '/includes/page/PageArchive.php',
'PageExists' => __DIR__ . '/maintenance/pageExists.php',
'PageLangLogFormatter' => __DIR__ . '/includes/logging/PageLangLogFormatter.php',
'PageProps' => __DIR__ . '/includes/PageProps.php',
"liuggio/statsd-php-client": "1.0.18",
"mediawiki/at-ease": "1.1.0",
"oojs/oojs-ui": "0.19.4",
- "oyejorge/less.php": "1.7.0.10",
+ "oyejorge/less.php": "1.7.0.13",
"php": ">=5.5.9",
"psr/log": "1.0.2",
"wikimedia/assert": "0.2.2",
"description": {
"type": ["string", "array"],
"description": "A description of the config setting, mostly for documentation/developers"
+ },
+ "decriptionmsg": {
+ "type": "string",
+ "description": "The message key which should be used as a description for this configuration option in a user interface. If empty, description will be used."
+ },
+ "public": {
+ "type": "boolean",
+ "default": false,
+ "description": "Whether this configuration option and its value is allowed to be revealed in public or not."
}
}
}
* protection, or false if there's none.
*/
public function getTitleProtection() {
+ $protection = $this->getTitleProtectionInternal();
+ if ( $protection ) {
+ if ( $protection['permission'] == 'sysop' ) {
+ $protection['permission'] = 'editprotected'; // B/C
+ }
+ if ( $protection['permission'] == 'autoconfirmed' ) {
+ $protection['permission'] = 'editsemiprotected'; // B/C
+ }
+ }
+ return $protection;
+ }
+
+ /**
+ * Fetch title protection settings
+ *
+ * To work correctly, $this->loadRestrictions() needs to have access to the
+ * actual protections in the database without munging 'sysop' =>
+ * 'editprotected' and 'autoconfirmed' => 'editsemiprotected'. Other
+ * callers probably want $this->getTitleProtection() instead.
+ *
+ * @return array|bool
+ */
+ protected function getTitleProtectionInternal() {
// Can't protect pages in special namespaces
if ( $this->getNamespace() < 0 ) {
return false;
// fetchRow returns false if there are no rows.
$row = $dbr->fetchRow( $res );
if ( $row ) {
- if ( $row['permission'] == 'sysop' ) {
- $row['permission'] = 'editprotected'; // B/C
- }
- if ( $row['permission'] == 'autoconfirmed' ) {
- $row['permission'] = 'editsemiprotected'; // B/C
- }
$row['expiry'] = $dbr->decodeExpiry( $row['expiry'] );
}
$this->mTitleProtection = $row;
$this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
} else {
- $title_protection = $this->getTitleProtection();
+ $title_protection = $this->getTitleProtectionInternal();
if ( $title_protection ) {
$now = wfTimestampNow();
"apihelp-query+langlinks-param-inlanguagecode": "Code de langue pour les noms de langue localisés.",
"apihelp-query+langlinks-example-simple": "Obtenir les liens interlangue de la page <kbd>Main Page</kbd>.",
"apihelp-query+links-description": "Renvoie tous les liens des pages fournies.",
- "apihelp-query+links-param-namespace": "Afficher les liens uniquement dans ces espaces de nom.",
+ "apihelp-query+links-param-namespace": "Afficher les liens uniquement dans ces espaces de noms.",
"apihelp-query+links-param-limit": "Combien de liens renvoyer.",
"apihelp-query+links-param-titles": "Lister uniquement les liens vers ces titres. Utile pour vérifier si une certaine page a un lien vers un titre donné.",
"apihelp-query+links-param-dir": "La direction dans laquelle lister.",
"apihelp-query+links-example-simple": "Obtenir les liens de la page <kbd>Main Page</kbd>",
"apihelp-query+links-example-generator": "Obtenir des informations sur tous les liens de page dans <kbd>Main Page</kbd>.",
- "apihelp-query+links-example-namespaces": "Obtenir les liens de la page <kbd>Accueil</kbd> dans les espaces de nom {{ns:user}} et {{ns:template}}.",
+ "apihelp-query+links-example-namespaces": "Obtenir les liens de la page <kbd>Main Page</kbd> dans les espaces de nom {{ns:user}} et {{ns:template}}.",
"apihelp-query+linkshere-description": "Trouver toutes les pages ayant un lien vers les pages données.",
"apihelp-query+linkshere-param-prop": "Quelles propriétés obtenir :",
"apihelp-query+linkshere-paramvalue-prop-pageid": "ID de chaque page.",
"apihelp-query+linkshere-paramvalue-prop-title": "Titre de chaque page.",
"apihelp-query+linkshere-paramvalue-prop-redirect": "Indique si la page est une redirection.",
- "apihelp-query+linkshere-param-namespace": "Inclure uniquement les pages dans ces espaces de nom.",
+ "apihelp-query+linkshere-param-namespace": "Inclure uniquement les pages dans ces espaces de noms.",
"apihelp-query+linkshere-param-limit": "Combien de résultats renvoyer.",
"apihelp-query+linkshere-param-show": "Afficher uniquement les éléments qui correspondent à ces critères :\n;redirect:Afficher uniquement les redirections.\n;!redirect:Afficher uniquement les non-redirections.",
"apihelp-query+linkshere-example-simple": "Obtenir une liste des pages liées à [[Main Page]]",
"apihelp-query+logevents-description": "Obtenir des événements des journaux.",
"apihelp-query+logevents-param-prop": "Quelles propriétés obtenir :",
"apihelp-query+logevents-paramvalue-prop-ids": "Ajoute l’ID de l’événement.",
- "apihelp-query+logevents-paramvalue-prop-title": "Ajoute le titre de la page pour l’événement.",
- "apihelp-query+logevents-paramvalue-prop-type": "Ajoute le type de l’événement.",
+ "apihelp-query+logevents-paramvalue-prop-title": "Ajoute le titre de la page pour l’événement enregistré.",
+ "apihelp-query+logevents-paramvalue-prop-type": "Ajoute le type de l’événement enregistré.",
"apihelp-query+logevents-paramvalue-prop-user": "Ajoute l’utilisateur responsable de l’événement.",
"apihelp-query+logevents-paramvalue-prop-userid": "Ajoute l’ID de l’utilisateur responsable de l’événement.",
"apihelp-query+logevents-paramvalue-prop-timestamp": "Ajoute l’horodatage de l’événement.",
"apihelp-query+logevents-paramvalue-prop-parsedcomment": "Ajoute le commentaire analysé de l’événement.",
"apihelp-query+logevents-paramvalue-prop-details": "Liste les détails supplémentaires sur l’événement.",
"apihelp-query+logevents-paramvalue-prop-tags": "Liste les balises de l’événement.",
- "apihelp-query+logevents-param-type": "Filtrer les entrées du journal à ce seul type.",
- "apihelp-query+logevents-param-action": "Filtrer les actions du journal à cette seule action. Écrase <var>$1type</var>. La présence d'une valeur avec un astérisque dans la liste, comme <var>$1type</var>, indique qu'une chaîne arbitraire peut être passée dans dans la requête à la place de l'astérisque.",
+ "apihelp-query+logevents-param-type": "Filtrer les entrées du journal sur ce seul type.",
+ "apihelp-query+logevents-param-action": "Filtrer les actions du journal sur cette seule action. Écrase <var>$1type</var>. Dans le liste des valeurs possibles, les valeurs suivies d'un astérisque, comme <kbd>action/*</kbd>, peuvent avoir différentes chaînes à la place du slash.",
"apihelp-query+logevents-param-start": "L’horodatage auquel démarrer l’énumération.",
"apihelp-query+logevents-param-end": "L’horodatage auquel arrêter l’énumération.",
"apihelp-query+logevents-param-user": "Restreindre aux entrées générées par l’utilisateur spécifié.",
"apihelp-query+logevents-param-title": "Restreindre aux entrées associées à une page donnée.",
- "apihelp-query+logevents-param-namespace": "Restreindre aux entrées dans l’espace de nom spécifié.",
+ "apihelp-query+logevents-param-namespace": "Restreindre aux entrées dans l’espace de noms spécifié.",
"apihelp-query+logevents-param-prefix": "Restreindre aux entrées commençant par ce préfixe.",
"apihelp-query+logevents-param-tag": "Lister seulement les entrées ayant cette balise.",
"apihelp-query+logevents-param-limit": "Combien d'entrées renvoyer au total.",
"apierror-invalidexpiry": "Hora de caducidade incorrecta \"$1\".",
"apierror-invalid-file-key": "Non se corresponde cunha clave válida de ficheiro.",
"apierror-invalidlang": "Código de lingua incorrecto para o parámetro <var>$1</var>.",
- "apierror-invalidoldimage": "O parámetro oldimage ten un formato incorrecto.",
+ "apierror-invalidoldimage": "O parámetro <var>oldimage</var> ten un formato incorrecto.",
"apierror-invalidparammix-cannotusewith": "O parámetro <kbd>$1</kbd> non pode usarse xunto con <kbd>$2</kbd>.",
"apierror-invalidparammix-mustusewith": "O parámetro <kbd>$1</kbd> só pode usarse xunto con <kbd>$2</kbd>.",
"apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> non se pode combinar cos parámetros <var>oldid</var>, <var>pageid</var> e <var>page</var>. Por favor, utilice <var>title</var> e <var>text</var>.",
"apierror-invalidparammix": "{{PLURAL:$2|Os parámetros}} $1 non poden usarse xuntos.",
- "apierror-invalidsection": "O parámetro sección debe ser un ID de sección válido ou <kbd>new</kbd>.",
+ "apierror-invalidsection": "O parámetro <var>section</var> debe ser un ID de sección válido ou <kbd>new</kbd>.",
"apierror-invalidsha1base36hash": "O código hash SHA1Base36 proporcionado non é correcto.",
"apierror-invalidsha1hash": "O código hash SHA1 proporcionado non é correcto.",
"apierror-invalidtitle": "Título incorrecto \"$1\".",
"apiwarn-notfile": "\"$1\" non é un ficheiro.",
"apiwarn-parse-nocontentmodel": "Non se proporcionou <var>title</var> nin <var>contentmodel</var>, asúmese $1.",
"apiwarn-tokennotallowed": "A acción \"$1\" non está permitida para o usuario actual.",
- "apiwarn-toomanyvalues": "Demasiados valores para o parámetro <var>$1</var>: o límite é $2.",
+ "apiwarn-toomanyvalues": "Demasiados valores para o parámetro <var>$1</var>. O límite é $2.",
"apiwarn-truncatedresult": "Truncouse este resultado porque doutra maneira sobrepasaría o límite de $1 bytes.",
"apiwarn-validationfailed-badpref": "non é unha preferencia válida.",
"apiwarn-validationfailed-cannotset": "non pode ser establecido por este módulo.",
"apihelp-createaccount-param-language": "사용자에게 기본으로 설정할 언어 코드. (선택 사항, 기본값으로는 본문의 언어입니다)",
"apihelp-createaccount-example-pass": "사용자 <kbd>testuser</kbd>를 만들고 비밀번호를 <kbd>test123</kbd>으로 설정합니다.",
"apihelp-createaccount-example-mail": "사용자 <kbd>testmailuser</kbd>를 만들고 자동 생성된 비밀번호를 이메일로 보냅니다.",
+ "apihelp-cspreport-description": "브라우저가 콘텐츠 보안 정책의 위반을 보고하기 위해 사용합니다. 이 모듈은 SCP를 준수하는 웹 브라우저에 의해 자동으로 사용될 때를 제외하고는 사용해서는 안 됩니다.",
+ "apihelp-cspreport-param-reportonly": "강제적 정책이 아닌, 모니터링 정책에서 나온 보고서인 것으로 표시합니다",
"apihelp-delete-description": "문서 삭제",
"apihelp-delete-param-title": "삭제할 문서의 제목. <var>$1pageid</var>과 함께 사용할 수 없습니다.",
"apihelp-delete-param-pageid": "삭제할 문서의 ID. <var>$1title</var>과 함께 사용할 수 없습니다.",
"apihelp-feedwatchlist-param-feedformat": "피드 포맷.",
"apihelp-feedwatchlist-example-default": "주시문서 목록 피드를 보여줍니다.",
"apihelp-filerevert-description": "파일을 이전 판으로 되돌립니다.",
+ "apihelp-filerevert-param-filename": "파일: 접두어가 없는 대상 파일 이름.",
"apihelp-filerevert-param-comment": "업로드 댓글입니다.",
"apihelp-filerevert-example-revert": "<kbd>Wiki.png</kbd>를 <kbd>2011-03-05T15:27:40Z</kbd> 판으로 되돌립니다.",
"apihelp-help-description": "지정된 모듈의 도움말을 표시합니다.",
+ "apihelp-help-param-modules": "(<var>action</var>, <var>format</var> 변수의 값 또는 <kbd>main</kbd>)에 대한 도움말을 표시하는 모듈입니다. <kbd>+</kbd>로 하위 모듈을 지정할 수 있습니다.",
+ "apihelp-help-param-submodules": "명명된 모듈의 하위 모듈의 도움말을 포함합니다.",
+ "apihelp-help-param-recursivesubmodules": "하위 모듈의 도움말을 반복하여 포함합니다.",
"apihelp-help-param-helpformat": "도움말 출력 포맷.",
+ "apihelp-help-param-wrap": "표준 API 응답 구조로 출력을 감쌉니다.",
+ "apihelp-help-param-toc": "HTML 출력에 목차를 포함합니다.",
"apihelp-help-example-main": "메인 모듈의 도움말입니다.",
"apihelp-help-example-recursive": "모든 도움말을 한 페이지로 모읍니다.",
+ "apihelp-help-example-help": "도움말 모듈 자체에 대한 도움말입니다.",
+ "apihelp-help-example-query": "2개의 쿼리 하위 모듈의 도움말입니다.",
"apihelp-imagerotate-description": "하나 이상의 그림을 회전합니다.",
"apihelp-imagerotate-param-rotation": "시계 방향으로 회전할 그림의 각도.",
"apihelp-import-param-xml": "업로드한 XML 파일.",
"apihelp-parse-example-page": "페이지의 구문을 분석합니다.",
"apihelp-parse-example-text": "위키텍스트의 구문을 분석합니다.",
"apihelp-parse-example-summary": "요약을 구문 분석합니다.",
+ "apihelp-patrol-param-rcid": "점검할 최근 바뀜 ID입니다.",
+ "apihelp-patrol-param-revid": "점검할 판 ID입니다.",
+ "apihelp-patrol-example-rcid": "최근의 변경사항을 점검합니다.",
"apihelp-patrol-example-revid": "판을 점검합니다.",
"apihelp-protect-description": "문서의 보호 수준을 변경합니다.",
"apihelp-protect-param-reason": "보호 또는 보호 해제의 이유.",
"apihelp-query+watchlist-paramvalue-prop-flags": "편집에 대한 플래그를 추가합니다.",
"apihelp-query+watchlist-paramvalue-prop-loginfo": "적절한 곳에 로그 정보를 추가합니다.",
"apihelp-removeauthenticationdata-description": "현재 사용자의 인증 데이터를 제거합니다.",
+ "apihelp-resetpassword-description": "비밀번호 재설정 이메일을 사용자에게 보냅니다.",
+ "apihelp-resetpassword-param-user": "재설정할 사용자입니다.",
+ "apihelp-resetpassword-param-email": "재설정할 사용자의 이메일 주소입니다.",
+ "apihelp-resetpassword-example-user": "사용자 <kbd>Example</kbd>에게 비밀번호 재설정 이메일을 보냅니다.",
+ "apihelp-resetpassword-example-email": "<kbd>user@example.com</kbd> 이메일 주소를 가진 모든 사용자에 대해 비밀번호 재설정 이메일을 보냅니다.",
"apihelp-revisiondelete-description": "판을 삭제하거나 되살립니다.",
"apihelp-revisiondelete-param-reason": "삭제 또는 복구 이유.",
"apihelp-rollback-param-tags": "되돌리기를 적용하기 위해 태그합니다.",
"apihelp-unblock-param-userid": "차단을 해제할 사용자 ID입니다. <var>$1id</var> 또는 <var>$1user</var>와(과) 함께 사용할 수 없습니다.",
"apihelp-unblock-param-reason": "차단 해제 이유.",
"apihelp-unblock-param-tags": "차단 기록의 항목에 적용할 태그를 변경합니다.",
+ "apihelp-upload-param-filename": "대상 파일 이름.",
"apihelp-upload-param-ignorewarnings": "모든 경고를 무시합니다.",
"apihelp-userrights-param-user": "사용자 이름.",
"apihelp-userrights-param-userid": "사용자 ID.",
"apihelp-edit-param-tags": "Ознаки за измена што се однесуваат на преработката.",
"apihelp-edit-param-minor": "Ситно уредување.",
"apihelp-edit-param-notminor": "Неситно уредување.",
- "apihelp-edit-param-bot": "Означи го уредувањево како ботско.",
+ "apihelp-edit-param-bot": "Означи го уредувањево како ботовско.",
"apihelp-edit-param-basetimestamp": "Датум и време на преработката на базата, кои се користат за утврдување на спротиставености во уредувањето. Може да се добие преку [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].",
"apihelp-edit-param-starttimestamp": "Датум и време кога сте почнало уредувањето, кои се користат за утврдување на спротиставености во уредувањата. Соодветната вредност се добива користејќи <var>[[Special:ApiHelp/main|curtimestamp]]</var> кога ќе почнете со уредување (на пр. кога ќе се вчита содржината што ќе ја уредувате).",
"apihelp-edit-param-recreate": "Занемари ги грешките што се појавуваат во врска со страницата што е избришана во меѓувреме.",
"apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Видає інформацію щодо прав (ліцензії) вікі, якщо наявна.",
"apihelp-query+siteinfo-paramvalue-prop-restrictions": "Видає інформацію про наявні типи обмежень (захисту).",
"apihelp-query+siteinfo-paramvalue-prop-languages": "Видає список мов, які підтримує MediaWiki (за бажанням локалізовані через <var>$1inlanguagecode</var>).",
+ "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Виводить список кодів мов, для яких увімкнено [[mw:LanguageConverter|LanguageConverter]], а також варіанти, підтримувані кожною з цих мов.",
"apihelp-query+siteinfo-paramvalue-prop-skins": "Видає список усіх доступних тем оформлення (опціонально локалізовані з використанням <var>$1inlanguagecode</var>, в іншому разі — мовою вмісту).",
"apihelp-query+siteinfo-paramvalue-prop-extensiontags": "Видає список теґів розширення парсеру.",
"apihelp-query+siteinfo-paramvalue-prop-functionhooks": "Видає список гуків парсерних функцій.",
"apierror-invalidexpiry": "Недійсний час завершення «$1».",
"apierror-invalid-file-key": "Недійсний ключ файлу.",
"apierror-invalidlang": "Недійсний код мови для параметра <var>$1</var>.",
- "apierror-invalidoldimage": "Параметр «oldimage» має недійсний формат.",
+ "apierror-invalidoldimage": "Параметр <var>oldimage</var> має недійсний формат.",
"apierror-invalidparammix-cannotusewith": "Параметр <kbd>$1</kbd> не можна використовувати з <kbd>$2</kbd>.",
"apierror-invalidparammix-mustusewith": "Параметр <kbd>$1</kbd> можна використовувати тільки з <kbd>$2</kbd>.",
"apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> не можна поєднувати з параметрами <var>oldid</var>, <var>pageid</var> чи <var>page</var>. Будь ласка, використовуйте <var>title</var> і <var>text</var>.",
"apierror-invalidparammix": "{{PLURAL:$2|Ці параметри}} $1 не можна використовувати водночас.",
- "apierror-invalidsection": "Параметр «section» має бути дійсним ідентифікатором розділу або <kbd>new</kbd>.",
+ "apierror-invalidsection": "Параметр <var>section</var> має бути дійсним ідентифікатором розділу або <kbd>new</kbd>.",
"apierror-invalidsha1base36hash": "Поданий хеш SHA1Base36 недійсний.",
"apierror-invalidsha1hash": "Поданий хеш SHA1 недійсний.",
"apierror-invalidtitle": "Погана назва «$1».",
"apiwarn-redirectsandrevids": "Вирішення перенаправлень не може використовуватись разом з параметром <var>revids</var>. Усі перенаправлення, на які вказує <var>revids</var>, не було вирішено.",
"apiwarn-tokennotallowed": "Дія «$1» недозволена для поточного користувача.",
"apiwarn-tokens-origin": "Токени не можна отримати, поки не застосована політика одного походження.",
- "apiwarn-toomanyvalues": "Надто багато значень задано для параметра <var>$1</var>: ліміт становить $2.",
+ "apiwarn-toomanyvalues": "Надто багато значень задано для параметра <var>$1</var>. Ліміт становить $2.",
"apiwarn-truncatedresult": "Цей результат було скорочено, оскільки інакше він перевищив би ліміт у $1 байтів.",
"apiwarn-unclearnowtimestamp": "Вказування «$2» для параметра мітки часу <var>$1</var> є застарілим. Якщо з якоїсь причини Вам треба чітко вказати поточний час без вираховування його з боку клієнта, використайте <kbd>now</kbd>.",
"apiwarn-unrecognizedvalues": "{{PLURAL:$3|Нерозпізнане|Нерозпізнані}} значення для параметра <var>$1</var>: $2.",
: IP::sanitizeRange( "$ip/16" );
# Bail out if a request already came from this range...
- $key = wfMemcKey( get_class( $this ), 'attempt', $this->mType, $this->mKey, $ip );
+ $key = wfMemcKey( static::class, 'attempt', $this->mType, $this->mKey, $ip );
if ( $cache->get( $key ) ) {
return; // possibly the same user
}
* @return string
*/
protected function cacheMissKey() {
- return wfMemcKey( get_class( $this ), 'misses', $this->mType, $this->mKey );
+ return wfMemcKey( static::class, 'misses', $this->mType, $this->mKey );
}
}
foreach ( $modules as $name => $module ) {
$key = $cacheKeys[$name];
if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) {
- $this->logger->info( 'Message blob cache-miss for {module}',
- [ 'module' => $name, 'cacheKey' => $key ]
- );
$blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang );
} else {
// Use unexpired cache
}
}
- wfDebugLog( 'caches', get_class( $this ) . ": using store $storeClass" );
+ wfDebugLog( 'caches', static::class . ": using store $storeClass" );
if ( !empty( $conf['storeDirectory'] ) ) {
$storeConf['directory'] = $conf['storeDirectory'];
}
*/
protected $db;
+ /**
+ * @var Maintenance
+ */
+ protected $maintenance;
+
protected $shared = false;
/**
// Do a consistency check to see if the backends are consistent...
$syncStatus = $this->consistencyCheck( $relevantPaths );
if ( !$syncStatus->isOK() ) {
- wfDebugLog( 'FileOperation', get_class( $this ) .
+ wfDebugLog( 'FileOperation', static::class .
" failed sync check: " . FormatJson::encode( $relevantPaths ) );
// Try to resync the clone backends to the master on the spot...
if ( $this->autoResync === false
}
if ( !$status->isOK() ) {
- wfDebugLog( 'FileOperation', get_class( $this ) .
+ wfDebugLog( 'FileOperation', static::class .
" failed to resync: " . FormatJson::encode( $paths ) );
}
$status->merge( $this->doConcatenate( $params ) );
$sec = microtime( true ) - $start_time;
if ( !$status->isOK() ) {
- $this->logger->error( get_class( $this ) . "-{$this->name}" .
+ $this->logger->error( static::class . "-{$this->name}" .
" failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" );
}
}
$subStatus->success[$i] = false;
++$subStatus->failCount;
}
- $this->logger->error( get_class( $this ) . "-{$this->name} " .
+ $this->logger->error( static::class . "-{$this->name} " .
" stat failure; aborted operations: " . FormatJson::encode( $ops ) );
}
$params = $this->params;
$params['failedAction'] = $action;
try {
- $this->logger->error( get_class( $this ) .
+ $this->logger->error( static::class .
" failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
} catch ( Exception $e ) {
// bad config? debug log error?
protected function debug( $text ) {
if ( $this->debugMode ) {
$this->logger->debug( "{class} debug: $text", [
- 'class' => get_class( $this ),
+ 'class' => static::class,
] );
}
}
*
* The simplest purge method is delete().
*
- * There are two supported ways to handle broadcasted operations:
+ * There are three supported ways to handle broadcasted operations:
* - a) Configure the 'purge' EventRelayer to point to a valid PubSub endpoint
- * that has subscribed listeners on the cache servers applying the cache updates.
+ * that has subscribed listeners on the cache servers applying the cache updates.
* - b) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
- * and set up mcrouter as the underlying cache backend, using one of the memcached
- * BagOStuff classes as 'cache'. Use OperationSelectorRoute in the mcrouter settings
- * to configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and
- * configure other operations to go to the local DC via PoolRoute (for reference,
- * see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles).
+ * and set up mcrouter as the underlying cache backend, using one of the memcached
+ * BagOStuff classes as 'cache'. Use OperationSelectorRoute in the mcrouter settings
+ * to configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and
+ * configure other operations to go to the local DC via PoolRoute (for reference,
+ * see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles).
+ * - c) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
+ * and set up dynomite as cache middleware between the web servers and either
+ * memcached or redis. This will also broadcast all key setting operations, not just purges,
+ * which can be useful for cache warming. Writes are eventually consistent via the
+ * Dynamo replication model (see https://github.com/Netflix/dynomite).
*
* Broadcasted operations like delete() and touchCheckKey() are done asynchronously
* in all datacenters this way, though the local one should likely be near immediate.
*/
public function __clone() {
$this->connLogger->warning(
- "Cloning " . get_class( $this ) . " is not recomended; forking connection:\n" .
+ "Cloning " . static::class . " is not recomended; forking connection:\n" .
( new RuntimeException() )->getTraceAsString()
);
*/
private function getDB() {
if ( !$this->db ) {
- throw new RuntimeException( get_class( $this ) . ' needs a DB handle for iteration.' );
+ throw new RuntimeException( static::class . ' needs a DB handle for iteration.' );
}
return $this->db;
* @return string The name of the service behind this VRS object.
*/
public function getName() {
- return isset( $this->params['name'] ) ? $this->params['name'] :
- get_class( $this );
+ return isset( $this->params['name'] ) ? $this->params['name'] : static::class;
}
/**
+++ /dev/null
-<?php
-/**
- * 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
- */
-
-use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
-
-/**
- * Used to show archived pages and eventually restore them.
- */
-class PageArchive {
- /** @var Title */
- protected $title;
-
- /** @var Status */
- protected $fileStatus;
-
- /** @var Status */
- protected $revisionStatus;
-
- /** @var Config */
- protected $config;
-
- public function __construct( $title, Config $config = null ) {
- if ( is_null( $title ) ) {
- throw new MWException( __METHOD__ . ' given a null title.' );
- }
- $this->title = $title;
- if ( $config === null ) {
- wfDebug( __METHOD__ . ' did not have a Config object passed to it' );
- $config = MediaWikiServices::getInstance()->getMainConfig();
- }
- $this->config = $config;
- }
-
- public function doesWrites() {
- return true;
- }
-
- /**
- * List all deleted pages recorded in the archive table. Returns result
- * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
- * namespace/title.
- *
- * @return ResultWrapper
- */
- public static function listAllPages() {
- $dbr = wfGetDB( DB_REPLICA );
-
- return self::listPages( $dbr, '' );
- }
-
- /**
- * List deleted pages recorded in the archive table matching the
- * given title prefix.
- * Returns result wrapper with (ar_namespace, ar_title, count) fields.
- *
- * @param string $prefix Title prefix
- * @return ResultWrapper
- */
- public static function listPagesByPrefix( $prefix ) {
- $dbr = wfGetDB( DB_REPLICA );
-
- $title = Title::newFromText( $prefix );
- if ( $title ) {
- $ns = $title->getNamespace();
- $prefix = $title->getDBkey();
- } else {
- // Prolly won't work too good
- // @todo handle bare namespace names cleanly?
- $ns = 0;
- }
-
- $conds = [
- 'ar_namespace' => $ns,
- 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
- ];
-
- return self::listPages( $dbr, $conds );
- }
-
- /**
- * @param IDatabase $dbr
- * @param string|array $condition
- * @return bool|ResultWrapper
- */
- protected static function listPages( $dbr, $condition ) {
- return $dbr->select(
- [ 'archive' ],
- [
- 'ar_namespace',
- 'ar_title',
- 'count' => 'COUNT(*)'
- ],
- $condition,
- __METHOD__,
- [
- 'GROUP BY' => [ 'ar_namespace', 'ar_title' ],
- 'ORDER BY' => [ 'ar_namespace', 'ar_title' ],
- 'LIMIT' => 100,
- ]
- );
- }
-
- /**
- * List the revisions of the given page. Returns result wrapper with
- * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
- *
- * @return ResultWrapper
- */
- public function listRevisions() {
- $dbr = wfGetDB( DB_REPLICA );
-
- $tables = [ 'archive' ];
-
- $fields = [
- 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
- 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
- ];
-
- if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
- $fields[] = 'ar_content_format';
- $fields[] = 'ar_content_model';
- }
-
- $conds = [ 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey() ];
-
- $options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
-
- $join_conds = [];
-
- ChangeTags::modifyDisplayQuery(
- $tables,
- $fields,
- $conds,
- $join_conds,
- $options,
- ''
- );
-
- return $dbr->select( $tables,
- $fields,
- $conds,
- __METHOD__,
- $options,
- $join_conds
- );
- }
-
- /**
- * List the deleted file revisions for this page, if it's a file page.
- * Returns a result wrapper with various filearchive fields, or null
- * if not a file page.
- *
- * @return ResultWrapper
- * @todo Does this belong in Image for fuller encapsulation?
- */
- public function listFiles() {
- if ( $this->title->getNamespace() != NS_FILE ) {
- return null;
- }
-
- $dbr = wfGetDB( DB_REPLICA );
- return $dbr->select(
- 'filearchive',
- ArchivedFile::selectFields(),
- [ 'fa_name' => $this->title->getDBkey() ],
- __METHOD__,
- [ 'ORDER BY' => 'fa_timestamp DESC' ]
- );
- }
-
- /**
- * Return a Revision object containing data for the deleted revision.
- * Note that the result *may* or *may not* have a null page ID.
- *
- * @param string $timestamp
- * @return Revision|null
- */
- public function getRevision( $timestamp ) {
- $dbr = wfGetDB( DB_REPLICA );
-
- $fields = [
- 'ar_rev_id',
- 'ar_text',
- 'ar_comment',
- 'ar_user',
- 'ar_user_text',
- 'ar_timestamp',
- 'ar_minor_edit',
- 'ar_flags',
- 'ar_text_id',
- 'ar_deleted',
- 'ar_len',
- 'ar_sha1',
- ];
-
- if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
- $fields[] = 'ar_content_format';
- $fields[] = 'ar_content_model';
- }
-
- $row = $dbr->selectRow( 'archive',
- $fields,
- [ 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey(),
- 'ar_timestamp' => $dbr->timestamp( $timestamp ) ],
- __METHOD__ );
-
- if ( $row ) {
- return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] );
- }
-
- return null;
- }
-
- /**
- * Return the most-previous revision, either live or deleted, against
- * the deleted revision given by timestamp.
- *
- * May produce unexpected results in case of history merges or other
- * unusual time issues.
- *
- * @param string $timestamp
- * @return Revision|null Null when there is no previous revision
- */
- public function getPreviousRevision( $timestamp ) {
- $dbr = wfGetDB( DB_REPLICA );
-
- // Check the previous deleted revision...
- $row = $dbr->selectRow( 'archive',
- 'ar_timestamp',
- [ 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey(),
- 'ar_timestamp < ' .
- $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
- __METHOD__,
- [
- 'ORDER BY' => 'ar_timestamp DESC',
- 'LIMIT' => 1 ] );
- $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
-
- $row = $dbr->selectRow( [ 'page', 'revision' ],
- [ 'rev_id', 'rev_timestamp' ],
- [
- 'page_namespace' => $this->title->getNamespace(),
- 'page_title' => $this->title->getDBkey(),
- 'page_id = rev_page',
- 'rev_timestamp < ' .
- $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
- __METHOD__,
- [
- 'ORDER BY' => 'rev_timestamp DESC',
- 'LIMIT' => 1 ] );
- $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
- $prevLiveId = $row ? intval( $row->rev_id ) : null;
-
- if ( $prevLive && $prevLive > $prevDeleted ) {
- // Most prior revision was live
- return Revision::newFromId( $prevLiveId );
- } elseif ( $prevDeleted ) {
- // Most prior revision was deleted
- return $this->getRevision( $prevDeleted );
- }
-
- // No prior revision on this page.
- return null;
- }
-
- /**
- * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
- *
- * @param object $row Database row
- * @return string
- */
- public function getTextFromRow( $row ) {
- if ( is_null( $row->ar_text_id ) ) {
- // An old row from MediaWiki 1.4 or previous.
- // Text is embedded in this row in classic compression format.
- return Revision::getRevisionText( $row, 'ar_' );
- }
-
- // New-style: keyed to the text storage backend.
- $dbr = wfGetDB( DB_REPLICA );
- $text = $dbr->selectRow( 'text',
- [ 'old_text', 'old_flags' ],
- [ 'old_id' => $row->ar_text_id ],
- __METHOD__ );
-
- return Revision::getRevisionText( $text );
- }
-
- /**
- * Fetch (and decompress if necessary) the stored text of the most
- * recently edited deleted revision of the page.
- *
- * If there are no archived revisions for the page, returns NULL.
- *
- * @return string|null
- */
- public function getLastRevisionText() {
- $dbr = wfGetDB( DB_REPLICA );
- $row = $dbr->selectRow( 'archive',
- [ 'ar_text', 'ar_flags', 'ar_text_id' ],
- [ 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey() ],
- __METHOD__,
- [ 'ORDER BY' => 'ar_timestamp DESC' ] );
-
- if ( $row ) {
- return $this->getTextFromRow( $row );
- }
-
- return null;
- }
-
- /**
- * Quick check if any archived revisions are present for the page.
- *
- * @return bool
- */
- public function isDeleted() {
- $dbr = wfGetDB( DB_REPLICA );
- $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
- [ 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey() ],
- __METHOD__
- );
-
- return ( $n > 0 );
- }
-
- /**
- * Restore the given (or all) text and file revisions for the page.
- * Once restored, the items will be removed from the archive tables.
- * The deletion log will be updated with an undeletion notice.
- *
- * This also sets Status objects, $this->fileStatus and $this->revisionStatus
- * (depending what operations are attempted).
- *
- * @param array $timestamps Pass an empty array to restore all revisions,
- * otherwise list the ones to undelete.
- * @param string $comment
- * @param array $fileVersions
- * @param bool $unsuppress
- * @param User $user User performing the action, or null to use $wgUser
- * @param string|string[] $tags Change tags to add to log entry
- * ($user should be able to add the specified tags before this is called)
- * @return array|bool array(number of file revisions restored, number of image revisions
- * restored, log message) on success, false on failure.
- */
- public function undelete( $timestamps, $comment = '', $fileVersions = [],
- $unsuppress = false, User $user = null, $tags = null
- ) {
- // If both the set of text revisions and file revisions are empty,
- // restore everything. Otherwise, just restore the requested items.
- $restoreAll = empty( $timestamps ) && empty( $fileVersions );
-
- $restoreText = $restoreAll || !empty( $timestamps );
- $restoreFiles = $restoreAll || !empty( $fileVersions );
-
- if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
- $img = wfLocalFile( $this->title );
- $img->load( File::READ_LATEST );
- $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
- if ( !$this->fileStatus->isOK() ) {
- return false;
- }
- $filesRestored = $this->fileStatus->successCount;
- } else {
- $filesRestored = 0;
- }
-
- if ( $restoreText ) {
- $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
- if ( !$this->revisionStatus->isOK() ) {
- return false;
- }
-
- $textRestored = $this->revisionStatus->getValue();
- } else {
- $textRestored = 0;
- }
-
- // Touch the log!
-
- if ( $textRestored && $filesRestored ) {
- $reason = wfMessage( 'undeletedrevisions-files' )
- ->numParams( $textRestored, $filesRestored )->inContentLanguage()->text();
- } elseif ( $textRestored ) {
- $reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored )
- ->inContentLanguage()->text();
- } elseif ( $filesRestored ) {
- $reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored )
- ->inContentLanguage()->text();
- } else {
- wfDebug( "Undelete: nothing undeleted...\n" );
-
- return false;
- }
-
- if ( trim( $comment ) != '' ) {
- $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
- }
-
- if ( $user === null ) {
- global $wgUser;
- $user = $wgUser;
- }
-
- $logEntry = new ManualLogEntry( 'delete', 'restore' );
- $logEntry->setPerformer( $user );
- $logEntry->setTarget( $this->title );
- $logEntry->setComment( $reason );
- $logEntry->setTags( $tags );
-
- Hooks::run( 'ArticleUndeleteLogEntry', [ $this, &$logEntry, $user ] );
-
- $logid = $logEntry->insert();
- $logEntry->publish( $logid );
-
- return [ $textRestored, $filesRestored, $reason ];
- }
-
- /**
- * This is the meaty bit -- It restores archived revisions of the given page
- * to the revision table.
- *
- * @param array $timestamps Pass an empty array to restore all revisions,
- * otherwise list the ones to undelete.
- * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs
- * @param string $comment
- * @throws ReadOnlyError
- * @return Status Status object containing the number of revisions restored on success
- */
- private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
- if ( wfReadOnly() ) {
- throw new ReadOnlyError();
- }
-
- $dbw = wfGetDB( DB_MASTER );
- $dbw->startAtomic( __METHOD__ );
-
- $restoreAll = empty( $timestamps );
-
- # Does this page already exist? We'll have to update it...
- $article = WikiPage::factory( $this->title );
- # Load latest data for the current page (T33179)
- $article->loadPageData( 'fromdbmaster' );
- $oldcountable = $article->isCountable();
-
- $page = $dbw->selectRow( 'page',
- [ 'page_id', 'page_latest' ],
- [ 'page_namespace' => $this->title->getNamespace(),
- 'page_title' => $this->title->getDBkey() ],
- __METHOD__,
- [ 'FOR UPDATE' ] // lock page
- );
-
- if ( $page ) {
- $makepage = false;
- # Page already exists. Import the history, and if necessary
- # we'll update the latest revision field in the record.
-
- # Get the time span of this page
- $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
- [ 'rev_id' => $page->page_latest ],
- __METHOD__ );
-
- if ( $previousTimestamp === false ) {
- wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" );
-
- $status = Status::newGood( 0 );
- $status->warning( 'undeleterevision-missing' );
- $dbw->endAtomic( __METHOD__ );
-
- return $status;
- }
- } else {
- # Have to create a new article...
- $makepage = true;
- $previousTimestamp = 0;
- }
-
- $oldWhere = [
- 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey(),
- ];
- if ( !$restoreAll ) {
- $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
- }
-
- $fields = [
- 'ar_id',
- 'ar_rev_id',
- 'rev_id',
- 'ar_text',
- 'ar_comment',
- 'ar_user',
- 'ar_user_text',
- 'ar_timestamp',
- 'ar_minor_edit',
- 'ar_flags',
- 'ar_text_id',
- 'ar_deleted',
- 'ar_page_id',
- 'ar_len',
- 'ar_sha1'
- ];
-
- if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
- $fields[] = 'ar_content_format';
- $fields[] = 'ar_content_model';
- }
-
- /**
- * Select each archived revision...
- */
- $result = $dbw->select(
- [ 'archive', 'revision' ],
- $fields,
- $oldWhere,
- __METHOD__,
- /* options */
- [ 'ORDER BY' => 'ar_timestamp' ],
- [ 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ] ]
- );
-
- $rev_count = $result->numRows();
- if ( !$rev_count ) {
- wfDebug( __METHOD__ . ": no revisions to restore\n" );
-
- $status = Status::newGood( 0 );
- $status->warning( "undelete-no-results" );
- $dbw->endAtomic( __METHOD__ );
-
- return $status;
- }
-
- // We use ar_id because there can be duplicate ar_rev_id even for the same
- // page. In this case, we may be able to restore the first one.
- $restoreFailedArIds = [];
-
- // Map rev_id to the ar_id that is allowed to use it. When checking later,
- // if it doesn't match, the current ar_id can not be restored.
-
- // Value can be an ar_id or -1 (-1 means no ar_id can use it, since the
- // rev_id is taken before we even start the restore).
- $allowedRevIdToArIdMap = [];
-
- $latestRestorableRow = null;
-
- foreach ( $result as $row ) {
- if ( $row->ar_rev_id ) {
- // rev_id is taken even before we start restoring.
- if ( $row->ar_rev_id === $row->rev_id ) {
- $restoreFailedArIds[] = $row->ar_id;
- $allowedRevIdToArIdMap[$row->ar_rev_id] = -1;
- } else {
- // rev_id is not taken yet in the DB, but it might be taken
- // by a prior revision in the same restore operation. If
- // not, we need to reserve it.
- if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) {
- $restoreFailedArIds[] = $row->ar_id;
- } else {
- $allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id;
- $latestRestorableRow = $row;
- }
- }
- } else {
- // If ar_rev_id is null, there can't be a collision, and a
- // rev_id will be chosen automatically.
- $latestRestorableRow = $row;
- }
- }
-
- $result->seek( 0 ); // move back
-
- $oldPageId = 0;
- if ( $latestRestorableRow !== null ) {
- $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
-
- // grab the content to check consistency with global state before restoring the page.
- $revision = Revision::newFromArchiveRow( $latestRestorableRow,
- [
- 'title' => $article->getTitle(), // used to derive default content model
- ]
- );
- $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
- $content = $revision->getContent( Revision::RAW );
-
- // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
- $status = $content->prepareSave( $article, 0, -1, $user );
- if ( !$status->isOK() ) {
- $dbw->endAtomic( __METHOD__ );
-
- return $status;
- }
- }
-
- $newid = false; // newly created page ID
- $restored = 0; // number of revisions restored
- /** @var Revision $revision */
- $revision = null;
-
- // If there are no restorable revisions, we can skip most of the steps.
- if ( $latestRestorableRow === null ) {
- $failedRevisionCount = $rev_count;
- } else {
- if ( $makepage ) {
- // Check the state of the newest to-be version...
- if ( !$unsuppress
- && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
- ) {
- $dbw->endAtomic( __METHOD__ );
-
- return Status::newFatal( "undeleterevdel" );
- }
- // Safe to insert now...
- $newid = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id );
- if ( $newid === false ) {
- // The old ID is reserved; let's pick another
- $newid = $article->insertOn( $dbw );
- }
- $pageId = $newid;
- } else {
- // Check if a deleted revision will become the current revision...
- if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
- // Check the state of the newest to-be version...
- if ( !$unsuppress
- && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
- ) {
- $dbw->endAtomic( __METHOD__ );
-
- return Status::newFatal( "undeleterevdel" );
- }
- }
-
- $newid = false;
- $pageId = $article->getId();
- }
-
- foreach ( $result as $row ) {
- // Check for key dupes due to needed archive integrity.
- if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) {
- continue;
- }
- // Insert one revision at a time...maintaining deletion status
- // unless we are specifically removing all restrictions...
- $revision = Revision::newFromArchiveRow( $row,
- [
- 'page' => $pageId,
- 'title' => $this->title,
- 'deleted' => $unsuppress ? 0 : $row->ar_deleted
- ] );
-
- $revision->insertOn( $dbw );
- $restored++;
-
- Hooks::run( 'ArticleRevisionUndeleted',
- [ &$this->title, $revision, $row->ar_page_id ] );
- }
-
- // Now that it's safely stored, take it out of the archive
- // Don't delete rows that we failed to restore
- $toDeleteConds = $oldWhere;
- $failedRevisionCount = count( $restoreFailedArIds );
- if ( $failedRevisionCount > 0 ) {
- $toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )';
- }
-
- $dbw->delete( 'archive',
- $toDeleteConds,
- __METHOD__ );
- }
-
- $status = Status::newGood( $restored );
-
- if ( $failedRevisionCount > 0 ) {
- $status->warning(
- wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) );
- }
-
- // Was anything restored at all?
- if ( $restored ) {
- $created = (bool)$newid;
- // Attach the latest revision to the page...
- $wasnew = $article->updateIfNewerOn( $dbw, $revision );
- if ( $created || $wasnew ) {
- // Update site stats, link tables, etc
- $article->doEditUpdates(
- $revision,
- User::newFromName( $revision->getUserText( Revision::RAW ), false ),
- [
- 'created' => $created,
- 'oldcountable' => $oldcountable,
- 'restored' => true
- ]
- );
- }
-
- Hooks::run( 'ArticleUndelete', [ &$this->title, $created, $comment, $oldPageId ] );
- if ( $this->title->getNamespace() == NS_FILE ) {
- DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) );
- }
- }
-
- $dbw->endAtomic( __METHOD__ );
-
- return $status;
- }
-
- /**
- * @return Status
- */
- public function getFileStatus() {
- return $this->fileStatus;
- }
-
- /**
- * @return Status
- */
- public function getRevisionStatus() {
- return $this->revisionStatus;
- }
-}
--- /dev/null
+<?php
+/**
+ * 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
+ */
+
+use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\ResultWrapper;
+
+/**
+ * Used to show archived pages and eventually restore them.
+ */
+class PageArchive {
+ /** @var Title */
+ protected $title;
+
+ /** @var Status */
+ protected $fileStatus;
+
+ /** @var Status */
+ protected $revisionStatus;
+
+ /** @var Config */
+ protected $config;
+
+ public function __construct( $title, Config $config = null ) {
+ if ( is_null( $title ) ) {
+ throw new MWException( __METHOD__ . ' given a null title.' );
+ }
+ $this->title = $title;
+ if ( $config === null ) {
+ wfDebug( __METHOD__ . ' did not have a Config object passed to it' );
+ $config = MediaWikiServices::getInstance()->getMainConfig();
+ }
+ $this->config = $config;
+ }
+
+ public function doesWrites() {
+ return true;
+ }
+
+ /**
+ * List all deleted pages recorded in the archive table. Returns result
+ * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
+ * namespace/title.
+ *
+ * @return ResultWrapper
+ */
+ public static function listAllPages() {
+ $dbr = wfGetDB( DB_REPLICA );
+
+ return self::listPages( $dbr, '' );
+ }
+
+ /**
+ * List deleted pages recorded in the archive table matching the
+ * given title prefix.
+ * Returns result wrapper with (ar_namespace, ar_title, count) fields.
+ *
+ * @param string $prefix Title prefix
+ * @return ResultWrapper
+ */
+ public static function listPagesByPrefix( $prefix ) {
+ $dbr = wfGetDB( DB_REPLICA );
+
+ $title = Title::newFromText( $prefix );
+ if ( $title ) {
+ $ns = $title->getNamespace();
+ $prefix = $title->getDBkey();
+ } else {
+ // Prolly won't work too good
+ // @todo handle bare namespace names cleanly?
+ $ns = 0;
+ }
+
+ $conds = [
+ 'ar_namespace' => $ns,
+ 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
+ ];
+
+ return self::listPages( $dbr, $conds );
+ }
+
+ /**
+ * @param IDatabase $dbr
+ * @param string|array $condition
+ * @return bool|ResultWrapper
+ */
+ protected static function listPages( $dbr, $condition ) {
+ return $dbr->select(
+ [ 'archive' ],
+ [
+ 'ar_namespace',
+ 'ar_title',
+ 'count' => 'COUNT(*)'
+ ],
+ $condition,
+ __METHOD__,
+ [
+ 'GROUP BY' => [ 'ar_namespace', 'ar_title' ],
+ 'ORDER BY' => [ 'ar_namespace', 'ar_title' ],
+ 'LIMIT' => 100,
+ ]
+ );
+ }
+
+ /**
+ * List the revisions of the given page. Returns result wrapper with
+ * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
+ *
+ * @return ResultWrapper
+ */
+ public function listRevisions() {
+ $dbr = wfGetDB( DB_REPLICA );
+
+ $tables = [ 'archive' ];
+
+ $fields = [
+ 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
+ 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
+ ];
+
+ if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
+ $fields[] = 'ar_content_format';
+ $fields[] = 'ar_content_model';
+ }
+
+ $conds = [ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ];
+
+ $options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
+
+ $join_conds = [];
+
+ ChangeTags::modifyDisplayQuery(
+ $tables,
+ $fields,
+ $conds,
+ $join_conds,
+ $options,
+ ''
+ );
+
+ return $dbr->select( $tables,
+ $fields,
+ $conds,
+ __METHOD__,
+ $options,
+ $join_conds
+ );
+ }
+
+ /**
+ * List the deleted file revisions for this page, if it's a file page.
+ * Returns a result wrapper with various filearchive fields, or null
+ * if not a file page.
+ *
+ * @return ResultWrapper
+ * @todo Does this belong in Image for fuller encapsulation?
+ */
+ public function listFiles() {
+ if ( $this->title->getNamespace() != NS_FILE ) {
+ return null;
+ }
+
+ $dbr = wfGetDB( DB_REPLICA );
+ return $dbr->select(
+ 'filearchive',
+ ArchivedFile::selectFields(),
+ [ 'fa_name' => $this->title->getDBkey() ],
+ __METHOD__,
+ [ 'ORDER BY' => 'fa_timestamp DESC' ]
+ );
+ }
+
+ /**
+ * Return a Revision object containing data for the deleted revision.
+ * Note that the result *may* or *may not* have a null page ID.
+ *
+ * @param string $timestamp
+ * @return Revision|null
+ */
+ public function getRevision( $timestamp ) {
+ $dbr = wfGetDB( DB_REPLICA );
+
+ $fields = [
+ 'ar_rev_id',
+ 'ar_text',
+ 'ar_comment',
+ 'ar_user',
+ 'ar_user_text',
+ 'ar_timestamp',
+ 'ar_minor_edit',
+ 'ar_flags',
+ 'ar_text_id',
+ 'ar_deleted',
+ 'ar_len',
+ 'ar_sha1',
+ ];
+
+ if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
+ $fields[] = 'ar_content_format';
+ $fields[] = 'ar_content_model';
+ }
+
+ $row = $dbr->selectRow( 'archive',
+ $fields,
+ [ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ 'ar_timestamp' => $dbr->timestamp( $timestamp ) ],
+ __METHOD__ );
+
+ if ( $row ) {
+ return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] );
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the most-previous revision, either live or deleted, against
+ * the deleted revision given by timestamp.
+ *
+ * May produce unexpected results in case of history merges or other
+ * unusual time issues.
+ *
+ * @param string $timestamp
+ * @return Revision|null Null when there is no previous revision
+ */
+ public function getPreviousRevision( $timestamp ) {
+ $dbr = wfGetDB( DB_REPLICA );
+
+ // Check the previous deleted revision...
+ $row = $dbr->selectRow( 'archive',
+ 'ar_timestamp',
+ [ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ 'ar_timestamp < ' .
+ $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
+ __METHOD__,
+ [
+ 'ORDER BY' => 'ar_timestamp DESC',
+ 'LIMIT' => 1 ] );
+ $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
+
+ $row = $dbr->selectRow( [ 'page', 'revision' ],
+ [ 'rev_id', 'rev_timestamp' ],
+ [
+ 'page_namespace' => $this->title->getNamespace(),
+ 'page_title' => $this->title->getDBkey(),
+ 'page_id = rev_page',
+ 'rev_timestamp < ' .
+ $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
+ __METHOD__,
+ [
+ 'ORDER BY' => 'rev_timestamp DESC',
+ 'LIMIT' => 1 ] );
+ $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
+ $prevLiveId = $row ? intval( $row->rev_id ) : null;
+
+ if ( $prevLive && $prevLive > $prevDeleted ) {
+ // Most prior revision was live
+ return Revision::newFromId( $prevLiveId );
+ } elseif ( $prevDeleted ) {
+ // Most prior revision was deleted
+ return $this->getRevision( $prevDeleted );
+ }
+
+ // No prior revision on this page.
+ return null;
+ }
+
+ /**
+ * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
+ *
+ * @param object $row Database row
+ * @return string
+ */
+ public function getTextFromRow( $row ) {
+ if ( is_null( $row->ar_text_id ) ) {
+ // An old row from MediaWiki 1.4 or previous.
+ // Text is embedded in this row in classic compression format.
+ return Revision::getRevisionText( $row, 'ar_' );
+ }
+
+ // New-style: keyed to the text storage backend.
+ $dbr = wfGetDB( DB_REPLICA );
+ $text = $dbr->selectRow( 'text',
+ [ 'old_text', 'old_flags' ],
+ [ 'old_id' => $row->ar_text_id ],
+ __METHOD__ );
+
+ return Revision::getRevisionText( $text );
+ }
+
+ /**
+ * Fetch (and decompress if necessary) the stored text of the most
+ * recently edited deleted revision of the page.
+ *
+ * If there are no archived revisions for the page, returns NULL.
+ *
+ * @return string|null
+ */
+ public function getLastRevisionText() {
+ $dbr = wfGetDB( DB_REPLICA );
+ $row = $dbr->selectRow( 'archive',
+ [ 'ar_text', 'ar_flags', 'ar_text_id' ],
+ [ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ],
+ __METHOD__,
+ [ 'ORDER BY' => 'ar_timestamp DESC' ] );
+
+ if ( $row ) {
+ return $this->getTextFromRow( $row );
+ }
+
+ return null;
+ }
+
+ /**
+ * Quick check if any archived revisions are present for the page.
+ *
+ * @return bool
+ */
+ public function isDeleted() {
+ $dbr = wfGetDB( DB_REPLICA );
+ $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
+ [ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ],
+ __METHOD__
+ );
+
+ return ( $n > 0 );
+ }
+
+ /**
+ * Restore the given (or all) text and file revisions for the page.
+ * Once restored, the items will be removed from the archive tables.
+ * The deletion log will be updated with an undeletion notice.
+ *
+ * This also sets Status objects, $this->fileStatus and $this->revisionStatus
+ * (depending what operations are attempted).
+ *
+ * @param array $timestamps Pass an empty array to restore all revisions,
+ * otherwise list the ones to undelete.
+ * @param string $comment
+ * @param array $fileVersions
+ * @param bool $unsuppress
+ * @param User $user User performing the action, or null to use $wgUser
+ * @param string|string[] $tags Change tags to add to log entry
+ * ($user should be able to add the specified tags before this is called)
+ * @return array|bool array(number of file revisions restored, number of image revisions
+ * restored, log message) on success, false on failure.
+ */
+ public function undelete( $timestamps, $comment = '', $fileVersions = [],
+ $unsuppress = false, User $user = null, $tags = null
+ ) {
+ // If both the set of text revisions and file revisions are empty,
+ // restore everything. Otherwise, just restore the requested items.
+ $restoreAll = empty( $timestamps ) && empty( $fileVersions );
+
+ $restoreText = $restoreAll || !empty( $timestamps );
+ $restoreFiles = $restoreAll || !empty( $fileVersions );
+
+ if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
+ $img = wfLocalFile( $this->title );
+ $img->load( File::READ_LATEST );
+ $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
+ if ( !$this->fileStatus->isOK() ) {
+ return false;
+ }
+ $filesRestored = $this->fileStatus->successCount;
+ } else {
+ $filesRestored = 0;
+ }
+
+ if ( $restoreText ) {
+ $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
+ if ( !$this->revisionStatus->isOK() ) {
+ return false;
+ }
+
+ $textRestored = $this->revisionStatus->getValue();
+ } else {
+ $textRestored = 0;
+ }
+
+ // Touch the log!
+
+ if ( $textRestored && $filesRestored ) {
+ $reason = wfMessage( 'undeletedrevisions-files' )
+ ->numParams( $textRestored, $filesRestored )->inContentLanguage()->text();
+ } elseif ( $textRestored ) {
+ $reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored )
+ ->inContentLanguage()->text();
+ } elseif ( $filesRestored ) {
+ $reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored )
+ ->inContentLanguage()->text();
+ } else {
+ wfDebug( "Undelete: nothing undeleted...\n" );
+
+ return false;
+ }
+
+ if ( trim( $comment ) != '' ) {
+ $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
+ }
+
+ if ( $user === null ) {
+ global $wgUser;
+ $user = $wgUser;
+ }
+
+ $logEntry = new ManualLogEntry( 'delete', 'restore' );
+ $logEntry->setPerformer( $user );
+ $logEntry->setTarget( $this->title );
+ $logEntry->setComment( $reason );
+ $logEntry->setTags( $tags );
+
+ Hooks::run( 'ArticleUndeleteLogEntry', [ $this, &$logEntry, $user ] );
+
+ $logid = $logEntry->insert();
+ $logEntry->publish( $logid );
+
+ return [ $textRestored, $filesRestored, $reason ];
+ }
+
+ /**
+ * This is the meaty bit -- It restores archived revisions of the given page
+ * to the revision table.
+ *
+ * @param array $timestamps Pass an empty array to restore all revisions,
+ * otherwise list the ones to undelete.
+ * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs
+ * @param string $comment
+ * @throws ReadOnlyError
+ * @return Status Status object containing the number of revisions restored on success
+ */
+ private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
+ if ( wfReadOnly() ) {
+ throw new ReadOnlyError();
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->startAtomic( __METHOD__ );
+
+ $restoreAll = empty( $timestamps );
+
+ # Does this page already exist? We'll have to update it...
+ $article = WikiPage::factory( $this->title );
+ # Load latest data for the current page (T33179)
+ $article->loadPageData( 'fromdbmaster' );
+ $oldcountable = $article->isCountable();
+
+ $page = $dbw->selectRow( 'page',
+ [ 'page_id', 'page_latest' ],
+ [ 'page_namespace' => $this->title->getNamespace(),
+ 'page_title' => $this->title->getDBkey() ],
+ __METHOD__,
+ [ 'FOR UPDATE' ] // lock page
+ );
+
+ if ( $page ) {
+ $makepage = false;
+ # Page already exists. Import the history, and if necessary
+ # we'll update the latest revision field in the record.
+
+ # Get the time span of this page
+ $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
+ [ 'rev_id' => $page->page_latest ],
+ __METHOD__ );
+
+ if ( $previousTimestamp === false ) {
+ wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" );
+
+ $status = Status::newGood( 0 );
+ $status->warning( 'undeleterevision-missing' );
+ $dbw->endAtomic( __METHOD__ );
+
+ return $status;
+ }
+ } else {
+ # Have to create a new article...
+ $makepage = true;
+ $previousTimestamp = 0;
+ }
+
+ $oldWhere = [
+ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ ];
+ if ( !$restoreAll ) {
+ $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
+ }
+
+ $fields = [
+ 'ar_id',
+ 'ar_rev_id',
+ 'rev_id',
+ 'ar_text',
+ 'ar_comment',
+ 'ar_user',
+ 'ar_user_text',
+ 'ar_timestamp',
+ 'ar_minor_edit',
+ 'ar_flags',
+ 'ar_text_id',
+ 'ar_deleted',
+ 'ar_page_id',
+ 'ar_len',
+ 'ar_sha1'
+ ];
+
+ if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
+ $fields[] = 'ar_content_format';
+ $fields[] = 'ar_content_model';
+ }
+
+ /**
+ * Select each archived revision...
+ */
+ $result = $dbw->select(
+ [ 'archive', 'revision' ],
+ $fields,
+ $oldWhere,
+ __METHOD__,
+ /* options */
+ [ 'ORDER BY' => 'ar_timestamp' ],
+ [ 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ] ]
+ );
+
+ $rev_count = $result->numRows();
+ if ( !$rev_count ) {
+ wfDebug( __METHOD__ . ": no revisions to restore\n" );
+
+ $status = Status::newGood( 0 );
+ $status->warning( "undelete-no-results" );
+ $dbw->endAtomic( __METHOD__ );
+
+ return $status;
+ }
+
+ // We use ar_id because there can be duplicate ar_rev_id even for the same
+ // page. In this case, we may be able to restore the first one.
+ $restoreFailedArIds = [];
+
+ // Map rev_id to the ar_id that is allowed to use it. When checking later,
+ // if it doesn't match, the current ar_id can not be restored.
+
+ // Value can be an ar_id or -1 (-1 means no ar_id can use it, since the
+ // rev_id is taken before we even start the restore).
+ $allowedRevIdToArIdMap = [];
+
+ $latestRestorableRow = null;
+
+ foreach ( $result as $row ) {
+ if ( $row->ar_rev_id ) {
+ // rev_id is taken even before we start restoring.
+ if ( $row->ar_rev_id === $row->rev_id ) {
+ $restoreFailedArIds[] = $row->ar_id;
+ $allowedRevIdToArIdMap[$row->ar_rev_id] = -1;
+ } else {
+ // rev_id is not taken yet in the DB, but it might be taken
+ // by a prior revision in the same restore operation. If
+ // not, we need to reserve it.
+ if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) {
+ $restoreFailedArIds[] = $row->ar_id;
+ } else {
+ $allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id;
+ $latestRestorableRow = $row;
+ }
+ }
+ } else {
+ // If ar_rev_id is null, there can't be a collision, and a
+ // rev_id will be chosen automatically.
+ $latestRestorableRow = $row;
+ }
+ }
+
+ $result->seek( 0 ); // move back
+
+ $oldPageId = 0;
+ if ( $latestRestorableRow !== null ) {
+ $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
+
+ // grab the content to check consistency with global state before restoring the page.
+ $revision = Revision::newFromArchiveRow( $latestRestorableRow,
+ [
+ 'title' => $article->getTitle(), // used to derive default content model
+ ]
+ );
+ $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
+ $content = $revision->getContent( Revision::RAW );
+
+ // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
+ $status = $content->prepareSave( $article, 0, -1, $user );
+ if ( !$status->isOK() ) {
+ $dbw->endAtomic( __METHOD__ );
+
+ return $status;
+ }
+ }
+
+ $newid = false; // newly created page ID
+ $restored = 0; // number of revisions restored
+ /** @var Revision $revision */
+ $revision = null;
+
+ // If there are no restorable revisions, we can skip most of the steps.
+ if ( $latestRestorableRow === null ) {
+ $failedRevisionCount = $rev_count;
+ } else {
+ if ( $makepage ) {
+ // Check the state of the newest to-be version...
+ if ( !$unsuppress
+ && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
+ ) {
+ $dbw->endAtomic( __METHOD__ );
+
+ return Status::newFatal( "undeleterevdel" );
+ }
+ // Safe to insert now...
+ $newid = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id );
+ if ( $newid === false ) {
+ // The old ID is reserved; let's pick another
+ $newid = $article->insertOn( $dbw );
+ }
+ $pageId = $newid;
+ } else {
+ // Check if a deleted revision will become the current revision...
+ if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
+ // Check the state of the newest to-be version...
+ if ( !$unsuppress
+ && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
+ ) {
+ $dbw->endAtomic( __METHOD__ );
+
+ return Status::newFatal( "undeleterevdel" );
+ }
+ }
+
+ $newid = false;
+ $pageId = $article->getId();
+ }
+
+ foreach ( $result as $row ) {
+ // Check for key dupes due to needed archive integrity.
+ if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) {
+ continue;
+ }
+ // Insert one revision at a time...maintaining deletion status
+ // unless we are specifically removing all restrictions...
+ $revision = Revision::newFromArchiveRow( $row,
+ [
+ 'page' => $pageId,
+ 'title' => $this->title,
+ 'deleted' => $unsuppress ? 0 : $row->ar_deleted
+ ] );
+
+ $revision->insertOn( $dbw );
+ $restored++;
+
+ Hooks::run( 'ArticleRevisionUndeleted',
+ [ &$this->title, $revision, $row->ar_page_id ] );
+ }
+
+ // Now that it's safely stored, take it out of the archive
+ // Don't delete rows that we failed to restore
+ $toDeleteConds = $oldWhere;
+ $failedRevisionCount = count( $restoreFailedArIds );
+ if ( $failedRevisionCount > 0 ) {
+ $toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )';
+ }
+
+ $dbw->delete( 'archive',
+ $toDeleteConds,
+ __METHOD__ );
+ }
+
+ $status = Status::newGood( $restored );
+
+ if ( $failedRevisionCount > 0 ) {
+ $status->warning(
+ wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) );
+ }
+
+ // Was anything restored at all?
+ if ( $restored ) {
+ $created = (bool)$newid;
+ // Attach the latest revision to the page...
+ $wasnew = $article->updateIfNewerOn( $dbw, $revision );
+ if ( $created || $wasnew ) {
+ // Update site stats, link tables, etc
+ $article->doEditUpdates(
+ $revision,
+ User::newFromName( $revision->getUserText( Revision::RAW ), false ),
+ [
+ 'created' => $created,
+ 'oldcountable' => $oldcountable,
+ 'restored' => true
+ ]
+ );
+ }
+
+ Hooks::run( 'ArticleUndelete', [ &$this->title, $created, $comment, $oldPageId ] );
+ if ( $this->title->getNamespace() == NS_FILE ) {
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) );
+ }
+ }
+
+ $dbw->endAtomic( __METHOD__ );
+
+ return $status;
+ }
+
+ /**
+ * @return Status
+ */
+ public function getFileStatus() {
+ return $this->fileStatus;
+ }
+
+ /**
+ * @return Status
+ */
+ public function getRevisionStatus() {
+ return $this->revisionStatus;
+ }
+}
# Everything except bracket, space, or control characters
# \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
# as well as U+3000 is IDEOGRAPHIC SPACE for T21052
- const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
+ # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM
+ # uses to replace invalid HTML characters.
+ const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
# Simplified expression to match an IPv4 or IPv6 address, or
# at least one character of a host name (embeds EXT_LINK_URL_CLASS)
- const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}])';
+ const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
# RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
// @codingStandardsIgnoreStart Generic.Files.LineLength
- const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
+ const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
// @codingStandardsIgnoreEnd
$this->mUrlProtocols = wfUrlProtocols();
$this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
self::EXT_LINK_ADDR .
- self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
+ self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
if ( isset( $conf['preprocessorClass'] ) ) {
$this->mPreprocessorClass = $conf['preprocessorClass'];
} elseif ( defined( 'HPHP_VERSION' ) ) {
$text = strtr( $text, "\x7f", "?" );
$magicScopeVariable = $this->lock();
}
+ // Strip U+0000 NULL (T159174)
+ $text = str_replace( "\000", '', $text );
$this->startParse( $title, $options, self::OT_HTML, $clearState );
$this->startParse( $title, $options, self::OT_WIKI, $clearState );
$this->setUser( $user );
+ // Strip U+0000 NULL (T159174)
+ $text = str_replace( "\000", '', $text );
+
// We still normalize line endings for backwards-compatibility
// with other code that just calls PST, but this should already
// be handled in TextContent subclasses
* @since 1.25
*/
public function getIndicators() {
- $out = "<div class=\"mw-indicators\">\n";
+ $out = "<div class=\"mw-indicators mw-body-content\">\n";
foreach ( $this->data['indicators'] as $id => $content ) {
$out .= Html::rawElement(
'div',
$panel[] = $form;
$panelString = implode( "\n", $panel );
+ $rcoptions = Xml::fieldset(
+ $this->msg( 'recentchanges-legend' )->text(),
+ $panelString,
+ [ 'class' => 'rcoptions' ]
+ );
+
// Insert a placeholder for RCFilters
if ( $this->getUser()->getOption( 'rcenhancedfilters' ) ) {
+ $rcfilterContainer = Html::element(
+ 'div',
+ [ 'class' => 'rcfilters-container' ]
+ );
+
+ // Wrap both with rcfilters-head
$this->getOutput()->addHTML(
- Html::element(
+ Html::rawElement(
'div',
- [ 'class' => 'rcfilters-container' ]
+ [ 'class' => 'rcfilters-head' ],
+ $rcfilterContainer . $rcoptions
)
);
+ } else {
+ $this->getOutput()->addHTML( $rcoptions );
}
- $this->getOutput()->addHTML(
- Xml::fieldset(
- $this->msg( 'recentchanges-legend' )->text(),
- $panelString,
- [ 'class' => 'rcoptions' ]
- )
- );
-
$this->setBottomText( $opts );
}
$literalBlob = '';
// Guard against delimiter nulls in the input
+ // (should never happen: see T159174)
$text = str_replace( "\000", '', $text );
$markupMatches = null;
"rcfilters-filterlist-feedbacklink": "تقديم مراجعات لمرشحات (بيتا) الجديدة",
"rcfilters-highlightbutton-title": "التعليم على النتائج",
"rcfilters-highlightmenu-title": "اختر لونًا",
+ "rcfilters-highlightmenu-help": "اختر لونا للتعليم على هذه الخاصية",
"rcfilters-filterlist-noresults": "لم يتم العثور على مرشحات",
"rcfilters-filtergroup-registration": "تسجيل المستخدم",
"rcfilters-filter-registered-label": "مسجل",
"rcfilters-invalid-filter": "Filtru inválidu",
"rcfilters-empty-filter": "Nun hai filtros activos. Amuésense toles contribuciones.",
"rcfilters-filterlist-title": "Filtros",
+ "rcfilters-filterlist-feedbacklink": "Comentar sobro los nuevos filtros (beta)",
"rcfilters-highlightbutton-title": "Resaltar resultaos",
"rcfilters-highlightmenu-title": "Seleiciona un color",
"rcfilters-filterlist-noresults": "Nun s'alcontraron filtros",
"rcfilters-filterlist-feedbacklink": "Пакінуць водгук пра новыя (бэта) фільтры",
"rcfilters-highlightbutton-title": "Вылучыць вынікі",
"rcfilters-highlightmenu-title": "Абярыце колер",
+ "rcfilters-highlightmenu-help": "Абярыце колер для вылучэньня гэтай уласьцівасьці",
"rcfilters-filterlist-noresults": "Фільтры ня знойдзеныя",
"rcfilters-filtergroup-registration": "Рэгістрацыя ўдзельнікаў",
"rcfilters-filter-registered-label": "Зарэгістраваныя",
"authmanager-link-no-primary": "Пададзеныя ўліковыя зьвесткі ня могуць быць выкарыстаныя для злучэньня рахункаў.",
"authmanager-link-not-in-progress": "Злучэньне рахункаў не выконваецца або страчаныя зьвесткі сэсіі. Калі ласка, пачніце ізноў спачатку.",
"authmanager-authplugin-setpass-failed-title": "Памылка зьмены паролю",
+ "authmanager-authplugin-setpass-failed-message": "Дадатак аўтэнтыфікацыі адмовіў зьмену паролю.",
"authmanager-realname-label": "Сапраўднае імя",
"authmanager-provider-temporarypassword": "Часовы пароль",
"changecredentials": "Зьмена ўліковых зьвестак",
"editcomment": "Резюмето на редакцията беше: <em>$1</em>.",
"revertpage": "Премахване на [[Special:Contributions/$2|редакции на $2]] ([[User talk:$2|беседа]]); възвръщане към последната версия на [[User:$1|$1]]",
"revertpage-nouser": "Премахнати редакции на (скрито потребителско име) и връщане към последната версия на [[User:$1|$1]]",
- "rollback-success": "Отменени редакции на $1; възвръщане към последната версия на $2.",
+ "rollback-success": "Отменени редакции на {{GENDER:$3|$1}};\nвъзвръщане към последната версия на {{GENDER:$4|$2}}.",
"sessionfailure-title": "Прекъсната сесия",
"sessionfailure": "Изглежда има проблем със сесията ви; действието беше отказано като предпазна мярка срещу крадене на сесията. Натиснете бутона за връщане на браузъра, презаредете страницата, от която сте дошли, и опитайте отново.",
"changecontentmodel-title-label": "Заглавие на страницата",
"api-error-emptypage": "Създаването на нови, празени страници, не е разрешено.",
"api-error-publishfailed": "Вътрешна грешка: Сървърът не успя да съхрани временния файл.",
"api-error-stashfailed": "Вътрешна грешка: Сървърът не успя да съхрани временния файл.",
- "api-error-unknown-warning": "Непознато предупреждение: „$1“",
+ "api-error-unknown-warning": "Непознато предупреждение: „$1“.",
"api-error-unknownerror": "Неизвестна грешка: „$1“.",
"duration-seconds": "$1 {{PLURAL:$1|секунда|секунди}}",
"duration-minutes": "$1 {{PLURAL:$1|минута|минути}}",
"passwordremindertext": "কেউ একজন ($1 আইপি ঠিকানাটি থেকে সম্ভবত আপনি) অনুরোধ করেছেন যেন আমরা আপনাকে {{SITENAME}} ($4) এর জন্য একটি নতুন পাসওয়ার্ড পাঠাই।\n\"$2\" নামে অ্যাকাউন্ট খোলা হয়েছে এবং এর পাসওয়ার্ড \"$3\"। আপনি যদি এটাই চেয়ে থাকেন, তাহলে আপনাকে এখন অ্যাকাউন্টে প্রবেশ করতে হবে ও নতুন একটি পাসওয়ার্ড পছন্দ করতে হবে।\n{{PLURAL:$5|এক দিন|$5 দিন}} পরে আপনার এই অস্থায়ী পাসওয়ার্ডের মেয়াদ উত্তীর্ণ হয়ে যাবে।\n\nযদি আপনি ছাড়া অন্য কেউ এই অনুরোধ করে থাকে, কিংবা যদি আপনার পুরনো পাসওয়ার্ড মনে পড়ে গিয়ে থাকে ও সেটি আর বদলাবার ইচ্ছা না থাকে, তাহলে এই বার্তাটি উপেক্ষা করতে পারেন এবং পুরনো পাসওয়ার্ডটিই ব্যবহার করে যেতে পারেন।",
"noemail": "\"$1\" ব্যবহারকারীর জন্য কোন ই-মেইল ঠিকানা সংরক্ষিত নেই।",
"noemailcreate": "আপনাকে অবশ্যই একটি সঠিক ইমেইল ঠিকানা দিতে হবে",
- "passwordsent": "একটি নতুন পাসওয়ার্ড \"$1\" ব্যবহারকারীর ই-মেইল ঠিকানায় পাঠানো হয়েছে। দয়াকরে তা পাওয়ার পর আবার লগ-ইন করুন।",
+ "passwordsent": "একটি নতুন পাসওয়ার্ড \"$1\" ব্যবহারকারীর ই-মেইল ঠিকানায় পাঠানো হয়েছে। দয়া করে তা পাওয়ার পর আবার প্রবেশ করুন।",
"blocked-mailpassword": "আপনার আইপি ঠিকানাটি থেকে সম্পাদনা করতে বাধা আছে। অপব্যবহার রোধ করার জন্য, এই আইপি ঠিকানা থেকে পাসওয়ার্ড পুনরুদ্ধার করার অনুমতি দেয়া হয়নি।",
"eauthentsent": "মনোনীত ই-মেইল ঠিকানায় একটি নিশ্চিতকরণ ই-মেইল পাঠানো হয়েছে।\nঐ অ্যাকাউন্টটে অন্য কোন ই-মেইল পাঠানোর আগে আপনাকে ই-মেইলের নির্দেশগুলি অনুসরণ করতে হবে, যাতে অ্যাকাউন্টটি যে আসলেই আপনার, তা নিশ্চিত হয়।",
"throttled-mailpassword": "বিগত {{PLURAL:$1|ঘণ্টার|$1 ঘণ্টার}} মধ্যে ইতিমধ্যেই একবার পাসওয়ার্ড বদলের তথ্য পাঠানো হয়েছে। অপব্যবহার রোধে প্রতি {{PLURAL:$1|ঘণ্টায়|$1 ঘণ্টায়}} কেবল একবার পাসওয়ার্ড বদলের তথ্য পাঠানো যাবে।",
"uploadbtn": "ফাইল আপলোড করুন",
"reuploaddesc": "আপলোড বাতিল করো এবং আপলোড ফর্মে ফেরত যাও।",
"upload-tryagain": "পরিবর্তিত ফাইল বর্ণনা জমা দিন",
- "uploadnologin": "à¦\86পনি লà¦\97-à¦\87ন à¦\95রà§\87ননি।",
+ "uploadnologin": "à¦\86পনি পà§\8dরবà§\87শ à¦\95রà§\87ননি",
"uploadnologintext": "ফাইল আপলোড করতে হলে আপনাকে অবশ্যই $1 করতে হবে।",
"upload_directory_missing": "আপলোড ডাইরেক্টরি ($1) পাওয়া যাচ্ছে না এবং ওয়েব সার্ভার কর্তৃক তৈরি করা যাচ্ছে না।",
"upload_directory_read_only": "আপলোড ডিরেক্টরিটি ($1) ওয়েবসার্ভার কর্তৃক লিখনযোগ্য নয়।",
"editcomment": "সম্পাদনা সারাংশ ছিল: \"''$1''\"।",
"revertpage": "[[Special:Contributions/$2|$2]] ([[User talk:$2|আলাপ]]) এর সম্পাদিত সংস্করণ হতে [[User:$1|$1]] এর সম্পাদিত সর্বশেষ সংস্করণে ফেরত যাওয়া হয়েছে।",
"revertpage-nouser": "একজন গোপন ব্যবহারকারী কর্তৃক সম্পাদিত সম্পাদনাটি বাতিলপূর্বক {{GENDER:$1|[[User:$1|$1]]}}-এর সর্বশেষ সম্পাদনায় ফেরত যাওয়া হয়েছে।",
- "rollback-success": "$1-এর সম্পাদনাগুলি পূর্বাবস্থায় ফিরিয়ে নেওয়া হয়েছে; $2-এর করা শেষ সংস্করণে পাতাটি ফেরত নেওয়া হয়েছে।",
+ "rollback-success": "{{GENDER:$3|$1}}-এর সম্পাদনাগুলি পূর্বাবস্থায় ফিরিয়ে নেওয়া হয়েছে; {{GENDER:$4|$2}}-এর করা শেষ সংস্করণে পাতাটি ফেরত নেওয়া হয়েছে।",
"rollback-success-notify": "$1-এর সম্পাদনাগুলি বাতিল করা হয়েছে; \n$2-এর করা শেষ সংস্করণে ফেরত নেওয়া হয়েছে। [$3 পরিবর্তন দেখুন]",
"sessionfailure-title": "সেশন পরিত্যক্ত",
"sessionfailure": "আপনার প্রবেশ সেশনে একটি সমস্যা হয়েছে বলে মনে হচ্ছে;\nসেশন হাইজ্যাক প্রতিরোধের উপায় হিসেবে এই কাজটি বাতিল করা হয়েছে।\nঅনুগ্রহ ব্রাউজারের \"পিছনে\" বোতাম চাপুন এবং যে পাতা থেকে এসেছিলেন, তা পুনঃলোড করুন এবং আবার চেষ্টা করুন।",
"activeusers-count": "$1 {{PLURAL:$1|oberiadenn}} abaoe an {{PLURAL:$3|deiz|$3 deiz}} diwezhañ",
"activeusers-from": "Diskouez an implijerien adal :",
"activeusers-groups": "Diskouez an implijerien zo ezel eus ar strolladoù :",
+ "activeusers-excludegroups": "Skarzhañ an implijerien ezel eus ar strolladoù :",
"activeusers-noresult": "N'eus bet kavet implijer ebet.",
"activeusers-submit": "Diskouez an implijerien oberiant",
"listgrouprights": "Gwirioù ar strolladoù implijer",
"trackingcategories-msg": "Rummad evezhiañ",
"trackingcategories-name": "Anv ar gemennadenn",
"trackingcategories-desc": "Dezverkoù evit degemer rummadoù",
+ "restricted-displaytitle-ignored": "Pajennoù gant titloù diskwel lezet a-gostez",
+ "restricted-displaytitle-ignored-desc": "Ar bajenn-mañ zo dezhi un <code><nowiki>{{DISPLAYTITLE}}</nowiki></code> zo bet laosket a-gostez peogwir n'eo ket kevatal d'an titl zo d'ar bajenn bremañ.",
+ "noindex-category-desc": "Ar bajenn-mañ n'eo ket menegeret gant ar robotoù rak ar ger hud <code><nowiki>__NOINDEX__</nowiki></code> zo enni hag emañ en un esaouenn anv m'eo aotreet ar merkañ.",
"broken-file-category-desc": "Er bajenn-mañ ez eus ul liamm restr torr (ul liamm da enframmañ ur restr pa n'eus ket eus ar restr-se).",
"trackingcategories-nodesc": "N'eus deskrivadur ebet.",
"trackingcategories-disabled": "Diweredekaet eo ar rummad",
"rollbacklinkcount": "disteurel $1 {{PLURAL:$1|kemm}}",
"rollbacklinkcount-morethan": "disteurel ouzhpenn $1 {{PLURAL:$1|kemm}}",
"rollbackfailed": "C'hwitet eo bet an distaoladenn",
+ "rollback-missingparam": "Arventennoù rekis d'ar reked a vank.",
+ "rollback-missingrevision": "Dibosupl kargañ roadennoù ar stumm.",
"cantrollback": "Dibosupl da zisteuler: an aozer diwezhañ eo an hini nemetañ da vezañ kemmet ar pennad-mañ",
"alreadyrolled": "Dibosupl eo disteuler ar c'hemm diwezhañ graet d'ar bajenn [[:$1]] gant [[User:$2|$2]] ([[User talk:$2|Kaozeal]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]);\nkemmet pe distaolet eo bet c'hoazh gant unan bennak all.\n\nAr c'hemm diwezhañ d'ar bajenn-mañ a oa bet graet gant [[User:$3|$3]] ([[User talk:$3|Kaozeal]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
"editcomment": "Diverradenn ar c'hemm a oa : <em>$1</em>.",
"rollback-success": "Disteuler kemmoù $1; distreiñ da stumm diwezhañ $2.",
"sessionfailure-title": "Fazi dalc'h",
"sessionfailure": "Evit doare ez eus ur gudenn gant ho talc'h;\nNullet eo bet an ober-mañ a-benn en em wareziñ diouzh an tagadennoù preizhañ.\nKlikit war \"kent\" hag adkargit ar bajenn oc'h deuet drezi; goude klaskit en-dro.",
+ "changecontentmodel": "Cheñch patrom danvez ur bajenn",
+ "changecontentmodel-legend": "Cheñch ar patrom danvez",
"changecontentmodel-title-label": "Anv ar bajenn",
"changecontentmodel-model-label": "Patrom danvez nevez",
"changecontentmodel-reason-label": "Abeg :",
"changecontentmodel-submit": "Kemmañ",
"changecontentmodel-success-title": "Cheñchet eo bet ar patrom danvez",
+ "changecontentmodel-success-text": "Kemmet eo bet patrom danvez [[:$1]].",
+ "changecontentmodel-cannot-convert": "N'hall ket danvez [[:$1]] bezañ troet en ur seurt $2.",
"changecontentmodel-emptymodels-title": "N'eus patrom danvez hegerz ebet",
"logentry-contentmodel-change-revertlink": "disteuler",
"logentry-contentmodel-change-revert": "disteuler",
"block-log-flags-hiddenname": "anv implijer kuzhet",
"range_block_disabled": "Diweredekaet eo bet ar stankañ stuc'hadoù IP.",
"ipb_expiry_invalid": "amzer termen direizh.",
+ "ipb_expiry_old": "Tremenet eo an termen echuiñ.",
"ipb_expiry_temp": "Peurbadus e rank bezañ bloc'hadoù an implijerien guzh.",
"ipb_hide_invalid": "Ne c'haller ket dilemel ar gont-mañ : Ouzhpenn {{PLURAL:$1|ur c'hemm|$1 kemm}} zo enni.",
"ipb_already_blocked": "Stanket eo \"$1\" dija",
"viewcount": "Ovoj stranici je pristupljeno {{PLURAL:$1|$1 put|$1 puta}}.",
"protectedpage": "Zaštićena stranica",
"jumpto": "Idi na:",
- "jumptonavigation": "navigacija",
- "jumptosearch": "traži",
+ "jumptonavigation": "navigaciju",
+ "jumptosearch": "pretragu",
"view-pool-error": "Žao nam je, serveri su trenutno preopterećeni.\nPreviše korisnika pokušava da pregleda ovu stranicu.\nMolimo pričekajte trenutak prije nego što ponovno pokušate pristupiti ovoj stranici.\n\n$1",
"generic-pool-error": "Nažalost, serveri su trenutno preopterećeni.\nPreviše korisnika pokušava da vidi ovaj resurs.\nMolimo pričekajte trenutak prije nego što ponovo pokušate da mu pristupite.",
"pool-timeout": "Zaustavi čekanje na zaključavanje",
"headline_sample": "Naslov",
"headline_tip": "Podnaslov",
"nowiki_sample": "Dodaj neformatirani tekst ovdje",
- "nowiki_tip": "Ignoriši viki formatiranje teksta",
+ "nowiki_tip": "Zanemari wikiformatiranje",
"image_sample": "ime_slike.jpg",
"image_tip": "Uklopljena slika",
"media_sample": "ime_medija_fajla.ogg",
"importcantopen": "Ne mogu otvoriti datoteku za uvoz",
"importbadinterwiki": "Loš interwiki link",
"importsuccess": "Uspješno ste uvezli stranicu!",
- "importnosources": "Nije definisan međuwiki izvor za uvoz i direktna postavljanja historije su isključena.",
+ "importnosources": "Nije definiran međuwiki izvor za uvoz i direktna postavljanja historije su isključena.",
"importnofile": "Uvozna datoteka nije postavljena.",
"importuploaderrorsize": "Postavljanje uvozne datoteke nije uspjelo.\nDatoteka je veća nego što je dopušteno.",
"importuploaderrorpartial": "Postavljanje uvozne datoteke nije uspjelo.\nDatoteka je samo djelimično postavljena.",
"modifiedarticleprotection": "ئاستی پاراستنی «[[$1]]»ی گۆڕی",
"unprotectedarticle": "پاراستنی لەسەر «[[$1]]» لابرد",
"movedarticleprotection": "ڕێککارییەکانی پاراستن لە «[[$2]]» گوازرایەوە بۆ «[[$1]]»",
+ "unprotectedarticle-comment": "{{GENDER:$2|پاراستنی}} لەسەر ''[[$1]]'' لابرد",
"protect-title": "گۆڕینی ئاستی پاراستنی \"$1\"",
"protect-title-notallowed": "دیتنی ئاستی پاراستنی «$1»",
"prot_1movedto2": "[[$1]] گوازرایەوە بۆ [[$2]]",
"rcfilters-filterlist-feedbacklink": "Rückmeldung zu den neuen (Beta-)Filtern hinterlassen",
"rcfilters-highlightbutton-title": "Ergebnisse hervorheben",
"rcfilters-highlightmenu-title": "Eine Farbe auswählen",
+ "rcfilters-highlightmenu-help": "Eine Farbe auswählen, um diese Eigenschaft hervorzuheben.",
"rcfilters-filterlist-noresults": "Keine Filter gefunden",
"rcfilters-filtergroup-registration": "Benutzerregistrierung",
"rcfilters-filter-registered-label": "Angemeldet",
"checkbox-select": "Weçinaye: $1",
"checkbox-all": "Pêro",
"checkbox-none": "Temam",
- "checkbox-invert": "Dimlaşt ke (verdindayış)",
+ "checkbox-invert": "Verdindayış",
"allpages": "Pêro peli",
"nextpage": "Pela badê cû ($1)",
"prevpage": "Pela verêne ($1)",
"rcfilters-filterlist-feedbacklink": "Provide feedback on the new (beta) filters",
"rcfilters-highlightbutton-title": "Highlight results",
"rcfilters-highlightmenu-title": "Select a color",
+ "rcfilters-highlightmenu-help": "Select a color to highlight this property",
"rcfilters-filterlist-noresults": "No filters found",
"rcfilters-filtergroup-registration": "User registration",
"rcfilters-filter-registered-label": "Registered",
"databaseerror-query": "Consulta: $1",
"databaseerror-function": "Función: $1",
"databaseerror-error": "Error: $1",
- "transaction-duration-limit-exceeded": "Para evitar la creación de lentitud alta de respuesta, la transacción fue abortada porque la duración de escritura ($1) excedió el límite de $2 {{PLURAL:$2|segundo|segundos}}.\nSi estás cambiando muchos elementos a la vez, trata de hacer operaciones similares más pequeñas.",
+ "transaction-duration-limit-exceeded": "Con el fin de evitar un aumento excesivo del retardo de replicación, se anuló esta transacción porque la duración de escritura ($1) excedió el límite de $2 {{PLURAL:$2|segundo|segundos}}.\nSi estás cambiando muchos elementos a la vez, trata de hacer operaciones similares más pequeñas.",
"laggedslavemode": "<strong>Advertencia:</strong> puede que falten las actualizaciones más recientes en esta página.",
"readonly": "Base de datos bloqueada",
"enterlockreason": "Explica el motivo del bloqueo, incluyendo una estimación de cuándo se producirá el desbloqueo",
"reblock-logentry": "cambió el bloqueo para [[$1]] con una caducidad de $2 $3",
"blocklogtext": "Esto es un registro de acciones de bloqueo y desbloqueo de usuarios.\nLas direcciones IP bloqueadas automáticamente no aparecen aquí.\nConsulta la [[Special:BlockList|lista de bloqueos]] para ver la lista de bloqueos y prohibiciones de operar en vigor.",
"unblocklogentry": "desbloqueó a $1",
- "block-log-flags-anononly": "sólo anónimos",
+ "block-log-flags-anononly": "solo anónimos",
"block-log-flags-nocreate": "desactivada la creación de cuentas",
"block-log-flags-noautoblock": "bloqueo automático desactivado",
"block-log-flags-noemail": "correo electrónico desactivado",
"editcomment": "Redaktsiooni resümee oli: <em>$1</em>.",
"revertpage": "Tühistati kasutaja [[Special:Contributions/$2|$2]] ([[User talk:$2|arutelu]]) tehtud muudatused ja pöörduti tagasi viimasele muudatusele, mille tegi [[User:$1|$1]].",
"revertpage-nouser": "Tühistati peidetud kasutaja muudatused ja pöörduti tagasi viimasele muudatusele, mille tegi [[User:$1|$1]].",
- "rollback-success": "Tühistati muudatused, mille tegi $1;\npöörduti tagasi viimasele muudatusele, mille tegi $2.",
+ "rollback-success": "Tühistati muudatused, mille tegi {{GENDER:$3|$1}};\npöörduti tagasi viimasele muudatusele, mille tegi {{GENDER:$4|$2}}.",
"rollback-success-notify": "Tühistatud kasutaja $1 tehtud muudatused;\npöördutud tagasi kasutaja $2 viimase redaktsiooni juurde. [$3 Näita muudatusi]",
"sessionfailure-title": "Seansiviga",
"sessionfailure": "Sinu sisselogimisseansiga näib probleem olevat.\nSee toiming on seansiärandamise vastase ettevaatusabinõuna tühistatud.\nMine tagasi eelmisele leheküljele ja taaslaadi see, seejärel proovi uuesti.",
"htmlform-date-placeholder": "AAAA-KK-PP",
"htmlform-time-placeholder": "TT:MM:SS",
"htmlform-datetime-placeholder": "AAAA-KK-PP TT:MM:SS",
+ "htmlform-date-invalid": "Väärtus, mille ette andsid, pole äratuntav kuupäev. Proovi kasutada vormingut AAAA-KK-PP.",
+ "htmlform-time-invalid": "Väärtus, mille ette andsid, pole äratuntav kellaaeg. Proovi kasutada vormingut TT:MM:SS.",
+ "htmlform-datetime-invalid": "Väärtus, mille ette andsid, pole äratuntav kuupäev ja kellaaeg. Proovi kasutada vormingut AAAA-KK-PP TT:MM:SS.",
+ "htmlform-date-toolow": "Väärtus, mille ette andsid, on enne varaseimat lubatud kuupäeva $1.",
+ "htmlform-date-toohigh": "Väärtus, mille ette andsid, on pärast hiliseimat lubatud kuupäeva $1.",
+ "htmlform-time-toolow": "Väärtus, mille ette andsid, on enne varaseimat lubatud kellaaega $1.",
+ "htmlform-time-toohigh": "Väärtus, mille ette andsid, on pärast hiliseimat lubatud kellaaega $1.",
+ "htmlform-datetime-toolow": "Väärtus, mille ette andsid, on enne varaseimat lubatud kuupäeva ja kellaaega $1.",
+ "htmlform-datetime-toohigh": "Väärtus, mille ette andsid, on pärast hiliseimat lubatud kuupäeva ja kellaaega $1.",
"htmlform-title-badnamespace": "[[:$1]] pole nimeruumis \"{{ns:$2}}\".",
"htmlform-title-not-creatable": "Pealkirja \"$1\" all ei saa lehekülge alustada.",
"htmlform-title-not-exists": "Lehekülge $1 pole olemas.",
"logentry-tag-update-logentry": "$1 {{GENDER:$2|uuendas}} leheküljel \"$3\" logisissekande $5 märgiseid ({{PLURAL:$7|lisatud}} $6; {{PLURAL:$9|eemaldatud}} $8)",
"rightsnone": "(puudub)",
"revdelete-summary": "resümee",
+ "rightslogentry-temporary-group": "$1 (ajutine, tähtaeg $2)",
"feedback-adding": "Tagasiside lisamine leheküljele...",
"feedback-back": "Tagasi",
"feedback-bugcheck": "Hästi! Kontrolli vaid, ega tegu pole juba [$1 teada oleva veaga].",
"rcfilters-filterlist-feedbacklink": "Fournir un commentaire sur les nouveaux filtres (en bêta)",
"rcfilters-highlightbutton-title": "Mettre en valeur les résultats",
"rcfilters-highlightmenu-title": "Choisir une couleur",
+ "rcfilters-highlightmenu-help": "Sélectionner une couleur pour mettre en évidence cette propriété",
"rcfilters-filterlist-noresults": "Aucun filtre trouvé",
"rcfilters-filtergroup-registration": "Inscription de l’utilisateur",
"rcfilters-filter-registered-label": "Connectés",
"rcfilters-invalid-filter": "Filtro no válido",
"rcfilters-empty-filter": "Non hai filtros activos. Móstranse tódalas contribucións.",
"rcfilters-filterlist-title": "Filtros",
+ "rcfilters-filterlist-feedbacklink": "Deixar comentarios sobre os novos filtros (en fase beta)",
"rcfilters-highlightbutton-title": "Resaltar resultados",
"rcfilters-highlightmenu-title": "Seleccione unha cor",
"rcfilters-filterlist-noresults": "Non se atoparon filtros",
"editcomment": "O resumo de edición foi: <em>$1</em>.",
"revertpage": "Desfixéronse as edicións de [[Special:Contributions/$2|$2]] ([[User talk:$2|conversa]]); cambiado á última versión feita por [[User:$1|$1]]",
"revertpage-nouser": "Desfixéronse as edicións dun usuario agochado; cambiado á última versión feita por {{GENDER:$1|[[User:$1|$1]]}}",
- "rollback-success": "Desfixéronse as edicións de $1;\nvolveuse á última edición, feita por $2.",
+ "rollback-success": "Desfixéronse as edicións de {{GENDER:$3|$1}};\nvolveuse á última edición, feita por {{GENDER:$4|$2}}.",
"rollback-success-notify": "Revertéronse as edicións de $1;\nrestaurouse a última revisión de $2. [$3 Mostrar os cambios]",
"sessionfailure-title": "Erro de sesión",
"sessionfailure": "Parece que hai un problema co rexistro da súa sesión;\nesta acción cancelouse como precaución fronte ao secuestro de sesións.\nPrema no botón \"atrás\", volva cargar a páxina da que proviña e inténteo de novo.",
"specialpages-group-changes": "D letschte Änderige un Logbüecher",
"specialpages-group-media": "Medie",
"specialpages-group-users": "Benutzer un Rächt",
- "specialpages-group-highuse": "Syte wo oft bruucht werde",
+ "specialpages-group-highuse": "Syte wo hüüfig bruucht werde",
"specialpages-group-pages": "Lischte vo Syte",
"specialpages-group-pagetools": "Sytewerchzüüg",
"specialpages-group-wiki": "Date un Wärchzyyg",
"rcfilters-filterlist-feedbacklink": "שליחת משוב על המסננים החדשים (בטא)",
"rcfilters-highlightbutton-title": "הבלטת התוצאות",
"rcfilters-highlightmenu-title": "בחירת צבע",
+ "rcfilters-highlightmenu-help": "בחירת צבע להדגשת מאפיין זה",
"rcfilters-filterlist-noresults": "לא נמצאו מסננים",
"rcfilters-filtergroup-registration": "רישום העורכים",
"rcfilters-filter-registered-label": "רשומים",
"logentry-move-move-noredirect": "$1 je {{GENDER:$2|premjestio|premjestila}} stranicu $3 na $4 bez preusmjeravanja",
"logentry-move-move_redir": "$1 je {{GENDER:$2|premjestio|premjestila}} stranicu $3 na $4 preko preusmjeravanja",
"logentry-move-move_redir-noredirect": "$1 je {{GENDER:$2|premjestio|premjestila}} stranicu $3 na $4 preko preusmjeravanja bez ostavljanja preusmjeravanja",
- "logentry-patrol-patrol": "$1 je {{GENDER:$2|označio|označila}} uređivanje $4 stranice $3 pregledanim",
+ "logentry-patrol-patrol": "$1 {{GENDER:$2|označio|označila}} je uređivanje $4 stranice $3 ophođenim",
"logentry-patrol-patrol-auto": "$1 je automatski {{GENDER:$2|označio|označila}} uređivanje $4 stranice $3 pregledanim",
"logentry-newusers-newusers": "$1 je {{GENDER:$2|otvorio|otvorila}} suradnički račun",
"logentry-newusers-create": "$1 je {{GENDER:$2|stvorio|stvorila}} suradnički račun.",
"selfredirect": "<strong>Peringatan:</strong> Anda mengalihkan halaman ini kembali ke halaman semula.\nAnda bisa jadi telah memberikan tujuan pengalihan yang salah, atau telah menyunting halaman yang salah.\nJika Anda mengeklik \"{{int:savearticle}}\" sekali lagi, halaman pengalihan akan dibuat.",
"missingcommenttext": "Harap masukkan komentar di bawah ini.",
"missingcommentheader": "'''Peringatan:''' Anda belum memberikan subjek atau judul untuk komentar Anda. Jika Anda kembali menekan \"{{int:savearticle}}\", suntingan Anda akan disimpan tanpa komentar tersebut.",
- "summary-preview": "Pratayang ringkasan:",
+ "summary-preview": "Pratayang ringkasan suntingan:",
"subject-preview": "Pratayang subjek:",
"previewerrortext": "Kesalahan terjadi saat mencoba memperlihatkan pratayang perubahan Anda.",
"blockedtitle": "Pengguna diblokir",
"search-interwiki-caption": "Proyek lain",
"search-interwiki-default": "Hasil dari $1:",
"search-interwiki-more": "(selanjutnya)",
+ "search-interwiki-more-results": "Hasil lainnya",
"search-relatedarticle": "Berkaitan",
"searchrelated": "berkaitan",
"searchall": "semua",
"editusergroup": "Muat kelompok pengguna",
"editinguser": "Mengubah hak pengguna untuk {{GENDER:$1|pengguna}} <strong>[[User:$1|$1]]</strong> $2",
"viewinguserrights": "Melihat hak pengguna dari {{GENDER:$1|pengguna}} <strong>[[User:$1|$1]]</strong> $2",
- "userrights-editusergroup": "Sunting kelompok pengguna",
- "userrights-viewusergroup": "Lihat kelompok pengguna",
+ "userrights-editusergroup": "Sunting kelompok {{GENDER:$1|pengguna}}",
+ "userrights-viewusergroup": "Lihat kelompok {{GENDER:$1|pengguna}}",
"saveusergroups": "Simpan kelompok {{GENDER:$1|pengguna}}",
"userrights-groupsmember": "Anggota dari:",
"userrights-groupsmember-auto": "Anggota implisit dari:",
"userrights-changeable-col": "Kelompok yang dapat Anda ubah",
"userrights-unchangeable-col": "Kelompok yang tidak dapat Anda ubah",
"userrights-irreversible-marker": "$1*",
+ "userrights-expiry-current": "Udang $1",
+ "userrights-expiry-none": "Tidak usang",
+ "userrights-expiry": "Usang:",
"userrights-expiry-othertime": "Waktu lain:",
"userrights-conflict": "Konflik perubahan hak pengguna! Silakan tinjau ulang dan konfirmasi perubahan Anda.",
"group": "Kelompok:",
"recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (lihat pula [[Special:NewPages|daftar halaman baru]])",
"recentchanges-submit": "Tampilkan",
"rcfilters-activefilters": "Filter aktif",
+ "rcfilters-restore-default-filters": "Kembalikan filter bawaan",
+ "rcfilters-clear-all-filters": "Hapus semua penyaringan",
+ "rcfilters-search-placeholder": "Filter perubahan terbaru (jelajahi atau masukan input)",
+ "rcfilters-invalid-filter": "Penyqringan tidak sah",
+ "rcfilters-empty-filter": "Tidak ada filter aktif. Semua kontribusi ditampilkan.",
+ "rcfilters-filterlist-title": "Penyaringan",
+ "rcfilters-filterlist-feedbacklink": "Berikan umpan balik untuk filter uji coba baru",
+ "rcfilters-highlightmenu-title": "Pilih warna",
+ "rcfilters-filterlist-noresults": "Tidak ada penyaring ditemukan",
+ "rcfilters-filtergroup-registration": "Pendaftaran pengguna",
"rcfilters-filter-registered-label": "Terdaftar",
+ "rcfilters-filter-registered-description": "Penyunting masuk log",
"rcfilters-filter-unregistered-label": "Tidak terdaftar",
+ "rcfilters-filter-unregistered-description": "Penyunting yang tidak masuk log",
+ "rcfilters-filter-editsbyself-label": "Suntingan Anda",
+ "rcfilters-filter-editsbyself-description": "Suntingan oleh Anda",
+ "rcfilters-filter-editsbyother-label": "Suntingan orang lain",
+ "rcfilters-filter-editsbyother-description": "Suntingan dibuat oleh orang lain (bukan Anda)",
"rcfilters-filter-userExpLevel-newcomer-label": "Pendatang baru",
+ "rcfilters-filter-userExpLevel-newcomer-description": "Kurang dari 10 suntingan dan aktivitas selama 4 hari.",
+ "rcfilters-filter-userExpLevel-learner-label": "Pelajar",
+ "rcfilters-filter-userExpLevel-experienced-label": "Pengguna berpengalaman",
+ "rcfilters-filtergroup-automated": "Kontribusi otomatis",
"rcfilters-filter-bots-label": "Bot",
+ "rcfilters-filter-bots-description": "Suntingan yang dibuat dengan perkakas terotomatisasi.",
+ "rcfilters-filter-humans-label": "Manusia (bukan bot)",
+ "rcfilters-filter-humans-description": "Suntingan yang dibuat oleh penyunting manusia.",
+ "rcfilters-filtergroup-significance": "Kepentingan",
"rcfilters-filter-minor-label": "Suntingan kecil",
+ "rcfilters-filter-minor-description": "Suntingan yang ditandai penyunting sebagai suntingan kecil",
+ "rcfilters-filter-major-label": "Suntingan yang bukan suntingan kecil",
+ "rcfilters-filter-major-description": "Suntingan yang ditandai sebagai suntingan kecil",
+ "rcfilters-filtergroup-changetype": "Jenis perubahan",
+ "rcfilters-filter-pageedits-label": "Suntingan halaman",
+ "rcfilters-filter-pageedits-description": "Suntingan pada konten wiki, diskusi, deskripsi kategori....",
+ "rcfilters-filter-newpages-label": "Pembuatan halaman",
+ "rcfilters-filter-newpages-description": "Suntingan yang membuat halaman baru",
"rcfilters-filter-categorization-label": "Perubahan kategori",
+ "rcfilters-filter-categorization-description": "Rekam jejak halaman yang telah ditambahkan atau dihapus dari kategori.",
+ "rcfilters-filter-logactions-label": "Tindakan tercatat",
+ "rcfilters-filter-logactions-description": "Tindakan administratif, pembuatan akun, penghapusan halaman, pengunggahan....",
"rcnotefrom": "Di bawah ini adalah {{PLURAL:$5|perubahan}} sejak <strong>$3, $4</strong> (ditampilkan sampai <strong>$1</strong> perubahan).",
"rclistfrom": "Perlihatkan perubahan terbaru sejak $3 $2",
"rcshowhideminor": "$1 suntingan kecil",
"apisandbox-sending-request": "Mengirim permintaan API...",
"apisandbox-loading-results": "Menerima hasil API...",
"apisandbox-results-error": "Sebuah galat terjadi ketika memuat permintaan respon API: $1.",
+ "apisandbox-request-selectformat-label": "Tampilkan permintaan data sebagai:",
"apisandbox-request-url-label": "URL Permintaan:",
+ "apisandbox-request-json-label": "Meminta JSON:",
"apisandbox-request-time": "Lama permintaan: {{PLURAL:$1|$1 ms}}",
"apisandbox-results-fixtoken": "Perbaiki token dan kirim kembali",
"apisandbox-results-fixtoken-fail": "Gagal mendapatkan token \"$1\".",
"apisandbox-alert-field": "Nilai dalam kolom ini tidak valid.",
"apisandbox-continue": "Lanjutkan",
"apisandbox-continue-clear": "Kosongkan",
+ "apisandbox-param-limit": "Masukan <kbd>max</kbd> untuk menggunakan batas maksimum.",
+ "apisandbox-multivalue-all-namespaces": "$1 (Semua ruang nama)",
+ "apisandbox-multivalue-all-values": "$1 (Semua nilai)",
"booksources": "Sumber buku",
"booksources-search-legend": "Cari di sumber buku",
"booksources-isbn": "ISBN:",
"booksources-search": "Cari",
"booksources-text": "Di bawah ini adalah daftar pranala ke situs lain yang menjual buku baru dan bekas, dan mungkin juga mempunyai informasi lebih lanjut mengenai buku yang sedang Anda cari:",
"booksources-invalid-isbn": "ISBN yang diberikan tampaknya tidak valid; periksa kesalahan penyalinan dari sumber asli.",
+ "magiclink-tracking-rfc": "Halaman menggunakan pranala magis RFC",
+ "magiclink-tracking-pmid": "Halaman menggunakan pranala magis PMID",
+ "magiclink-tracking-isbn": "Halaman yang menggunakan pranala magis ISBN",
"specialloguserlabel": "Pengguna:",
"speciallogtitlelabel": "Target (judul atau{{ns:pengguna}}:nama pengguna untuk pengguna)",
"log": "Catatan (Log)",
"activeusers-intro": "Berikut adalah daftar pengguna yang memiliki suatu bentuk aktivitas selama paling tidak $1 {{PLURAL:$1|hari|hari}} terakhir.",
"activeusers-count": "$1 {{PLURAL:$1|aktivitas|aktivitas}} dalam {{PLURAL:$3|1 hari|$3 hari}} terakhir",
"activeusers-from": "Tampilkan pengguna mulai dari:",
+ "activeusers-groups": "Tampilkan pengguna yang termasuk kelompok:",
+ "activeusers-excludegroups": "Sembunyikan pengguna yang termasuk kelompok:",
"activeusers-noresult": "Pengguna tidak ditemukan.",
"activeusers-submit": "Tampilkan pengguna aktif",
"listgrouprights": "Daftar hak kelompok",
"rollbacklinkcount-morethan": "kembalikan lebih dari $1 {{PLURAL:$1|suntingan|suntingan}}",
"rollbackfailed": "Pengembalian gagal dilakukan",
"rollback-missingparam": "Parameter dibutuhkan ketika diminta tidak tersedia.",
+ "rollback-missingrevision": "Tidak mampu memuat data revisi.",
"cantrollback": "Tidak dapat membatalkan suntingan;\nkontributor terakhir adalah satu-satunya penulis halaman ini.",
"alreadyrolled": "Tidak dapat melakukan pengembalian ke revisi terakhir [[:$1]] oleh [[User:$2|$2]] ([[User talk:$2|bicara]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]);\npengguna lain telah menyunting atau melakukan pengembalian terhadap halaman ini.\n\nSuntingan terakhir dilakukan oleh [[User:$3|$3]] ([[User talk:$3|bicara]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
"editcomment": "Komentar penyuntingan adalah: <em>$1</em>.",
"revertpage": "←Suntingan [[Special:Contributions/$2|$2]] ([[User talk:$2|bicara]]) dibatalkan ke versi terakhir oleh [[User:$1|$1]]",
"revertpage-nouser": "Mengembalikan suntingan oleh (nama pengguna dihapus) ke suntingan terakhir oleh [[User:$1|$1]]",
- "rollback-success": "Pembatalan suntingan oleh $1; dibatalkan ke versi terakhir oleh $2.",
+ "rollback-success": "Pembatalan suntingan oleh {{GENDER:$3|$1}}; dibatalkan ke versi terakhir oleh {{GENDER:$4|$2}}.",
"rollback-success-notify": "Mengembalikan suntingan oleh $1; rubah kembali untuk revisi terakhir oleh $2. [$3 Lihat perubahan]",
"sessionfailure-title": "Kegagalan sesi",
"sessionfailure": "Sepertinya ada masalah dengan sesi log Anda; log Anda telah dibatalkan untuk mencegah pembajakan. Silakan tekan tombol \"kembali\" dan muat kembali halaman sebelum Anda masuk, lalu coba lagi.",
"sorbs": "DNSBL",
"sorbsreason": "Alamat IP anda terdaftar sebagai proxy terbuka di DNSBL.",
"sorbs_create_account_reason": "Alamat IP anda terdaftar sebagai proxy terbuka di DNSBL. Anda tidak dapat membuat akun.",
+ "softblockrangesreason": "Kontribusi anonim tidak diizinkan dari alamat IP Anda ($1). Silakan masuk log.",
"xffblockreason": "Sebuah alamat IP terdapat di kepala X-Forwarded-For, entah milik Anda atau peladen ''proxy'' yang Anda gunakan, telah diblokir. Alasan pemblokirannya adalah: $1",
"cant-see-hidden-user": "Pengguna yang Anda coba blokir telah diblokir dan disembunyikan. Selama Anda tidak memiliki hak sembunyikan pengguna, Anda tidak dapat melihat atau menyunting pemblokiran pengguna ini.",
"ipbblocked": "Anda tidak dapat memblokir atau membuka blokir pengguna lain, karena Anda sendiri diblokir.",
"cant-move-to-user-page": "Anda tidak memiliki hak akses untuk memindahkan halaman ke suatu halaman pengguna (kecuali ke subhalaman pengguna).",
"cant-move-category-page": "Anda tidak memiliki izin untuk memindahkan halaman kategori.",
"cant-move-to-category-page": "Anda tidak memiliki izin untuk memindahkan halaman ke halaman kategori.",
+ "cant-move-subpages": "Anda tidak memiliki izin untuk memindahkan subhalaman",
+ "namespace-nosubpages": "Ruang nama \"$1\" tidak mengizinkan subhalaman.",
"newtitle": "Judul baru:",
"move-watch": "Pantau halaman ini",
"movepagebtn": "Pindahkan halaman",
"newimages-showbots": "Tampilkan unggahan oleh bot",
"newimages-hidepatrolled": "Sembunyikan unggahan yang telah dipatroli",
"noimages": "Tidak ada yang dilihat.",
+ "gallery-slideshow-toggle": "Beralih ''thumbnails''",
"ilsubmit": "Cari",
"bydate": "berdasarkan tanggal",
"sp-newimages-showfrom": "Tampilkan berkas baru dimulai dari $2, $1",
"tag-filter": "Filter [[Special:Tags|tag]]:",
"tag-filter-submit": "Penyaring",
"tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Tag}}]]: $2)",
+ "tag-mw-contentmodelchange": "Perubahan model konten",
+ "tag-mw-contentmodelchange-description": "Perubahan yang [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel mengubah model konten] suatu halaman",
"tags-title": "Tanda",
"tags-intro": "Halaman ini berisi daftar tag yang dapat ditandai oleh perangkat lunak terhadap suatu suntingan berikut artinya.",
"tags-tag": "Nama tag",
"tags-apply-not-allowed-multi": "{{PLURAL:$2|Tag|Tag}} berikut tidak diizinkan untuk diterapkan secara manual: $1",
"tags-update-no-permission": "Anda tidak memiliki izin untuk menambah atau menghapus perubahan tag dari revisi atau entri log individu.",
"tags-update-blocked": "Anda tidak dapat menambahkan atau menghapus perubahan tag ketika {{GENDER:$1|Anda}} sedang diblokir.",
+ "tags-update-add-not-allowed-one": "Tag \"$1\"tidak diizinkan untuk ditambahkan secara manual.",
+ "tags-update-add-not-allowed-multi": "{{PLURAL:$2|tag is|Tag ini}} tidak diizinkan untuk ditambahkan secara manual: $1",
+ "tags-update-remove-not-allowed-one": "Tag \"$1\" tidak diizinkan untuk dihapus.",
"tags-edit-title": "Sunting tag",
"tags-edit-manage-link": "Kelola tag",
"tags-edit-revision-selected": "{{PLURAL:$1|Revisi terpilih|Revisi terpilih}} dari [[:$2]]:",
"htmlform-time-placeholder": "JJ:MM:DD",
"htmlform-datetime-placeholder": "TTTT-BB-HH JJ:MM:DD",
"htmlform-date-invalid": "Nilai yang diberikan tidak dikenali sebagai tanggal. Coba lagi menggunakan format TTTT-BB-HH.",
+ "htmlform-time-invalid": "Nilai yang Anda tentukan bukan waktu yang dikenali. Cobalah menggunakan format HH:MM:SS",
"htmlform-datetime-invalid": "Nilai yang Anda masukkan tidak dikenali sebagai tanggal dan waktu. Coba gunakan format YYYY-MM-DD HH:MM:SS",
"htmlform-date-toolow": "Nilai yang Anda masukkan adalah sebelum tanggal paling dini yang dibolehkan $1",
"htmlform-date-toohigh": "Nilai yang Anda masukkan adalah setelah tanggal paling akhir yang dibolehkan $1",
"htmlform-time-toolow": "Nilai yang Anda tentukan adalah sebelum waktu paling dini yang dibolehkan $1",
"htmlform-time-toohigh": "Nilai yang Anda tentukan adalah setelah waktu paling baru yang dibolehkan $1",
+ "htmlform-datetime-toolow": "Nilai yang Anda tentukan berada sebelum tanggal dan waktu paling awal yang diperbolehkan $1",
+ "htmlform-datetime-toohigh": "Nilai yang Anda masukan telah terlewati setelah tanggal dan waktu terakhir yang diperbolehkan $1.",
"htmlform-title-badnamespace": "[[:$1]] tidak berada dalam ruang nama \"{{ns:$2}}\".",
"htmlform-title-not-creatable": "\"$1\" bukan merupakan judul halaman yang dapat dibuat",
"htmlform-title-not-exists": "$1 tidak ada.",
"logentry-tag-update-logentry": "$1 {{GENDER:$2|memperbarui}} tag pada entri log $5 dari halaman $3 ({{PLURAL:$7|menambahkan}} $6; {{PLURAL:$9|menghapus}} $8)",
"rightsnone": "(tidak ada)",
"revdelete-summary": "ringkasan",
+ "rightslogentry-temporary-group": "$1 (sementara, hingga $2)",
"feedback-adding": "Menambahkan umpan balik ke halaman...",
"feedback-back": "Kembali",
"feedback-bugcheck": "Hebat! Hanya periksa bahwa itu bukan satu di antara [$1 bug yang telah dikenal].",
"pagelang-language": "Bahasa",
"pagelang-use-default": "Gunakan bahasa baku",
"pagelang-select-lang": "Pilih bahasa",
+ "pagelang-reason": "Alasan",
"pagelang-submit": "Kirim",
+ "pagelang-nonexistent-page": "Halaman $1 tidak tersedia",
+ "pagelang-unchanged-language": "Halaman $1 telah di atur ke bahasa $2",
+ "pagelang-unchanged-language-default": "Halaman $1 Telah diatur ke bahasa konten bawaan.",
+ "pagelang-db-failed": "Basis data gagal mengubah bahasa halaman",
"right-pagelang": "Ubah bahasa halaman",
"action-pagelang": "mengubah bahasa halaman",
"log-name-pagelang": "Log perubahan bahasa",
"mw-widgets-titleinput-description-new-page": "halaman belum ada",
"mw-widgets-titleinput-description-redirect": "mengalihkan ke $1",
"mw-widgets-categoryselector-add-category-placeholder": "Tambah sebuah kategori...",
+ "mw-widgets-usersmultiselect-placeholder": "Tambahkan lebih banyak...",
"sessionmanager-tie": "Tidak dapat menggabungkan banyak jenis otentikasi permintaan: $1.",
"sessionprovider-generic": "sesi $1",
"sessionprovider-mediawiki-session-cookiesessionprovider": "sesi berdasarkan kuki",
"sessionprovider-nocookies": "Kuki mungkin dimatikan. Pastikan Anda telah mengaktifkan kuki dan coba mulai kembali.",
"randomrootpage": "Halaman dasar sembarang",
"log-action-filter-block": "Jenis pemblokiran:",
- "log-action-filter-contentmodel": "Jenis modifikasi modelkonten:",
+ "log-action-filter-contentmodel": "Jenis perubahan modelkonten:",
"log-action-filter-delete": "Jenis penghapusan:",
"log-action-filter-import": "Jenis impor:",
"log-action-filter-managetags": "Jenis tindakan manajemen tag:",
"authmanager-link-no-primary": "Kredensial yang diberikan tidak dapat digunakan untuk menautkan akun.",
"authmanager-link-not-in-progress": "Penautan akun tidak dilanjutkan atau data sesi telah hilang. Ulang kembali dari awal.",
"authmanager-authplugin-setpass-failed-title": "Penggantian kata sandi gagal",
+ "authmanager-authplugin-setpass-failed-message": "Plugin autentikasi mencegah pengubahan pasword.",
+ "authmanager-authplugin-create-fail": "Plugin autentikasi mencegah pembuatan akun.",
+ "authmanager-authplugin-setpass-denied": "Plugin autentikasi tidak memperbolehkan mengubah kata kunci.",
"authmanager-authplugin-setpass-bad-domain": "Domain tidak sah.",
"authmanager-autocreate-noperm": "Pembuatan akun otomatis tidak diizinkan.",
"authmanager-autocreate-exception": "Pembuatan akun otomatis dimatikan sementara karena galat sebelumnya.",
"notloggedin": "Accesso non effettuato",
"userlogin-noaccount": "Non hai ancora effettuato la registrazione?",
"userlogin-joinproject": "Registrati su {{SITENAME}}",
- "nologin": "Non hai ancora un accesso? $1.",
+ "nologin": "Non hai un'utenza? $1.",
"nologinlink": "Registrati",
"createaccount": "Registrati",
- "gotaccount": "Hai già un accesso? $1.",
+ "gotaccount": "Hai già un'utenza? $1.",
"gotaccountlink": "Entra",
"userlogin-resetlink": "Hai dimenticato i tuoi dati di accesso?",
"userlogin-resetpassword-link": "Hai dimenticato la password?",
"rcfilters-empty-filter": "Nessun filtro attivo. Sono mostrati tutti i contributi.",
"rcfilters-filterlist-title": "Filtri",
"rcfilters-highlightmenu-title": "Seleziona un colore",
+ "rcfilters-highlightmenu-help": "Seleziona un colore per evidenziare questa proprietà",
"rcfilters-filterlist-noresults": "Nessun filtro trovato",
"rcfilters-filtergroup-registration": "Registrazione utente",
"rcfilters-filter-registered-label": "Registrato",
"វ័ណថារិទ្ធ",
"아라",
"Macofe",
- "Dcljr"
+ "Dcljr",
+ "Aefgh39622"
]
},
"tog-underline": "គូសបន្ទាត់ក្រោមតំណភ្ជាប់៖",
"tog-enotifminoredits": "ផ្ញើអ៊ីមែលមកខ្ញុំពេលមានបន្លាស់ប្ដូរតិចតួចលើទំព័រឬឯកសារផងដែរ",
"tog-enotifrevealaddr": "បង្ហាញអាសយដ្ឋានអ៊ីមែលរបស់ខ្ញុំក្នុងអ៊ីមែលក្រើនរំលឹក",
"tog-shownumberswatching": "បង្ហាញចំនួនអ្នកប្រើប្រាស់ដែលតាមដានទំព័រនេះ",
- "tog-oldsig": "ហត្ថលេខាមានហើយ៖",
+ "tog-oldsig": "á\9e á\9e\8fá\9f\92á\9e\90á\9e\9bá\9f\81á\9e\81á\9e¶á\9e\8aá\9f\82á\9e\9bá\9e¢á\9f\92á\9e\93á\9e\80á\9e\98á\9e¶á\9e\93á\9e á\9e¾á\9e\99á\9f\96",
"tog-fancysig": "ចុះហត្ថលេខាជាអត្ថបទវិគី (ដោយគ្មានតំណភ្ជាប់ស្វ័យប្រវត្តិ)",
"tog-uselivepreview": "ប្រើប្រាស់ការមើលមុនរហ័ស",
"tog-forceeditsummary": "សូមរំលឹកខ្ញុំកាលបើខ្ញុំទុកប្រអប់ចំណារពន្យល់ឱ្យនៅទំនេរ",
"tog-showhiddencats": "បង្ហាញចំណាត់ថ្នាក់ក្រុមដែលត្រូវបានលាក់",
"tog-norollbackdiff": "បំភ្លេចភាពខុសគ្នាបន្ទាប់ពីអនុវត្តការស្ដារវិញ",
"tog-useeditwarning": "សូមព្រមានខ្ញុំ ពេលដែលខ្ញុំចាកចេញពីទំព័រកែប្រែដោយមិនបានរក្សាទុកបន្លាស់ប្ដូរនានា",
- "tog-prefershttps": "ប្រើប្រាស់ការតភ្ជាប់មានសុវត្ថិភាពជានិច្ចពេលកត់ឈ្មោះចូល",
+ "tog-prefershttps": "á\9e\94á\9f\92á\9e\9aá\9e¾á\9e\94á\9f\92á\9e\9aá\9e¶á\9e\9fá\9f\8bá\9e\80á\9e¶á\9e\9aá\9e\8fá\9e\97á\9f\92á\9e\87á\9e¶á\9e\94á\9f\8bá\9e\8aá\9f\82á\9e\9bá\9e\98á\9e¶á\9e\93á\9e\9fá\9e»á\9e\9cá\9e\8fá\9f\92á\9e\90á\9e·á\9e\97á\9e¶á\9e\96á\9e\87á\9e¶á\9e\93á\9e·á\9e\85á\9f\92á\9e\85á\9e\96á\9f\81á\9e\9bá\9e\80á\9e\8fá\9f\8bá\9e\88á\9f\92á\9e\98á\9f\84á\9f\87á\9e\85á\9e¼á\9e\9b",
"underline-always": "ជានិច្ច",
"underline-never": "កុំឲ្យសោះ",
"underline-default": "តាមលំនាំដើមនៃកម្មវិធីរុករក",
"morenotlisted": "បញ្ជីនេះមិនទាន់ពេញលេញទេ។",
"mypage": "ទំព័រ",
"mytalk": "ការពិភាក្សា",
- "anontalk": "á\9e\91á\9f\86á\9e\96á\9f\90á\9e\9aá\9e\96á\9e·á\9e\97á\9e¶á\9e\80á\9f\92á\9e\9fá\9e¶á\9e\9fá\9f\86á\9e\9aá\9e¶á\9e\94á\9f\8b IP á\9e\93á\9f\81á\9f\87",
+ "anontalk": "á\9e\80á\9e¶á\9e\9aâ\80\8bá\9e\96á\9e·á\9e\97á\9e¶á\9e\80á\9f\92á\9e\9fá\9e¶",
"navigation": "ការណែនាំ",
"and": " និង",
"qbfind": "ស្វែងរក",
"nocookieslogin": "{{SITENAME}}ប្រើខូឃីដើម្បីកត់ឈ្មោះចូល។\n\nអ្នកបានជ្រើសមិនប្រើខូឃី។\n\nសូមជ្រើសប្រើខូឃីវិញ រួចព្យាយាមម្តងទៀត។",
"nocookiesforlogin": "{{int:nocookieslogin}}",
"noname": "អ្នកមិនបានផ្ដល់អត្តនាមត្រឹមត្រូវទេ។",
- "loginsuccesstitle": "á\9e\80á\9e\8fá\9f\8bá\9e\88á\9f\92á\9e\98á\9f\84á\9f\87á\9e\85á\9e¼á\9e\9bá\9e\94á\9e¶á\9e\93á\9e\9fá\9e\98á\9f\92á\9e\9aá\9f\81á\9e\85",
+ "loginsuccesstitle": "á\9e\94á\9e¶á\9e\93á\9e\80á\9e\8fá\9f\8bá\9e\88á\9f\92á\9e\98á\9f\84á\9f\87á\9e\85á\9e¼á\9e\9bá\9e á\9e¾á\9e\99",
"loginsuccess": "'''ពេលនេះអ្នកបានកត់ឈ្មោះចូល{{SITENAME}}ដោយប្រើឈ្មោះ \"$1\"ហើយ។'''",
"nosuchuser": "មិនមានអ្នកប្រើដែលមានឈ្មោះ \"$1\" ទេ។\n\nសូមពិនិត្យក្រែងលោមានកំហុសអក្ខរាវិរុទ្ធឬ [[Special:CreateAccount|បង្កើតគណនីថ្មី]]។",
"nosuchusershort": "គ្មានអ្នកប្រើដែលមានឈ្មោះ $1\" ទេ។\n\nសូមពិនិត្យអក្ខរាវិរុទ្ធរបស់អ្នក ។",
"newpassword": "ពាក្យសម្ងាត់ថ្មី៖",
"retypenew": "សូមវាយពាក្យសម្ងាត់ថ្មីម្តងទៀត៖",
"resetpass_submit": "ដាក់ប្រើពាក្យសម្ងាត់និងកត់ឈ្មោះចូល",
- "changepassword-success": "á\9e\96á\9e¶á\9e\80á\9f\92á\9e\99á\9e\9fá\9e\98á\9f\92á\9e\84á\9e¶á\9e\8fá\9f\8bá\9e\9aá\9e\94á\9e\9fá\9f\8bá\9e¢á\9f\92á\9e\93á\9e\80á\9e\8fá\9f\92á\9e\9aá\9e¼á\9e\9cá\9e\94á\9e¶á\9e\93á\9e\95á\9f\92á\9e\9bá\9e¶á\9e\9fá\9f\8bá\9e\94á\9f\92á\9e\8fá\9e¼á\9e\9aá\9e\94á\9e¶á\9e\93á\9e\9fá\9f\86á\9e\9aá\9f\81á\9e\85á\9e á\9e¾á\9e\99!",
+ "changepassword-success": "ពាក្យសម្ងាត់របស់អ្នកត្រូវបានផ្លាស់ប្តូរហើយ!",
"changepassword-throttled": "អ្នកបានព្យាយាមកត់ឈ្មោះចូលជាប់ៗគ្នាច្រើនដងពេកហើយ។\nសូមរង់ចាំរយៈពេល$1 មុនពេលសាកល្បងម្ដងទៀត។",
"resetpass_forbidden": "ពាក្យសម្ងាត់មិនអាចផ្លាស់ប្តូរបានទេ",
"resetpass-no-info": "អ្នកចាំបាច់ត្រូវតែកត់ឈ្មោះចូល ដើម្បីចូលទៅកាន់ទំព័រនេះដោយផ្ទាល់។",
"passwordreset-emaildisabled": "មុខងារអ៊ីមែលត្រូវបានបិទមិនអោយប្រើនៅលើវិគីនេះ។",
"passwordreset-username": "អត្តនាម៖",
"passwordreset-domain": "ដូម៉ែន៖",
- "passwordreset-capture": "មើលអ៊ីមែលលទ្ធផល?",
- "passwordreset-capture-help": "ប្រសិនបើអ្នកគូសធីកប្រអប់នេះ អ៊ីមែល (ដែលមានពាក្យសម្ងាត់បណ្ដោះអាសន្ន) មិនត្រូវបានបង្ហាញដូចគ្នានឹងអ៊ីមែលដែលនឹងត្រូវផ្ញើទៅទៅកាន់អ្នកប្រើប្រាស់ដែរ។",
"passwordreset-email": "អាសយដ្ឋានអ៊ីមែល៖",
"passwordreset-emailtitle": "ព័ត៌មានលំអិតពីគណនីនៅលើ {{SITENAME}}",
"passwordreset-emailtext-ip": "មាននរណាម្នាក់ (ប្រហែលជាខ្លួនអ្នកផ្ទាល់, មកពីអាស័យដ្ឋាន IP $1) បានស្នើសុំស្ដារពាក្យសម្ងាត់របស់អ្នកសម្រាប់ {{SITENAME}} ($4)។ {{PLURAL:$3|គណនី|គណនី}}អ្នកប្រើប្រាស់ដូចតទៅនេះ\nមានជាប់ទាក់ទិននឹងអាសយដ្ឋានអ៊ីមែលនេះ៖\n\n$2\n\n{{PLURAL:$3|ពាក្យសម្ងាត់បណ្ដោះអាសន្ននេះ|ពាក្យសម្ងាត់បណ្ដោះអាសន្នទាំងនេះ}} និងហួសសុពលភាពក្នុងរយៈពេល {{PLURAL:$5|មួយថ្ងៃ|$5 ថ្ងៃ}}។\nយកល្អអ្នកគួរតែកត់ឈ្មោះចូលរួចជ្រើសរើសពាក្យសម្ងាត់ថ្មីមួយ។ ប្រសិនបើមាននរណាម្នាក់ផ្សេងធ្វើការស្នើសុំនេះ \nឬប្រសិនបើអ្នកនឹកឃើញពាក្យសម្ងាត់ដើមរបស់អ្នក ហើយអ្នកមិនប្រាថ្នាផ្លាស់ប្ដូរវាទៀតទេនោះ អ្នកគ្រាន់តែ\nបំភ្លេចអំពីសារមួយនេះ ហើយបន្តប្រើប្រាស់ពាក្យសម្ងាត់ចាស់របស់អ្នកទៅបានហើយ។",
"saveprefs": "រក្សាទុក",
"restoreprefs": "ស្ដារការកំណត់ទាំងអស់ទៅលំនាំដើម (គ្រប់ផ្នែកទាំងអស់)",
"prefs-editing": "កំណែប្រែ",
- "rows": "ជួរដេក៖",
- "columns": "ជួរឈរ៖",
"searchresultshead": "ស្វែងរក",
"stub-threshold": "ទំហំអប្បបរមាសម្រាប់ដាក់ជាទម្រង់ទំព័រកំប៉ិចកំប៉ុក($1)៖",
"stub-threshold-sample-link": "គំរូ",
"userrights-reason": "មូលហេតុ៖",
"userrights-no-interwiki": "អ្នកគ្មានការអនុញ្ញាតកែប្រែសិទ្ធិរបស់អ្នកប្រើប្រាស់លើវិគីផ្សេងទេ។",
"userrights-nodatabase": "មូលដ្ឋានទិន្នន័យ $1 មិនមាន ឬ ស្ថិតនៅខាងក្រៅ។",
- "userrights-nologin": "អ្នកត្រូវតែ [[Special:UserLogin|កត់ឈ្មោះចូល]]ដោយប្រើគណនីអ្នកអភិបាលដើម្បីផ្ដល់សិទ្ធិឱ្យអ្នកប្រើប្រាស់។",
- "userrights-notallowed": "លោកអ្នកគ្មានការអនុញ្ញាតដើម្បីបន្ថែមឬដកសិទ្ធិរបស់អ្នកប្រើប្រាស់ដទៃទេ។",
"userrights-changeable-col": "ក្រុមនានាដែលអ្នកអាចផ្លាស់ប្ដូរបាន",
"userrights-unchangeable-col": "ក្រុមនានាដែលអ្នកមិនអាចផ្លាស់ប្ដូរបាន",
"userrights-conflict": "មានទំនាស់អំពីការកែប្រែសិទ្ធិអ្នកប្រើប្រាស់! សូមត្រួតពិនិត្យឡើងវិញរួចអះអាងពីការកែប្រែរបស់អ្នក។",
- "userrights-removed-self": "អ្នកបានដកសិទ្ធិខ្លួនឯងបានសម្រេចហើយ។ ហេតុនេះ អ្នកមិនអាចចូលមើលទំព័រនេះតទៅទៀតទេ។",
"group": "ក្រុម៖",
"group-user": "អ្នកប្រើប្រាស់",
"group-autoconfirmed": "អ្នកប្រើប្រាស់ទទួលស្គាល់ដោយស្វ័យប្រវត្តិ",
"right-siteadmin": "ចាក់សោនិងបើកសោមូលដ្ឋានទិន្នន័យ",
"right-override-export-depth": "នាំចេញទំព័ររួមទាំងទំព័រដែលមានភ្ជាប់តំណភ្ជាប់រហូតដល់លំដាប់ទី៥",
"right-sendemail": "ផ្ញើអ៊ីមែលទៅកាន់អ្នកប្រើដទៃ",
- "right-passwordreset": "មើលអ៊ីមែលសំរាប់កំណត់ពាក្យសម្ងាត់ឡើងវិញ",
"newuserlogpage": "កំណត់ហេតុនៃការបង្កើតគណនី",
"newuserlogpagetext": "នេះជាកំណត់ហេតុនៃការបង្កើតអ្នកប្រើប្រាស់។",
"rightslog": "កំណត់ហេតុនៃការប្តូរសិទ្ធិអ្នកប្រើប្រាស់",
"rightslogtext": "នេះជាកំណត់ហេតុនៃបំលាស់ប្ដូរចំពោះកាប្ដូរក្រុមសមាជិកភាពរបស់អ្នកប្រើប្រាស់។",
"action-read": "អានទំព័រនេះ",
"action-edit": "កែប្រែទំព័រនេះ",
- "action-createpage": "á\9e\94á\9e\84á\9f\92á\9e\80á\9e¾á\9e\8fá\9e\91á\9f\86á\9e\96á\9f\90á\9e\9aá\9e\93á\9e¶á\9e\93á\9e¶",
- "action-createtalk": "á\9e\94á\9e\84á\9f\92á\9e\80á\9e¾á\9e\8fá\9e\91á\9f\86á\9e\96á\9f\90á\9e\9aá\9e\96á\9e·á\9e\97á\9e¶á\9e\80á\9f\92á\9e\9fá\9e¶á\9e\93á\9e¶á\9e\93á\9e¶",
+ "action-createpage": "á\9e\94á\9e\84á\9f\92á\9e\80á\9e¾á\9e\8fá\9e\91á\9f\86á\9e\96á\9f\90á\9e\9aá\9e\93á\9f\81á\9f\87",
+ "action-createtalk": "á\9e\94á\9e\84á\9f\92á\9e\80á\9e¾á\9e\8fá\9e\91á\9f\86á\9e\96á\9f\90á\9e\9aá\9e\96á\9e·á\9e\97á\9e¶á\9e\80á\9f\92á\9e\9fá\9e¶á\9e\93á\9f\81á\9f\87",
"action-createaccount": "បង្កើតគណនីអ្នកប្រើប្រាស់នេះ",
"action-history": "មើលប្រវត្តិទំព័រនេះ",
"action-minoredit": "ចំណាំកំណែប្រែនេះថាជាកំណែប្រែតិចតួច",
"feedback-thanks-title": "សូមអរគុណ!",
"searchsuggest-search": "ស្វែងរក",
"searchsuggest-containing": "ដែលមានពាក្យ...",
- "api-error-badaccess-groups": "អ្នកគ្មានការអនុញ្ញាតអោយផ្ទុកឯកសារឡើងទៅក្នុងវិគីនេះទេ។",
- "api-error-empty-file": "ឯកសារដែលអ្នកបានដាក់ស្នើគឺទទេ។",
"api-error-emptypage": "ការអនុញ្ញាតអោយបង្កើតទំព័រថ្មីដែលគ្មានសរសេរអ្វីទេ",
- "api-error-fileexists-forbidden": "ឯកសារដែលមានឈ្មោះ \"$1\" មានរួចហើយ ហើយមិនអាចសរសេរជាន់ពីលើបានទេ។",
- "api-error-fileexists-shared-forbidden": "ឯកសារដែលមានឈ្មោះ \"$1\" មានរួចហើយនៅក្នុងថតឯកសាររួម ហើយមិនអាចសរសេរជាន់ពីលើបានទេ។",
- "api-error-file-too-large": "ឯកសារដែលអ្នកបានដាក់ស្នើធំពេកហើយ។",
- "api-error-filename-tooshort": "ឈ្មោះឯកសារខ្លីពេកហើយ។",
- "api-error-filetype-banned": "ឯកសារប្រភេទនេះត្រូវបានហាមប្រាម។",
- "api-error-filetype-banned-type": "$1 {{PLURAL:$4|មិនមែនជាប្រភេទឯកសារដែលត្រូវបានគេអនុញ្ញាតទេ|មិនមែនជាប្រភេទឯកសារដែលត្រូវបានគេអនុញ្ញាតទេ}}។\n{{PLURAL:$3|ប្រភេទឯកសារ|ប្រភេទឯកសារ}}ដែលត្រូវបានគេអនុញ្ញាតគឺ $2 ។",
- "api-error-filetype-missing": "ឈ្មោះឯកសារបាត់កន្ទុយ។",
- "api-error-http": "បញ្ហាខាងក្នុង៖ មិនអាចភ្ជាប់ទោកាន់ម៉ាស៊ីនបំរើការ។",
- "api-error-illegal-filename": "មិនអនុញ្ញាតអោយប្រើឈ្មោះឯកសារនេះ។",
- "api-error-internal-error": "បញ្ហាខាងក្នុង៖ មានបញ្ហាណាមួយកើតឡើងពេលកំពុងដំណើរការផ្ទុកឯកសារអ្នកឡើងទៅក្នុងវិគី។",
- "api-error-missingresult": "បញ្ហាខាងក្នុង៖ មិនអាចកំណត់បានថាការថតចំលងបានសំរេចទេ។",
- "api-error-mustbeloggedin": "អ្នកត្រូវតែកត់ឈ្មោះចូលដើម្បីផ្ទុកឯកសារឡើង។",
- "api-error-ok-but-empty": "បញ្ហាខាងក្នុង៖ គ្មានចម្លើយពីម៉ាស៊ីនបម្រើការ។",
- "api-error-overwrite": "មិនអនុញ្ញាតអោយសរសេរជាន់ពីលើឯកសារដែលមានស្រាប់ហើយ។",
- "api-error-timeout": "ម៉ាស៊ីនបំរើការមិនបានឆ្លើយតបក្នុងរយៈពេលដែលយើងរំពឹងទុក។",
- "api-error-unclassified": "បញ្ហាមិនស្គាល់មួយបានកើតឡើង។",
- "api-error-unknown-code": "បញ្ហាមិនស្គាល់៖ \"$1\" ។",
- "api-error-unknown-error": "បញ្ហាខាងក្នុង៖ មានបញ្ហាមិនស្រួលពេលកំពុងព្យាយាមផ្ទុកឯកសាររបស់អ្នកឡើង។",
"api-error-unknown-warning": "ការព្រមានមិនស្គាល់៖ \"$1 ។",
"api-error-unknownerror": "បញ្ហាមិនស្គាល់៖ \"$1\" ។",
- "api-error-uploaddisabled": "ការផ្ទុកឡើងត្រូវបានបិទមិនអោយប្រើនៅលើវិគីនេះទេ។",
- "api-error-verification-error": "ឯកសារនេះប្រហែលជាខូច ឯមានកន្ទុយមិនត្រឹមត្រូវ។",
"duration-seconds": "$1 {{PLURAL:$1|វិនាទី|វិនាទី}}",
"duration-minutes": "$1 {{PLURAL:$1|នាទី|នាទី}}",
"duration-hours": "$1 {{PLURAL:$1|ម៉ោង|ម៉ោង}}",
"apisandbox-alert-field": "이 필드의 값이 유효하지 않습니다.",
"apisandbox-continue": "계속",
"apisandbox-continue-clear": "지우기",
- "apisandbox-continue-help": "{{int:apisandbox-continue}}은(는) 마지막 요청을 [https://www.mediawiki.org/wiki/API:Query#Continuing_queries 지속]합니다. {{int:apisandbox-continue-clear}}은(는) 지속 관련 변수들을 삭제합니다.",
+ "apisandbox-continue-help": "{{int:apisandbox-continue}}은 마지막 요청을 [https://www.mediawiki.org/wiki/API:Query#Continuing_queries 계속]합니다. {{int:apisandbox-continue-clear}}는 계속 관련 변수들을 삭제합니다.",
"apisandbox-param-limit": "최대 한계치를 사용하려면 <kbd>max</kbd>를 입력하십시오.",
"apisandbox-multivalue-all-namespaces": "$1 (모든 이름공간)",
"apisandbox-multivalue-all-values": "$1 (모든 값)",
"imagenocrossnamespace": "파일을 파일이 아닌 이름공간으로 이동할 수 없습니다.",
"nonfile-cannot-move-to-file": "파일이 아닌 문서를 파일 이름공간으로 이동할 수 없습니다.",
"imagetypemismatch": "새 파일의 확장자가 원래의 확장자와 일치하지 않습니다.",
- "imageinvalidfilename": "새 파일 이름이 잘못되었습니다.",
+ "imageinvalidfilename": "대상 파일 이름이 잘못되었습니다.",
"fix-double-redirects": "원래 제목을 가리키는 넘겨주기를 새로 고침",
"move-leave-redirect": "이동한 뒤 넘겨주기를 남기기",
"protectedpagemovewarning": "<strong>경고:</strong> 이 문서는 관리자만 이동할 수 있도록 보호되어 있습니다.\n최근 기록을 참조를 위해 아래에 제공합니다:",
"rcfilters-invalid-filter": "Nieprawidłowy filtr",
"rcfilters-empty-filter": "Brak aktywnych filtrów. Wyświetlane są wszystkie zmiany.",
"rcfilters-filterlist-title": "Filtry",
+ "rcfilters-filterlist-feedbacklink": "Podziel się swoją opinią na temat tych nowych (beta) filtrów",
"rcfilters-highlightbutton-title": "Podświetl wyniki",
"rcfilters-highlightmenu-title": "Wybierz kolor",
"rcfilters-filterlist-noresults": "Nie znaleziono filtrów",
"rcfilters-filtergroup-registration": "Rejestracja użytkownika",
- "rcfilters-filter-registered-label": "Zarejestrowany",
+ "rcfilters-filter-registered-label": "Zarejestrowani",
"rcfilters-filter-registered-description": "Zalogowani edytorzy.",
- "rcfilters-filter-unregistered-label": "Niezarejestrowany",
+ "rcfilters-filter-unregistered-label": "Niezarejestrowani",
"rcfilters-filter-unregistered-description": "Niezalogowani",
"rcfilters-filtergroup-authorship": "Autorstwo edycji",
"rcfilters-filter-editsbyself-label": "Moje edycje",
"rcfilters-filter-pageedits-label": "Edycje strony",
"rcfilters-filter-pageedits-description": "Edycje treści, stron dyskusji, opisów kategorii...",
"rcfilters-filter-newpages-label": "Tworzenie stron",
- "rcfilters-filter-newpages-description": "Zmiany, prowadzące do utworzenia nowych stron.",
+ "rcfilters-filter-newpages-description": "Zmiany prowadzące do utworzenia nowych stron.",
"rcfilters-filter-categorization-label": "Zmiany kategorii",
"rcfilters-filter-categorization-description": "Dodanie lub usunięcie strony z kategorii",
"rcfilters-filter-logactions-label": "Działania rejestrowane",
"rcfilters-filterlist-feedbacklink": "Caption for the link to the feedback page about the filters beta feature.",
"rcfilters-highlightbutton-title": "Title for the highlight button used to toggle the highlight feature on and off.",
"rcfilters-highlightmenu-title": "Title for the highlight menu used to select the highlight color for an individual filter.",
+ "rcfilters-highlightmenu-help": "Tooltip for the highlight menu for individual filters.",
"rcfilters-filterlist-noresults": "Message showing no results found for searching a filter.",
"rcfilters-filtergroup-registration": "Title for the filter group for editor registration type.",
"rcfilters-filter-registered-label": "Label for the filter for showing edits made by logged-in users.\n{{Identical|Registered}}",
"missing-revision": "Версия $1 страницы «{{FULLPAGENAME}}» не существует.\n\nЭто обычно бывает, если последовать по устаревшей ссылке на страницу, которая была удалена.\nПодробности могут быть в [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале удалений].",
"userpage-userdoesnotexist": "Учётной записи «<nowiki>$1</nowiki>» не существует. Убедитесь, что вы действительно желаете создать или изменить эту страницу.",
"userpage-userdoesnotexist-view": "Не зарегистрировано учётной записи «$1».",
- "blocked-notice-logextract": "Этот участник в данный момент заблокирован.\nНиже приведена последняя запись из журнала блокировок:",
+ "blocked-notice-logextract": "{{GENDER:$1|Этот участник|Эта участница}} в данный момент {{GENDER:$1|заблокирован|заблокирована}}.\nНиже приведена последняя запись из журнала блокировок:",
"clearyourcache": "<strong>Замечание.</strong> Возможно, после сохранения вам придётся очистить кэш своего браузера, чтобы увидеть изменения.\n* <strong>Firefox / Safari:</strong> Удерживая клавишу <em>Shift</em>, нажмите на панели инструментов <em>Обновить</em> либо нажмите <em>Ctrl-F5</em> или <em>Ctrl-R</em> (<em>⌘-R</em> на Mac)\n* <strong>Google Chrome:</strong> Нажмите <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> на Mac)\n* <strong>Internet Explorer:</strong> Удерживая <em>Ctrl</em>, нажмите <em>Обновить</em> либо нажмите <em>Ctrl-F5</em>\n* <strong>Opera:</strong> Перейдите в <em>Menu → Настройки</em> (<em>Opera → Настройки</em> на Mac), а затем <em>Безопасность → Очистить историю посещений → Кэшированные изображения и файлы</em>",
"usercssyoucanpreview": "'''Подсказка.''' Нажмите кнопку «{{int:showpreview}}», чтобы проверить свой новый CSS-файл перед сохранением.",
"userjsyoucanpreview": "'''Подсказка.''' Нажмите кнопку «{{int:showpreview}}», чтобы проверить свой новый JS-файл перед сохранением.",
"saveusergroups": "Сохранить группы {{GENDER:$1|участника|участницы}}",
"userrights-groupsmember": "Состоит в группах:",
"userrights-groupsmember-auto": "Неявно состоит в группах:",
- "userrights-groups-help": "Вы можете изменить группы, в которые входит этот участник.\n* Если около названия группы стоит отметка — участник входит в эту группу.\n* Если отметка не стоит — участник не входит в эту группу.\n* Символ * указывает на то, что вы не сможете удалить участника из группы, если добавите его в неё (или наоборот).\n* Символ # указывает на то, что вы можете только отложить время истечения этой группы, вы не можете перенести его на более ранний срок.",
+ "userrights-groups-help": "Вы можете изменить группы, в которые входит {{GENDER:$1|этот участник|эта участница}}.\n* Если около названия группы стоит отметка — {{GENDER:$1|участник|участница}} входит в эту группу.\n* Если отметка не стоит — {{GENDER:$1|участник|участница}} не входит в эту группу.\n* Символ * указывает на то, что вы не сможете удалить {{GENDER:$1|участника|участницу}} из группы, если добавите {{GENDER:$1|его|её}} в неё (или наоборот).\n* Символ # указывает на то, что вы можете только отложить время истечения этой группы, вы не можете перенести его на более ранний срок.",
"userrights-reason": "Причина:",
"userrights-no-interwiki": "У вас нет разрешения изменять права участников в других вики.",
"userrights-nodatabase": "База данных $1 не существует или расположена не локально.",
"rcfilters-filterlist-feedbacklink": "Оставить отзыв о новых (бета) фильтрах",
"rcfilters-highlightbutton-title": "Выделить результаты",
"rcfilters-highlightmenu-title": "Выберите цвет",
+ "rcfilters-highlightmenu-help": "Выберите цвет, чтобы подсветить это свойство",
"rcfilters-filterlist-noresults": "Фильтры не найдены",
"rcfilters-filtergroup-registration": "Регистрация участников",
"rcfilters-filter-registered-label": "Зарегистрированные",
"rcfilters-filterlist-feedbacklink": "Podajte povratne informacije o novih (preizkusnih) filtrih",
"rcfilters-highlightbutton-title": "Označi rezultate",
"rcfilters-highlightmenu-title": "Izberite barvo",
+ "rcfilters-highlightmenu-help": "Izberite barvo za označitev te lastnosti",
"rcfilters-filterlist-noresults": "Nismo našli nobenega filtra",
"rcfilters-filtergroup-registration": "Registracija uporabnika",
"rcfilters-filter-registered-label": "Registriran",
"htmlform-user-not-exists": "<strong>$1</strong> не постоји.",
"htmlform-user-not-valid": "<strong>$1</strong> није исправно корисничко име.",
"logentry-delete-delete": "$1 је {{GENDER:$2|обрисао|обрисала}} страницу $3",
- "logentry-delete-delete_redir": "$1 је {{GENDER:$2|обрисао|обрисала}} преусмерење $3 преснимавањем",
+ "logentry-delete-delete_redir": "$1 је {{GENDER:$2|обрисао|обрисала}} преусмерење $3 преписивањем",
"logentry-delete-restore": "$1 је {{GENDER:$2|вратио|вратила}} страницу $3",
"logentry-delete-event": "$1 је {{GENDER:$2|променио|променила}} видљивост {{PLURAL:$5|1=догађаја|$5 догађаја}} у дневнику $3: $4",
"logentry-delete-revision": "$1 је {{GENDER:$2|променио|променила}} видљивост {{PLURAL:$5|1=једне измене|$5 измене|$5 измена}} на страници $3: $4",
"log-action-filter-managetags-activate": "активирање ознаке",
"log-action-filter-managetags-deactivate": "деактивирање ознаке",
"log-action-filter-move-move": "премештање без преснимавања преусмерења",
- "log-action-filter-move-move_redir": "пÑ\80емеÑ\88Ñ\82аÑ\9aе Ñ\81а пÑ\80еÑ\81нимавањем преусмерења",
+ "log-action-filter-move-move_redir": "Ð\9fÑ\80емеÑ\88Ñ\82аÑ\9aе Ñ\81а пÑ\80епиÑ\81ивањем преусмерења",
"log-action-filter-newusers-create": "отворио анониман корисник",
"log-action-filter-newusers-create2": "отворио регистрован корисник",
"log-action-filter-newusers-autocreate": "аутоматски отворен",
"htmlform-user-not-exists": "<strong>$1</strong> ne postoji.",
"htmlform-user-not-valid": "<strong>$1</strong> nije ispravno korisničko ime.",
"logentry-delete-delete": "$1 je {{GENDER:$2|obrisao|obrisala}} stranicu $3",
+ "logentry-delete-delete_redir": "$1 je {{GENDER:$2|obrisao|obrisala}} preusmerenje $3 prepisivanjem",
"logentry-delete-restore": "$1 je {{GENDER:$2|vratio|vratila}} stranicu $3",
"logentry-delete-event": "$1 je {{GENDER:$2|promenio|promenila}} vidljivost {{PLURAL:$5|1=događaja|$5 događaja}} u dnevniku $3: $4",
"logentry-delete-revision": "$1 je {{GENDER:$2|promenio|promenila}} vidljivost {{PLURAL:$5|1=jedne izmene|$5 izmene|$5 izmena}} na stranici $3: $4",
"mw-widgets-titleinput-description-redirect": "preusmerava na $1",
"randomrootpage": "Slučajna korenska stranica",
"log-action-filter-all": "Sve",
+ "log-action-filter-move-move_redir": "Premeštanje sa prepisivanjem preusmerenja",
"log-action-filter-upload-upload": "Novo otpremanje",
"authmanager-email-label": "Imejl",
"authmanager-email-help": "Imejl adresa",
"selfredirect": "<strong>Попередження:</strong> Ви створюєте перенаправлення на цю ж сторінку.\nВи могли вказати невірну цільову сторінку, або ж редагуєте хибну сторінку.\nЯкщо Ви натиснете \"{{int:savearticle}}\" ще раз, перенаправлення буде створено.",
"missingcommenttext": "Будь ласка, введіть нижче ваше повідомлення.",
"missingcommentheader": "<strong>Нагадування</strong>: Ви не вказали тему для цього коментаря.\nНатиснувши кнопку «{{int:savearticle}}» ще раз, Ви збережете редагування без заголовка.",
- "summary-preview": "Ð\9eпиÑ\81 бÑ\83де:",
- "subject-preview": "Тема бÑ\83де:",
+ "summary-preview": "Ð\9fопеÑ\80еднÑ\96й пеÑ\80еглÑ\8fд опиÑ\81Ñ\83 Ñ\80едагÑ\83ваннÑ\8f:",
+ "subject-preview": "Ð\9fопеÑ\80еднÑ\96й пеÑ\80еглÑ\8fд Ñ\82еми:",
"previewerrortext": "Сталася помилка при спробі попереднього перегляду Ваших змін.",
"blockedtitle": "Користувача заблоковано",
"blockedtext": "<strong>Ваш обліковий запис або IP-адреса заблоковані.</strong>\n\nБлокування виконане адміністратором $1.\nПричина блокування: <em>$2</em>.\n\n* Початок блокування: $8\n* Закінчення блокування: $6\n* Діапазон блокування: $7\n\nВи можете надіслати листа користувачеві $1 або будь-якому іншому [[{{MediaWiki:Grouppage-sysop}}|адміністратору]], щоб обговорити блокування.\n\nЗверніть увагу, що ви не зможете надіслати листа адміністратору, якщо ви не зареєстровані або не підтвердили свою електронну адресу в [[Special:Preferences|особистих налаштуваннях]], а також якщо вам було заборонено надсилати листи при блокуванні.\n\nВаша поточна IP-адреса — $3, ідентифікатор блокування — #$5. Будь ласка, зазначайте ці дані у своїх запитах.",
"missing-revision": "Версія #$1 сторінки «{{FULLPAGENAME}}» не існує.\n\nІмовірно, Ви перейшли за застарілим посиланням на вилучену сторінку.\nПодробиці можна дізнатися з [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журналу вилучень].",
"userpage-userdoesnotexist": "Користувач під назвою \"<nowiki>$1</nowiki>\" не зареєстрований. Переконайтеся, що ви хочете створити/редагувати цю сторінку.",
"userpage-userdoesnotexist-view": "Обліковий запис користувача «$1» не зареєстровано.",
- "blocked-notice-logextract": "Цей користувач наразі заблокований.\nОстанній запис у журналі блокувань такий:",
+ "blocked-notice-logextract": "{{GENDER:$1|Цей користувач|Ця користувачка}} наразі {{GENDER:$1|заблокований|заблокована}}.\nОстанній запис у журналі блокувань такий:",
"clearyourcache": "<strong>Увага:</strong> Після збереження слід очистити кеш оглядача, щоб побачити зміни.\n* <strong>Firefox / Safari:</strong> тримайте <em>Shift</em>, коли натискаєте <em>Оновити</em>, або натисніть <em>Ctrl-F5</em> чи <em>Ctrl-Shift-R</em> (<em>⌘-R</em> на Apple Mac)\n* <strong>Google Chrome:</strong> натисніть <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> на Apple Mac)\n* <strong>Internet Explorer:</strong> тримайте <em>Ctrl</em>, коли натискаєте <em>Оновити</em>, або натисніть <em>Ctrl-F5</em>\n* <strong>Opera:</strong> очистіть кеш за допомогою <em>Інструменти → Налаштування</em> (<em>Opera → Побажання</em> на Apple Mac) та перейдіть на <em>Приватність & безпека → очистити дані браузера → кеш</em>",
"usercssyoucanpreview": "'''Підказка:''' використовуйте кнопку «{{int:showpreview}}», щоб протестувати ваш новий css-файл перед збереженням.",
"userjsyoucanpreview": "'''Підказка:''' використовуйте кнопку «{{int:showpreview}}», щоб протестувати ваш новий код JavaScript перед збереженням.",
"saveusergroups": "Зберегти групи {{GENDER:$1|користувачів}}",
"userrights-groupsmember": "Член груп:",
"userrights-groupsmember-auto": "Неявний член:",
- "userrights-groups-help": "Ви можете змінити групи, до яких належить цей користувач:\n* Якщо біля назви групи стоїть позначка, то користувач належить до цієї групи.\n* Якщо позначка не стоїть — користувач не належить до відповідної групи.\n* Зірочка «*» означає, що ви не можете вилучити користувача з групи, якщо додасте його до неї, і навпаки.\n* Ґратка «#» означає, що ви можете зменшити строк членства в групі, але не збільшити.",
+ "userrights-groups-help": "Ви можете змінити групи, до яких належить {{GENDER:$1|цей користувач|ця користувачка}}:\n* Якщо біля назви групи стоїть позначка, то {{GENDER:$1|користувач|користувачка}} належить до цієї групи.\n* Якщо позначка не стоїть — {{GENDER:$1|користувач|користувачка}} не належить до відповідної групи.\n* Зірочка «*» означає, що Ви не можете вилучити {{GENDER:$1|користувача|користувачку}} з групи, якщо додасте {{GENDER:$1|його|її}} до неї, і навпаки.\n* Решітка «#» означає, що Ви можете зменшити строк членства в групі, але не збільшити.",
"userrights-reason": "Причина:",
"userrights-no-interwiki": "У вас нема дозволу змінювати права користувачів на інших вікі.",
"userrights-nodatabase": "База даних $1 не існує або не є локальною.",
"rcfilters-invalid-filter": "Недійсний фільтр",
"rcfilters-empty-filter": "Без фільтрів. Показано всі зміни.",
"rcfilters-filterlist-title": "Фільтри",
+ "rcfilters-filterlist-feedbacklink": "Надайте відгук про нові (бета) фільтри",
+ "rcfilters-highlightbutton-title": "Виділити результати",
+ "rcfilters-highlightmenu-title": "Вибрати колір",
+ "rcfilters-highlightmenu-help": "Вибрати колір, щоб виділити цю властивість",
"rcfilters-filterlist-noresults": "Фільтри не знайдено",
"rcfilters-filtergroup-registration": "Реєстрація користувача",
"rcfilters-filter-registered-label": "Зареєстровані",
"editcomment": "Пояснення редагування було: «<em>$1</em>.».",
"revertpage": "Відкинуто редагування [[Special:Contributions/$2|$2]] ([[User talk:$2|обговорення]]) до зробленого [[User:$1|$1]]",
"revertpage-nouser": "Відкинуто редагування прихованого користувача до останньої версії, зробленої {{GENDER:$1|[[User:$1|$1]]}}",
- "rollback-success": "Відкинуті редагування користувача $1; повернення до версії користувача $2.",
+ "rollback-success": "Відкинуті редагування {{GENDER:$3|користувача|користувачки}} $1; повернення до версії {{GENDER:$4|користувача|користувачки}} $2.",
"rollback-success-notify": "Відкинуті редагування користувача $1; \nповернено до останньої версії користувача $2. [$3 Показати зміни]",
"sessionfailure-title": "Помилка сеансу",
"sessionfailure": "Здається, виникли проблеми з поточним сеансом роботи;\nця дія була скасована з метою попередити «захоплення сеансу».\nБудь ласка, натисніть кнопку «Назад» і перезавантажте сторінку, з якої ви прийшли.",
"resettokens": "重置密钥",
"resettokens-text": "您可以在这里重置允许访问与您的账户有关的特定私人数据的密钥。\n\n如果您意外将它们分享给他人,或是您的账户已经被入侵,您应该重置它们。",
"resettokens-no-tokens": "没有可以重置的密钥。",
- "resettokens-tokens": "密钥:",
+ "resettokens-tokens": "令牌:",
"resettokens-token-label": "$1(当前值:$2)",
"resettokens-watchlist-token": "[[Special:Watchlist|对你的监视列表中的页面的更改]]的网页feed(Atom/RSS)的密钥",
"resettokens-done": "密钥已重置。",
"prefs-advancedwatchlist": "高级选项",
"prefs-displayrc": "显示",
"prefs-displaywatchlist": "显示",
- "prefs-tokenwatchlist": "密钥",
+ "prefs-tokenwatchlist": "令牌",
"prefs-diffs": "差异对比",
"prefs-help-prefershttps": "该设置将在下次登录时生效。",
"prefswarning-warning": "您对您的参数设置的更改尚未保存。如果您不点击“$1”就离开,您的设置就不会更新。",
"rcfilters-filterlist-feedbacklink": "在新(测试版)过滤器中提供反馈",
"rcfilters-highlightbutton-title": "高亮结果",
"rcfilters-highlightmenu-title": "选择颜色",
+ "rcfilters-highlightmenu-help": "选择颜色来高亮该属性",
"rcfilters-filterlist-noresults": "找不到过滤器",
"rcfilters-filtergroup-registration": "用户注册",
"rcfilters-filter-registered-label": "已注册",
"Knch903",
"Winstonyin",
"Wmr",
- "烈羽"
+ "烈羽",
+ "和平奮鬥救地球"
]
},
"tog-underline": "底線標示連結:",
"rcfilters-search-placeholder": "過濾最近變更(瀏覽或開始輸入)",
"rcfilters-invalid-filter": "過濾規則無效",
"rcfilters-filterlist-title": "篩選器",
+ "rcfilters-filterlist-feedbacklink": "在新(測試版)過濾器中提供反饋",
"rcfilters-highlightmenu-title": "選擇顏色",
"rcfilters-filterlist-noresults": "找不到過濾規則",
+ "rcfilters-filter-registered-label": "已註冊",
+ "rcfilters-filter-unregistered-label": "未註冊",
"rcfilters-filtergroup-authorship": "編輯者",
"rcfilters-filter-editsbyself-label": "您自己的編輯",
"rcfilters-filter-editsbyself-description": "您的編輯。",
'mediawiki.user' => [
'scripts' => 'resources/src/mediawiki/mediawiki.user.js',
'dependencies' => [
- 'mediawiki.cookie',
'mediawiki.api',
'mediawiki.api.user',
+ 'mediawiki.cookie',
+ 'mediawiki.storage',
'user.options',
'user.tokens',
],
'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css',
'dependencies' => [
'jquery.makeCollapsible',
- 'mediawiki.cookie',
+ 'mediawiki.storage',
'mediawiki.icon',
],
],
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.variables.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less',
- 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less',
'rcfilters-filter-logactions-description',
'rcfilters-highlightbutton-title',
'rcfilters-highlightmenu-title',
+ 'rcfilters-highlightmenu-help',
'recentchanges-noresult',
],
'dependencies' => [
'mediawiki.special.block' => [
'scripts' => 'resources/src/mediawiki.special/mediawiki.special.block.js',
'styles' => 'resources/src/mediawiki.special/mediawiki.special.block.css',
- 'dependencies' => 'mediawiki.util',
+ 'dependencies' => [
+ 'mediawiki.util',
+ 'mediawiki.htmlform',
+ ],
],
'mediawiki.special.changeslist' => [
'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.css',
#wpTextbox1 {
margin: 0;
display: block;
+ /* Ensure the textarea is not higher than browser's viewport on small screens */
+ max-height: 100vh;
+ /* But don't let it collapse into nothingness on really tiny screens */
+ min-height: 5em;
}
/* Adjustments to edit form elements */
}
/* Expand URLs for printing */
-.mw-body a.external.text:after,
-.mw-body a.external.autonumber:after {
+.mw-body-content a.external.text:after,
+.mw-body-content a.external.autonumber:after {
content: ' (' attr( href ) ')';
word-break: break-all;
word-wrap: break-word;
}
/* Expand protocol-relative URLs for printing */
-.mw-body a.external.text[href^='//']:after,
-.mw-body a.external.autonumber[href^='//']:after {
+.mw-body-content a.external.text[href^='//']:after,
+.mw-body-content a.external.autonumber[href^='//']:after {
content: ' (https:' attr( href ) ')';
}
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24">
+ <path d="M5.066 18.236l.14-.244c.976-1.69 1.341-4.587.815-6.469l-.14-.507.2-.365L11.074 2l9.011 5.203-4.994 8.65-.204.354-.522.134c-1.893.485-4.22 2.252-5.195 3.94l-.14.244-.721-.416-1.041 1.89H3.914l1.893-3.336z" fill-rule="evenodd"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24">
+ <path d="M18.934 18.236l-.14-.244c-.976-1.69-1.341-4.587-.815-6.469l.14-.507-.2-.365L12.926 2 3.914 7.203l4.994 8.65.204.354.522.134c1.893.485 4.22 2.252 5.195 3.94l.14.244.721-.416 1.041 1.89h3.355l-1.893-3.336z" fill-rule="evenodd"/>
+</svg>
function ( pieces ) {
var $changesListContent = pieces.changes,
$fieldset = pieces.fieldset;
-
this.changesListModel.update( $changesListContent, $fieldset );
}.bind( this )
// Do nothing for failure
filtersWidget = new mw.rcfilters.ui.FilterWrapperWidget(
controller, filtersModel, { $overlay: $overlay } );
+ // TODO: The changesListWrapperWidget should be able to initialize
+ // after the model is ready.
// eslint-disable-next-line no-new
new mw.rcfilters.ui.ChangesListWrapperWidget(
filtersModel, changesListModel, $( '.mw-changeslist, .mw-changeslist-empty' ) );
- // eslint-disable-next-line no-new
- new mw.rcfilters.ui.FormWrapperWidget(
- changesListModel, controller, $( 'fieldset.rcoptions' ) );
-
controller.initialize( {
registration: {
title: mw.msg( 'rcfilters-filtergroup-registration' ),
}
} );
+ // eslint-disable-next-line no-new
+ new mw.rcfilters.ui.FormWrapperWidget(
+ filtersModel, changesListModel, controller, $( 'fieldset.rcoptions' ) );
+
$( '.rcfilters-container' ).append( filtersWidget.$element );
$( 'body' ).append( $overlay );
- // HACK: Remove old-style filter links for filters handled by the widget
- // Ideally the widget would handle all filters and we'd just remove .rcshowhide entirely
- $( '.rcshowhide' ).children().each( function () {
- // HACK: Interpret the class name to get the filter name
- // This should really be set as a data attribute
- var i,
- name = null,
- // Some of the older browsers we support don't have .classList,
- // so we have to interpret the class attribute manually.
- classes = this.getAttribute( 'class' ).split( ' ' );
- for ( i = 0; i < classes.length; i++ ) {
- if ( classes[ i ].substr( 0, 'rcshow'.length ) === 'rcshow' ) {
- name = classes[ i ].substr( 'rcshow'.length );
- break;
- }
- }
- if ( name === null ) {
- return;
- }
- if ( name === 'hidemine' ) {
- // HACK: the span for hidemyself is called hidemine
- name = 'hidemyself';
- }
- // This span corresponds to a filter that's in our model, so remove it
- if ( filtersModel.getItemByName( name ) ) {
- // HACK: Remove the text node after the span.
- // If there isn't one, we're at the end, so remove the text node before the span.
- // This would be unnecessary if we added separators with CSS.
- if ( this.nextSibling && this.nextSibling.nodeType === Node.TEXT_NODE ) {
- this.parentNode.removeChild( this.nextSibling );
- } else if ( this.previousSibling && this.previousSibling.nodeType === Node.TEXT_NODE ) {
- this.parentNode.removeChild( this.previousSibling );
- }
- // Remove the span itself
- this.parentNode.removeChild( this );
- }
- } );
+ // Set as ready
+ $( '.rcfilters-head' ).addClass( 'mw-rcfilters-ui-ready' );
window.addEventListener( 'popstate', function () {
controller.updateChangesList();
legend {
display: none;
}
+ }
+ .rcfilters-head {
+ min-height: 270px;
&:not( .mw-rcfilters-ui-ready ) {
/* @embed */
background-image: url( ../images/pending.gif );
.rcfilters-container {
min-height: 100px;
margin: 0;
-
- &:not( .mw-rcfilters-ui-ready ) {
- /* @embed */
- background-image: url( ../images/pending.gif );
- }
}
}
}
&[data-color='c1'] {
- .mw-rcfilters-mixin-circle( @highlight-c1, 0.7em, ~'0 0.5em 0 0' );
+ .mw-rcfilters-mixin-circle( @highlight-c1, 10px, ~'0 0.5em 0 0' );
}
&[data-color='c2'] {
- .mw-rcfilters-mixin-circle( @highlight-c2, 0.7em, ~'0 0.5em 0 0' );
+ .mw-rcfilters-mixin-circle( @highlight-c2, 10px, ~'0 0.5em 0 0' );
}
&[data-color='c3'] {
- .mw-rcfilters-mixin-circle( @highlight-c3, 0.7em, ~'0 0.5em 0 0' );
+ .mw-rcfilters-mixin-circle( @highlight-c3, 10px, ~'0 0.5em 0 0' );
}
&[data-color='c4'] {
- .mw-rcfilters-mixin-circle( @highlight-c4, 0.7em, ~'0 0.5em 0 0' );
+ .mw-rcfilters-mixin-circle( @highlight-c4, 10px, ~'0 0.5em 0 0' );
}
&[data-color='c5'] {
- .mw-rcfilters-mixin-circle( @highlight-c5, 0.7em, ~'0 0.5em 0 0' );
+ .mw-rcfilters-mixin-circle( @highlight-c5, 10px, ~'0 0.5em 0 0' );
}
}
}
// Each li's margin-left should be the width of the highlights
// element + the margin
margin-left: ~'calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 + @{result-circle-general-margin} )';
+
+ li {
+ list-style: none;
+ }
}
}
@import 'mw.rcfilters.mixins';
.mw-rcfilters-ui-filterItemHighlightButton {
+ .oo-ui-iconElement-icon.oo-ui-icon-highlight {
+ /* @embed */
+ background-image: url( ../images/marker-ltr.svg );
+ }
.oo-ui-buttonWidget.oo-ui-popupButtonWidget .oo-ui-buttonElement-button > &-circle {
display: inline-block;
}
&:hover {
- background-color: #f8f9fa; // Base90 AAA
+ background-color: #fbfbfb;
}
.mw-rcfilters-ui-table {
// Parent
mw.rcfilters.ui.FilterItemHighlightButton.parent.call( this, $.extend( {}, config, {
- icon: 'edit',
+ icon: 'highlight',
indicator: 'down',
popup: {
anchor: false,
this.controller,
this.model,
{
- $overlay: config.$overlay || this.$element
+ $overlay: config.$overlay || this.$element,
+ title: mw.msg( 'rcfilters-highlightmenu-help' )
}
);
this.highlightButton.toggle( this.model.isHighlightEnabled() );
// Initialize
this.$element
.addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
- .addClass( 'mw-rcfilters-ui-ready' )
.append( this.capsule.$element, this.textInput.$element );
};
*/
mw.rcfilters.ui.FilterWrapperWidget.prototype.scrollToTop = function ( $element, marginFromTop ) {
var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
- pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) );
+ pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
+ containerScrollTop = $( container ).is( 'body, html' ) ? 0 : $( container ).scrollTop();
// Scroll to item
$( container ).animate( {
- scrollTop: $( container ).scrollTop() + pos.top + ( marginFromTop || 0 )
+ scrollTop: containerScrollTop + pos.top - ( marginFromTop || 0 )
} );
};
}( mediaWiki ) );
( function ( mw ) {
/**
* Wrapper for the RC form with hide/show links
+ * Must be constructed after the model is initialized.
*
* @extends OO.ui.Widget
*
* @constructor
- * @param {mw.rcfilters.dm.ChangesListViewModel} model Changes list view model
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Changes list view model
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changeListModel Changes list view model
* @param {mw.rcfilters.Controller} controller RCfilters controller
* @param {jQuery} $formRoot Root element of the form to attach to
* @param {Object} config Configuration object
*/
- mw.rcfilters.ui.FormWrapperWidget = function MwRcfiltersUiFormWrapperWidget( model, controller, $formRoot, config ) {
+ mw.rcfilters.ui.FormWrapperWidget = function MwRcfiltersUiFormWrapperWidget( filtersModel, changeListModel, controller, $formRoot, config ) {
config = config || {};
// Parent
// Mixin constructors
OO.ui.mixin.PendingElement.call( this, config );
- this.model = model;
+ this.changeListModel = changeListModel;
+ this.filtersModel = filtersModel;
this.controller = controller;
this.$submitButton = this.$element.find( 'form input[type=submit]' );
.on( 'submit', 'form', this.onFormSubmit.bind( this ) );
// Events
- this.model.connect( this, {
- invalidate: 'onModelInvalidate',
- update: 'onModelUpdate'
+ this.changeListModel.connect( this, {
+ invalidate: 'onChangesModelInvalidate',
+ update: 'onChangesModelUpdate'
} );
// Initialize
- this.cleanupForm();
+ this.cleanUpFieldset();
this.$element
- .addClass( 'mw-rcfilters-ui-FormWrapperWidget' )
- .addClass( 'mw-rcfilters-ui-ready' );
+ .addClass( 'mw-rcfilters-ui-FormWrapperWidget' );
};
/* Initialization */
OO.inheritClass( mw.rcfilters.ui.FormWrapperWidget, OO.ui.Widget );
OO.mixinClass( mw.rcfilters.ui.FormWrapperWidget, OO.ui.mixin.PendingElement );
- /**
- * Clean up the base form we're getting from the back-end.
- * Remove <strong> tags and replace those with classes, so
- * we can toggle those on click.
- */
- mw.rcfilters.ui.FormWrapperWidget.prototype.cleanupForm = function () {
- this.$element.find( '[data-keys] strong' ).each( function () {
- $( this )
- .parent().addClass( 'mw-rcfilters-staticfilters-selected' );
-
- $( this )
- .replaceWith( $( this ).contents() );
- } );
- };
-
/**
* Respond to link click
*
* @return {boolean} false
*/
mw.rcfilters.ui.FormWrapperWidget.prototype.onLinkClick = function ( e ) {
- var $element = $( e.target ),
- data = $element.data( 'params' ),
- keys = $element.data( 'keys' ),
- $similarElements = $element.parent().find( '[data-keys="' + keys + '"]' );
-
- // Only highlight choice if this link isn't a show/hide link
- if ( !$element.parents( '.rcshowhideoption' ).length ) {
- // Remove the class from similar elements
- $similarElements.removeClass( 'mw-rcfilters-staticfilters-selected' );
- // Add the class to this element
- $element.addClass( 'mw-rcfilters-staticfilters-selected' );
- }
-
- e.stopPropagation();
-
- this.controller.updateChangesList( data );
+ this.controller.updateChangesList( $( e.target ).data( 'params' ) );
return false;
};
/**
* Respond to model invalidate
*/
- mw.rcfilters.ui.FormWrapperWidget.prototype.onModelInvalidate = function () {
+ mw.rcfilters.ui.FormWrapperWidget.prototype.onChangesModelInvalidate = function () {
this.pushPending();
this.$submitButton.prop( 'disabled', true );
};
* @param {jQuery|string} $changesList Updated changes list
* @param {jQuery} $fieldset Updated fieldset
*/
- mw.rcfilters.ui.FormWrapperWidget.prototype.onModelUpdate = function ( $changesList, $fieldset ) {
+ mw.rcfilters.ui.FormWrapperWidget.prototype.onChangesModelUpdate = function ( $changesList, $fieldset ) {
this.$submitButton.prop( 'disabled', false );
- // Replace the links we have in the content
- // We don't want to replace the entire thing, because there is a big difference between
- // the links in the backend and the links we have initialized, since we are removing
- // the ones that are implemented in the new system
+ // Replace the entire fieldset
+ this.$element.empty().append( $fieldset.contents() );
+
+ this.cleanUpFieldset();
+
+ this.popPending();
+ };
+
+ /**
+ * Clean up the old-style show/hide that we have implemented in the filter list
+ */
+ mw.rcfilters.ui.FormWrapperWidget.prototype.cleanUpFieldset = function () {
+ var widget = this;
+
+ // HACK: Remove old-style filter links for filters handled by the widget
+ // Ideally the widget would handle all filters and we'd just remove .rcshowhide entirely
this.$element.find( '.rcshowhide' ).children().each( function () {
- // Go over existing links and replace only them
- var classes = $( this ).attr( 'class' ).split( ' ' ),
- // Look for that item in the fieldset from the server
- $remoteItem = $fieldset.find( '.' + classes.join( '.' ) );
+ // HACK: Interpret the class name to get the filter name
+ // This should really be set as a data attribute
+ var i,
+ name = null,
+ // Some of the older browsers we support don't have .classList,
+ // so we have to interpret the class attribute manually.
+ classes = this.getAttribute( 'class' ).split( ' ' );
+ for ( i = 0; i < classes.length; i++ ) {
+ if ( classes[ i ].substr( 0, 'rcshow'.length ) === 'rcshow' ) {
+ name = classes[ i ].substr( 'rcshow'.length );
+ break;
+ }
+ }
+ if ( name === null ) {
+ return;
+ }
+ if ( name === 'hidemine' ) {
+ // HACK: the span for hidemyself is called hidemine
+ name = 'hidemyself';
+ }
- if ( $remoteItem ) {
- $( this ).replaceWith( $remoteItem );
+ // This span corresponds to a filter that's in our model, so remove it
+ if ( widget.filtersModel.getItemByName( name ) ) {
+ // HACK: Remove the text node after the span.
+ // If there isn't one, we're at the end, so remove the text node before the span.
+ // This would be unnecessary if we added separators with CSS.
+ if ( this.nextSibling && this.nextSibling.nodeType === Node.TEXT_NODE ) {
+ this.parentNode.removeChild( this.nextSibling );
+ } else if ( this.previousSibling && this.previousSibling.nodeType === Node.TEXT_NODE ) {
+ this.parentNode.removeChild( this.previousSibling );
+ }
+ // Remove the span itself
+ this.parentNode.removeChild( this );
}
} );
-
- this.popPending();
};
}( mediaWiki ) );
* compatibility ( browsers able to understand gradient syntax support also SVG ).
* http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique */
-.mw-body a.external,
+.mw-body-content a.external,
.link-https {
background: url( images/external-ltr.png ) center right no-repeat;
/* @embed */
padding-right: 15px;
}
-.mw-body a.external[href^='mailto:'],
+.mw-body-content a.external[href^='mailto:'],
.link-mailto {
background: url( images/mail.png ) center right no-repeat;
/* @embed */
padding-right: 15px;
}
-.mw-body a.external[href^='ftp://'],
+.mw-body-content a.external[href^='ftp://'],
.link-ftp {
background: url( images/ftp-ltr.png ) center right no-repeat;
/* @embed */
padding-right: 15px;
}
-.mw-body a.external[href^='irc://'],
-.mw-body a.external[href^='ircs://'],
+.mw-body-content a.external[href^='irc://'],
+.mw-body-content a.external[href^='ircs://'],
.link-irc {
background: url( images/chat-ltr.png ) center right no-repeat;
/* @embed */
padding-right: 15px;
}
-.mw-body a.external[href$='.ogg'],
-.mw-body a.external[href$='.OGG'],
-.mw-body a.external[href$='.mid'],
-.mw-body a.external[href$='.MID'],
-.mw-body a.external[href$='.midi'],
-.mw-body a.external[href$='.MIDI'],
-.mw-body a.external[href$='.mp3'],
-.mw-body a.external[href$='.MP3'],
-.mw-body a.external[href$='.wav'],
-.mw-body a.external[href$='.WAV'],
-.mw-body a.external[href$='.wma'],
-.mw-body a.external[href$='.WMA'],
+.mw-body-content a.external[href$='.ogg'],
+.mw-body-content a.external[href$='.OGG'],
+.mw-body-content a.external[href$='.mid'],
+.mw-body-content a.external[href$='.MID'],
+.mw-body-content a.external[href$='.midi'],
+.mw-body-content a.external[href$='.MIDI'],
+.mw-body-content a.external[href$='.mp3'],
+.mw-body-content a.external[href$='.MP3'],
+.mw-body-content a.external[href$='.wav'],
+.mw-body-content a.external[href$='.WAV'],
+.mw-body-content a.external[href$='.wma'],
+.mw-body-content a.external[href$='.WMA'],
.link-audio {
background: url( images/audio-ltr.png ) center right no-repeat;
/* @embed */
padding-right: 15px;
}
-.mw-body a.external[href$='.ogm'],
-.mw-body a.external[href$='.OGM'],
-.mw-body a.external[href$='.avi'],
-.mw-body a.external[href$='.AVI'],
-.mw-body a.external[href$='.mpeg'],
-.mw-body a.external[href$='.MPEG'],
-.mw-body a.external[href$='.mpg'],
-.mw-body a.external[href$='.MPG'],
+.mw-body-content a.external[href$='.ogm'],
+.mw-body-content a.external[href$='.OGM'],
+.mw-body-content a.external[href$='.avi'],
+.mw-body-content a.external[href$='.AVI'],
+.mw-body-content a.external[href$='.mpeg'],
+.mw-body-content a.external[href$='.MPEG'],
+.mw-body-content a.external[href$='.mpg'],
+.mw-body-content a.external[href$='.MPG'],
.link-video {
background: url( images/video.png ) center right no-repeat;
/* @embed */
padding-right: 15px;
}
-.mw-body a.external[href$='.pdf'],
-.mw-body a.external[href$='.PDF'],
-.mw-body a.external[href*='.pdf#'],
-.mw-body a.external[href*='.PDF#'],
-.mw-body a.external[href*='.pdf?'],
-.mw-body a.external[href*='.PDF?'],
+.mw-body-content a.external[href$='.pdf'],
+.mw-body-content a.external[href$='.PDF'],
+.mw-body-content a.external[href*='.pdf#'],
+.mw-body-content a.external[href*='.PDF#'],
+.mw-body-content a.external[href*='.pdf?'],
+.mw-body-content a.external[href*='.PDF?'],
.link-document {
background: url( images/document-ltr.png ) center right no-repeat;
/* @embed */
}
/* Interwiki styling */
-.mw-body a.extiw,
-.mw-body a.extiw:active {
+.mw-body-content a.extiw,
+.mw-body-content a.extiw:active {
color: #36b;
}
/* External link color */
-.mw-body a.external {
+.mw-body-content a.external {
color: #36b;
}
}
/* Interwiki Styling */
-.mw-body a.extiw,
-.mw-body a.extiw:active {
+.mw-body-content a.extiw,
+.mw-body-content a.extiw:active {
color: #36b;
}
-.mw-body a.extiw:visited {
+.mw-body-content a.extiw:visited {
color: #636;
}
-.mw-body a.extiw:active {
+.mw-body-content a.extiw:active {
color: #b63;
}
/* External links */
-.mw-body a.external {
+.mw-body-content a.external {
color: #36b;
}
-.mw-body a.external:visited {
+.mw-body-content a.external:visited {
color: #636; /* T5112 */
}
-.mw-body a.external:active {
+.mw-body-content a.external:active {
color: #b63;
}
-.mw-body a.external.free {
+.mw-body-content a.external.free {
word-wrap: break-word;
}
*/
getModuleSize: function ( moduleName ) {
var module = mw.loader.moduleRegistry[ moduleName ],
- args, i;
+ args, i, size;
- if ( mw.loader.getState( moduleName ) !== 'ready' ) {
+ if ( module.state !== 'ready' ) {
return null;
}
return 0;
}
- // Reverse-engineer the load.php response for this module.
- // For example: `mw.loader.implement("example", function(){}, );`
+ function getFunctionBody( func ) {
+ return String( func )
+ // To ensure a deterministic result, replace the start of the function
+ // declaration with a fixed string. For example, in Chrome 55, it seems
+ // V8 seemingly-at-random decides to sometimes put a line break between
+ // the opening brace and first statement of the function body. T159751.
+ .replace( /^\s*function\s*\([^)]*\)\s*{\s*/, 'function(){' )
+ .replace( /\s*}\s*$/, '}' );
+ }
+
+ // Based on the load.php response for this module.
+ // For example: `mw.loader.implement("example", function(){}, {"css":[".x{color:red}"]});`
// @see mw.loader.store.set().
args = [
- JSON.stringify( moduleName ),
- // function, array of urls, or eval string
- typeof module.script === 'function' ?
- String( module.script ) :
- JSON.stringify( module.script ),
- JSON.stringify( module.style ),
- JSON.stringify( module.messages ),
- JSON.stringify( module.templates )
+ moduleName,
+ module.script,
+ module.style,
+ module.messages,
+ module.templates
];
// Trim trailing null or empty object, as load.php would have done.
// @see ResourceLoader::makeLoaderImplementScript and ResourceLoader::trimArray.
i = args.length;
while ( i-- ) {
- if ( args[ i ] === '{}' || args[ i ] === 'null' ) {
+ if ( args[ i ] === null || ( $.isPlainObject( args[ i ] ) && $.isEmptyObject( args[ i ] ) ) ) {
args.splice( i, 1 );
} else {
break;
}
}
- return $.byteLength( args.join( ',' ) );
+ size = 0;
+ for ( i = 0; i < args.length; i++ ) {
+ if ( typeof args[ i ] === 'function' ) {
+ size += $.byteLength( getFunctionBody( args[ i ] ) );
+ } else {
+ size += $.byteLength( JSON.stringify( args[ i ] ) );
+ }
+ }
+
+ return size;
},
/**
/* eslint-enable no-bitwise */
}
+ // <https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Set>
StringSet = window.Set || ( function () {
/**
* @private
this.set[ value ] = true;
};
StringSet.prototype.has = function ( value ) {
- return this.set.hasOwnProperty( value );
+ return hasOwn.call( this.set, value );
};
return StringSet;
}() );
* copied in one direction only. Changes to globals do not reflect in the map.
*/
function Map( global ) {
- this.internalValues = {};
+ this.values = {};
if ( global === true ) {
-
// Override #set to also set the global variable
this.set = function ( selection, value ) {
var s;
return false;
};
}
-
- // Deprecated since MediaWiki 1.28
- log.deprecate(
- this,
- 'values',
- this.internalValues,
- 'mw.Map#values is deprecated. Use mw.Map#get() instead.',
- 'Map-values'
- );
}
/**
* @param {Mixed} value
*/
function setGlobalMapValue( map, key, value ) {
- map.internalValues[ key ] = value;
+ map.values[ key ] = value;
log.deprecate(
window,
key,
*
* @param {string|Array} [selection] Key or array of keys to retrieve values for.
* @param {Mixed} [fallback=null] Value for keys that don't exist.
- * @return {Mixed|Object| null} If selection was a string, returns the value,
+ * @return {Mixed|Object|null} If selection was a string, returns the value,
* If selection was an array, returns an object of key/values.
- * If no selection is passed, the internal container is returned. (Beware that,
- * as is the default in JavaScript, the object is returned by reference.)
+ * If no selection is passed, a new object with all key/values is returned.
*/
get: function ( selection, fallback ) {
var results, i;
- // If we only do this in the `return` block, it'll fail for the
- // call to get() from the mutli-selection block.
fallback = arguments.length > 1 ? fallback : null;
if ( $.isArray( selection ) ) {
- selection = slice.call( selection );
results = {};
for ( i = 0; i < selection.length; i++ ) {
- results[ selection[ i ] ] = this.get( selection[ i ], fallback );
+ if ( typeof selection[ i ] === 'string' ) {
+ results[ selection[ i ] ] = hasOwn.call( this.values, selection[ i ] ) ?
+ this.values[ selection[ i ] ] :
+ fallback;
+ }
}
return results;
}
if ( typeof selection === 'string' ) {
- if ( !hasOwn.call( this.internalValues, selection ) ) {
- return fallback;
- }
- return this.internalValues[ selection ];
+ return hasOwn.call( this.values, selection ) ?
+ this.values[ selection ] :
+ fallback;
}
if ( selection === undefined ) {
- return this.internalValues;
+ results = {};
+ for ( i in this.values ) {
+ results[ i ] = this.values[ i ];
+ }
+ return results;
}
// Invalid selection key
- return null;
+ return fallback;
},
/**
if ( $.isPlainObject( selection ) ) {
for ( s in selection ) {
- this.internalValues[ s ] = selection[ s ];
+ this.values[ s ] = selection[ s ];
}
return true;
}
if ( typeof selection === 'string' && arguments.length > 1 ) {
- this.internalValues[ selection ] = value;
+ this.values[ selection ] = value;
return true;
}
return false;
* @return {boolean} True if the key(s) exist
*/
exists: function ( selection ) {
- var s;
-
+ var i;
if ( $.isArray( selection ) ) {
- for ( s = 0; s < selection.length; s++ ) {
- if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.internalValues, selection[ s ] ) ) {
+ for ( i = 0; i < selection.length; i++ ) {
+ if ( typeof selection[ i ] !== 'string' || !hasOwn.call( this.values, selection[ i ] ) ) {
return false;
}
}
return true;
}
- return typeof selection === 'string' && hasOwn.call( this.internalValues, selection );
+ return typeof selection === 'string' && hasOwn.call( this.values, selection );
}
};
* @group FileRepo
* @group FileBackend
* @group medium
+ *
+ * @covers FileBackend
+ *
+ * @covers CopyFileOp
+ * @covers CreateFileOp
+ * @covers DeleteFileOp
+ * @covers DescribeFileOp
+ * @covers FSFile
+ * @covers FSFileBackend
+ * @covers FSFileBackendDirList
+ * @covers FSFileBackendFileList
+ * @covers FSFileBackendList
+ * @covers FSFileOpHandle
+ * @covers FileBackendDBRepoWrapper
+ * @covers FileBackendError
+ * @covers FileBackendGroup
+ * @covers FileBackendMultiWrite
+ * @covers FileBackendStore
+ * @covers FileBackendStoreOpHandle
+ * @covers FileBackendStoreShardDirIterator
+ * @covers FileBackendStoreShardFileIterator
+ * @covers FileBackendStoreShardListIterator
+ * @covers FileJournal
+ * @covers FileOp
+ * @covers FileOpBatch
+ * @covers HTTPFileStreamer
+ * @covers LockManagerGroup
+ * @covers MemoryFileBackend
+ * @covers MoveFileOp
+ * @covers MySqlLockManager
+ * @covers NullFileJournal
+ * @covers NullFileOp
+ * @covers StoreFileOp
+ * @covers TempFSFile
+ *
+ * @covers FSLockManager
+ * @covers LockManager
+ * @covers NullLockManager
*/
class FileBackendTest extends MediaWikiTestCase {
/**
* @dataProvider provider_testIsStoragePath
- * @covers FileBackend::isStoragePath
*/
public function testIsStoragePath( $path, $isStorePath ) {
$this->assertEquals( $isStorePath, FileBackend::isStoragePath( $path ),
/**
* @dataProvider provider_testSplitStoragePath
- * @covers FileBackend::splitStoragePath
*/
public function testSplitStoragePath( $path, $res ) {
$this->assertEquals( $res, FileBackend::splitStoragePath( $path ),
/**
* @dataProvider provider_normalizeStoragePath
- * @covers FileBackend::normalizeStoragePath
*/
public function testNormalizeStoragePath( $path, $res ) {
$this->assertEquals( $res, FileBackend::normalizeStoragePath( $path ),
/**
* @dataProvider provider_testParentStoragePath
- * @covers FileBackend::parentStoragePath
*/
public function testParentStoragePath( $path, $res ) {
$this->assertEquals( $res, FileBackend::parentStoragePath( $path ),
/**
* @dataProvider provider_testExtensionFromPath
- * @covers FileBackend::extensionFromPath
*/
public function testExtensionFromPath( $path, $res ) {
$this->assertEquals( $res, FileBackend::extensionFromPath( $path ),
$this->tearDownFiles();
}
- /**
- * @covers FileBackend::doOperation
- */
private function doTestStore( $op ) {
$backendName = $this->backendClass();
/**
* @dataProvider provider_testCopy
- * @covers FileBackend::doOperation
*/
public function testCopy( $op ) {
$this->backend = $this->singleBackend;
/**
* @dataProvider provider_testMove
- * @covers FileBackend::doOperation
*/
public function testMove( $op ) {
$this->backend = $this->singleBackend;
/**
* @dataProvider provider_testDelete
- * @covers FileBackend::doOperation
*/
public function testDelete( $op, $withSource, $okStatus ) {
$this->backend = $this->singleBackend;
/**
* @dataProvider provider_testDescribe
- * @covers FileBackend::doOperation
*/
public function testDescribe( $op, $withSource, $okStatus ) {
$this->backend = $this->singleBackend;
/**
* @dataProvider provider_testCreate
- * @covers FileBackend::doOperation
*/
public function testCreate( $op, $alreadyExists, $okStatus, $newSize ) {
$this->backend = $this->singleBackend;
return $cases;
}
- /**
- * @covers FileBackend::doQuickOperations
- */
public function testDoQuickOperations() {
$this->backend = $this->singleBackend;
$this->doTestDoQuickOperations();
/**
* @dataProvider provider_testGetFileStat
- * @covers FileBackend::getFileStat
*/
public function testGetFileStat( $path, $content, $alreadyExists ) {
$this->backend = $this->singleBackend;
/**
* @dataProvider provider_testGetFileStat
- * @covers FileBackend::streamFile
*/
public function testStreamFile( $path, $content, $alreadyExists ) {
$this->backend = $this->singleBackend;
/**
* @dataProvider provider_testGetFileContents
- * @covers FileBackend::getFileContents
- * @covers FileBackend::getFileContentsMulti
*/
public function testGetFileContents( $source, $content ) {
$this->backend = $this->singleBackend;
/**
* @dataProvider provider_testGetLocalCopy
- * @covers FileBackend::getLocalCopy
*/
public function testGetLocalCopy( $source, $content ) {
$this->backend = $this->singleBackend;
/**
* @dataProvider provider_testGetLocalReference
- * @covers FileBackend::getLocalReference
*/
public function testGetLocalReference( $source, $content ) {
$this->backend = $this->singleBackend;
return $cases;
}
- /**
- * @covers FileBackend::getLocalCopy
- * @covers FileBackend::getLocalReference
- */
public function testGetLocalCopyAndReference404() {
$this->backend = $this->singleBackend;
$this->tearDownFiles();
/**
* @dataProvider provider_testGetFileHttpUrl
- * @covers FileBackend::getFileHttpUrl
*/
public function testGetFileHttpUrl( $source, $content ) {
$this->backend = $this->singleBackend;
/**
* @dataProvider provider_testPrepareAndClean
- * @covers FileBackend::prepare
- * @covers FileBackend::clean
*/
public function testPrepareAndClean( $path, $isOK ) {
$this->backend = $this->singleBackend;
$this->tearDownFiles();
}
- /**
- * @covers FileBackend::clean
- */
private function doTestRecursiveClean() {
$backendName = $this->backendClass();
}
}
- /**
- * @covers FileBackend::doOperations
- */
public function testDoOperations() {
$this->backend = $this->singleBackend;
$this->tearDownFiles();
"Correct file SHA-1 of $fileC" );
}
- /**
- * @covers FileBackend::doOperations
- */
public function testDoOperationsPipeline() {
$this->backend = $this->singleBackend;
$this->tearDownFiles();
"Correct file SHA-1 of $fileC" );
}
- /**
- * @covers FileBackend::doOperations
- */
public function testDoOperationsFailing() {
$this->backend = $this->singleBackend;
$this->tearDownFiles();
"Correct file SHA-1 of $fileA" );
}
- /**
- * @covers FileBackend::getFileList
- */
public function testGetFileList() {
$this->backend = $this->singleBackend;
$this->tearDownFiles();
}
}
- /**
- * @covers FileBackend::getTopDirectoryList
- * @covers FileBackend::getDirectoryList
- */
public function testGetDirectoryList() {
$this->backend = $this->singleBackend;
$this->tearDownFiles();
$this->assertEquals( [], $items, "Directory listing is empty." );
}
- /**
- * @covers FileBackend::lockFiles
- * @covers FileBackend::unlockFiles
- */
public function testLockCalls() {
$this->backend = $this->singleBackend;
$this->doTestLockCalls();
* @group FileRepo
* @group FileBackend
* @group medium
+ *
+ * @covers SwiftFileBackend
+ * @covers SwiftFileBackendDirList
+ * @covers SwiftFileBackendFileList
+ * @covers SwiftFileBackendList
*/
class SwiftFileBackendTest extends MediaWikiTestCase {
/** @var TestingAccessWrapper Proxy to SwiftFileBackend */
/**
* @dataProvider provider_testSanitizeHdrs
- * @covers SwiftFileBackend::sanitizeHdrs
- * @covers SwiftFileBackend::getCustomHeaders
*/
public function testSanitizeHdrs( $raw, $sanitized ) {
$hdrs = $this->backend->sanitizeHdrs( [ 'headers' => $raw ] );
/**
* @dataProvider provider_testGetMetadataHeaders
- * @covers SwiftFileBackend::getMetadataHeaders
*/
public function testGetMetadataHeaders( $raw, $sanitized ) {
$hdrs = $this->backend->getMetadataHeaders( $raw );
/**
* @dataProvider provider_testGetMetadata
- * @covers SwiftFileBackend::getMetadata
*/
public function testGetMetadata( $raw, $sanitized ) {
$hdrs = $this->backend->getMetadata( $raw );
( function ( mw ) {
- // Whitespace and serialisation of function bodies
- // different in browsers.
- var functionSize = String( function () {} ).length;
QUnit.module( 'mediawiki.inspect' );
return mw.loader.using( 'test.inspect.script' ).then( function () {
assert.equal(
- mw.inspect.getModuleSize( 'test.inspect.script' ) - functionSize,
+ mw.inspect.getModuleSize( 'test.inspect.script' ),
// name, script function
- 32,
+ 43,
'test.inspect.script'
);
} );
return mw.loader.using( 'test.inspect.both' ).then( function () {
assert.equal(
- mw.inspect.getModuleSize( 'test.inspect.both' ) - functionSize,
+ mw.inspect.getModuleSize( 'test.inspect.both' ),
// name, script function, styles object
- 54,
+ 64,
'test.inspect.both'
);
} );
return mw.loader.using( 'test.inspect.scriptmsg' ).then( function () {
assert.equal(
- mw.inspect.getModuleSize( 'test.inspect.scriptmsg' ) - functionSize,
+ mw.inspect.getModuleSize( 'test.inspect.scriptmsg' ),
// name, script function, empty styles object, messages object
- 65,
+ 74,
'test.inspect.scriptmsg'
);
} );
return mw.loader.using( 'test.inspect.all' ).then( function () {
assert.equal(
- mw.inspect.getModuleSize( 'test.inspect.all' ) - functionSize,
+ mw.inspect.getModuleSize( 'test.inspect.all' ),
// name, script function, styles object, messages object, templates object
- 118,
+ 126,
'test.inspect.all'
);
} );
assert.strictEqual( conf.set( arry, 'Arry' ), false, 'Map.set returns boolean false if key was invalid (Array)' );
assert.strictEqual( conf.set( nummy, 'Nummy' ), false, 'Map.set returns boolean false if key was invalid (Number)' );
- assert.strictEqual( conf.get( funky ), null, 'Map.get ruturns null if selection was invalid (Function)' );
- assert.strictEqual( conf.get( nummy ), null, 'Map.get ruturns null if selection was invalid (Number)' );
-
conf.set( String( nummy ), 'I used to be a number' );
+ assert.strictEqual( conf.get( funky ), null, 'Map.get returns null if selection was invalid (Function)' );
+ assert.strictEqual( conf.get( nummy ), null, 'Map.get returns null if selection was invalid (Number)' );
+ assert.propEqual( conf.get( [ nummy ] ), {}, 'Map.get returns null if selection was invalid (multiple)' );
+ assert.strictEqual( conf.get( nummy, false ), false, 'Map.get returns custom fallback for invalid selection' );
+
assert.strictEqual( conf.exists( 'doesNotExist' ), false, 'Map.exists where property does not exist' );
assert.strictEqual( conf.exists( 'undef' ), true, 'Map.exists where value is `undefined`' );
- assert.strictEqual( conf.exists( nummy ), false, 'Map.exists where key is invalid but looks like an existing key' );
+ assert.strictEqual( conf.exists( [ 'undef', 'example' ] ), true, 'Map.exists with multiple keys (all existing)' );
+ assert.strictEqual( conf.exists( [ 'example', 'doesNotExist' ] ), false, 'Map.exists with multiple keys (some non-existing)' );
+ assert.strictEqual( conf.exists( [] ), true, 'Map.exists with no keys' );
+ assert.strictEqual( conf.exists( nummy ), false, 'Map.exists with invalid key that looks like an existing key' );
+ assert.strictEqual( conf.exists( [ nummy ] ), false, 'Map.exists with invalid key that looks like an existing key' );
// Multiple values at once
+ conf = new mw.Map();
someValues = {
foo: 'bar',
lorem: 'ipsum',
notExist: null
}, 'Map.get return includes keys that were not found as null values' );
- // Interacting with globals and accessing the values object
- this.suppressWarnings();
- assert.strictEqual( conf.get(), conf.values, 'Map.get returns the entire values object by reference (if called without arguments)' );
- this.restoreWarnings();
+ assert.propEqual( conf.values, someValues, 'Map.values is an internal object with all values (exposed for convenience)' );
+ assert.propEqual( conf.get(), someValues, 'Map.get() returns an object with all values' );
+ // Interacting with globals
conf.set( 'globalMapChecker', 'Hi' );
assert.ok( ( 'globalMapChecker' in window ) === false, 'Map does not its store values in the window object by default' );