Merge "Fixing display issue with interwiki search sidebar"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 8 Jun 2017 14:27:36 +0000 (14:27 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 8 Jun 2017 14:27:36 +0000 (14:27 +0000)
29 files changed:
RELEASE-NOTES-1.30
includes/api/i18n/gl.json
includes/api/i18n/lb.json
includes/api/i18n/zh-hans.json
includes/password/Pbkdf2Password.php
includes/specials/SpecialContributions.php
includes/widget/search/InterwikiSearchResultSetWidget.php
languages/i18n/atj.json
languages/i18n/az.json
languages/i18n/azb.json
languages/i18n/be-tarask.json
languages/i18n/hr.json
languages/i18n/qqq.json
languages/i18n/zh-hant.json
resources/Resources.php
resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.less
resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.styles.less [new file with mode: 0644]
tests/phpunit/includes/password/BcryptPasswordTest.php
tests/phpunit/includes/password/EncryptedPasswordTest.php [new file with mode: 0644]
tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php
tests/phpunit/includes/password/MWOldPasswordTest.php [new file with mode: 0644]
tests/phpunit/includes/password/MWSaltedPasswordTest.php [new file with mode: 0644]
tests/phpunit/includes/password/PasswordFactoryTest.php [new file with mode: 0644]
tests/phpunit/includes/password/PasswordPolicyChecksTest.php
tests/phpunit/includes/password/PasswordTest.php
tests/phpunit/includes/password/PasswordTestCase.php
tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php [new file with mode: 0644]
tests/phpunit/includes/password/Pbkdf2PasswordTest.php
tests/phpunit/includes/password/UserPasswordPolicyTest.php

index fa5c280..343c296 100644 (file)
@@ -97,6 +97,8 @@ changes to languages because of Phabricator reports.
   As a result of the new uniform handling, '-{' may need to be escaped
   (for example, as '-<nowiki/>{') where it occurs inside template arguments
   or wikilinks.
+* (T163966) Page moves are now counted as edits for the purposes of
+  autopromotion, i.e., they increment the user_editcount field in the database.
 
 == Compatibility ==
 MediaWiki 1.30 requires PHP 5.5.9 or later. There is experimental support for
index 1cf2c7f..c7bcf02 100644 (file)
@@ -69,6 +69,7 @@
        "apihelp-compare-param-prop": "Que información obter.",
        "apihelp-compare-paramvalue-prop-diff": "O diff HTML.",
        "apihelp-compare-paramvalue-prop-diffsize": "O tamaño do diff HTML, en bytes.",
+       "apihelp-compare-paramvalue-prop-size": "Tamaño das revisións 'desde' e 'a'.",
        "apihelp-compare-example-1": "Mostrar diferencias entre a revisión 1 e a 2",
        "apihelp-createaccount-description": "Crear unha nova conta de usuario.",
        "apihelp-createaccount-param-preservestate": "SE <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> devolve o valor \"certo\" para  <samp>hasprimarypreservedstate</samp>, as consultas marcadas como <samp>primary-required</samp> deben ser omitidas. Se devolve un valor non baleiro para <samp>preservedusername</samp>, ese nome de usuario debe usarse para o parámetro <var>username</var>.",
        "apihelp-parse-paramvalue-prop-limitreporthtml": "Devolve a versión HTML do informe de límite. Non devolve datos cando <var>$1disablelimitreport</var> está fixado.",
        "apihelp-parse-paramvalue-prop-parsetree": "Árbores de análise XML do contido da revisión (precisa o modelo de contido <code>$1</code>)",
        "apihelp-parse-paramvalue-prop-parsewarnings": "Devolve os avisos que ocorreron ó analizar o contido.",
+       "apihelp-parse-param-wrapoutputclass": "Clase CSS a usar para formatar a saída do analizador sintáctico.",
        "apihelp-parse-param-pst": "Fai unha transformación antes de gardar a entrada antes de analizala. Válida unicamente para usar con texto.",
        "apihelp-parse-param-onlypst": "Facer unha transformación antes de gardar (PST) a entrada, pero sen analizala. Devolve o mesmo wikitexto, despois de que a PST foi aplicada. Só válida cando se usa con <var>$1text</var>.",
        "apihelp-parse-param-effectivelanglinks": "Inclúe ligazóns de idioma proporcionadas polas extensións (para usar con <kbd>$1prop=langlinks</kbd>).",
        "apihelp-parse-param-preview": "Analizar en modo vista previa.",
        "apihelp-parse-param-sectionpreview": "Analizar en modo vista previa de sección (activa tamén o modo de vista previa).",
        "apihelp-parse-param-disabletoc": "Omitir o índice na saída.",
+       "apihelp-parse-param-useskin": "Aplicar o tema seleccionado á saída do analizador. Pode afectar ás seguintes propiedades: <kbd>langlinks</kbd>, <kbd>headitems</kbd>, <kbd>módulos</kbd>, <kbd>jsconfigvars</kbd>, <kbd>indicadores</kbd>.",
        "apihelp-parse-param-contentformat": "Formato de serialización do contido usado para o texto de entrada. Só válido cando se usa con $1text.",
        "apihelp-parse-param-contentmodel": "Modelo de contido do texto de entrada. Se se omite, debe especificarse $1title, e o valor por defecto será o modelo do título especificado. Só válido cando se usa con $1text.",
        "apihelp-parse-example-page": "Analizar unha páxina.",
index ceb6571..4dfd1c7 100644 (file)
        "apierror-invalidcategory": "Den Numm vun der Kategorie deen Dir aginn hutt ass net valabel.",
        "apierror-invalidtitle": "Schlechten Titel \"$1\".",
        "apierror-invaliduserid": "Benotzer ID <var>$1</var> ass net valabel.",
+       "apierror-missingrev-title": "Keng aktuell Versioun vum Titel $1.",
        "apierror-missingtitle": "D'Säit déi Dir spezifizéiert hutt gëtt et net.",
        "apierror-missingtitle-byname": "D'Säit $1 gëtt et net.",
        "apierror-moduledisabled": "De(n) <kbd>$1</kbd> Modul gouf ausgeschalt.",
index db305b7..34d4a8d 100644 (file)
        "apihelp-compare-param-totitle": "要比较的第二个标题。",
        "apihelp-compare-param-toid": "要比较的第二个页面 ID。",
        "apihelp-compare-param-torev": "要比较的第二个修订版本。",
+       "apihelp-compare-param-tocontentformat": "<var>totext</var>的内容序列化格式。",
+       "apihelp-compare-param-prop": "要获取的信息束。",
        "apihelp-compare-paramvalue-prop-diff": "差异HTML。",
        "apihelp-compare-paramvalue-prop-diffsize": "差异HTML的大小(字节)。",
+       "apihelp-compare-paramvalue-prop-title": "“from”和“to”修订版本的页面标题。",
+       "apihelp-compare-paramvalue-prop-comment": "“from”和“to”修订版本的注释。",
        "apihelp-compare-example-1": "在版本1和2中创建差异。",
        "apihelp-createaccount-description": "创建一个新用户账户。",
        "apihelp-createaccount-param-preservestate": "如果<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>返回用于<samp>hasprimarypreservedstate</samp>的真值,标记为<samp>primary-required</samp>的请求应被忽略。如果它返回用于<samp>preservedusername</samp>的非空值,用户名必须用于<var>username</var>参数。",
        "apierror-maxlag": "正在等待$2:已延迟$1{{PLURAL:$1|秒}}。",
        "apierror-mimesearchdisabled": "MIME搜索在Miser模式中被禁用。",
        "apierror-missingcontent-pageid": "丢失ID为$1的页面的内容。",
+       "apierror-missingcontent-revid": "丢失ID为$1的修订版本的内容。",
        "apierror-missingparam-at-least-one-of": "需要{{PLURAL:$2|参数$1|$1中的至少一个参数}}。",
        "apierror-missingparam-one-of": "需要{{PLURAL:$2|参数$1|$1中的一个参数}}。",
        "apierror-missingparam": "<var>$1</var>参数必须被设置。",
        "apierror-missingrev-pageid": "没有ID为$1的页面的当前修订版本。",
+       "apierror-missingrev-title": "没有标题$1的当前修订版本。",
        "apierror-missingtitle-createonly": "丢失标题只可以通过<kbd>create</kbd>保护。",
        "apierror-missingtitle": "您指定的页面不存在。",
        "apierror-missingtitle-byname": "页面$1不存在。",
index 6ffada3..4a8831e 100644 (file)
@@ -41,12 +41,17 @@ class Pbkdf2Password extends ParameterizedPassword {
                return ':';
        }
 
+       protected function shouldUseHashExtension() {
+               return isset( $this->config['use-hash-extension'] ) ?
+                       $this->config['use-hash-extension'] : function_exists( 'hash_pbkdf2' );
+       }
+
        public function crypt( $password ) {
                if ( count( $this->args ) == 0 ) {
                        $this->args[] = base64_encode( MWCryptRand::generate( 16, true ) );
                }
 
-               if ( function_exists( 'hash_pbkdf2' ) ) {
+               if ( $this->shouldUseHashExtension() ) {
                        $hash = hash_pbkdf2(
                                $this->params['algo'],
                                $password,
index 4da1c85..e2fa8a3 100644 (file)
@@ -40,7 +40,7 @@ class SpecialContributions extends IncludableSpecialPage {
                $out->addModuleStyles( [
                        'mediawiki.special',
                        'mediawiki.special.changeslist',
-                       'mediawiki.widgets.DateInputWidget',
+                       'mediawiki.widgets.DateInputWidget.styles',
                ] );
                $out->addModules( 'mediawiki.special.contributions' );
                $this->addHelpLink( 'Help:User contributions' );
index 6d942de..11f9364 100644 (file)
@@ -177,7 +177,7 @@ class InterwikiSearchResultSetWidget implements SearchResultSetWidget {
                $iwIconUrl = $parsed['scheme'] .
                        $parsed['delimiter'] .
                        $parsed['host'] .
-                       ( $parsed['port'] ? ':' . $parsed['port'] : '' ) .
+                       ( isset ( $parsed['port'] ) ? ':' . $parsed['port'] : '' ) .
                        '/favicon.ico';
 
                $iwIcon = new OOUI\IconWidget( [
index 21a1fb5..6f23131 100644 (file)
@@ -16,6 +16,9 @@
        "tog-hidepatrolled": "Nohwe nta ka ki kweskisinihikateki nama weckat katcicta ka ki aci koski kanawapatcikateki",
        "tog-newpageshidepatrolled": " Katcicta paskickwemakana ka ki koski aci tapwatcikateki  taci e ici masinateki ocki paskickwemikana",
        "tog-hidecategorization": "Katcicta tipanictawin paskickwemikana",
+       "tog-extendwatchlist": "Ekoci kaskina kata nokok nohwe nosinesinihan ka kweskisinihikateki ka masinateki aka tepirak nohwe kata nokok aka weckat ka ki otci otamirotakaniwiki.",
+       "tog-numberheadings": " Nicike kata masinihikepirik akitasowina  e icinikateki tipanisinihikanica",
+       "tog-showtoolbar": "Motena ka maskotikw kata nokoki irapitcitcikana masinihikakan e nisawitakaniwok",
        "underline-always": "Mocak",
        "underline-never": "Nama wiskat",
        "sunday": "Manactakaniwon",
index 341b83d..e59b05d 100644 (file)
        "filehist-filesize": "Faylın həcmi",
        "filehist-comment": "Şərh",
        "imagelinks": "Fayl keçidləri",
-       "linkstoimage": "{{PLURAL:$1|səhifə|$1 səhifə}} bu fayla istinad edir:",
+       "linkstoimage": "Aşağıdakı {{PLURAL:$1|səhifə|$1 səhifə}} bu fayla istinad edir:",
        "nolinkstoimage": "Bu fayla keçid verən səhifə yoxdur.",
        "linkstoimage-redirect": "$1 (fayl istiqamətləndirilir) $2",
        "sharedupload": "Bu fayl $1-dandır və ola bilsin ki, başqa layihələrdə də istifadə edilir.",
        "whatlinkshere": "Bu səhifəyə bağlantılar",
        "whatlinkshere-title": "\"$1\" məqaləsinə keçid verən səhifələr",
        "whatlinkshere-page": "Səhifə:",
-       "linkshere": "'''[[:$1]]''' səhifəsinə istinad edən səhifələr:",
+       "linkshere": "'''[[:$1]]''' səhifəsinə keçid verən səhifələr:",
        "nolinkshere": "<strong>[[:$1]]</strong> səhifəsinə keçid verən səhifə yoxdur.",
        "nolinkshere-ns": "Seçilmiş ad aralığında heç bir səhifə '''[[:$1]]''' səhifəsinə keçid vermir.",
        "isredirect": "İstiqamətləndirmə səhifəsi",
index 04ee5eb..dd65fc7 100644 (file)
        "protectedarticle": "«[[$1]]» قوْروندو",
        "modifiedarticleprotection": "\"[[$1]]\" صحیفه‌سی اوچون محافظه سویه‌سی دییشیلدی",
        "unprotectedarticle": "محافظه کنارلاشدیریلدی \"[[$1]]\"",
-       "movedarticleprotection": "قوروما نیزام‌لاری \"[[$2]]\" صحیفه‌سین‌دن \"[[$1]]\" صحیفه‌سی داشیندی",
+       "movedarticleprotection": "قوروما تنظیماتلاری \"[[$2]]\" صفحه‌سیندن \"[[$1]]\" صفحه‌سینه داشیندی",
        "protect-title": "\"$1\" اوچون محافظه سویه‌سی‌نین دییشدیریلمه‌سی",
        "protect-title-notallowed": "\"$1\"اوچون محافظه سویه‌سی‌نین گؤسترین",
        "prot_1movedto2": "[[$1]] آدی دییشیلدی. یئنی آدی: [[$2]]",
        "pageinfo-hidden-categories": "گیزلی {{PLURAL:$1|بؤلمه|بؤلمه‌لر}} ($1)",
        "pageinfo-templates": "ایشله‌دیلمیش {{PLURAL:$1|بیر|$1}} شابلون ($1)",
        "pageinfo-transclusions": "ایچینده گلن {{PLURAL:$1|صحیفه|صحیفه‌لر}} ($1)",
-       "pageinfo-toolboxlink": "صÙ\81Ø­Ù\87 Ø¨Û\8cÙ\84Ú¯Û\8câ\80\8cسی",
+       "pageinfo-toolboxlink": "صÙ\81Ø­Ù\87 Ø§Û\8cØ·Ù\84اعاتی",
        "pageinfo-redirectsto": "ایستیقامتلن‌دیریلن",
        "pageinfo-redirectsto-info": "بیلگی",
        "pageinfo-contentpage": "بیر مضمون صفحه‌سی ساییلیر",
        "logentry-newusers-create2": "$1 ایستیفاده‌چی، $3 حسابی {{GENDER:$2|یاراتدی}}",
        "logentry-newusers-byemail": "$3 ایستیفاده‌چی حسابی، $1 ایله {{GENDER:$2|یارادیلیب}} و رمز، ایمیل ایله گؤندریلیب‌دیر",
        "logentry-newusers-autocreate": "$1 ایشلدن حسابی اوْتوماتیک {{GENDER:$2|یارادیلدی}}",
+       "logentry-protect-move_prot": "$1 قوروما تنظیماتلارینی $4-دن/دان  $3-ه {{GENDER:$2|داشیدی}}",
        "logentry-protect-protect": "$1 $3-ی/و  {{GENDER:$2|قوْرودو}} $4",
        "logentry-rights-rights": "$1، $3-ین قروپ عۆضولوگونو $4-دن $5-ه {{GENDER:$2|دَییشدیردی}}",
        "logentry-rights-rights-legacy": "$1، $3-ین قروپ عوضولوگونو {{GENDER:$2|دَییشدیردی}}",
index 50b3ee3..9b82d2a 100644 (file)
        "authprovider-confirmlink-message": "Калі грунтавацца на вашых няўдаўніх спробах уваходу, наступныя рахункі могуць быць далучаныя да вашага вікірахунку. Іх далучэньне дазволіць вам уваходзіць праз гэтыя рахункі. Калі ласка, абярце якія рахункі трэба далучыць.",
        "authprovider-confirmlink-request-label": "Рахункі, якія павінны быць злучаныя",
        "authprovider-confirmlink-success-line": "$1: пасьпяхова далучаны.",
+       "authprovider-confirmlink-failed": "Далучэньне рахунку не атрымалася цалкам: $1",
        "changecredentials": "Зьмена ўліковых зьвестак",
        "removecredentials": "Выдаленьне ўліковых зьвестак",
        "removecredentials-submit": "Выдаліць уліковыя зьвесткі",
index fd21587..8fc3630 100644 (file)
        "redirectedfrom": "(Preusmjereno s $1)",
        "redirectpagesub": "Preusmjeravanje",
        "redirectto": "Preusmjerava na:",
-       "lastmodifiedat": "Ova stranica posljednji je put izmijenjena $1 u $2.",
+       "lastmodifiedat": "Ova je stranica posljednji put uređivana dana $1 u $2.",
        "viewcount": "Ova stranica je pogledana {{PLURAL:$1|$1 put|$1 puta}}.",
        "protectedpage": "Zaštićena stranica",
        "jumpto": "Skoči na:",
        "missingarticle-rev": "(izmjena#: $1)",
        "missingarticle-diff": "(razlika: $1, $2)",
        "readonly_lag": "Baza podataka je automatski zaključana dok se sekundarni bazni poslužitelji ne usklade s glavnim",
-       "internalerror": "Pogreška sustava",
+       "internalerror": "Pogrješka sustava",
        "internalerror_info": "Interna pogrješka: $1",
        "internalerror-fatal-exception": "Terminalna pogreška \"$1\"",
        "filecopyerror": "Ne mogu kopirati datoteku \"$1\" u \"$2\".",
        "rcshowhidecategorization": "$1 kategorizaciju stranica",
        "rcshowhidecategorization-show": "prikaži",
        "rcshowhidecategorization-hide": "Sakrij",
-       "rclinks": "Prikaži posljednjih $1 promjena {{PLURAL:$2|prethodni dan|u posljednja $2 dana|u posljednjih $2 dana}}",
+       "rclinks": "Prikaži posljednjih $1 promjena u zadnjih $2 dana",
        "diff": "razl",
        "hist": "pov",
        "hide": "sakrij",
        "sp-contributions-uploads": "postavljene datoteke",
        "sp-contributions-logs": "evidencije",
        "sp-contributions-talk": "razgovor",
-       "sp-contributions-userrights": "upravljanje suradničkim pravima",
+       "sp-contributions-userrights": "upravljanje {{GENDER:$1|suradnikovim|suradničinim|suradničkim}} pravima",
        "sp-contributions-blocked-notice": "Ovaj suradnik je trenutačno blokiran. Posljednja stavka evidencije blokiranja navedena je niže kao napomena:",
        "sp-contributions-blocked-notice-anon": "Ova IP adresa je trenutačno blokirana.\nPosljednja stavka evidencije blokiranja navedena je niže kao napomena:",
        "sp-contributions-search": "Pretraži doprinose",
index d6ef868..4d46e6f 100644 (file)
        "revid": "Used to format a revision ID number in text. Parameters:\n* $1 - Revision ID number.\n{{Identical|Revision}}",
        "pageid": "Used to format a page ID number in text. Parameters:\n* $1 - Page ID number.",
        "rawhtml-notallowed": "Error message given when $wgRawHtml = true; is set and a user uses an &lt;html&gt; tag in a system message or somewhere other than a normal page.",
-       "gotointerwiki": "{{doc-special|GoToInterwiki}}\n\nSpecial:GoToInterwiki is a warning page displayed before redirecting users to external interwiki links. Its triggered by people going to something like [[Special:Search/google:foo]].",
+       "gotointerwiki": "{{doc-special|GoToInterwiki}}\n\nSpecial:GoToInterwiki is a warning page displayed before redirecting users to external interwiki links. Its triggered by people going to something like [[Special:Search/google:foo]].\n{{Identical|Leaving}}",
        "gotointerwiki-invalid": "Message shown on Special:GoToInterwiki if given an invalid title.",
        "gotointerwiki-external": "Message shown on Special:GoToInterwiki if given a external interwiki link (e.g. [[Special:GoToInterwiki/Google:Foo]]). $1 is the full url the user is trying to get to. $2 is the text of the interwiki link (e.g. \"Google:foo\").",
        "undelete-cantedit": "Shown if the user tries to undelete a page that they cannot edit",
index 3d8563d..1f5f78e 100644 (file)
        "recentchanges-legend-plusminus": "(<em>±123</em>)",
        "recentchanges-submit": "顯示",
        "rcfilters-activefilters": "使用中的過濾條件",
-       "rcfilters-quickfilters": "已儲存的過濾器設定",
-       "rcfilters-savedqueries-defaultlabel": "儲存過濾器",
+       "rcfilters-quickfilters": "已儲存的查詢條件設定",
+       "rcfilters-quickfilters-placeholder-title": "尚未儲存任何連結",
+       "rcfilters-savedqueries-defaultlabel": "已儲存的查詢條件",
        "rcfilters-savedqueries-rename": "重新命名",
-       "rcfilters-savedqueries-setdefault": "為預設",
-       "rcfilters-savedqueries-unsetdefault": "取消為預設",
+       "rcfilters-savedqueries-setdefault": "為預設",
+       "rcfilters-savedqueries-unsetdefault": "取消為預設",
        "rcfilters-savedqueries-remove": "移除",
        "rcfilters-savedqueries-new-name-label": "名稱",
        "rcfilters-savedqueries-apply-label": "儲存設定",
        "rcfilters-filter-watchlist-notwatched-label": "不在監視列表上",
        "rcfilters-filtergroup-changetype": "變更類型",
        "rcfilters-filter-pageedits-label": "頁面編輯",
-       "rcfilters-filter-pageedits-description": "對 Wiki 內容、討論、分類說明所做的編輯...",
+       "rcfilters-filter-pageedits-description": "對 Wiki 內容、討論、分類說明所做的編輯",
        "rcfilters-filter-newpages-label": "頁面建立",
        "rcfilters-filter-newpages-description": "建立新頁面的編輯。",
        "rcfilters-filter-categorization-label": "分類變更",
        "rcfilters-typeofchange-conflicts-hideminor": "此變更類型過濾條件與 \"次要編輯\" 過濾條件衝突,某些變更類型無法指定為 \"次要\"。",
        "rcfilters-filtergroup-lastRevision": "最新版本",
        "rcfilters-filter-lastrevision-label": "最新版本",
+       "rcfilters-filter-lastrevision-description": "對頁面最近做的更改。",
        "rcfilters-filter-previousrevision-label": "早期版本",
        "rcnotefrom": "以下{{PLURAL:$5|為}}自 <strong>$3 $4</strong> 以來的變更 (最多顯示 <strong>$1</strong> 筆)。",
+       "rclistfromreset": "重設日期選擇",
        "rclistfrom": "顯示自 $3 $2 以來的新變更",
        "rcshowhideminor": "$1 次要編輯",
        "rcshowhideminor-show": "顯示",
        "enotif_body_intro_moved": "{{SITENAME}} 的頁面 $1 已於 $PAGEEDITDATE 被使用者 $2 {{GENDER:$2|移動}},詳見目前的修訂 $3。",
        "enotif_body_intro_restored": "{{SITENAME}} 的頁面 $1 已於 $PAGEEDITDATE 被使用者 $2 {{GENDER:$2|還原}},詳見目前的修訂 $3。",
        "enotif_body_intro_changed": "{{SITENAME}} 的頁面 $1 已於 $PAGEEDITDATE 被使用者 $2 {{GENDER:$2|更改}},詳見目前的修訂 $3。",
-       "enotif_lastvisited": "è«\8bå\8f\83è\80\83 $1 æª¢è¦\96è\87ªæ\82¨ä¸\8a次檢è¦\96å¾\8cæ\89\80æ\9c\89ç\9a\84è®\8aæ\9b´ã\80\82",
-       "enotif_lastdiff": "è«\8bå\8f\83è\80\83 $1 æª¢è¦\96æ­¤è®\8aæ\9b´ã\80\82",
+       "enotif_lastvisited": "è¦\81檢è¦\96è\87ªæ\82¨ä¸\8a次檢è¦\96å¾\8cæ\89\80æ\9c\89ç\9a\84è®\8aæ\9b´è«\8bè¦\8b $1",
+       "enotif_lastdiff": "è¦\81檢è¦\96此次è®\8aæ\9b´è«\8bè¦\8b $1",
        "enotif_anon_editor": "匿名使用者 $1",
        "enotif_body": "$WATCHINGUSERNAME 您好,\n\n$PAGEINTRO $NEWPAGE\n\n編輯摘要:$PAGESUMMARY $PAGEMINOREDIT\n\n編輯者聯絡方式:\n信箱:$PAGEEDITOR_EMAIL\n本站:$PAGEEDITOR_WIKI\n\n在您檢視該頁面之前,接下來的變更系統不會再向您發出通知。您也可以在監視清單中重設您所有監視頁面的通知狀態。\n\n{{SITENAME}} 通知系統\n\n--\n更改您的電子郵件通知設定,請至:\n{{canonicalurl:{{#special:Preferences}}}}\n\n更改您的監視清單設定,請至:\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\n從監視清單中刪除此頁面,請至:\n$UNWATCHURL\n\n回函並取得進一步協助:\n$HELPPAGE",
        "created": "建立了",
        "blocklist": "已封鎖的使用者",
        "autoblocklist": "自動封禁",
        "autoblocklist-submit": "搜尋",
+       "autoblocklist-legend": "列出自動封鎖",
+       "autoblocklist-localblocks": "本地{{PLURAL:$1|自動封鎖|自動封鎖}}",
+       "autoblocklist-total-autoblocks": "自動封鎖總數:$1",
+       "autoblocklist-empty": "自動封鎖清單是空的。",
+       "autoblocklist-otherblocks": "其他{{PLURAL:$1|自動封鎖|自動封鎖}}",
        "ipblocklist": "已封鎖的使用者",
        "ipblocklist-legend": "搜尋已封鎖的使用者",
        "blocklist-userblocks": "隱藏帳號封鎖",
        "tooltip-pt-mycontris": "{{GENDER:|您的}}貢獻清單",
        "tooltip-pt-anoncontribs": "由此 IP 位址編輯的清單",
        "tooltip-pt-login": "建議您先登入,但並非必要。",
+       "tooltip-pt-login-private": "您需要登入才能使用此 wiki",
        "tooltip-pt-logout": "登出",
        "tooltip-pt-createaccount": "我們會鼓勵您建立一個帳號並且登入,即使這不是必要的動作。",
        "tooltip-ca-talk": "有關頁面內容的討論",
        "anonymous": "{{SITENAME}} 的匿名{{PLURAL:$1|使用者}}",
        "siteuser": "{{SITENAME}} 使用者 $1",
        "anonuser": "{{SITENAME}} 匿名使用者 $1",
-       "lastmodifiedatby": "此頁面由 $3 於 $1 $2 做最後修改。",
+       "lastmodifiedatby": "此頁面由 $3 於 $1 $2 做最後編輯。",
        "othercontribs": "此頁面由 $1 所貢獻。",
        "others": "其他",
        "siteusers": "{{SITENAME}} {{PLURAL:$2|使用者}} $1",
        "confirmrecreate": "在您編輯的同時,使用者 [[User:$1|$1]] ([[User talk:$1|對話]]) 刪除了此頁面,原因為:\n: <em>$2</em>\n請確認您是否真的要重新建立此頁面。",
        "confirmrecreate-noreason": "在您編輯的同時,使用者 [[User:$1|$1]] ([[User talk:$1|對話]]) 刪除了此頁面,請確認您是否真的要重新建立此頁面。",
        "recreate": "重新建立",
+       "confirm-purge-title": "刪除此頁",
        "confirm_purge_button": "確定",
        "confirm-purge-top": "要清除此頁面的快取嗎?",
        "confirm-purge-bottom": "清除頁面會清空頁面的快取記錄並強制顯示最近的頁面修訂。",
        "logentry-delete-delete": "$1 刪除頁面 $3",
        "logentry-delete-delete_redir": "$1 透過覆寫{{GENDER:$2|刪除了}}重新導向 $3",
        "logentry-delete-restore": "$1{{GENDER:$2|還原}}頁面 $3($4)",
+       "logentry-delete-restore-nocount": "$1 {{GENDER:$2|已還原}}頁面 $3",
+       "restore-count-revisions": "{{PLURAL:$1|1 修訂|$1 修訂}}",
+       "restore-count-files": "{{PLURAL:$1|1 檔案|$1 檔案}}",
        "logentry-delete-event": "$1 {{GENDER:$2|已更改}} $3 中 {{PLURAL:$5|1 筆日誌|$5 筆日誌}}的可見性:$4",
        "logentry-delete-revision": "$1 {{GENDER:$2|已更改}}頁面 $3 中 {{PLURAL:$5|1 筆修訂|$5 筆修訂}}的可見性:$4",
        "logentry-delete-event-legacy": "$1 {{GENDER:$2|已變更}} $3 中日誌的可見性",
        "mw-widgets-titleinput-description-redirect": "重新導向至 $1",
        "mw-widgets-categoryselector-add-category-placeholder": "加入分類...",
        "mw-widgets-usersmultiselect-placeholder": "加入更多...",
+       "date-range-from": "開始日期:",
+       "date-range-to": "結束日期:",
        "sessionmanager-tie": "無法合併多個請求認証類型:$1。",
        "sessionprovider-generic": "$1 連線階段",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "以 cookie 為基礎的連線階段",
        "restrictionsfield-help": "一個 IP 位址或 CIDR 範圍一行,要開啟所有範圍可使用:<pre>0.0.0.0/0\n::/0</pre>",
        "revid": "修訂 $1",
        "pageid": "頁面 ID $1",
-       "rawhtml-notallowed": "&lt;html&gt; 標籤無法在一般頁面之外使用。"
+       "rawhtml-notallowed": "&lt;html&gt; 標籤無法在一般頁面之外使用。",
+       "gotointerwiki": "離開 {{SITENAME}}",
+       "gotointerwiki-invalid": "指定的標題無效。",
+       "gotointerwiki-external": "您正離開 {{SITENAME}} 並前往 [[$2]],這是另一個網站。\n\n'''[$1 繼續前往 $1]'''",
+       "undelete-cantedit": "您無法取消刪除此頁面,由於您並不被允許編輯此頁。",
+       "undelete-cantcreate": "您無法取消刪除此頁面,由於使用此名稱的頁面並不存在且您並不被允許建立此頁面。"
 }
index 46aafd5..7c0e4af 100644 (file)
@@ -2338,6 +2338,15 @@ return [
                        'oojs-ui-widgets',
                        'oojs-ui.styles.icons-movement',
                        'moment',
+                       'mediawiki.widgets.DateInputWidget.styles',
+               ],
+               'targets' => [ 'desktop', 'mobile' ],
+       ],
+       'mediawiki.widgets.DateInputWidget.styles' => [
+               'skinStyles' => [
+                       'default' => [
+                               'resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.styles.less',
+                       ],
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
index 3ecdea8..47f8045 100644 (file)
@@ -1,16 +1,10 @@
 /*!
- * MediaWiki Widgets – DateInputWidget styles.
+ * MediaWiki Widgets – JS DateInputWidget styles.
  *
  * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
  * @license The MIT License (MIT); see LICENSE.txt
  */
 
-.oo-ui-box-sizing( @type: border-box ) {
-       -webkit-box-sizing: @type;
-       -moz-box-sizing: @type;
-       box-sizing: @type;
-}
-
 .oo-ui-unselectable() {
        -webkit-touch-callout: none;
        -webkit-user-select: none;
        user-select: none;
 }
 
-.oo-ui-inline-spacing( @spacing, @cancelled-spacing: 0 ) {
-       margin-right: @spacing;
-
-       &:last-child {
-               margin-right: @cancelled-spacing;
-       }
-}
-
 @indicator-size: unit( 12 / 16 / 0.8, em );
 
 .mw-widget-dateInputWidget {
-       &.oo-ui-textInputWidget {
-               display: inline-block;
-               position: relative;
-               width: 21em;
-               margin-top: 0.25em;
-               .oo-ui-inline-spacing( 0.5em );
-               margin-bottom: 0.25em;
-               margin-left: 0;
-       }
-
-       &-handle,
-       &.oo-ui-textInputWidget input {
-               background-color: #fff;
-               display: inline-block;
-               position: relative;
-               .oo-ui-box-sizing( border-box );
-               width: 100%;
-               cursor: pointer;
-               padding: 0.5em 1em;
-               border: 1px solid #a2a9b1;
-               border-radius: 2px;
-               outline: 0;
-               line-height: 1.275;
-               /**
-                * Ensures non-infused and infused widget have the same height.
-                * Equal to line height + top padding + bottom padding
-                */
-               height: 2.275em;
-       }
-
        &-handle {
                .oo-ui-unselectable();
 
diff --git a/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.styles.less b/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.styles.less
new file mode 100644 (file)
index 0000000..18cf723
--- /dev/null
@@ -0,0 +1,53 @@
+/*!
+ * MediaWiki Widgets – PHP DateInputWidget styles.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+.oo-ui-box-sizing( @type: border-box ) {
+       -webkit-box-sizing: @type;
+       -moz-box-sizing: @type;
+       box-sizing: @type;
+}
+
+.oo-ui-inline-spacing( @spacing, @cancelled-spacing: 0 ) {
+       margin-right: @spacing;
+
+       &:last-child {
+               margin-right: @cancelled-spacing;
+       }
+}
+
+.mw-widget-dateInputWidget {
+       &.oo-ui-textInputWidget {
+               display: inline-block;
+               position: relative;
+               width: 21em;
+               margin-top: 0.25em;
+               .oo-ui-inline-spacing( 0.5em );
+               margin-bottom: 0.25em;
+               margin-left: 0;
+       }
+
+       // Note that this block applies to both the PHP widget and the JS widget
+       &-handle,
+       &.oo-ui-textInputWidget input {
+               background-color: #fff;
+               display: inline-block;
+               position: relative;
+               .oo-ui-box-sizing( border-box );
+               width: 100%;
+               cursor: pointer;
+               padding: 0.5em 1em;
+               border: 1px solid #a2a9b1;
+               border-radius: 2px;
+               outline: 0;
+               line-height: 1.275;
+               /**
+                * Ensures non-infused and infused widget have the same height.
+                * Equal to line height + top padding + bottom padding
+                */
+               height: 2.275em;
+       }
+}
index 8f80362..9b8e01e 100644 (file)
@@ -2,6 +2,10 @@
 
 /**
  * @group large
+ * @covers BcryptPassword
+ * @covers ParameterizedPassword
+ * @covers Password
+ * @covers PasswordFactory
  */
 class BcryptPasswordTest extends PasswordTestCase {
        protected function getTypeConfigs() {
diff --git a/tests/phpunit/includes/password/EncryptedPasswordTest.php b/tests/phpunit/includes/password/EncryptedPasswordTest.php
new file mode 100644 (file)
index 0000000..0c85653
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * @covers EncryptedPassword
+ * @covers ParameterizedPassword
+ * @covers Password
+ * @codingStandardsIgnoreStart Generic.Files.LineLength
+ */
+class EncryptedPasswordTest extends PasswordTestCase {
+       protected function getTypeConfigs() {
+               return [
+                       'both' => [
+                               'class' => 'EncryptedPassword',
+                               'underlying' => 'pbkdf2',
+                               'secrets' => [
+                                       md5( 'secret1' ),
+                                       md5( 'secret2' ),
+                               ],
+                               'cipher' => 'aes-256-cbc',
+                       ],
+                       'secret1' => [
+                               'class' => 'EncryptedPassword',
+                               'underlying' => 'pbkdf2',
+                               'secrets' => [
+                                       md5( 'secret1' ),
+                               ],
+                               'cipher' => 'aes-256-cbc',
+                       ],
+                       'secret2' => [
+                               'class' => 'EncryptedPassword',
+                               'underlying' => 'pbkdf2',
+                               'secrets' => [
+                                       md5( 'secret2' ),
+                               ],
+                               'cipher' => 'aes-256-cbc',
+                       ],
+                       'pbkdf2' => [
+                               'class' => 'Pbkdf2Password',
+                               'algo' => 'sha256',
+                               'cost' => '10',
+                               'length' => '64',
+                       ],
+               ];
+       }
+
+       public static function providePasswordTests() {
+               return [
+                       // Encrypted with secret1
+                       [ true, ':both:aes-256-cbc:0:izBpxujqC1YbzpCB3qAzgg==:ZqHnitT1pL4YJqKqFES2KEevZYSy2LtlibW5+IMi4XKOGKGy6sE638BXyBbLQQsBtTSrt+JyzwOayKtwIfRbaQsBridx/O1JwBSai1TkGkOsYMBXnlu2Bu/EquCBj5QpjYh7p3Uq4rpiop1KQlin1BJMwnAa1PovhxjpxnYhlhkM4X5ALoGi3XM0bapN48vt', 'password' ],
+                       [ true, ':secret1:aes-256-cbc:0:izBpxujqC1YbzpCB3qAzgg==:ZqHnitT1pL4YJqKqFES2KEevZYSy2LtlibW5+IMi4XKOGKGy6sE638BXyBbLQQsBtTSrt+JyzwOayKtwIfRbaQsBridx/O1JwBSai1TkGkOsYMBXnlu2Bu/EquCBj5QpjYh7p3Uq4rpiop1KQlin1BJMwnAa1PovhxjpxnYhlhkM4X5ALoGi3XM0bapN48vt', 'password' ],
+                       [ false, ':secret1:aes-256-cbc:0:izBpxujqC1YbzpCB3qAzgg==:ZqHnitT1pL4YJqKqFES2KEevZYSy2LtlibW5+IMi4XKOGKGy6sE638BXyBbLQQsBtTSrt+JyzwOayKtwIfRbaQsBridx/O1JwBSai1TkGkOsYMBXnlu2Bu/EquCBj5QpjYh7p3Uq4rpiop1KQlin1BJMwnAa1PovhxjpxnYhlhkM4X5ALoGi3XM0bapN48vt', 'notpassword' ],
+
+                       // Encrypted with secret2
+                       [ true, ':both:aes-256-cbc:1:m1LCnQVIakfYBNlr9KEgQg==:5yPTctqrzsybdgaMEag18AZYbnL37pAtXVBqmWxkjXbnNmiDH+1bHoL8lsEVTH/sJntC82kNVgE7zeiD8xUVLYF2VUnvB5+sU+aysE45/zwsCu7a22TaischMAOWrsHZ/tIgS/TnZY2d+HNyxgsEeeYf/QoL+FhmqHquK02+4SRbA5lLuj9niYy1r5CoM9cQ', 'password' ],
+                       [ true, ':secret2:aes-256-cbc:0:m1LCnQVIakfYBNlr9KEgQg==:5yPTctqrzsybdgaMEag18AZYbnL37pAtXVBqmWxkjXbnNmiDH+1bHoL8lsEVTH/sJntC82kNVgE7zeiD8xUVLYF2VUnvB5+sU+aysE45/zwsCu7a22TaischMAOWrsHZ/tIgS/TnZY2d+HNyxgsEeeYf/QoL+FhmqHquK02+4SRbA5lLuj9niYy1r5CoM9cQ', 'password' ],
+               ];
+       }
+
+       /**
+        * Wrong encryption key selected
+        * @expectedException PasswordError
+        */
+       public function testDecryptionError() {
+               $hash = ':secret1:aes-256-cbc:0:m1LCnQVIakfYBNlr9KEgQg==:5yPTctqrzsybdgaMEag18AZYbnL37pAtXVBqmWxkjXbnNmiDH+1bHoL8lsEVTH/sJntC82kNVgE7zeiD8xUVLYF2VUnvB5+sU+aysE45/zwsCu7a22TaischMAOWrsHZ/tIgS/TnZY2d+HNyxgsEeeYf/QoL+FhmqHquK02+4SRbA5lLuj9niYy1r5CoM9cQ';
+               $password = $this->passwordFactory->newFromCiphertext( $hash );
+               $password->crypt( 'password' );
+       }
+
+       public function testUpdate() {
+               $hash = ':both:aes-256-cbc:0:izBpxujqC1YbzpCB3qAzgg==:ZqHnitT1pL4YJqKqFES2KEevZYSy2LtlibW5+IMi4XKOGKGy6sE638BXyBbLQQsBtTSrt+JyzwOayKtwIfRbaQsBridx/O1JwBSai1TkGkOsYMBXnlu2Bu/EquCBj5QpjYh7p3Uq4rpiop1KQlin1BJMwnAa1PovhxjpxnYhlhkM4X5ALoGi3XM0bapN48vt';
+               $fromHash = $this->passwordFactory->newFromCiphertext( $hash );
+               $fromPlaintext = $this->passwordFactory->newFromPlaintext( 'password', $fromHash );
+               $this->assertTrue( $fromHash->update() );
+
+               $serialized = $fromHash->toString();
+               $this->assertRegExp( '/^:both:aes-256-cbc:1:/', $serialized );
+               $fromNewHash = $this->passwordFactory->newFromCiphertext( $serialized );
+               $fromPlaintext = $this->passwordFactory->newFromPlaintext( 'password', $fromNewHash );
+               $this->assertTrue( $fromHash->equals( $fromPlaintext ) );
+       }
+}
index 773f033..cf96d06 100644 (file)
@@ -1,5 +1,9 @@
 <?php
 
+/**
+ * @covers LayeredParameterizedPassword
+ * @covers Password
+ */
 class LayeredParameterizedPasswordTest extends PasswordTestCase {
        protected function getTypeConfigs() {
                return [
@@ -26,6 +30,10 @@ class LayeredParameterizedPasswordTest extends PasswordTestCase {
                ];
        }
 
+       protected function getValidTypes() {
+               return [ 'testLargeLayeredFinal' ];
+       }
+
        public static function providePasswordTests() {
                // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
                return [
diff --git a/tests/phpunit/includes/password/MWOldPasswordTest.php b/tests/phpunit/includes/password/MWOldPasswordTest.php
new file mode 100644 (file)
index 0000000..51e739c
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @covers MWOldPassword
+ * @covers ParameterizedPassword
+ * @covers Password
+ */
+class MWOldPasswordTest extends PasswordTestCase {
+       protected function getTypeConfigs() {
+               return [ 'A' => [
+                       'class' => 'MWOldPassword',
+               ] ];
+       }
+
+       public static function providePasswordTests() {
+               return [
+                       [ true, ':A:5f4dcc3b5aa765d61d8327deb882cf99', 'password' ],
+                       // Type-B password with incorrect type name is accepted
+                       [ true, ':A:salt:9842afc7cb949c440c51347ed809362f', 'password' ],
+                       [ false, ':A:d529e941509eb9e9b9cfaeae1fe7ca23', 'password' ],
+                       [ false, ':A:salt:d529e941509eb9e9b9cfaeae1fe7ca23', 'password' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/includes/password/MWSaltedPasswordTest.php b/tests/phpunit/includes/password/MWSaltedPasswordTest.php
new file mode 100644 (file)
index 0000000..53a6ad1
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @covers MWSaltedPassword
+ * @covers ParameterizedPassword
+ * @covers Password
+ */
+class MWSaltedPasswordTest extends PasswordTestCase {
+       protected function getTypeConfigs() {
+               return [ 'B' => [
+                       'class' => 'MWSaltedPassword',
+               ] ];
+       }
+
+       public static function providePasswordTests() {
+               return [
+                       [ true, ':B:salt:9842afc7cb949c440c51347ed809362f', 'password' ],
+                       [ false, ':B:salt:d529e941509eb9e9b9cfaeae1fe7ca23', 'password' ],
+               ];
+       }
+}
diff --git a/tests/phpunit/includes/password/PasswordFactoryTest.php b/tests/phpunit/includes/password/PasswordFactoryTest.php
new file mode 100644 (file)
index 0000000..5d585f3
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @covers PasswordFactory
+ */
+class PasswordFactoryTest extends MediaWikiTestCase {
+       public function testRegister() {
+               $pf = new PasswordFactory;
+               $pf->register( 'foo', [ 'class' => 'InvalidPassword' ] );
+               $this->assertArrayHasKey( 'foo', $pf->getTypes() );
+       }
+
+       public function testSetDefaultType() {
+               $pf = new PasswordFactory;
+               $pf->register( '1', [ 'class' => 'InvalidPassword' ] );
+               $pf->register( '2', [ 'class' => 'InvalidPassword' ] );
+               $pf->setDefaultType( '1' );
+               $this->assertSame( '1', $pf->getDefaultType() );
+               $pf->setDefaultType( '2' );
+               $this->assertSame( '2', $pf->getDefaultType() );
+       }
+
+       /**
+        * @expectedException Exception
+        */
+       public function testSetDefaultTypeError() {
+               $pf = new PasswordFactory;
+               $pf->setDefaultType( 'bogus' );
+       }
+
+       public function testInit() {
+               $config = new HashConfig( [
+                       'PasswordConfig' => [
+                               'foo' => [ 'class' => 'InvalidPassword' ],
+                       ],
+                       'PasswordDefault' => 'foo'
+               ] );
+               $pf = new PasswordFactory;
+               $pf->init( $config );
+               $this->assertSame( 'foo', $pf->getDefaultType() );
+               $this->assertArrayHasKey( 'foo', $pf->getTypes() );
+       }
+
+       public function testNewFromCiphertext() {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] );
+               $pw = $pf->newFromCiphertext( ':B:salt:d529e941509eb9e9b9cfaeae1fe7ca23' );
+               $this->assertInstanceOf( MWSaltedPassword::class, $pw );
+       }
+
+       public function provideNewFromCiphertextErrors() {
+               return [ [ 'blah' ], [ ':blah:' ] ];
+       }
+
+       /**
+        * @dataProvider provideNewFromCiphertextErrors
+        * @expectedException PasswordError
+        */
+       public function testNewFromCiphertextErrors( $hash ) {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] );
+               $pf->newFromCiphertext( $hash );
+       }
+
+       public function testNewFromType() {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] );
+               $pw = $pf->newFromType( 'B' );
+               $this->assertInstanceOf( MWSaltedPassword::class, $pw );
+       }
+
+       /**
+        * @expectedException PasswordError
+        */
+       public function testNewFromTypeError() {
+               $pf = new PasswordFactory;
+               $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] );
+               $pf->newFromType( 'bogus' );
+       }
+
+       public function testNewFromPlaintext() {
+               $pf = new PasswordFactory;
+               $pf->register( 'A', [ 'class' => 'MWOldPassword' ] );
+               $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] );
+               $pf->setDefaultType( 'A' );
+
+               $this->assertInstanceOf( 'InvalidPassword', $pf->newFromPlaintext( null ) );
+               $this->assertInstanceOf( 'MWOldPassword', $pf->newFromPlaintext( 'password' ) );
+               $this->assertInstanceOf( 'MWSaltedPassword',
+                       $pf->newFromPlaintext( 'password', $pf->newFromType( 'B' ) ) );
+       }
+
+       public function testNeedsUpdate() {
+               $pf = new PasswordFactory;
+               $pf->register( 'A', [ 'class' => 'MWOldPassword' ] );
+               $pf->register( 'B', [ 'class' => 'MWSaltedPassword' ] );
+               $pf->setDefaultType( 'A' );
+
+               $this->assertFalse( $pf->needsUpdate( $pf->newFromType( 'A' ) ) );
+               $this->assertTrue( $pf->needsUpdate( $pf->newFromType( 'B' ) ) );
+       }
+
+       public function testGenerateRandomPasswordString() {
+               $this->assertSame( 13, strlen( PasswordFactory::generateRandomPasswordString( 13 ) ) );
+       }
+
+       public function testNewInvalidPassword() {
+               $this->assertInstanceOf( 'InvalidPassword', PasswordFactory::newInvalidPassword() );
+       }
+}
index 6357510..7dfb3cf 100644 (file)
@@ -133,4 +133,27 @@ class PasswordPolicyChecksTest extends MediaWikiTestCase {
                $this->assertTrue( $statusLong->isOK(), 'Password matches blacklist, not fatal' );
        }
 
+       public static function providePopularBlacklist() {
+               return [
+                       [ false, 'sitename' ],
+                       [ false, 'password' ],
+                       [ false, '12345' ],
+                       [ true, 'hqY98gCZ6qM8s8' ],
+               ];
+       }
+
+       /**
+        * @covers PasswordPolicyChecks::checkPopularPasswordBlacklist
+        * @dataProvider providePopularBlacklist
+        */
+       public function testCheckPopularPasswordBlacklist( $expected, $password ) {
+               global $IP;
+               $this->setMwGlobals( [
+                       'wgSitename' => 'sitename',
+                       'wgPopularPasswordFile' => "$IP/serialized/commonpasswords.cdb"
+               ] );
+               $user = User::newFromName( 'username' );
+               $status = PasswordPolicyChecks::checkPopularPasswordBlacklist( PHP_INT_MAX, $user, $password );
+               $this->assertSame( $expected, $status->isGood() );
+       }
 }
index e8a4d5c..d0177d0 100644 (file)
  * @file
  */
 
+/**
+ * @covers InvalidPassword
+ */
 class PasswordTest extends MediaWikiTestCase {
-       /**
-        * @covers InvalidPassword::equals
-        */
        public function testInvalidUnequalInvalid() {
                $passwordFactory = new PasswordFactory();
                $invalid1 = $passwordFactory->newFromCiphertext( null );
index 9a142cb..80b9838 100644 (file)
@@ -78,8 +78,7 @@ abstract class PasswordTestCase extends MediaWikiTestCase {
 
        /**
         * @dataProvider providePasswordTests
-        * @covers InvalidPassword::equals
-        * @covers InvalidPassword::toString
+        * @covers InvalidPassword
         */
        public function testInvalidUnequalNormal( $shouldMatch, $hash, $password ) {
                $invalid = $this->passwordFactory->newFromCiphertext( null );
@@ -88,4 +87,26 @@ abstract class PasswordTestCase extends MediaWikiTestCase {
                $this->assertFalse( $invalid->equals( $normal ) );
                $this->assertFalse( $normal->equals( $invalid ) );
        }
+
+       protected function getValidTypes() {
+               return array_keys( $this->getTypeConfigs() );
+       }
+
+       public function provideTypes( $type ) {
+               $params = [];
+               foreach ( $this->getValidTypes() as $type ) {
+                       $params[] = [ $type ];
+               }
+               return $params;
+       }
+
+       /**
+        * @dataProvider provideTypes
+        */
+       public function testCrypt( $type ) {
+               $fromType = $this->passwordFactory->newFromType( $type );
+               $fromType->crypt( 'password' );
+               $fromPlaintext = $this->passwordFactory->newFromPlaintext( 'password', $fromType );
+               $this->assertTrue( $fromType->equals( $fromPlaintext ) );
+       }
 }
diff --git a/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php b/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php
new file mode 100644 (file)
index 0000000..605d190
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+
+/**
+ * @group large
+ * @covers Pbkdf2Password
+ */
+class Pbkdf2PasswordFallbackTest extends PasswordTestCase {
+       protected function getTypeConfigs() {
+               return [
+                       'pbkdf2' => [
+                               'class' => 'Pbkdf2Password',
+                               'algo' => 'sha256',
+                               'cost' => '10000',
+                               'length' => '128',
+                               'use-hash-extension' => false,
+                       ],
+               ];
+       }
+
+       public static function providePasswordTests() {
+               return [
+                       [ true, ":pbkdf2:sha1:1:20:c2FsdA==:DGDID5YfDnHzqbUkr2ASBi/gN6Y=", 'password' ],
+                       [ true, ":pbkdf2:sha1:2:20:c2FsdA==:6mwBTcctb4zNHtkqzh1B8NjeiVc=", 'password' ],
+                       [ true, ":pbkdf2:sha1:4096:20:c2FsdA==:SwB5AbdlSJq+rUnZJvch0GWkKcE=", 'password' ],
+                       [ true, ":pbkdf2:sha1:4096:16:c2EAbHQ=:Vvpqp1VICZ3MN9fwNCXgww==", "pass\x00word" ],
+               ];
+       }
+}
index 07cdab0..ff069cd 100644 (file)
@@ -2,6 +2,10 @@
 
 /**
  * @group large
+ * @covers Pbkdf2Password
+ * @covers Password
+ * @covers ParameterizedPassword
+ * @requires function hash_pbkdf2
  */
 class Pbkdf2PasswordTest extends PasswordTestCase {
        protected function getTypeConfigs() {
@@ -10,6 +14,7 @@ class Pbkdf2PasswordTest extends PasswordTestCase {
                        'algo' => 'sha256',
                        'cost' => '10000',
                        'length' => '128',
+                       'use-hash-extension' => true,
                ] ];
        }
 
index 8a69b5c..3bd82b4 100644 (file)
@@ -22,6 +22,7 @@
 
 /**
  * @group Database
+ * @covers UserPasswordPolicy
  */
 class UserPasswordPolicyTest extends MediaWikiTestCase {
 
@@ -56,9 +57,6 @@ class UserPasswordPolicyTest extends MediaWikiTestCase {
                return new UserPasswordPolicy( $this->policies, $this->checks );
        }
 
-       /**
-        * @covers UserPasswordPolicy::getPoliciesForUser
-        */
        public function testGetPoliciesForUser() {
 
                $upp = $this->getUserPasswordPolicy();
@@ -79,9 +77,6 @@ class UserPasswordPolicyTest extends MediaWikiTestCase {
                );
        }
 
-       /**
-        * @covers UserPasswordPolicy::getPoliciesForGroups
-        */
        public function testGetPoliciesForGroups() {
                $effective = UserPasswordPolicy::getPoliciesForGroups(
                        $this->policies,
@@ -103,7 +98,6 @@ class UserPasswordPolicyTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideCheckUserPassword
-        * @covers UserPasswordPolicy::checkUserPassword
         */
        public function testCheckUserPassword( $username, $groups, $password, $valid, $ok, $msg ) {
 
@@ -183,7 +177,6 @@ class UserPasswordPolicyTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideMaxOfPolicies
-        * @covers UserPasswordPolicy::maxOfPolicies
         */
        public function testMaxOfPolicies( $p1, $p2, $max, $msg ) {
                $this->assertArrayEquals(