'CapsCleanup' => __DIR__ . '/maintenance/cleanupCaps.php',
'CategoriesRdf' => __DIR__ . '/includes/CategoriesRdf.php',
'Category' => __DIR__ . '/includes/Category.php',
+ 'CategoryChangesAsRdf' => __DIR__ . '/maintenance/categoryChangesAsRdf.php',
'CategoryFinder' => __DIR__ . '/includes/CategoryFinder.php',
'CategoryMembershipChange' => __DIR__ . '/includes/changes/CategoryMembershipChange.php',
'CategoryMembershipChangeJob' => __DIR__ . '/includes/jobqueue/jobs/CategoryMembershipChangeJob.php',
{
"name": "mediawiki/core",
"description": "Free software wiki application developed by the Wikimedia Foundation and others",
+ "type": "mediawiki-core",
"keywords": ["mediawiki", "wiki"],
"homepage": "https://www.mediawiki.org/",
"authors": [
"apihelp-query+allimages-param-sha1base36": "גיבוב SHA1 של התמונה בבסיס 36 (הבסיס בו נעשה שימוש במדיה־ויקי).",
"apihelp-query+allimages-param-user": "להחזיר רק קבצים שהועלו על־ידי המשתמש הזה. יכול לשמש רק עם $1sort=timestamp. לא יכול לשמש יחד עם $1filterbots.",
"apihelp-query+allimages-param-filterbots": "איך לסנן קבצים שמעלים בוטים. יכול לשמש רק עם $1sort=timestamp. לא יכול לשמש יחד עם $1user.",
- "apihelp-query+allimages-param-mime": "×\90×\99×\9c×\95 ס×\95×\92×\99 MIME ×\9c×\97×\91פש, ×\9c×\9eש×\9c <kbd>image/jpeg</kbd>.",
+ "apihelp-query+allimages-param-mime": "אילו סוגי MIME לחפש, למשל <kbd>image/jpeg</kbd>.",
"apihelp-query+allimages-param-limit": "כמה תמונות להחזיר בסך הכול.",
"apihelp-query+allimages-example-B": "הצגת רשימה של קבצים שמתחילים באות <kbd>B</kbd>.",
"apihelp-query+allimages-example-recent": "הצגת רשימת קבצים שהועלו לאחרונה, דומה ל־[[Special:NewFiles]].",
"apihelp-query+categories-paramvalue-prop-hidden": "תיוג קטגוריות שהוסתרו באמצעות <code>__HIDDENCAT__</code>.",
"apihelp-query+categories-param-show": "איזה סוג של קטגוריות להציג.",
"apihelp-query+categories-param-limit": "כמה קטגוריות להחזיר.",
- "apihelp-query+categories-param-categories": "×\9cרש×\95×\9d רק ×\90ת ×\94ק×\98×\92×\95ר×\99×\95ת ×\94×\90×\9c×\95. ש×\99×\9e×\95ש×\99 ×\9c×\91×\93×\99ק×\94 ×¢ם דף מסוים נמצא בקטגוריה מסוימת.",
+ "apihelp-query+categories-param-categories": "×\9cרש×\95×\9d רק ×\90ת ×\94ק×\98×\92×\95ר×\99×\95ת ×\94×\90×\9c×\95. ש×\99×\9e×\95ש×\99 ×\9c×\91×\93×\99ק×\94 ×\90ם דף מסוים נמצא בקטגוריה מסוימת.",
"apihelp-query+categories-param-dir": "באיזה כיוון לרשום.",
"apihelp-query+categories-example-simple": "קבלת רשימת קטגוריות שהם <kbd>Albert Einstein</kbd> שייך אליהן.",
"apihelp-query+categories-example-generator": "קבלת מידע על כל הקטגוריות שמשמשות בדף <kbd>Albert Einstein</kbd>.",
"apihelp-query+imageinfo-paramvalue-prop-uploadwarning": "משמש את Special:Upload כדי לקבל מידע על קובץ קיים. לא נועד לשימוש מחוץ לליבת MediaWiki.",
"apihelp-query+imageinfo-paramvalue-prop-badfile": "מוסיף האם הקובץ נמצא ב־[[MediaWiki:Bad image list]]",
"apihelp-query+imageinfo-param-limit": "כמה גרסאות של קובץ לכל קובץ.",
- "apihelp-query+imageinfo-param-start": "×\9e×\90×\99×\96 ×\97×\95ת×\9dÖ¾×\96×\9e×\9f ×\9c×\94ת×\97×\99×\9c רשימה.",
+ "apihelp-query+imageinfo-param-start": "×\9e×\90×\99×\9c×\95 ת×\90ר×\99×\9a ×\95שע×\94 ×\9c×\94ת×\97×\99×\9c ×\90ת ×\94רשימה.",
"apihelp-query+imageinfo-param-end": "באיזה חותם־זמן לסיים את הרשימה.",
"apihelp-query+imageinfo-param-urlwidth": "אם מוגדר $2prop=url, יוחזר URL לתמונה שגודלה הותאם לרוחב הזה.\nמסיבות של ביצועים, אם האפשרות הזאת משמשת, לא יוחזרו יותר מ־$1 תמונות.",
"apihelp-query+imageinfo-param-urlheight": "דומה ל־$1urlwidth.",
const ROUND_BEGINNING = 'within-begin';
const ROUND_COMMITTING = 'within-commit';
const ROUND_ROLLING_BACK = 'within-rollback';
+ const ROUND_COMMIT_CALLBACKS = 'within-commit-callbacks';
+ const ROUND_ROLLBACK_CALLBACKS = 'within-rollback-callbacks';
private static $loggerFields =
[ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
// Actually perform the commit on all master DB connections and revert DBO_TRX
$this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
// Run all post-commit callbacks in a separate step
+ $this->trxRoundStage = self::ROUND_COMMIT_CALLBACKS;
$e = $this->executePostTransactionCallbacks();
$this->trxRoundStage = self::ROUND_CURSORY;
// Throw any last post-commit callback error
// Actually perform the rollback on all master DB connections and revert DBO_TRX
$this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] );
// Run all post-commit callbacks in a separate step
+ $this->trxRoundStage = self::ROUND_ROLLBACK_CALLBACKS;
$this->executePostTransactionCallbacks();
$this->trxRoundStage = self::ROUND_CURSORY;
}
* @return array
*/
final protected function baseLoadBalancerParams() {
+ if ( $this->trxRoundStage === self::ROUND_COMMIT_CALLBACKS ) {
+ $initStage = ILoadBalancer::STAGE_POSTCOMMIT_CALLBACKS;
+ } elseif ( $this->trxRoundStage === self::ROUND_ROLLBACK_CALLBACKS ) {
+ $initStage = ILoadBalancer::STAGE_POSTROLLBACK_CALLBACKS;
+ } else {
+ $initStage = null;
+ }
+
return [
'localDomain' => $this->localDomain,
'readOnlyReason' => $this->readOnlyReason,
// Defer ChronologyProtector construction in case setRequestInfo() ends up
// being called later (but before the first connection attempt) (T192611)
$this->getChronologyProtector()->initLB( $lb );
- }
+ },
+ 'roundStage' => $initStage
];
}
/** @var int Alias for CONN_TRX_AUTOCOMMIT for b/c; deprecated since 1.31 */
const CONN_TRX_AUTO = 1;
+ /** @var string Manager of ILoadBalancer instances is running post-commit callbacks */
+ const STAGE_POSTCOMMIT_CALLBACKS = 'stage-postcommit-callbacks';
+ /** @var string Manager of ILoadBalancer instances is running post-rollback callbacks */
+ const STAGE_POSTROLLBACK_CALLBACKS = 'stage-postrollback-callbacks';
+
/**
* Construct a manager of IDatabase connection objects
*
* - perfLogger: PSR-3 logger instance. [optional]
* - errorLogger : Callback that takes an Exception and logs it. [optional]
* - deprecationLogger: Callback to log a deprecation warning. [optional]
+ * - roundStage: STAGE_POSTCOMMIT_* class constant; for internal use [optional]
* @throws InvalidArgumentException
*/
public function __construct( array $params );
if ( isset( $params['chronologyCallback'] ) ) {
$this->chronologyCallback = $params['chronologyCallback'];
}
+
+ if ( isset( $params['roundStage'] ) ) {
+ if ( $params['roundStage'] === self::STAGE_POSTCOMMIT_CALLBACKS ) {
+ $this->trxRoundStage = self::ROUND_COMMIT_CALLBACKS;
+ } elseif ( $params['roundStage'] === self::STAGE_POSTROLLBACK_CALLBACKS ) {
+ $this->trxRoundStage = self::ROUND_ROLLBACK_CALLBACKS;
+ }
+ }
}
/**
* @param array|string $types
* @param string $user
* @param string $page
- * @param string $pattern
+ * @param bool $pattern
* @param int|string $year Use 0 to start with no year preselected.
* @param int|string $month A month in the 1..12 range. Use 0 to start with no month
* preselected.
* @param string $tagFilter Tag to select by default
* @param string $action
*/
- public function showOptions( $types = [], $user = '', $page = '', $pattern = '', $year = 0,
+ public function showOptions( $types = [], $user = '', $page = '', $pattern = false, $year = 0,
$month = 0, $filter = null, $tagFilter = '', $action = null
) {
global $wgScript, $wgMiserMode;
}
/**
- * @param string $pattern
+ * @param bool $pattern
* @return string Checkbox
*/
private function getTitlePattern( $pattern ) {
/** @var string|Title Events limited to those about Title when set */
private $title = '';
- /** @var string */
- private $pattern = '';
+ /** @var bool */
+ private $pattern = false;
/** @var string */
private $typeCGI = '';
* @param string|array $types Log types to show
* @param string $performer The user who made the log entries
* @param string|Title $title The page title the log entries are for
- * @param string $pattern Do a prefix search rather than an exact title match
+ * @param bool $pattern Do a prefix search rather than an exact title match
* @param array $conds Extra conditions for the query
* @param int|bool $year The year to start from. Default: false
* @param int|bool $month The month to start from. Default: false
* @param int $logId Log entry ID, to limit to a single log entry.
*/
public function __construct( $list, $types = [], $performer = '', $title = '',
- $pattern = '', $conds = [], $year = false, $month = false, $tagFilter = '',
+ $pattern = false, $conds = [], $year = false, $month = false, $tagFilter = '',
$action = '', $logId = false
) {
parent::__construct( $list->getContext() );
* (For the block and rights logs, this is a user page.)
*
* @param string|Title $page Title name
- * @param string $pattern
+ * @param bool $pattern
* @return void
*/
private function limitTitle( $page, $pattern ) {
return $this->title;
}
+ /**
+ * @return bool
+ */
public function getPattern() {
return $this->pattern;
}
"botpasswords-restriction-failed": "Уваход ня выкананы праз абмежаваньні на пароль робата",
"botpasswords-invalid-name": "Пададзенае імя ўдзельніка ня ўтрымлівае падзяляльнік для паролю робата («$1»).",
"botpasswords-not-exist": "Удзельнік «$1» ня мае паролю для робата з назвай «$2».",
+ "botpasswords-needs-reset": "Пароль для робата зь імем «$2» {{GENDER:$1|удзельніка|удзельніцы}} «$1» мусіць быць скінуты.",
"resetpass_forbidden": "Пароль ня можа быць зьменены",
"resetpass_forbidden-reason": "Паролі ня могуць быць зьмененыя: $1",
"resetpass-no-info": "Для непасрэднага доступу да гэтай старонкі Вам неабходна ўвайсьці ў сыстэму.",
"upload": "Загрузіць файл",
"uploadbtn": "Загрузіць файл",
"reuploaddesc": "Скасаваць загрузку і вярнуцца да формы загрузкі",
- "upload-tryagain": "Даслаць зьмененае апісаньне файла",
+ "upload-tryagain": "Даслаць зьмененае апісаньне файлу",
"upload-tryagain-nostash": "Даслаць паўторна загружаны файл і зьмененае апісаньне",
"uploadnologin": "Вы не ўвайшлі ў сыстэму",
"uploadnologintext": "Вам трэба $1, каб загружаць файлы.",
"recentchangesdays-max": "(найбольш $1 {{PLURAL:$1|дзень|дзён}})",
"recentchangescount": "Перадвызначаная колькасць правак для паказу:",
"prefs-help-recentchangescount": "Максімальная колькасць: 1000",
- "prefs-help-watchlist-token2": "Гэта сакрэтны ключ да сеціўнай стужкі з Вашага спіса назірання.\nКожны, хто ведае гэты ключ, будзе мець магчымасць чытаць Ваш спіс назірання, таму не дзяліцеся ім.\nКалі трэба, можна [[Special:ResetTokens|скінуць яго]].",
+ "prefs-help-watchlist-token2": "Гэта сакрэтны ключ да сеціўнай стужкі з Вашага спісу назірання.\nКожны, хто ведае гэты ключ, будзе мець магчымасць чытаць Ваш спіс назірання, таму не дзяліцеся ім.\nКалі трэба, можна [[Special:ResetTokens|скінуць яго]].",
"savedprefs": "Настройкі замацаваныя.",
"savedrights": "Групы {{GENDER:$1|ўдзельніка|ўдзельніцы}} $1 захаваныя.",
"timezonelegend": "Часавы пояс:",
"uploaded-event-handler-on-svg": "Устаноўка атрыбутаў апрацоўшчыка падзей <code>$1=\"$2\"</code> у SVG файле не дазваляецца.",
"uploaded-href-attribute-svg": "у SVG файлах атрыбутам href дазволены толькі мэты віду http:// або https://, знойдзена <code><$1 $2=\"$3\"></code>.",
"uploaded-href-unsafe-target-svg": "У ўкладзеным SVG файле знойдзена спасылка на небяспечныя звесткі: URI мэты <code><$1 $2=\"$3\"></code>.",
- "uploaded-animate-svg": "У ўкладзеным SVG файле знойдзены тэг \"animate\", здольны змяніць спасылку з дапамогай атрыбута \"from\" <code><$1 $2=\"$3\"></code>.",
+ "uploaded-animate-svg": "Ð\92а ўкладзеным SVG файле знойдзены тэг \"animate\", здольны змяніць спасылку з дапамогай атрыбута \"from\" <code><$1 $2=\"$3\"></code>.",
"uploaded-setting-event-handler-svg": "Устаноўка атрыбутаў апрацоўкі падзей заблакавана, у ўкладзеным SVG-файле знойдзены код <code><$1 $2=\"$3\"></code>.",
"uploaded-setting-href-svg": "Выкарыстанне тэга \"set\" для дадання атрыбута \"href\" у бацькоўскі элемент заблакавана.",
"uploaded-wrong-setting-svg": "Ужыванне тэга \"set\" для задання ў якасці мэты аддаленага адраса/звестак/сцэнарыя для любога атрыбута заблакавана. У ўкладзеным SVG-файле знойдзены <code><set to=\"$1\"></code>.",
"tooltip-ca-undelete": "Аднавіць праўкі, зробленыя на гэтай старонцы перад тым, як яна была сцёрта",
"tooltip-ca-move": "Перанесці гэтую старонку пад іншую назву",
"tooltip-ca-watch": "Дадаць гэтую старонку да свайго спісу назіраных старонак",
- "tooltip-ca-unwatch": "Выняць гэту старонку з вашага спіса назірання",
+ "tooltip-ca-unwatch": "Выняць гэтую старонку з Вашага спісу назірання",
"tooltip-search": "Знайсці ў {{SITENAME}}",
"tooltip-search-go": "Перайсці да старонкі з дакладна такой назвай, калі такая наогул існуе",
"tooltip-search-fulltext": "Знайсці гэты тэкст у тэкстах старонак",
"confirm-watch-button": "ОК",
"confirm-watch-top": "Дабавіць старонку ў спіс назірання",
"confirm-unwatch-button": "ОК",
- "confirm-unwatch-top": "Ð\92Ñ\8bнÑ\8fÑ\86Ñ\8c гÑ\8dÑ\82Ñ\83 Ñ\81Ñ\82аÑ\80онкÑ\83 з ваÑ\88ага Ñ\81пÑ\96Ñ\81а назірання?",
+ "confirm-unwatch-top": "Ð\92Ñ\8bнÑ\8fÑ\86Ñ\8c гÑ\8dÑ\82Ñ\83 Ñ\81Ñ\82аÑ\80онкÑ\83 з Ð\92аÑ\88ага Ñ\81пÑ\96Ñ\81Ñ\83 назірання?",
"confirm-rollback-button": "Добра",
"confirm-rollback-top": "Адкаціць праўкі гэтай старонкі?",
"quotation-marks": "«$1»",
"autosumm-replace": "Замена старонкі на '$1'",
"autoredircomment": "Перасылае да [[$1]]",
"autosumm-removed-redirect": "Выдаленае перенаправление на [[$1]]",
+ "autosumm-changed-redirect-target": "Мэта перасылкі зменена з [[$1]] на [[$2]]",
"autosumm-new": "Новая старонка: '$1'",
"autosumm-newblank": "Створана пустая старонка",
"size-bytes": "$1 {{PLURAL:$1|байт|байты|байтаў}}",
"watchlistedit-normal-legend": "Выдаленне складнікаў са спісу назірання",
"watchlistedit-normal-explain": "Назвы старонак з ліку назіраных паказаныя ніжэй. Каб нешта выдаліць, адзначце клетку побач з адпаведным радком, пасля чаго націсніце \"Выняць складнікі\". Таксама можна правіць гэты спіс непасрэдна, [[Special:EditWatchlist/raw|без афармлення]].",
"watchlistedit-normal-submit": "Выняць складнікі",
- "watchlistedit-normal-done": "Ð\97 ваÑ\88ага Ñ\81пÑ\96Ñ\81а назірання {{PLURAL:$1|быў выдалены|былі выдалены|было выдалена}} $1 {{PLURAL:$1|складнік|складнікі|складнікаў}}:",
+ "watchlistedit-normal-done": "Ð\97 Ð\92аÑ\88ага Ñ\81пÑ\96Ñ\81Ñ\83 назірання {{PLURAL:$1|быў выдалены|былі выдалены|было выдалена}} $1 {{PLURAL:$1|складнік|складнікі|складнікаў}}:",
"watchlistedit-raw-title": "Нефарматаваны спіс назірання",
"watchlistedit-raw-legend": "Правіць нефарматаваны спіс назірання",
"watchlistedit-raw-explain": "Назвы старонак з ліку назіраных паказаныя ніжэй, без афармлення, адна назва на адзін радок; такім чынам, спіс можна правіць як звычайны тэкст. Па сканчэнні націсніце \"{{int:Watchlistedit-raw-submit}}\". Таксама гэта можна зрабіць праз [[Special:EditWatchlist|стандартны інтэрфейс]].",
"logentry-delete-delete": "$1 {{GENDER:$2|выдаліў|выдаліла}} старонку $3",
"logentry-delete-delete_redir": "$1 {{GENDER:$2|выдаліў|выдаліла}} перанакіраванне $3 шляхам перазапісу",
"logentry-delete-restore": "$1 {{GENDER:$2|аднавіў|аднавіла}} старонку $3 ($4)",
+ "restore-count-revisions": "{{PLURAL:$1|1 версія|$1 версіі|$1 версій}}",
"logentry-delete-event": "$1 {{GENDER:$2|змяніў|змяніла}} бачнасць {{PLURAL:$5|запісу журнала|$5 запісаў журнала}} $3: $4",
"logentry-delete-revision": "$1 {{GENDER:$2|змяніў|змяніла}} бачнасць {{PLURAL:$5|версіі|$5 версій|$5 версій}} старонкі $3: $4",
"logentry-delete-event-legacy": "$1 {{GENDER:$2|змяніў|змяніла}} бачнасць запісаў журнала $3",
"longpageerror": "'''HIBA: Az általad beküldött szöveg {{PLURAL:$1|egy kilobájt|$1 kilobájt}} hosszú, ami több az engedélyezett {{PLURAL:$2|egy kilobájtnál|$2 kilobájtnál}}.\nA szerkesztést nem lehet elmenteni.'''",
"readonlywarning": "<strong>FIGYELMEZTETÉS: A wiki adatbázisát karbantartás miatt zárolták, ezért most nem fogod tudni elmenteni a szerkesztéseidet!</strong>\nA lap szövegét másold egy szövegfájlba, amit később felhasználhatsz!\n\nAz adatbázist lezáró rendszeradminisztrátor az alábbi magyarázatot adta: $1",
"protectedpagewarning": "<strong>Figyelem: Ez a lap védett, így csak adminisztrátori jogosultságokkal rendelkező szerkesztők módosíthatják.</strong>\nA legutolsó ide vonatkozó naplóbejegyzés alább látható:",
- "semiprotectedpagewarning": "'''Megjegyzés:''' ez a lap védett, így regisztrálatlan vagy újonnan regisztrált szerkesztők nem módosíthatják.",
+ "semiprotectedpagewarning": "<strong>Megjegyzés:</strong> ez a lap védett, így regisztrálatlan vagy újonnan regisztrált szerkesztők nem módosíthatják.\nA lapra vonatkozó utolsó naplóbejegyzés alább látható:",
"cascadeprotectedwarning": "<strong>Figyelem:</strong> ez a lap le van zárva, csak [[Special:ListGroupRights|megfelelő jogosultságú]] felhasználók szerkeszthetik, mert a következő kaszkádvédelemmel ellátott {{PLURAL:$1|lapon|lapokon}} be van illesztve:",
"titleprotectedwarning": "'''Figyelem: Ez a lap le van védve, így csak a [[Special:ListGroupRights|megfelelő jogosultságokkal]] rendelkező szerkesztők hozhatják létre.'''\nA legutolsó ide vonatkozó naplóbejegyzés alább látható:",
"templatesused": "A lapon használt {{PLURAL:$1|sablon|sablonok}}:",
"logout": "Fwuör",
"userlogout": "Fwuör",
"notloggedin": "I bátà bò",
+ "userlogin-joinproject": "Bàkọ {{SITENAME}}",
"createaccount": "Ké otụ buwa",
"createaccountmail": "na e-mail",
+ "createacct-benefit-heading": "{{SITENAME}} sì nà aka ndị dị kà gị.",
"createacct-benefit-body1": "{{PLURAL:$1|ḿmezi}}",
"badretype": "Mkpurụ okwu ejị a gafẹ é jëghị.",
"userexists": "Áhè ọ'bànifé tírí di na áká onye ozor.\nBíkó nwèré áhà nke ozor.",
"pageinfo-header-edits": "Mèzí ịta",
"pageinfo-length": "Ogologo ihü (na baitusu)",
"pageinfo-article-id": "ID Ihü",
+ "pageinfo-toolboxlink": "Nkàta ihu",
"pageinfo-redirectsto-info": "ọ́márí",
"pageinfo-contentpage-yes": "Eeh",
"pageinfo-protect-cascading-yes": "Eeh",
"userpage-userdoesnotexist-view": "«$1» яха дагара йоазув долаш дац.",
"clearyourcache": "<strong>Теркал де.</strong> Хетаргахьа, оагIув дIаязъяь яьлча шоай браузера кэш IоцIанъе езаргья шун, даь хувцамаш гургдолаш.\n* <strong>Firefox / Safari:</strong> <em>Shift</em> яха лак тоIояь лоаттаеш инструментий цхьа дакъа тIа тоIае <em>Обновить</em> е <em>Ctrl-F5</em> тоIае е <em>Ctrl-R</em> (<em>⌘-R</em> Mac тIа)\n* <strong>Google Chrome:</strong> ТоIае <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> Mac тIа)\n* <strong>Internet Explorer:</strong> <em>Ctrl</em> яха лак тоIояь лоаттаеш, тоIае <em>Обновить</em> е <em>Ctrl-F5</em> тоIае\n* <strong>Opera:</strong> ДехьагIо <em>Menu → Настройки</em> (<em>Opera → Настройки</em> Mac тIа), тIаккха <em>Безопасность → Очистить историю посещений → Кэшированные изображения и файлы</em>",
"note": "'''Белгалдоахар:'''",
- "previewnote": "'''Теркам бе, ер хьалххе бIаргтохар мара бац.'''\nХьа хувцамаш хIанза а дIаяздаь дац!",
- "continue-editing": "Хувцар кхы дIахо де",
+ "previewnote": "'''Теркам бе! Ер хьалххе бIаргтохар мара бац.'''\nХьа хувцамаш хIанзехьа кхы а дIаяздаь дац!",
+ "continue-editing": "Хувцам бергболаш дIахо дехьавáла",
"editing": "Хувцам: $1",
- "creating": "«$1» оагIув хьакхоллар",
- "editingsection": "Хувцам: $1 (оагӀон дáкъа)",
+ "creating": "«$1» яха оагIув хьакхоллар",
+ "editingsection": "Хувцар: $1 (оагӀон дáкъа)",
"editingcomment": "Хувцам: $1 (оагӀон керда дáкъа)",
"editconflict": "Хувцама вIашдухьалъоттам: $1",
"yourtext": "Хьа текст",
"아라",
"Macofe",
"Matma Rex",
- "Fitoschido"
+ "Fitoschido",
+ "Notanotheramy"
]
},
"tog-underline": "Garis ngisori pranala:",
"savechanges": "Simpen owahan",
"publishpage": "Babar kaca",
"publishchanges": "Babar owahan",
+ "publishchanges-start": "Babar owahan...",
"preview": "Pratuduh",
"showpreview": "Deleng pratuduh",
"showdiff": "Tuduhaké owahan",
"botpasswords-existing": "Aktuell Botpasswierder.",
"botpasswords-createnew": "En neit Botpasswuert uleeën",
"botpasswords-editexisting": "E Botpasswuert änneren",
+ "botpasswords-label-needsreset": "(Passwuert muss zréckgesat ginn)",
"botpasswords-label-appid": "Numm vum Bot:",
"botpasswords-label-create": "Uleeën",
"botpasswords-label-update": "Aktualiséieren",
"botpasswords-existing": "Senhas de robôs existentes",
"botpasswords-createnew": "Crie uma nova senha de robô",
"botpasswords-editexisting": "Editar uma senha de robô existente",
+ "botpasswords-label-needsreset": "(senha precisa ser redefinida)",
"botpasswords-label-appid": "Nome do robô:",
"botpasswords-label-create": "Criar",
"botpasswords-label-update": "Atualizar",
"botpasswords-existing": "Palavras-passe de robô existentes",
"botpasswords-createnew": "Criar uma nova palavra-passe para robô",
"botpasswords-editexisting": "Editar uma palavra-passe de robô existente",
+ "botpasswords-label-needsreset": "(a palavra-passe precisa ser redefinida)",
"botpasswords-label-appid": "Nome do robô:",
"botpasswords-label-create": "Criar",
"botpasswords-label-update": "Atualizar",
"botpasswords-restriction-failed": "Restrições da palavra-passe de robô impedem esta autenticação.",
"botpasswords-invalid-name": "O nome de utilizador especificado não contém o separador de palavra-passe de robô (\"$1\").",
"botpasswords-not-exist": "O utilizador \"$1\" não tem uma palavra-passe para o robô chamado \"$2\".",
+ "botpasswords-needs-reset": "A palavra-passe de robô, para o robô de nome \"$2\" {{GENDER:$1|do utilizador|da utilizadora}} \"$1\" deve ser redefinida.",
"resetpass_forbidden": "As palavras-passe não podem ser alteradas",
"resetpass_forbidden-reason": "As palavras-passe não podem ser alteradas: $1",
"resetpass-no-info": "Precisa de iniciar sessão para aceder diretamente a esta página.",
"recentchangeslinked-feed": "Alterações relacionadas",
"recentchangeslinked-toolbox": "Alterações relacionadas",
"recentchangeslinked-title": "Alterações relacionadas com \"$1\"",
- "recentchangeslinked-summary": "Introduza o nome de uma página para ver as mudanças a todas as páginas que contêm hiperligações para ela ou para as quais a página fornecida contém hiperligações (para ver as que pertencem a uma categoria, introduza Categoria:Nome da categoria). As mudanças às suas [[Special:Watchlist|páginas vigiadas]] aparecem a <strong>negrito</strong>.",
+ "recentchangeslinked-summary": "Introduza o nome de uma página para ver as mudanças a todas as páginas que contêm hiperligações para ela ou para as quais a página fornecida contém hiperligações (para ver as que pertencem a uma categoria, introduza {{ns:category}}:Nome da categoria). As mudanças às suas [[Special:Watchlist|páginas vigiadas]] aparecem a <strong>negrito</strong>.",
"recentchangeslinked-page": "Nome da página:",
"recentchangeslinked-to": "Inversamente, mostrar mudanças às páginas que contêm hiperligações para esta",
"recentchanges-page-added-to-category": "[[:$1]] foi adicionada à categoria",
"botpasswords-existing": "Существующие пароли бота",
"botpasswords-createnew": "Создать новый пароль бота",
"botpasswords-editexisting": "Редактировать существующий пароль бота",
+ "botpasswords-label-needsreset": "(пароль должен быть сброшен)",
"botpasswords-label-appid": "Название бота:",
"botpasswords-label-create": "Создать",
"botpasswords-label-update": "Обновить",
"botpasswords-restriction-failed": "Из-за ограничений, связанных с паролем бота, вход не произведён.",
"botpasswords-invalid-name": "Указанное имя участника не содержит разделителя для пароля бота («$1»).",
"botpasswords-not-exist": "У участника «$1» нет пароля для бота с названием «$2».",
+ "botpasswords-needs-reset": "Пароль для бота «$1» {{GENDER:$2|участника|участницы}} «$2» должен быть сброшен.",
"resetpass_forbidden": "Пароль не может быть изменён",
"resetpass_forbidden-reason": "Пароли не могут быть изменены: $1",
"resetpass-no-info": "Чтобы обращаться непосредственно к этой странице, вам следует представиться системе.",
"welcomecreation-msg": "Uotwarli my sam lo Ćebje kůnto.\nPamjyntej coby posztalować [[Special:Preferences|preferencyji]]",
"yourname": "Mjano użytkowńika:",
"userlogin-yourname": "Mjano używocza",
- "userlogin-yourname-ph": "Wszkryflej swoje mjano użytkowńika",
+ "userlogin-yourname-ph": "Wkludź swoje miano używacza",
"createacct-another-username-ph": "Wszkryflej mjano użytkowńika",
"yourpassword": "Hasło:",
"userlogin-yourpassword": "Hasło",
- "userlogin-yourpassword-ph": "Wszkryflej swoje hasło",
- "createacct-yourpassword-ph": "Wszkryflej hasło",
+ "userlogin-yourpassword-ph": "Wkludź swoje hasło",
+ "createacct-yourpassword-ph": "Wkludź hasło",
"yourpasswordagain": "Naszkryflej ausdruk zaś",
"createacct-yourpasswordagain": "Potwjyrdź hasło",
- "createacct-yourpasswordagain-ph": "Wszkryflej hasło jeszcze roz",
+ "createacct-yourpasswordagain-ph": "Wkludź hasło jeszcze rŏz",
"userlogin-remembermypassword": "Ńy wylogůwywuj mje",
"userlogin-signwithsecure": "Użyj bezpjecznygo połůnczyńa",
"yourdomainname": "Twoja domyna",
"userlogin-createanother": "Twůrz inksze kůnto",
"createacct-emailrequired": "E-brif",
"createacct-emailoptional": "E-brif (uopcjůnalne)",
- "createacct-email-ph": "Wszkryflej swůj adres do e-brifa",
+ "createacct-email-ph": "Wkludź swojã adresã e-brifa",
"createacct-another-email-ph": "Nastow e-brif",
"createaccountmail": "Użyj chwilowygo hasła losowo genyrowanygo a wyślij je na wrychtowany adres e-brifa.",
"createacct-realname": "Prawdźiwe imje a nazwisko (uopcjůnalńe)",
--- /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
+ *
+ */
+use Wikimedia\Purtle\RdfWriter;
+use Wikimedia\Purtle\TurtleRdfWriter;
+use Wikimedia\Rdbms\IDatabase;
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script to provide RDF representation of the recent changes in category tree.
+ *
+ * @ingroup Maintenance
+ * @since 1.30
+ */
+class CategoryChangesAsRdf extends Maintenance {
+ /**
+ * Insert query
+ */
+ const SPARQL_INSERT = <<<SPARQL
+INSERT DATA {
+%s
+};
+
+SPARQL;
+
+ /**
+ * Delete/Insert query
+ */
+ const SPARQL_DELETE_INSERT = <<<SPARQLDI
+DELETE {
+?category ?x ?y
+} INSERT {
+%s
+} WHERE {
+ VALUES ?category {
+ %s
+ }
+};
+
+SPARQLDI;
+
+ /**
+ * @var RdfWriter
+ */
+ private $rdfWriter;
+ /**
+ * Categories RDF helper.
+ * @var CategoriesRdf
+ */
+ private $categoriesRdf;
+
+ private $startTS;
+ private $endTS;
+
+ /**
+ * List of processed page IDs,
+ * so we don't try to process same thing twice
+ * @var int[]
+ */
+ protected $processed = [];
+
+ public function __construct() {
+ parent::__construct();
+
+ $this->addDescription( "Generate RDF dump of category changes in a wiki." );
+
+ $this->setBatchSize( 200 );
+ $this->addOption( 'output', "Output file (default is stdout). Will be overwritten.", false,
+ true, 'o' );
+ $this->addOption( 'start', 'Starting timestamp (inclusive), in ISO or Mediawiki format.',
+ true, true, 's' );
+ $this->addOption( 'end', 'Ending timestamp (exclusive), in ISO or Mediawiki format.', true,
+ true, 'e' );
+ }
+
+ /**
+ * Initialize external service classes.
+ */
+ public function initialize() {
+ // SPARQL Update syntax is close to Turtle format, so we can use Turtle writer.
+ $this->rdfWriter = new TurtleRdfWriter();
+ $this->categoriesRdf = new CategoriesRdf( $this->rdfWriter );
+ }
+
+ public function execute() {
+ global $wgRCMaxAge;
+
+ $this->initialize();
+
+ $startTS = new MWTimestamp( $this->getOption( "start" ) );
+ $endTS = new MWTimestamp( $this->getOption( "end" ) );
+ $now = new MWTimestamp();
+
+ if ( $now->getTimestamp() - $startTS->getTimestamp() > $wgRCMaxAge ) {
+ $this->error( "Start timestamp too old, maximum RC age is $wgRCMaxAge!" );
+ }
+ if ( $now->getTimestamp() - $endTS->getTimestamp() > $wgRCMaxAge ) {
+ $this->error( "End timestamp too old, maximum RC age is $wgRCMaxAge!" );
+ }
+
+ $this->startTS = $startTS->getTimestamp();
+ $this->endTS = $endTS->getTimestamp();
+
+ $outFile = $this->getOption( 'output', 'php://stdout' );
+ if ( $outFile === '-' ) {
+ $outFile = 'php://stdout';
+ }
+
+ $output = fopen( $outFile, 'wb' );
+
+ $this->categoriesRdf->setupPrefixes();
+ $this->rdfWriter->start();
+
+ $prefixes = $this->getRdf();
+ // We have to strip @ from prefix, since SPARQL UPDATE doesn't use them
+ // Also strip dot at the end.
+ $prefixes = preg_replace( [ '/^@/m', '/\s*[.]$/m' ], '', $prefixes );
+ fwrite( $output, $prefixes );
+
+ $dbr = $this->getDB( DB_REPLICA, [ 'vslow' ] );
+
+ // Deletes go first because if the page was deleted, other changes
+ // do not matter. This only gets true deletes, i.e. not pages that were restored.
+ $this->handleDeletes( $dbr, $output );
+ // Moves go before additions because if category is moved, we should not process creation
+ // as it would produce wrong data - because create row has old title
+ $this->handleMoves( $dbr, $output );
+ // We need to handle restores too since delete may have happened in previous update.
+ $this->handleRestores( $dbr, $output );
+ $this->handleAdds( $dbr, $output );
+ $this->handleChanges( $dbr, $output );
+
+ // Update timestamp
+ fwrite( $output, $this->updateTS( $this->endTS ) );
+ }
+
+ /**
+ * Get SPARQL for updating set of categories
+ * @param IDatabase $dbr
+ * @param string[] $deleteUrls List of URIs to be deleted, with <>
+ * @param string[] $pages List of categories: id => title
+ * @param string $mark Marks which operation requests the query
+ * @return string SPARQL query
+ */
+ private function getCategoriesUpdate( IDatabase $dbr, $deleteUrls, $pages, $mark ) {
+ if ( empty( $deleteUrls ) ) {
+ return "";
+ }
+
+ if ( !empty( $pages ) ) {
+ $this->writeParentCategories( $dbr, $pages );
+ }
+
+ return "# $mark\n" . sprintf( self::SPARQL_DELETE_INSERT,
+ $this->getRdf(),
+ implode( ' ', $deleteUrls ) );
+ }
+
+ /**
+ * Write data for a set of categories
+ * @param IDatabase $dbr
+ * @param string[] $pages List of categories: id => title
+ */
+ private function writeParentCategories( IDatabase $dbr, $pages ) {
+ foreach ( $this->getCategoryLinksIterator( $dbr, array_keys( $pages ) ) as $row ) {
+ $this->categoriesRdf->writeCategoryLinkData( $pages[$row->cl_from], $row->cl_to );
+ }
+ }
+
+ /**
+ * Generate SPARQL Update code for updating dump timestamp
+ * @param string|int $timestamp Timestamp for last change
+ * @return string SPARQL Update query for timestamp.
+ */
+ public function updateTS( $timestamp ) {
+ $dumpUrl = '<' . $this->categoriesRdf->getDumpURI() . '>';
+ $ts = wfTimestamp( TS_ISO_8601, $timestamp );
+ $tsQuery = <<<SPARQL
+DELETE {
+ $dumpUrl schema:dateModified ?o .
+}
+WHERE {
+ $dumpUrl schema:dateModified ?o .
+};
+INSERT DATA {
+ $dumpUrl schema:dateModified "$ts"^^xsd:dateTime .
+}
+
+SPARQL;
+ return $tsQuery;
+ }
+
+ /**
+ * Set up standard iterator for retrieving category changes.
+ * @param IDatabase $dbr
+ * @param string[] $columns List of additional fields to get
+ * @param string[] $extra_tables List of additional tables to join
+ * @return BatchRowIterator
+ */
+ private function setupChangesIterator(
+ IDatabase $dbr,
+ array $columns = [],
+ array $extra_tables = []
+ ) {
+ $tables = [ 'recentchanges', 'page_props', 'category' ];
+ if ( $extra_tables ) {
+ $tables += $extra_tables;
+ }
+ $it = new BatchRowIterator( $dbr,
+ $tables,
+ [ 'rc_timestamp' ],
+ $this->mBatchSize
+ );
+ $this->addTimestampConditions( $it, $dbr );
+ $it->addJoinConditions(
+ [
+ 'page_props' => [
+ 'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = rc_cur_id' ]
+ ],
+ 'category' => [
+ 'LEFT JOIN', [ 'cat_title = rc_title' ]
+ ]
+ ]
+ );
+ $it->setFetchColumns( array_merge( $columns, [
+ 'rc_title',
+ 'rc_cur_id',
+ 'pp_propname',
+ 'cat_pages',
+ 'cat_subcats',
+ 'cat_files'
+ ] ) );
+ return $it;
+ }
+
+ /**
+ * Fetch newly created categories
+ * @param IDatabase $dbr
+ * @return BatchRowIterator
+ */
+ protected function getNewCatsIterator( IDatabase $dbr ) {
+ $it = $this->setupChangesIterator( $dbr );
+ $it->addConditions( [
+ 'rc_namespace' => NS_CATEGORY,
+ 'rc_new' => 1,
+ ] );
+ return $it;
+ }
+
+ /**
+ * Fetch moved categories
+ * @param IDatabase $dbr
+ * @return BatchRowIterator
+ */
+ protected function getMovedCatsIterator( IDatabase $dbr ) {
+ $it = $this->setupChangesIterator( $dbr, [ 'page_title', 'page_namespace' ], [ 'page' ] );
+ $it->addConditions( [
+ 'rc_namespace' => NS_CATEGORY,
+ 'rc_new' => 0,
+ 'rc_log_type' => 'move',
+ 'rc_type' => RC_LOG,
+ ] );
+ $it->addJoinConditions( [
+ 'page' => [ 'INNER JOIN', 'rc_cur_id = page_id' ],
+ ] );
+ $this->addIndex( $it );
+ return $it;
+ }
+
+ /**
+ * Fetch deleted categories
+ * @param IDatabase $dbr
+ * @return BatchRowIterator
+ */
+ protected function getDeletedCatsIterator( IDatabase $dbr ) {
+ $it = new BatchRowIterator( $dbr,
+ 'recentchanges',
+ [ 'rc_timestamp' ],
+ $this->mBatchSize
+ );
+ $this->addTimestampConditions( $it, $dbr );
+ $it->addConditions( [
+ 'rc_namespace' => NS_CATEGORY,
+ 'rc_new' => 0,
+ 'rc_log_type' => 'delete',
+ 'rc_log_action' => 'delete',
+ 'rc_type' => RC_LOG,
+ // We will fetch ones that do not have page record. If they do,
+ // this means they were restored, thus restoring handler will pick it up.
+ 'NOT EXISTS (SELECT * FROM page WHERE page_id = rc_cur_id)',
+ ] );
+ $this->addIndex( $it );
+ $it->setFetchColumns( [ 'rc_cur_id', 'rc_title' ] );
+ return $it;
+ }
+
+ /**
+ * Fetch restored categories
+ * @param IDatabase $dbr
+ * @return BatchRowIterator
+ */
+ protected function getRestoredCatsIterator( IDatabase $dbr ) {
+ $it = $this->setupChangesIterator( $dbr );
+ $it->addConditions( [
+ 'rc_namespace' => NS_CATEGORY,
+ 'rc_new' => 0,
+ 'rc_log_type' => 'delete',
+ 'rc_log_action' => 'restore',
+ 'rc_type' => RC_LOG,
+ // We will only fetch ones that have page record
+ 'EXISTS (SELECT page_id FROM page WHERE page_id = rc_cur_id)',
+ ] );
+ $this->addIndex( $it );
+ return $it;
+ }
+
+ /**
+ * Fetch categorization changes
+ * @param IDatabase $dbr
+ * @return BatchRowIterator
+ */
+ protected function getChangedCatsIterator( IDatabase $dbr ) {
+ $it = $this->setupChangesIterator( $dbr );
+ $it->addConditions( [
+ 'rc_namespace' => NS_CATEGORY,
+ 'rc_new' => 0,
+ 'rc_type' => [ RC_EDIT, RC_CATEGORIZE ],
+ ] );
+ $this->addIndex( $it );
+ return $it;
+ }
+
+ /**
+ * Add timestamp limits to iterator
+ * @param BatchRowIterator $it Iterator
+ * @param IDatabase $dbr
+ */
+ private function addTimestampConditions( BatchRowIterator $it, IDatabase $dbr ) {
+ $it->addConditions( [
+ 'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( $this->startTS ) ),
+ 'rc_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $this->endTS ) ),
+ ] );
+ }
+
+ /**
+ * Need to force index, somehow on terbium the optimizer chooses wrong one
+ * @param BatchRowIterator $it
+ */
+ private function addIndex( BatchRowIterator $it ) {
+ $it->addOptions( [
+ 'USE INDEX' => [ 'recentchanges' => 'new_name_timestamp' ]
+ ] );
+ }
+
+ /**
+ * Get iterator for links for categories.
+ * @param IDatabase $dbr
+ * @param array $ids List of page IDs
+ * @return Traversable
+ */
+ protected function getCategoryLinksIterator( IDatabase $dbr, array $ids ) {
+ $it = new BatchRowIterator(
+ $dbr,
+ 'categorylinks',
+ [ 'cl_from', 'cl_to' ],
+ $this->mBatchSize
+ );
+ $it->addConditions( [
+ 'cl_type' => 'subcat',
+ 'cl_from' => $ids
+ ] );
+ $it->setFetchColumns( [ 'cl_from', 'cl_to' ] );
+ return new RecursiveIteratorIterator( $it );
+ }
+
+ /**
+ * Get accumulated RDF.
+ * @return string
+ */
+ public function getRdf() {
+ return $this->rdfWriter->drain();
+ }
+
+ /**
+ * Handle category deletes.
+ * @param IDatabase $dbr
+ * @param resource $output File to write the output
+ */
+ public function handleDeletes( IDatabase $dbr, $output ) {
+ // This only does "true" deletes - i.e. those that the page stays deleted
+ foreach ( $this->getDeletedCatsIterator( $dbr ) as $batch ) {
+ $deleteUrls = [];
+ foreach ( $batch as $row ) {
+ // This can produce duplicates, we don't care
+ $deleteUrls[] = '<' . $this->categoriesRdf->labelToUrl( $row->rc_title ) . '>';
+ $this->processed[$row->rc_cur_id] = true;
+ }
+ fwrite( $output, $this->getCategoriesUpdate( $dbr, $deleteUrls, [], "Deletes" ) );
+ }
+ }
+
+ /**
+ * Write category data to RDF.
+ * @param stdclass $row Database row
+ */
+ private function writeCategoryData( $row ) {
+ $this->categoriesRdf->writeCategoryData(
+ $row->rc_title,
+ $row->pp_propname === 'hiddencat',
+ (int)$row->cat_pages - (int)$row->cat_subcats - (int)$row->cat_files,
+ (int)$row->cat_subcats
+ );
+ }
+
+ /**
+ * @param IDatabase $dbr
+ * @param resource $output
+ */
+ public function handleMoves( IDatabase $dbr, $output ) {
+ foreach ( $this->getMovedCatsIterator( $dbr ) as $batch ) {
+ $pages = [];
+ $deleteUrls = [];
+ foreach ( $batch as $row ) {
+ $deleteUrls[] = '<' . $this->categoriesRdf->labelToUrl( $row->rc_title ) . '>';
+
+ if ( isset( $this->processed[$row->rc_cur_id] ) ) {
+ // We already captured this one before
+ continue;
+ }
+
+ if ( $row->page_namespace != NS_CATEGORY ) {
+ // If page was moved out of Category:, we'll just delete
+ continue;
+ }
+ $row->rc_title = $row->page_title;
+ $this->writeCategoryData( $row );
+ $pages[$row->rc_cur_id] = $row->page_title;
+ $this->processed[$row->rc_cur_id] = true;
+ }
+
+ fwrite( $output, $this->getCategoriesUpdate( $dbr, $deleteUrls, $pages, "Moves" ) );
+ }
+ }
+
+ /**
+ * @param IDatabase $dbr
+ * @param resource $output
+ */
+ public function handleRestores( IDatabase $dbr, $output ) {
+ fwrite( $output, "# Restores\n" );
+ // This will only find those restores that were not deleted later.
+ foreach ( $this->getRestoredCatsIterator( $dbr ) as $batch ) {
+ $pages = [];
+ foreach ( $batch as $row ) {
+ if ( isset( $this->processed[$row->rc_cur_id] ) ) {
+ // We already captured this one before
+ continue;
+ }
+ $this->writeCategoryData( $row );
+ $pages[$row->rc_cur_id] = $row->rc_title;
+ $this->processed[$row->rc_cur_id] = true;
+ }
+
+ if ( empty( $pages ) ) {
+ continue;
+ }
+
+ $this->writeParentCategories( $dbr, $pages );
+
+ fwrite( $output, sprintf( self::SPARQL_INSERT, $this->getRdf() ) );
+ }
+ }
+
+ /**
+ * @param IDatabase $dbr
+ * @param resource $output
+ */
+ public function handleAdds( IDatabase $dbr, $output ) {
+ fwrite( $output, "# Additions\n" );
+ foreach ( $this->getNewCatsIterator( $dbr ) as $batch ) {
+ $pages = [];
+ foreach ( $batch as $row ) {
+ if ( isset( $this->processed[$row->rc_cur_id] ) ) {
+ // We already captured this one before
+ continue;
+ }
+ $this->writeCategoryData( $row );
+ $pages[$row->rc_cur_id] = $row->rc_title;
+ $this->processed[$row->rc_cur_id] = true;
+ }
+
+ if ( empty( $pages ) ) {
+ continue;
+ }
+
+ $this->writeParentCategories( $dbr, $pages );
+ fwrite( $output, sprintf( self::SPARQL_INSERT, $this->getRdf() ) );
+ }
+ }
+
+ /**
+ * @param IDatabase $dbr
+ * @param resource $output
+ */
+ public function handleChanges( IDatabase $dbr, $output ) {
+ foreach ( $this->getChangedCatsIterator( $dbr ) as $batch ) {
+ $pages = [];
+ $deleteUrls = [];
+ foreach ( $batch as $row ) {
+ if ( isset( $this->processed[$row->rc_cur_id] ) ) {
+ // We already captured this one before
+ continue;
+ }
+ $this->writeCategoryData( $row );
+ $pages[$row->rc_cur_id] = $row->rc_title;
+ $this->processed[$row->rc_cur_id] = true;
+ $deleteUrls[] = '<' . $this->categoriesRdf->labelToUrl( $row->rc_title ) . '>';
+ }
+
+ fwrite( $output, $this->getCategoriesUpdate( $dbr, $deleteUrls, $pages, "Changes" ) );
+ }
+ }
+}
+
+$maintClass = CategoryChangesAsRdf::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
"grunt-jsonlint": "1.1.0",
"grunt-karma": "2.0.0",
"grunt-stylelint": "0.10.0",
- "karma": "1.7.1",
+ "karma": "2.0.2",
"karma-chrome-launcher": "2.2.0",
"karma-firefox-launcher": "1.0.1",
"karma-mocha-reporter": "2.2.5",
"karma-qunit": "2.0.1",
"postcss-less": "1.1.5",
- "qunit": "2.5.0",
+ "qunit": "2.6.0",
"stylelint": "9.2.0",
"stylelint-config-wikimedia": "0.4.3",
"wdio-junit-reporter": "0.2.0",
&.mw-halign-right {
/* @noflip */
- margin: 0.5em 0 1.3em 1.4em;
+ margin: 0 0 0.5em 0.5em;
/* @noflip */
clear: right;
/* @noflip */
&.mw-halign-left {
/* @noflip */
- margin: 0.5em 1.4em 1.3em 0;
+ margin: 0 0.5em 0.5em 0;
/* @noflip */
clear: left;
/* @noflip */
clear: right;
float: right;
+ &.mw-halign-left {
+ /* @noflip */
+ margin: 0.5em 1.4em 1.3em 0;
+ }
+ &.mw-halign-right {
+ /* @noflip */
+ margin: 0.5em 0 1.3em 1.4em;
+ }
+
> *:first-child {
> img,
> video {
--- /dev/null
+# Changes
+DELETE {
+?category ?x ?y
+} INSERT {
+
+<http://acme.test/wiki/Category:Changed_category> a mediawiki:Category ;
+ rdfs:label "Changed category" ;
+ mediawiki:pages "7"^^xsd:integer ;
+ mediawiki:subcategories "2"^^xsd:integer ;
+ mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_30> .
+
+} WHERE {
+ VALUES ?category {
+ <http://acme.test/wiki/Category:Changed_category>
+ }
+};
--- /dev/null
+# Deletes
+DELETE {
+?category ?x ?y
+} INSERT {
+
+} WHERE {
+ VALUES ?category {
+ <http://acme.test/wiki/Category:Test> <http://acme.test/wiki/Category:Test_2>
+ }
+};
--- /dev/null
+# Moves
+DELETE {
+?category ?x ?y
+} INSERT {
+
+<http://acme.test/wiki/Category:MovedTo> a mediawiki:Category ;
+ rdfs:label "MovedTo" ;
+ mediawiki:pages "7"^^xsd:integer ;
+ mediawiki:subcategories "2"^^xsd:integer .
+
+<http://acme.test/wiki/Category:AlsoMoved> a mediawiki:Category ;
+ rdfs:label "AlsoMoved" ;
+ mediawiki:pages "7"^^xsd:integer ;
+ mediawiki:subcategories "2"^^xsd:integer .
+
+<http://acme.test/wiki/Category:MovedTo> mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_4> .
+
+<http://acme.test/wiki/Category:AlsoMoved> mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_5> .
+
+} WHERE {
+ VALUES ?category {
+ <http://acme.test/wiki/Category:Test> <http://acme.test/wiki/Category:MovedTo> <http://acme.test/wiki/Category:Test_2> <http://acme.test/wiki/Category:Test_3> <http://acme.test/wiki/Category:Test_4>
+ }
+};
--- /dev/null
+# Additions
+INSERT DATA {
+
+<http://acme.test/wiki/Category:New_category> a mediawiki:Category ;
+ rdfs:label "New category" ;
+ mediawiki:pages "7"^^xsd:integer ;
+ mediawiki:subcategories "2"^^xsd:integer .
+
+<http://acme.test/wiki/Category:%D0%9D%D0%BE%D0%B2%D0%B0%D1%8F_%D0%BA%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F_%F0%9F%98%83> a mediawiki:Category,
+ mediawiki:HiddenCategory ;
+ rdfs:label "Новая категория 😃" ;
+ mediawiki:pages "7"^^xsd:integer ;
+ mediawiki:subcategories "2"^^xsd:integer .
+
+<http://acme.test/wiki/Category:New_category> mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_20> .
+
+<http://acme.test/wiki/Category:%D0%9D%D0%BE%D0%B2%D0%B0%D1%8F_%D0%BA%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F_%F0%9F%98%83> mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_21> .
+
+};
--- /dev/null
+# Restores
+INSERT DATA {
+
+<http://acme.test/wiki/Category:Restored_cat> a mediawiki:Category ;
+ rdfs:label "Restored cat" ;
+ mediawiki:pages "7"^^xsd:integer ;
+ mediawiki:subcategories "2"^^xsd:integer ;
+ mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_10> .
+
+};
--- /dev/null
+DELETE {
+ <http://acme.test/wiki/Special:CategoryDump> schema:dateModified ?o .
+}
+WHERE {
+ <http://acme.test/wiki/Special:CategoryDump> schema:dateModified ?o .
+};
+INSERT DATA {
+ <http://acme.test/wiki/Special:CategoryDump> schema:dateModified "2017-08-25T00:29:09Z"^^xsd:dateTime .
+}
$lb->closeAll();
}
- public function testLBFactoryMulti() {
- global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
+ public function testLBFactoryMultiConns() {
+ $factory = $this->newLBFactoryMultiLBs();
- $factory = new LBFactoryMulti( [
- 'sectionsByDB' => [
- 's1wiki' => 's1',
- ],
- 'sectionLoads' => [
- 's1' => [
- 'test-db3' => 0,
- 'test-db4' => 100,
- ],
- 'DEFAULT' => [
- 'test-db1' => 0,
- 'test-db2' => 100,
- ]
- ],
- 'serverTemplate' => [
- 'dbname' => $wgDBname,
- 'user' => $wgDBuser,
- 'password' => $wgDBpassword,
- 'type' => $wgDBtype,
- 'dbDirectory' => $wgSQLiteDataDir,
- 'flags' => DBO_DEFAULT
- ],
- 'hostsByName' => [
- 'test-db1' => $wgDBserver,
- 'test-db2' => $wgDBserver,
- 'test-db3' => $wgDBserver,
- 'test-db4' => $wgDBserver
- ],
- 'loadMonitorClass' => LoadMonitorNull::class
- ] );
- $lb = $factory->getMainLB();
-
- $dbw = $lb->getConnection( DB_MASTER );
+ $dbw = $factory->getMainLB()->getConnection( DB_MASTER );
$this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
- $dbr = $lb->getConnection( DB_REPLICA );
+ $dbr = $factory->getMainLB()->getConnection( DB_REPLICA );
$this->assertTrue( $dbr->getLBInfo( 'replica' ), 'slave shows as slave' );
- // Test that LoadBalancer instances made during commitMasterChanges() do not throw
- // DBTransactionError due to transaction ROUND_* stages being mismatched.
+ // Destructor should trigger without round stage errors
+ unset( $factory );
+ }
+
+ public function testLBFactoryMultiRoundCallbacks() {
+ $called = 0;
+ $countLBsFunc = function ( LBFactoryMulti $factory ) {
+ $count = 0;
+ $factory->forEachLB( function () use ( &$count ) {
+ ++$count;
+ } );
+
+ return $count;
+ };
+
+ $factory = $this->newLBFactoryMultiLBs();
+ $this->assertEquals( 0, $countLBsFunc( $factory ) );
+ $dbw = $factory->getMainLB()->getConnection( DB_MASTER );
+ $this->assertEquals( 1, $countLBsFunc( $factory ) );
+ // Test that LoadBalancer instances made during pre-commit callbacks in do not
+ // throw DBTransactionError due to transaction ROUND_* stages being mismatched.
$factory->beginMasterChanges( __METHOD__ );
- $dbw->onTransactionPreCommitOrIdle( function () use ( $factory ) {
+ $dbw->onTransactionPreCommitOrIdle( function () use ( $factory, &$called ) {
+ ++$called;
// Trigger s1 LoadBalancer instantiation during "finalize" stage.
// There is no s1wiki DB to select so it is not in getConnection(),
// but this fools getMainLB() at least.
$factory->getMainLB( 's1wiki' )->getConnection( DB_MASTER );
} );
$factory->commitMasterChanges( __METHOD__ );
+ $this->assertEquals( 1, $called );
+ $this->assertEquals( 2, $countLBsFunc( $factory ) );
+ $factory->shutdown();
+ $factory->closeAll();
- $count = 0;
- $factory->forEachLB( function () use ( &$count ) {
- ++$count;
+ $called = 0;
+ $factory = $this->newLBFactoryMultiLBs();
+ $this->assertEquals( 0, $countLBsFunc( $factory ) );
+ $dbw = $factory->getMainLB()->getConnection( DB_MASTER );
+ $this->assertEquals( 1, $countLBsFunc( $factory ) );
+ // Test that LoadBalancer instances made during pre-commit callbacks in do not
+ // throw DBTransactionError due to transaction ROUND_* stages being mismatched.hrow
+ // DBTransactionError due to transaction ROUND_* stages being mismatched.
+ $factory->beginMasterChanges( __METHOD__ );
+ $dbw->query( "SELECT 1 as t", __METHOD__ );
+ $dbw->onTransactionResolution( function () use ( $factory, &$called ) {
+ ++$called;
+ // Trigger s1 LoadBalancer instantiation during "finalize" stage.
+ // There is no s1wiki DB to select so it is not in getConnection(),
+ // but this fools getMainLB() at least.
+ $factory->getMainLB( 's1wiki' )->getConnection( DB_MASTER );
} );
- $this->assertEquals( 2, $count );
+ $factory->commitMasterChanges( __METHOD__ );
+ $this->assertEquals( 1, $called );
+ $this->assertEquals( 2, $countLBsFunc( $factory ) );
+ $factory->shutdown();
+ $factory->closeAll();
+ $factory = $this->newLBFactoryMultiLBs();
+ $dbw = $factory->getMainLB()->getConnection( DB_MASTER );
// DBTransactionError should not be thrown
$ran = 0;
$dbw->onTransactionPreCommitOrIdle( function () use ( &$ran ) {
$factory->closeAll();
}
+ private function newLBFactoryMultiLBs() {
+ global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
+
+ return new LBFactoryMulti( [
+ 'sectionsByDB' => [
+ 's1wiki' => 's1',
+ ],
+ 'sectionLoads' => [
+ 's1' => [
+ 'test-db3' => 0,
+ 'test-db4' => 100,
+ ],
+ 'DEFAULT' => [
+ 'test-db1' => 0,
+ 'test-db2' => 100,
+ ]
+ ],
+ 'serverTemplate' => [
+ 'dbname' => $wgDBname,
+ 'user' => $wgDBuser,
+ 'password' => $wgDBpassword,
+ 'type' => $wgDBtype,
+ 'dbDirectory' => $wgSQLiteDataDir,
+ 'flags' => DBO_DEFAULT
+ ],
+ 'hostsByName' => [
+ 'test-db1' => $wgDBserver,
+ 'test-db2' => $wgDBserver,
+ 'test-db3' => $wgDBserver,
+ 'test-db4' => $wgDBserver
+ ],
+ 'loadMonitorClass' => LoadMonitorNull::class
+ ] );
+ }
+
/**
* @covers \Wikimedia\Rdbms\ChronologyProtector
*/
--- /dev/null
+<?php
+
+/**
+ * Tests for CategoryChangesAsRdf recent changes exporter.
+ * @covers CategoryChangesAsRdf
+ */
+class CategoryChangesRdfTest extends MediaWikiLangTestCase {
+
+ public function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( [
+ 'wgServer' => 'http://acme.test',
+ 'wgCanonicalServer' => 'http://acme.test',
+ 'wgArticlePath' => '/wiki/$1',
+ ] );
+ }
+
+ public function provideCategoryData() {
+ return [
+ 'delete category' => [
+ __DIR__ . "/../data/categoriesrdf/delete.sparql",
+ 'getDeletedCatsIterator',
+ 'handleDeletes',
+ [
+ (object)[ 'rc_title' => 'Test', 'rc_cur_id' => 1, '_processed' => 1 ],
+ (object)[ 'rc_title' => 'Test 2', 'rc_cur_id' => 2, '_processed' => 2 ],
+ ],
+ ],
+ 'move category' => [
+ __DIR__ . "/../data/categoriesrdf/move.sparql",
+ 'getMovedCatsIterator',
+ 'handleMoves',
+ [
+ (object)[
+ 'rc_title' => 'Test',
+ 'rc_cur_id' => 4,
+ 'page_title' => 'MovedTo',
+ 'page_namespace' => NS_CATEGORY,
+ '_processed' => 4,
+ 'pp_propname' => null,
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ (object)[
+ 'rc_title' => 'MovedTo',
+ 'rc_cur_id' => 4,
+ 'page_title' => 'MovedAgain',
+ 'page_namespace' => NS_CATEGORY,
+ 'pp_propname' => 'hiddencat',
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ (object)[
+ 'rc_title' => 'Test 2',
+ 'rc_cur_id' => 5,
+ 'page_title' => 'AlsoMoved',
+ 'page_namespace' => NS_CATEGORY,
+ '_processed' => 5,
+ 'pp_propname' => null,
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ (object)[
+ 'rc_title' => 'Test 3',
+ 'rc_cur_id' => 6,
+ 'page_title' => 'MovedOut',
+ 'page_namespace' => NS_MAIN,
+ 'pp_propname' => null,
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ (object)[
+ 'rc_title' => 'Test 4',
+ 'rc_cur_id' => 7,
+ 'page_title' => 'Already Done',
+ 'page_namespace' => NS_CATEGORY,
+ 'pp_propname' => null,
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ ],
+ [ 7 => true ],
+ ],
+ 'restore deleted category' => [
+ __DIR__ . "/../data/categoriesrdf/restore.sparql",
+ 'getRestoredCatsIterator',
+ 'handleRestores',
+ [
+ (object)[
+ 'rc_title' => 'Restored cat',
+ 'rc_cur_id' => 10,
+ '_processed' => 10,
+ 'pp_propname' => null,
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ (object)[
+ 'rc_title' => 'Restored again',
+ 'rc_cur_id' => 10,
+ 'pp_propname' => null,
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ (object)[
+ 'rc_title' => 'Already seen',
+ 'rc_cur_id' => 11,
+ 'pp_propname' => null,
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ ],
+ [ 11 => true ],
+ ],
+ 'new page' => [
+ __DIR__ . "/../data/categoriesrdf/new.sparql",
+ 'getNewCatsIterator',
+ 'handleAdds',
+ [
+ (object)[
+ 'rc_title' => 'New category',
+ 'rc_cur_id' => 20,
+ '_processed' => 20,
+ 'pp_propname' => null,
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ (object)[
+ 'rc_title' => 'Новая категория 😃',
+ 'rc_cur_id' => 21,
+ '_processed' => 21,
+ 'pp_propname' => 'hiddencat',
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ (object)[
+ 'rc_title' => 'Processed already',
+ 'rc_cur_id' => 22,
+ ],
+ ],
+ [ 22 => true ],
+ ],
+ 'change in categories' => [
+ __DIR__ . "/../data/categoriesrdf/change.sparql",
+ 'getChangedCatsIterator',
+ 'handleChanges',
+ [
+ (object)[
+ 'rc_title' => 'Changed category',
+ 'rc_cur_id' => 30,
+ '_processed' => 30,
+ 'pp_propname' => null,
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ (object)[
+ 'rc_title' => 'Changed again',
+ 'rc_cur_id' => 30,
+ 'pp_propname' => null,
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ (object)[
+ 'rc_title' => 'Processed already',
+ 'rc_cur_id' => 31,
+ 'pp_propname' => null,
+ 'cat_pages' => 10,
+ 'cat_subcats' => 2,
+ 'cat_files' => 1,
+ ],
+ ],
+ [ 31 => true ],
+ ],
+
+ ];
+ }
+
+ /**
+ * Mock category links iterator.
+ * @param $dbr
+ * @param array $ids
+ * @return array
+ */
+ public function getCategoryLinksIterator( $dbr, array $ids ) {
+ $res = [];
+ foreach ( $ids as $pageid ) {
+ $res[] = (object)[ 'cl_from' => $pageid, 'cl_to' => "Parent of $pageid" ];
+ }
+ return $res;
+ }
+
+ /**
+ * @dataProvider provideCategoryData
+ * @param string $testFileName Name of the test, defines filename with expected results.
+ * @param string $iterator Iterator method name to mock
+ * @param string $handler Handler method to call
+ * @param array $result Result to be returned from mock iterator
+ * @param array $preProcessed List of pre-processed items
+ */
+ public function testSparqlUpdate( $testFileName, $iterator, $handler, $result,
+ array $preProcessed = [] ) {
+ $dumpScript =
+ $this->getMockBuilder( CategoryChangesAsRdf::class )
+ ->setMethods( [ $iterator, 'getCategoryLinksIterator' ] )
+ ->getMock();
+
+ $dumpScript->expects( $this->any() )
+ ->method( 'getCategoryLinksIterator' )
+ ->willReturnCallback( [ $this, 'getCategoryLinksIterator' ] );
+
+ $dumpScript->expects( $this->once() )
+ ->method( $iterator )
+ ->willReturn( [ $result ] );
+
+ $ref = new ReflectionObject( $dumpScript );
+ $processedProperty = $ref->getProperty( 'processed' );
+ $processedProperty->setAccessible( true );
+ $processedProperty->setValue( $dumpScript, $preProcessed );
+
+ $output = fopen( "php://memory", "w+b" );
+ $dbr = wfGetDB( DB_REPLICA );
+ /** @var CategoryChangesAsRdf $dumpScript */
+ $dumpScript->initialize();
+ $dumpScript->getRdf();
+ $dumpScript->$handler( $dbr, $output );
+
+ rewind( $output );
+ $sparql = stream_get_contents( $output );
+ $this->assertFileContains( $testFileName, $sparql );
+
+ $processed = $processedProperty->getValue( $dumpScript );
+ $expectedProcessed = $preProcessed;
+ foreach ( $result as $row ) {
+ if ( isset( $row->_processed ) ) {
+ $this->assertArrayHasKey( $row->_processed, $processed,
+ "ID {$row->_processed} was not processed!" );
+ $expectedProcessed[] = $row->_processed;
+ }
+ }
+ $this->assertArrayEquals( $expectedProcessed, array_keys( $processed ),
+ 'Processed array has wrong items' );
+ }
+
+ public function testUpdateTs() {
+ $dumpScript = new CategoryChangesAsRdf();
+ $dumpScript->initialize();
+ $update = $dumpScript->updateTS( 1503620949 );
+ $outFile = __DIR__ . '/../data/categoriesrdf/updatets.txt';
+ $this->assertFileContains( $outFile, $update );
+ }
+
+}