From 775bff3bac71c2d63157966878a7fc862b324038 Mon Sep 17 00:00:00 2001 From: gnosygnu Date: Sun, 2 Feb 2020 21:02:11 -0500 Subject: [PATCH] Wikibase: Do not fail for FormatStatements and getPropertyOrder [#658] --- 100_core/src/gplx/core/bits/Bitmask_.java | 3 + .../src/gplx/core/strings/String_bldr.java | 20 +- 100_core/src/gplx/langs/regxs/Regx_adp.java | 26 +- 100_core/src/gplx/langs/regxs/Regx_adp_.java | 4 +- 100_core/src/gplx/langs/regxs/Regx_match.java | 1 + 110_gfml/src_100_tkn/gplx/gfml/GfmlLxr_.java | 2 +- .../src/gplx/xowa/mediawiki/XophpArray_.java | 22 + .../gplx/xowa/mediawiki/XophpArray__tst.java | 21 + .../xowa/mediawiki/XophpCallbackOwner.java | 19 + .../src/gplx/xowa/mediawiki/XophpRegex_.java | 152 +- .../mediawiki/XophpRegex_match_all_tst.java | 95 + .../mediawiki/XophpRegex_replace_tst.java | 49 + .../xowa/mediawiki/XophpRuntimeException.java | 19 + .../src/gplx/xowa/mediawiki/XophpString_.java | 17 +- .../client/includes/Wbase_client.java | 36 - .../client/includes/WikibaseClient.java | 1264 ++++ ...ikibaseLanguageIndependentLuaBindings.java | 4 +- ...va => XomwEntityRetrievingTermLookup.java} | 4 +- .../Store/XomwPropertyOrderProvider.java | 36 + .../XomwPropertyOrderProviderException.java | 20 + .../XomwWikiPagePropertyOrderProvider.java | 66 + .../XomwWikiTextPropertyOrderProvider.java | 80 + ...XomwWikiTextPropertyOrderProvider_tst.java | 63 + .../xowa/mediawiki/includes/XomwHooks.java | 190 + .../xowa/mediawiki/includes/XomwRevision.java | 1321 ++++ .../includes/content/XomwContent.java | 1 + .../includes/dao/XomwIDBAccessObject.java | 49 + .../includes/dao/XomwIDBAccessObject_.java | 36 + .../mediawiki/includes/page/XomwPage.java | 22 + .../includes/page/XomwWikiCategoryPage.java | 74 + .../includes/page/XomwWikiFilePage.java | 250 + .../mediawiki/includes/page/XomwWikiPage.java | 3833 +++++++++++ .../mediawiki/includes/user/XomwUser.java | 5725 +++++++++++++++++ .../scribunto/libs/Scrib_lib_wikibase.java | 104 +- .../libs/Scrib_lib_wikibase_entity.java | 3 +- 35 files changed, 13567 insertions(+), 64 deletions(-) create mode 100644 400_xowa/src/gplx/xowa/mediawiki/XophpCallbackOwner.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/XophpRegex_match_all_tst.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/XophpRegex_replace_tst.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/XophpRuntimeException.java delete mode 100644 400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/Wbase_client.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/WikibaseClient.java rename 400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/{EntityRetrievingTermLookup.java => XomwEntityRetrievingTermLookup.java} (91%) create mode 100644 400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwPropertyOrderProvider.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwPropertyOrderProviderException.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiPagePropertyOrderProvider.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiTextPropertyOrderProvider.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiTextPropertyOrderProvider_tst.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/includes/XomwHooks.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/includes/XomwRevision.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/includes/dao/XomwIDBAccessObject.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/includes/dao/XomwIDBAccessObject_.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwPage.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiCategoryPage.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiFilePage.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiPage.java create mode 100644 400_xowa/src/gplx/xowa/mediawiki/includes/user/XomwUser.java diff --git a/100_core/src/gplx/core/bits/Bitmask_.java b/100_core/src/gplx/core/bits/Bitmask_.java index 5b085d9dd..b532239d9 100644 --- a/100_core/src/gplx/core/bits/Bitmask_.java +++ b/100_core/src/gplx/core/bits/Bitmask_.java @@ -33,6 +33,9 @@ public class Bitmask_ { } return rv; } + public static int Set_or_add(int val, int flag) { + return val == 0 ? flag : val | flag; + } public static boolean Has_byte(byte val, byte find) {return find == (val & find);} public static byte Add_byte(byte flag, byte itm) {return (byte)(flag | itm);} } diff --git a/100_core/src/gplx/core/strings/String_bldr.java b/100_core/src/gplx/core/strings/String_bldr.java index edc0f7066..ee04ab422 100644 --- a/100_core/src/gplx/core/strings/String_bldr.java +++ b/100_core/src/gplx/core/strings/String_bldr.java @@ -37,8 +37,9 @@ public interface String_bldr { String_bldr Add(char c); String_bldr Add(int i); String_bldr Add_obj(Object o); - String_bldr Add_mid(char[] ary, int bgn, int count); - String_bldr Add_mid(String str, int bgn, int count); + String_bldr Add_mid(String str, int bgn, int end); + String_bldr Add_mid_len(char[] ary, int bgn, int count); + String_bldr Add_mid_len(String str, int bgn, int count); String_bldr Add_at(int idx, String s); String_bldr Del(int bgn, int len); } @@ -83,8 +84,9 @@ abstract class String_bldr_base implements String_bldr { public abstract String_bldr Add(String s); public abstract String_bldr Add(char c); public abstract String_bldr Add(int i); - public abstract String_bldr Add_mid(char[] ary, int bgn, int count); - public abstract String_bldr Add_mid(String str, int bgn, int count); + public abstract String_bldr Add_mid(String str, int bgn, int end); + public abstract String_bldr Add_mid_len(char[] ary, int bgn, int count); + public abstract String_bldr Add_mid_len(String str, int bgn, int count); public abstract String_bldr Add_obj(Object o); public abstract String_bldr Del(int bgn, int len); } @@ -97,8 +99,9 @@ class String_bldr_thread_single extends String_bldr_base { @Override public String_bldr Add(String s) {sb.append(s); return this;} @Override public String_bldr Add(char c) {sb.append(c); return this;} @Override public String_bldr Add(int i) {sb.append(i); return this;} - @Override public String_bldr Add_mid(char[] ary, int bgn, int count) {sb.append(ary, bgn, count); return this;} - @Override public String_bldr Add_mid(String str, int bgn, int count) {sb.append(str, bgn, count); return this;} + @Override public String_bldr Add_mid(String str, int bgn, int end) {sb.append(str, bgn, end); return this;} + @Override public String_bldr Add_mid_len(char[] ary, int bgn, int count) {sb.append(ary, bgn, count); return this;} + @Override public String_bldr Add_mid_len(String str, int bgn, int count) {sb.append(str, bgn, count); return this;} @Override public String_bldr Add_obj(Object o) {sb.append(o); return this;} @Override public String_bldr Del(int bgn, int len) {sb.delete(bgn, len); return this;} } @@ -111,8 +114,9 @@ class String_bldr_thread_multiple extends String_bldr_base { @Override public String_bldr Add(String s) {sb.append(s); return this;} @Override public String_bldr Add(char c) {sb.append(c); return this;} @Override public String_bldr Add(int i) {sb.append(i); return this;} - @Override public String_bldr Add_mid(char[] ary, int bgn, int count) {sb.append(ary, bgn, count); return this;} - @Override public String_bldr Add_mid(String str, int bgn, int count) {sb.append(str, bgn, count); return this;} + @Override public String_bldr Add_mid(String str, int bgn, int end) {sb.append(str, bgn, end); return this;} + @Override public String_bldr Add_mid_len(char[] ary, int bgn, int count) {sb.append(ary, bgn, count); return this;} + @Override public String_bldr Add_mid_len(String str, int bgn, int count) {sb.append(str, bgn, count); return this;} @Override public String_bldr Add_obj(Object o) {sb.append(o); return this;} @Override public String_bldr Del(int bgn, int len) {sb.delete(bgn, len); return this;} } diff --git a/100_core/src/gplx/langs/regxs/Regx_adp.java b/100_core/src/gplx/langs/regxs/Regx_adp.java index fc23d38d5..25ea38900 100644 --- a/100_core/src/gplx/langs/regxs/Regx_adp.java +++ b/100_core/src/gplx/langs/regxs/Regx_adp.java @@ -17,7 +17,10 @@ package gplx.langs.regxs; import gplx.*; import gplx.langs.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Regx_adp { - @gplx.Internal protected Regx_adp(String regx) {Pattern_(regx);} + public Regx_adp(String regx, int flags) { + this.flags = flags; + Pattern_(regx); + } public String Pattern() {return pattern;} public Regx_adp Pattern_(String val) {pattern = val; Under_sync(); return this;} private String pattern; public boolean Pattern_is_invalid() {return pattern_is_invalid;} private boolean pattern_is_invalid = false; public Exception Pattern_is_invalid_exception() {return pattern_is_invalid_exception;} private Exception pattern_is_invalid_exception = null; @@ -38,14 +41,15 @@ public class Regx_adp { } return (Regx_match[])rv.To_ary(Regx_match.class); } - private Pattern under; + private int flags = FLAG__DOTALL | FLAG__UNICODE_CHARACTER_CLASS;// JRE.7:UNICODE_CHARACTER_CLASS; added during %w fix for en.w:A#; DATE:2015-06-10 + private Pattern under; public Pattern Under() {return under;} private void Under_sync() { - try {under = Pattern.compile(pattern, Pattern.DOTALL | Pattern.UNICODE_CHARACTER_CLASS);} // JRE.7:UNICODE_CHARACTER_CLASS; added during %w fix for en.w:A#; DATE:2015-06-10 + try {under = Pattern.compile(pattern, flags);} catch (Exception e) { // NOTE: if invalid, then default to empty pattern (which should return nothing); EX:d:〆る generates [^]; DATE:2013-10-20 pattern_is_invalid = true; pattern_is_invalid_exception = e; - under = Pattern.compile("", Pattern.DOTALL | Pattern.UNICODE_CHARACTER_CLASS); + under = Pattern.compile("", flags); } } public Regx_match Match(String input, int bgn) { @@ -67,4 +71,18 @@ public class Regx_adp { return new Regx_match(success, match_bgn, match_end, ary); } public String ReplaceAll(String input, String replace) {return under.matcher(input).replaceAll(replace);} + // https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html + public static final int + FLAG__NONE = 0 + , FLAG__UNIX_LINES = Pattern.UNIX_LINES + , FLAG__CASE_INSENSITIVE = Pattern.CASE_INSENSITIVE + , FLAG__COMMENTS = Pattern.COMMENTS + , FLAG__MULTILINE = Pattern.MULTILINE + , FLAG__LITERAL = Pattern.LITERAL + , FLAG__DOTALL = Pattern.DOTALL + , FLAG__UNICODE_CASE = Pattern.UNICODE_CASE + , FLAG__CANON_EQ = Pattern.CANON_EQ + , FLAG__UNICODE_CHARACTER_CLASS = Pattern.UNICODE_CHARACTER_CLASS + ; + public static final int FLAG__DEFAULT = FLAG__DOTALL | FLAG__UNICODE_CHARACTER_CLASS;// JRE.7:UNICODE_CHARACTER_CLASS; added during %w fix for en.w:A#; DATE:2015-06-10 } diff --git a/100_core/src/gplx/langs/regxs/Regx_adp_.java b/100_core/src/gplx/langs/regxs/Regx_adp_.java index 68c3aa947..46b731a16 100644 --- a/100_core/src/gplx/langs/regxs/Regx_adp_.java +++ b/100_core/src/gplx/langs/regxs/Regx_adp_.java @@ -15,7 +15,7 @@ Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt */ package gplx.langs.regxs; import gplx.*; import gplx.langs.*; public class Regx_adp_ { - public static Regx_adp new_(String pattern) {return new Regx_adp(pattern);} + public static Regx_adp new_(String pattern) {return new Regx_adp(pattern, Regx_adp.FLAG__DEFAULT);} public static List_adp Find_all(String src, String pat) { int src_len = String_.Len(src); Regx_adp regx = Regx_adp_.new_(pat); @@ -34,7 +34,7 @@ public class Regx_adp_ { return regx.ReplaceAll(raw, replace); } public static boolean Match(String input, String pattern) { - Regx_adp rv = new Regx_adp(pattern); + Regx_adp rv = new Regx_adp(pattern, Regx_adp.FLAG__DEFAULT); return rv.Match(input, 0).Rslt(); } } diff --git a/100_core/src/gplx/langs/regxs/Regx_match.java b/100_core/src/gplx/langs/regxs/Regx_match.java index 7eed05e5a..7df9e55f0 100644 --- a/100_core/src/gplx/langs/regxs/Regx_match.java +++ b/100_core/src/gplx/langs/regxs/Regx_match.java @@ -26,6 +26,7 @@ public class Regx_match { public int Find_bgn() {return find_bgn;} private final int find_bgn; public int Find_end() {return find_end;} private final int find_end; public int Find_len() {return find_end - find_bgn;} + public String Find_str(String s) {return String_.Mid(s, find_bgn, find_end);} public Regx_group[] Groups() {return groups;} private final Regx_group[] groups; public static final Regx_match[] Ary_empty = new Regx_match[0]; diff --git a/110_gfml/src_100_tkn/gplx/gfml/GfmlLxr_.java b/110_gfml/src_100_tkn/gplx/gfml/GfmlLxr_.java index 286c014f0..e444f0ef2 100644 --- a/110_gfml/src_100_tkn/gplx/gfml/GfmlLxr_.java +++ b/110_gfml/src_100_tkn/gplx/gfml/GfmlLxr_.java @@ -80,7 +80,7 @@ class GfmlLxr_group implements GfmlLxr { public GfmlTkn MakeTkn(CharStream stream, int hookLength) { while (stream.AtMid()) { if (!ignoreOutput) - sb.Add_mid(stream.Ary(), stream.Pos(), hookLength); + sb.Add_mid_len(stream.Ary(), stream.Pos(), hookLength); stream.MoveNextBy(hookLength); String found = String_.cast(trie.FindMatch(stream)); diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpArray_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpArray_.java index 357446b56..dd968c02e 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpArray_.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpArray_.java @@ -142,4 +142,26 @@ public class XophpArray_ { return true; return false; } + + // REF.PHP:https://www.php.net/manual/en/function.array-map.php + public static XophpArray array_map(XophpCallbackOwner callback_owner, String method, XophpArray array) { + XophpArray rv = XophpArray.New(); + int len = array.count(); + for (int i = 0; i < len; i++) { + String itm = array.Get_at_str(i); + rv.Add((String)callback_owner.Callback(method, itm)); + } + return rv; + } + + // REF.PHP:https://www.php.net/manual/en/function.array-flip.php + public static XophpArray array_flip(XophpArray array) { + XophpArray rv = XophpArray.New(); + int len = array.count(); + for (int i = 0; i < len; i++) { + XophpArrayItm itm = array.Get_at_itm(i); + rv.Set(Object_.Xto_str_strict_or_null(itm.Val()), itm.Key()); + } + return rv; + } } diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpArray__tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpArray__tst.java index 5e73e1d60..7cfd86698 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpArray__tst.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpArray__tst.java @@ -222,6 +222,27 @@ public class XophpArray__tst { // REF:https://www.php.net/manual/en/function.arr , orig.values() ); } + @Test public void array_map() { + XophpArray orig = fxt.Make().Add_many("a", "b", "c"); + fxt.Test__eq + ( fxt.Make().Add_many("A", "B", "C") + , XophpArray_.array_map(XophpString_.Callback_owner, "strtoupper", orig) + ); + } + @Test public void array_flip__basic() { + XophpArray orig = fxt.Make().Add_many("oranges", "apples", "pears"); + fxt.Test__eq + ( fxt.Make().Add("oranges", 0).Add("apples", 1).Add("pears", 2) + , XophpArray_.array_flip(orig) + ); + } + @Test public void array_flip__collision() { + XophpArray orig = fxt.Make().Add("a", 1).Add("b", 1).Add("c", 2); + fxt.Test__eq + ( fxt.Make().Add("1", "b").Add("2", "c") + , XophpArray_.array_flip(orig) + ); + } } class XophpArray__fxt { public XophpArray Make() {return new XophpArray();} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpCallbackOwner.java b/400_xowa/src/gplx/xowa/mediawiki/XophpCallbackOwner.java new file mode 100644 index 000000000..ddfbed3bc --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpCallbackOwner.java @@ -0,0 +1,19 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +public interface XophpCallbackOwner { + Object Callback(String method, Object... args); +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_.java index db1483fa4..96fd05966 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_.java @@ -15,6 +15,7 @@ Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt */ package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; import gplx.langs.regxs.*; +import gplx.core.strings.*; import gplx.core.primitives.*; import gplx.core.bits.*; public class XophpRegex_ { public static boolean preg_match_bool(Regx_adp pattern, int modifier, String subject) {return preg_match_bool(pattern, modifier, subject, null, 0, 0);} public static boolean preg_match_bool(Regx_adp pattern, String subject, XophpArray matches, int flags, int offset) {return preg_match(pattern, MODIFIER_NONE, subject, matches, flags, offset) == FOUND;} @@ -70,17 +71,160 @@ public class XophpRegex_ { } } } + // REF.PHP:https://www.php.net/manual/en/function.preg-match-all.php + // $flags = PREG_PATTERN_ORDER + public static int preg_match_all(Regx_adp pattern, String subject, XophpArray matches, int flags) {return preg_match_all(pattern, subject, matches, flags, 0);} + public static int preg_match_all(Regx_adp pattern, String subject, XophpArray matches, int flags, int offset) { + // decompose flags to bools + // boolean unmatched_as_null = Bitmask_.Has_int(flags, PREG_OFFSET_CAPTURE); + boolean offset_capture = Bitmask_.Has_int(flags, PREG_OFFSET_CAPTURE); + boolean pattern_order = Bitmask_.Has_int(flags, PREG_PATTERN_ORDER); + boolean set_order = Bitmask_.Has_int(flags, PREG_SET_ORDER); + + if (pattern_order && set_order) { // PHP.TEST:echo(preg_match_all("|a|U", "a", $out, PREG_SET_ORDER | PREG_PATTERN_ORDER)); + matches.Clear(); + return 0; + } +// else if (!pattern_order && !set_order) { // occurs when passing just PREG_OFFSET_CAPTURE +// set_order = true; +// } + + // ARRAY + XophpArray array_0 = null; + XophpArray array_1 = null; + boolean match_is_full = true; + if (pattern_order) { + array_0 = XophpArray.New(); + array_1 = XophpArray.New(); + matches.Add(array_0); + matches.Add(array_1); + } + int len = String_.Len(subject); + int count = 0; + while (offset < len) { + Regx_match match = pattern.Match(subject, offset); + if (!match.Rslt()) + break; + + Regx_group[] groups = match.Groups(); + int groups_len = groups.length; + XophpArray array = null; + if (set_order) { + array = XophpArray.New(); + matches.Add(array); + } + for (int i = 0; i < groups_len; i++) { + Regx_group group = groups[i]; + if (pattern_order) { + array = match_is_full ? array_0 : array_1; + match_is_full = !match_is_full; + } + if (offset_capture) { + matches.Add(XophpArray.New(group.Val(), group.Bgn())); + } + else { + array.Add(group.Val()); + } + } + + offset = match.Find_end(); + count++; + } + return count; + } + + // REF.PHP:https://www.php.net/manual/en/function.preg-replace.php + public static final int preg_replace_limit_none = -1; + public static String preg_replace(Regx_adp pattern, String replacement, String subject) {return preg_replace(pattern, replacement, subject, -1, null);} + public static String preg_replace(Regx_adp pattern, String replacement, String subject, int limit, Int_obj_ref count_rslt) { + // if no limit specified, default to max + if (limit == preg_replace_limit_none) limit = Int_.Max_value; + + // init vars for loop + int pos = 0; + int count = 0; + String_bldr sb = null; + + // exec match + for (int i = 0; i < limit; i++) { + // find next + Regx_match match = pattern.Match(subject, pos); + + // found nothing; stop + if (!match.Rslt()) { + if (count == 0) + return subject; // optimized case if no matches + else + break; + } + + // found something + if (sb == null) {sb = String_bldr_.new_();} // lazy-make sb + + // add everything up to match + sb.Add_mid(subject, pos, match.Find_bgn()); + + // add repl + sb.Add(replacement); + + // update counters + pos = match.Find_end(); + count++; + } + + // add rest of String + sb.Add_mid(subject, pos, String_.Len(subject)); + + // update count_rslt if set + if (count_rslt != null) count_rslt.Val_(count); + + // return + return sb.To_str_and_clear(); + } // REF.PHP:https://www.php.net/manual/en/pcre.constants.php + // REF.PHP:https://github.com/php/php-src/blob/master/ext/pcre/php_pcre.c public static final int - PREG_OFFSET_CAPTURE = 256 - , PREG_UNMATCHED_AS_NULL = 0 - , PREG_NO_FLAG = Int_.Min_value - , PREG_ERR = -1 + PREG_NO_FLAG = Int_.Min_value + , PREG_ERR = -1 + + , PREG_PATTERN_ORDER = 1 + , PREG_SET_ORDER = 2 + , PREG_OFFSET_CAPTURE = 1<<8 + , PREG_UNMATCHED_AS_NULL = 1<<9 + +// , PREG_SPLIT_NO_EMPTY = 1<<0 +// , PREG_SPLIT_DELIM_CAPTURE = 1<<1 +// , PREG_SPLIT_OFFSET_CAPTURE = 1<<2 + +// , PREG_REPLACE_EVAL = 1<<0 +// +// , PREG_GREP_INVERT = 1<<0 +// +// , PREG_JIT = 1<<3 ; + public static Regx_adp Pattern(String pattern) {return new Regx_adp(pattern, Regx_adp.FLAG__NONE);} + public static Regx_adp Pattern(String pattern, int modifier) { + int flags = Regx_adp.FLAG__NONE; + if (Bitmask_.Has_int(modifier, MODIFIER_i)) + flags = Bitmask_.Set_or_add(flags, Regx_adp.FLAG__CASE_INSENSITIVE); + if (Bitmask_.Has_int(modifier, MODIFIER_m)) + flags = Bitmask_.Set_or_add(flags, Regx_adp.FLAG__MULTILINE); + if (Bitmask_.Has_int(modifier, MODIFIER_s)) + flags = Bitmask_.Set_or_add(flags, Regx_adp.FLAG__DOTALL); + if (Bitmask_.Has_int(modifier, MODIFIER_x)) + flags = Bitmask_.Set_or_add(flags, Regx_adp.FLAG__COMMENTS); + if (Bitmask_.Has_int(modifier, MODIFIER_U)) { + pattern = String_.Replace(pattern, ".*", ".*?"); + } + return new Regx_adp(pattern, flags); + } + public static final int NOT_FOUND = 0, FOUND = 1; // REF.PHP:https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php + // Some modifiers can be set using "(?LETTER)"; EX: "(?J)"; REF.PHP:https://www.php.net/manual/en/regexp.reference.@gplx.Internal protected-options.php + // https://stackoverflow.com/questions/5767627/how-to-add-features-missing-from-the-java-regex-implementation/5771326#5771326 public static final int MODIFIER_NONE = 0 , MODIFIER_i = Math_.Pow_int(2, 0) // PCRE_CASELESS: If this modifier is set, letters in the pattern match both upper and lower case letters. diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_match_all_tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_match_all_tst.java new file mode 100644 index 000000000..1eaca3c4d --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_match_all_tst.java @@ -0,0 +1,95 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +import org.junit.*; import gplx.core.tests.*; +import gplx.core.primitives.*; +import gplx.langs.regxs.*; +public class XophpRegex_match_all_tst { + private final XophpRegex_match_all_fxt fxt = new XophpRegex_match_all_fxt(); + @Test public void Pattern_order() { + fxt.Test__preg_match_all + ( XophpRegex_.Pattern("<[^>]+>(.*)]+>", XophpRegex_.MODIFIER_U) + , "example:
this is a test
" + , XophpRegex_.PREG_PATTERN_ORDER + , fxt.Expd() + .Add_many(XophpArray.New("example: ", "
this is a test
")) + .Add_many(XophpArray.New("example: ", "this is a test")) + ); + } +// @Test public void Pattern_order_matches() { +// // PCRE does not allow duplicate named groups by default. PCRE 6.7 and later allow them if you turn on that option or use the mode modifier (?J). +// fxt.Test__preg_match_all +// ( XophpRegex_.Pattern("(?foo)|(?bar)", XophpRegex_.MODIFIER_U | XophpRegex_.MODIFIER_J) // (?J) changed to MODIFIER_J +// , "foo bar" +// , XophpRegex_.PREG_PATTERN_ORDER +// , fxt.Expd() +// .Add(0, "example: ").Add(0, "
this is a test
") +// .Add(1, "example: ").Add(1, "this is a test") +// ); +// } + @Test public void Set_order() { + fxt.Test__preg_match_all + ( XophpRegex_.Pattern("<[^>]+>(.*)]+>", XophpRegex_.MODIFIER_U) + , "example:
this is a test
" + , XophpRegex_.PREG_SET_ORDER + , fxt.Expd() + .Add_many(XophpArray.New("example: ", "example: ")) + .Add_many(XophpArray.New("
this is a test
", "this is a test")) + ); + } + @Test public void Offset_capture() { + fxt.Test__preg_match_all + ( XophpRegex_.Pattern("(foo)(bar)(baz)", XophpRegex_.MODIFIER_U) + , "foobarbaz" + , XophpRegex_.PREG_OFFSET_CAPTURE + , fxt.Expd() + .Add_many + ( XophpArray.New("foobarbaz", "0") + , XophpArray.New("foo", "0") + , XophpArray.New("bar", "3") + , XophpArray.New("baz", "6") + ) + ); + } +} +class XophpRegex_match_all_fxt { + public XophpRegex_match_all_expd Expd() {return new XophpRegex_match_all_expd();} + public void Test__preg_match_all(Regx_adp pattern, String subject, XophpRegex_match_all_expd rslt) {Test__preg_match_all(pattern, subject, XophpRegex_.PREG_NO_FLAG, 0, rslt);} + public void Test__preg_match_all(Regx_adp pattern, String subject, int flags, XophpRegex_match_all_expd rslt) {Test__preg_match_all(pattern, subject, flags, 0, rslt);} + public void Test__preg_match_all(Regx_adp pattern, String subject, int flags, int offset, XophpRegex_match_all_expd expd) { + XophpArray actl = XophpArray.New(); + XophpRegex_.preg_match_all(pattern, subject, actl, flags, offset); + + Gftest.Eq__ary__lines(expd.Ary().To_str(), actl.To_str()); + } +} +class XophpRegex_match_all_expd { + public XophpArray Ary() {return ary;} private final XophpArray ary = XophpArray.New(); + public XophpRegex_match_all_expd Add(int idx, Object val) { + XophpArray sub = ary.Get_at_ary(idx); + if (sub == null) { + sub = XophpArray.New(); + ary.Set(idx, sub); + } + sub.Add(val); + return this; + } + public XophpRegex_match_all_expd Add_many(Object... vals) { + for (Object val : vals) + ary.Add(val); + return this; + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_replace_tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_replace_tst.java new file mode 100644 index 000000000..498c0eba0 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_replace_tst.java @@ -0,0 +1,49 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +import org.junit.*; import gplx.core.tests.*; +import gplx.core.primitives.*; +import gplx.langs.regxs.*; +public class XophpRegex_replace_tst { + private final XophpRegex_replace_fxt fxt = new XophpRegex_replace_fxt(); + @Test public void Basic() { + // basic + fxt.Test__preg_replace("0", "1", "0ab0cd0ef", fxt.Expd("1ab1cd1ef").Count_(3)); + + // limit + fxt.Test__preg_replace("0", "1", "0ab0cd0ef", 2, fxt.Expd("1ab1cd0ef").Count_(2)); + } +} +class XophpRegex_replace_fxt { + public XophpRegex_replace_expd Expd(String rslt) {return new XophpRegex_replace_expd(rslt);} + public void Test__preg_replace(String pattern, String replacement, String subject, XophpRegex_replace_expd rslt) {Test__preg_replace(pattern, replacement, subject, XophpRegex_.preg_replace_limit_none, rslt);} + public void Test__preg_replace(String pattern, String replacement, String subject, int limit, XophpRegex_replace_expd expd) { + Int_obj_ref actl_count = Int_obj_ref.New_zero(); + String actl = XophpRegex_.preg_replace(Regx_adp_.new_(pattern), replacement, subject, limit, actl_count); + + Gftest.Eq__str(expd.Rslt(), actl); + if (expd.Count() != -1) + Gftest.Eq__int(expd.Count(), actl_count.Val()); + } +} +class XophpRegex_replace_expd { + public XophpRegex_replace_expd(String rslt) { + this.rslt = rslt; + } + public String Rslt() {return rslt;} private final String rslt; + public int Count() {return count;} private int count = -1; + public XophpRegex_replace_expd Count_(int v) {count = v; return this;} +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpRuntimeException.java b/400_xowa/src/gplx/xowa/mediawiki/XophpRuntimeException.java new file mode 100644 index 000000000..34dbf9e91 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpRuntimeException.java @@ -0,0 +1,19 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +public class XophpRuntimeException extends XophpError { public XophpRuntimeException(String msg) {super(msg); + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java index 69c820838..65dce67a4 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java @@ -18,7 +18,7 @@ import gplx.core.btries.*; import gplx.core.intls.*; import gplx.objects.strings.unicodes.*; import gplx.core.primitives.*; -public class XophpString_ { +public class XophpString_ implements XophpCallbackOwner { public static final String Null = null; public static boolean is_true(String s) {return s != null;} // handles code like "if ($var)" where var is an Object; @@ -358,6 +358,11 @@ public class XophpString_ { public static boolean is_string(Object o) { return String_.as_(o) != null; } + + // REF.PHP: https://www.php.net/manual/en/function.strtoupper.php + public static String strtoupper(String s) { + return String_.Upper(s); + } public static String strtolower(String s) { return String_.Lower(s); } @@ -462,4 +467,14 @@ public class XophpString_ { return b >= 128 && b <= 255; } } + public Object Callback(String method, Object... args) { + if (String_.Eq(method, "strtoupper")) { + String val = (String)args[0]; + return strtoupper(val); + } + else { + throw Err_.new_unhandled_default(method); + } + } + public static final XophpCallbackOwner Callback_owner = new XophpString_(); } diff --git a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/Wbase_client.java b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/Wbase_client.java deleted file mode 100644 index 8a472afa6..000000000 --- a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/Wbase_client.java +++ /dev/null @@ -1,36 +0,0 @@ -/* -XOWA: the XOWA Offline Wiki Application -Copyright (C) 2012-2017 gnosygnu@gmail.com - -XOWA is licensed under the terms of the General Public License (GPL) Version 3, -or alternatively under the terms of the Apache License Version 2.0. - -You may use XOWA according to either of these licenses as is most appropriate -for your project on a case-by-case basis. - -The terms of each license can be found in the source code repository: - -GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt -Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt -*/ -package gplx.xowa.mediawiki.extensions.Wikibase.client.includes; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.extensions.*; import gplx.xowa.mediawiki.extensions.Wikibase.*; import gplx.xowa.mediawiki.extensions.Wikibase.client.*; -import gplx.xowa.mediawiki.*; -public class Wbase_client { - private Wbase_repo_linker repoLinker; - public Wbase_client(Wbase_settings settings) { - this.repoLinker = new Wbase_repo_linker - ( settings.getSetting(Wbase_settings.Setting_repoUrl) - , settings.getSetting(Wbase_settings.Setting_repoArticlePath) - , settings.getSetting(Wbase_settings.Setting_repoScriptPath) - ); - } - public Wbase_repo_linker RepoLinker() {return repoLinker;} - - private static Wbase_client defaultInstance; - public static Wbase_client getDefaultInstance() { - if (defaultInstance == null) { - defaultInstance = new Wbase_client(Wbase_settings.New_dflt()); - } - return defaultInstance; - } -} diff --git a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/WikibaseClient.java b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/WikibaseClient.java new file mode 100644 index 000000000..b0b2e7add --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/WikibaseClient.java @@ -0,0 +1,1264 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.extensions.Wikibase.client.includes; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.extensions.*; import gplx.xowa.mediawiki.extensions.Wikibase.*; import gplx.xowa.mediawiki.extensions.Wikibase.client.*; +import gplx.xowa.mediawiki.*; +import gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.Store.*; +public class WikibaseClient { + private Wbase_repo_linker repoLinker; + public WikibaseClient(Wbase_settings settings) { + this.repoLinker = new Wbase_repo_linker + (settings.getSetting(Wbase_settings.Setting_repoUrl) + , settings.getSetting(Wbase_settings.Setting_repoArticlePath) + , settings.getSetting(Wbase_settings.Setting_repoScriptPath) + ); + } + public Wbase_repo_linker RepoLinker() {return repoLinker;} + + private static WikibaseClient defaultInstance; + public static WikibaseClient getDefaultInstance() { + if (defaultInstance == null) { + defaultInstance = new WikibaseClient(Wbase_settings.New_dflt()); + } + return defaultInstance; + } + +// /** +// * @var SettingsArray +// */ +// private settings; +// +// /** +// * @var SiteLookup +// */ +// private siteLookup; +// +// /** +// * @var WikibaseServices +// */ +// private wikibaseServices; +// +// /** +// * @var PropertyDataTypeLookup|null +// */ +// private propertyDataTypeLookup = null; +// +// /** +// * @var DataTypeFactory|null +// */ +// private dataTypeFactory = null; +// +// /** +// * @var Deserializer|null +// */ +// private entityDeserializer = null; +// +// /** +// * @var Serializer|null +// */ +// private compactEntitySerializer = null; +// +// /** +// * @var EntityIdParser|null +// */ +// private entityIdParser = null; +// +// /** +// * @var EntityIdComposer|null +// */ +// private entityIdComposer = null; +// +// /** +// * @var ClientStore|null +// */ +// private store = null; +// +// /** +// * @var Site|null +// */ +// private site = null; +// +// /** +// * @var String|null +// */ +// private siteGroup = null; +// +// /** +// * @var OutputFormatSnakFormatterFactory|null +// */ +// private snakFormatterFactory = null; +// +// /** +// * @var OutputFormatValueFormatterFactory|null +// */ +// private valueFormatterFactory = null; +// +// /** +// * @var LangLinkHandler|null +// */ +// private langLinkHandler = null; +// +// /** +// * @var ClientParserOutputDataUpdater|null +// */ +// private parserOutputDataUpdater = null; +// +// /** +// * @var NamespaceChecker|null +// */ +// private namespaceChecker = null; +// +// /** +// * @var RestrictedEntityLookup|null +// */ +// private restrictedEntityLookup = null; +// +// /** +// * @var DataTypeDefinitions +// */ +// private dataTypeDefinitions; +// +// /** +// * @var EntityTypeDefinitions +// */ +// private entityTypeDefinitions; +// +// /** +// * @var RepositoryDefinitions +// */ +// private repositoryDefinitions; +// +// /** +// * @var TermLookup|null +// */ +// private termLookup = null; +// +// /** +// * @var TermBuffer|null +// */ +// private termBuffer = null; +// +// /** +// * @var PrefetchingTermLookup|null +// */ +// private prefetchingTermLookup = null; + + /** + * @var PropertyOrderProvider|null + */ + private XomwPropertyOrderProvider propertyOrderProvider = null; +// +// /** +// * @var SidebarLinkBadgeDisplay|null +// */ +// private sidebarLinkBadgeDisplay = null; +// +// /** +// * @var WikibaseValueFormatterBuilders|null +// */ +// private valueFormatterBuilders = null; +// +// /** +// * @var WikibaseContentLanguages|null +// */ +// private wikibaseContentLanguages = null; +// +// /** +// * @warning This is for use with bootstrap code in WikibaseClient.datatypes.php only! +// * Program logic should use WikibaseClient::getSnakFormatterFactory() instead! +// * +// * @return WikibaseValueFormatterBuilders +// */ +// public static function getDefaultValueFormatterBuilders() { +// return self::getDefaultInstance().newWikibaseValueFormatterBuilders(); +// } +// +// /** +// * Returns a low level factory Object for creating formatters for well known data types. +// * +// * @warning This is for use with getDefaultValueFormatterBuilders() during bootstrap only! +// * Program logic should use WikibaseClient::getSnakFormatterFactory() instead! +// * +// * @return WikibaseValueFormatterBuilders +// */ +// private function newWikibaseValueFormatterBuilders() { +// if (this.valueFormatterBuilders == null) { +// entityTitleLookup = new ClientSiteLinkTitleLookup( +// this.getStore().getSiteLinkLookup(), +// this.settings.getSetting('siteGlobalID') +// ); +// +// this.valueFormatterBuilders = new WikibaseValueFormatterBuilders( +// this.getContentLanguage(), +// new FormatterLabelDescriptionLookupFactory(this.getTermLookup()), +// new LanguageNameLookup(this.getUserLanguage().getCode()), +// this.getRepoItemUriParser(), +// this.settings.getSetting('geoShapeStorageBaseUrl'), +// this.settings.getSetting('tabularDataStorageBaseUrl'), +// this.getFormatterCache(), +// this.settings.getSetting('sharedCacheDuration'), +// this.getEntityLookup(), +// this.getStore().getEntityRevisionLookup(), +// entityTitleLookup +// ); +// } +// +// return this.valueFormatterBuilders; +// } +// +// /** +// * @warning This is for use with bootstrap code in WikibaseClient.datatypes.php only! +// * Program logic should use WikibaseClient::getSnakFormatterFactory() instead! +// * +// * @return WikibaseSnakFormatterBuilders +// */ +// public static function getDefaultSnakFormatterBuilders() { +// static builders; +// +// if (builders == null) { +// builders = self::getDefaultInstance().newWikibaseSnakFormatterBuilders( +// self::getDefaultValueFormatterBuilders() +// ); +// } +// +// return builders; +// } +// +// /** +// * Returns a low level factory Object for creating formatters for well known data types. +// * +// * @warning This is for use with getDefaultValueFormatterBuilders() during bootstrap only! +// * Program logic should use WikibaseClient::getSnakFormatterFactory() instead! +// * +// * @param WikibaseValueFormatterBuilders valueFormatterBuilders +// * +// * @return WikibaseSnakFormatterBuilders +// */ +// private function newWikibaseSnakFormatterBuilders(WikibaseValueFormatterBuilders valueFormatterBuilders) { +// return new WikibaseSnakFormatterBuilders( +// valueFormatterBuilders, +// this.getStore().getPropertyInfoLookup(), +// this.getPropertyDataTypeLookup(), +// this.getDataTypeFactory() +// ); +// } +// +// public function __construct( +// SettingsArray settings, +// DataTypeDefinitions dataTypeDefinitions, +// EntityTypeDefinitions entityTypeDefinitions, +// RepositoryDefinitions repositoryDefinitions, +// SiteLookup siteLookup +// ) { +// this.settings = settings; +// this.dataTypeDefinitions = dataTypeDefinitions; +// this.entityTypeDefinitions = entityTypeDefinitions; +// this.repositoryDefinitions = repositoryDefinitions; +// this.siteLookup = siteLookup; +// } +// +// /** +// * @return DataTypeFactory +// */ +// public function getDataTypeFactory() { +// if (this.dataTypeFactory == null) { +// this.dataTypeFactory = new DataTypeFactory(this.dataTypeDefinitions.getValueTypes()); +// } +// +// return this.dataTypeFactory; +// } +// +// /** +// * @return EntityIdParser +// */ +// public function getEntityIdParser() { +// if (this.entityIdParser == null) { +// this.entityIdParser = new DispatchingEntityIdParser( +// this.entityTypeDefinitions.getEntityIdBuilders() +// ); +// } +// +// return this.entityIdParser; +// } +// +// /** +// * @return EntityIdComposer +// */ +// public function getEntityIdComposer() { +// if (this.entityIdComposer == null) { +// this.entityIdComposer = new EntityIdComposer( +// this.entityTypeDefinitions.getEntityIdComposers() +// ); +// } +// +// return this.entityIdComposer; +// } +// +// /** +// * @return WikibaseServices +// */ +// public function getWikibaseServices() { +// if (this.wikibaseServices == null) { +// this.wikibaseServices = new MultipleRepositoryAwareWikibaseServices( +// this.getEntityIdParser(), +// this.getEntityIdComposer(), +// this.repositoryDefinitions, +// this.entityTypeDefinitions, +// this.getDataAccessSettings(), +// this.getMultiRepositoryServiceWiring(), +// this.getPerRepositoryServiceWiring(), +// MediaWikiServices::getInstance().getNameTableStoreFactory() +// ); +// } +// +// return this.wikibaseServices; +// } +// +// private function getDataAccessSettings() { +// return new DataAccessSettings( +// this.settings.getSetting('maxSerializedEntitySize'), +// this.settings.getSetting('useTermsTableSearchFields'), +// this.settings.getSetting('forceWriteTermsTableSearchFields') +// ); +// } +// +// private function getMultiRepositoryServiceWiring() { +// global wgWikibaseMultiRepositoryServiceWiringFiles; +// +// wiring = []; +// foreach (wgWikibaseMultiRepositoryServiceWiringFiles as file) { +// wiring = array_merge( +// wiring, +// require file +// ); +// } +// return wiring; +// } +// +// private function getPerRepositoryServiceWiring() { +// global wgWikibasePerRepositoryServiceWiringFiles; +// +// wiring = []; +// foreach (wgWikibasePerRepositoryServiceWiringFiles as file) { +// wiring = array_merge( +// wiring, +// require file +// ); +// } +// return wiring; +// } +// +// /** +// * @return EntityLookup +// */ +// private function getEntityLookup() { +// return this.getStore().getEntityLookup(); +// } +// +// /** +// * @return array[] +// */ +// private static function getDefaultEntityTypes() { +// return require __DIR__ . '/../../lib/WikibaseLib.entitytypes.php'; +// } +// +// /** +// * @return TermBuffer +// */ +// public function getTermBuffer() { +// if (!this.termBuffer) { +// this.termBuffer = this.getPrefetchingTermLookup(); +// } +// +// return this.termBuffer; +// } +// +// /** +// * @return TermLookup +// */ +// public function getTermLookup() { +// if (!this.termLookup) { +// this.termLookup = this.getPrefetchingTermLookup(); +// } +// +// return this.termLookup; +// } +// +// /** +// * @return PrefetchingTermLookup +// */ +// private function getPrefetchingTermLookup() { +// if (!this.prefetchingTermLookup) { +// // TODO: This should not assume the TermBuffer instance to be a PrefetchingTermLookup +// this.prefetchingTermLookup = this.getWikibaseServices().getTermBuffer(); +// } +// +// return this.prefetchingTermLookup; +// } +// +// /** +// * @param String displayLanguageCode +// * +// * XXX: This is not used by client itself, but is used by ArticlePlaceholder! +// * +// * @return TermSearchInteractor +// */ +// public function newTermSearchInteractor(displayLanguageCode) { +// return this.getWikibaseServices().getTermSearchInteractorFactory() +// .newInteractor(displayLanguageCode); +// } +// +// /** +// * @return PropertyDataTypeLookup +// */ +// public function getPropertyDataTypeLookup() { +// if (this.propertyDataTypeLookup == null) { +// infoLookup = this.getStore().getPropertyInfoLookup(); +// retrievingLookup = new EntityRetrievingDataTypeLookup(this.getEntityLookup()); +// this.propertyDataTypeLookup = new PropertyInfoDataTypeLookup(infoLookup, retrievingLookup); +// } +// +// return this.propertyDataTypeLookup; +// } +// +// /** +// * @return StringNormalizer +// */ +// public function getStringNormalizer() { +// return this.getWikibaseServices().getStringNormalizer(); +// } +// +// /** +// * @return RepoLinker +// */ +// public function newRepoLinker() { +// return new RepoLinker( +// this.settings.getSetting('repoUrl'), +// this.getRepositoryDefinitions().getConceptBaseUris(), +// this.settings.getSetting('repoArticlePath'), +// this.settings.getSetting('repoScriptPath') +// ); +// } +// +// /** +// * @return LanguageFallbackChainFactory +// */ +// public function getLanguageFallbackChainFactory() { +// return this.getWikibaseServices().getLanguageFallbackChainFactory(); +// } +// +// /** +// * @return LanguageFallbackLabelDescriptionLookupFactory +// */ +// public function getLanguageFallbackLabelDescriptionLookupFactory() { +// return new LanguageFallbackLabelDescriptionLookupFactory( +// this.getLanguageFallbackChainFactory(), +// this.getTermLookup(), +// this.getTermBuffer() +// ); +// } +// +// /** +// * Returns an instance of the default store. +// * +// * @return ClientStore +// */ +// public function getStore() { +// if (this.store == null) { +// this.store = new DirectSqlStore( +// this.getEntityChangeFactory(), +// this.getEntityIdParser(), +// this.getEntityIdComposer(), +// this.getEntityNamespaceLookup(), +// this.getWikibaseServices(), +// this.getSettings(), +// this.getRepositoryDefinitions().getDatabaseNames()[''], +// this.getContentLanguage().getCode(), +// LoggerFactory::getInstance('PageRandomLookup') +// ); +// } +// +// return this.store; +// } +// +// /** +// * Overrides the default store to be used in the client app context. +// * This is intended for use by test cases. +// * +// * @param ClientStore|null store +// * +// * @throws LogicException If MW_PHPUNIT_TEST is not defined, to avoid this +// * method being abused in production code. +// */ +// public function overrideStore(ClientStore store = null) { +// if (!defined('MW_PHPUNIT_TEST')) { +// throw new LogicException('Overriding the store instance is only supported in test mode'); +// } +// +// this.store = store; +// } +// +// /** +// * Overrides the TermLookup to be used. +// * This is intended for use by test cases. +// * +// * @param TermLookup|null lookup +// * +// * @throws LogicException If MW_PHPUNIT_TEST is not defined, to avoid this +// * method being abused in production code. +// */ +// public function overrideTermLookup(TermLookup lookup = null) { +// if (!defined('MW_PHPUNIT_TEST')) { +// throw new LogicException('Overriding TermLookup is only supported in test mode'); +// } +// +// this.termLookup = lookup; +// } +// +// /** +// * @throws MWException when called to early +// * @return Language +// */ +// public function getContentLanguage() { +// global wgContLang; +// +// // TODO: define a LanguageProvider service instead of using a global directly. +// // NOTE: we cannot inject wgContLang in the constructor, because it may still be null +// // when WikibaseClient is initialized. In particular, the language Object may not yet +// // be there when the SetupAfterCache hook is run during bootstrapping. +// +// if (!wgContLang) { +// throw new MWException('Premature access: wgContLang is not yet initialized!'); +// } +// +// StubObject::unstub(wgContLang); +// return wgContLang; +// } +// +// /** +// * @throws MWException when called to early +// * @return Language +// */ +// private function getUserLanguage() { +// global wgLang; +// +// // TODO: define a LanguageProvider service instead of using a global directly. +// // NOTE: we cannot inject wgLang in the constructor, because it may still be null +// // when WikibaseClient is initialized. In particular, the language Object may not yet +// // be there when the SetupAfterCache hook is run during bootstrapping. +// +// if (!wgLang) { +// throw new MWException('Premature access: wgLang is not yet initialized!'); +// } +// +// StubObject::unstub(wgLang); +// return wgLang; +// } +// +// /** +// * @return SettingsArray +// */ +// public function getSettings() { +// return this.settings; +// } +// +// /** +// * Returns a new instance constructed from global settings. +// * IMPORTANT: Use only when it is not feasible to inject an instance properly. +// * +// * @throws MWException +// * @return self +// */ +// private static function newInstance() { +// global wgWBClientDataTypes; +// +// if (!is_array(wgWBClientDataTypes)) { +// throw new MWException('wgWBClientDataTypes must be array. ' +// . 'Maybe you forgot to require WikibaseClient.php in your LocalSettings.php?'); +// } +// +// dataTypeDefinitions = wgWBClientDataTypes; +// Hooks::run('WikibaseClientDataTypes', [ &dataTypeDefinitions ]); +// +// entityTypeDefinitionsArray = self::getDefaultEntityTypes(); +// Hooks::run('WikibaseClientEntityTypes', [ &entityTypeDefinitionsArray ]); +// +// settings = WikibaseSettings::getClientSettings(); +// +// entityTypeDefinitions = new EntityTypeDefinitions(entityTypeDefinitionsArray); +// +// return new self( +// settings, +// new DataTypeDefinitions( +// dataTypeDefinitions, +// settings.getSetting('disabledDataTypes') +// ), +// entityTypeDefinitions, +// self::getRepositoryDefinitionsFromSettings(settings, entityTypeDefinitions), +// MediaWikiServices::getInstance().getSiteLookup() +// ); +// } +// +// /** +// * @param SettingsArray settings +// * @param EntityTypeDefinitions entityTypeDefinitions +// * +// * @return RepositoryDefinitions +// */ +// private static function getRepositoryDefinitionsFromSettings(SettingsArray settings, EntityTypeDefinitions entityTypeDefinitions) { +// definitions = []; +// +// // Backwards compatibility: if the old "foreignRepositories" settings is there, +// // use its values. +// repoSettingsArray = settings.hasSetting('foreignRepositories') +// ? settings.getSetting('foreignRepositories') +// : settings.getSetting('repositories'); +// +// // Backwards compatibility: if settings of the "local" repository +// // are not defined in the "repositories" settings but with individual settings, +// // fallback to old single-repo settings +// if (settings.hasSetting('repoDatabase') +// && settings.hasSetting('entityNamespaces') +// && settings.hasSetting('repoConceptBaseUri') +// ) { +// definitions = [ '' => [ +// 'database' => settings.getSetting('repoDatabase'), +// 'super-uri' => settings.getSetting('repoConceptBaseUri'), +// 'prefix-mapping' => [ '' => '' ], +// 'entity-namespaces' => settings.getSetting('entityNamespaces'), +// ] ]; +// unset(repoSettingsArray['']); +// } +// +// foreach (repoSettingsArray as repository => repositorySettings) { +// definitions[repository] = [ +// 'database' => repositorySettings['repoDatabase'], +// 'super-uri' => repositorySettings['baseUri'], +// 'entity-namespaces' => repositorySettings['entityNamespaces'], +// 'prefix-mapping' => repositorySettings['prefixMapping'], +// ]; +// } +// +// return new RepositoryDefinitions(definitions, entityTypeDefinitions); +// } +// +// /** +// * IMPORTANT: Use only when it is not feasible to inject an instance properly. +// * +// * @param String reset Flag: Pass "reset" to reset the default instance +// * +// * @return self +// */ +// public static function getDefaultInstance(reset = 'noreset') { +// static instance = null; +// +// if (instance == null || reset == 'reset') { +// instance = self::newInstance(); +// } +// +// return instance; +// } +// +// /** +// * Returns the this client wiki's site Object. +// * +// * This is taken from the siteGlobalID setting, which defaults +// * to the wiki's database name. +// * +// * If the configured site ID is not found in the sites table, a +// * new Site Object is constructed from the configured ID. +// * +// * @throws MWException +// * @return Site +// */ +// public function getSite() { +// if (this.site == null) { +// globalId = this.settings.getSetting('siteGlobalID'); +// localId = this.settings.getSetting('siteLocalID'); +// +// this.site = this.siteLookup.getSite(globalId); +// +// if (!this.site) { +// wfDebugLog(__CLASS__, __FUNCTION__ . ": Unable to resolve site ID '{globalId}'!"); +// +// this.site = new MediaWikiSite(); +// this.site.setGlobalId(globalId); +// this.site.addLocalId(Site::ID_INTERWIKI, localId); +// this.site.addLocalId(Site::ID_EQUIVALENT, localId); +// } +// +// if (!in_array(localId, this.site.getLocalIds())) { +// wfDebugLog(__CLASS__, __FUNCTION__ +// . ": The configured local id localId does not match any local ID of site globalId: " +// . var_export(this.site.getLocalIds(), true)); +// } +// } +// +// return this.site; +// } +// +// /** +// * Returns the site group ID for the group to be used for language links. +// * This is typically the group the client wiki itself belongs to, but +// * can be configured to be otherwise using the languageLinkSiteGroup setting. +// * +// * @return String +// */ +// public function getLangLinkSiteGroup() { +// group = this.settings.getSetting('languageLinkSiteGroup'); +// +// if (group == null) { +// group = this.getSiteGroup(); +// } +// +// return group; +// } +// +// /** +// * Gets the site group ID from setting, which if not set then does +// * lookup in site store. +// * +// * @return String +// */ +// private function newSiteGroup() { +// siteGroup = this.settings.getSetting('siteGroup'); +// +// if (!siteGroup) { +// siteId = this.settings.getSetting('siteGlobalID'); +// +// site = this.siteLookup.getSite(siteId); +// +// if (!site) { +// return true; +// } +// +// siteGroup = site.getGroup(); +// } +// +// return siteGroup; +// } +// +// /** +// * Get site group ID +// * +// * @return String +// */ +// public function getSiteGroup() { +// if (this.siteGroup == null) { +// this.siteGroup = this.newSiteGroup(); +// } +// +// return this.siteGroup; +// } +// +// /** +// * Returns a OutputFormatSnakFormatterFactory the provides SnakFormatters +// * for different output formats. +// * +// * @return OutputFormatSnakFormatterFactory +// */ +// private function getSnakFormatterFactory() { +// if (this.snakFormatterFactory == null) { +// this.snakFormatterFactory = new OutputFormatSnakFormatterFactory( +// this.dataTypeDefinitions.getSnakFormatterFactoryCallbacks(), +// this.getValueFormatterFactory(), +// this.getPropertyDataTypeLookup(), +// this.getDataTypeFactory() +// ); +// } +// +// return this.snakFormatterFactory; +// } +// +// /** +// * Returns a OutputFormatValueFormatterFactory the provides ValueFormatters +// * for different output formats. +// * +// * @return OutputFormatValueFormatterFactory +// */ +// private function getValueFormatterFactory() { +// if (this.valueFormatterFactory == null) { +// this.valueFormatterFactory = new OutputFormatValueFormatterFactory( +// this.dataTypeDefinitions.getFormatterFactoryCallbacks(DataTypeDefinitions::PREFIXED_MODE), +// this.getContentLanguage(), +// this.getLanguageFallbackChainFactory() +// ); +// } +// +// return this.valueFormatterFactory; +// } +// +// /** +// * @return EntityIdParser +// */ +// private function getRepoItemUriParser() { +// // B/C compatibility, should be removed soon +// // TODO: Move to check repo that has item entity not the default repo +// return new SuffixEntityIdParser( +// this.getRepositoryDefinitions().getConceptBaseUris()[''], +// new ItemIdParser() +// ); +// } +// +// /** +// * @return NamespaceChecker +// */ +// public function getNamespaceChecker() { +// if (this.namespaceChecker == null) { +// this.namespaceChecker = new NamespaceChecker( +// this.settings.getSetting('excludeNamespaces'), +// this.settings.getSetting('namespaces') +// ); +// } +// +// return this.namespaceChecker; +// } +// +// /** +// * @return LangLinkHandler +// */ +// public function getLangLinkHandler() { +// if (this.langLinkHandler == null) { +// this.langLinkHandler = new LangLinkHandler( +// this.getLanguageLinkBadgeDisplay(), +// this.getNamespaceChecker(), +// this.getStore().getSiteLinkLookup(), +// this.getStore().getEntityLookup(), +// this.siteLookup, +// this.settings.getSetting('siteGlobalID'), +// this.getLangLinkSiteGroup() +// ); +// } +// +// return this.langLinkHandler; +// } +// +// /** +// * @return ClientParserOutputDataUpdater +// */ +// public function getParserOutputDataUpdater() { +// if (this.parserOutputDataUpdater == null) { +// this.parserOutputDataUpdater = new ClientParserOutputDataUpdater( +// this.getOtherProjectsSidebarGeneratorFactory(), +// this.getStore().getSiteLinkLookup(), +// this.getStore().getEntityLookup(), +// this.settings.getSetting('siteGlobalID') +// ); +// } +// +// return this.parserOutputDataUpdater; +// } +// +// /** +// * @return SidebarLinkBadgeDisplay +// */ +// public function getSidebarLinkBadgeDisplay() { +// if (this.sidebarLinkBadgeDisplay == null) { +// labelDescriptionLookupFactory = this.getLanguageFallbackLabelDescriptionLookupFactory(); +// badgeClassNames = this.settings.getSetting('badgeClassNames'); +// lang = this.getUserLanguage(); +// +// this.sidebarLinkBadgeDisplay = new SidebarLinkBadgeDisplay( +// labelDescriptionLookupFactory.newLabelDescriptionLookup(lang), +// is_array(badgeClassNames) ? badgeClassNames : [], +// lang +// ); +// } +// +// return this.sidebarLinkBadgeDisplay; +// } +// +// /** +// * @return LanguageLinkBadgeDisplay +// */ +// public function getLanguageLinkBadgeDisplay() { +// return new LanguageLinkBadgeDisplay( +// this.getSidebarLinkBadgeDisplay() +// ); +// } +// +// /** +// * @return DeserializerFactory A factory with knowledge about items, properties, and the +// * elements they are made of, but no other entity types. +// */ +// public function getBaseDataModelDeserializerFactory() { +// return new DeserializerFactory( +// this.getDataValueDeserializer(), +// this.getEntityIdParser() +// ); +// } +// +// /** +// * @return InternalDeserializerFactory +// */ +// private function getInternalFormatDeserializerFactory() { +// return new InternalDeserializerFactory( +// this.getDataValueDeserializer(), +// this.getEntityIdParser(), +// this.getAllTypesEntityDeserializer() +// ); +// } +// +// /** +// * @return DispatchingDeserializer +// */ +// private function getAllTypesEntityDeserializer() { +// if (this.entityDeserializer == null) { +// deserializerFactoryCallbacks = this.getEntityDeserializerFactoryCallbacks(); +// baseDeserializerFactory = this.getBaseDataModelDeserializerFactory(); +// deserializers = []; +// +// foreach (deserializerFactoryCallbacks as callback) { +// deserializers[] = call_user_func(callback, baseDeserializerFactory); +// } +// +// this.entityDeserializer = new DispatchingDeserializer(deserializers); +// } +// +// return this.entityDeserializer; +// } +// +// /** +// * Returns a deserializer to deserialize statements in both current and legacy serialization. +// * +// * @return Deserializer +// */ +// public function getInternalFormatStatementDeserializer() { +// return this.getInternalFormatDeserializerFactory().newStatementDeserializer(); +// } +// +// /** +// * @return callable[] +// */ +// public function getEntityDeserializerFactoryCallbacks() { +// return this.entityTypeDefinitions.getDeserializerFactoryCallbacks(); +// } +// +// /** +// * Returns a SerializerFactory creating serializers that generate the most compact serialization. +// * A factory returned has knowledge about items, properties, and the elements they are made of, +// * but no other entity types. +// * +// * @return SerializerFactory +// */ +// public function getCompactBaseDataModelSerializerFactory() { +// return this.getWikibaseServices().getCompactBaseDataModelSerializerFactory(); +// } +// +// /** +// * Returns an entity serializer that generates the most compact serialization. +// * +// * @return Serializer +// */ +// public function getCompactEntitySerializer() { +// return this.getWikibaseServices().getCompactEntitySerializer(); +// } +// +// /** +// * @return DataValueDeserializer +// */ +// private function getDataValueDeserializer() { +// return new DataValueDeserializer([ +// 'String' => StringValue::class, +// 'unknown' => UnknownValue::class, +// 'globecoordinate' => GlobeCoordinateValue::class, +// 'monolingualtext' => MonolingualTextValue::class, +// 'quantity' => QuantityValue::class, +// 'time' => TimeValue::class, +// 'wikibase-entityid' => function (value) { +// return isset(value['id']) +// ? new EntityIdValue(this.getEntityIdParser().parse(value['id'])) +// : EntityIdValue::newFromArray(value); +// }, +// ]); +// } +// +// /** +// * @return OtherProjectsSidebarGeneratorFactory +// */ +// public function getOtherProjectsSidebarGeneratorFactory() { +// return new OtherProjectsSidebarGeneratorFactory( +// this.settings, +// this.getStore().getSiteLinkLookup(), +// this.siteLookup, +// this.getStore().getEntityLookup(), +// this.getSidebarLinkBadgeDisplay() +// ); +// } +// +// /** +// * @return EntityChangeFactory +// */ +// public function getEntityChangeFactory() { +// //TODO: take this from a setting or registry. +// changeClasses = [ +// Item::ENTITY_TYPE => ItemChange::class, +// // Other types of entities will use EntityChange +// ]; +// +// return new EntityChangeFactory( +// this.getEntityDiffer(), +// this.getEntityIdParser(), +// changeClasses +// ); +// } +// +// /** +// * @return EntityDiffer +// */ +// private function getEntityDiffer() { +// entityDiffer = new EntityDiffer(); +// foreach (this.entityTypeDefinitions.getEntityDifferStrategyBuilders() as builder) { +// entityDiffer.registerEntityDifferStrategy(call_user_func(builder)); +// } +// return entityDiffer; +// } +// +// /** +// * @return ParserFunctionRegistrant +// */ +// public function getParserFunctionRegistrant() { +// return new ParserFunctionRegistrant( +// this.settings.getSetting('allowDataTransclusion'), +// this.settings.getSetting('allowLocalShortDesc') +// ); +// } +// +// /** +// * @return StatementGroupRendererFactory +// */ +// private function getStatementGroupRendererFactory() { +// return new StatementGroupRendererFactory( +// this.getStore().getPropertyLabelResolver(), +// new SnaksFinder(), +// this.getRestrictedEntityLookup(), +// this.getDataAccessSnakFormatterFactory(), +// this.settings.getSetting('allowDataAccessInUserLanguage') +// ); +// } +// +// /** +// * @return DataAccessSnakFormatterFactory +// */ +// public function getDataAccessSnakFormatterFactory() { +// return new DataAccessSnakFormatterFactory( +// this.getLanguageFallbackChainFactory(), +// this.getSnakFormatterFactory(), +// this.getPropertyDataTypeLookup(), +// this.getRepoItemUriParser(), +// this.getLanguageFallbackLabelDescriptionLookupFactory(), +// this.settings.getSetting('allowDataAccessInUserLanguage') +// ); +// } +// +// /** +// * @return Runner +// */ +// public function getPropertyParserFunctionRunner() { +// return new Runner( +// this.getStatementGroupRendererFactory(), +// this.getStore().getSiteLinkLookup(), +// this.getEntityIdParser(), +// this.getRestrictedEntityLookup(), +// this.settings.getSetting('siteGlobalID'), +// this.settings.getSetting('allowArbitraryDataAccess') +// ); +// } +// +// /** +// * @return OtherProjectsSitesProvider +// */ +// public function getOtherProjectsSitesProvider() { +// return new CachingOtherProjectsSitesProvider( +// new OtherProjectsSitesGenerator( +// this.siteLookup, +// this.settings.getSetting('siteGlobalID'), +// this.settings.getSetting('specialSiteLinkGroups') +// ), +// // TODO: Make configurable? Should be similar, maybe identical to sharedCacheType and +// // sharedCacheDuration, but can not reuse these because this here is not shared. +// wfGetMainCache(), +// 60 * 60 +// ); +// } +// +// /** +// * @return AffectedPagesFinder +// */ +// private function getAffectedPagesFinder() { +// return new AffectedPagesFinder( +// this.getStore().getUsageLookup(), +// new TitleFactory(), +// this.settings.getSetting('siteGlobalID'), +// this.getContentLanguage().getCode() +// ); +// } +// +// /** +// * @return ChangeHandler +// */ +// public function getChangeHandler() { +// pageUpdater = new WikiPageUpdater( +// JobQueueGroup::singleton(), +// this.getRecentChangeFactory(), +// MediaWikiServices::getInstance().getDBLoadBalancerFactory(), +// this.getStore().getRecentChangesDuplicateDetector(), +// MediaWikiServices::getInstance().getStatsdDataFactory() +// ); +// +// pageUpdater.setPurgeCacheBatchSize(this.settings.getSetting('purgeCacheBatchSize')); +// pageUpdater.setRecentChangesBatchSize(this.settings.getSetting('recentChangesBatchSize')); +// +// changeListTransformer = new ChangeRunCoalescer( +// this.getStore().getEntityRevisionLookup(), +// this.getEntityChangeFactory(), +// this.settings.getSetting('siteGlobalID') +// ); +// +// return new ChangeHandler( +// this.getAffectedPagesFinder(), +// new TitleFactory(), +// pageUpdater, +// changeListTransformer, +// this.siteLookup, +// this.settings.getSetting('injectRecentChanges') +// ); +// } +// +// /** +// * @return RecentChangeFactory +// */ +// public function getRecentChangeFactory() { +// repoSite = this.siteLookup.getSite( +// this.getRepositoryDefinitions().getDatabaseNames()[''] +// ); +// interwikiPrefixes = (repoSite !== null) ? repoSite.getInterwikiIds() : []; +// interwikiPrefix = (interwikiPrefixes !== []) ? interwikiPrefixes[0] : null; +// +// return new RecentChangeFactory( +// this.getContentLanguage(), +// new SiteLinkCommentCreator( +// this.getContentLanguage(), +// this.siteLookup, +// this.settings.getSetting('siteGlobalID') +// ), +// (new CentralIdLookupFactory()).getCentralIdLookup(), +// (interwikiPrefix !== null) ? +// new ExternalUserNames(interwikiPrefix, false) : null +// ); +// } +// +// public function getWikibaseContentLanguages() { +// if (this.wikibaseContentLanguages == null) { +// this.wikibaseContentLanguages = WikibaseContentLanguages::getDefaultInstance(); +// } +// +// return this.wikibaseContentLanguages; +// } +// +// /** +// * Get a ContentLanguages Object holding the languages available for labels, descriptions and aliases. +// * +// * @return ContentLanguages +// */ +// public function getTermsLanguages() { +// return this.getWikibaseContentLanguages().getContentLanguages('term'); +// } +// +// /** +// * @return RestrictedEntityLookup +// */ +// public function getRestrictedEntityLookup() { +// if (this.restrictedEntityLookup == null) { +// disabledEntityTypesEntityLookup = new DisabledEntityTypesEntityLookup( +// this.getEntityLookup(), +// this.settings.getSetting('disabledAccessEntityTypes') +// ); +// this.restrictedEntityLookup = new RestrictedEntityLookup( +// disabledEntityTypesEntityLookup, +// this.settings.getSetting('entityAccessLimit') +// ); +// } +// +// return this.restrictedEntityLookup; +// } + + /** + * @return PropertyOrderProvider + */ + public XomwPropertyOrderProvider getPropertyOrderProvider() { + if (this.propertyOrderProvider == null) { +// title = Title::newFromText('MediaWiki:Wikibase-SortedProperties'); +// innerProvider = new WikiPagePropertyOrderProvider(title); +// +// url = this.settings.getSetting('propertyOrderUrl'); +// if (url !== null) { +// innerProvider = new FallbackPropertyOrderProvider( +// innerProvider, +// new HttpUrlPropertyOrderProvider(url, new Http()) +// ); +// } +// +// this.propertyOrderProvider = new CachingPropertyOrderProvider( +// innerProvider, +// wfGetMainCache() +// ); + } + + return this.propertyOrderProvider; + } +// +// /** +// * @return EntityNamespaceLookup +// */ +// public function getEntityNamespaceLookup() { +// return this.getWikibaseServices().getEntityNamespaceLookup(); +// } +// +// /** +// * @param Language language +// * +// * @return LanguageFallbackChain +// */ +// public function getDataAccessLanguageFallbackChain(Language language) { +// return this.getLanguageFallbackChainFactory().newFromLanguage( +// language, +// LanguageFallbackChainFactory::FALLBACK_ALL +// ); +// } +// +// /** +// * @return RepositoryDefinitions +// */ +// public function getRepositoryDefinitions() { +// return this.repositoryDefinitions; +// } +// +// /** +// * @return CacheInterface +// */ +// private function getFormatterCache() { +// global wgSecretKey; +// +// cacheType = this.settings.getSetting('sharedCacheType'); +// cacheSecret = hash('sha256', wgSecretKey); +// +// return new SimpleCacheWithBagOStuff( +// wfGetCache(cacheType), +// 'wikibase.client.formatter.', +// cacheSecret +// ); +// } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/dataAccess/scribunto/WikibaseLanguageIndependentLuaBindings.java b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/dataAccess/scribunto/WikibaseLanguageIndependentLuaBindings.java index e7b5b3cca..17057d60f 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/dataAccess/scribunto/WikibaseLanguageIndependentLuaBindings.java +++ b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/dataAccess/scribunto/WikibaseLanguageIndependentLuaBindings.java @@ -17,10 +17,10 @@ package gplx.xowa.mediawiki.extensions.Wikibase.client.includes.dataAccess.scrib import gplx.xowa.xtns.wbases.*; import gplx.xowa.xtns.wbases.claims.*; import gplx.xowa.xtns.wbases.claims.itms.*; import gplx.xowa.xtns.wbases.claims.enums.*; import gplx.xowa.xtns.wbases.stores.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.Store.*; import gplx.xowa.mediawiki.extensions.Wikibase.client.config.*; public class WikibaseLanguageIndependentLuaBindings { - private final EntityRetrievingTermLookup termLookup; + private final XomwEntityRetrievingTermLookup termLookup; private final WikibaseClientDefault settings; public WikibaseLanguageIndependentLuaBindings(Wbase_doc_mgr entity_mgr, byte[] wiki_abrv_wm) { - this.termLookup = new EntityRetrievingTermLookup(entity_mgr); + this.termLookup = new XomwEntityRetrievingTermLookup(entity_mgr); this.settings = WikibaseClientDefault.New(wiki_abrv_wm); } public byte[] getLabelByLanguage_or_null(byte[] prefixedEntityId, byte[] languageCode) { diff --git a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/EntityRetrievingTermLookup.java b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwEntityRetrievingTermLookup.java similarity index 91% rename from 400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/EntityRetrievingTermLookup.java rename to 400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwEntityRetrievingTermLookup.java index b3b6df85b..eaed7010a 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/EntityRetrievingTermLookup.java +++ b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwEntityRetrievingTermLookup.java @@ -16,9 +16,9 @@ Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt package gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.Store; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.extensions.*; import gplx.xowa.mediawiki.extensions.Wikibase.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.*; import gplx.xowa.xtns.wbases.core.*; import gplx.xowa.xtns.wbases.claims.*; import gplx.xowa.xtns.wbases.claims.enums.*; import gplx.xowa.xtns.wbases.claims.itms.*; import gplx.xowa.xtns.wbases.stores.*; import gplx.xowa.xtns.wbases.*; -public class EntityRetrievingTermLookup { +public class XomwEntityRetrievingTermLookup { private final Wbase_doc_mgr entity_mgr; - public EntityRetrievingTermLookup(Wbase_doc_mgr entity_mgr) { + public XomwEntityRetrievingTermLookup(Wbase_doc_mgr entity_mgr) { this.entity_mgr = entity_mgr; } diff --git a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwPropertyOrderProvider.java b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwPropertyOrderProvider.java new file mode 100644 index 000000000..8961ea645 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwPropertyOrderProvider.java @@ -0,0 +1,36 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.Store; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.extensions.*; import gplx.xowa.mediawiki.extensions.Wikibase.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.*; +// REF.WBASE:2020-01-19 +/** +* Interface that contains method for the PropertyOrderProvider +* +* @license GPL-2.0-or-later +* @author Lucie-Aim�e Kaffee +*/ +public interface XomwPropertyOrderProvider { + + /** + * Get order of properties in the form [ $propertyIdSerialization => $ordinalNumber ] + * + * @return null|int[] An associative array mapping property ID strings to ordinal numbers. + * The order of properties is represented by the ordinal numbers associated with them. + * The array is not guaranteed to be sorted. + * Null if no information exists. + * @throws PropertyOrderProviderException + */ + XophpArray getPropertyOrder(); +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwPropertyOrderProviderException.java b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwPropertyOrderProviderException.java new file mode 100644 index 000000000..e2512b338 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwPropertyOrderProviderException.java @@ -0,0 +1,20 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.Store; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.extensions.*; import gplx.xowa.mediawiki.extensions.Wikibase.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.*; +// REF.WBASE:2020-01-19 +class XomwPropertyOrderProviderException extends XophpRuntimeException { public XomwPropertyOrderProviderException(String msg) {super(msg); + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiPagePropertyOrderProvider.java b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiPagePropertyOrderProvider.java new file mode 100644 index 000000000..75179b4a7 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiPagePropertyOrderProvider.java @@ -0,0 +1,66 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.Store; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.extensions.*; import gplx.xowa.mediawiki.extensions.Wikibase.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.*; +import gplx.xowa.mediawiki.includes.*; +import gplx.xowa.mediawiki.includes.page.*; +// REF.WBASE:2020-01-19 +/** +* Provides a list of ordered Property numbers +* +* @license GPL-2.0-or-later +* @author Lucie-Aim�e Kaffee +*/ +class XomwWikiPagePropertyOrderProvider extends XomwWikiTextPropertyOrderProvider implements XomwPropertyOrderProvider { + /** + * @var Title + */ + private XomwTitle pageTitle; + + /** + * @param Title pageTitle page name the ordered property list is on + */ + public XomwWikiPagePropertyOrderProvider(XomwTitle pageTitle) { + this.pageTitle = pageTitle; + } + + /** + * Get Content of MediaWiki:Wikibase-SortedProperties + * + * @return String|null + * @throws PropertyOrderProviderException + */ + @Override protected String getPropertyOrderWikitext() { + if (!XophpObject_.is_true(this.pageTitle)) { + throw new XomwPropertyOrderProviderException("Not able to get a title"); + } + +// XomwWikiPage wikiPage = XomwWikiPage.factory(this.pageTitle); +// +// $pageContent = $wikiPage->getContent(); +// +// if ($pageContent === null) { +// return null; +// } +// +// if (!($pageContent instanceof TextContent)) { +// throw new PropertyOrderProviderException("The page content of " + this.pageTitle->getText() + " is not TextContent"); +// } +// +// return strval($pageContent->getNativeData()); + return null; + } + +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiTextPropertyOrderProvider.java b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiTextPropertyOrderProvider.java new file mode 100644 index 000000000..cc2bb6978 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiTextPropertyOrderProvider.java @@ -0,0 +1,80 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.Store; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.extensions.*; import gplx.xowa.mediawiki.extensions.Wikibase.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.*; +import gplx.langs.regxs.*; +// REF.WBASE:2020-01-19 +/** +* Base cl+ass for PropertyOrderProviders, that parse the property order from a +* wikitext page. +* +* @license GPL-2.0-or-later +* @author Lucie-Aim�e Kaffee +* @author Marius Hoch +*/ +abstract class XomwWikiTextPropertyOrderProvider implements XomwPropertyOrderProvider { + + /** + * @see parent::getPropertyOrder() + * @return null|int[] null if page doesn't exist + * @throws PropertyOrderProviderException + */ + public XophpArray getPropertyOrder() { + String pageContent = this.getPropertyOrderWikitext(); + if (pageContent == null) { + return null; + } + XophpArray parsedList = this.parseList(pageContent); + + return XophpArray_.array_flip(parsedList); + } + + /** + * Get the wikitext of the property order list. + * + * @return String|null + * @throws PropertyOrderProviderException + */ + abstract protected String getPropertyOrderWikitext(); + + /** + * @param String pageContent + * + * @return String[] + */ + private XophpArray parseList(String pageContent) { + pageContent = XophpRegex_.preg_replace(parseList_replace_regx, String_.Empty, pageContent); + + XophpArray orderedPropertiesMatches = XophpArray.New(); + XophpRegex_.preg_match_all( + parseList_match_regx, + pageContent, + orderedPropertiesMatches, + XophpRegex_.PREG_PATTERN_ORDER + ); + + XophpArray orderedProperties = XophpArray_.array_map(XophpString_.Callback_owner, "strtoupper", (XophpArray)orderedPropertiesMatches.Get_at_ary(1)); + + return orderedProperties; + } + + private static final Regx_adp + parseList_replace_regx = XophpRegex_.Pattern + ( "", XophpRegex_.MODIFIER_s) + , parseList_match_regx = XophpRegex_.Pattern + //'@^\*\h*(?:\[\[(?:d:)?Property:)?(P\d+\b)@im' + ( "^\\*\\h*(?:\\[\\[(?:d:)?Property:)?(P\\d+\\b)", XophpRegex_.MODIFIER_i | XophpRegex_.MODIFIER_m) + ; +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiTextPropertyOrderProvider_tst.java b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiTextPropertyOrderProvider_tst.java new file mode 100644 index 000000000..08f521252 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/lib/includes/Store/XomwWikiTextPropertyOrderProvider_tst.java @@ -0,0 +1,63 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.Store; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.extensions.*; import gplx.xowa.mediawiki.extensions.Wikibase.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.*; import gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.*; +import org.junit.*; import gplx.core.tests.*; +import gplx.xowa.mediawiki.includes.xohtml.*; +public class XomwWikiTextPropertyOrderProvider_tst { + private final XomwWikiTextPropertyOrderProvider_fxt fxt = new XomwWikiTextPropertyOrderProvider_fxt(); + @Test public void Basic() { + fxt.Test__getPropertyOrder(String_.Concat_lines_nl + ( "* [[Property:P1]]" + , "* [[Property:P2]]" + ), XophpArray.New() + .Add("P1", "0") + .Add("P2", "1") + ); + } + @Test public void Comments() { + fxt.Test__getPropertyOrder(String_.Concat_lines_nl + ( "" + , "* [[Property:P1]]" + , "* [[Property:P2]]" + ), XophpArray.New() + .Add("P1", "0") + .Add("P2", "1") + ); + } + @Test public void Invalid_properties() { + fxt.Test__getPropertyOrder(String_.Concat_lines_nl + ( "* [[Property:P0a]]" + , "* [[Property:P1]]" + , "* [[Property:P2]]" + ), XophpArray.New() + .Add("P1", "0") + .Add("P2", "1") + ); + } +} +class XomwWikiTextPropertyOrderProvider_fxt { + public void Test__getPropertyOrder(String page, XophpArray expd) { + MockXomwWikiTextPropertyOrderProvider provider = new MockXomwWikiTextPropertyOrderProvider(page); + XophpArray actl = provider.getPropertyOrder(); + Gftest.Eq__str(expd.To_str(), actl.To_str()); + } +} +class MockXomwWikiTextPropertyOrderProvider extends XomwWikiTextPropertyOrderProvider { private final String text; + public MockXomwWikiTextPropertyOrderProvider(String text) {this.text = text;} + @Override protected String getPropertyOrderWikitext() { + return text; + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwHooks.java b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwHooks.java new file mode 100644 index 000000000..5dd42a193 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwHooks.java @@ -0,0 +1,190 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; +/** +* Hooks cl+ass. +* +* Used to supersede $wgHooks, because globals are EVIL. +* +* @since 1.18 +*/ +public class XomwHooks { +// /** +// * Array of events mapped to an array of callbacks to be run +// * when that event is triggered. +// */ +// protected static $handlers = []; +// +// /** +// * Attach an event handler to a given hook. +// * +// * @param String $name Name of hook +// * @param callable $callback Callback function to attach +// * +// * @since 1.18 +// */ +// public static function register( $name, $callback ) { +// if ( !isset( self::$handlers[$name] ) ) { +// self::$handlers[$name] = []; +// } +// +// self::$handlers[$name][] = $callback; +// } +// +// /** +// * Clears hooks registered via Hooks::register(). Does not touch $wgHooks. +// * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined. +// * +// * @param String $name The name of the hook to clear. +// * +// * @since 1.21 +// * @throws MWException If not in testing mode. +// */ +// public static function clear( $name ) { +// if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) { +// throw new MWException( 'Cannot reset hooks in operation.' ); +// } +// +// unset( self::$handlers[$name] ); +// } +// +// /** +// * Returns true if a hook has a function registered to it. +// * The function may have been registered either via Hooks::register or in $wgHooks. +// * +// * @since 1.18 +// * +// * @param String $name Name of hook +// * @return boolean True if the hook has a function registered to it +// */ +// public static function isRegistered( $name ) { +// global $wgHooks; +// return !empty( $wgHooks[$name] ) || !empty( self::$handlers[$name] ); +// } +// +// /** +// * Returns an array of all the event functions attached to a hook +// * This combines functions registered via Hooks::register and with $wgHooks. +// * +// * @since 1.18 +// * +// * @param String $name Name of the hook +// * @return array +// */ +// public static function getHandlers( $name ) { +// global $wgHooks; +// +// if ( !self::isRegistered( $name ) ) { +// return []; +// } elseif ( !isset( self::$handlers[$name] ) ) { +// return $wgHooks[$name]; +// } elseif ( !isset( $wgHooks[$name] ) ) { +// return self::$handlers[$name]; +// } else { +// return array_merge( self::$handlers[$name], $wgHooks[$name] ); +// } +// } +// +// /** +// * Call hook functions defined in Hooks::register and $wgHooks. +// * +// * For a certain hook event, fetch the array of hook events and +// * process them. Determine the proper callback for each hook and +// * then call the actual hook using the appropriate arguments. +// * Finally, process the return value and return/throw accordingly. +// * +// * @param String $event Event name +// * @param array $args Array of parameters passed to hook functions +// * @param String|null $deprecatedVersion Optionally, mark hook as deprecated with version number +// * @return boolean True if no handler aborted the hook +// * +// * @throws Exception +// * @throws FatalError +// * @throws MWException +// * @since 1.22 A hook function is not required to return a value for +// * processing to continue. Not returning a value (or explicitly +// * returning null) is equivalent to returning true. +// */ +// public static function run( $event, array $args = [], $deprecatedVersion = null ) { +// foreach ( self::getHandlers( $event ) as $hook ) { +// // Turn non-array values into an array. (Can't use casting because of objects.) +// if ( !is_array( $hook ) ) { +// $hook = [ $hook ]; +// } +// +// if ( !array_filter( $hook ) ) { +// // Either array is empty or it's an array filled with null/false/empty. +// continue; +// } elseif ( is_array( $hook[0] ) ) { +// // First element is an array, meaning the developer intended +// // the first element to be a callback. Merge it in so that +// // processing can be uniform. +// $hook = array_merge( $hook[0], array_slice( $hook, 1 ) ); +// } +// +// /** +// * $hook can be: a function, an Object, an array of $function and +// * $data, an array of just a function, an array of Object and +// * method, or an array of Object, method, and data. +// */ +// if ( $hook[0] instanceof Closure ) { +// $func = "hook-$event-closure"; +// $callback = array_shift( $hook ); +// } elseif ( is_object( $hook[0] ) ) { +// $Object = array_shift( $hook ); +// $method = array_shift( $hook ); +// +// // If no method was specified, default to on$event. +// if ( $method === null ) { +// $method = "on$event"; +// } +// +// $func = get_class( $Object ) . '::' . $method; +// $callback = [ $Object, $method ]; +// } elseif ( is_string( $hook[0] ) ) { +// $func = $callback = array_shift( $hook ); +// } else { +// throw new MWException( 'Unknown datatype in hooks for ' . $event . "\n" ); +// } +// +// // Run autoloader (workaround for call_user_func_array bug) +// // and throw error if not callable. +// if ( !is_callable( $callback ) ) { +// throw new MWException( 'Invalid callback ' . $func . ' in hooks for ' . $event . "\n" ); +// } +// +// // mark hook as deprecated, if deprecation version is specified +// if ( $deprecatedVersion !== null ) { +// wfDeprecated( "$event hook (used in $func)", $deprecatedVersion ); +// } +// +// // Call the hook. +// $hook_args = array_merge( $hook, $args ); +// $retval = call_user_func_array( $callback, $hook_args ); +// +// // Process the return value. +// if ( is_string( $retval ) ) { +// // String returned means error. +// throw new FatalError( $retval ); +// } elseif ( $retval === false ) { +// // False was returned. Stop processing, but no error. +// return false; +// } +// } +// +// return true; +// } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwRevision.java b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwRevision.java new file mode 100644 index 000000000..0b596e45c --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwRevision.java @@ -0,0 +1,1321 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; +import gplx.xowa.mediawiki.includes.dao.*; +// MW.SRC:1.33.1 +/** +* @+deprecated since 1.31, use RevisionRecord, RevisionStore, and BlobStore instead. +*/ +public class XomwRevision implements XomwIDBAccessObject { +// +// /** @var RevisionRecord */ +// protected $mRecord; +// +// // Revision deletion constants +// static final DELETED_TEXT = RevisionRecord::DELETED_TEXT; +// static final DELETED_COMMENT = RevisionRecord::DELETED_COMMENT; +// static final DELETED_USER = RevisionRecord::DELETED_USER; +// static final DELETED_RESTRICTED = RevisionRecord::DELETED_RESTRICTED; +// static final SUPPRESSED_USER = RevisionRecord::SUPPRESSED_USER; +// static final SUPPRESSED_ALL = RevisionRecord::SUPPRESSED_ALL; +// +// // Audience options for accessors +// static final FOR_PUBLIC = RevisionRecord::FOR_PUBLIC; +// static final FOR_THIS_USER = RevisionRecord::FOR_THIS_USER; +// static final RAW = RevisionRecord::RAW; +// +// static final TEXT_CACHE_GROUP = SqlBlobStore::TEXT_CACHE_GROUP; +// +// /** +// * @return RevisionStore +// */ +// protected static function getRevisionStore( $wiki = false ) { +// if ( $wiki ) { +// return MediaWikiServices::getInstance()->getRevisionStoreFactory() +// ->getRevisionStore( $wiki ); +// } else { +// return MediaWikiServices::getInstance()->getRevisionStore(); +// } +// } +// +// /** +// * @return RevisionLookup +// */ +// protected static function getRevisionLookup() { +// return MediaWikiServices::getInstance()->getRevisionLookup(); +// } +// +// /** +// * @return RevisionFactory +// */ +// protected static function getRevisionFactory() { +// return MediaWikiServices::getInstance()->getRevisionFactory(); +// } +// +// /** +// * @param boolean|String $wiki The ID of the target wiki database. Use false for the local wiki. +// * +// * @return SqlBlobStore +// */ +// protected static function getBlobStore( $wiki = false ) { +// $store = MediaWikiServices::getInstance() +// ->getBlobStoreFactory() +// ->newSqlBlobStore( $wiki ); +// +// if ( !$store instanceof SqlBlobStore ) { +// throw new RuntimeException( +// 'The backwards compatibility code in Revision currently requires the BlobStore ' +// . 'service to be an SqlBlobStore instance, but it is a ' . get_class( $store ) +// ); +// } +// +// return $store; +// } +// +// /** +// * Load a page revision from a given revision ID number. +// * Returns null if no such revision can be found. +// * +// * $flags include: +// * Revision::READ_LATEST : Select the data from the master +// * Revision::READ_LOCKING : Select & synchronized the data from the master +// * +// * @param int $id +// * @param int $flags (optional) +// * @return Revision|null +// */ +// public static function newFromId( $id, $flags = 0 ) { +// $rec = self::getRevisionLookup()->getRevisionById( $id, $flags ); +// return $rec ? new Revision( $rec, $flags ) : null; +// } +// +// /** +// * Load either the current, or a specified, revision +// * that's attached to a given link target. If not attached +// * to that link target, will return null. +// * +// * $flags include: +// * Revision::READ_LATEST : Select the data from the master +// * Revision::READ_LOCKING : Select & synchronized the data from the master +// * +// * @param LinkTarget $linkTarget +// * @param int $id (optional) +// * @param int $flags Bitfield (optional) +// * @return Revision|null +// */ +// public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) { +// $rec = self::getRevisionLookup()->getRevisionByTitle( $linkTarget, $id, $flags ); +// return $rec ? new Revision( $rec, $flags ) : null; +// } +// +// /** +// * Load either the current, or a specified, revision +// * that's attached to a given page ID. +// * Returns null if no such revision can be found. +// * +// * $flags include: +// * Revision::READ_LATEST : Select the data from the master (since 1.20) +// * Revision::READ_LOCKING : Select & synchronized the data from the master +// * +// * @param int $pageId +// * @param int $revId (optional) +// * @param int $flags Bitfield (optional) +// * @return Revision|null +// */ +// public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) { +// $rec = self::getRevisionLookup()->getRevisionByPageId( $pageId, $revId, $flags ); +// return $rec ? new Revision( $rec, $flags ) : null; +// } +// +// /** +// * Make a fake revision Object from an archive table row. This is queried +// * for permissions or even inserted (as in Special:Undelete) +// * +// * @param Object $row +// * @param array $overrides +// * +// * @throws MWException +// * @return Revision +// */ +// public static function newFromArchiveRow( $row, $overrides = [] ) { +// /** +// * MCR Migration: https://phabricator.wikimedia.org/T183564 +// * This method used to overwrite attributes, then passed to Revision::__construct +// * RevisionStore::newRevisionFromArchiveRow instead overrides row field names +// * So do a conversion here. +// */ +// if ( array_key_exists( 'page', $overrides ) ) { +// $overrides['page_id'] = $overrides['page']; +// unset( $overrides['page'] ); +// } +// +// /** +// * We require a Title for both the Revision Object and the RevisionRecord. +// * Below is duplicated logic from RevisionStore::newRevisionFromArchiveRow +// * to fetch a title in order pass it into the Revision Object. +// */ +// $title = null; +// if ( isset( $overrides['title'] ) ) { +// if ( !( $overrides['title'] instanceof Title ) ) { +// throw new MWException( 'title field override must contain a Title Object.' ); +// } +// +// $title = $overrides['title']; +// } +// if ( $title !== null ) { +// if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) { +// $title = Title::makeTitle( $row->ar_namespace, $row->ar_title ); +// } else { +// throw new InvalidArgumentException( +// 'A Title or ar_namespace and ar_title must be given' +// ); +// } +// } +// +// $rec = self::getRevisionFactory()->newRevisionFromArchiveRow( $row, 0, $title, $overrides ); +// return new Revision( $rec, self::READ_NORMAL, $title ); +// } +// +// /** +// * @since 1.19 +// * +// * MCR migration note: replaced by RevisionStore::newRevisionFromRow(). Note that +// * newFromRow() also accepts arrays, while newRevisionFromRow() does not. Instead, +// * a MutableRevisionRecord should be constructed directly. +// * RevisionStore::newMutableRevisionFromArray() can be used as a temporary replacement, +// * but should be avoided. +// * +// * @param Object|array $row +// * @return Revision +// */ +// public static function newFromRow( $row ) { +// if ( is_array( $row ) ) { +// $rec = self::getRevisionFactory()->newMutableRevisionFromArray( $row ); +// } else { +// $rec = self::getRevisionFactory()->newRevisionFromRow( $row ); +// } +// +// return new Revision( $rec ); +// } +// +// /** +// * Load a page revision from a given revision ID number. +// * Returns null if no such revision can be found. +// * +// * @deprecated since 1.31, use RevisionStore::getRevisionById() instead. +// * +// * @param IDatabase $db +// * @param int $id +// * @return Revision|null +// */ +// public static function loadFromId( $db, $id ) { +// wfDeprecated( __METHOD__, '1.31' ); // no known callers +// $rec = self::getRevisionStore()->loadRevisionFromId( $db, $id ); +// return $rec ? new Revision( $rec ) : null; +// } +// +// /** +// * Load either the current, or a specified, revision +// * that's attached to a given page. If not attached +// * to that page, will return null. +// * +// * @deprecated since 1.31, use RevisionStore::getRevisionByPageId() instead. +// * +// * @param IDatabase $db +// * @param int $pageid +// * @param int $id +// * @return Revision|null +// */ +// public static function loadFromPageId( $db, $pageid, $id = 0 ) { +// $rec = self::getRevisionStore()->loadRevisionFromPageId( $db, $pageid, $id ); +// return $rec ? new Revision( $rec ) : null; +// } +// +// /** +// * Load either the current, or a specified, revision +// * that's attached to a given page. If not attached +// * to that page, will return null. +// * +// * @deprecated since 1.31, use RevisionStore::getRevisionByTitle() instead. +// * +// * @param IDatabase $db +// * @param Title $title +// * @param int $id +// * @return Revision|null +// */ +// public static function loadFromTitle( $db, $title, $id = 0 ) { +// $rec = self::getRevisionStore()->loadRevisionFromTitle( $db, $title, $id ); +// return $rec ? new Revision( $rec ) : null; +// } +// +// /** +// * Load the revision for the given title with the given timestamp. +// * WARNING: Timestamps may in some circumstances not be unique, +// * so this isn't the best key to use. +// * +// * @deprecated since 1.31, use RevisionStore::getRevisionByTimestamp() +// * or RevisionStore::loadRevisionFromTimestamp() instead. +// * +// * @param IDatabase $db +// * @param Title $title +// * @param String $timestamp +// * @return Revision|null +// */ +// public static function loadFromTimestamp( $db, $title, $timestamp ) { +// $rec = self::getRevisionStore()->loadRevisionFromTimestamp( $db, $title, $timestamp ); +// return $rec ? new Revision( $rec ) : null; +// } +// +// /** +// * Return the value of a select() JOIN conds array for the user table. +// * This will get user table rows for logged-in users. +// * @since 1.19 +// * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead. +// * @return array +// */ +// public static function userJoinCond() { +// global $wgActorTableSchemaMigrationStage; +// +// wfDeprecated( __METHOD__, '1.31' ); +// if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { +// // If code is using this instead of self::getQueryInfo(), there's +// // no way the join it's trying to do can work once the old fields +// // aren't being used anymore. +// throw new BadMethodCallException( +// 'Cannot use ' . __METHOD__ +// . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' +// ); +// } +// +// return [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ]; +// } +// +// /** +// * Return the value of a select() page conds array for the page table. +// * This will assure that the revision(s) are not orphaned from live pages. +// * @since 1.19 +// * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead. +// * @return array +// */ +// public static function pageJoinCond() { +// wfDeprecated( __METHOD__, '1.31' ); +// return [ 'JOIN', [ 'page_id = rev_page' ] ]; +// } +// +// /** +// * Return the list of revision fields that should be selected to create +// * a new revision. +// * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead. +// * @return array +// */ +// public static function selectFields() { +// global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage; +// global $wgMultiContentRevisionSchemaMigrationStage; +// +// if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { +// // If code is using this instead of self::getQueryInfo(), there's a +// // decent chance it's going to try to directly access +// // $row->rev_user or $row->rev_user_text and we can't give it +// // useful values here once those aren't being used anymore. +// throw new BadMethodCallException( +// 'Cannot use ' . __METHOD__ +// . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' +// ); +// } +// +// if ( !( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) { +// // If code is using this instead of self::getQueryInfo(), there's a +// // decent chance it's going to try to directly access +// // $row->rev_text_id or $row->rev_content_model and we can't give it +// // useful values here once those aren't being written anymore, +// // and may not exist at all. +// throw new BadMethodCallException( +// 'Cannot use ' . __METHOD__ . ' when $wgMultiContentRevisionSchemaMigrationStage ' +// . 'does not have SCHEMA_COMPAT_WRITE_OLD set.' +// ); +// } +// +// wfDeprecated( __METHOD__, '1.31' ); +// +// $fields = [ +// 'rev_id', +// 'rev_page', +// 'rev_text_id', +// 'rev_timestamp', +// 'rev_user_text', +// 'rev_user', +// 'rev_actor' => 'NULL', +// 'rev_minor_edit', +// 'rev_deleted', +// 'rev_len', +// 'rev_parent_id', +// 'rev_sha1', +// ]; +// +// $fields += CommentStore::getStore()->getFields( 'rev_comment' ); +// +// if ( $wgContentHandlerUseDB ) { +// $fields[] = 'rev_content_format'; +// $fields[] = 'rev_content_model'; +// } +// +// return $fields; +// } +// +// /** +// * Return the list of revision fields that should be selected to create +// * a new revision from an archive row. +// * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead. +// * @return array +// */ +// public static function selectArchiveFields() { +// global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage; +// global $wgMultiContentRevisionSchemaMigrationStage; +// +// if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { +// // If code is using this instead of self::getQueryInfo(), there's a +// // decent chance it's going to try to directly access +// // $row->ar_user or $row->ar_user_text and we can't give it +// // useful values here once those aren't being used anymore. +// throw new BadMethodCallException( +// 'Cannot use ' . __METHOD__ +// . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' +// ); +// } +// +// if ( !( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) { +// // If code is using this instead of self::getQueryInfo(), there's a +// // decent chance it's going to try to directly access +// // $row->ar_text_id or $row->ar_content_model and we can't give it +// // useful values here once those aren't being written anymore, +// // and may not exist at all. +// throw new BadMethodCallException( +// 'Cannot use ' . __METHOD__ . ' when $wgMultiContentRevisionSchemaMigrationStage ' +// . 'does not have SCHEMA_COMPAT_WRITE_OLD set.' +// ); +// } +// +// wfDeprecated( __METHOD__, '1.31' ); +// +// $fields = [ +// 'ar_id', +// 'ar_page_id', +// 'ar_rev_id', +// 'ar_text_id', +// 'ar_timestamp', +// 'ar_user_text', +// 'ar_user', +// 'ar_actor' => 'NULL', +// 'ar_minor_edit', +// 'ar_deleted', +// 'ar_len', +// 'ar_parent_id', +// 'ar_sha1', +// ]; +// +// $fields += CommentStore::getStore()->getFields( 'ar_comment' ); +// +// if ( $wgContentHandlerUseDB ) { +// $fields[] = 'ar_content_format'; +// $fields[] = 'ar_content_model'; +// } +// return $fields; +// } +// +// /** +// * Return the list of text fields that should be selected to read the +// * revision text +// * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'text' ] ) instead. +// * @return array +// */ +// public static function selectTextFields() { +// wfDeprecated( __METHOD__, '1.31' ); +// return [ +// 'old_text', +// 'old_flags' +// ]; +// } +// +// /** +// * Return the list of page fields that should be selected from page table +// * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead. +// * @return array +// */ +// public static function selectPageFields() { +// wfDeprecated( __METHOD__, '1.31' ); +// return [ +// 'page_namespace', +// 'page_title', +// 'page_id', +// 'page_latest', +// 'page_is_redirect', +// 'page_len', +// ]; +// } +// +// /** +// * Return the list of user fields that should be selected from user table +// * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead. +// * @return array +// */ +// public static function selectUserFields() { +// wfDeprecated( __METHOD__, '1.31' ); +// return [ 'user_name' ]; +// } +// +// /** +// * Return the tables, fields, and join conditions to be selected to create +// * a new revision Object. +// * @since 1.31 +// * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead. +// * @param array $options Any combination of the following strings +// * - 'page': Join with the page table, and select fields to identify the page +// * - 'user': Join with the user table, and select the user name +// * - 'text': Join with the text table, and select fields to load page text +// * @return array With three keys: +// * - tables: (String[]) to include in the `$table` to `IDatabase->select()` +// * - fields: (String[]) to include in the `$vars` to `IDatabase->select()` +// * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` +// */ +// public static function getQueryInfo( $options = [] ) { +// return self::getRevisionStore()->getQueryInfo( $options ); +// } +// +// /** +// * Return the tables, fields, and join conditions to be selected to create +// * a new archived revision Object. +// * @since 1.31 +// * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead. +// * @return array With three keys: +// * - tables: (String[]) to include in the `$table` to `IDatabase->select()` +// * - fields: (String[]) to include in the `$vars` to `IDatabase->select()` +// * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` +// */ +// public static function getArchiveQueryInfo() { +// return self::getRevisionStore()->getArchiveQueryInfo(); +// } +// +// /** +// * Do a batched query to get the parent revision lengths +// * +// * @deprecated in 1.31, use RevisionStore::getRevisionSizes instead. +// * +// * @param IDatabase $db +// * @param array $revIds +// * @return array +// */ +// public static function getParentLengths( $db, array $revIds ) { +// return self::getRevisionStore()->listRevisionSizes( $db, $revIds ); +// } +// +// /** +// * @param Object|array|RevisionRecord $row Either a database row or an array +// * @param int $queryFlags +// * @param Title|null $title +// * +// * @private +// */ +// function __construct( $row, $queryFlags = 0, Title $title = null ) { +// global $wgUser; +// +// if ( $row instanceof RevisionRecord ) { +// $this->mRecord = $row; +// } elseif ( is_array( $row ) ) { +// // If no user is specified, fall back to using the global user Object, to stay +// // compatible with pre-1.31 behavior. +// if ( !isset( $row['user'] ) && !isset( $row['user_text'] ) ) { +// $row['user'] = $wgUser; +// } +// +// $this->mRecord = self::getRevisionFactory()->newMutableRevisionFromArray( +// $row, +// $queryFlags, +// $this->ensureTitle( $row, $queryFlags, $title ) +// ); +// } elseif ( is_object( $row ) ) { +// $this->mRecord = self::getRevisionFactory()->newRevisionFromRow( +// $row, +// $queryFlags, +// $this->ensureTitle( $row, $queryFlags, $title ) +// ); +// } else { +// throw new InvalidArgumentException( +// '$row must be a row Object, an associative array, or a RevisionRecord' +// ); +// } +// +// Assert::postcondition( $this->mRecord !== null, 'Failed to construct a RevisionRecord' ); +// } +// +// /** +// * Make sure we have *some* Title Object for use by the constructor. +// * For B/C, the constructor shouldn't fail even for a bad page ID or bad revision ID. +// * +// * @param array|Object $row +// * @param int $queryFlags +// * @param Title|null $title +// * +// * @return Title $title if not null, or a Title constructed from information in $row. +// */ +// private function ensureTitle( $row, $queryFlags, $title = null ) { +// if ( $title ) { +// return $title; +// } +// +// if ( is_array( $row ) ) { +// if ( isset( $row['title'] ) ) { +// if ( !( $row['title'] instanceof Title ) ) { +// throw new MWException( 'title field must contain a Title Object.' ); +// } +// +// return $row['title']; +// } +// +// $pageId = $row['page'] ?? 0; +// $revId = $row['id'] ?? 0; +// } else { +// $pageId = $row->rev_page ?? 0; +// $revId = $row->rev_id ?? 0; +// } +// +// try { +// $title = self::getRevisionStore()->getTitle( $pageId, $revId, $queryFlags ); +// } catch ( RevisionAccessException $ex ) { +// // construct a dummy title! +// wfLogWarning( __METHOD__ . ': ' . $ex->getMessage() ); +// +// // NOTE: this Title will only be used inside RevisionRecord +// $title = Title::makeTitleSafe( NS_SPECIAL, "Badtitle/ID=$pageId" ); +// $title->resetArticleID( $pageId ); +// } +// +// return $title; +// } +// +// /** +// * @return RevisionRecord +// */ +// public function getRevisionRecord() { +// return $this->mRecord; +// } +// +// /** +// * Get revision ID +// * +// * @return int|null +// */ +// public function getId() { +// return $this->mRecord->getId(); +// } +// +// /** +// * Set the revision ID +// * +// * This should only be used for proposed revisions that turn out to be null edits. +// * +// * @note Only supported on Revisions that were constructed based on associative arrays, +// * since they are mutable. +// * +// * @since 1.19 +// * @param int|String $id +// * @throws MWException +// */ +// public function setId( $id ) { +// if ( $this->mRecord instanceof MutableRevisionRecord ) { +// $this->mRecord->setId( intval( $id ) ); +// } else { +// throw new MWException( __METHOD__ . ' is not supported on this instance' ); +// } +// } +// +// /** +// * Set the user ID/name +// * +// * This should only be used for proposed revisions that turn out to be null edits +// * +// * @note Only supported on Revisions that were constructed based on associative arrays, +// * since they are mutable. +// * +// * @since 1.28 +// * @deprecated since 1.31, please reuse old Revision Object +// * @param int $id User ID +// * @param String $name User name +// * @throws MWException +// */ +// public function setUserIdAndName( $id, $name ) { +// if ( $this->mRecord instanceof MutableRevisionRecord ) { +// $user = User::newFromAnyId( intval( $id ), $name, null ); +// $this->mRecord->setUser( $user ); +// } else { +// throw new MWException( __METHOD__ . ' is not supported on this instance' ); +// } +// } +// +// /** +// * @return SlotRecord +// */ +// private function getMainSlotRaw() { +// return $this->mRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ); +// } +// +// /** +// * Get the ID of the row of the text table that contains the content of the +// * revision's main slot, if that content is stored in the text table. +// * +// * If the content is stored elsewhere, this returns null. +// * +// * @deprecated since 1.31, use RevisionRecord()->getSlot()->getContentAddress() to +// * get that actual address that can be used with BlobStore::getBlob(); or use +// * RevisionRecord::hasSameContent() to check if two revisions have the same content. +// * +// * @return int|null +// */ +// public function getTextId() { +// $slot = $this->getMainSlotRaw(); +// return $slot->hasAddress() +// ? self::getBlobStore()->getTextIdFromAddress( $slot->getAddress() ) +// : null; +// } +// +// /** +// * Get parent revision ID (the original previous page revision) +// * +// * @return int|null The ID of the parent revision. 0 indicates that there is no +// * parent revision. Null indicates that the parent revision is not known. +// */ +// public function getParentId() { +// return $this->mRecord->getParentId(); +// } +// +// /** +// * Returns the length of the text in this revision, or null if unknown. +// * +// * @return int|null +// */ +// public function getSize() { +// try { +// return $this->mRecord->getSize(); +// } catch ( RevisionAccessException $ex ) { +// return null; +// } +// } +// +// /** +// * Returns the base36 sha1 of the content in this revision, or null if unknown. +// * +// * @return String|null +// */ +// public function getSha1() { +// try { +// return $this->mRecord->getSha1(); +// } catch ( RevisionAccessException $ex ) { +// return null; +// } +// } +// +// /** +// * Returns the title of the page associated with this entry. +// * Since 1.31, this will never return null. +// * +// * Will do a query, when title is not set and id is given. +// * +// * @return Title +// */ +// public function getTitle() { +// $linkTarget = $this->mRecord->getPageAsLinkTarget(); +// return Title::newFromLinkTarget( $linkTarget ); +// } +// +// /** +// * Set the title of the revision +// * +// * @deprecated since 1.31, this is now a noop. Pass the Title to the constructor instead. +// * +// * @param Title $title +// */ +// public function setTitle( $title ) { +// if ( !$title->equals( $this->getTitle() ) ) { +// throw new InvalidArgumentException( +// $title->getPrefixedText() +// . ' is not the same as ' +// . $this->mRecord->getPageAsLinkTarget()->__toString() +// ); +// } +// } +// +// /** +// * Get the page ID +// * +// * @return int|null +// */ +// public function getPage() { +// return $this->mRecord->getPageId(); +// } +// +// /** +// * Fetch revision's user id if it's available to the specified audience. +// * If the specified audience does not have access to it, zero will be +// * returned. +// * +// * @param int $audience One of: +// * Revision::FOR_PUBLIC to be displayed to all users +// * Revision::FOR_THIS_USER to be displayed to the given user +// * Revision::RAW get the ID regardless of permissions +// * @param User|null $user User Object to check for, only if FOR_THIS_USER is passed +// * to the $audience parameter +// * @return int +// */ +// public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) { +// global $wgUser; +// +// if ( $audience === self::FOR_THIS_USER && !$user ) { +// $user = $wgUser; +// } +// +// $user = $this->mRecord->getUser( $audience, $user ); +// return $user ? $user->getId() : 0; +// } +// +// /** +// * Fetch revision's username if it's available to the specified audience. +// * If the specified audience does not have access to the username, an +// * empty String will be returned. +// * +// * @param int $audience One of: +// * Revision::FOR_PUBLIC to be displayed to all users +// * Revision::FOR_THIS_USER to be displayed to the given user +// * Revision::RAW get the text regardless of permissions +// * @param User|null $user User Object to check for, only if FOR_THIS_USER is passed +// * to the $audience parameter +// * @return String +// */ +// public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) { +// global $wgUser; +// +// if ( $audience === self::FOR_THIS_USER && !$user ) { +// $user = $wgUser; +// } +// +// $user = $this->mRecord->getUser( $audience, $user ); +// return $user ? $user->getName() : ''; +// } +// +// /** +// * @param int $audience One of: +// * Revision::FOR_PUBLIC to be displayed to all users +// * Revision::FOR_THIS_USER to be displayed to the given user +// * Revision::RAW get the text regardless of permissions +// * @param User|null $user User Object to check for, only if FOR_THIS_USER is passed +// * to the $audience parameter +// * +// * @return String|null Returns null if the specified audience does not have access to the +// * comment. +// */ +// function getComment( $audience = self::FOR_PUBLIC, User $user = null ) { +// global $wgUser; +// +// if ( $audience === self::FOR_THIS_USER && !$user ) { +// $user = $wgUser; +// } +// +// $comment = $this->mRecord->getComment( $audience, $user ); +// return $comment === null ? null : $comment->text; +// } +// +// /** +// * @return boolean +// */ +// public function isMinor() { +// return $this->mRecord->isMinor(); +// } +// +// /** +// * @return int Rcid of the unpatrolled row, zero if there isn't one +// */ +// public function isUnpatrolled() { +// return self::getRevisionStore()->getRcIdIfUnpatrolled( $this->mRecord ); +// } +// +// /** +// * Get the RC Object belonging to the current revision, if there's one +// * +// * @param int $flags (optional) $flags include: +// * Revision::READ_LATEST : Select the data from the master +// * +// * @since 1.22 +// * @return RecentChange|null +// */ +// public function getRecentChange( $flags = 0 ) { +// return self::getRevisionStore()->getRecentChange( $this->mRecord, $flags ); +// } +// +// /** +// * @param int $field One of DELETED_* bitfield constants +// * +// * @return boolean +// */ +// public function isDeleted( $field ) { +// return $this->mRecord->isDeleted( $field ); +// } +// +// /** +// * Get the deletion bitfield of the revision +// * +// * @return int +// */ +// public function getVisibility() { +// return $this->mRecord->getVisibility(); +// } +// +// /** +// * Fetch revision content if it's available to the specified audience. +// * If the specified audience does not have the ability to view this +// * revision, or the content could not be loaded, null will be returned. +// * +// * @param int $audience One of: +// * Revision::FOR_PUBLIC to be displayed to all users +// * Revision::FOR_THIS_USER to be displayed to $user +// * Revision::RAW get the text regardless of permissions +// * @param User|null $user User Object to check for, only if FOR_THIS_USER is passed +// * to the $audience parameter +// * @since 1.21 +// * @return Content|null +// */ +// public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) { +// global $wgUser; +// +// if ( $audience === self::FOR_THIS_USER && !$user ) { +// $user = $wgUser; +// } +// +// try { +// return $this->mRecord->getContent( SlotRecord::MAIN, $audience, $user ); +// } +// catch ( RevisionAccessException $e ) { +// return null; +// } +// } +// +// /** +// * Get original serialized data (without checking view restrictions) +// * +// * @since 1.21 +// * @deprecated since 1.31, use BlobStore::getBlob instead. +// * +// * @return String +// */ +// public function getSerializedData() { +// $slot = $this->getMainSlotRaw(); +// return $slot->getContent()->serialize(); +// } +// +// /** +// * Returns the content model for the main slot of this revision. +// * +// * If no content model was stored in the database, the default content model for the title is +// * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT +// * is used as a last resort. +// * +// * @todo drop this, with MCR, there no longer is a single model associated with a revision. +// * +// * @return String The content model id associated with this revision, +// * see the CONTENT_MODEL_XXX constants. +// */ +// public function getContentModel() { +// return $this->getMainSlotRaw()->getModel(); +// } +// +// /** +// * Returns the content format for the main slot of this revision. +// * +// * If no content format was stored in the database, the default format for this +// * revision's content model is returned. +// * +// * @todo drop this, the format is irrelevant to the revision! +// * +// * @return String The content format id associated with this revision, +// * see the CONTENT_FORMAT_XXX constants. +// */ +// public function getContentFormat() { +// $format = $this->getMainSlotRaw()->getFormat(); +// +// if ( $format === null ) { +// // if no format was stored along with the blob, fall back to default format +// $format = $this->getContentHandler()->getDefaultFormat(); +// } +// +// return $format; +// } +// +// /** +// * Returns the content handler appropriate for this revision's content model. +// * +// * @throws MWException +// * @return ContentHandler +// */ +// public function getContentHandler() { +// return ContentHandler::getForModelID( $this->getContentModel() ); +// } +// +// /** +// * @return String +// */ +// public function getTimestamp() { +// return $this->mRecord->getTimestamp(); +// } +// +// /** +// * @return boolean +// */ +// public function isCurrent() { +// return ( $this->mRecord instanceof RevisionStoreRecord ) && $this->mRecord->isCurrent(); +// } +// +// /** +// * Get previous revision for this title +// * +// * @return Revision|null +// */ +// public function getPrevious() { +// $title = $this->getTitle(); +// $rec = self::getRevisionLookup()->getPreviousRevision( $this->mRecord, $title ); +// return $rec ? new Revision( $rec, self::READ_NORMAL, $title ) : null; +// } +// +// /** +// * Get next revision for this title +// * +// * @return Revision|null +// */ +// public function getNext() { +// $title = $this->getTitle(); +// $rec = self::getRevisionLookup()->getNextRevision( $this->mRecord, $title ); +// return $rec ? new Revision( $rec, self::READ_NORMAL, $title ) : null; +// } +// +// /** +// * Get revision text associated with an old or archive row +// * +// * If the text field is not included, this uses RevisionStore to load the appropriate slot +// * and return its serialized content. This is the default backwards-compatibility behavior +// * when reading from the MCR aware database schema is enabled. For this to work, either +// * the revision ID or the page ID must be included in the row. +// * +// * When using the old text field, the flags field must also be set. Including the old_id +// * field will activate cache usage as long as the $wiki parameter is not set. +// * +// * @deprecated since 1.32, use RevisionStore::newRevisionFromRow instead. +// * +// * @param stdClass $row The text data. If a falsy value is passed instead, false is returned. +// * @param String $prefix Table prefix (default 'old_') +// * @param String|boolean $wiki The name of the wiki to load the revision text from +// * (same as the wiki $row was loaded from) or false to indicate the local +// * wiki (this is the default). Otherwise, it must be a symbolic wiki database +// * identifier as understood by the LoadBalancer class. +// * @return String|false Text the text requested or false on failure +// */ +// public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) { +// global $wgMultiContentRevisionSchemaMigrationStage; +// +// if ( !$row ) { +// return false; +// } +// +// $textField = $prefix . 'text'; +// $flagsField = $prefix . 'flags'; +// +// if ( isset( $row->$textField ) ) { +// if ( !( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) { +// // The text field was read, but it's no longer being populated! +// // We could gloss over this by using the text when it's there and loading +// // if when it's not, but it seems preferable to complain loudly about a +// // query that is no longer guaranteed to work reliably. +// throw new LogicException( +// 'Cannot use ' . __METHOD__ . ' with the ' . $textField . ' field when' +// . ' $wgMultiContentRevisionSchemaMigrationStage does not include' +// . ' SCHEMA_COMPAT_WRITE_OLD. The field may not be populated for all revisions!' +// ); +// } +// +// $text = $row->$textField; +// } else { +// // Missing text field, we are probably looking at the MCR-enabled DB schema. +// +// if ( !( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) { +// // This method should no longer be used with the new schema. Ideally, we +// // would already trigger a deprecation warning when SCHEMA_COMPAT_READ_NEW is set. +// wfDeprecated( __METHOD__ . ' (MCR without SCHEMA_COMPAT_WRITE_OLD)', '1.32' ); +// } +// +// $store = self::getRevisionStore( $wiki ); +// $rev = $prefix === 'ar_' +// ? $store->newRevisionFromArchiveRow( $row ) +// : $store->newRevisionFromRow( $row ); +// +// $content = $rev->getContent( SlotRecord::MAIN ); +// return $content ? $content->serialize() : false; +// } +// +// if ( isset( $row->$flagsField ) ) { +// $flags = explode( ',', $row->$flagsField ); +// } else { +// $flags = []; +// } +// +// $cacheKey = isset( $row->old_id ) +// ? SqlBlobStore::makeAddressFromTextId( $row->old_id ) +// : null; +// +// $revisionText = self::getBlobStore( $wiki )->expandBlob( $text, $flags, $cacheKey ); +// +// if ( $revisionText === false ) { +// if ( isset( $row->old_id ) ) { +// wfLogWarning( __METHOD__ . ": Bad data in text row {$row->old_id}! " ); +// } else { +// wfLogWarning( __METHOD__ . ": Bad data in text row! " ); +// } +// return false; +// } +// +// return $revisionText; +// } +// +// /** +// * If $wgCompressRevisions is enabled, we will compress data. +// * The input String is modified in place. +// * Return value is the flags field: contains 'gzip' if the +// * data is compressed, and 'utf-8' if we're saving in UTF-8 +// * mode. +// * +// * @param mixed &$text Reference to a text +// * @return String +// */ +// public static function compressRevisionText( &$text ) { +// return self::getBlobStore()->compressData( $text ); +// } +// +// /** +// * Re-converts revision text according to it's flags. +// * +// * @param mixed $text Reference to a text +// * @param array $flags Compression flags +// * @return String|boolean Decompressed text, or false on failure +// */ +// public static function decompressRevisionText( $text, $flags ) { +// if ( $text === false ) { +// // Text failed to be fetched; nothing to do +// return false; +// } +// +// return self::getBlobStore()->decompressData( $text, $flags ); +// } +// +// /** +// * Insert a new revision into the database, returning the new revision ID +// * number on success and dies horribly on failure. +// * +// * @param IDatabase $dbw (master connection) +// * @throws MWException +// * @return int The revision ID +// */ +// public function insertOn( $dbw ) { +// global $wgUser; +// +// // Note that $this->mRecord->getId() will typically return null here, but not always, +// // e.g. not when restoring a revision. +// +// if ( $this->mRecord->getUser( RevisionRecord::RAW ) === null ) { +// if ( $this->mRecord instanceof MutableRevisionRecord ) { +// $this->mRecord->setUser( $wgUser ); +// } else { +// throw new MWException( 'Cannot insert revision with no associated user.' ); +// } +// } +// +// $rec = self::getRevisionStore()->insertRevisionOn( $this->mRecord, $dbw ); +// +// $this->mRecord = $rec; +// Assert::postcondition( $this->mRecord !== null, 'Failed to acquire a RevisionRecord' ); +// +// return $rec->getId(); +// } +// +// /** +// * Get the super 36 SHA-1 value for a String of text +// * @param String $text +// * @return String +// */ +// public static function base36Sha1( $text ) { +// return SlotRecord::base36Sha1( $text ); +// } +// +// /** +// * Create a new null-revision for insertion into a page's +// * history. This will not re-save the text, but simply refer +// * to the text from the previous version. +// * +// * Such revisions can for instance identify page rename +// * operations and other such meta-modifications. +// * +// * @param IDatabase $dbw +// * @param int $pageId ID number of the page to read from +// * @param String $summary Revision's summary +// * @param boolean $minor Whether the revision should be considered as minor +// * @param User|null $user User Object to use or null for $wgUser +// * @return Revision|null Revision or null on error +// */ +// public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) { +// global $wgUser; +// if ( !$user ) { +// $user = $wgUser; +// } +// +// $comment = CommentStoreComment::newUnsavedComment( $summary, null ); +// +// $title = Title::newFromID( $pageId, Title::GAID_FOR_UPDATE ); +// if ( $title === null ) { +// return null; +// } +// +// $rec = self::getRevisionStore()->newNullRevision( $dbw, $title, $comment, $minor, $user ); +// +// return $rec ? new Revision( $rec ) : null; +// } +// +// /** +// * Determine if the current user is allowed to view a particular +// * field of this revision, if it's marked as deleted. +// * +// * @param int $field One of self::DELETED_TEXT, +// * self::DELETED_COMMENT, +// * self::DELETED_USER +// * @param User|null $user User Object to check, or null to use $wgUser +// * @return boolean +// */ +// public function userCan( $field, User $user = null ) { +// return self::userCanBitfield( $this->getVisibility(), $field, $user ); +// } +// +// /** +// * Determine if the current user is allowed to view a particular +// * field of this revision, if it's marked as deleted. This is used +// * by various classes to avoid duplication. +// * +// * @param int $bitfield Current field +// * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE, +// * self::DELETED_COMMENT = File::DELETED_COMMENT, +// * self::DELETED_USER = File::DELETED_USER +// * @param User|null $user User Object to check, or null to use $wgUser +// * @param Title|null $title A Title Object to check for per-page restrictions on, +// * instead of just plain userrights +// * @return boolean +// */ +// public static function userCanBitfield( $bitfield, $field, User $user = null, +// Title $title = null +// ) { +// global $wgUser; +// +// if ( !$user ) { +// $user = $wgUser; +// } +// +// return RevisionRecord::userCanBitfield( $bitfield, $field, $user, $title ); +// } +// +// /** +// * Get rev_timestamp from rev_id, without loading the rest of the row +// * +// * @param Title $title +// * @param int $id +// * @param int $flags +// * @return String|boolean False if not found +// */ +// static function getTimestampFromId( $title, $id, $flags = 0 ) { +// return self::getRevisionStore()->getTimestampFromId( $title, $id, $flags ); +// } +// +// /** +// * Get count of revisions per page...not very efficient +// * +// * @param IDatabase $db +// * @param int $id Page id +// * @return int +// */ +// static function countByPageId( $db, $id ) { +// return self::getRevisionStore()->countRevisionsByPageId( $db, $id ); +// } +// +// /** +// * Get count of revisions per page...not very efficient +// * +// * @param IDatabase $db +// * @param Title $title +// * @return int +// */ +// static function countByTitle( $db, $title ) { +// return self::getRevisionStore()->countRevisionsByTitle( $db, $title ); +// } +// +// /** +// * Check if no edits were made by other users since +// * the time a user started editing the page. Limit to +// * 50 revisions for the sake of performance. +// * +// * @since 1.20 +// * @deprecated since 1.24 +// * +// * @param IDatabase|int $db The Database to perform the check on. May be given as a +// * Database Object or a database identifier usable with wfGetDB. +// * @param int $pageId The ID of the page in question +// * @param int $userId The ID of the user in question +// * @param String $since Look at edits since this time +// * +// * @return boolean True if the given user was the only one to edit since the given timestamp +// */ +// public static function userWasLastToEdit( $db, $pageId, $userId, $since ) { +// if ( is_int( $db ) ) { +// $db = wfGetDB( $db ); +// } +// +// return self::getRevisionStore()->userWasLastToEdit( $db, $pageId, $userId, $since ); +// } +// +// /** +// * Load a revision based on a known page ID and current revision ID from the DB +// * +// * This method allows for the use of caching, though accessing anything that normally +// * requires permission checks (aside from the text) will trigger a small DB lookup. +// * The title will also be loaded if $pageIdOrTitle is an integer ID. +// * +// * @param IDatabase $db ignored! +// * @param int|Title $pageIdOrTitle Page ID or Title Object +// * @param int $revId Known current revision of this page. Determined automatically if not given. +// * @return Revision|boolean Returns false if missing +// * @since 1.28 +// */ +// public static function newKnownCurrent( IDatabase $db, $pageIdOrTitle, $revId = 0 ) { +// $title = $pageIdOrTitle instanceof Title +// ? $pageIdOrTitle +// : Title::newFromID( $pageIdOrTitle ); +// +// if ( !$title ) { +// return false; +// } +// +// $record = self::getRevisionLookup()->getKnownCurrentRevision( $title, $revId ); +// return $record ? new Revision( $record ) : false; +// } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/content/XomwContent.java b/400_xowa/src/gplx/xowa/mediawiki/includes/content/XomwContent.java index 94c5de82f..86b5e257b 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/content/XomwContent.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/content/XomwContent.java @@ -15,6 +15,7 @@ Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt */ package gplx.xowa.mediawiki.includes.content; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; import gplx.xowa.mediawiki.includes.parsers.*; +// MW.SRC:1.33.1 /** * A content Object represents page content, e.g. the text to show on a page. * Content objects have no knowledge about how they relate to wiki pages. diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/dao/XomwIDBAccessObject.java b/400_xowa/src/gplx/xowa/mediawiki/includes/dao/XomwIDBAccessObject.java new file mode 100644 index 000000000..b8e5180be --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/dao/XomwIDBAccessObject.java @@ -0,0 +1,49 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.dao; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; +/** +* Interface for database access objects. +* +* Classes using this support a set of constants in a bitfield argument to their data loading +* functions. In general, objects should assume READ_NORMAL if no flags are explicitly given, +* though certain objects may assume READ_LATEST for common use case or legacy reasons. +* +* There are four types of reads: +* - READ_NORMAL : Potentially cached read of data (e.g. from a replica DB or stale replica) +* - READ_LATEST : Up-to-date read as of transaction start (e.g. from master or a quorum read) +* - READ_LOCKING : Up-to-date read as of now, that locks (shared) the records +* - READ_EXCLUSIVE : Up-to-date read as of now, that locks (exclusive) the records +* All record locks persist for the duration of the transaction. +* +* A special constant READ_LATEST_IMMUTABLE can be used for fetching append-only data. Such +* data is either (a) on a replica DB and up-to-date or (b) not yet there, but on the master/quorum. +* Because the data is append-only, it can never be stale on a replica DB if present. +* +* Callers should use READ_NORMAL (or pass in no flags) unless the read determines a write. +* In theory, such cases may require READ_LOCKING, though to avoid contention, READ_LATEST is +* often good enough. If UPDATE race condition checks are required on a row and expensive code +* must run after the row is fetched to determine the UPDATE, it may help to do something like: +* - a) Start transaction +* - b) Read the current row with READ_LATEST +* - c) Determine the new row (expensive, so we don't want to hold locks now) +* - d) Re-read the current row with READ_LOCKING; if it changed then bail out +* - e) otherwise, do the updates +* - f) Commit transaction +* +* @since 1.20 +*/ +public interface XomwIDBAccessObject { +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/dao/XomwIDBAccessObject_.java b/400_xowa/src/gplx/xowa/mediawiki/includes/dao/XomwIDBAccessObject_.java new file mode 100644 index 000000000..1b1afb9f8 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/dao/XomwIDBAccessObject_.java @@ -0,0 +1,36 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.dao; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; +public class XomwIDBAccessObject_ { + /** Constants for Object loading bitfield flags (higher => higher QoS) */ + /** @var int Read from a replica DB/non-quorum */ + public static final int READ_NORMAL = 0; + + /** @var int Read from the master/quorum */ + public static final int READ_LATEST = 1; + + /* @var int Read from the master/quorum and synchronized out other writers */ + public static final int READ_LOCKING = READ_LATEST | 2; // READ_LATEST (1) and "LOCK IN SHARE MODE" (2) + + /** @var int Read from the master/quorum and synchronized out other writers and locking readers */ + public static final int READ_EXCLUSIVE = READ_LOCKING | 4; // READ_LOCKING (3) and "FOR UPDATE" (4) + + /** @var int Read from a replica DB or without a quorum, using the master/quorum on miss */ + public static final int READ_LATEST_IMMUTABLE = 8; + + // Convenience constant for tracking how data was loaded (higher => higher QoS) + public static final int READ_NONE = -1; // not loaded yet (or the Object was cleared) +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwPage.java b/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwPage.java new file mode 100644 index 000000000..fce914c5d --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwPage.java @@ -0,0 +1,22 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.page; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; +// MW.SRC:1.33.1 +/** +* Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage) +*/ +public interface XomwPage { +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiCategoryPage.java b/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiCategoryPage.java new file mode 100644 index 000000000..c737cc77c --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiCategoryPage.java @@ -0,0 +1,74 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.page; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; +// MW.SRC:1.33.1 +/** +* Special handling for category pages +*/ +public class XomwWikiCategoryPage extends XomwWikiPage { public XomwWikiCategoryPage(XomwTitle title) {super(title); + } +// +// /** +// * Don't return a 404 for categories in use. +// * In use defined as: either the actual page exists +// * or the category currently has members. +// * +// * @return boolean +// */ +// public function hasViewableContent() { +// if ( parent::hasViewableContent() ) { +// return true; +// } else { +// $cat = Category::newFromTitle( $this->mTitle ); +// // If any of these are not 0, then has members +// if ( $cat->getPageCount() +// || $cat->getSubcatCount() +// || $cat->getFileCount() +// ) { +// return true; +// } +// } +// return false; +// } +// +// /** +// * Checks if a category is hidden. +// * +// * @since 1.27 +// * +// * @return boolean +// */ +// public function isHidden() { +// $pageId = $this->getTitle()->getArticleID(); +// $pageProps = PageProps::getInstance()->getProperties( $this->getTitle(), 'hiddencat' ); +// +// return isset( $pageProps[$pageId] ); +// } +// +// /** +// * Checks if a category is expected to be an unused category. +// * +// * @since 1.33 +// * +// * @return boolean +// */ +// public function isExpectedUnusedCategory() { +// $pageId = $this->getTitle()->getArticleID(); +// $pageProps = PageProps::getInstance()->getProperties( $this->getTitle(), 'expectunusedcategory' ); +// +// return isset( $pageProps[$pageId] ); +// } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiFilePage.java b/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiFilePage.java new file mode 100644 index 000000000..c662117c9 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiFilePage.java @@ -0,0 +1,250 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.page; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; +// MW.SRC:1.33.1 +/** +* Special handling for file pages +* +* @ingroup Media +*/ +public class XomwWikiFilePage extends XomwWikiPage { // /** @var File */ +// protected $mFile = false; +// /** @var LocalRepo */ +// protected $mRepo = null; +// /** @var boolean */ +// protected $mFileLoaded = false; +// /** @var array */ +// protected $mDupes = null; + + public XomwWikiFilePage(XomwTitle title) {super(title); +// $this->mDupes = null; +// $this->mRepo = null; + } +// +// /** +// * @param File $file +// */ +// public function setFile( $file ) { +// $this->mFile = $file; +// $this->mFileLoaded = true; +// } +// +// /** +// * @return boolean +// */ +// protected function loadFile() { +// if ( $this->mFileLoaded ) { +// return true; +// } +// $this->mFileLoaded = true; +// +// $this->mFile = wfFindFile( $this->mTitle ); +// if ( !$this->mFile ) { +// $this->mFile = wfLocalFile( $this->mTitle ); // always a File +// } +// $this->mRepo = $this->mFile->getRepo(); +// return true; +// } +// +// /** +// * @return mixed|null|Title +// */ +// public function getRedirectTarget() { +// $this->loadFile(); +// if ( $this->mFile->isLocal() ) { +// return parent::getRedirectTarget(); +// } +// // Foreign image page +// $from = $this->mFile->getRedirected(); +// $to = $this->mFile->getName(); +// if ( $from == $to ) { +// return null; +// } +// $this->mRedirectTarget = Title::makeTitle( NS_FILE, $to ); +// return $this->mRedirectTarget; +// } +// +// /** +// * @return boolean|mixed|Title +// */ +// public function followRedirect() { +// $this->loadFile(); +// if ( $this->mFile->isLocal() ) { +// return parent::followRedirect(); +// } +// $from = $this->mFile->getRedirected(); +// $to = $this->mFile->getName(); +// if ( $from == $to ) { +// return false; +// } +// return Title::makeTitle( NS_FILE, $to ); +// } +// +// /** +// * @return boolean +// */ +// public function isRedirect() { +// $this->loadFile(); +// if ( $this->mFile->isLocal() ) { +// return parent::isRedirect(); +// } +// +// return (boolean)$this->mFile->getRedirected(); +// } +// +// /** +// * @return boolean +// */ +// public function isLocal() { +// $this->loadFile(); +// return $this->mFile->isLocal(); +// } +// +// /** +// * @return boolean|File +// */ +// public function getFile() { +// $this->loadFile(); +// return $this->mFile; +// } +// +// /** +// * @return array|null +// */ +// public function getDuplicates() { +// $this->loadFile(); +// if ( !is_null( $this->mDupes ) ) { +// return $this->mDupes; +// } +// $hash = $this->mFile->getSha1(); +// if ( !( $hash ) ) { +// $this->mDupes = []; +// return $this->mDupes; +// } +// $dupes = RepoGroup::singleton()->findBySha1( $hash ); +// // Remove duplicates with self and non matching file sizes +// $self = $this->mFile->getRepoName() . ':' . $this->mFile->getName(); +// $size = $this->mFile->getSize(); +// +// /** +// * @var $file File +// */ +// foreach ( $dupes as $index => $file ) { +// $key = $file->getRepoName() . ':' . $file->getName(); +// if ( $key == $self ) { +// unset( $dupes[$index] ); +// } +// if ( $file->getSize() != $size ) { +// unset( $dupes[$index] ); +// } +// } +// $this->mDupes = $dupes; +// return $this->mDupes; +// } +// +// /** +// * Override handling of action=purge +// * @return boolean +// */ +// public function doPurge() { +// $this->loadFile(); +// +// if ( $this->mFile->exists() ) { +// wfDebug( 'ImagePage::doPurge purging ' . $this->mFile->getName() . "\n" ); +// DeferredUpdates::addUpdate( +// new HTMLCacheUpdate( $this->mTitle, 'imagelinks', 'file-purge' ) +// ); +// } else { +// wfDebug( 'ImagePage::doPurge no image for ' +// . $this->mFile->getName() . "; limiting purge to cache only\n" ); +// } +// +// // even if the file supposedly doesn't exist, force any cached information +// // to be updated (in case the cached information is wrong) +// +// // Purge current version and its thumbnails +// $this->mFile->purgeCache( [ 'forThumbRefresh' => true ] ); +// +// // Purge the old versions and their thumbnails +// foreach ( $this->mFile->getHistory() as $oldFile ) { +// $oldFile->purgeCache( [ 'forThumbRefresh' => true ] ); +// } +// +// if ( $this->mRepo ) { +// // Purge redirect cache +// $this->mRepo->invalidateImageRedirect( $this->mTitle ); +// } +// +// return parent::doPurge(); +// } +// +// /** +// * Get the categories this file is a member of on the wiki where it was uploaded. +// * For local files, this is the same as getCategories(). +// * For foreign API files (InstantCommons), this is not supported currently. +// * Results will include hidden categories. +// * +// * @return TitleArray|Title[] +// * @since 1.23 +// */ +// public function getForeignCategories() { +// $this->loadFile(); +// $title = $this->mTitle; +// $file = $this->mFile; +// +// if ( !$file instanceof LocalFile ) { +// wfDebug( __CLASS__ . '::' . __METHOD__ . " is not supported for this file\n" ); +// return TitleArray::newFromResult( new FakeResultWrapper( [] ) ); +// } +// +// /** @var LocalRepo $repo */ +// $repo = $file->getRepo(); +// $dbr = $repo->getReplicaDB(); +// +// $res = $dbr->select( +// [ 'page', 'categorylinks' ], +// [ +// 'page_title' => 'cl_to', +// 'page_namespace' => NS_CATEGORY, +// ], +// [ +// 'page_namespace' => $title->getNamespace(), +// 'page_title' => $title->getDBkey(), +// ], +// __METHOD__, +// [], +// [ 'categorylinks' => [ 'JOIN', 'page_id = cl_from' ] ] +// ); +// +// return TitleArray::newFromResult( $res ); +// } +// +// /** +// * @since 1.28 +// * @return String +// */ +// public function getWikiDisplayName() { +// return $this->getFile()->getRepo()->getDisplayName(); +// } +// +// /** +// * @since 1.28 +// * @return String +// */ +// public function getSourceURL() { +// return $this->getFile()->getDescriptionUrl(); +// } +} \ No newline at end of file diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiPage.java b/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiPage.java new file mode 100644 index 000000000..426b877d6 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/page/XomwWikiPage.java @@ -0,0 +1,3833 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.page; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; +// MW.SRC:1.33.1 +import gplx.xowa.mediawiki.includes.content.*; +import gplx.xowa.mediawiki.includes.dao.*; +import gplx.xowa.mediawiki.includes.exception.*; +import gplx.xowa.mediawiki.includes.user.*; +/** +* Cl+ass representing a MediaWiki article and history. +* +* Some fields are public only for backwards-compatibility. Use accessors. +* In the past, this cl+ass was part of Article.php and everything was public. +*/ +public class XomwWikiPage implements XomwPage, XomwIDBAccessObject { + // Constants for mDataLoadedFrom and related + + /** + * @var Title + */ + public XomwTitle mTitle = null; + + /** + * @var boolean + * @protected + */ + public boolean mDataLoaded = false; + + /** + * @var boolean + * @protected + */ + public boolean mIsRedirect = false; + + /** + * @var int|false False means "not loaded" + * @protected + */ + public int mLatest = -1; // = false; + +// /** @var PreparedEdit Map of cache fields (text, parser output, ect) for a proposed/new edit */ +// public mPreparedEdit = false; + +// /** +// * @var int +// */ +// protected int mId = null; +// +// /** +// * @var int One of the READ_* constants +// */ +// protected mDataLoadedFrom = self::READ_NONE; +// +// /** +// * @var Title +// */ +// protected mRedirectTarget = null; + + /** + * @var Revision + */ + protected XomwRevision mLastRevision = null; + +// /** +// * @var String Timestamp of the current revision or empty String if not loaded +// */ +// protected mTimestamp = ''; +// +// /** +// * @var String +// */ +// protected mTouched = '19700101000000'; +// +// /** +// * @var String +// */ +// protected mLinksUpdated = '19700101000000'; +// +// /** +// * @var DerivedPageDataUpdater|null +// */ +// private derivedDataUpdater = null; + + /** + * Constructor and clear the article + * @param Title title Reference to a Title Object. + */ + public XomwWikiPage(XomwTitle title) { + this.mTitle = title; + } + +// /** +// * Makes sure that the mTitle Object is cloned +// * to the newly cloned WikiPage. +// */ +// public function __clone() { +// this.mTitle = clone this.mTitle; +// } + + /** + * Create a WikiPage Object of the appropriate class for the given title. + * + * @param Title title + * + * @throws MWException + * @return WikiPage|WikiCategoryPage|WikiFilePage + */ + public static XomwWikiPage factory(XomwTitle title) { + int ns = title.getNamespace(); + + if (ns == XomwDefines.NS_MEDIA) { + throw new XomwMWException("NS_MEDIA is @gplx.Virtual a namespace; use NS_FILE."); + } else if (ns < 0) { + throw new XomwMWException("Invalid @gplx.Virtual or namespace ns given."); + } + + XomwWikiPage page = null; + //if (!Hooks::run('WikiPageFactory', [ title, &page ])) { + // return page; + //} + + switch (ns) { + case XomwDefines.NS_FILE: +// page = new XomwWikiFilePage(title); + break; + case XomwDefines.NS_CATEGORY: +// page = new XomwWikiCategoryPage(title); + break; + default: + page = new XomwWikiPage(title); + break; + } + + return page; + } + +// /** +// * Constructor from a page id +// * +// * @param int id Article ID to load +// * @param String|int from One of the following values: +// * - "fromdb" or WikiPage::READ_NORMAL to select from a replica DB +// * - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database +// * +// * @return WikiPage|null +// */ +// public static function newFromID(id, from = 'fromdb') { +// // page ids are never 0 or negative, see T63166 +// if (id < 1) { +// return null; +// } +// +// from = self::convertSelectType(from); +// db = wfGetDB(from === self::READ_LATEST ? DB_MASTER : DB_REPLICA); +// pageQuery = self::getQueryInfo(); +// row = db.selectRow( +// pageQuery['tables'], pageQuery['fields'], [ 'page_id' => id ], __METHOD__, +// [], pageQuery['joins'] +// ); +// if (!row) { +// return null; +// } +// return self::newFromRow(row, from); +// } +// +// /** +// * Constructor from a database row +// * +// * @since 1.20 +// * @param Object row Database row containing at least fields returned by selectFields(). +// * @param String|int from Source of data: +// * - "fromdb" or WikiPage::READ_NORMAL: from a replica DB +// * - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB +// * - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE +// * @return WikiPage +// */ +// public static function newFromRow(row, from = 'fromdb') { +// page = self::factory(Title::newFromRow(row)); +// page.loadFromRow(row, from); +// return page; +// } +// +// /** +// * Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants. +// * +// * @param Object|String|int type +// * @return mixed +// */ +// protected static function convertSelectType(type) { +// switch (type) { +// case 'fromdb': +// return self::READ_NORMAL; +// case 'fromdbmaster': +// return self::READ_LATEST; +// case 'forupdate': +// return self::READ_LOCKING; +// default: +// // It may already be an integer or whatever else +// return type; +// } +// } +// +// /** +// * @return RevisionStore +// */ +// private function getRevisionStore() { +// return MediaWikiServices::getInstance().getRevisionStore(); +// } +// +// /** +// * @return RevisionRenderer +// */ +// private function getRevisionRenderer() { +// return MediaWikiServices::getInstance().getRevisionRenderer(); +// } +// +// /** +// * @return SlotRoleRegistry +// */ +// private function getSlotRoleRegistry() { +// return MediaWikiServices::getInstance().getSlotRoleRegistry(); +// } +// +// /** +// * @return ParserCache +// */ +// private function getParserCache() { +// return MediaWikiServices::getInstance().getParserCache(); +// } +// +// /** +// * @return LoadBalancer +// */ +// private function getDBLoadBalancer() { +// return MediaWikiServices::getInstance().getDBLoadBalancer(); +// } +// +// /** +// * @todo Move this UI stuff somewhere else +// * +// * @see ContentHandler::getActionOverrides +// * @return array +// */ +// public function getActionOverrides() { +// return this.getContentHandler().getActionOverrides(); +// } +// +// /** +// * Returns the ContentHandler instance to be used to deal with the content of this WikiPage. +// * +// * Shorthand for ContentHandler::getForModelID(this.getContentModel()); +// * +// * @return ContentHandler +// * +// * @since 1.21 +// */ +// public function getContentHandler() { +// return ContentHandler::getForModelID(this.getContentModel()); +// } +// +// /** +// * Get the title Object of the article +// * @return Title Title Object of this page +// */ +// public function getTitle() { +// return this.mTitle; +// } +// +// /** +// * Clear the Object +// * @return void +// */ +// public function clear() { +// this.mDataLoaded = false; +// this.mDataLoadedFrom = self::READ_NONE; +// +// this.clearCacheFields(); +// } +// +// /** +// * Clear the Object cache fields +// * @return void +// */ +// protected function clearCacheFields() { +// this.mId = null; +// this.mRedirectTarget = null; // Title Object if set +// this.mLastRevision = null; // Latest revision +// this.mTouched = '19700101000000'; +// this.mLinksUpdated = '19700101000000'; +// this.mTimestamp = ''; +// this.mIsRedirect = false; +// this.mLatest = false; +// // T59026: do not clear this.derivedDataUpdater since getDerivedDataUpdater() already +// // checks the requested rev ID and content against the cached one. For most +// // content types, the output should not change during the lifetime of this cache. +// // Clearing it can cause extra parses on edit for no reason. +// } +// +// /** +// * Clear the mPreparedEdit cache field, as may be needed by mutable content types +// * @return void +// * @since 1.23 +// */ +// public function clearPreparedEdit() { +// this.mPreparedEdit = false; +// } +// +// /** +// * Return the list of revision fields that should be selected to create +// * a new page. +// * +// * @deprecated since 1.31, use self::getQueryInfo() instead. +// * @return array +// */ +// public static function selectFields() { +// global wgContentHandlerUseDB, wgPageLanguageUseDB; +// +// wfDeprecated(__METHOD__, '1.31'); +// +// fields = [ +// 'page_id', +// 'page_namespace', +// 'page_title', +// 'page_restrictions', +// 'page_is_redirect', +// 'page_is_new', +// 'page_random', +// 'page_touched', +// 'page_links_updated', +// 'page_latest', +// 'page_len', +// ]; +// +// if (wgContentHandlerUseDB) { +// fields[] = 'page_content_model'; +// } +// +// if (wgPageLanguageUseDB) { +// fields[] = 'page_lang'; +// } +// +// return fields; +// } +// +// /** +// * Return the tables, fields, and join conditions to be selected to create +// * a new page Object. +// * @since 1.31 +// * @return array With three keys: +// * - tables: (String[]) to include in the `table` to `IDatabase.select()` +// * - fields: (String[]) to include in the `vars` to `IDatabase.select()` +// * - joins: (array) to include in the `join_conds` to `IDatabase.select()` +// */ +// public static function getQueryInfo() { +// global wgContentHandlerUseDB, wgPageLanguageUseDB; +// +// ret = [ +// 'tables' => [ 'page' ], +// 'fields' => [ +// 'page_id', +// 'page_namespace', +// 'page_title', +// 'page_restrictions', +// 'page_is_redirect', +// 'page_is_new', +// 'page_random', +// 'page_touched', +// 'page_links_updated', +// 'page_latest', +// 'page_len', +// ], +// 'joins' => [], +// ]; +// +// if (wgContentHandlerUseDB) { +// ret['fields'][] = 'page_content_model'; +// } +// +// if (wgPageLanguageUseDB) { +// ret['fields'][] = 'page_lang'; +// } +// +// return ret; +// } +// +// /** +// * Fetch a page record with the given conditions +// * @param IDatabase dbr +// * @param array conditions +// * @param array options +// * @return Object|boolean Database result resource, or false on failure +// */ +// protected function pageData(dbr, conditions, options = []) { +// pageQuery = self::getQueryInfo(); +// +// // Avoid PHP 7.1 warning of passing this by reference +// wikiPage = this; +// +// Hooks::run('ArticlePageDataBefore', [ +// &wikiPage, &pageQuery['fields'], &pageQuery['tables'], &pageQuery['joins'] +// ]); +// +// row = dbr.selectRow( +// pageQuery['tables'], +// pageQuery['fields'], +// conditions, +// __METHOD__, +// options, +// pageQuery['joins'] +// ); +// +// Hooks::run('ArticlePageDataAfter', [ &wikiPage, &row ]); +// +// return row; +// } +// +// /** +// * Fetch a page record matching the Title Object's namespace and title +// * using a sanitized title String +// * +// * @param IDatabase dbr +// * @param Title title +// * @param array options +// * @return Object|boolean Database result resource, or false on failure +// */ +// public function pageDataFromTitle(dbr, title, options = []) { +// return this.pageData(dbr, [ +// 'page_namespace' => title.getNamespace(), +// 'page_title' => title.getDBkey() ], options); +// } +// +// /** +// * Fetch a page record matching the requested ID +// * +// * @param IDatabase dbr +// * @param int id +// * @param array options +// * @return Object|boolean Database result resource, or false on failure +// */ +// public function pageDataFromId(dbr, id, options = []) { +// return this.pageData(dbr, [ 'page_id' => id ], options); +// } +// +// /** +// * Load the Object from a given source by title +// * +// * @param Object|String|int from One of the following: +// * - A DB query result Object. +// * - "fromdb" or WikiPage::READ_NORMAL to get from a replica DB. +// * - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB. +// * - "forupdate" or WikiPage::READ_LOCKING to get from the master DB +// * using SELECT FOR UPDATE. +// * +// * @return void +// */ +// public function loadPageData(from = 'fromdb') { +// from = self::convertSelectType(from); +// if (is_int(from) && from <= this.mDataLoadedFrom) { +// // We already have the data from the correct location, no need to load it twice. +// return; +// } +// +// if (is_int(from)) { +// list(index, opts) = DBAccessObjectUtils::getDBOptions(from); +// loadBalancer = this.getDBLoadBalancer(); +// db = loadBalancer.getConnection(index); +// data = this.pageDataFromTitle(db, this.mTitle, opts); +// +// if (!data +// && index == DB_REPLICA +// && loadBalancer.getServerCount() > 1 +// && loadBalancer.hasOrMadeRecentMasterChanges() +// ) { +// from = self::READ_LATEST; +// list(index, opts) = DBAccessObjectUtils::getDBOptions(from); +// db = loadBalancer.getConnection(index); +// data = this.pageDataFromTitle(db, this.mTitle, opts); +// } +// } else { +// // No idea from where the caller got this data, assume replica DB. +// data = from; +// from = self::READ_NORMAL; +// } +// +// this.loadFromRow(data, from); +// } +// +// /** +// * Checks whether the page data was loaded using the given database access mode (or better). +// * +// * @since 1.32 +// * +// * @param String|int from One of the following: +// * - "fromdb" or WikiPage::READ_NORMAL to get from a replica DB. +// * - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB. +// * - "forupdate" or WikiPage::READ_LOCKING to get from the master DB +// * using SELECT FOR UPDATE. +// * +// * @return boolean +// */ +// public function wasLoadedFrom(from) { +// from = self::convertSelectType(from); +// +// if (!is_int(from)) { +// // No idea from where the caller got this data, assume replica DB. +// from = self::READ_NORMAL; +// } +// +// if (is_int(from) && from <= this.mDataLoadedFrom) { +// return true; +// } +// +// return false; +// } +// +// /** +// * Load the Object from a database row +// * +// * @since 1.20 +// * @param Object|boolean data DB row containing fields returned by selectFields() or false +// * @param String|int from One of the following: +// * - "fromdb" or WikiPage::READ_NORMAL if the data comes from a replica DB +// * - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB +// * - "forupdate" or WikiPage::READ_LOCKING if the data comes from +// * the master DB using SELECT FOR UPDATE +// */ +// public function loadFromRow(data, from) { +// lc = MediaWikiServices::getInstance().getLinkCache(); +// lc.clearLink(this.mTitle); +// +// if (data) { +// lc.addGoodLinkObjFromRow(this.mTitle, data); +// +// this.mTitle.loadFromRow(data); +// +// // Old-fashioned restrictions +// this.mTitle.loadRestrictions(data.page_restrictions); +// +// this.mId = intval(data.page_id); +// this.mTouched = wfTimestamp(TS_MW, data.page_touched); +// this.mLinksUpdated = wfTimestampOrNull(TS_MW, data.page_links_updated); +// this.mIsRedirect = intval(data.page_is_redirect); +// this.mLatest = intval(data.page_latest); +// // T39225: latest may no longer match the cached latest Revision Object. +// // Double-check the ID of any cached latest Revision Object for consistency. +// if (this.mLastRevision && this.mLastRevision.getId() != this.mLatest) { +// this.mLastRevision = null; +// this.mTimestamp = ''; +// } +// } else { +// lc.addBadLinkObj(this.mTitle); +// +// this.mTitle.loadFromRow(false); +// +// this.clearCacheFields(); +// +// this.mId = 0; +// } +// +// this.mDataLoaded = true; +// this.mDataLoadedFrom = self::convertSelectType(from); +// } +// +// /** +// * @return int Page ID +// */ +// public function getId() { +// if (!this.mDataLoaded) { +// this.loadPageData(); +// } +// return this.mId; +// } +// +// /** +// * @return boolean Whether or not the page exists in the database +// */ +// public function exists() { +// if (!this.mDataLoaded) { +// this.loadPageData(); +// } +// return this.mId > 0; +// } +// +// /** +// * Check if this page is something we're going to be showing +// * some sort of sensible content for. If we return false, page +// * views (plain action=view) will return an HTTP 404 response, +// * so spiders and robots can know they're following a bad link. +// * +// * @return boolean +// */ +// public function hasViewableContent() { +// return this.mTitle.isKnown(); +// } +// +// /** +// * Tests if the article content represents a redirect +// * +// * @return boolean +// */ +// public function isRedirect() { +// if (!this.mDataLoaded) { +// this.loadPageData(); +// } +// +// return (boolean)this.mIsRedirect; +// } +// +// /** +// * Returns the page's content model id (see the CONTENT_MODEL_XXX constants). +// * +// * Will use the revisions actual content model if the page exists, +// * and the page's default if the page doesn't exist yet. +// * +// * @return String +// * +// * @since 1.21 +// */ +// public function getContentModel() { +// if (this.exists()) { +// cache = MediaWikiServices::getInstance().getMainWANObjectCache(); +// +// return cache.getWithSetCallback( +// cache.makeKey('page-content-model', this.getLatest()), +// cache::TTL_MONTH, +// function () { +// rev = this.getRevision(); +// if (rev) { +// // Look at the revision's actual content model +// return rev.getContentModel(); +// } else { +// title = this.mTitle.getPrefixedDBkey(); +// wfWarn("Page title exists but has no (visible) revisions!"); +// return this.mTitle.getContentModel(); +// } +// } +// ); +// } +// +// // use the default model for this page +// return this.mTitle.getContentModel(); +// } +// +// /** +// * Loads page_touched and returns a value indicating if it should be used +// * @return boolean True if this page exists and is not a redirect +// */ +// public function checkTouched() { +// if (!this.mDataLoaded) { +// this.loadPageData(); +// } +// return (this.mId && !this.mIsRedirect); +// } +// +// /** +// * Get the page_touched field +// * @return String Containing GMT timestamp +// */ +// public function getTouched() { +// if (!this.mDataLoaded) { +// this.loadPageData(); +// } +// return this.mTouched; +// } +// +// /** +// * Get the page_links_updated field +// * @return String|null Containing GMT timestamp +// */ +// public function getLinksTimestamp() { +// if (!this.mDataLoaded) { +// this.loadPageData(); +// } +// return this.mLinksUpdated; +// } +// +// /** +// * Get the page_latest field +// * @return int The rev_id of current revision +// */ +// public function getLatest() { +// if (!this.mDataLoaded) { +// this.loadPageData(); +// } +// return (int)this.mLatest; +// } +// +// /** +// * Get the Revision Object of the oldest revision +// * @return Revision|null +// */ +// public function getOldestRevision() { +// // Try using the replica DB first, then try the master +// rev = this.mTitle.getFirstRevision(); +// if (!rev) { +// rev = this.mTitle.getFirstRevision(Title::GAID_FOR_UPDATE); +// } +// return rev; +// } + + /** + * Loads everything except the text + * This isn't necessary for all uses, so it's only done if needed. + */ + protected void loadLastEdit() { +// if (this.mLastRevision !== null) { +// return; // already loaded +// } +// +// latest = this.getLatest(); +// if (!latest) { +// return; // page doesn't exist or is missing page_latest info +// } +// +// if (this.mDataLoadedFrom == self::READ_LOCKING) { +// // T39225: if session S1 loads the page row FOR UPDATE, the result always +// // includes the latest changes committed. This is true even within REPEATABLE-READ +// // transactions, where S1 normally only sees changes committed before the first S1 +// // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it +// // may not find it since a page row UPDATE and revision row INSERT by S2 may have +// // happened after the first S1 SELECT. +// // https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read +// flags = Revision::READ_LOCKING; +// revision = Revision::newFromPageId(this.getId(), latest, flags); +// } elseif (this.mDataLoadedFrom == self::READ_LATEST) { +// // Bug T93976: if page_latest was loaded from the master, fetch the +// // revision from there as well, as it may not exist yet on a replica DB. +// // Also, this keeps the queries in the same REPEATABLE-READ snapshot. +// flags = Revision::READ_LATEST; +// revision = Revision::newFromPageId(this.getId(), latest, flags); +// } else { +// dbr = wfGetDB(DB_REPLICA); +// revision = Revision::newKnownCurrent(dbr, this.getTitle(), latest); +// } +// +// if (revision) { // sanity +// this.setLastEdit(revision); +// } + } +// +// /** +// * Set the latest revision +// * @param Revision revision +// */ +// protected function setLastEdit(Revision revision) { +// this.mLastRevision = revision; +// this.mTimestamp = revision.getTimestamp(); +// } +// +// /** +// * Get the latest revision +// * @return Revision|null +// */ +// public function getRevision() { +// this.loadLastEdit(); +// if (this.mLastRevision) { +// return this.mLastRevision; +// } +// return null; +// } +// +// /** +// * Get the latest revision +// * @return RevisionRecord|null +// */ +// public function getRevisionRecord() { +// this.loadLastEdit(); +// if (this.mLastRevision) { +// return this.mLastRevision.getRevisionRecord(); +// } +// return null; +// } + + /** + * Get the content of the current revision. No side-effects... + * + * @param int audience One of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to wgUser + * Revision::RAW get the text regardless of permissions + * @param User|null user User Object to check for, only if FOR_THIS_USER is passed + * to the audience parameter + * @return Content|null The content of the current revision + * + * @since 1.21 + */ + public XomwContent getContent(int audience, XomwUser user) { // = Revision::FOR_PUBLIC + this.loadLastEdit(); +// if (XophpObject_.is_true(this.mLastRevision)) { +// return this.mLastRevision.getContent(audience, user); +// } + return null; + } + +// /** +// * @return String MW timestamp of last article revision +// */ +// public function getTimestamp() { +// // Check if the field has been filled by WikiPage::setTimestamp() +// if (!this.mTimestamp) { +// this.loadLastEdit(); +// } +// +// return wfTimestamp(TS_MW, this.mTimestamp); +// } +// +// /** +// * Set the page timestamp (use only to avoid DB queries) +// * @param String ts MW timestamp of last article revision +// * @return void +// */ +// public function setTimestamp(ts) { +// this.mTimestamp = wfTimestamp(TS_MW, ts); +// } +// +// /** +// * @param int audience One of: +// * Revision::FOR_PUBLIC to be displayed to all users +// * Revision::FOR_THIS_USER to be displayed to the given user +// * Revision::RAW get the text regardless of permissions +// * @param User|null user User Object to check for, only if FOR_THIS_USER is passed +// * to the audience parameter +// * @return int User ID for the user that made the last article revision +// */ +// public function getUser(audience = Revision::FOR_PUBLIC, User user = null) { +// this.loadLastEdit(); +// if (this.mLastRevision) { +// return this.mLastRevision.getUser(audience, user); +// } else { +// return -1; +// } +// } +// +// /** +// * Get the User Object of the user who created the page +// * @param int audience One of: +// * Revision::FOR_PUBLIC to be displayed to all users +// * Revision::FOR_THIS_USER to be displayed to the given user +// * Revision::RAW get the text regardless of permissions +// * @param User|null user User Object to check for, only if FOR_THIS_USER is passed +// * to the audience parameter +// * @return User|null +// */ +// public function getCreator(audience = Revision::FOR_PUBLIC, User user = null) { +// revision = this.getOldestRevision(); +// if (revision) { +// userName = revision.getUserText(audience, user); +// return User::newFromName(userName, false); +// } else { +// return null; +// } +// } +// +// /** +// * @param int audience One of: +// * Revision::FOR_PUBLIC to be displayed to all users +// * Revision::FOR_THIS_USER to be displayed to the given user +// * Revision::RAW get the text regardless of permissions +// * @param User|null user User Object to check for, only if FOR_THIS_USER is passed +// * to the audience parameter +// * @return String Username of the user that made the last article revision +// */ +// public function getUserText(audience = Revision::FOR_PUBLIC, User user = null) { +// this.loadLastEdit(); +// if (this.mLastRevision) { +// return this.mLastRevision.getUserText(audience, user); +// } else { +// return ''; +// } +// } +// +// /** +// * @param int audience One of: +// * Revision::FOR_PUBLIC to be displayed to all users +// * Revision::FOR_THIS_USER to be displayed to the given user +// * Revision::RAW get the text regardless of permissions +// * @param User|null user User Object to check for, only if FOR_THIS_USER is passed +// * to the audience parameter +// * @return String|null Comment stored for the last article revision, or null if the specified +// * audience does not have access to the comment. +// */ +// public function getComment(audience = Revision::FOR_PUBLIC, User user = null) { +// this.loadLastEdit(); +// if (this.mLastRevision) { +// return this.mLastRevision.getComment(audience, user); +// } else { +// return ''; +// } +// } +// +// /** +// * Returns true if last revision was marked as "minor edit" +// * +// * @return boolean Minor edit indicator for the last article revision. +// */ +// public function getMinorEdit() { +// this.loadLastEdit(); +// if (this.mLastRevision) { +// return this.mLastRevision.isMinor(); +// } else { +// return false; +// } +// } +// +// /** +// * Determine whether a page would be suitable for being counted as an +// * article in the site_stats table based on the title & its content +// * +// * @param PreparedEdit|boolean editInfo (false): Object returned by prepareTextForEdit(), +// * if false, the current database state will be used +// * @return boolean +// */ +// public function isCountable(editInfo = false) { +// global wgArticleCountMethod; +// +// // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable. +// +// if (!this.mTitle.isContentPage()) { +// return false; +// } +// +// if (editInfo) { +// // NOTE: only the main slot can make a page a redirect +// content = editInfo.pstContent; +// } else { +// content = this.getContent(); +// } +// +// if (!content || content.isRedirect()) { +// return false; +// } +// +// hasLinks = null; +// +// if (wgArticleCountMethod === 'link') { +// // nasty special case to avoid re-parsing to detect links +// +// if (editInfo) { +// // ParserOutput::getLinks() is a 2D array of page links, so +// // to be really correct we would need to recurse in the array +// // but the main array should only have items in it if there are +// // links. +// hasLinks = (boolean)count(editInfo.output.getLinks()); +// } else { +// // NOTE: keep in sync with RevisionRenderer::getLinkCount +// // NOTE: keep in sync with DerivedPageDataUpdater::isCountable +// hasLinks = (boolean)wfGetDB(DB_REPLICA).selectField('pagelinks', 1, +// [ 'pl_from' => this.getId() ], __METHOD__); +// } +// } +// +// // TODO: MCR: determine hasLinks for each slot, and use that info +// // with that slot's Content's isCountable method. That requires per- +// // slot ParserOutput in the ParserCache, or per-slot info in the +// // pagelinks table. +// return content.isCountable(hasLinks); +// } +// +// /** +// * If this page is a redirect, get its target +// * +// * The target will be fetched from the redirect table if possible. +// * If this page doesn't have an entry there, call insertRedirect() +// * @return Title|null Title Object, or null if this page is not a redirect +// */ +// public function getRedirectTarget() { +// if (!this.mTitle.isRedirect()) { +// return null; +// } +// +// if (this.mRedirectTarget !== null) { +// return this.mRedirectTarget; +// } +// +// // Query the redirect table +// dbr = wfGetDB(DB_REPLICA); +// row = dbr.selectRow('redirect', +// [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ], +// [ 'rd_from' => this.getId() ], +// __METHOD__ +// ); +// +// // rd_fragment and rd_interwiki were added later, populate them if empty +// if (row && !is_null(row.rd_fragment) && !is_null(row.rd_interwiki)) { +// // (T203942) We can't redirect to Media namespace because it's virtual. +// // We don't want to modify Title objects farther down the +// // line. So, let's fix this here by changing to File namespace. +// if (row.rd_namespace == NS_MEDIA) { +// namespace = NS_FILE; +// } else { +// namespace = row.rd_namespace; +// } +// this.mRedirectTarget = Title::makeTitle( +// namespace, row.rd_title, +// row.rd_fragment, row.rd_interwiki +// ); +// return this.mRedirectTarget; +// } +// +// // This page doesn't have an entry in the redirect table +// this.mRedirectTarget = this.insertRedirect(); +// return this.mRedirectTarget; +// } +// +// /** +// * Insert an entry for this page into the redirect table if the content is a redirect +// * +// * The database update will be deferred via DeferredUpdates +// * +// * Don't call this function directly unless you know what you're doing. +// * @return Title|null Title Object or null if not a redirect +// */ +// public function insertRedirect() { +// content = this.getContent(); +// retval = content ? content.getUltimateRedirectTarget() : null; +// if (!retval) { +// return null; +// } +// +// // Update the DB post-send if the page has not cached since now +// latest = this.getLatest(); +// DeferredUpdates::addCallableUpdate( +// function () use (retval, latest) { +// this.insertRedirectEntry(retval, latest); +// }, +// DeferredUpdates::POSTSEND, +// wfGetDB(DB_MASTER) +// ); +// +// return retval; +// } +// +// /** +// * Insert or update the redirect table entry for this page to indicate it redirects to rt +// * @param Title rt Redirect target +// * @param int|null oldLatest Prior page_latest for check and set +// */ +// public function insertRedirectEntry(Title rt, oldLatest = null) { +// dbw = wfGetDB(DB_MASTER); +// dbw.startAtomic(__METHOD__); +// +// if (!oldLatest || oldLatest == this.lockAndGetLatest()) { +// contLang = MediaWikiServices::getInstance().getContentLanguage(); +// truncatedFragment = contLang.truncateForDatabase(rt.getFragment(), 255); +// dbw.upsert( +// 'redirect', +// [ +// 'rd_from' => this.getId(), +// 'rd_namespace' => rt.getNamespace(), +// 'rd_title' => rt.getDBkey(), +// 'rd_fragment' => truncatedFragment, +// 'rd_interwiki' => rt.getInterwiki(), +// ], +// [ 'rd_from' ], +// [ +// 'rd_namespace' => rt.getNamespace(), +// 'rd_title' => rt.getDBkey(), +// 'rd_fragment' => truncatedFragment, +// 'rd_interwiki' => rt.getInterwiki(), +// ], +// __METHOD__ +// ); +// } +// +// dbw.endAtomic(__METHOD__); +// } +// +// /** +// * Get the Title Object or URL this page redirects to +// * +// * @return boolean|Title|String False, Title of in-wiki target, or String with URL +// */ +// public function followRedirect() { +// return this.getRedirectURL(this.getRedirectTarget()); +// } +// +// /** +// * Get the Title Object or URL to use for a redirect. We use Title +// * objects for same-wiki, non-special redirects and URLs for everything +// * else. +// * @param Title rt Redirect target +// * @return boolean|Title|String False, Title Object of local target, or String with URL +// */ +// public function getRedirectURL(rt) { +// if (!rt) { +// return false; +// } +// +// if (rt.isExternal()) { +// if (rt.isLocal()) { +// // Offsite wikis need an HTTP redirect. +// // This can be hard to reverse and may produce loops, +// // so they may be disabled in the site configuration. +// source = this.mTitle.getFullURL('redirect=no'); +// return rt.getFullURL([ 'rdfrom' => source ]); +// } else { +// // External pages without "local" bit set are not valid +// // redirect targets +// return false; +// } +// } +// +// if (rt.isSpecialPage()) { +// // Gotta handle redirects to special pages differently: +// // Fill the HTTP response "Location" header and ignore the rest of the page we're on. +// // Some pages are not valid targets. +// if (rt.isValidRedirectTarget()) { +// return rt.getFullURL(); +// } else { +// return false; +// } +// } +// +// return rt; +// } +// +// /** +// * Get a list of users who have edited this article, not including the user who made +// * the most recent revision, which you can get from article.getUser() if you want it +// * @return UserArrayFromResult +// */ +// public function getContributors() { +// // @todo: This is expensive; cache this info somewhere. +// +// dbr = wfGetDB(DB_REPLICA); +// +// actorMigration = ActorMigration::newMigration(); +// actorQuery = actorMigration.getJoin('rev_user'); +// +// tables = array_merge([ 'revision' ], actorQuery['tables'], [ 'user' ]); +// +// fields = [ +// 'user_id' => actorQuery['fields']['rev_user'], +// 'user_name' => actorQuery['fields']['rev_user_text'], +// 'actor_id' => actorQuery['fields']['rev_actor'], +// 'user_real_name' => 'MIN(user_real_name)', +// 'timestamp' => 'MAX(rev_timestamp)', +// ]; +// +// conds = [ 'rev_page' => this.getId() ]; +// +// // The user who made the top revision gets credited as "this page was last edited by +// // John, based on contributions by Tom, Dick and Harry", so don't include them twice. +// user = this.getUser() +// ? User::newFromId(this.getUser()) +// : User::newFromName(this.getUserText(), false); +// conds[] = 'NOT(' . actorMigration.getWhere(dbr, 'rev_user', user)['conds'] . ')'; +// +// // Username hidden? +// conds[] = "{dbr.bitAnd('rev_deleted', Revision::DELETED_USER)} = 0"; +// +// jconds = [ +// 'user' => [ 'LEFT JOIN', actorQuery['fields']['rev_user'] . ' = user_id' ], +// ] + actorQuery['joins']; +// +// options = [ +// 'GROUP BY' => [ fields['user_id'], fields['user_name'] ], +// 'ORDER BY' => 'timestamp DESC', +// ]; +// +// res = dbr.select(tables, fields, conds, __METHOD__, options, jconds); +// return new UserArrayFromResult(res); +// } +// +// /** +// * Should the parser cache be used? +// * +// * @param ParserOptions parserOptions ParserOptions to check +// * @param int oldId +// * @return boolean +// */ +// public function shouldCheckParserCache(ParserOptions parserOptions, oldId) { +// return parserOptions.getStubThreshold() == 0 +// && this.exists() +// && (oldId === null || oldId === 0 || oldId === this.getLatest()) +// && this.getContentHandler().isParserCacheSupported(); +// } +// +// /** +// * Get a ParserOutput for the given ParserOptions and revision ID. +// * +// * The parser cache will be used if possible. Cache misses that result +// * in parser runs are debounced with PoolCounter. +// * +// * XXX merge this with updateParserCache()? +// * +// * @since 1.19 +// * @param ParserOptions parserOptions ParserOptions to use for the parse operation +// * @param null|int oldid Revision ID to get the text from, passing null or 0 will +// * get the current revision (default value) +// * @param boolean forceParse Force reindexing, regardless of cache settings +// * @return boolean|ParserOutput ParserOutput or false if the revision was not found +// */ +// public function getParserOutput( +// ParserOptions parserOptions, oldid = null, forceParse = false +// ) { +// useParserCache = +// (!forceParse) && this.shouldCheckParserCache(parserOptions, oldid); +// +// if (useParserCache && !parserOptions.isSafeToCache()) { +// throw new InvalidArgumentException( +// 'The supplied ParserOptions are not safe to cache. Fix the options or set forceParse = true.' +// ); +// } +// +// wfDebug(__METHOD__ . +// ': using parser cache: ' . (useParserCache ? 'yes' : 'no') . "\n"); +// if (parserOptions.getStubThreshold()) { +// wfIncrStats('pcache.miss.stub'); +// } +// +// if (useParserCache) { +// parserOutput = this.getParserCache() +// .get(this, parserOptions); +// if (parserOutput !== false) { +// return parserOutput; +// } +// } +// +// if (oldid === null || oldid === 0) { +// oldid = this.getLatest(); +// } +// +// pool = new PoolWorkArticleView(this, parserOptions, oldid, useParserCache); +// pool.execute(); +// +// return pool.getParserOutput(); +// } +// +// /** +// * Do standard deferred updates after page view (existing or missing page) +// * @param User user The relevant user +// * @param int oldid Revision id being viewed; if not given or 0, latest revision is assumed +// */ +// public function doViewUpdates(User user, oldid = 0) { +// if (wfReadOnly()) { +// return; +// } +// +// // Update newtalk / watchlist notification status; +// // Avoid outage if the master is not reachable by using a deferred updated +// DeferredUpdates::addCallableUpdate( +// function () use (user, oldid) { +// Hooks::run('PageViewUpdates', [ this, user ]); +// +// user.clearNotification(this.mTitle, oldid); +// }, +// DeferredUpdates::PRESEND +// ); +// } +// +// /** +// * Perform the actions of a page purging +// * @return boolean +// * @note In 1.28 (and only 1.28), this took a flags parameter that +// * controlled how much purging was done. +// */ +// public function doPurge() { +// // Avoid PHP 7.1 warning of passing this by reference +// wikiPage = this; +// +// if (!Hooks::run('ArticlePurge', [ &wikiPage ])) { +// return false; +// } +// +// this.mTitle.invalidateCache(); +// +// // Clear file cache +// HTMLFileCache::clearFileCache(this.getTitle()); +// // Send purge after above page_touched update was committed +// DeferredUpdates::addUpdate( +// new CdnCacheUpdate(this.mTitle.getCdnUrls()), +// DeferredUpdates::PRESEND +// ); +// +// if (this.mTitle.getNamespace() == NS_MEDIAWIKI) { +// messageCache = MessageCache::singleton(); +// messageCache.updateMessageOverride(this.mTitle, this.getContent()); +// } +// +// return true; +// } +// +// /** +// * Insert a new empty page record for this article. +// * This *must* be followed up by creating a revision +// * and running this.updateRevisionOn(...); +// * or else the record will be left in a funky state. +// * Best if all done inside a transaction. +// * +// * @todo Factor out into a PageStore service, to be used by PageUpdater. +// * +// * @param IDatabase dbw +// * @param int|null pageId Custom page ID that will be used for the insert statement +// * +// * @return boolean|int The newly created page_id key; false if the row was not +// * inserted, e.g. because the title already existed or because the specified +// * page ID is already in use. +// */ +// public function insertOn(dbw, pageId = null) { +// pageIdForInsert = pageId ? [ 'page_id' => pageId ] : []; +// dbw.insert( +// 'page', +// [ +// 'page_namespace' => this.mTitle.getNamespace(), +// 'page_title' => this.mTitle.getDBkey(), +// 'page_restrictions' => '', +// 'page_is_redirect' => 0, // Will set this shortly... +// 'page_is_new' => 1, +// 'page_random' => wfRandom(), +// 'page_touched' => dbw.timestamp(), +// 'page_latest' => 0, // Fill this in shortly... +// 'page_len' => 0, // Fill this in shortly... +// ] + pageIdForInsert, +// __METHOD__, +// 'IGNORE' +// ); +// +// if (dbw.affectedRows() > 0) { +// newid = pageId ? (int)pageId : dbw.insertId(); +// this.mId = newid; +// this.mTitle.resetArticleID(newid); +// +// return newid; +// } else { +// return false; // nothing changed +// } +// } +// +// /** +// * Update the page record to point to a newly saved revision. +// * +// * @todo Factor out into a PageStore service, or move into PageUpdater. +// * +// * @param IDatabase dbw +// * @param Revision revision For ID number, and text used to set +// * length and redirect status fields +// * @param int|null lastRevision If given, will not overwrite the page field +// * when different from the currently set value. +// * Giving 0 indicates the new page flag should be set on. +// * @param boolean|null lastRevIsRedirect If given, will optimize adding and +// * removing rows in redirect table. +// * @return boolean Success; false if the page row was missing or page_latest changed +// */ +// public function updateRevisionOn(dbw, revision, lastRevision = null, +// lastRevIsRedirect = null +// ) { +// global wgContentHandlerUseDB; +// +// // TODO: move into PageUpdater or PageStore +// // NOTE: when doing that, make sure cached fields get reset in doEditContent, +// // and in the compat stub! +// +// // Assertion to try to catch T92046 +// if ((int)revision.getId() === 0) { +// throw new InvalidArgumentException( +// __METHOD__ . ': Revision has ID ' . var_export(revision.getId(), 1) +// ); +// } +// +// content = revision.getContent(); +// len = content ? content.getSize() : 0; +// rt = content ? content.getUltimateRedirectTarget() : null; +// +// conditions = [ 'page_id' => this.getId() ]; +// +// if (!is_null(lastRevision)) { +// // An extra check against threads stepping on each other +// conditions['page_latest'] = lastRevision; +// } +// +// revId = revision.getId(); +// Assert::parameter(revId > 0, 'revision.getId()', 'must be > 0'); +// +// row = [ /* SET */ +// 'page_latest' => revId, +// 'page_touched' => dbw.timestamp(revision.getTimestamp()), +// 'page_is_new' => (lastRevision === 0) ? 1 : 0, +// 'page_is_redirect' => rt !== null ? 1 : 0, +// 'page_len' => len, +// ]; +// +// if (wgContentHandlerUseDB) { +// row['page_content_model'] = revision.getContentModel(); +// } +// +// dbw.update('page', +// row, +// conditions, +// __METHOD__); +// +// result = dbw.affectedRows() > 0; +// if (result) { +// this.updateRedirectOn(dbw, rt, lastRevIsRedirect); +// this.setLastEdit(revision); +// this.mLatest = revision.getId(); +// this.mIsRedirect = (boolean)rt; +// // Update the LinkCache. +// linkCache = MediaWikiServices::getInstance().getLinkCache(); +// linkCache.addGoodLinkObj( +// this.getId(), +// this.mTitle, +// len, +// this.mIsRedirect, +// this.mLatest, +// revision.getContentModel() +// ); +// } +// +// return result; +// } +// +// /** +// * Add row to the redirect table if this is a redirect, remove otherwise. +// * +// * @param IDatabase dbw +// * @param Title|null redirectTitle Title Object pointing to the redirect target, +// * or NULL if this is not a redirect +// * @param null|boolean lastRevIsRedirect If given, will optimize adding and +// * removing rows in redirect table. +// * @return boolean True on success, false on failure +// * @private +// */ +// public function updateRedirectOn(dbw, redirectTitle, lastRevIsRedirect = null) { +// // Always update redirects (target link might have changed) +// // Update/Insert if we don't know if the last revision was a redirect or not +// // Delete if changing from redirect to non-redirect +// isRedirect = !is_null(redirectTitle); +// +// if (!isRedirect && lastRevIsRedirect === false) { +// return true; +// } +// +// if (isRedirect) { +// this.insertRedirectEntry(redirectTitle); +// } else { +// // This is not a redirect, remove row from redirect table +// where = [ 'rd_from' => this.getId() ]; +// dbw.delete('redirect', where, __METHOD__); +// } +// +// if (this.getTitle().getNamespace() == NS_FILE) { +// RepoGroup::singleton().getLocalRepo().invalidateImageRedirect(this.getTitle()); +// } +// +// return (dbw.affectedRows() != 0); +// } +// +// /** +// * If the given revision is newer than the currently set page_latest, +// * update the page record. Otherwise, do nothing. +// * +// * @deprecated since 1.24, use updateRevisionOn instead +// * +// * @param IDatabase dbw +// * @param Revision revision +// * @return boolean +// */ +// public function updateIfNewerOn(dbw, revision) { +// row = dbw.selectRow( +// [ 'revision', 'page' ], +// [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ], +// [ +// 'page_id' => this.getId(), +// 'page_latest=rev_id' ], +// __METHOD__); +// +// if (row) { +// if (wfTimestamp(TS_MW, row.rev_timestamp) >= revision.getTimestamp()) { +// return false; +// } +// prev = row.rev_id; +// lastRevIsRedirect = (boolean)row.page_is_redirect; +// } else { +// // No or missing previous revision; mark the page as new +// prev = 0; +// lastRevIsRedirect = null; +// } +// +// ret = this.updateRevisionOn(dbw, revision, prev, lastRevIsRedirect); +// +// return ret; +// } +// +// /** +// * Helper method for checking whether two revisions have differences that go +// * beyond the main slot. +// * +// * MCR migration note: this method should go away! +// * +// * @deprecated Use only as a stop-gap before refactoring to support MCR. +// * +// * @param Revision a +// * @param Revision b +// * @return boolean +// */ +// public static function hasDifferencesOutsideMainSlot(Revision a, Revision b) { +// aSlots = a.getRevisionRecord().getSlots(); +// bSlots = b.getRevisionRecord().getSlots(); +// changedRoles = aSlots.getRolesWithDifferentContent(bSlots); +// +// return (changedRoles !== [ SlotRecord::MAIN ] && changedRoles !== []); +// } +// +// /** +// * Get the content that needs to be saved in order to undo all revisions +// * between undo and undoafter. Revisions must belong to the same page, +// * must exist and must not be deleted +// * +// * @param Revision undo +// * @param Revision undoafter Must be an earlier revision than undo +// * @return Content|boolean Content on success, false on failure +// * @since 1.21 +// * Before we had the Content Object, this was done in getUndoText +// */ +// public function getUndoContent(Revision undo, Revision undoafter) { +// // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate +// +// if (self::hasDifferencesOutsideMainSlot(undo, undoafter)) { +// // Cannot yet undo edits that involve anything other the main slot. +// return false; +// } +// +// handler = undo.getContentHandler(); +// return handler.getUndoContent(this.getRevision(), undo, undoafter); +// } +// +// /** +// * Returns true if this page's content model supports sections. +// * +// * @return boolean +// * +// * @todo The skin should check this and not offer section functionality if +// * sections are not supported. +// * @todo The EditPage should check this and not offer section functionality +// * if sections are not supported. +// */ +// public function supportsSections() { +// return this.getContentHandler().supportsSections(); +// } +// +// /** +// * @param String|int|null|boolean sectionId Section identifier as a number or String +// * (e.g. 0, 1 or 'T-1'), null/false or an empty String for the whole page +// * or 'new' for a new section. +// * @param Content sectionContent New content of the section. +// * @param String sectionTitle New section's subject, only if section is "new". +// * @param String edittime Revision timestamp or null to use the current revision. +// * +// * @throws MWException +// * @return Content|null New complete article content, or null if error. +// * +// * @since 1.21 +// * @deprecated since 1.24, use replaceSectionAtRev instead +// */ +// public function replaceSectionContent( +// sectionId, Content sectionContent, sectionTitle = '', edittime = null +// ) { +// baseRevId = null; +// if (edittime && sectionId !== 'new') { +// lb = this.getDBLoadBalancer(); +// dbr = lb.getConnection(DB_REPLICA); +// rev = Revision::loadFromTimestamp(dbr, this.mTitle, edittime); +// // Try the master if this thread may have just added it. +// // This could be abstracted into a Revision method, but we don't want +// // to encourage loading of revisions by timestamp. +// if (!rev +// && lb.getServerCount() > 1 +// && lb.hasOrMadeRecentMasterChanges() +// ) { +// dbw = lb.getConnection(DB_MASTER); +// rev = Revision::loadFromTimestamp(dbw, this.mTitle, edittime); +// } +// if (rev) { +// baseRevId = rev.getId(); +// } +// } +// +// return this.replaceSectionAtRev(sectionId, sectionContent, sectionTitle, baseRevId); +// } +// +// /** +// * @param String|int|null|boolean sectionId Section identifier as a number or String +// * (e.g. 0, 1 or 'T-1'), null/false or an empty String for the whole page +// * or 'new' for a new section. +// * @param Content sectionContent New content of the section. +// * @param String sectionTitle New section's subject, only if section is "new". +// * @param int|null baseRevId +// * +// * @throws MWException +// * @return Content|null New complete article content, or null if error. +// * +// * @since 1.24 +// */ +// public function replaceSectionAtRev(sectionId, Content sectionContent, +// sectionTitle = '', baseRevId = null +// ) { +// if (strval(sectionId) === '') { +// // Whole-page edit; let the whole text through +// newContent = sectionContent; +// } else { +// if (!this.supportsSections()) { +// throw new MWException("sections not supported for content model " . +// this.getContentHandler().getModelID()); +// } +// +// // T32711: always use current version when adding a new section +// if (is_null(baseRevId) || sectionId === 'new') { +// oldContent = this.getContent(); +// } else { +// rev = Revision::newFromId(baseRevId); +// if (!rev) { +// wfDebug(__METHOD__ . " asked for bogus section (page: " . +// this.getId() . "; section: sectionId)\n"); +// return null; +// } +// +// oldContent = rev.getContent(); +// } +// +// if (!oldContent) { +// wfDebug(__METHOD__ . ": no page text\n"); +// return null; +// } +// +// newContent = oldContent.replaceSection(sectionId, sectionContent, sectionTitle); +// } +// +// return newContent; +// } +// +// /** +// * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed. +// * +// * @deprecated since 1.32, use exists() instead, or simply omit the EDIT_UPDATE +// * and EDIT_NEW flags. To protect against race conditions, use PageUpdater::grabParentRevision. +// * +// * @param int flags +// * @return int Updated flags +// */ +// public function checkFlags(flags) { +// if (!(flags & EDIT_NEW) && !(flags & EDIT_UPDATE)) { +// if (this.exists()) { +// flags |= EDIT_UPDATE; +// } else { +// flags |= EDIT_NEW; +// } +// } +// +// return flags; +// } +// +// /** +// * @return DerivedPageDataUpdater +// */ +// private function newDerivedDataUpdater() { +// global wgRCWatchCategoryMembership, wgArticleCountMethod; +// +// derivedDataUpdater = new DerivedPageDataUpdater( +// this, // NOTE: eventually, PageUpdater should not know about WikiPage +// this.getRevisionStore(), +// this.getRevisionRenderer(), +// this.getSlotRoleRegistry(), +// this.getParserCache(), +// JobQueueGroup::singleton(), +// MessageCache::singleton(), +// MediaWikiServices::getInstance().getContentLanguage(), +// MediaWikiServices::getInstance().getDBLoadBalancerFactory() +// ); +// +// derivedDataUpdater.setRcWatchCategoryMembership(wgRCWatchCategoryMembership); +// derivedDataUpdater.setArticleCountMethod(wgArticleCountMethod); +// +// return derivedDataUpdater; +// } +// +// /** +// * Returns a DerivedPageDataUpdater for use with the given target revision or new content. +// * This method attempts to re-use the same DerivedPageDataUpdater instance for subsequent calls. +// * The parameters passed to this method are used to ensure that the DerivedPageDataUpdater +// * returned matches that caller's expectations, allowing an existing instance to be re-used +// * if the given parameters match that instance's @gplx.Internal protected state according to +// * DerivedPageDataUpdater::isReusableFor(), and creating a new instance of the parameters do not +// * match the existign one. +// * +// * If neither forRevision nor forUpdate is given, a new DerivedPageDataUpdater is always +// * created, replacing any DerivedPageDataUpdater currently cached. +// * +// * MCR migration note: this replaces WikiPage::prepareContentForEdit. +// * +// * @since 1.32 +// * +// * @param User|null forUser The user that will be used for, or was used for, PST. +// * @param RevisionRecord|null forRevision The revision created by the edit for which +// * to perform updates, if the edit was already saved. +// * @param RevisionSlotsUpdate|null forUpdate The new content to be saved by the edit (pre PST), +// * if the edit was not yet saved. +// * @param boolean forEdit Only re-use if the cached DerivedPageDataUpdater has the current +// * revision as the edit's parent revision. This ensures that the same +// * DerivedPageDataUpdater cannot be re-used for two consecutive edits. +// * +// * @return DerivedPageDataUpdater +// */ +// private function getDerivedDataUpdater( +// User forUser = null, +// RevisionRecord forRevision = null, +// RevisionSlotsUpdate forUpdate = null, +// forEdit = false +// ) { +// if (!forRevision && !forUpdate) { +// // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is +// // going to use it with. +// this.derivedDataUpdater = null; +// } +// +// if (this.derivedDataUpdater && !this.derivedDataUpdater.isContentPrepared()) { +// // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference +// // to it did not yet initialize it, because we don't know what data it will be +// // initialized with. +// this.derivedDataUpdater = null; +// } +// +// // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance. +// // However, there is no good way to construct a cache key. We'd need to check against all +// // cached instances. +// +// if (this.derivedDataUpdater +// && !this.derivedDataUpdater.isReusableFor( +// forUser, +// forRevision, +// forUpdate, +// forEdit ? this.getLatest() : null +// ) +// ) { +// this.derivedDataUpdater = null; +// } +// +// if (!this.derivedDataUpdater) { +// this.derivedDataUpdater = this.newDerivedDataUpdater(); +// } +// +// return this.derivedDataUpdater; +// } +// +// /** +// * Returns a PageUpdater for creating new revisions on this page (or creating the page). +// * +// * The PageUpdater can also be used to detect the need for edit conflict resolution, +// * and to protected such conflict resolution from concurrent edits using a check-and-set +// * mechanism. +// * +// * @since 1.32 +// * +// * @param User user +// * @param RevisionSlotsUpdate|null forUpdate If given, allows any cached ParserOutput +// * that may already have been returned via getDerivedDataUpdater to be re-used. +// * +// * @return PageUpdater +// */ +// public function newPageUpdater(User user, RevisionSlotsUpdate forUpdate = null) { +// global wgAjaxEditStash, wgUseAutomaticEditSummaries, wgPageCreationLog; +// +// pageUpdater = new PageUpdater( +// user, +// this, // NOTE: eventually, PageUpdater should not know about WikiPage +// this.getDerivedDataUpdater(user, null, forUpdate, true), +// this.getDBLoadBalancer(), +// this.getRevisionStore(), +// this.getSlotRoleRegistry() +// ); +// +// pageUpdater.setUsePageCreationLog(wgPageCreationLog); +// pageUpdater.setAjaxEditStash(wgAjaxEditStash); +// pageUpdater.setUseAutomaticEditSummaries(wgUseAutomaticEditSummaries); +// +// return pageUpdater; +// } +// +// /** +// * Change an existing article or create a new article. Updates RC and all necessary caches, +// * optionally via the deferred update array. +// * +// * @deprecated since 1.32, use PageUpdater::saveRevision instead. Note that the new method +// * expects callers to take care of checking EDIT_MINOR against the minoredit right, and to +// * apply the autopatrol right as appropriate. +// * +// * @param Content content New content +// * @param String|CommentStoreComment summary Edit summary +// * @param int flags Bitfield: +// * EDIT_NEW +// * Article is known or assumed to be non-existent, create a new one +// * EDIT_UPDATE +// * Article is known or assumed to be pre-existing, update it +// * EDIT_MINOR +// * Mark this edit minor, if the user is allowed to do so +// * EDIT_SUPPRESS_RC +// * Do not log the change in recentchanges +// * EDIT_FORCE_BOT +// * Mark the edit a "bot" edit regardless of user rights +// * EDIT_AUTOSUMMARY +// * Fill in blank summaries with generated text where possible +// * EDIT_INTERNAL +// * Signal that the page retrieve/save cycle happened entirely in this request. +// * +// * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the +// * article will be detected. If EDIT_UPDATE is specified and the article +// * doesn't exist, the function will return an edit-gone-missing error. If +// * EDIT_NEW is specified and the article does exist, an edit-already-exists +// * error will be returned. These two conditions are also possible with +// * auto-detection due to MediaWiki's performance-optimised locking strategy. +// * +// * @param boolean|int originalRevId: The ID of an original revision that the edit +// * restores or repeats. The new revision is expected to have the exact same content as +// * the given original revision. This is used with rollbacks and with dummy "null" revisions +// * which are created to record things like page moves. +// * @param User|null user The user doing the edit +// * @param String|null serialFormat IGNORED. +// * @param array|null tags Change tags to apply to this edit +// * Callers are responsible for permission checks +// * (with ChangeTags::canAddTagsAccompanyingChange) +// * @param Int undidRevId Id of revision that was undone or 0 +// * +// * @throws MWException +// * @return Status Possible errors: +// * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't +// * set the fatal flag of status. +// * edit-gone-missing: In update mode, but the article didn't exist. +// * edit-conflict: In update mode, the article changed unexpectedly. +// * edit-no-change: Warning that the text was the same as before. +// * edit-already-exists: In creation mode, but the article already exists. +// * +// * Extensions may define additional errors. +// * +// * return.value will contain an associative array with members as follows: +// * new: Boolean indicating if the function attempted to create a new article. +// * revision: The revision Object for the inserted revision, or null. +// * +// * @since 1.21 +// * @throws MWException +// */ +// public function doEditContent( +// Content content, summary, flags = 0, originalRevId = false, +// User user = null, serialFormat = null, tags = [], undidRevId = 0 +// ) { +// global wgUser, wgUseNPPatrol, wgUseRCPatrol; +// +// if (!(summary instanceof CommentStoreComment)) { +// summary = CommentStoreComment::newUnsavedComment(trim(summary)); +// } +// +// if (!user) { +// user = wgUser; +// } +// +// // TODO: this check is here for backwards-compatibility with 1.31 behavior. +// // Checking the minoredit right should be done in the same place the 'bot' right is +// // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave. +// if ((flags & EDIT_MINOR) && !user.isAllowed('minoredit')) { +// flags = (flags & ~EDIT_MINOR); +// } +// +// slotsUpdate = new RevisionSlotsUpdate(); +// slotsUpdate.modifyContent(SlotRecord::MAIN, content); +// +// // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and +// // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also +// // used by this PageUpdater. However, there is no guarantee for this. +// updater = this.newPageUpdater(user, slotsUpdate); +// updater.setContent(SlotRecord::MAIN, content); +// updater.setOriginalRevisionId(originalRevId); +// updater.setUndidRevisionId(undidRevId); +// +// needsPatrol = wgUseRCPatrol || (wgUseNPPatrol && !this.exists()); +// +// // TODO: this logic should not be in the storage layer, it's here for compatibility +// // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same +// // place the 'bot' right is handled, which is currently in EditPage::attemptSave. +// if (needsPatrol && this.getTitle().userCan('autopatrol', user)) { +// updater.setRcPatrolStatus(RecentChange::PRC_AUTOPATROLLED); +// } +// +// updater.addTags(tags); +// +// revRec = updater.saveRevision( +// summary, +// flags +// ); +// +// // revRec will be null if the edit failed, or if no new revision was created because +// // the content did not change. +// if (revRec) { +// // update cached fields +// // TODO: this is currently redundant to what is done in updateRevisionOn. +// // But updateRevisionOn() should move into PageStore, and then this will be needed. +// this.setLastEdit(new Revision(revRec)); // TODO: use RevisionRecord +// this.mLatest = revRec.getId(); +// } +// +// return updater.getStatus(); +// } +// +// /** +// * Get parser options suitable for rendering the primary article wikitext +// * +// * @see ParserOptions::newCanonical +// * +// * @param IContextSource|User|String context One of the following: +// * - IContextSource: Use the User and the Language of the provided +// * context +// * - User: Use the provided User Object and wgLang for the language, +// * so use an IContextSource Object if possible. +// * - 'canonical': Canonical options (anonymous user with default +// * preferences and content language). +// * @return ParserOptions +// */ +// public function makeParserOptions(context) { +// options = ParserOptions::newCanonical(context); +// +// if (this.getTitle().isConversionTable()) { +// // @todo ConversionTable should become a separate content model, so +// // we don't need special cases like this one. +// options.disableContentConversion(); +// } +// +// return options; +// } +// +// /** +// * Prepare content which is about to be saved. +// * +// * Prior to 1.30, this returned a stdClass. +// * +// * @deprecated since 1.32, use getDerivedDataUpdater instead. +// * +// * @param Content content +// * @param Revision|RevisionRecord|int|null revision Revision Object. +// * For backwards compatibility, a revision ID is also accepted, +// * but this is deprecated. +// * Used with vary-revision or vary-revision-id. +// * @param User|null user +// * @param String|null serialFormat IGNORED +// * @param boolean useCache Check shared prepared edit cache +// * +// * @return PreparedEdit +// * +// * @since 1.21 +// */ +// public function prepareContentForEdit( +// Content content, +// revision = null, +// User user = null, +// serialFormat = null, +// useCache = true +// ) { +// global wgUser; +// +// if (!user) { +// user = wgUser; +// } +// +// if (!is_object(revision)) { +// revid = revision; +// // This code path is deprecated, and nothing is known to +// // use it, so performance here shouldn't be a worry. +// if (revid !== null) { +// wfDeprecated(__METHOD__ . ' with revision = revision ID', '1.25'); +// store = this.getRevisionStore(); +// revision = store.getRevisionById(revid, Revision::READ_LATEST); +// } else { +// revision = null; +// } +// } elseif (revision instanceof Revision) { +// revision = revision.getRevisionRecord(); +// } +// +// slots = RevisionSlotsUpdate::newFromContent([ SlotRecord::MAIN => content ]); +// updater = this.getDerivedDataUpdater(user, revision, slots); +// +// if (!updater.isUpdatePrepared()) { +// updater.prepareContent(user, slots, useCache); +// +// if (revision) { +// updater.prepareUpdate( +// revision, +// [ +// 'causeAction' => 'prepare-edit', +// 'causeAgent' => user.getName(), +// ] +// ); +// } +// } +// +// return updater.getPreparedEdit(); +// } +// +// /** +// * Do standard deferred updates after page edit. +// * Update links tables, site stats, search index and message cache. +// * Purges pages that include this page if the text was changed here. +// * Every 100th edit, prune the recent changes table. +// * +// * @deprecated since 1.32, use PageUpdater::doUpdates instead. +// * +// * @param Revision revision +// * @param User user User Object that did the revision +// * @param array options Array of options, following indexes are used: +// * - changed: boolean, whether the revision changed the content (default true) +// * - created: boolean, whether the revision created the page (default false) +// * - moved: boolean, whether the page was moved (default false) +// * - restored: boolean, whether the page was undeleted (default false) +// * - oldrevision: Revision Object for the pre-update revision (default null) +// * - oldcountable: boolean, null, or String 'no-change' (default null): +// * - boolean: whether the page was counted as an article before that +// * revision, only used in changed is true and created is false +// * - null: if created is false, don't update the article count; if created +// * is true, do update the article count +// * - 'no-change': don't update the article count, ever +// * - causeAction: an arbitrary String identifying the reason for the update. +// * See DataUpdate::getCauseAction(). (default 'edit-page') +// * - causeAgent: name of the user who caused the update. See DataUpdate::getCauseAgent(). +// * (String, defaults to the passed user) +// */ +// public function doEditUpdates(Revision revision, User user, array options = []) { +// options += [ +// 'causeAction' => 'edit-page', +// 'causeAgent' => user.getName(), +// ]; +// +// revision = revision.getRevisionRecord(); +// +// updater = this.getDerivedDataUpdater(user, revision); +// +// updater.prepareUpdate(revision, options); +// +// updater.doUpdates(); +// } +// +// /** +// * Update the parser cache. +// * +// * @note This is a temporary workaround until there is a proper data updater class. +// * It will become deprecated soon. +// * +// * @param array options +// * - causeAction: an arbitrary String identifying the reason for the update. +// * See DataUpdate::getCauseAction(). (default 'edit-page') +// * - causeAgent: name of the user who caused the update (String, defaults to the +// * user who created the revision) +// * @since 1.32 +// */ +// public function updateParserCache(array options = []) { +// revision = this.getRevisionRecord(); +// if (!revision || !revision.getId()) { +// LoggerFactory::getInstance('wikipage').info( +// __METHOD__ . 'called with ' . (revision ? 'unsaved' : 'no') . ' revision' +// ); +// return; +// } +// user = User::newFromIdentity(revision.getUser(RevisionRecord::RAW)); +// +// updater = this.getDerivedDataUpdater(user, revision); +// updater.prepareUpdate(revision, options); +// updater.doParserCacheUpdate(); +// } +// +// /** +// * Do secondary data updates (such as updating link tables). +// * Secondary data updates are only a small part of the updates needed after saving +// * a new revision; normally PageUpdater::doUpdates should be used instead (which includes +// * secondary data updates). This method is provided for partial purges. +// * +// * @note This is a temporary workaround until there is a proper data updater class. +// * It will become deprecated soon. +// * +// * @param array options +// * - recursive (boolean, default true): whether to do a recursive update (update pages that +// * depend on this page, e.g. transclude it). This will set the recursive parameter of +// * Content::getSecondaryDataUpdates. Typically this should be true unless the update +// * was something that did not really change the page, such as a null edit. +// * - triggeringUser: The user triggering the update (UserIdentity, defaults to the +// * user who created the revision) +// * - causeAction: an arbitrary String identifying the reason for the update. +// * See DataUpdate::getCauseAction(). (default 'unknown') +// * - causeAgent: name of the user who caused the update (String, default 'unknown') +// * - defer: one of the DeferredUpdates constants, or false to run immediately (default: false). +// * Note that even when this is set to false, some updates might still get deferred (as +// * some update might directly add child updates to DeferredUpdates). +// * - transactionTicket: a transaction ticket from LBFactory::getEmptyTransactionTicket(), +// * only when defer is false (default: null) +// * @since 1.32 +// */ +// public function doSecondaryDataUpdates(array options = []) { +// options['recursive'] = options['recursive'] ?? true; +// revision = this.getRevisionRecord(); +// if (!revision || !revision.getId()) { +// LoggerFactory::getInstance('wikipage').info( +// __METHOD__ . 'called with ' . (revision ? 'unsaved' : 'no') . ' revision' +// ); +// return; +// } +// user = User::newFromIdentity(revision.getUser(RevisionRecord::RAW)); +// +// updater = this.getDerivedDataUpdater(user, revision); +// updater.prepareUpdate(revision, options); +// updater.doSecondaryDataUpdates(options); +// } +// +// /** +// * Update the article's restriction field, and leave a log entry. +// * This works for protection both existing and non-existing pages. +// * +// * @param array limit Set of restriction keys +// * @param array expiry Per restriction type expiration +// * @param int &cascade Set to false if cascading protection isn't allowed. +// * @param String reason +// * @param User user The user updating the restrictions +// * @param String|String[]|null tags Change tags to add to the pages and protection log entries +// * (user should be able to add the specified tags before this is called) +// * @return Status Status Object; if action is taken, status.value is the log_id of the +// * protection log entry. +// */ +// public function doUpdateRestrictions(array limit, array expiry, +// &cascade, reason, User user, tags = null +// ) { +// global wgCascadingRestrictionLevels; +// +// if (wfReadOnly()) { +// return Status::newFatal(wfMessage('readonlytext', wfReadOnlyReason())); +// } +// +// this.loadPageData('fromdbmaster'); +// this.mTitle.loadRestrictions(null, Title::READ_LATEST); +// restrictionTypes = this.mTitle.getRestrictionTypes(); +// id = this.getId(); +// +// if (!cascade) { +// cascade = false; +// } +// +// // Take this opportunity to purge out expired restrictions +// Title::purgeExpiredRestrictions(); +// +// // @todo: Same limitations as described in ProtectionForm.php (line 37); +// // we expect a single selection, but the schema allows otherwise. +// isProtected = false; +// protect = false; +// changed = false; +// +// dbw = wfGetDB(DB_MASTER); +// +// foreach (restrictionTypes as action) { +// if (!isset(expiry[action]) || expiry[action] === dbw.getInfinity()) { +// expiry[action] = 'infinity'; +// } +// if (!isset(limit[action])) { +// limit[action] = ''; +// } elseif (limit[action] != '') { +// protect = true; +// } +// +// // Get current restrictions on action +// current = implode('', this.mTitle.getRestrictions(action)); +// if (current != '') { +// isProtected = true; +// } +// +// if (limit[action] != current) { +// changed = true; +// } elseif (limit[action] != '') { +// // Only check expiry change if the action is actually being +// // protected, since expiry does nothing on an not-protected +// // action. +// if (this.mTitle.getRestrictionExpiry(action) != expiry[action]) { +// changed = true; +// } +// } +// } +// +// if (!changed && protect && this.mTitle.areRestrictionsCascading() != cascade) { +// changed = true; +// } +// +// // If nothing has changed, do nothing +// if (!changed) { +// return Status::newGood(); +// } +// +// if (!protect) { // No protection at all means unprotection +// revCommentMsg = 'unprotectedarticle-comment'; +// logAction = 'unprotect'; +// } elseif (isProtected) { +// revCommentMsg = 'modifiedarticleprotection-comment'; +// logAction = 'modify'; +// } else { +// revCommentMsg = 'protectedarticle-comment'; +// logAction = 'protect'; +// } +// +// logRelationsValues = []; +// logRelationsField = null; +// logParamsDetails = []; +// +// // Null revision (used for change tag insertion) +// nullRevision = null; +// +// if (id) { // Protection of existing page +// // Avoid PHP 7.1 warning of passing this by reference +// wikiPage = this; +// +// if (!Hooks::run('ArticleProtect', [ &wikiPage, &user, limit, reason ])) { +// return Status::newGood(); +// } +// +// // Only certain restrictions can cascade... +// editrestriction = isset(limit['edit']) +// ? [ limit['edit'] ] +// : this.mTitle.getRestrictions('edit'); +// foreach (array_keys(editrestriction, 'sysop') as key) { +// editrestriction[key] = 'editprotected'; // backwards compatibility +// } +// foreach (array_keys(editrestriction, 'autoconfirmed') as key) { +// editrestriction[key] = 'editsemiprotected'; // backwards compatibility +// } +// +// cascadingRestrictionLevels = wgCascadingRestrictionLevels; +// foreach (array_keys(cascadingRestrictionLevels, 'sysop') as key) { +// cascadingRestrictionLevels[key] = 'editprotected'; // backwards compatibility +// } +// foreach (array_keys(cascadingRestrictionLevels, 'autoconfirmed') as key) { +// cascadingRestrictionLevels[key] = 'editsemiprotected'; // backwards compatibility +// } +// +// // The schema allows multiple restrictions +// if (!array_intersect(editrestriction, cascadingRestrictionLevels)) { +// cascade = false; +// } +// +// // insert null revision to identify the page protection change as edit summary +// latest = this.getLatest(); +// nullRevision = this.insertProtectNullRevision( +// revCommentMsg, +// limit, +// expiry, +// cascade, +// reason, +// user +// ); +// +// if (nullRevision === null) { +// return Status::newFatal('no-null-revision', this.mTitle.getPrefixedText()); +// } +// +// logRelationsField = 'pr_id'; +// +// // Update restrictions table +// foreach (limit as action => restrictions) { +// dbw.delete( +// 'page_restrictions', +// [ +// 'pr_page' => id, +// 'pr_type' => action +// ], +// __METHOD__ +// ); +// if (restrictions != '') { +// cascadeValue = (cascade && action == 'edit') ? 1 : 0; +// dbw.insert( +// 'page_restrictions', +// [ +// 'pr_page' => id, +// 'pr_type' => action, +// 'pr_level' => restrictions, +// 'pr_cascade' => cascadeValue, +// 'pr_expiry' => dbw.encodeExpiry(expiry[action]) +// ], +// __METHOD__ +// ); +// logRelationsValues[] = dbw.insertId(); +// logParamsDetails[] = [ +// 'type' => action, +// 'level' => restrictions, +// 'expiry' => expiry[action], +// 'cascade' => (boolean)cascadeValue, +// ]; +// } +// } +// +// // Clear out legacy restriction fields +// dbw.update( +// 'page', +// [ 'page_restrictions' => '' ], +// [ 'page_id' => id ], +// __METHOD__ +// ); +// +// // Avoid PHP 7.1 warning of passing this by reference +// wikiPage = this; +// +// Hooks::run('NewRevisionFromEditComplete', +// [ this, nullRevision, latest, user ]); +// Hooks::run('ArticleProtectComplete', [ &wikiPage, &user, limit, reason ]); +// } else { // Protection of non-existing page (also known as "title protection") +// // Cascade protection is meaningless in this case +// cascade = false; +// +// if (limit['create'] != '') { +// commentFields = CommentStore::getStore().insert(dbw, 'pt_reason', reason); +// dbw.replace('protected_titles', +// [ [ 'pt_namespace', 'pt_title' ] ], +// [ +// 'pt_namespace' => this.mTitle.getNamespace(), +// 'pt_title' => this.mTitle.getDBkey(), +// 'pt_create_perm' => limit['create'], +// 'pt_timestamp' => dbw.timestamp(), +// 'pt_expiry' => dbw.encodeExpiry(expiry['create']), +// 'pt_user' => user.getId(), +// ] + commentFields, __METHOD__ +// ); +// logParamsDetails[] = [ +// 'type' => 'create', +// 'level' => limit['create'], +// 'expiry' => expiry['create'], +// ]; +// } else { +// dbw.delete('protected_titles', +// [ +// 'pt_namespace' => this.mTitle.getNamespace(), +// 'pt_title' => this.mTitle.getDBkey() +// ], __METHOD__ +// ); +// } +// } +// +// this.mTitle.flushRestrictions(); +// InfoAction::invalidateCache(this.mTitle); +// +// if (logAction == 'unprotect') { +// params = []; +// } else { +// protectDescriptionLog = this.protectDescriptionLog(limit, expiry); +// params = [ +// '4::description' => protectDescriptionLog, // parameter for IRC +// '5:boolean:cascade' => cascade, +// 'details' => logParamsDetails, // parameter for localize and api +// ]; +// } +// +// // Update the protection log +// logEntry = new ManualLogEntry('protect', logAction); +// logEntry.setTarget(this.mTitle); +// logEntry.setComment(reason); +// logEntry.setPerformer(user); +// logEntry.setParameters(params); +// if (!is_null(nullRevision)) { +// logEntry.setAssociatedRevId(nullRevision.getId()); +// } +// logEntry.setTags(tags); +// if (logRelationsField !== null && count(logRelationsValues)) { +// logEntry.setRelations([ logRelationsField => logRelationsValues ]); +// } +// logId = logEntry.insert(); +// logEntry.publish(logId); +// +// return Status::newGood(logId); +// } +// +// /** +// * Insert a new null revision for this page. +// * +// * @param String revCommentMsg Comment message key for the revision +// * @param array limit Set of restriction keys +// * @param array expiry Per restriction type expiration +// * @param int cascade Set to false if cascading protection isn't allowed. +// * @param String reason +// * @param User|null user +// * @return Revision|null Null on error +// */ +// public function insertProtectNullRevision(revCommentMsg, array limit, +// array expiry, cascade, reason, user = null +// ) { +// dbw = wfGetDB(DB_MASTER); +// +// // Prepare a null revision to be added to the history +// editComment = wfMessage( +// revCommentMsg, +// this.mTitle.getPrefixedText(), +// user ? user.getName() : '' +// ).inContentLanguage().text(); +// if (reason) { +// editComment .= wfMessage('colon-separator').inContentLanguage().text() . reason; +// } +// protectDescription = this.protectDescription(limit, expiry); +// if (protectDescription) { +// editComment .= wfMessage('word-separator').inContentLanguage().text(); +// editComment .= wfMessage('parentheses').params(protectDescription) +// .inContentLanguage().text(); +// } +// if (cascade) { +// editComment .= wfMessage('word-separator').inContentLanguage().text(); +// editComment .= wfMessage('brackets').params( +// wfMessage('protect-summary-cascade').inContentLanguage().text() +// ).inContentLanguage().text(); +// } +// +// nullRev = Revision::newNullRevision(dbw, this.getId(), editComment, true, user); +// if (nullRev) { +// nullRev.insertOn(dbw); +// +// // Update page record and touch page +// oldLatest = nullRev.getParentId(); +// this.updateRevisionOn(dbw, nullRev, oldLatest); +// } +// +// return nullRev; +// } +// +// /** +// * @param String expiry 14-char timestamp or "infinity", or false if the input was invalid +// * @return String +// */ +// protected function formatExpiry(expiry) { +// if (expiry != 'infinity') { +// contLang = MediaWikiServices::getInstance().getContentLanguage(); +// return wfMessage( +// 'protect-expiring', +// contLang.timeanddate(expiry, false, false), +// contLang.date(expiry, false, false), +// contLang.time(expiry, false, false) +// ).inContentLanguage().text(); +// } else { +// return wfMessage('protect-expiry-indefinite') +// .inContentLanguage().text(); +// } +// } +// +// /** +// * Builds the description to serve as comment for the edit. +// * +// * @param array limit Set of restriction keys +// * @param array expiry Per restriction type expiration +// * @return String +// */ +// public function protectDescription(array limit, array expiry) { +// protectDescription = ''; +// +// foreach (array_filter(limit) as action => restrictions) { +// # action is one of wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ]. +// # All possible message keys are listed here for easier grepping: +// # * restriction-create +// # * restriction-edit +// # * restriction-move +// # * restriction-upload +// actionText = wfMessage('restriction-' . action).inContentLanguage().text(); +// # restrictions is one of wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ], +// # with '' filtered out. All possible message keys are listed below: +// # * protect-level-autoconfirmed +// # * protect-level-sysop +// restrictionsText = wfMessage('protect-level-' . restrictions) +// .inContentLanguage().text(); +// +// expiryText = this.formatExpiry(expiry[action]); +// +// if (protectDescription !== '') { +// protectDescription .= wfMessage('word-separator').inContentLanguage().text(); +// } +// protectDescription .= wfMessage('protect-summary-desc') +// .params(actionText, restrictionsText, expiryText) +// .inContentLanguage().text(); +// } +// +// return protectDescription; +// } +// +// /** +// * Builds the description to serve as comment for the log entry. +// * +// * Some bots may parse IRC lines, which are generated from log entries which contain plain +// * protect description text. Keep them in old format to avoid breaking compatibility. +// * TODO: Fix protection log to store structured description and format it on-the-fly. +// * +// * @param array limit Set of restriction keys +// * @param array expiry Per restriction type expiration +// * @return String +// */ +// public function protectDescriptionLog(array limit, array expiry) { +// protectDescriptionLog = ''; +// +// dirMark = MediaWikiServices::getInstance().getContentLanguage().getDirMark(); +// foreach (array_filter(limit) as action => restrictions) { +// expiryText = this.formatExpiry(expiry[action]); +// protectDescriptionLog .= +// dirMark . +// "[action=restrictions] (expiryText)"; +// } +// +// return trim(protectDescriptionLog); +// } +// +// /** +// * Take an array of page restrictions and flatten it to a String +// * suitable for insertion into the page_restrictions field. +// * +// * @param String[] limit +// * +// * @throws MWException +// * @return String +// */ +// protected static function flattenRestrictions(limit) { +// if (!is_array(limit)) { +// throw new MWException(__METHOD__ . ' given non-array restriction set'); +// } +// +// bits = []; +// ksort(limit); +// +// foreach (array_filter(limit) as action => restrictions) { +// bits[] = "action=restrictions"; +// } +// +// return implode(':', bits); +// } +// +// /** +// * Determines if deletion of this page would be batched (executed over time by the job queue) +// * or not (completed in the same request as the delete call). +// * +// * It is unlikely but possible that an edit from another request could push the page over the +// * batching threshold after this function is called, but before the caller acts upon the +// * return value. Callers must decide for themselves how to deal with this. safetyMargin +// * is provided as an unreliable but situationally useful help for some common cases. +// * +// * @param int safetyMargin Added to the revision count when checking for batching +// * @return boolean True if deletion would be batched, false otherwise +// */ +// public function isBatchedDelete(safetyMargin = 0) { +// global wgDeleteRevisionsBatchSize; +// +// dbr = wfGetDB(DB_REPLICA); +// revCount = this.getRevisionStore().countRevisionsByPageId(dbr, this.getId()); +// revCount += safetyMargin; +// +// return revCount >= wgDeleteRevisionsBatchSize; +// } +// +// /** +// * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for +// * backwards compatibility, if you care about error reporting you should use +// * doDeleteArticleReal() instead. +// * +// * Deletes the article with database consistency, writes logs, purges caches +// * +// * @param String reason Delete reason for deletion log +// * @param boolean suppress Suppress all revisions and log the deletion in +// * the suppression log instead of the deletion log +// * @param int|null u1 Unused +// * @param boolean|null u2 Unused +// * @param array|String &error Array of errors to append to +// * @param User|null user The deleting user +// * @param boolean immediate false allows deleting over time via the job queue +// * @return boolean True if successful +// * @throws FatalError +// * @throws MWException +// */ +// public function doDeleteArticle( +// reason, suppress = false, u1 = null, u2 = null, &error = '', User user = null, +// immediate = false +// ) { +// status = this.doDeleteArticleReal(reason, suppress, u1, u2, error, user, +// [], 'delete', immediate); +// +// // Returns true if the page was actually deleted, or is scheduled for deletion +// return status.isOK(); +// } +// +// /** +// * Back-end article deletion +// * Deletes the article with database consistency, writes logs, purges caches +// * +// * @since 1.19 +// * +// * @param String reason Delete reason for deletion log +// * @param boolean suppress Suppress all revisions and log the deletion in +// * the suppression log instead of the deletion log +// * @param int|null u1 Unused +// * @param boolean|null u2 Unused +// * @param array|String &error Array of errors to append to +// * @param User|null deleter The deleting user +// * @param array tags Tags to apply to the deletion action +// * @param String logsubtype +// * @param boolean immediate false allows deleting over time via the job queue +// * @return Status Status Object; if successful, status.value is the log_id of the +// * deletion log entry. If the page couldn't be deleted because it wasn't +// * found, status is a non-fatal 'cannotdelete' error +// * @throws FatalError +// * @throws MWException +// */ +// public function doDeleteArticleReal( +// reason, suppress = false, u1 = null, u2 = null, &error = '', User deleter = null, +// tags = [], logsubtype = 'delete', immediate = false +// ) { +// global wgUser; +// +// wfDebug(__METHOD__ . "\n"); +// +// status = Status::newGood(); +// +// // Avoid PHP 7.1 warning of passing this by reference +// wikiPage = this; +// +// if (!deleter) { +// deleter = wgUser; +// } +// if (!Hooks::run('ArticleDelete', +// [ &wikiPage, &deleter, &reason, &error, &status, suppress ] +// )) { +// if (status.isOK()) { +// // Hook aborted but didn't set a fatal status +// status.fatal('delete-hook-aborted'); +// } +// return status; +// } +// +// return this.doDeleteArticleBatched(reason, suppress, deleter, tags, +// logsubtype, immediate); +// } +// +// /** +// * Back-end article deletion +// * +// * Only invokes batching via the job queue if necessary per wgDeleteRevisionsBatchSize. +// * Deletions can often be completed inline without involving the job queue. +// * +// * Potentially called many times per deletion operation for pages with many revisions. +// */ +// public function doDeleteArticleBatched( +// reason, suppress, User deleter, tags, +// logsubtype, immediate = false, webRequestId = null +// ) { +// wfDebug(__METHOD__ . "\n"); +// +// status = Status::newGood(); +// +// dbw = wfGetDB(DB_MASTER); +// dbw.startAtomic(__METHOD__); +// +// this.loadPageData(self::READ_LATEST); +// id = this.getId(); +// // T98706: synchronized the page from various other updates but avoid using +// // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to +// // the revisions queries (which also JOIN on user). Only synchronized the page +// // row and CAS check on page_latest to see if the trx snapshot matches. +// lockedLatest = this.lockAndGetLatest(); +// if (id == 0 || this.getLatest() != lockedLatest) { +// dbw.endAtomic(__METHOD__); +// // Page not there or trx snapshot is stale +// status.error('cannotdelete', +// wfEscapeWikiText(this.getTitle().getPrefixedText())); +// return status; +// } +// +// // At this point we are now committed to returning an OK +// // status unless some DB query error or other exception comes up. +// // This way callers don't have to call rollback() if status is bad +// // unless they actually try to catch exceptions (which is rare). +// +// // we need to remember the old content so we can use it to generate all deletion updates. +// revision = this.getRevision(); +// try { +// content = this.getContent(Revision::RAW); +// } catch (Exception ex) { +// wfLogWarning(__METHOD__ . ': failed to load content during deletion! ' +// . ex.getMessage()); +// +// content = null; +// } +// +// // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive +// // one batch of revisions and defer archival of any others to the job queue. +// explictTrxLogged = false; +// while (true) { +// done = this.archiveRevisions(dbw, id, suppress); +// if (done || !immediate) { +// break; +// } +// dbw.endAtomic(__METHOD__); +// if (dbw.explicitTrxActive()) { +// // Explict transactions may never happen here in practice. Log to be sure. +// if (!explictTrxLogged) { +// explictTrxLogged = true; +// LoggerFactory::getInstance('wfDebug').debug( +// 'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [ +// 'title' => this.getTitle().getText(), +// ]); +// } +// continue; +// } +// if (dbw.trxLevel()) { +// dbw.commit(); +// } +// lbFactory = MediaWikiServices::getInstance().getDBLoadBalancerFactory(); +// lbFactory.waitForReplication(); +// dbw.startAtomic(__METHOD__); +// } +// +// // If done archiving, also delete the article. +// if (!done) { +// dbw.endAtomic(__METHOD__); +// +// jobParams = [ +// 'wikiPageId' => id, +// 'requestId' => webRequestId ?? WebRequest::getRequestId(), +// 'reason' => reason, +// 'suppress' => suppress, +// 'userId' => deleter.getId(), +// 'tags' => json_encode(tags), +// 'logsubtype' => logsubtype, +// ]; +// +// job = new DeletePageJob(this.getTitle(), jobParams); +// JobQueueGroup::singleton().push(job); +// +// status.warning('delete-scheduled', +// wfEscapeWikiText(this.getTitle().getPrefixedText())); +// } else { +// // Get archivedRevisionCount by db query, because there's no better alternative. +// // Jobs cannot pass a count of archived revisions to the next job, because additional +// // deletion operations can be started while the first is running. Jobs from each +// // gracefully interleave, but would not know about each other's count. Deduplication +// // in the job queue to avoid simultaneous deletion operations would add overhead. +// // Number of archived revisions cannot be known beforehand, because edits can be made +// // while deletion operations are being processed, changing the number of archivals. +// archivedRevisionCount = (int)dbw.selectField( +// 'archive', 'COUNT(*)', +// [ +// 'ar_namespace' => this.getTitle().getNamespace(), +// 'ar_title' => this.getTitle().getDBkey(), +// 'ar_page_id' => id +// ], __METHOD__ +// ); +// +// // Clone the title and wikiPage, so we have the information we need when +// // we log and run the ArticleDeleteComplete hook. +// logTitle = clone this.mTitle; +// wikiPageBeforeDelete = clone this; +// +// // Now that it's safely backed up, delete it +// dbw.delete('page', [ 'page_id' => id ], __METHOD__); +// +// // Log the deletion, if the page was suppressed, put it in the suppression log instead +// logtype = suppress ? 'suppress' : 'delete'; +// +// logEntry = new ManualLogEntry(logtype, logsubtype); +// logEntry.setPerformer(deleter); +// logEntry.setTarget(logTitle); +// logEntry.setComment(reason); +// logEntry.setTags(tags); +// logid = logEntry.insert(); +// +// dbw.onTransactionPreCommitOrIdle( +// function () use (logEntry, logid) { +// // T58776: avoid deadlocks (especially from FileDeleteForm) +// logEntry.publish(logid); +// }, +// __METHOD__ +// ); +// +// dbw.endAtomic(__METHOD__); +// +// this.doDeleteUpdates(id, content, revision, deleter); +// +// Hooks::run('ArticleDeleteComplete', [ +// &wikiPageBeforeDelete, +// &deleter, +// reason, +// id, +// content, +// logEntry, +// archivedRevisionCount +// ]); +// status.value = logid; +// +// // Show log excerpt on 404 pages rather than just a link +// cache = MediaWikiServices::getInstance().getMainObjectStash(); +// key = cache.makeKey('page-recent-delete', md5(logTitle.getPrefixedText())); +// cache.set(key, 1, cache::TTL_DAY); +// } +// +// return status; +// } +// +// /** +// * Archives revisions as part of page deletion. +// * +// * @param IDatabase dbw +// * @param int id +// * @param boolean suppress Suppress all revisions and log the deletion in +// * the suppression log instead of the deletion log +// * @return boolean +// */ +// protected function archiveRevisions(dbw, id, suppress) { +// global wgContentHandlerUseDB, wgMultiContentRevisionSchemaMigrationStage, +// wgActorTableSchemaMigrationStage, wgDeleteRevisionsBatchSize; +// +// // Given the synchronized above, we can be confident in the title and page ID values +// namespace = this.getTitle().getNamespace(); +// dbKey = this.getTitle().getDBkey(); +// +// commentStore = CommentStore::getStore(); +// actorMigration = ActorMigration::newMigration(); +// +// revQuery = Revision::getQueryInfo(); +// bitfield = false; +// +// // Bitfields to further suppress the content +// if (suppress) { +// bitfield = Revision::SUPPRESSED_ALL; +// revQuery['fields'] = array_diff(revQuery['fields'], [ 'rev_deleted' ]); +// } +// +// // For now, shunt the revision data into the archive table. +// // Text is *not* removed from the text table; bulk storage +// // is left intact to avoid breaking block-compression or +// // immutable storage schemes. +// // In the future, we may keep revisions and mark them with +// // the rev_deleted field, which is reserved for this purpose. +// +// // Lock rows in `revision` and its temp tables, but not any others. +// // Note array_intersect() preserves keys from the first arg, and we're +// // assuming revQuery has `revision` primary and isn't using subtables +// // for anything we care about. +// dbw.lockForUpdate( +// array_intersect( +// revQuery['tables'], +// [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ] +// ), +// [ 'rev_page' => id ], +// __METHOD__, +// [], +// revQuery['joins'] +// ); +// +// // If SCHEMA_COMPAT_WRITE_OLD is set, also select all extra fields we still write, +// // so we can copy it to the archive table. +// // We know the fields exist, otherwise SCHEMA_COMPAT_WRITE_OLD could not function. +// if (wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD) { +// revQuery['fields'][] = 'rev_text_id'; +// +// if (wgContentHandlerUseDB) { +// revQuery['fields'][] = 'rev_content_model'; +// revQuery['fields'][] = 'rev_content_format'; +// } +// } +// +// // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the +// // unusual case where there were exactly wgDeleteRevisionBatchSize revisions remaining. +// res = dbw.select( +// revQuery['tables'], +// revQuery['fields'], +// [ 'rev_page' => id ], +// __METHOD__, +// [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => wgDeleteRevisionsBatchSize + 1 ], +// revQuery['joins'] +// ); +// +// // Build their equivalent archive rows +// rowsInsert = []; +// revids = []; +// +// /** @var int[] Revision IDs of edits that were made by IPs */ +// ipRevIds = []; +// +// done = true; +// foreach (res as row) { +// if (count(revids) >= wgDeleteRevisionsBatchSize) { +// done = false; +// break; +// } +// +// comment = commentStore.getComment('rev_comment', row); +// user = User::newFromAnyId(row.rev_user, row.rev_user_text, row.rev_actor); +// rowInsert = [ +// 'ar_namespace' => namespace, +// 'ar_title' => dbKey, +// 'ar_timestamp' => row.rev_timestamp, +// 'ar_minor_edit' => row.rev_minor_edit, +// 'ar_rev_id' => row.rev_id, +// 'ar_parent_id' => row.rev_parent_id, +// /** +// * ar_text_id should probably not be written to when the multi content schema has +// * been migrated to (wgMultiContentRevisionSchemaMigrationStage) however there is no +// * default for the field in WMF production currently so we must keep writing +// * writing until a default of 0 is set. +// * Task: https://phabricator.wikimedia.org/T190148 +// * Copying the value from the revision table should not lead to any issues for now. +// */ +// 'ar_len' => row.rev_len, +// 'ar_page_id' => id, +// 'ar_deleted' => suppress ? bitfield : row.rev_deleted, +// 'ar_sha1' => row.rev_sha1, +// ] + commentStore.insert(dbw, 'ar_comment', comment) +// + actorMigration.getInsertValues(dbw, 'ar_user', user); +// +// if (wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD) { +// rowInsert['ar_text_id'] = row.rev_text_id; +// +// if (wgContentHandlerUseDB) { +// rowInsert['ar_content_model'] = row.rev_content_model; +// rowInsert['ar_content_format'] = row.rev_content_format; +// } +// } +// +// rowsInsert[] = rowInsert; +// revids[] = row.rev_id; +// +// // Keep track of IP edits, so that the corresponding rows can +// // be deleted in the ip_changes table. +// if ((int)row.rev_user === 0 && IP::isValid(row.rev_user_text)) { +// ipRevIds[] = row.rev_id; +// } +// } +// +// // This conditional is just a sanity check +// if (count(revids) > 0) { +// // Copy them into the archive table +// dbw.insert('archive', rowsInsert, __METHOD__); +// +// dbw.delete('revision', [ 'rev_id' => revids ], __METHOD__); +// dbw.delete('revision_comment_temp', [ 'revcomment_rev' => revids ], __METHOD__); +// if (wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW) { +// dbw.delete('revision_actor_temp', [ 'revactor_rev' => revids ], __METHOD__); +// } +// +// // Also delete records from ip_changes as applicable. +// if (count(ipRevIds) > 0) { +// dbw.delete('ip_changes', [ 'ipc_rev_id' => ipRevIds ], __METHOD__); +// } +// } +// +// return done; +// } +// +// /** +// * Lock the page row for this title+id and return page_latest (or 0) +// * +// * @return int Returns 0 if no row was found with this title+id +// * @since 1.27 +// */ +// public function lockAndGetLatest() { +// return (int)wfGetDB(DB_MASTER).selectField( +// 'page', +// 'page_latest', +// [ +// 'page_id' => this.getId(), +// // Typically page_id is enough, but some code might try to do +// // updates assuming the title is the same, so verify that +// 'page_namespace' => this.getTitle().getNamespace(), +// 'page_title' => this.getTitle().getDBkey() +// ], +// __METHOD__, +// [ 'FOR UPDATE' ] +// ); +// } +// +// /** +// * Do some database updates after deletion +// * +// * @param int id The page_id value of the page being deleted +// * @param Content|null content Page content to be used when determining +// * the required updates. This may be needed because this.getContent() +// * may already return null when the page proper was deleted. +// * @param Revision|null revision The current page revision at the time of +// * deletion, used when determining the required updates. This may be needed because +// * this.getRevision() may already return null when the page proper was deleted. +// * @param User|null user The user that caused the deletion +// */ +// public function doDeleteUpdates( +// id, Content content = null, Revision revision = null, User user = null +// ) { +// if (id !== this.getId()) { +// throw new InvalidArgumentException('Mismatching page ID'); +// } +// +// try { +// countable = this.isCountable(); +// } catch (Exception ex) { +// // fallback for deleting broken pages for which we cannot load the content for +// // some reason. Note that doDeleteArticleReal() already logged this problem. +// countable = false; +// } +// +// // Update site status +// DeferredUpdates::addUpdate(SiteStatsUpdate::factory( +// [ 'edits' => 1, 'articles' => -countable, 'pages' => -1 ] +// )); +// +// // Delete pagelinks, update secondary indexes, etc +// updates = this.getDeletionUpdates( +// revision ? revision.getRevisionRecord() : content +// ); +// foreach (updates as update) { +// DeferredUpdates::addUpdate(update); +// } +// +// causeAgent = user ? user.getName() : 'unknown'; +// // Reparse any pages transcluding this page +// LinksUpdate::queueRecursiveJobsForTable( +// this.mTitle, 'templatelinks', 'delete-page', causeAgent); +// // Reparse any pages including this image +// if (this.mTitle.getNamespace() == NS_FILE) { +// LinksUpdate::queueRecursiveJobsForTable( +// this.mTitle, 'imagelinks', 'delete-page', causeAgent); +// } +// +// // Clear caches +// self::onArticleDelete(this.mTitle); +// ResourceLoaderWikiModule::invalidateModuleCache( +// this.mTitle, +// revision, +// null, +// WikiMap::getCurrentWikiDbDomain().getId() +// ); +// +// // Reset this Object and the Title Object +// this.loadFromRow(false, self::READ_LATEST); +// +// // Search engine +// DeferredUpdates::addUpdate(new SearchUpdate(id, this.mTitle)); +// } +// +// /** +// * Roll back the most recent consecutive set of edits to a page +// * from the same user; fails if there are no eligible edits to +// * roll back to, e.g. user is the sole contributor. This function +// * performs permissions checks on user, then calls commitRollback() +// * to do the dirty work +// * +// * @todo Separate the business/permission stuff out from backend code +// * @todo Remove token parameter. Already verified by RollbackAction and ApiRollback. +// * +// * @param String fromP Name of the user whose edits to rollback. +// * @param String summary Custom summary. Set to default summary if empty. +// * @param String token Rollback token. +// * @param boolean bot If true, mark all reverted edits as bot. +// * +// * @param array &resultDetails Array contains result-specific array of additional values +// * 'alreadyrolled' : 'current' (rev) +// * success : 'summary' (str), 'current' (rev), 'target' (rev) +// * +// * @param User user The user performing the rollback +// * @param array|null tags Change tags to apply to the rollback +// * Callers are responsible for permission checks +// * (with ChangeTags::canAddTagsAccompanyingChange) +// * +// * @return array Array of errors, each error formatted as +// * array(messagekey, param1, param2, ...). +// * On success, the array is empty. This array can also be passed to +// * OutputPage::showPermissionsErrorPage(). +// */ +// public function doRollback( +// fromP, summary, token, bot, &resultDetails, User user, tags = null +// ) { +// resultDetails = null; +// +// // Check permissions +// editErrors = this.mTitle.getUserPermissionsErrors('edit', user); +// rollbackErrors = this.mTitle.getUserPermissionsErrors('rollback', user); +// errors = array_merge(editErrors, wfArrayDiff2(rollbackErrors, editErrors)); +// +// if (!user.matchEditToken(token, 'rollback')) { +// errors[] = [ 'sessionfailure' ]; +// } +// +// if (user.pingLimiter('rollback') || user.pingLimiter()) { +// errors[] = [ 'actionthrottledtext' ]; +// } +// +// // If there were errors, bail out now +// if (!empty(errors)) { +// return errors; +// } +// +// return this.commitRollback(fromP, summary, bot, resultDetails, user, tags); +// } +// +// /** +// * Backend implementation of doRollback(), please refer there for parameter +// * and return value documentation +// * +// * NOTE: This function does NOT check ANY permissions, it just commits the +// * rollback to the DB. Therefore, you should only call this function direct- +// * ly if you want to use custom permissions checks. If you don't, use +// * doRollback() instead. +// * @param String fromP Name of the user whose edits to rollback. +// * @param String summary Custom summary. Set to default summary if empty. +// * @param boolean bot If true, mark all reverted edits as bot. +// * +// * @param array &resultDetails Contains result-specific array of additional values +// * @param User guser The user performing the rollback +// * @param array|null tags Change tags to apply to the rollback +// * Callers are responsible for permission checks +// * (with ChangeTags::canAddTagsAccompanyingChange) +// * +// * @return array An array of error messages, as returned by Status::getErrorsArray() +// */ +// public function commitRollback(fromP, summary, bot, +// &resultDetails, User guser, tags = null +// ) { +// global wgUseRCPatrol; +// +// dbw = wfGetDB(DB_MASTER); +// +// if (wfReadOnly()) { +// return [ [ 'readonlytext' ] ]; +// } +// +// // Begin revision creation cycle by creating a PageUpdater. +// // If the page is changed concurrently after grabParentRevision(), the rollback will fail. +// updater = this.newPageUpdater(guser); +// current = updater.grabParentRevision(); +// +// if (is_null(current)) { +// // Something wrong... no page? +// return [ [ 'notanarticle' ] ]; +// } +// +// currentEditorForPublic = current.getUser(RevisionRecord::FOR_PUBLIC); +// legacyCurrent = new Revision(current); +// from = str_replace('_', ' ', fromP); +// +// // User name given should match up with the top revision. +// // If the revision's user is not visible, then from should be empty. +// if (from !== (currentEditorForPublic ? currentEditorForPublic.getName() : '')) { +// resultDetails = [ 'current' => legacyCurrent ]; +// return [ [ 'alreadyrolled', +// htmlspecialchars(this.mTitle.getPrefixedText()), +// htmlspecialchars(fromP), +// htmlspecialchars(currentEditorForPublic ? currentEditorForPublic.getName() : '') +// ] ]; +// } +// +// // Get the last edit not by this person... +// // Note: these may not be public values +// actorWhere = ActorMigration::newMigration().getWhere( +// dbw, +// 'rev_user', +// current.getUser(RevisionRecord::RAW) +// ); +// +// s = dbw.selectRow( +// [ 'revision' ] + actorWhere['tables'], +// [ 'rev_id', 'rev_timestamp', 'rev_deleted' ], +// [ +// 'rev_page' => current.getPageId(), +// 'NOT(' . actorWhere['conds'] . ')', +// ], +// __METHOD__, +// [ +// 'USE INDEX' => [ 'revision' => 'page_timestamp' ], +// 'ORDER BY' => 'rev_timestamp DESC' +// ], +// actorWhere['joins'] +// ); +// if (s === false) { +// // No one else ever edited this page +// return [ [ 'cantrollback' ] ]; +// } elseif (s.rev_deleted & RevisionRecord::DELETED_TEXT +// || s.rev_deleted & RevisionRecord::DELETED_USER +// ) { +// // Only admins can see this text +// return [ [ 'notvisiblerev' ] ]; +// } +// +// // Generate the edit summary if necessary +// target = this.getRevisionStore().getRevisionById( +// s.rev_id, +// RevisionStore::READ_LATEST +// ); +// if (empty(summary)) { +// if (!currentEditorForPublic) { // no public user name +// summary = wfMessage('revertpage-nouser'); +// } else { +// summary = wfMessage('revertpage'); +// } +// } +// legacyTarget = new Revision(target); +// targetEditorForPublic = target.getUser(RevisionRecord::FOR_PUBLIC); +// +// // Allow the custom summary to use the same args as the default message +// contLang = MediaWikiServices::getInstance().getContentLanguage(); +// args = [ +// targetEditorForPublic ? targetEditorForPublic.getName() : null, +// currentEditorForPublic ? currentEditorForPublic.getName() : null, +// s.rev_id, +// contLang.timeanddate(wfTimestamp(TS_MW, s.rev_timestamp)), +// current.getId(), +// contLang.timeanddate(current.getTimestamp()) +// ]; +// if (summary instanceof Message) { +// summary = summary.params(args).inContentLanguage().text(); +// } else { +// summary = wfMsgReplaceArgs(summary, args); +// } +// +// // Trim spaces on user supplied text +// summary = trim(summary); +// +// // Save +// flags = EDIT_UPDATE | EDIT_INTERNAL; +// +// if (guser.isAllowed('minoredit')) { +// flags |= EDIT_MINOR; +// } +// +// if (bot && (guser.isAllowedAny('markbotedits', 'bot'))) { +// flags |= EDIT_FORCE_BOT; +// } +// +// // TODO: MCR: also log model changes in other slots, in case that becomes possible! +// currentContent = current.getContent(SlotRecord::MAIN); +// targetContent = target.getContent(SlotRecord::MAIN); +// changingContentModel = targetContent.getModel() !== currentContent.getModel(); +// +// if (in_array('mw-rollback', ChangeTags::getSoftwareTags())) { +// tags[] = 'mw-rollback'; +// } +// +// // Build rollback revision: +// // Restore old content +// // TODO: MCR: test this once we can store multiple slots +// foreach (target.getSlots().getSlots() as slot) { +// updater.inheritSlot(slot); +// } +// +// // Remove extra slots +// // TODO: MCR: test this once we can store multiple slots +// foreach (current.getSlotRoles() as role) { +// if (!target.hasSlot(role)) { +// updater.removeSlot(role); +// } +// } +// +// updater.setOriginalRevisionId(target.getId()); +// // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374) +// updater.addTags(tags); +// +// // TODO: this logic should not be in the storage layer, it's here for compatibility +// // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same +// // place the 'bot' right is handled, which is currently in EditPage::attemptSave. +// if (wgUseRCPatrol && this.getTitle().userCan('autopatrol', guser)) { +// updater.setRcPatrolStatus(RecentChange::PRC_AUTOPATROLLED); +// } +// +// // Actually store the rollback +// rev = updater.saveRevision( +// CommentStoreComment::newUnsavedComment(summary), +// flags +// ); +// +// // Set patrolling and bot flag on the edits, which gets rollbacked. +// // This is done even on edit failure to have patrolling in that case (T64157). +// set = []; +// if (bot && guser.isAllowed('markbotedits')) { +// // Mark all reverted edits as bot +// set['rc_bot'] = 1; +// } +// +// if (wgUseRCPatrol) { +// // Mark all reverted edits as patrolled +// set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED; +// } +// +// if (count(set)) { +// actorWhere = ActorMigration::newMigration().getWhere( +// dbw, +// 'rc_user', +// current.getUser(RevisionRecord::RAW), +// false +// ); +// dbw.update('recentchanges', set, +// [ /* WHERE */ +// 'rc_cur_id' => current.getPageId(), +// 'rc_timestamp > ' . dbw.addQuotes(s.rev_timestamp), +// actorWhere['conds'], // No tables/joins are needed for rc_user +// ], +// __METHOD__ +// ); +// } +// +// if (!updater.wasSuccessful()) { +// return updater.getStatus().getErrorsArray(); +// } +// +// // Report if the edit was not created because it did not change the content. +// if (updater.isUnchanged()) { +// resultDetails = [ 'current' => legacyCurrent ]; +// return [ [ 'alreadyrolled', +// htmlspecialchars(this.mTitle.getPrefixedText()), +// htmlspecialchars(fromP), +// htmlspecialchars(targetEditorForPublic ? targetEditorForPublic.getName() : '') +// ] ]; +// } +// +// if (changingContentModel) { +// // If the content model changed during the rollback, +// // make sure it gets logged to Special:Log/contentmodel +// log = new ManualLogEntry('contentmodel', 'change'); +// log.setPerformer(guser); +// log.setTarget(this.mTitle); +// log.setComment(summary); +// log.setParameters([ +// '4::oldmodel' => currentContent.getModel(), +// '5::newmodel' => targetContent.getModel(), +// ]); +// +// logId = log.insert(dbw); +// log.publish(logId); +// } +// +// revId = rev.getId(); +// +// Hooks::run('ArticleRollbackComplete', [ this, guser, legacyTarget, legacyCurrent ]); +// +// resultDetails = [ +// 'summary' => summary, +// 'current' => legacyCurrent, +// 'target' => legacyTarget, +// 'newid' => revId, +// 'tags' => tags +// ]; +// +// // TODO: make this return a Status Object and wrap resultDetails in that. +// return []; +// } +// +// /** +// * The onArticle*() functions are supposed to be a kind of hooks +// * which should be called whenever any of the specified actions +// * are done. +// * +// * This is a good place to put code to clear caches, for instance. +// * +// * This is called on page move and undelete, as well as edit +// * +// * @param Title title +// */ +// public static function onArticleCreate(Title title) { +// // TODO: move this into a PageEventEmitter service +// +// // Update existence markers on article/talk tabs... +// other = title.getOtherPage(); +// +// other.purgeSquid(); +// +// title.touchLinks(); +// title.purgeSquid(); +// title.deleteTitleProtection(); +// +// MediaWikiServices::getInstance().getLinkCache().invalidateTitle(title); +// +// // Invalidate caches of articles which include this page +// DeferredUpdates::addUpdate( +// new HTMLCacheUpdate(title, 'templatelinks', 'page-create') +// ); +// +// if (title.getNamespace() == NS_CATEGORY) { +// // Load the Category Object, which will schedule a job to create +// // the category table row if necessary. Checking a replica DB is ok +// // here, in the worst case it'll run an unnecessary recount job on +// // a category that probably doesn't have many members. +// Category::newFromTitle(title).getID(); +// } +// } +// +// /** +// * Clears caches when article is deleted +// * +// * @param Title title +// */ +// public static function onArticleDelete(Title title) { +// // TODO: move this into a PageEventEmitter service +// +// // Update existence markers on article/talk tabs... +// // Clear Backlink cache first so that purge jobs use more up-to-date backlink information +// BacklinkCache::get(title).clear(); +// other = title.getOtherPage(); +// +// other.purgeSquid(); +// +// title.touchLinks(); +// title.purgeSquid(); +// +// MediaWikiServices::getInstance().getLinkCache().invalidateTitle(title); +// +// // File cache +// HTMLFileCache::clearFileCache(title); +// InfoAction::invalidateCache(title); +// +// // Messages +// if (title.getNamespace() == NS_MEDIAWIKI) { +// MessageCache::singleton().updateMessageOverride(title, null); +// } +// +// // Images +// if (title.getNamespace() == NS_FILE) { +// DeferredUpdates::addUpdate( +// new HTMLCacheUpdate(title, 'imagelinks', 'page-delete') +// ); +// } +// +// // User talk pages +// if (title.getNamespace() == NS_USER_TALK) { +// user = User::newFromName(title.getText(), false); +// if (user) { +// user.setNewtalk(false); +// } +// } +// +// // Image redirects +// RepoGroup::singleton().getLocalRepo().invalidateImageRedirect(title); +// +// // Purge cross-wiki cache entities referencing this page +// self::purgeInterwikiCheckKey(title); +// } +// +// /** +// * Purge caches on page update etc +// * +// * @param Title title +// * @param Revision|null revision Revision that was just saved, may be null +// * @param String[]|null slotsChanged The role names of the slots that were changed. +// * If not given, all slots are assumed to have changed. +// */ +// public static function onArticleEdit( +// Title title, +// Revision revision = null, +// slotsChanged = null +// ) { +// // TODO: move this into a PageEventEmitter service +// +// if (slotsChanged === null || in_array(SlotRecord::MAIN, slotsChanged)) { +// // Invalidate caches of articles which include this page. +// // Only for the main slot, because only the main slot is transcluded. +// // TODO: MCR: not true for TemplateStyles! [SlotHandler] +// DeferredUpdates::addUpdate( +// new HTMLCacheUpdate(title, 'templatelinks', 'page-edit') +// ); +// } +// +// // Invalidate the caches of all pages which redirect here +// DeferredUpdates::addUpdate( +// new HTMLCacheUpdate(title, 'redirect', 'page-edit') +// ); +// +// MediaWikiServices::getInstance().getLinkCache().invalidateTitle(title); +// +// // Purge CDN for this page only +// title.purgeSquid(); +// // Clear file cache for this page only +// HTMLFileCache::clearFileCache(title); +// +// // Purge ?action=info cache +// revid = revision ? revision.getId() : null; +// DeferredUpdates::addCallableUpdate(function () use (title, revid) { +// InfoAction::invalidateCache(title, revid); +// }); +// +// // Purge cross-wiki cache entities referencing this page +// self::purgeInterwikiCheckKey(title); +// } +// +// /**#@-*/ +// +// /** +// * Purge the check key for cross-wiki cache entries referencing this page +// * +// * @param Title title +// */ +// private static function purgeInterwikiCheckKey(Title title) { +// global wgEnableScaryTranscluding; +// +// if (!wgEnableScaryTranscluding) { +// return; // @todo: perhaps this wiki is only used as a *source* for content? +// } +// +// DeferredUpdates::addCallableUpdate(function () use (title) { +// cache = MediaWikiServices::getInstance().getMainWANObjectCache(); +// cache.resetCheckKey( +// // Do not include the namespace since there can be multiple aliases to it +// // due to different namespace text definitions on different wikis. This only +// // means that some cache invalidations happen that are not strictly needed. +// cache.makeGlobalKey( +// 'interwiki-page', +// WikiMap::getCurrentWikiDbDomain().getId(), +// title.getDBkey() +// ) +// ); +// }); +// } +// +// /** +// * Returns a list of categories this page is a member of. +// * Results will include hidden categories +// * +// * @return TitleArray +// */ +// public function getCategories() { +// id = this.getId(); +// if (id == 0) { +// return TitleArray::newFromResult(new FakeResultWrapper([])); +// } +// +// dbr = wfGetDB(DB_REPLICA); +// res = dbr.select('categorylinks', +// [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ], +// // Have to do that since Database::fieldNamesWithAlias treats numeric indexes +// // as not being aliases, and NS_CATEGORY is numeric +// [ 'cl_from' => id ], +// __METHOD__); +// +// return TitleArray::newFromResult(res); +// } +// +// /** +// * Returns a list of hidden categories this page is a member of. +// * Uses the page_props and categorylinks tables. +// * +// * @return array Array of Title objects +// */ +// public function getHiddenCategories() { +// result = []; +// id = this.getId(); +// +// if (id == 0) { +// return []; +// } +// +// dbr = wfGetDB(DB_REPLICA); +// res = dbr.select([ 'categorylinks', 'page_props', 'page' ], +// [ 'cl_to' ], +// [ 'cl_from' => id, 'pp_page=page_id', 'pp_propname' => 'hiddencat', +// 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ], +// __METHOD__); +// +// if (res !== false) { +// foreach (res as row) { +// result[] = Title::makeTitle(NS_CATEGORY, row.cl_to); +// } +// } +// +// return result; +// } +// +// /** +// * Auto-generates a deletion reason +// * +// * @param boolean &hasHistory Whether the page has a history +// * @return String|boolean String containing deletion reason or empty String, or boolean false +// * if no revision occurred +// */ +// public function getAutoDeleteReason(&hasHistory) { +// return this.getContentHandler().getAutoDeleteReason(this.getTitle(), hasHistory); +// } +// +// /** +// * Update all the appropriate counts in the category table, given that +// * we've added the categories added and deleted the categories deleted. +// * +// * This should only be called from deferred updates or jobs to avoid contention. +// * +// * @param array added The names of categories that were added +// * @param array deleted The names of categories that were deleted +// * @param int id Page ID (this should be the original deleted page ID) +// */ +// public function updateCategoryCounts(array added, array deleted, id = 0) { +// id = id ?: this.getId(); +// type = MWNamespace::getCategoryLinkType(this.getTitle().getNamespace()); +// +// addFields = [ 'cat_pages = cat_pages + 1' ]; +// removeFields = [ 'cat_pages = cat_pages - 1' ]; +// if (type !== 'page') { +// addFields[] = "cat_{type}s = cat_{type}s + 1"; +// removeFields[] = "cat_{type}s = cat_{type}s - 1"; +// } +// +// dbw = wfGetDB(DB_MASTER); +// +// if (count(added)) { +// existingAdded = dbw.selectFieldValues( +// 'category', +// 'cat_title', +// [ 'cat_title' => added ], +// __METHOD__ +// ); +// +// // For category rows that already exist, do a plain +// // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE +// // to avoid creating gaps in the cat_id sequence. +// if (count(existingAdded)) { +// dbw.update( +// 'category', +// addFields, +// [ 'cat_title' => existingAdded ], +// __METHOD__ +// ); +// } +// +// missingAdded = array_diff(added, existingAdded); +// if (count(missingAdded)) { +// insertRows = []; +// foreach (missingAdded as cat) { +// insertRows[] = [ +// 'cat_title' => cat, +// 'cat_pages' => 1, +// 'cat_subcats' => (type === 'subcat') ? 1 : 0, +// 'cat_files' => (type === 'file') ? 1 : 0, +// ]; +// } +// dbw.upsert( +// 'category', +// insertRows, +// [ 'cat_title' ], +// addFields, +// __METHOD__ +// ); +// } +// } +// +// if (count(deleted)) { +// dbw.update( +// 'category', +// removeFields, +// [ 'cat_title' => deleted ], +// __METHOD__ +// ); +// } +// +// foreach (added as catName) { +// cat = Category::newFromName(catName); +// Hooks::run('CategoryAfterPageAdded', [ cat, this ]); +// } +// +// foreach (deleted as catName) { +// cat = Category::newFromName(catName); +// Hooks::run('CategoryAfterPageRemoved', [ cat, this, id ]); +// // Refresh counts on categories that should be empty now (after commit, T166757) +// DeferredUpdates::addCallableUpdate(function () use (cat) { +// cat.refreshCountsIfEmpty(); +// }); +// } +// } +// +// /** +// * Opportunistically enqueue link update jobs given fresh parser output if useful +// * +// * @param ParserOutput parserOutput Current version page output +// * @since 1.25 +// */ +// public function triggerOpportunisticLinksUpdate(ParserOutput parserOutput) { +// if (wfReadOnly()) { +// return; +// } +// +// if (!Hooks::run('OpportunisticLinksUpdate', +// [ this, this.mTitle, parserOutput ] +// )) { +// return; +// } +// +// config = RequestContext::getMain().getConfig(); +// +// params = [ +// 'isOpportunistic' => true, +// 'rootJobTimestamp' => parserOutput.getCacheTime() +// ]; +// +// if (this.mTitle.areRestrictionsCascading()) { +// // If the page is cascade protecting, the links should really be up-to-date +// JobQueueGroup::singleton().lazyPush( +// RefreshLinksJob::newPrioritized(this.mTitle, params) +// ); +// } elseif (!config.get('MiserMode') && parserOutput.hasDynamicContent()) { +// // Assume the output contains "dynamic" time/random based magic words. +// // Only update pages that expired due to dynamic content and NOT due to edits +// // to referenced templates/files. When the cache expires due to dynamic content, +// // page_touched is unchanged. We want to avoid triggering redundant jobs due to +// // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the +// // template/file edit already triggered recursive RefreshLinksJob jobs. +// if (this.getLinksTimestamp() > this.getTouched()) { +// // If a page is uncacheable, do not keep spamming a job for it. +// // Although it would be de-duplicated, it would still waste I/O. +// cache = ObjectCache::getLocalClusterInstance(); +// key = cache.makeKey('dynamic-linksupdate', 'last', this.getId()); +// ttl = max(parserOutput.getCacheExpiry(), 3600); +// if (cache.add(key, time(), ttl)) { +// JobQueueGroup::singleton().lazyPush( +// RefreshLinksJob::newDynamic(this.mTitle, params) +// ); +// } +// } +// } +// } +// +// /** +// * Returns a list of updates to be performed when this page is deleted. The +// * updates should remove any information about this page from secondary data +// * stores such as links tables. +// * +// * @param RevisionRecord|Content|null rev The revision being deleted. Also accepts a Content +// * Object for backwards compatibility. +// * @return DeferrableUpdate[] +// */ +// public function getDeletionUpdates(rev = null) { +// if (!rev) { +// wfDeprecated(__METHOD__ . ' without a RevisionRecord', '1.32'); +// +// try { +// rev = this.getRevisionRecord(); +// } catch (Exception ex) { +// // If we can't load the content, something is wrong. Perhaps that's why +// // the user is trying to delete the page, so let's not fail in that case. +// // Note that doDeleteArticleReal() will already have logged an issue with +// // loading the content. +// wfDebug(__METHOD__ . ' failed to load current revision of page ' . this.getId()); +// } +// } +// +// if (!rev) { +// slotContent = []; +// } elseif (rev instanceof Content) { +// wfDeprecated(__METHOD__ . ' with a Content Object instead of a RevisionRecord', '1.32'); +// +// slotContent = [ SlotRecord::MAIN => rev ]; +// } else { +// slotContent = array_map(function (SlotRecord slot) { +// return slot.getContent(Revision::RAW); +// }, rev.getSlots().getSlots()); +// } +// +// allUpdates = [ new LinksDeletionUpdate(this) ]; +// +// // NOTE: once Content::getDeletionUpdates() is removed, we only need to content +// // model here, not the content Object! +// // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates() +// /** @var Content content */ +// foreach (slotContent as role => content) { +// handler = content.getContentHandler(); +// +// updates = handler.getDeletionUpdates( +// this.getTitle(), +// role +// ); +// allUpdates = array_merge(allUpdates, updates); +// +// // TODO: remove B/C hack in 1.32! +// legacyUpdates = content.getDeletionUpdates(this); +// +// // HACK: filter out redundant and incomplete LinksDeletionUpdate +// legacyUpdates = array_filter(legacyUpdates, function (update) { +// return !(update instanceof LinksDeletionUpdate); +// }); +// +// allUpdates = array_merge(allUpdates, legacyUpdates); +// } +// +// Hooks::run('PageDeletionDataUpdates', [ this.getTitle(), rev, &allUpdates ]); +// +// // TODO: hard deprecate old hook in 1.33 +// Hooks::run('WikiPageDeletionUpdates', [ this, content, &allUpdates ]); +// return allUpdates; +// } +// +// /** +// * Whether this content displayed on this page +// * comes from the local database +// * +// * @since 1.28 +// * @return boolean +// */ +// public function isLocal() { +// return true; +// } +// +// /** +// * The display name for the site this content +// * come from. If a subclass overrides isLocal(), +// * this could return something other than the +// * current site name +// * +// * @since 1.28 +// * @return String +// */ +// public function getWikiDisplayName() { +// global wgSitename; +// return wgSitename; +// } +// +// /** +// * Get the source URL for the content on this page, +// * typically the canonical URL, but may be a remote +// * link if the content comes from another site +// * +// * @since 1.28 +// * @return String +// */ +// public function getSourceURL() { +// return this.getTitle().getCanonicalURL(); +// } +// +// /** +// * @param WANObjectCache cache +// * @return String[] +// * @since 1.28 +// */ +// public function getMutableCacheKeys(WANObjectCache cache) { +// linkCache = MediaWikiServices::getInstance().getLinkCache(); +// +// return linkCache.getMutableCacheKeys(cache, this.getTitle()); +// } + +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/user/XomwUser.java b/400_xowa/src/gplx/xowa/mediawiki/includes/user/XomwUser.java new file mode 100644 index 000000000..522a6120f --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/user/XomwUser.java @@ -0,0 +1,5725 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.user; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; +/** +* The User Object encapsulates all of the user-specific settings (user_id, +* name, rights, email address, options, last login time). Client +* classes use the getXXX() functions to access these fields. These functions +* do all the work of determining whether the user is logged in, +* whether the requested option can be satisfied from cookies or +* whether a database query is needed. Most of the settings needed +* for rendering normal pages are set in the cookie to minimize use +* of the database. +*/ +public class XomwUser { // implements IDBAccessObject, UserIdentity +// /** +// * @static final int Number of characters in user_token field. +// */ +// static final TOKEN_LENGTH = 32; +// +// /** +// * @static final String An invalid value for user_token +// */ +// static final INVALID_TOKEN = '*** INVALID ***'; +// +// /** +// * @static final int Serialized record version. +// */ +// static final VERSION = 13; +// +// /** +// * Exclude user options that are set to their default value. +// * @since 1.25 +// */ +// static final GETOPTIONS_EXCLUDE_DEFAULTS = 1; +// +// /** +// * @since 1.27 +// */ +// static final CHECK_USER_RIGHTS = true; +// +// /** +// * @since 1.27 +// */ +// static final IGNORE_USER_RIGHTS = false; +// +// /** +// * Array of Strings List of member variables which are saved to the +// * shared cache (memcached). Any operation which changes the +// * corresponding database fields must call a cache-clearing function. +// * @showinitializer +// */ +// protected static $mCacheVars = [ +// // user table +// 'mId', +// 'mName', +// 'mRealName', +// 'mEmail', +// 'mTouched', +// 'mToken', +// 'mEmailAuthenticated', +// 'mEmailToken', +// 'mEmailTokenExpires', +// 'mRegistration', +// 'mEditCount', +// // user_groups table +// 'mGroupMemberships', +// // user_properties table +// 'mOptionOverrides', +// // actor table +// 'mActorId', +// ]; +// +// /** +// * Array of Strings Core rights. +// * Each of these should have a corresponding message of the form +// * "right-$right". +// * @showinitializer +// */ +// protected static $mCoreRights = [ +// 'apihighlimits', +// 'applychangetags', +// 'autoconfirmed', +// 'autocreateaccount', +// 'autopatrol', +// 'bigdelete', +// 'block', +// 'blockemail', +// 'bot', +// 'browsearchive', +// 'changetags', +// 'createaccount', +// 'createpage', +// 'createtalk', +// 'delete', +// 'deletechangetags', +// 'deletedhistory', +// 'deletedtext', +// 'deletelogentry', +// 'deleterevision', +// 'edit', +// 'editcontentmodel', +// 'editinterface', +// 'editprotected', +// 'editmyoptions', +// 'editmyprivateinfo', +// 'editmyusercss', +// 'editmyuserjson', +// 'editmyuserjs', +// 'editmywatchlist', +// 'editsemiprotected', +// 'editsitecss', +// 'editsitejson', +// 'editsitejs', +// 'editusercss', +// 'edituserjson', +// 'edituserjs', +// 'hideuser', +// 'import', +// 'importupload', +// 'ipblock-exempt', +// 'managechangetags', +// 'markbotedits', +// 'mergehistory', +// 'minoredit', +// 'move', +// 'movefile', +// 'move-categorypages', +// 'move-rootuserpages', +// 'move-subpages', +// 'nominornewtalk', +// 'noratelimit', +// 'override-export-depth', +// 'pagelang', +// 'patrol', +// 'patrolmarks', +// 'protect', +// 'purge', +// 'read', +// 'reupload', +// 'reupload-own', +// 'reupload-shared', +// 'rollback', +// 'sendemail', +// 'siteadmin', +// 'suppressionlog', +// 'suppressredirect', +// 'suppressrevision', +// 'unblockself', +// 'undelete', +// 'unwatchedpages', +// 'upload', +// 'upload_by_url', +// 'userrights', +// 'userrights-interwiki', +// 'viewmyprivateinfo', +// 'viewmywatchlist', +// 'viewsuppressed', +// 'writeapi', +// ]; +// +// /** +// * String Cached results of getAllRights() +// */ +// protected static $mAllRights = false; +// +// /** Cache variables */ +// // @{ +// /** @var int */ +// public $mId; +// /** @var String */ +// public $mName; +// /** @var int|null */ +// protected $mActorId; +// /** @var String */ +// public $mRealName; +// +// /** @var String */ +// public $mEmail; +// /** @var String TS_MW timestamp from the DB */ +// public $mTouched; +// /** @var String TS_MW timestamp from cache */ +// protected $mQuickTouched; +// /** @var String */ +// protected $mToken; +// /** @var String */ +// public $mEmailAuthenticated; +// /** @var String */ +// protected $mEmailToken; +// /** @var String */ +// protected $mEmailTokenExpires; +// /** @var String */ +// protected $mRegistration; +// /** @var int */ +// protected $mEditCount; +// /** @var UserGroupMembership[] Associative array of (group name => UserGroupMembership Object) */ +// protected $mGroupMemberships; +// /** @var array */ +// protected $mOptionOverrides; +// // @} +// +// /** +// * Bool Whether the cache variables have been loaded. +// */ +// // @{ +// public $mOptionsLoaded; +// +// /** +// * Array with already loaded items or true if all items have been loaded. +// */ +// protected $mLoadedItems = []; +// // @} +// +// /** +// * String Initialization data source if mLoadedItems!==true. May be one of: +// * - 'defaults' anonymous user initialised from class defaults +// * - 'name' initialise from mName +// * - 'id' initialise from mId +// * - 'actor' initialise from mActorId +// * - 'session' log in from session if possible +// * +// * Use the User::newFrom*() family of functions to set this. +// */ +// public $mFrom; +// +// /** +// * Lazy-initialized variables, invalidated with clearInstanceCache +// */ +// protected $mNewtalk; +// /** @var String */ +// protected $mDatePreference; +// /** @var String */ +// public $mBlockedby; +// /** @var String */ +// protected $mHash; +// /** @var array */ +// public $mRights; +// /** @var String */ +// protected $mBlockreason; +// /** @var array */ +// protected $mEffectiveGroups; +// /** @var array */ +// protected $mImplicitGroups; +// /** @var array */ +// protected $mFormerGroups; +// /** @var Block */ +// protected $mGlobalBlock; +// /** @var boolean */ +// protected $mLocked; +// /** @var boolean */ +// public $mHideName; +// /** @var array */ +// public $mOptions; +// +// /** @var WebRequest */ +// private $mRequest; +// +// /** @var Block */ +// public $mBlock; +// +// /** @var boolean */ +// protected $mAllowUsertalk; +// +// /** @var Block */ +// private $mBlockedFromCreateAccount = false; +// +// /** @var int User::READ_* constant bitfield used to load data */ +// protected $queryFlagsUsed = self::READ_NORMAL; +// +// public static $idCacheByName = []; +// +// /** +// * Lightweight constructor for an anonymous user. +// * Use the User::newFrom* factory functions for other kinds of users. +// * +// * @see newFromName() +// * @see newFromId() +// * @see newFromActorId() +// * @see newFromConfirmationCode() +// * @see newFromSession() +// * @see newFromRow() +// */ +// public function __construct() { +// $this->clearInstanceCache( 'defaults' ); +// } +// +// /** +// * @return String +// */ +// public function __toString() { +// return (String)$this->getName(); +// } +// +// /** +// * Test if it's safe to load this User Object. +// * +// * You should typically check this before using $wgUser or +// * RequestContext::getUser in a method that might be called before the +// * system has been fully initialized. If the Object is unsafe, you should +// * use an anonymous user: +// * \code +// * $user = $wgUser->isSafeToLoad() ? $wgUser : new User; +// * \endcode +// * +// * @since 1.27 +// * @return boolean +// */ +// public function isSafeToLoad() { +// global $wgFullyInitialised; +// +// // The user is safe to load if: +// // * MW_NO_SESSION is undefined AND $wgFullyInitialised is true (safe to use session data) +// // * mLoadedItems === true (already loaded) +// // * mFrom !== 'session' (sessions not involved at all) +// +// return ( !defined( 'MW_NO_SESSION' ) && $wgFullyInitialised ) || +// $this->mLoadedItems === true || $this->mFrom !== 'session'; +// } +// +// /** +// * Load the user table data for this Object from the source given by mFrom. +// * +// * @param int $flags User::READ_* constant bitfield +// */ +// public function load( $flags = self::READ_NORMAL ) { +// global $wgFullyInitialised; +// +// if ( $this->mLoadedItems === true ) { +// return; +// } +// +// // Set it now to avoid infinite recursion in accessors +// $oldLoadedItems = $this->mLoadedItems; +// $this->mLoadedItems = true; +// $this->queryFlagsUsed = $flags; +// +// // If this is called too early, things are likely to break. +// if ( !$wgFullyInitialised && $this->mFrom === 'session' ) { +// \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) +// ->warning( 'User::loadFromSession called before the end of Setup.php', [ +// 'exception' => new Exception( 'User::loadFromSession called before the end of Setup.php' ), +// ] ); +// $this->loadDefaults(); +// $this->mLoadedItems = $oldLoadedItems; +// return; +// } +// +// switch ( $this->mFrom ) { +// case 'defaults': +// $this->loadDefaults(); +// break; +// case 'name': +// // Make sure this thread sees its own changes +// $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); +// if ( $lb->hasOrMadeRecentMasterChanges() ) { +// $flags |= self::READ_LATEST; +// $this->queryFlagsUsed = $flags; +// } +// +// $this->mId = self::idFromName( $this->mName, $flags ); +// if ( !$this->mId ) { +// // Nonexistent user placeholder Object +// $this->loadDefaults( $this->mName ); +// } else { +// $this->loadFromId( $flags ); +// } +// break; +// case 'id': +// // Make sure this thread sees its own changes, if the ID isn't 0 +// if ( $this->mId != 0 ) { +// $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); +// if ( $lb->hasOrMadeRecentMasterChanges() ) { +// $flags |= self::READ_LATEST; +// $this->queryFlagsUsed = $flags; +// } +// } +// +// $this->loadFromId( $flags ); +// break; +// case 'actor': +// // Make sure this thread sees its own changes +// $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); +// if ( $lb->hasOrMadeRecentMasterChanges() ) { +// $flags |= self::READ_LATEST; +// $this->queryFlagsUsed = $flags; +// } +// +// list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags ); +// $row = wfGetDB( $index )->selectRow( +// 'actor', +// [ 'actor_user', 'actor_name' ], +// [ 'actor_id' => $this->mActorId ], +// __METHOD__, +// $options +// ); +// +// if ( !$row ) { +// // Ugh. +// $this->loadDefaults(); +// } elseif ( $row->actor_user ) { +// $this->mId = $row->actor_user; +// $this->loadFromId( $flags ); +// } else { +// $this->loadDefaults( $row->actor_name ); +// } +// break; +// case 'session': +// if ( !$this->loadFromSession() ) { +// // Loading from session failed. Load defaults. +// $this->loadDefaults(); +// } +// Hooks::run( 'UserLoadAfterLoadFromSession', [ $this ] ); +// break; +// default: +// throw new UnexpectedValueException( +// "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" ); +// } +// } +// +// /** +// * Load user table data, given mId has already been set. +// * @param int $flags User::READ_* constant bitfield +// * @return boolean False if the ID does not exist, true otherwise +// */ +// public function loadFromId( $flags = self::READ_NORMAL ) { +// if ( $this->mId == 0 ) { +// // Anonymous users are not in the database (don't need cache) +// $this->loadDefaults(); +// return false; +// } +// +// // Try cache (unless this needs data from the master DB). +// // NOTE: if this thread called saveSettings(), the cache was cleared. +// $latest = DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ); +// if ( $latest ) { +// if ( !$this->loadFromDatabase( $flags ) ) { +// // Can't load from ID +// return false; +// } +// } else { +// $this->loadFromCache(); +// } +// +// $this->mLoadedItems = true; +// $this->queryFlagsUsed = $flags; +// +// return true; +// } +// +// /** +// * @since 1.27 +// * @param String $wikiId +// * @param int $userId +// */ +// public static function purge( $wikiId, $userId ) { +// $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); +// $key = $cache->makeGlobalKey( 'user', 'id', $wikiId, $userId ); +// $cache->delete( $key ); +// } +// +// /** +// * @since 1.27 +// * @param WANObjectCache $cache +// * @return String +// */ +// protected function getCacheKey( WANObjectCache $cache ) { +// $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); +// +// return $cache->makeGlobalKey( 'user', 'id', $lbFactory->getLocalDomainID(), $this->mId ); +// } +// +// /** +// * @param WANObjectCache $cache +// * @return String[] +// * @since 1.28 +// */ +// public function getMutableCacheKeys( WANObjectCache $cache ) { +// $id = $this->getId(); +// +// return $id ? [ $this->getCacheKey( $cache ) ] : []; +// } +// +// /** +// * Load user data from shared cache, given mId has already been set. +// * +// * @return boolean True +// * @since 1.25 +// */ +// protected function loadFromCache() { +// $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); +// $data = $cache->getWithSetCallback( +// $this->getCacheKey( $cache ), +// $cache::TTL_HOUR, +// function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) { +// $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) ); +// wfDebug( "User: cache miss for user {$this->mId}\n" ); +// +// $this->loadFromDatabase( self::READ_NORMAL ); +// $this->loadGroups(); +// $this->loadOptions(); +// +// $data = []; +// foreach ( self::$mCacheVars as $name ) { +// $data[$name] = $this->$name; +// } +// +// $ttl = $cache->adaptiveTTL( wfTimestamp( TS_UNIX, $this->mTouched ), $ttl ); +// +// // if a user group membership is about to expire, the cache needs to +// // expire at that time (T163691) +// foreach ( $this->mGroupMemberships as $ugm ) { +// if ( $ugm->getExpiry() ) { +// $secondsUntilExpiry = wfTimestamp( TS_UNIX, $ugm->getExpiry() ) - time(); +// if ( $secondsUntilExpiry > 0 && $secondsUntilExpiry < $ttl ) { +// $ttl = $secondsUntilExpiry; +// } +// } +// } +// +// return $data; +// }, +// [ 'pcTTL' => $cache::TTL_PROC_LONG, 'version' => self::VERSION ] +// ); +// +// // Restore from cache +// foreach ( self::$mCacheVars as $name ) { +// $this->$name = $data[$name]; +// } +// +// return true; +// } +// +// /** @name newFrom*() static factory methods */ +// // @{ +// +// /** +// * Static factory method for creation from username. +// * +// * This is slightly less efficient than newFromId(), so use newFromId() if +// * you have both an ID and a name handy. +// * +// * @param String $name Username, validated by Title::newFromText() +// * @param String|boolean $validate Validate username. Takes the same parameters as +// * User::getCanonicalName(), except that true is accepted as an alias +// * for 'valid', for BC. +// * +// * @return User|boolean User Object, or false if the username is invalid +// * (e.g. if it contains illegal characters or is an IP address). If the +// * username is not present in the database, the result will be a user Object +// * with a name, zero user ID and default settings. +// */ +// public static function newFromName( $name, $validate = 'valid' ) { +// if ( $validate === true ) { +// $validate = 'valid'; +// } +// $name = self::getCanonicalName( $name, $validate ); +// if ( $name === false ) { +// return false; +// } +// +// // Create unloaded user Object +// $u = new User; +// $u->mName = $name; +// $u->mFrom = 'name'; +// $u->setItemLoaded( 'name' ); +// +// return $u; +// } +// +// /** +// * Static factory method for creation from a given user ID. +// * +// * @param int $id Valid user ID +// * @return User The corresponding User Object +// */ +// public static function newFromId( $id ) { +// $u = new User; +// $u->mId = $id; +// $u->mFrom = 'id'; +// $u->setItemLoaded( 'id' ); +// return $u; +// } +// +// /** +// * Static factory method for creation from a given actor ID. +// * +// * @since 1.31 +// * @param int $id Valid actor ID +// * @return User The corresponding User Object +// */ +// public static function newFromActorId( $id ) { +// global $wgActorTableSchemaMigrationStage; +// +// // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW, +// // but it does little harm and might be needed for write callers loading a User. +// if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) ) { +// throw new BadMethodCallException( +// 'Cannot use ' . __METHOD__ +// . ' when $wgActorTableSchemaMigrationStage lacks SCHEMA_COMPAT_NEW' +// ); +// } +// +// $u = new User; +// $u->mActorId = $id; +// $u->mFrom = 'actor'; +// $u->setItemLoaded( 'actor' ); +// return $u; +// } +// +// /** +// * Returns a User Object corresponding to the given UserIdentity. +// * +// * @since 1.32 +// * +// * @param UserIdentity $identity +// * +// * @return User +// */ +// public static function newFromIdentity( UserIdentity $identity ) { +// if ( $identity instanceof User ) { +// return $identity; +// } +// +// return self::newFromAnyId( +// $identity->getId() === 0 ? null : $identity->getId(), +// $identity->getName() === '' ? null : $identity->getName(), +// $identity->getActorId() === 0 ? null : $identity->getActorId() +// ); +// } +// +// /** +// * Static factory method for creation from an ID, name, and/or actor ID +// * +// * This does not check that the ID, name, and actor ID all correspond to +// * the same user. +// * +// * @since 1.31 +// * @param int|null $userId User ID, if known +// * @param String|null $userName User name, if known +// * @param int|null $actorId Actor ID, if known +// * @return User +// */ +// public static function newFromAnyId( $userId, $userName, $actorId ) { +// global $wgActorTableSchemaMigrationStage; +// +// $user = new User; +// $user->mFrom = 'defaults'; +// +// // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW, +// // but it does little harm and might be needed for write callers loading a User. +// if ( ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) && $actorId !== null ) { +// $user->mActorId = (int)$actorId; +// if ( $user->mActorId !== 0 ) { +// $user->mFrom = 'actor'; +// } +// $user->setItemLoaded( 'actor' ); +// } +// +// if ( $userName !== null && $userName !== '' ) { +// $user->mName = $userName; +// $user->mFrom = 'name'; +// $user->setItemLoaded( 'name' ); +// } +// +// if ( $userId !== null ) { +// $user->mId = (int)$userId; +// if ( $user->mId !== 0 ) { +// $user->mFrom = 'id'; +// } +// $user->setItemLoaded( 'id' ); +// } +// +// if ( $user->mFrom === 'defaults' ) { +// throw new InvalidArgumentException( +// 'Cannot create a user with no name, no ID, and no actor ID' +// ); +// } +// +// return $user; +// } +// +// /** +// * Factory method to fetch whichever user has a given email confirmation code. +// * This code is generated when an account is created or its e-mail address +// * has changed. +// * +// * If the code is invalid or has expired, returns NULL. +// * +// * @param String $code Confirmation code +// * @param int $flags User::READ_* bitfield +// * @return User|null +// */ +// public static function newFromConfirmationCode( $code, $flags = 0 ) { +// $db = ( $flags & self::READ_LATEST ) == self::READ_LATEST +// ? wfGetDB( DB_MASTER ) +// : wfGetDB( DB_REPLICA ); +// +// $id = $db->selectField( +// 'user', +// 'user_id', +// [ +// 'user_email_token' => md5( $code ), +// 'user_email_token_expires > ' . $db->addQuotes( $db->timestamp() ), +// ] +// ); +// +// return $id ? self::newFromId( $id ) : null; +// } +// +// /** +// * Create a new user Object using data from session. If the login +// * credentials are invalid, the result is an anonymous user. +// * +// * @param WebRequest|null $request Object to use; $wgRequest will be used if omitted. +// * @return User +// */ +// public static function newFromSession( WebRequest $request = null ) { +// $user = new User; +// $user->mFrom = 'session'; +// $user->mRequest = $request; +// return $user; +// } +// +// /** +// * Create a new user Object from a user row. +// * The row should have the following fields from the user table in it: +// * - either user_name or user_id to load further data if needed (or both) +// * - user_real_name +// * - all other fields (email, etc.) +// * It is useless to provide the remaining fields if either user_id, +// * user_name and user_real_name are not provided because the whole row +// * will be loaded once more from the database when accessing them. +// * +// * @param stdClass $row A row from the user table +// * @param array|null $data Further data to load into the Object +// * (see User::loadFromRow for valid keys) +// * @return User +// */ +// public static function newFromRow( $row, $data = null ) { +// $user = new User; +// $user->loadFromRow( $row, $data ); +// return $user; +// } +// +// /** +// * Static factory method for creation of a "system" user from username. +// * +// * A "system" user is an account that's used to attribute logged actions +// * taken by MediaWiki itself, as opposed to a bot or human user. Examples +// * might include the 'Maintenance script' or 'Conversion script' accounts +// * used by various scripts in the maintenance/ directory or accounts such +// * as 'MediaWiki message delivery' used by the MassMessage extension. +// * +// * This can optionally create the user if it doesn't exist, and "steal" the +// * account if it does exist. +// * +// * "Stealing" an existing user is intended to make it impossible for normal +// * authentication processes to use the account, effectively disabling the +// * account for normal use: +// * - Email is invalidated, to prevent account recovery by emailing a +// * temporary password and to disassociate the account from the existing +// * human. +// * - The token is set to a magic invalid value, to kill existing sessions +// * and to prevent $this->setToken() calls from resetting the token to a +// * valid value. +// * - SessionManager is instructed to prevent new sessions for the user, to +// * do things like deauthorizing OAuth consumers. +// * - AuthManager is instructed to revoke access, to invalidate or remove +// * passwords and other credentials. +// * +// * @param String $name Username +// * @param array $options Options are: +// * - validate: As for User::getCanonicalName(), default 'valid' +// * - create: Whether to create the user if it doesn't already exist, default true +// * - steal: Whether to "disable" the account for normal use if it already +// * exists, default false +// * @return User|null +// * @since 1.27 +// */ +// public static function newSystemUser( $name, $options = [] ) { +// $options += [ +// 'validate' => 'valid', +// 'create' => true, +// 'steal' => false, +// ]; +// +// $name = self::getCanonicalName( $name, $options['validate'] ); +// if ( $name === false ) { +// return null; +// } +// +// $dbr = wfGetDB( DB_REPLICA ); +// $userQuery = self::getQueryInfo(); +// $row = $dbr->selectRow( +// $userQuery['tables'], +// $userQuery['fields'], +// [ 'user_name' => $name ], +// __METHOD__, +// [], +// $userQuery['joins'] +// ); +// if ( !$row ) { +// // Try the master database... +// $dbw = wfGetDB( DB_MASTER ); +// $row = $dbw->selectRow( +// $userQuery['tables'], +// $userQuery['fields'], +// [ 'user_name' => $name ], +// __METHOD__, +// [], +// $userQuery['joins'] +// ); +// } +// +// if ( !$row ) { +// // No user. Create it? +// return $options['create'] +// ? self::createNew( $name, [ 'token' => self::INVALID_TOKEN ] ) +// : null; +// } +// +// $user = self::newFromRow( $row ); +// +// // A user is considered to exist as a non-system user if it can +// // authenticate, or has an email set, or has a non-invalid token. +// if ( $user->mEmail || $user->mToken !== self::INVALID_TOKEN || +// AuthManager::singleton()->userCanAuthenticate( $name ) +// ) { +// // User exists. Steal it? +// if ( !$options['steal'] ) { +// return null; +// } +// +// AuthManager::singleton()->revokeAccessForUser( $name ); +// +// $user->invalidateEmail(); +// $user->mToken = self::INVALID_TOKEN; +// $user->saveSettings(); +// SessionManager::singleton()->preventSessionsForUser( $user->getName() ); +// } +// +// return $user; +// } +// +// // @} +// +// /** +// * Get the username corresponding to a given user ID +// * @param int $id User ID +// * @return String|boolean The corresponding username +// */ +// public static function whoIs( $id ) { +// return UserCache::singleton()->getProp( $id, 'name' ); +// } +// +// /** +// * Get the real name of a user given their user ID +// * +// * @param int $id User ID +// * @return String|boolean The corresponding user's real name +// */ +// public static function whoIsReal( $id ) { +// return UserCache::singleton()->getProp( $id, 'real_name' ); +// } +// +// /** +// * Get database id given a user name +// * @param String $name Username +// * @param int $flags User::READ_* constant bitfield +// * @return int|null The corresponding user's ID, or null if user is nonexistent +// */ +// public static function idFromName( $name, $flags = self::READ_NORMAL ) { +// // Don't explode on self::$idCacheByName[$name] if $name is not a String but e.g. a User Object +// $name = (String)$name; +// $nt = Title::makeTitleSafe( NS_USER, $name ); +// if ( is_null( $nt ) ) { +// // Illegal name +// return null; +// } +// +// if ( !( $flags & self::READ_LATEST ) && array_key_exists( $name, self::$idCacheByName ) ) { +// return self::$idCacheByName[$name]; +// } +// +// list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags ); +// $db = wfGetDB( $index ); +// +// $s = $db->selectRow( +// 'user', +// [ 'user_id' ], +// [ 'user_name' => $nt->getText() ], +// __METHOD__, +// $options +// ); +// +// if ( $s === false ) { +// $result = null; +// } else { +// $result = (int)$s->user_id; +// } +// +// self::$idCacheByName[$name] = $result; +// +// if ( count( self::$idCacheByName ) > 1000 ) { +// self::$idCacheByName = []; +// } +// +// return $result; +// } +// +// /** +// * Reset the cache used in idFromName(). For use in tests. +// */ +// public static function resetIdByNameCache() { +// self::$idCacheByName = []; +// } +// +// /** +// * Does the String match an anonymous IP address? +// * +// * This function exists for username validation, in order to reject +// * usernames which are similar in form to IP addresses. Strings such +// * as 300.300.300.300 will return true because it looks like an IP +// * address, despite not being strictly valid. +// * +// * We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP +// * address because the usemod software would "cloak" anonymous IP +// * addresses like this, if we allowed accounts like this to be created +// * new users could get the old edits of these anonymous users. +// * +// * @param String $name Name to match +// * @return boolean +// */ +// public static function isIP( $name ) { +// return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name ) +// || IP::isIPv6( $name ); +// } +// +// /** +// * Is the user an IP range? +// * +// * @since 1.30 +// * @return boolean +// */ +// public function isIPRange() { +// return IP::isValidRange( $this->mName ); +// } +// +// /** +// * Is the input a valid username? +// * +// * Checks if the input is a valid username, we don't want an empty String, +// * an IP address, anything that contains slashes (would mess up subpages), +// * is longer than the maximum allowed username size or doesn't begin with +// * a capital letter. +// * +// * @param String $name Name to match +// * @return boolean +// */ +// public static function isValidUserName( $name ) { +// global $wgMaxNameChars; +// +// if ( $name == '' +// || self::isIP( $name ) +// || strpos( $name, '/' ) !== false +// || strlen( $name ) > $wgMaxNameChars +// || $name != MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name ) +// ) { +// return false; +// } +// +// // Ensure that the name can't be misresolved as a different title, +// // such as with extra namespace keys at the start. +// $parsed = Title::newFromText( $name ); +// if ( is_null( $parsed ) +// || $parsed->getNamespace() +// || strcmp( $name, $parsed->getPrefixedText() ) ) { +// return false; +// } +// +// // Check an additional blacklist of troublemaker characters. +// // Should these be merged into the title char list? +// $unicodeBlacklist = '/[' . +// '\x{0080}-\x{009f}' . # iso-8859-1 control chars +// '\x{00a0}' . # non-breaking space +// '\x{2000}-\x{200f}' . # various whitespace +// '\x{2028}-\x{202f}' . # breaks and control chars +// '\x{3000}' . # ideographic space +// '\x{e000}-\x{f8ff}' . # private use +// ']/u'; +// if ( preg_match( $unicodeBlacklist, $name ) ) { +// return false; +// } +// +// return true; +// } +// +// /** +// * Usernames which fail to pass this function will be blocked +// * from user login and new account registrations, but may be used +// * internally by batch processes. +// * +// * If an account already exists in this form, login will be blocked +// * by a failure to pass this function. +// * +// * @param String $name Name to match +// * @return boolean +// */ +// public static function isUsableName( $name ) { +// global $wgReservedUsernames; +// // Must be a valid username, obviously ;) +// if ( !self::isValidUserName( $name ) ) { +// return false; +// } +// +// static $reservedUsernames = false; +// if ( !$reservedUsernames ) { +// $reservedUsernames = $wgReservedUsernames; +// Hooks::run( 'UserGetReservedNames', [ &$reservedUsernames ] ); +// } +// +// // Certain names may be reserved for batch processes. +// foreach ( $reservedUsernames as $reserved ) { +// if ( substr( $reserved, 0, 4 ) == 'msg:' ) { +// $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->plain(); +// } +// if ( $reserved == $name ) { +// return false; +// } +// } +// return true; +// } +// +// /** +// * Return the users who are members of the given group(s). In case of multiple groups, +// * users who are members of at least one of them are returned. +// * +// * @param String|array $groups A single group name or an array of group names +// * @param int $limit Max number of users to return. The actual limit will never exceed 5000 +// * records; larger values are ignored. +// * @param int|null $after ID the user to start after +// * @return UserArrayFromResult +// */ +// public static function findUsersByGroup( $groups, $limit = 5000, $after = null ) { +// if ( $groups === [] ) { +// return UserArrayFromResult::newFromIDs( [] ); +// } +// +// $groups = array_unique( (array)$groups ); +// $limit = min( 5000, $limit ); +// +// $conds = [ 'ug_group' => $groups ]; +// if ( $after !== null ) { +// $conds[] = 'ug_user > ' . (int)$after; +// } +// +// $dbr = wfGetDB( DB_REPLICA ); +// $ids = $dbr->selectFieldValues( +// 'user_groups', +// 'ug_user', +// $conds, +// __METHOD__, +// [ +// 'DISTINCT' => true, +// 'ORDER BY' => 'ug_user', +// 'LIMIT' => $limit, +// ] +// ) ?: []; +// return UserArray::newFromIDs( $ids ); +// } +// +// /** +// * Usernames which fail to pass this function will be blocked +// * from new account registrations, but may be used internally +// * either by batch processes or by user accounts which have +// * already been created. +// * +// * Additional blacklisting may be added here rather than in +// * isValidUserName() to avoid disrupting existing accounts. +// * +// * @param String $name String to match +// * @return boolean +// */ +// public static function isCreatableName( $name ) { +// global $wgInvalidUsernameCharacters; +// +// // Ensure that the username isn't longer than 235 bytes, so that +// // (at least for the builtin skins) user javascript and css files +// // will work. (T25080) +// if ( strlen( $name ) > 235 ) { +// wfDebugLog( 'username', __METHOD__ . +// ": '$name' invalid due to length" ); +// return false; +// } +// +// // Preg yells if you try to give it an empty String +// if ( $wgInvalidUsernameCharacters !== '' && +// preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) +// ) { +// wfDebugLog( 'username', __METHOD__ . +// ": '$name' invalid due to wgInvalidUsernameCharacters" ); +// return false; +// } +// +// return self::isUsableName( $name ); +// } +// +// /** +// * Is the input a valid password for this user? +// * +// * @param String $password Desired password +// * @return boolean +// */ +// public function isValidPassword( $password ) { +// // simple boolean wrapper for checkPasswordValidity +// return $this->checkPasswordValidity( $password )->isGood(); +// } +// +// /** +// * Given unvalidated password input, return error message on failure. +// * +// * @param String $password Desired password +// * @return boolean|String|array True on success, String or array of error message on failure +// * @deprecated since 1.33, use checkPasswordValidity +// */ +// public function getPasswordValidity( $password ) { +// wfDeprecated( __METHOD__, '1.33' ); +// +// $result = $this->checkPasswordValidity( $password ); +// if ( $result->isGood() ) { +// return true; +// } +// +// $messages = []; +// foreach ( $result->getErrorsByType( 'error' ) as $error ) { +// $messages[] = $error['message']; +// } +// foreach ( $result->getErrorsByType( 'warning' ) as $warning ) { +// $messages[] = $warning['message']; +// } +// if ( count( $messages ) === 1 ) { +// return $messages[0]; +// } +// +// return $messages; +// } +// +// /** +// * Check if this is a valid password for this user +// * +// * Returns a Status Object with a set of messages describing +// * problems with the password. If the return status is fatal, +// * the action should be refused and the password should not be +// * checked at all (this is mainly meant for DoS mitigation). +// * If the return value is OK but not good, the password can be checked, +// * but the user should not be able to set their password to this. +// * The value of the returned Status Object will be an array which +// * can have the following fields: +// * - forceChange (boolean): if set to true, the user should not be +// * allowed to log with this password unless they change it during +// * the login process (see ResetPasswordSecondaryAuthenticationProvider). +// * - suggestChangeOnLogin (boolean): if set to true, the user should be prompted for +// * a password change on login. +// * +// * @param String $password Desired password +// * @return Status +// * @since 1.23 +// */ +// public function checkPasswordValidity( $password ) { +// global $wgPasswordPolicy; +// +// $upp = new UserPasswordPolicy( +// $wgPasswordPolicy['policies'], +// $wgPasswordPolicy['checks'] +// ); +// +// $status = Status::newGood( [] ); +// $result = false; // init $result to false for the @gplx.Internal protected checks +// +// if ( !Hooks::run( 'isValidPassword', [ $password, &$result, $this ] ) ) { +// $status->error( $result ); +// return $status; +// } +// +// if ( $result === false ) { +// $status->merge( $upp->checkUserPassword( $this, $password ), true ); +// return $status; +// } +// +// if ( $result === true ) { +// return $status; +// } +// +// $status->error( $result ); +// return $status; // the isValidPassword hook set a String $result and returned true +// } +// +// /** +// * Given unvalidated user input, return a canonical username, or false if +// * the username is invalid. +// * @param String $name User input +// * @param String|boolean $validate Type of validation to use: +// * - false No validation +// * - 'valid' Valid for batch processes +// * - 'usable' Valid for batch processes and login +// * - 'creatable' Valid for batch processes, login and account creation +// * +// * @throws InvalidArgumentException +// * @return boolean|String +// */ +// public static function getCanonicalName( $name, $validate = 'valid' ) { +// // Force usernames to capital +// $name = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name ); +// +// # Reject names containing '#'; these will be cleaned up +// # with title normalisation, but then it's too late to +// # check elsewhere +// if ( strpos( $name, '#' ) !== false ) { +// return false; +// } +// +// // Clean up name according to title rules, +// // but only when validation is requested (T14654) +// $t = ( $validate !== false ) ? +// Title::newFromText( $name, NS_USER ) : Title::makeTitle( NS_USER, $name ); +// // Check for invalid titles +// if ( is_null( $t ) || $t->getNamespace() !== NS_USER || $t->isExternal() ) { +// return false; +// } +// +// $name = $t->getText(); +// +// switch ( $validate ) { +// case false: +// break; +// case 'valid': +// if ( !self::isValidUserName( $name ) ) { +// $name = false; +// } +// break; +// case 'usable': +// if ( !self::isUsableName( $name ) ) { +// $name = false; +// } +// break; +// case 'creatable': +// if ( !self::isCreatableName( $name ) ) { +// $name = false; +// } +// break; +// default: +// throw new InvalidArgumentException( +// 'Invalid parameter value for $validate in ' . __METHOD__ ); +// } +// return $name; +// } +// +// /** +// * Return a random password. +// * +// * @deprecated since 1.27, use PasswordFactory::generateRandomPasswordString() +// * @return String New random password +// */ +// public static function randomPassword() { +// global $wgMinimalPasswordLength; +// return PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength ); +// } +// +// /** +// * Set cached properties to default. +// * +// * @note This no longer clears uncached lazy-initialised properties; +// * the constructor does that instead. +// * +// * @param String|boolean $name +// */ +// public function loadDefaults( $name = false ) { +// $this->mId = 0; +// $this->mName = $name; +// $this->mActorId = null; +// $this->mRealName = ''; +// $this->mEmail = ''; +// $this->mOptionOverrides = null; +// $this->mOptionsLoaded = false; +// +// $loggedOut = $this->mRequest && !defined( 'MW_NO_SESSION' ) +// ? $this->mRequest->getSession()->getLoggedOutTimestamp() : 0; +// if ( $loggedOut !== 0 ) { +// $this->mTouched = wfTimestamp( TS_MW, $loggedOut ); +// } else { +// $this->mTouched = '1'; # Allow any pages to be cached +// } +// +// $this->mToken = null; // Don't run cryptographic functions till we need a token +// $this->mEmailAuthenticated = null; +// $this->mEmailToken = ''; +// $this->mEmailTokenExpires = null; +// $this->mRegistration = wfTimestamp( TS_MW ); +// $this->mGroupMemberships = []; +// +// Hooks::run( 'UserLoadDefaults', [ $this, $name ] ); +// } +// +// /** +// * Return whether an item has been loaded. +// * +// * @param String $item Item to check. Current possibilities: +// * - id +// * - name +// * - realname +// * @param String $all 'all' to check if the whole Object has been loaded +// * or any other String to check if only the item is available (e.g. +// * for optimisation) +// * @return boolean +// */ +// public function isItemLoaded( $item, $all = 'all' ) { +// return ( $this->mLoadedItems === true && $all === 'all' ) || +// ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true ); +// } +// +// /** +// * Set that an item has been loaded +// * +// * @param String $item +// */ +// protected function setItemLoaded( $item ) { +// if ( is_array( $this->mLoadedItems ) ) { +// $this->mLoadedItems[$item] = true; +// } +// } +// +// /** +// * Load user data from the session. +// * +// * @return boolean True if the user is logged in, false otherwise. +// */ +// private function loadFromSession() { +// // Deprecated hook +// $result = null; +// Hooks::run( 'UserLoadFromSession', [ $this, &$result ], '1.27' ); +// if ( $result !== null ) { +// return $result; +// } +// +// // MediaWiki\Session\Session already did the necessary authentication of the user +// // returned here, so just use it if applicable. +// $session = $this->getRequest()->getSession(); +// $user = $session->getUser(); +// if ( $user->isLoggedIn() ) { +// $this->loadFromUserObject( $user ); +// if ( $user->isBlocked() ) { +// // If this user is autoblocked, set a cookie to track the Block. This has to be done on +// // every session load, because an autoblocked editor might not edit again from the same +// // IP address after being blocked. +// $this->trackBlockWithCookie(); +// } +// +// // Other code expects these to be set in the session, so set them. +// $session->set( 'wsUserID', $this->getId() ); +// $session->set( 'wsUserName', $this->getName() ); +// $session->set( 'wsToken', $this->getToken() ); +// +// return true; +// } +// +// return false; +// } +// +// /** +// * Set the 'BlockID' cookie depending on block type and user authentication status. +// */ +// public function trackBlockWithCookie() { +// $block = $this->getBlock(); +// +// if ( $block && $this->getRequest()->getCookie( 'BlockID' ) === null +// && $block->shouldTrackWithCookie( $this->isAnon() ) +// ) { +// $block->setCookie( $this->getRequest()->response() ); +// } +// } +// +// /** +// * Load user and user_group data from the database. +// * $this->mId must be set, this is how the user is identified. +// * +// * @param int $flags User::READ_* constant bitfield +// * @return boolean True if the user exists, false if the user is anonymous +// */ +// public function loadFromDatabase( $flags = self::READ_LATEST ) { +// // Paranoia +// $this->mId = intval( $this->mId ); +// +// if ( !$this->mId ) { +// // Anonymous users are not in the database +// $this->loadDefaults(); +// return false; +// } +// +// list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags ); +// $db = wfGetDB( $index ); +// +// $userQuery = self::getQueryInfo(); +// $s = $db->selectRow( +// $userQuery['tables'], +// $userQuery['fields'], +// [ 'user_id' => $this->mId ], +// __METHOD__, +// $options, +// $userQuery['joins'] +// ); +// +// $this->queryFlagsUsed = $flags; +// Hooks::run( 'UserLoadFromDatabase', [ $this, &$s ] ); +// +// if ( $s !== false ) { +// // Initialise user table data +// $this->loadFromRow( $s ); +// $this->mGroupMemberships = null; // deferred +// $this->getEditCount(); // revalidation for nulls +// return true; +// } +// +// // Invalid user_id +// $this->mId = 0; +// $this->loadDefaults(); +// +// return false; +// } +// +// /** +// * Initialize this Object from a row from the user table. +// * +// * @param stdClass $row Row from the user table to load. +// * @param array|null $data Further user data to load into the Object +// * +// * user_groups Array of arrays or stdClass result rows out of the user_groups +// * table. Previously you were supposed to pass an array of strings +// * here, but we also need expiry info nowadays, so an array of +// * strings is ignored. +// * user_properties Array with properties out of the user_properties table +// */ +// protected function loadFromRow( $row, $data = null ) { +// global $wgActorTableSchemaMigrationStage; +// +// if ( !is_object( $row ) ) { +// throw new InvalidArgumentException( '$row must be an Object' ); +// } +// +// $all = true; +// +// $this->mGroupMemberships = null; // deferred +// +// // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW, +// // but it does little harm and might be needed for write callers loading a User. +// if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) { +// if ( isset( $row->actor_id ) ) { +// $this->mActorId = (int)$row->actor_id; +// if ( $this->mActorId !== 0 ) { +// $this->mFrom = 'actor'; +// } +// $this->setItemLoaded( 'actor' ); +// } else { +// $all = false; +// } +// } +// +// if ( isset( $row->user_name ) && $row->user_name !== '' ) { +// $this->mName = $row->user_name; +// $this->mFrom = 'name'; +// $this->setItemLoaded( 'name' ); +// } else { +// $all = false; +// } +// +// if ( isset( $row->user_real_name ) ) { +// $this->mRealName = $row->user_real_name; +// $this->setItemLoaded( 'realname' ); +// } else { +// $all = false; +// } +// +// if ( isset( $row->user_id ) ) { +// $this->mId = intval( $row->user_id ); +// if ( $this->mId !== 0 ) { +// $this->mFrom = 'id'; +// } +// $this->setItemLoaded( 'id' ); +// } else { +// $all = false; +// } +// +// if ( isset( $row->user_id ) && isset( $row->user_name ) && $row->user_name !== '' ) { +// self::$idCacheByName[$row->user_name] = $row->user_id; +// } +// +// if ( isset( $row->user_editcount ) ) { +// $this->mEditCount = $row->user_editcount; +// } else { +// $all = false; +// } +// +// if ( isset( $row->user_touched ) ) { +// $this->mTouched = wfTimestamp( TS_MW, $row->user_touched ); +// } else { +// $all = false; +// } +// +// if ( isset( $row->user_token ) ) { +// // The definition for the column is binary(32), so trim the NULs +// // that appends. The previous definition was char(32), so trim +// // spaces too. +// $this->mToken = rtrim( $row->user_token, " \0" ); +// if ( $this->mToken === '' ) { +// $this->mToken = null; +// } +// } else { +// $all = false; +// } +// +// if ( isset( $row->user_email ) ) { +// $this->mEmail = $row->user_email; +// $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated ); +// $this->mEmailToken = $row->user_email_token; +// $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires ); +// $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration ); +// } else { +// $all = false; +// } +// +// if ( $all ) { +// $this->mLoadedItems = true; +// } +// +// if ( is_array( $data ) ) { +// if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) { +// if ( $data['user_groups'] === [] ) { +// $this->mGroupMemberships = []; +// } else { +// $firstGroup = reset( $data['user_groups'] ); +// if ( is_array( $firstGroup ) || is_object( $firstGroup ) ) { +// $this->mGroupMemberships = []; +// foreach ( $data['user_groups'] as $row ) { +// $ugm = UserGroupMembership::newFromRow( (Object)$row ); +// $this->mGroupMemberships[$ugm->getGroup()] = $ugm; +// } +// } +// } +// } +// if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) { +// $this->loadOptions( $data['user_properties'] ); +// } +// } +// } +// +// /** +// * Load the data for this user Object from another user Object. +// * +// * @param User $user +// */ +// protected function loadFromUserObject( $user ) { +// $user->load(); +// foreach ( self::$mCacheVars as $var ) { +// $this->$var = $user->$var; +// } +// } +// +// /** +// * Load the groups from the database if they aren't already loaded. +// */ +// private function loadGroups() { +// if ( is_null( $this->mGroupMemberships ) ) { +// $db = ( $this->queryFlagsUsed & self::READ_LATEST ) +// ? wfGetDB( DB_MASTER ) +// : wfGetDB( DB_REPLICA ); +// $this->mGroupMemberships = UserGroupMembership::getMembershipsForUser( +// $this->mId, $db ); +// } +// } +// +// /** +// * Add the user to the group if he/she meets given criteria. +// * +// * Contrary to autopromotion by \ref $wgAutopromote, the group will be +// * possible to remove manually via Special:UserRights. In such case it +// * will not be re-added automatically. The user will also not lose the +// * group if they no longer meet the criteria. +// * +// * @param String $event Key in $wgAutopromoteOnce (each one has groups/criteria) +// * +// * @return array Array of groups the user has been promoted to. +// * +// * @see $wgAutopromoteOnce +// */ +// public function addAutopromoteOnceGroups( $event ) { +// global $wgAutopromoteOnceLogInRC; +// +// if ( wfReadOnly() || !$this->getId() ) { +// return []; +// } +// +// $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event ); +// if ( $toPromote === [] ) { +// return []; +// } +// +// if ( !$this->checkAndSetTouched() ) { +// return []; // raced out (bug T48834) +// } +// +// $oldGroups = $this->getGroups(); // previous groups +// $oldUGMs = $this->getGroupMemberships(); +// foreach ( $toPromote as $group ) { +// $this->addGroup( $group ); +// } +// $newGroups = array_merge( $oldGroups, $toPromote ); // all groups +// $newUGMs = $this->getGroupMemberships(); +// +// // update groups in external authentication database +// Hooks::run( 'UserGroupsChanged', [ $this, $toPromote, [], false, false, $oldUGMs, $newUGMs ] ); +// +// $logEntry = new ManualLogEntry( 'rights', 'autopromote' ); +// $logEntry->setPerformer( $this ); +// $logEntry->setTarget( $this->getUserPage() ); +// $logEntry->setParameters( [ +// '4::oldgroups' => $oldGroups, +// '5::newgroups' => $newGroups, +// ] ); +// $logid = $logEntry->insert(); +// if ( $wgAutopromoteOnceLogInRC ) { +// $logEntry->publish( $logid ); +// } +// +// return $toPromote; +// } +// +// /** +// * Builds update conditions. Additional conditions may be added to $conditions to +// * protected against race conditions using a compare-and-set (CAS) mechanism +// * based on comparing $this->mTouched with the user_touched field. +// * +// * @param IDatabase $db +// * @param array $conditions WHERE conditions for use with Database::update +// * @return array WHERE conditions for use with Database::update +// */ +// protected function makeUpdateConditions( IDatabase $db, array $conditions ) { +// if ( $this->mTouched ) { +// // CAS check: only update if the row wasn't changed sicne it was loaded. +// $conditions['user_touched'] = $db->timestamp( $this->mTouched ); +// } +// +// return $conditions; +// } +// +// /** +// * Bump user_touched if it didn't change since this Object was loaded +// * +// * On success, the mTouched field is updated. +// * The user serialization cache is always cleared. +// * +// * @return boolean Whether user_touched was actually updated +// * @since 1.26 +// */ +// protected function checkAndSetTouched() { +// $this->load(); +// +// if ( !$this->mId ) { +// return false; // anon +// } +// +// // Get a new user_touched that is higher than the old one +// $newTouched = $this->newTouchedTimestamp(); +// +// $dbw = wfGetDB( DB_MASTER ); +// $dbw->update( 'user', +// [ 'user_touched' => $dbw->timestamp( $newTouched ) ], +// $this->makeUpdateConditions( $dbw, [ +// 'user_id' => $this->mId, +// ] ), +// __METHOD__ +// ); +// $success = ( $dbw->affectedRows() > 0 ); +// +// if ( $success ) { +// $this->mTouched = $newTouched; +// $this->clearSharedCache(); +// } else { +// // Clears on failure too since that is desired if the cache is stale +// $this->clearSharedCache( 'refresh' ); +// } +// +// return $success; +// } +// +// /** +// * Clear various cached data stored in this Object. The cache of the user table +// * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given. +// * +// * @param boolean|String $reloadFrom Reload user and user_groups table data from a +// * given source. May be "name", "id", "actor", "defaults", "session", or false for no reload. +// */ +// public function clearInstanceCache( $reloadFrom = false ) { +// $this->mNewtalk = -1; +// $this->mDatePreference = null; +// $this->mBlockedby = -1; # Unset +// $this->mHash = false; +// $this->mRights = null; +// $this->mEffectiveGroups = null; +// $this->mImplicitGroups = null; +// $this->mGroupMemberships = null; +// $this->mOptions = null; +// $this->mOptionsLoaded = false; +// $this->mEditCount = null; +// +// if ( $reloadFrom ) { +// $this->mLoadedItems = []; +// $this->mFrom = $reloadFrom; +// } +// } +// +// /** @var array|null */ +// private static $defOpt = null; +// /** @var String|null */ +// private static $defOptLang = null; +// +// /** +// * Reset the process cache of default user options. This is only necessary +// * if the wiki configuration has changed since defaults were calculated, +// * and as such should only be performed inside the testing suite that +// * regularly changes wiki configuration. +// */ +// public static function resetGetDefaultOptionsForTestsOnly() { +// Assert::invariant( defined( 'MW_PHPUNIT_TEST' ), 'Unit tests only' ); +// self::$defOpt = null; +// self::$defOptLang = null; +// } +// +// /** +// * Combine the language default options with any site-specific options +// * and add the default language variants. +// * +// * @return array Array of String options +// */ +// public static function getDefaultOptions() { +// global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgDefaultSkin; +// +// $contLang = MediaWikiServices::getInstance()->getContentLanguage(); +// if ( self::$defOpt !== null && self::$defOptLang === $contLang->getCode() ) { +// // The content language does not change (and should not change) mid-request, but the +// // unit tests change it anyway, and expect this method to return values relevant to the +// // current content language. +// return self::$defOpt; +// } +// +// self::$defOpt = $wgDefaultUserOptions; +// // Default language setting +// self::$defOptLang = $contLang->getCode(); +// self::$defOpt['language'] = self::$defOptLang; +// foreach ( LanguageConverter::$languagesWithVariants as $langCode ) { +// if ( $langCode === $contLang->getCode() ) { +// self::$defOpt['variant'] = $langCode; +// } else { +// self::$defOpt["variant-$langCode"] = $langCode; +// } +// } +// +// // NOTE: don't use SearchEngineConfig::getSearchableNamespaces here, +// // since extensions may change the set of searchable namespaces depending +// // on user groups/permissions. +// foreach ( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) { +// self::$defOpt['searchNs' . $nsnum] = (boolean)$val; +// } +// self::$defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin ); +// +// Hooks::run( 'UserGetDefaultOptions', [ &self::$defOpt ] ); +// +// return self::$defOpt; +// } +// +// /** +// * Get a given default option value. +// * +// * @param String $opt Name of option to retrieve +// * @return String Default option value +// */ +// public static function getDefaultOption( $opt ) { +// $defOpts = self::getDefaultOptions(); +// return $defOpts[$opt] ?? null; +// } +// +// /** +// * Get blocking information +// * @param boolean $fromReplica Whether to check the replica DB first. +// * To improve performance, non-critical checks are done against replica DBs. +// * Check when actually saving should be done against master. +// */ +// private function getBlockedStatus( $fromReplica = true ) { +// global $wgProxyWhitelist, $wgApplyIpBlocksToXff, $wgSoftBlockRanges; +// +// if ( $this->mBlockedby != -1 ) { +// return; +// } +// +// wfDebug( __METHOD__ . ": checking...\n" ); +// +// // Initialize data... +// // Otherwise something ends up stomping on $this->mBlockedby when +// // things get lazy-loaded later, causing false positive block hits +// // due to -1 !== 0. Probably session-related... Nothing should be +// // overwriting mBlockedby, surely? +// $this->load(); +// +// # We only need to worry about passing the IP address to the Block generator if the +// # user is not immune to autoblocks/hardblocks, and they are the current user so we +// # know which IP address they're actually coming from +// $ip = null; +// $sessionUser = RequestContext::getMain()->getUser(); +// // the session user is set up towards the end of Setup.php. Until then, +// // assume it's a logged-out user. +// $globalUserName = $sessionUser->isSafeToLoad() +// ? $sessionUser->getName() +// : IP::sanitizeIP( $sessionUser->getRequest()->getIP() ); +// if ( $this->getName() === $globalUserName && !$this->isAllowed( 'ipblock-exempt' ) ) { +// $ip = $this->getRequest()->getIP(); +// } +// +// // User/IP blocking +// $block = Block::newFromTarget( $this, $ip, !$fromReplica ); +// +// // Cookie blocking +// if ( !$block instanceof Block ) { +// $block = $this->getBlockFromCookieValue( $this->getRequest()->getCookie( 'BlockID' ) ); +// } +// +// // Proxy blocking +// if ( !$block instanceof Block && $ip !== null && !in_array( $ip, $wgProxyWhitelist ) ) { +// // Local list +// if ( self::isLocallyBlockedProxy( $ip ) ) { +// $block = new Block( [ +// 'byText' => wfMessage( 'proxyblocker' )->text(), +// 'reason' => wfMessage( 'proxyblockreason' )->plain(), +// 'address' => $ip, +// 'systemBlock' => 'proxy', +// ] ); +// } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) { +// $block = new Block( [ +// 'byText' => wfMessage( 'sorbs' )->text(), +// 'reason' => wfMessage( 'sorbsreason' )->plain(), +// 'address' => $ip, +// 'systemBlock' => 'dnsbl', +// ] ); +// } +// } +// +// // (T25343) Apply IP blocks to the contents of XFF headers, if enabled +// if ( !$block instanceof Block +// && $wgApplyIpBlocksToXff +// && $ip !== null +// && !in_array( $ip, $wgProxyWhitelist ) +// ) { +// $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' ); +// $xff = array_map( 'trim', explode( ',', $xff ) ); +// $xff = array_diff( $xff, [ $ip ] ); +// $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$fromReplica ); +// $block = Block::chooseBlock( $xffblocks, $xff ); +// if ( $block instanceof Block ) { +// # Mangle the reason to alert the user that the block +// # originated from matching the X-Forwarded-For header. +// $block->setReason( wfMessage( 'xffblockreason', $block->getReason() )->plain() ); +// } +// } +// +// if ( !$block instanceof Block +// && $ip !== null +// && $this->isAnon() +// && IP::isInRanges( $ip, $wgSoftBlockRanges ) +// ) { +// $block = new Block( [ +// 'address' => $ip, +// 'byText' => 'MediaWiki default', +// 'reason' => wfMessage( 'softblockrangesreason', $ip )->plain(), +// 'anonOnly' => true, +// 'systemBlock' => 'wgSoftBlockRanges', +// ] ); +// } +// +// if ( $block instanceof Block ) { +// wfDebug( __METHOD__ . ": Found block.\n" ); +// $this->mBlock = $block; +// $this->mBlockedby = $block->getByName(); +// $this->mBlockreason = $block->getReason(); +// $this->mHideName = $block->getHideName(); +// $this->mAllowUsertalk = $block->isUsertalkEditAllowed(); +// } else { +// $this->mBlock = null; +// $this->mBlockedby = ''; +// $this->mBlockreason = ''; +// $this->mHideName = 0; +// $this->mAllowUsertalk = false; +// } +// +// // Avoid PHP 7.1 warning of passing $this by reference +// $thisUser = $this; +// // Extensions +// Hooks::run( 'GetBlockedStatus', [ &$thisUser ] ); +// } +// +// /** +// * Try to load a Block from an ID given in a cookie value. +// * @param String|null $blockCookieVal The cookie value to check. +// * @return Block|boolean The Block Object, or false if none could be loaded. +// */ +// protected function getBlockFromCookieValue( $blockCookieVal ) { +// // Make sure there's something to check. The cookie value must start with a number. +// if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) { +// return false; +// } +// // Load the Block from the ID in the cookie. +// $blockCookieId = Block::getIdFromCookieValue( $blockCookieVal ); +// if ( $blockCookieId !== null ) { +// // An ID was found in the cookie. +// $tmpBlock = Block::newFromID( $blockCookieId ); +// if ( $tmpBlock instanceof Block ) { +// $config = RequestContext::getMain()->getConfig(); +// +// switch ( $tmpBlock->getType() ) { +// case Block::TYPE_USER: +// $blockIsValid = !$tmpBlock->isExpired() && $tmpBlock->isAutoblocking(); +// $useBlockCookie = ( $config->get( 'CookieSetOnAutoblock' ) === true ); +// break; +// case Block::TYPE_IP: +// case Block::TYPE_RANGE: +// // If block is type IP or IP range, load only if user is not logged in (T152462) +// $blockIsValid = !$tmpBlock->isExpired() && !$this->isLoggedIn(); +// $useBlockCookie = ( $config->get( 'CookieSetOnIpBlock' ) === true ); +// break; +// default: +// $blockIsValid = false; +// $useBlockCookie = false; +// } +// +// if ( $blockIsValid && $useBlockCookie ) { +// // Use the block. +// return $tmpBlock; +// } +// +// // If the block is not valid, remove the cookie. +// Block::clearCookie( $this->getRequest()->response() ); +// } else { +// // If the block doesn't exist, remove the cookie. +// Block::clearCookie( $this->getRequest()->response() ); +// } +// } +// return false; +// } +// +// /** +// * Whether the given IP is in a DNS blacklist. +// * +// * @param String $ip IP to check +// * @param boolean $checkWhitelist Whether to check the whitelist first +// * @return boolean True if blacklisted. +// */ +// public function isDnsBlacklisted( $ip, $checkWhitelist = false ) { +// global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist; +// +// if ( !$wgEnableDnsBlacklist || +// ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) +// ) { +// return false; +// } +// +// return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls ); +// } +// +// /** +// * Whether the given IP is in a given DNS blacklist. +// * +// * @param String $ip IP to check +// * @param String|array $bases Array of Strings: URL of the DNS blacklist +// * @return boolean True if blacklisted. +// */ +// public function inDnsBlacklist( $ip, $bases ) { +// $found = false; +// // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170) +// if ( IP::isIPv4( $ip ) ) { +// // Reverse IP, T23255 +// $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) ); +// +// foreach ( (array)$bases as $super ) { +// // Make hostname +// // If we have an access key, use that too (ProjectHoneypot, etc.) +// $basename = $super; +// if ( is_array( $super ) ) { +// if ( count( $super ) >= 2 ) { +// // Access key is 1, super URL is 0 +// $host = "{$super[1]}.$ipReversed.{$super[0]}"; +// } else { +// $host = "$ipReversed.{$super[0]}"; +// } +// $basename = $super[0]; +// } else { +// $host = "$ipReversed.$super"; +// } +// +// // Send query +// $ipList = gethostbynamel( $host ); +// +// if ( $ipList ) { +// wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" ); +// $found = true; +// break; +// } +// +// wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." ); +// } +// } +// +// return $found; +// } +// +// /** +// * Check if an IP address is in the local proxy list +// * +// * @param String $ip +// * +// * @return boolean +// */ +// public static function isLocallyBlockedProxy( $ip ) { +// global $wgProxyList; +// +// if ( !$wgProxyList ) { +// return false; +// } +// +// if ( !is_array( $wgProxyList ) ) { +// // Load values from the specified file +// $wgProxyList = array_map( 'trim', file( $wgProxyList ) ); +// } +// +// $resultProxyList = []; +// $deprecatedIPEntries = []; +// +// // backward compatibility: move all ip addresses in keys to values +// foreach ( $wgProxyList as $key => $value ) { +// $keyIsIP = IP::isIPAddress( $key ); +// $valueIsIP = IP::isIPAddress( $value ); +// if ( $keyIsIP && !$valueIsIP ) { +// $deprecatedIPEntries[] = $key; +// $resultProxyList[] = $key; +// } elseif ( $keyIsIP && $valueIsIP ) { +// $deprecatedIPEntries[] = $key; +// $resultProxyList[] = $key; +// $resultProxyList[] = $value; +// } else { +// $resultProxyList[] = $value; +// } +// } +// +// if ( $deprecatedIPEntries ) { +// wfDeprecated( +// 'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' . +// implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' ); +// } +// +// $proxyListIPSet = new IPSet( $resultProxyList ); +// return $proxyListIPSet->match( $ip ); +// } +// +// /** +// * Is this user subject to rate limiting? +// * +// * @return boolean True if rate limited +// */ +// public function isPingLimitable() { +// global $wgRateLimitsExcludedIPs; +// if ( IP::isInRanges( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) { +// // No other good way currently to disable rate limits +// // for specific IPs. :P +// // But this is a crappy hack and should die. +// return false; +// } +// return !$this->isAllowed( 'noratelimit' ); +// } +// +// /** +// * Primitive rate limits: enforce maximum actions per time period +// * to put a brake on flooding. +// * +// * The method generates both a generic profiling point and a per action one +// * (suffix being "-$action". +// * +// * @note When using a shared cache like memcached, IP-address +// * last-hit counters will be shared across wikis. +// * +// * @param String $action Action to enforce; 'edit' if unspecified +// * @param int $incrBy Positive amount to increment counter by [defaults to 1] +// * @return boolean True if a rate limiter was tripped +// */ +// public function pingLimiter( $action = 'edit', $incrBy = 1 ) { +// // Avoid PHP 7.1 warning of passing $this by reference +// $user = $this; +// // Call the 'PingLimiter' hook +// $result = false; +// if ( !Hooks::run( 'PingLimiter', [ &$user, $action, &$result, $incrBy ] ) ) { +// return $result; +// } +// +// global $wgRateLimits; +// if ( !isset( $wgRateLimits[$action] ) ) { +// return false; +// } +// +// $limits = array_merge( +// [ '&can-bypass' => true ], +// $wgRateLimits[$action] +// ); +// +// // Some groups shouldn't trigger the ping limiter, ever +// if ( $limits['&can-bypass'] && !$this->isPingLimitable() ) { +// return false; +// } +// +// $keys = []; +// $id = $this->getId(); +// $userLimit = false; +// $isNewbie = $this->isNewbie(); +// $cache = ObjectCache::getLocalClusterInstance(); +// +// if ( $id == 0 ) { +// // limits for anons +// if ( isset( $limits['anon'] ) ) { +// $keys[$cache->makeKey( 'limiter', $action, 'anon' )] = $limits['anon']; +// } +// } elseif ( isset( $limits['user'] ) ) { +// // limits for logged-in users +// $userLimit = $limits['user']; +// } +// +// // limits for anons and for newbie logged-in users +// if ( $isNewbie ) { +// // ip-based limits +// if ( isset( $limits['ip'] ) ) { +// $ip = $this->getRequest()->getIP(); +// $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip']; +// } +// // subnet-based limits +// if ( isset( $limits['subnet'] ) ) { +// $ip = $this->getRequest()->getIP(); +// $subnet = IP::getSubnet( $ip ); +// if ( $subnet !== false ) { +// $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet']; +// } +// } +// } +// +// // Check for group-specific permissions +// // If more than one group applies, use the group with the highest limit ratio (max/period) +// foreach ( $this->getGroups() as $group ) { +// if ( isset( $limits[$group] ) ) { +// if ( $userLimit === false +// || $limits[$group][0] / $limits[$group][1] > $userLimit[0] / $userLimit[1] +// ) { +// $userLimit = $limits[$group]; +// } +// } +// } +// +// // limits for newbie logged-in users (override all the normal user limits) +// if ( $id !== 0 && $isNewbie && isset( $limits['newbie'] ) ) { +// $userLimit = $limits['newbie']; +// } +// +// // Set the user limit key +// if ( $userLimit !== false ) { +// // phan is confused because &can-bypass's value is a boolean, so it assumes +// // that $userLimit is also a boolean here. +// // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring +// list( $max, $period ) = $userLimit; +// wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" ); +// $keys[$cache->makeKey( 'limiter', $action, 'user', $id )] = $userLimit; +// } +// +// // ip-based limits for all ping-limitable users +// if ( isset( $limits['ip-all'] ) ) { +// $ip = $this->getRequest()->getIP(); +// // ignore if user limit is more permissive +// if ( $isNewbie || $userLimit === false +// || $limits['ip-all'][0] / $limits['ip-all'][1] > $userLimit[0] / $userLimit[1] ) { +// $keys["mediawiki:limiter:$action:ip-all:$ip"] = $limits['ip-all']; +// } +// } +// +// // subnet-based limits for all ping-limitable users +// if ( isset( $limits['subnet-all'] ) ) { +// $ip = $this->getRequest()->getIP(); +// $subnet = IP::getSubnet( $ip ); +// if ( $subnet !== false ) { +// // ignore if user limit is more permissive +// if ( $isNewbie || $userLimit === false +// || $limits['ip-all'][0] / $limits['ip-all'][1] +// > $userLimit[0] / $userLimit[1] ) { +// $keys["mediawiki:limiter:$action:subnet-all:$subnet"] = $limits['subnet-all']; +// } +// } +// } +// +// $triggered = false; +// foreach ( $keys as $key => $limit ) { +// // phan is confused because &can-bypass's value is a boolean, so it assumes +// // that $userLimit is also a boolean here. +// // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring +// list( $max, $period ) = $limit; +// $summary = "(limit $max in {$period}s)"; +// $count = $cache->get( $key ); +// // Already pinged? +// if ( $count ) { +// if ( $count >= $max ) { +// wfDebugLog( 'ratelimit', "User '{$this->getName()}' " . +// "(IP {$this->getRequest()->getIP()}) tripped $key at $count $summary" ); +// $triggered = true; +// } else { +// wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" ); +// } +// } else { +// wfDebug( __METHOD__ . ": adding record for $key $summary\n" ); +// if ( $incrBy > 0 ) { +// $cache->add( $key, 0, intval( $period ) ); // first ping +// } +// } +// if ( $incrBy > 0 ) { +// $cache->incr( $key, $incrBy ); +// } +// } +// +// return $triggered; +// } +// +// /** +// * Check if user is blocked +// * +// * @param boolean $fromReplica Whether to check the replica DB instead of +// * the master. Hacked from false due to horrible probs on site. +// * @return boolean True if blocked, false otherwise +// */ +// public function isBlocked( $fromReplica = true ) { +// return $this->getBlock( $fromReplica ) instanceof Block && +// $this->getBlock()->appliesToRight( 'edit' ); +// } +// +// /** +// * Get the block affecting the user, or null if the user is not blocked +// * +// * @param boolean $fromReplica Whether to check the replica DB instead of the master +// * @return Block|null +// */ +// public function getBlock( $fromReplica = true ) { +// $this->getBlockedStatus( $fromReplica ); +// return $this->mBlock instanceof Block ? $this->mBlock : null; +// } +// +// /** +// * Check if user is blocked from editing a particular article +// * +// * @param Title $title Title to check +// * @param boolean $fromReplica Whether to check the replica DB instead of the master +// * @return boolean +// * @throws MWException +// * +// * @deprecated since 1.33, +// * use MediaWikiServices::getInstance()->getPermissionManager()->isBlockedFrom(..) +// * +// */ +// public function isBlockedFrom( $title, $fromReplica = false ) { +// return MediaWikiServices::getInstance()->getPermissionManager() +// ->isBlockedFrom( $this, $title, $fromReplica ); +// } +// +// /** +// * If user is blocked, return the name of the user who placed the block +// * @return String Name of blocker +// */ +// public function blockedBy() { +// $this->getBlockedStatus(); +// return $this->mBlockedby; +// } +// +// /** +// * If user is blocked, return the specified reason for the block +// * @return String Blocking reason +// */ +// public function blockedFor() { +// $this->getBlockedStatus(); +// return $this->mBlockreason; +// } +// +// /** +// * If user is blocked, return the ID for the block +// * @return int Block ID +// */ +// public function getBlockId() { +// $this->getBlockedStatus(); +// return ( $this->mBlock ? $this->mBlock->getId() : false ); +// } +// +// /** +// * Check if user is blocked on all wikis. +// * Do not use for actual edit permission checks! +// * This is intended for quick UI checks. +// * +// * @param String $ip IP address, uses current client if none given +// * @return boolean True if blocked, false otherwise +// */ +// public function isBlockedGlobally( $ip = '' ) { +// return $this->getGlobalBlock( $ip ) instanceof Block; +// } +// +// /** +// * Check if user is blocked on all wikis. +// * Do not use for actual edit permission checks! +// * This is intended for quick UI checks. +// * +// * @param String $ip IP address, uses current client if none given +// * @return Block|null Block Object if blocked, null otherwise +// * @throws FatalError +// * @throws MWException +// */ +// public function getGlobalBlock( $ip = '' ) { +// if ( $this->mGlobalBlock !== null ) { +// return $this->mGlobalBlock ?: null; +// } +// // User is already an IP? +// if ( IP::isIPAddress( $this->getName() ) ) { +// $ip = $this->getName(); +// } elseif ( !$ip ) { +// $ip = $this->getRequest()->getIP(); +// } +// // Avoid PHP 7.1 warning of passing $this by reference +// $user = $this; +// $blocked = false; +// $block = null; +// Hooks::run( 'UserIsBlockedGlobally', [ &$user, $ip, &$blocked, &$block ] ); +// +// if ( $blocked && $block === null ) { +// // back-compat: UserIsBlockedGlobally didn't have $block param first +// $block = new Block( [ +// 'address' => $ip, +// 'systemBlock' => 'global-block' +// ] ); +// } +// +// $this->mGlobalBlock = $blocked ? $block : false; +// return $this->mGlobalBlock ?: null; +// } +// +// /** +// * Check if user account is locked +// * +// * @return boolean True if locked, false otherwise +// */ +// public function isLocked() { +// if ( $this->mLocked !== null ) { +// return $this->mLocked; +// } +// // Reset for hook +// $this->mLocked = false; +// Hooks::run( 'UserIsLocked', [ $this, &$this->mLocked ] ); +// return $this->mLocked; +// } +// +// /** +// * Check if user account is hidden +// * +// * @return boolean True if hidden, false otherwise +// */ +// public function isHidden() { +// if ( $this->mHideName !== null ) { +// return (boolean)$this->mHideName; +// } +// $this->getBlockedStatus(); +// if ( !$this->mHideName ) { +// // Reset for hook +// $this->mHideName = false; +// Hooks::run( 'UserIsHidden', [ $this, &$this->mHideName ] ); +// } +// return (boolean)$this->mHideName; +// } +// +// /** +// * Get the user's ID. +// * @return int The user's ID; 0 if the user is anonymous or nonexistent +// */ +// public function getId() { +// if ( $this->mId === null && $this->mName !== null && self::isIP( $this->mName ) ) { +// // Special case, we know the user is anonymous +// return 0; +// } +// +// if ( !$this->isItemLoaded( 'id' ) ) { +// // Don't load if this was initialized from an ID +// $this->load(); +// } +// +// return (int)$this->mId; +// } +// +// /** +// * Set the user and reload all fields according to a given ID +// * @param int $v User ID to reload +// */ +// public function setId( $v ) { +// $this->mId = $v; +// $this->clearInstanceCache( 'id' ); +// } +// +// /** +// * Get the user name, or the IP of an anonymous user +// * @return String User's name or IP address +// */ +// public function getName() { +// if ( $this->isItemLoaded( 'name', 'only' ) ) { +// // Special case optimisation +// return $this->mName; +// } +// +// $this->load(); +// if ( $this->mName === false ) { +// // Clean up IPs +// $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() ); +// } +// +// return $this->mName; +// } +// +// /** +// * Set the user name. +// * +// * This does not reload fields from the database according to the given +// * name. Rather, it is used to create a temporary "nonexistent user" for +// * later addition to the database. It can also be used to set the IP +// * address for an anonymous user to something other than the current +// * remote IP. +// * +// * @note User::newFromName() has roughly the same function, when the named user +// * does not exist. +// * @param String $str New user name to set +// */ +// public function setName( $str ) { +// $this->load(); +// $this->mName = $str; +// } +// +// /** +// * Get the user's actor ID. +// * @since 1.31 +// * @param IDatabase|null $dbw Assign a new actor ID, using this DB handle, if none exists +// * @return int The actor's ID, or 0 if no actor ID exists and $dbw was null +// */ +// public function getActorId( IDatabase $dbw = null ) { +// global $wgActorTableSchemaMigrationStage; +// +// // Technically we should always return 0 without SCHEMA_COMPAT_READ_NEW, +// // but it does little harm and might be needed for write callers loading a User. +// if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) ) { +// return 0; +// } +// +// if ( !$this->isItemLoaded( 'actor' ) ) { +// $this->load(); +// } +// +// // Currently $this->mActorId might be null if $this was loaded from a +// // cache entry that was written when $wgActorTableSchemaMigrationStage +// // was SCHEMA_COMPAT_OLD. Once that is no longer a possibility (i.e. when +// // User::VERSION is incremented after $wgActorTableSchemaMigrationStage +// // has been removed), that condition may be removed. +// if ( $this->mActorId === null || !$this->mActorId && $dbw ) { +// $q = [ +// 'actor_user' => $this->getId() ?: null, +// 'actor_name' => (String)$this->getName(), +// ]; +// if ( $dbw ) { +// if ( $q['actor_user'] === null && self::isUsableName( $q['actor_name'] ) ) { +// throw new CannotCreateActorException( +// 'Cannot create an actor for a usable name that is not an existing user' +// ); +// } +// if ( $q['actor_name'] === '' ) { +// throw new CannotCreateActorException( 'Cannot create an actor for a user with no name' ); +// } +// $dbw->insert( 'actor', $q, __METHOD__, [ 'IGNORE' ] ); +// if ( $dbw->affectedRows() ) { +// $this->mActorId = (int)$dbw->insertId(); +// } else { +// // Outdated cache? +// // Use LOCK IN SHARE MODE to bypass any MySQL REPEATABLE-READ snapshot. +// $this->mActorId = (int)$dbw->selectField( +// 'actor', +// 'actor_id', +// $q, +// __METHOD__, +// [ 'LOCK IN SHARE MODE' ] +// ); +// if ( !$this->mActorId ) { +// throw new CannotCreateActorException( +// "Cannot create actor ID for user_id={$this->getId()} user_name={$this->getName()}" +// ); +// } +// } +// $this->invalidateCache(); +// } else { +// list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->queryFlagsUsed ); +// $db = wfGetDB( $index ); +// $this->mActorId = (int)$db->selectField( 'actor', 'actor_id', $q, __METHOD__, $options ); +// } +// $this->setItemLoaded( 'actor' ); +// } +// +// return (int)$this->mActorId; +// } +// +// /** +// * Get the user's name escaped by underscores. +// * @return String Username escaped by underscores. +// */ +// public function getTitleKey() { +// return str_replace( ' ', '_', $this->getName() ); +// } +// +// /** +// * Check if the user has new messages. +// * @return boolean True if the user has new messages +// */ +// public function getNewtalk() { +// $this->load(); +// +// // Load the newtalk status if it is unloaded (mNewtalk=-1) +// if ( $this->mNewtalk === -1 ) { +// $this->mNewtalk = false; # reset talk page status +// +// // Check memcached separately for anons, who have no +// // entire User Object stored in there. +// if ( !$this->mId ) { +// global $wgDisableAnonTalk; +// if ( $wgDisableAnonTalk ) { +// // Anon newtalk disabled by configuration. +// $this->mNewtalk = false; +// } else { +// $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() ); +// } +// } else { +// $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId ); +// } +// } +// +// return (boolean)$this->mNewtalk; +// } +// +// /** +// * Return the data needed to construct links for new talk page message +// * alerts. If there are new messages, this will return an associative array +// * with the following data: +// * wiki: The database name of the wiki +// * link: Root-relative link to the user's talk page +// * rev: The last talk page revision that the user has seen or null. This +// * is useful for building diff links. +// * If there are no new messages, it returns an empty array. +// * @note This function was designed to accomodate multiple talk pages, but +// * currently only returns a single link and revision. +// * @return array +// */ +// public function getNewMessageLinks() { +// // Avoid PHP 7.1 warning of passing $this by reference +// $user = $this; +// $talks = []; +// if ( !Hooks::run( 'UserRetrieveNewTalks', [ &$user, &$talks ] ) ) { +// return $talks; +// } +// +// if ( !$this->getNewtalk() ) { +// return []; +// } +// $utp = $this->getTalkPage(); +// $dbr = wfGetDB( DB_REPLICA ); +// // Get the "last viewed rev" timestamp from the oldest message notification +// $timestamp = $dbr->selectField( 'user_newtalk', +// 'MIN(user_last_timestamp)', +// $this->isAnon() ? [ 'user_ip' => $this->getName() ] : [ 'user_id' => $this->getId() ], +// __METHOD__ ); +// $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null; +// return [ +// [ +// 'wiki' => WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() ), +// 'link' => $utp->getLocalURL(), +// 'rev' => $rev +// ] +// ]; +// } +// +// /** +// * Get the revision ID for the last talk page revision viewed by the talk +// * page owner. +// * @return int|null Revision ID or null +// */ +// public function getNewMessageRevisionId() { +// $newMessageRevisionId = null; +// $newMessageLinks = $this->getNewMessageLinks(); +// +// // Note: getNewMessageLinks() never returns more than a single link +// // and it is always for the same wiki, but we double-check here in +// // case that changes some time in the future. +// if ( $newMessageLinks && count( $newMessageLinks ) === 1 +// && WikiMap::isCurrentWikiId( $newMessageLinks[0]['wiki'] ) +// && $newMessageLinks[0]['rev'] +// ) { +// /** @var Revision $newMessageRevision */ +// $newMessageRevision = $newMessageLinks[0]['rev']; +// $newMessageRevisionId = $newMessageRevision->getId(); +// } +// +// return $newMessageRevisionId; +// } +// +// /** +// * Internal uncached check for new messages +// * +// * @see getNewtalk() +// * @param String $field 'user_ip' for anonymous users, 'user_id' otherwise +// * @param String|int $id User's IP address for anonymous users, User ID otherwise +// * @return boolean True if the user has new messages +// */ +// protected function checkNewtalk( $field, $id ) { +// $dbr = wfGetDB( DB_REPLICA ); +// +// $ok = $dbr->selectField( 'user_newtalk', $field, [ $field => $id ], __METHOD__ ); +// +// return $ok !== false; +// } +// +// /** +// * Add or update the new messages flag +// * @param String $field 'user_ip' for anonymous users, 'user_id' otherwise +// * @param String|int $id User's IP address for anonymous users, User ID otherwise +// * @param Revision|null $curRev New, as yet unseen revision of the user talk page. Ignored if null. +// * @return boolean True if successful, false otherwise +// */ +// protected function updateNewtalk( $field, $id, $curRev = null ) { +// // Get timestamp of the talk page revision prior to the current one +// $prevRev = $curRev ? $curRev->getPrevious() : false; +// $ts = $prevRev ? $prevRev->getTimestamp() : null; +// // Mark the user as having new messages since this revision +// $dbw = wfGetDB( DB_MASTER ); +// $dbw->insert( 'user_newtalk', +// [ $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ], +// __METHOD__, +// 'IGNORE' ); +// if ( $dbw->affectedRows() ) { +// wfDebug( __METHOD__ . ": set on ($field, $id)\n" ); +// return true; +// } +// +// wfDebug( __METHOD__ . " already set ($field, $id)\n" ); +// return false; +// } +// +// /** +// * Clear the new messages flag for the given user +// * @param String $field 'user_ip' for anonymous users, 'user_id' otherwise +// * @param String|int $id User's IP address for anonymous users, User ID otherwise +// * @return boolean True if successful, false otherwise +// */ +// protected function deleteNewtalk( $field, $id ) { +// $dbw = wfGetDB( DB_MASTER ); +// $dbw->delete( 'user_newtalk', +// [ $field => $id ], +// __METHOD__ ); +// if ( $dbw->affectedRows() ) { +// wfDebug( __METHOD__ . ": killed on ($field, $id)\n" ); +// return true; +// } +// +// wfDebug( __METHOD__ . ": already gone ($field, $id)\n" ); +// return false; +// } +// +// /** +// * Update the 'You have new messages!' status. +// * @param boolean $val Whether the user has new messages +// * @param Revision|null $curRev New, as yet unseen revision of the user talk +// * page. Ignored if null or !$val. +// */ +// public function setNewtalk( $val, $curRev = null ) { +// if ( wfReadOnly() ) { +// return; +// } +// +// $this->load(); +// $this->mNewtalk = $val; +// +// if ( $this->isAnon() ) { +// $field = 'user_ip'; +// $id = $this->getName(); +// } else { +// $field = 'user_id'; +// $id = $this->getId(); +// } +// +// if ( $val ) { +// $changed = $this->updateNewtalk( $field, $id, $curRev ); +// } else { +// $changed = $this->deleteNewtalk( $field, $id ); +// } +// +// if ( $changed ) { +// $this->invalidateCache(); +// } +// } +// +// /** +// * Generate a current or new-future timestamp to be stored in the +// * user_touched field when we update things. +// * +// * @return String Timestamp in TS_MW format +// */ +// private function newTouchedTimestamp() { +// $time = time(); +// if ( $this->mTouched ) { +// $time = max( $time, wfTimestamp( TS_UNIX, $this->mTouched ) + 1 ); +// } +// +// return wfTimestamp( TS_MW, $time ); +// } +// +// /** +// * Clear user data from memcached +// * +// * Use after applying updates to the database; caller's +// * responsibility to update user_touched if appropriate. +// * +// * Called implicitly from invalidateCache() and saveSettings(). +// * +// * @param String $mode Use 'refresh' to clear now; otherwise before DB commit +// */ +// public function clearSharedCache( $mode = 'changed' ) { +// if ( !$this->getId() ) { +// return; +// } +// +// $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); +// $key = $this->getCacheKey( $cache ); +// if ( $mode === 'refresh' ) { +// $cache->delete( $key, 1 ); +// } else { +// $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); +// if ( $lb->hasOrMadeRecentMasterChanges() ) { +// $lb->getConnection( DB_MASTER )->onTransactionPreCommitOrIdle( +// function () use ( $cache, $key ) { +// $cache->delete( $key ); +// }, +// __METHOD__ +// ); +// } else { +// $cache->delete( $key ); +// } +// } +// } +// +// /** +// * Immediately touch the user data cache for this account +// * +// * Calls touch() and removes account data from memcached +// */ +// public function invalidateCache() { +// $this->touch(); +// $this->clearSharedCache(); +// } +// +// /** +// * Update the "touched" timestamp for the user +// * +// * This is useful on various login/logout events when making sure that +// * a browser or proxy that has multiple tenants does not suffer cache +// * pollution where the new user sees the old users content. The value +// * of getTouched() is checked when determining 304 vs 200 responses. +// * Unlike invalidateCache(), this preserves the User Object cache and +// * avoids database writes. +// * +// * @since 1.25 +// */ +// public function touch() { +// $id = $this->getId(); +// if ( $id ) { +// $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); +// $key = $cache->makeKey( 'user-quicktouched', 'id', $id ); +// $cache->touchCheckKey( $key ); +// $this->mQuickTouched = null; +// } +// } +// +// /** +// * Validate the cache for this account. +// * @param String $timestamp A timestamp in TS_MW format +// * @return boolean +// */ +// public function validateCache( $timestamp ) { +// return ( $timestamp >= $this->getTouched() ); +// } +// +// /** +// * Get the user touched timestamp +// * +// * Use this value only to validate caches via inequalities +// * such as in the case of HTTP If-Modified-Since response logic +// * +// * @return String TS_MW Timestamp +// */ +// public function getTouched() { +// $this->load(); +// +// if ( $this->mId ) { +// if ( $this->mQuickTouched === null ) { +// $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); +// $key = $cache->makeKey( 'user-quicktouched', 'id', $this->mId ); +// +// $this->mQuickTouched = wfTimestamp( TS_MW, $cache->getCheckKeyTime( $key ) ); +// } +// +// return max( $this->mTouched, $this->mQuickTouched ); +// } +// +// return $this->mTouched; +// } +// +// /** +// * Get the user_touched timestamp field (time of last DB updates) +// * @return String TS_MW Timestamp +// * @since 1.26 +// */ +// public function getDBTouched() { +// $this->load(); +// +// return $this->mTouched; +// } +// +// /** +// * Set the password and reset the random token. +// * Calls through to authentication plugin if necessary; +// * will have no effect if the auth plugin refuses to +// * pass the change through or if the legal password +// * checks fail. +// * +// * As a special case, setting the password to null +// * wipes it, so the account cannot be logged in until +// * a new password is set, for instance via e-mail. +// * +// * @deprecated since 1.27, use AuthManager instead +// * @param String $str New password to set +// * @throws PasswordError On failure +// * @return boolean +// */ +// public function setPassword( $str ) { +// wfDeprecated( __METHOD__, '1.27' ); +// return $this->setPasswordInternal( $str ); +// } +// +// /** +// * Set the password and reset the random token unconditionally. +// * +// * @deprecated since 1.27, use AuthManager instead +// * @param String|null $str New password to set or null to set an invalid +// * password hash meaning that the user will not be able to log in +// * through the web interface. +// */ +// public function setInternalPassword( $str ) { +// wfDeprecated( __METHOD__, '1.27' ); +// $this->setPasswordInternal( $str ); +// } +// +// /** +// * Actually set the password and such +// * @since 1.27 cannot set a password for a user not in the database +// * @param String|null $str New password to set or null to set an invalid +// * password hash meaning that the user will not be able to log in +// * through the web interface. +// * @return boolean Success +// */ +// private function setPasswordInternal( $str ) { +// $manager = AuthManager::singleton(); +// +// // If the user doesn't exist yet, fail +// if ( !$manager->userExists( $this->getName() ) ) { +// throw new LogicException( 'Cannot set a password for a user that is not in the database.' ); +// } +// +// $status = $this->changeAuthenticationData( [ +// 'username' => $this->getName(), +// 'password' => $str, +// 'retype' => $str, +// ] ); +// if ( !$status->isGood() ) { +// \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) +// ->info( __METHOD__ . ': Password change rejected: ' +// . $status->getWikiText( null, null, 'en' ) ); +// return false; +// } +// +// $this->setOption( 'watchlisttoken', false ); +// SessionManager::singleton()->invalidateSessionsForUser( $this ); +// +// return true; +// } +// +// /** +// * Changes credentials of the user. +// * +// * This is a convenience wrapper around AuthManager::changeAuthenticationData. +// * Note that this can return a status that isOK() but not isGood() on certain types of failures, +// * e.g. when no provider handled the change. +// * +// * @param array $data A set of authentication data in fieldname => value format. This is the +// * same data you would pass the changeauthenticationdata API - 'username', 'password' etc. +// * @return Status +// * @since 1.27 +// */ +// public function changeAuthenticationData( array $data ) { +// $manager = AuthManager::singleton(); +// $reqs = $manager->getAuthenticationRequests( AuthManager::ACTION_CHANGE, $this ); +// $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data ); +// +// $status = Status::newGood( 'ignored' ); +// foreach ( $reqs as $req ) { +// $status->merge( $manager->allowsAuthenticationDataChange( $req ), true ); +// } +// if ( $status->getValue() === 'ignored' ) { +// $status->warning( 'authenticationdatachange-ignored' ); +// } +// +// if ( $status->isGood() ) { +// foreach ( $reqs as $req ) { +// $manager->changeAuthenticationData( $req ); +// } +// } +// return $status; +// } +// +// /** +// * Get the user's current token. +// * @param boolean $forceCreation Force the generation of a new token if the +// * user doesn't have one (default=true for backwards compatibility). +// * @return String|null Token +// */ +// public function getToken( $forceCreation = true ) { +// global $wgAuthenticationTokenVersion; +// +// $this->load(); +// if ( !$this->mToken && $forceCreation ) { +// $this->setToken(); +// } +// +// if ( !$this->mToken ) { +// // The user doesn't have a token, return null to indicate that. +// return null; +// } +// +// if ( $this->mToken === self::INVALID_TOKEN ) { +// // We return a random value here so existing token checks are very +// // likely to fail. +// return MWCryptRand::generateHex( self::TOKEN_LENGTH ); +// } +// +// if ( $wgAuthenticationTokenVersion === null ) { +// // $wgAuthenticationTokenVersion not in use, so return the raw secret +// return $this->mToken; +// } +// +// // $wgAuthenticationTokenVersion in use, so hmac it. +// $ret = MWCryptHash::hmac( $wgAuthenticationTokenVersion, $this->mToken, false ); +// +// // The raw hash can be overly long. Shorten it up. +// $len = max( 32, self::TOKEN_LENGTH ); +// if ( strlen( $ret ) < $len ) { +// // Should never happen, even md5 is 128 bits +// throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' ); +// } +// +// return substr( $ret, -$len ); +// } +// +// /** +// * Set the random token (used for persistent authentication) +// * Called from loadDefaults() among other places. +// * +// * @param String|boolean $token If specified, set the token to this value +// */ +// public function setToken( $token = false ) { +// $this->load(); +// if ( $this->mToken === self::INVALID_TOKEN ) { +// \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) +// ->debug( __METHOD__ . ": Ignoring attempt to set token for system user \"$this\"" ); +// } elseif ( !$token ) { +// $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH ); +// } else { +// $this->mToken = $token; +// } +// } +// +// /** +// * Set the password for a password reminder or new account email +// * +// * @deprecated Removed in 1.27. Use PasswordReset instead. +// * @param String $str New password to set or null to set an invalid +// * password hash meaning that the user will not be able to use it +// * @param boolean $throttle If true, reset the throttle timestamp to the present +// */ +// public function setNewpassword( $str, $throttle = true ) { +// throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' ); +// } +// +// /** +// * Get the user's e-mail address +// * @return String User's email address +// */ +// public function getEmail() { +// $this->load(); +// Hooks::run( 'UserGetEmail', [ $this, &$this->mEmail ] ); +// return $this->mEmail; +// } +// +// /** +// * Get the timestamp of the user's e-mail authentication +// * @return String TS_MW timestamp +// */ +// public function getEmailAuthenticationTimestamp() { +// $this->load(); +// Hooks::run( 'UserGetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] ); +// return $this->mEmailAuthenticated; +// } +// +// /** +// * Set the user's e-mail address +// * @param String $str New e-mail address +// */ +// public function setEmail( $str ) { +// $this->load(); +// if ( $str == $this->mEmail ) { +// return; +// } +// $this->invalidateEmail(); +// $this->mEmail = $str; +// Hooks::run( 'UserSetEmail', [ $this, &$this->mEmail ] ); +// } +// +// /** +// * Set the user's e-mail address and a confirmation mail if needed. +// * +// * @since 1.20 +// * @param String $str New e-mail address +// * @return Status +// */ +// public function setEmailWithConfirmation( $str ) { +// global $wgEnableEmail, $wgEmailAuthentication; +// +// if ( !$wgEnableEmail ) { +// return Status::newFatal( 'emaildisabled' ); +// } +// +// $oldaddr = $this->getEmail(); +// if ( $str === $oldaddr ) { +// return Status::newGood( true ); +// } +// +// $type = $oldaddr != '' ? 'changed' : 'set'; +// $notificationResult = null; +// +// if ( $wgEmailAuthentication && $type === 'changed' ) { +// // Send the user an email notifying the user of the change in registered +// // email address on their previous email address +// $change = $str != '' ? 'changed' : 'removed'; +// $notificationResult = $this->sendMail( +// wfMessage( 'notificationemail_subject_' . $change )->text(), +// wfMessage( 'notificationemail_body_' . $change, +// $this->getRequest()->getIP(), +// $this->getName(), +// $str )->text() +// ); +// } +// +// $this->setEmail( $str ); +// +// if ( $str !== '' && $wgEmailAuthentication ) { +// // Send a confirmation request to the new address if needed +// $result = $this->sendConfirmationMail( $type ); +// +// if ( $notificationResult !== null ) { +// $result->merge( $notificationResult ); +// } +// +// if ( $result->isGood() ) { +// // Say to the caller that a confirmation and notification mail has been sent +// $result->value = 'eauth'; +// } +// } else { +// $result = Status::newGood( true ); +// } +// +// return $result; +// } +// +// /** +// * Get the user's real name +// * @return String User's real name +// */ +// public function getRealName() { +// if ( !$this->isItemLoaded( 'realname' ) ) { +// $this->load(); +// } +// +// return $this->mRealName; +// } +// +// /** +// * Set the user's real name +// * @param String $str New real name +// */ +// public function setRealName( $str ) { +// $this->load(); +// $this->mRealName = $str; +// } +// +// /** +// * Get the user's current setting for a given option. +// * +// * @param String $oname The option to check +// * @param String|array|null $defaultOverride A default value returned if the option does not exist +// * @param boolean $ignoreHidden Whether to ignore the effects of $wgHiddenPrefs +// * @return String|array|int|null User's current value for the option +// * @see getBoolOption() +// * @see getIntOption() +// */ +// public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) { +// global $wgHiddenPrefs; +// $this->loadOptions(); +// +// # We want 'disabled' preferences to always behave as the default value for +// # users, even if they have set the option explicitly in their settings (ie they +// # set it, and then it was disabled removing their ability to change it). But +// # we don't want to erase the preferences in the database in case the preference +// # is re-enabled again. So don't touch $mOptions, just override the returned value +// if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) { +// return self::getDefaultOption( $oname ); +// } +// +// if ( array_key_exists( $oname, $this->mOptions ) ) { +// return $this->mOptions[$oname]; +// } +// +// return $defaultOverride; +// } +// +// /** +// * Get all user's options +// * +// * @param int $flags Bitwise combination of: +// * User::GETOPTIONS_EXCLUDE_DEFAULTS Exclude user options that are set +// * to the default value. (Since 1.25) +// * @return array +// */ +// public function getOptions( $flags = 0 ) { +// global $wgHiddenPrefs; +// $this->loadOptions(); +// $options = $this->mOptions; +// +// # We want 'disabled' preferences to always behave as the default value for +// # users, even if they have set the option explicitly in their settings (ie they +// # set it, and then it was disabled removing their ability to change it). But +// # we don't want to erase the preferences in the database in case the preference +// # is re-enabled again. So don't touch $mOptions, just override the returned value +// foreach ( $wgHiddenPrefs as $pref ) { +// $default = self::getDefaultOption( $pref ); +// if ( $default !== null ) { +// $options[$pref] = $default; +// } +// } +// +// if ( $flags & self::GETOPTIONS_EXCLUDE_DEFAULTS ) { +// $options = array_diff_assoc( $options, self::getDefaultOptions() ); +// } +// +// return $options; +// } +// +// /** +// * Get the user's current setting for a given option, as a boolean value. +// * +// * @param String $oname The option to check +// * @return boolean User's current value for the option +// * @see getOption() +// */ +// public function getBoolOption( $oname ) { +// return (boolean)$this->getOption( $oname ); +// } +// +// /** +// * Get the user's current setting for a given option, as an integer value. +// * +// * @param String $oname The option to check +// * @param int $defaultOverride A default value returned if the option does not exist +// * @return int User's current value for the option +// * @see getOption() +// */ +// public function getIntOption( $oname, $defaultOverride = 0 ) { +// $val = $this->getOption( $oname ); +// if ( $val == '' ) { +// $val = $defaultOverride; +// } +// return intval( $val ); +// } +// +// /** +// * Set the given option for a user. +// * +// * You need to call saveSettings() to actually write to the database. +// * +// * @param String $oname The option to set +// * @param mixed $val New value to set +// */ +// public function setOption( $oname, $val ) { +// $this->loadOptions(); +// +// // Explicitly NULL values should refer to defaults +// if ( is_null( $val ) ) { +// $val = self::getDefaultOption( $oname ); +// } +// +// $this->mOptions[$oname] = $val; +// } +// +// /** +// * Get a token stored in the preferences (like the watchlist one), +// * resetting it if it's empty (and saving changes). +// * +// * @param String $oname The option name to retrieve the token from +// * @return String|boolean User's current value for the option, or false if this option is disabled. +// * @see resetTokenFromOption() +// * @see getOption() +// * @deprecated since 1.26 Applications should use the OAuth extension +// */ +// public function getTokenFromOption( $oname ) { +// global $wgHiddenPrefs; +// +// $id = $this->getId(); +// if ( !$id || in_array( $oname, $wgHiddenPrefs ) ) { +// return false; +// } +// +// $token = $this->getOption( $oname ); +// if ( !$token ) { +// // Default to a value based on the user token to avoid space +// // wasted on storing tokens for all users. When this option +// // is set manually by the user, only then is it stored. +// $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() ); +// } +// +// return $token; +// } +// +// /** +// * Reset a token stored in the preferences (like the watchlist one). +// * *Does not* save user's preferences (similarly to setOption()). +// * +// * @param String $oname The option name to reset the token in +// * @return String|boolean New token value, or false if this option is disabled. +// * @see getTokenFromOption() +// * @see setOption() +// */ +// public function resetTokenFromOption( $oname ) { +// global $wgHiddenPrefs; +// if ( in_array( $oname, $wgHiddenPrefs ) ) { +// return false; +// } +// +// $token = MWCryptRand::generateHex( 40 ); +// $this->setOption( $oname, $token ); +// return $token; +// } +// +// /** +// * Return a list of the types of user options currently returned by +// * User::getOptionKinds(). +// * +// * Currently, the option kinds are: +// * - 'registered' - preferences which are registered in core MediaWiki or +// * by extensions using the UserGetDefaultOptions hook. +// * - 'registered-multiselect' - as above, using the 'multiselect' type. +// * - 'registered-checkmatrix' - as above, using the 'checkmatrix' type. +// * - 'userjs' - preferences with names starting with 'userjs-', intended to +// * be used by user scripts. +// * - 'special' - "preferences" that are not accessible via User::getOptions +// * or User::setOptions. +// * - 'unused' - preferences about which MediaWiki doesn't know anything. +// * These are usually legacy options, removed in newer versions. +// * +// * The API (and possibly others) use this function to determine the possible +// * option types for validation purposes, so make sure to update this when a +// * new option kind is added. +// * +// * @see User::getOptionKinds +// * @return array Option kinds +// */ +// public static function listOptionKinds() { +// return [ +// 'registered', +// 'registered-multiselect', +// 'registered-checkmatrix', +// 'userjs', +// 'special', +// 'unused' +// ]; +// } +// +// /** +// * Return an associative array mapping preferences keys to the kind of a preference they're +// * used for. Different kinds are handled differently when setting or reading preferences. +// * +// * See User::listOptionKinds for the list of valid option types that can be provided. +// * +// * @see User::listOptionKinds +// * @param IContextSource $context +// * @param array|null $options Assoc. array with options keys to check as keys. +// * Defaults to $this->mOptions. +// * @return array The key => kind mapping data +// */ +// public function getOptionKinds( IContextSource $context, $options = null ) { +// $this->loadOptions(); +// if ( $options === null ) { +// $options = $this->mOptions; +// } +// +// $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory(); +// $prefs = $preferencesFactory->getFormDescriptor( $this, $context ); +// $mapping = []; +// +// // Pull out the "special" options, so they don't get converted as +// // multiselect or checkmatrix. +// $specialOptions = array_fill_keys( $preferencesFactory->getSaveBlacklist(), true ); +// foreach ( $specialOptions as $name => $value ) { +// unset( $prefs[$name] ); +// } +// +// // Multiselect and checkmatrix options are stored in the database with +// // one key per option, each having a boolean value. Extract those keys. +// $multiselectOptions = []; +// foreach ( $prefs as $name => $info ) { +// if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) || +// ( isset( $info['class'] ) && $info['class'] == HTMLMultiSelectField::class ) ) { +// $opts = HTMLFormField::flattenOptions( $info['options'] ); +// $prefix = $info['prefix'] ?? $name; +// +// foreach ( $opts as $value ) { +// $multiselectOptions["$prefix$value"] = true; +// } +// +// unset( $prefs[$name] ); +// } +// } +// $checkmatrixOptions = []; +// foreach ( $prefs as $name => $info ) { +// if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) || +// ( isset( $info['class'] ) && $info['class'] == HTMLCheckMatrix::class ) ) { +// $columns = HTMLFormField::flattenOptions( $info['columns'] ); +// $rows = HTMLFormField::flattenOptions( $info['rows'] ); +// $prefix = $info['prefix'] ?? $name; +// +// foreach ( $columns as $column ) { +// foreach ( $rows as $row ) { +// $checkmatrixOptions["$prefix$column-$row"] = true; +// } +// } +// +// unset( $prefs[$name] ); +// } +// } +// +// // $value is ignored +// foreach ( $options as $key => $value ) { +// if ( isset( $prefs[$key] ) ) { +// $mapping[$key] = 'registered'; +// } elseif ( isset( $multiselectOptions[$key] ) ) { +// $mapping[$key] = 'registered-multiselect'; +// } elseif ( isset( $checkmatrixOptions[$key] ) ) { +// $mapping[$key] = 'registered-checkmatrix'; +// } elseif ( isset( $specialOptions[$key] ) ) { +// $mapping[$key] = 'special'; +// } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) { +// $mapping[$key] = 'userjs'; +// } else { +// $mapping[$key] = 'unused'; +// } +// } +// +// return $mapping; +// } +// +// /** +// * Reset certain (or all) options to the site defaults +// * +// * The optional parameter determines which kinds of preferences will be reset. +// * Supported values are everything that can be reported by getOptionKinds() +// * and 'all', which forces a reset of *all* preferences and overrides everything else. +// * +// * @param array|String $resetKinds Which kinds of preferences to reset. Defaults to +// * array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ) +// * for backwards-compatibility. +// * @param IContextSource|null $context Context source used when $resetKinds +// * does not contain 'all', passed to getOptionKinds(). +// * Defaults to RequestContext::getMain() when null. +// */ +// public function resetOptions( +// $resetKinds = [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ], +// IContextSource $context = null +// ) { +// $this->load(); +// $defaultOptions = self::getDefaultOptions(); +// +// if ( !is_array( $resetKinds ) ) { +// $resetKinds = [ $resetKinds ]; +// } +// +// if ( in_array( 'all', $resetKinds ) ) { +// $newOptions = $defaultOptions; +// } else { +// if ( $context === null ) { +// $context = RequestContext::getMain(); +// } +// +// $optionKinds = $this->getOptionKinds( $context ); +// $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() ); +// $newOptions = []; +// +// // Use default values for the options that should be deleted, and +// // copy old values for the ones that shouldn't. +// foreach ( $this->mOptions as $key => $value ) { +// if ( in_array( $optionKinds[$key], $resetKinds ) ) { +// if ( array_key_exists( $key, $defaultOptions ) ) { +// $newOptions[$key] = $defaultOptions[$key]; +// } +// } else { +// $newOptions[$key] = $value; +// } +// } +// } +// +// Hooks::run( 'UserResetAllOptions', [ $this, &$newOptions, $this->mOptions, $resetKinds ] ); +// +// $this->mOptions = $newOptions; +// $this->mOptionsLoaded = true; +// } +// +// /** +// * Get the user's preferred date format. +// * @return String User's preferred date format +// */ +// public function getDatePreference() { +// // Important migration for old data rows +// if ( is_null( $this->mDatePreference ) ) { +// global $wgLang; +// $value = $this->getOption( 'date' ); +// $map = $wgLang->getDatePreferenceMigrationMap(); +// if ( isset( $map[$value] ) ) { +// $value = $map[$value]; +// } +// $this->mDatePreference = $value; +// } +// return $this->mDatePreference; +// } +// +// /** +// * Determine based on the wiki configuration and the user's options, +// * whether this user must be over HTTPS no matter what. +// * +// * @return boolean +// */ +// public function requiresHTTPS() { +// global $wgSecureLogin; +// if ( !$wgSecureLogin ) { +// return false; +// } +// +// $https = $this->getBoolOption( 'prefershttps' ); +// Hooks::run( 'UserRequiresHTTPS', [ $this, &$https ] ); +// if ( $https ) { +// $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() ); +// } +// +// return $https; +// } +// +// /** +// * Get the user preferred stub threshold +// * +// * @return int +// */ +// public function getStubThreshold() { +// global $wgMaxArticleSize; # Maximum article size, in Kb +// $threshold = $this->getIntOption( 'stubthreshold' ); +// if ( $threshold > $wgMaxArticleSize * 1024 ) { +// // If they have set an impossible value, disable the preference +// // so we can use the parser cache again. +// $threshold = 0; +// } +// return $threshold; +// } +// +// /** +// * Get the permissions this user has. +// * @return String[] permission names +// */ +// public function getRights() { +// if ( is_null( $this->mRights ) ) { +// $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() ); +// Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] ); +// +// // Deny any rights denied by the user's session, unless this +// // endpoint has no sessions. +// if ( !defined( 'MW_NO_SESSION' ) ) { +// $allowedRights = $this->getRequest()->getSession()->getAllowedUserRights(); +// if ( $allowedRights !== null ) { +// $this->mRights = array_intersect( $this->mRights, $allowedRights ); +// } +// } +// +// Hooks::run( 'UserGetRightsRemove', [ $this, &$this->mRights ] ); +// // Force reindexation of rights when a hook has unset one of them +// $this->mRights = array_values( array_unique( $this->mRights ) ); +// +// // If block disables login, we should also remove any +// // extra rights blocked users might have, in case the +// // blocked user has a pre-existing session (T129738). +// // This is checked here for cases where people only call +// // $user->isAllowed(). It is also checked in Title::checkUserBlock() +// // to give a better error message in the common case. +// $config = RequestContext::getMain()->getConfig(); +// if ( +// $this->isLoggedIn() && +// $config->get( 'BlockDisablesLogin' ) && +// $this->isBlocked() +// ) { +// $anon = new User; +// $this->mRights = array_intersect( $this->mRights, $anon->getRights() ); +// } +// } +// return $this->mRights; +// } +// +// /** +// * Get the list of explicit group memberships this user has. +// * The implicit * and user groups are not included. +// * +// * @return String[] Array of @gplx.Internal protected group names (sorted since 1.33) +// */ +// public function getGroups() { +// $this->load(); +// $this->loadGroups(); +// return array_keys( $this->mGroupMemberships ); +// } +// +// /** +// * Get the list of explicit group memberships this user has, stored as +// * UserGroupMembership objects. Implicit groups are not included. +// * +// * @return UserGroupMembership[] Associative array of (group name => UserGroupMembership Object) +// * @since 1.29 +// */ +// public function getGroupMemberships() { +// $this->load(); +// $this->loadGroups(); +// return $this->mGroupMemberships; +// } +// +// /** +// * Get the list of implicit group memberships this user has. +// * This includes all explicit groups, plus 'user' if logged in, +// * '*' for all accounts, and autopromoted groups +// * @param boolean $recache Whether to avoid the cache +// * @return array Array of String @gplx.Internal protected group names +// */ +// public function getEffectiveGroups( $recache = false ) { +// if ( $recache || is_null( $this->mEffectiveGroups ) ) { +// $this->mEffectiveGroups = array_unique( array_merge( +// $this->getGroups(), // explicit groups +// $this->getAutomaticGroups( $recache ) // implicit groups +// ) ); +// // Avoid PHP 7.1 warning of passing $this by reference +// $user = $this; +// // Hook for additional groups +// Hooks::run( 'UserEffectiveGroups', [ &$user, &$this->mEffectiveGroups ] ); +// // Force reindexation of groups when a hook has unset one of them +// $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) ); +// } +// return $this->mEffectiveGroups; +// } +// +// /** +// * Get the list of implicit group memberships this user has. +// * This includes 'user' if logged in, '*' for all accounts, +// * and autopromoted groups +// * @param boolean $recache Whether to avoid the cache +// * @return array Array of String @gplx.Internal protected group names +// */ +// public function getAutomaticGroups( $recache = false ) { +// if ( $recache || is_null( $this->mImplicitGroups ) ) { +// $this->mImplicitGroups = [ '*' ]; +// if ( $this->getId() ) { +// $this->mImplicitGroups[] = 'user'; +// +// $this->mImplicitGroups = array_unique( array_merge( +// $this->mImplicitGroups, +// Autopromote::getAutopromoteGroups( $this ) +// ) ); +// } +// if ( $recache ) { +// // Assure data consistency with rights/groups, +// // as getEffectiveGroups() depends on this function +// $this->mEffectiveGroups = null; +// } +// } +// return $this->mImplicitGroups; +// } +// +// /** +// * Returns the groups the user has belonged to. +// * +// * The user may still belong to the returned groups. Compare with getGroups(). +// * +// * The function will not return groups the user had belonged to before MW 1.17 +// * +// * @return array Names of the groups the user has belonged to. +// */ +// public function getFormerGroups() { +// $this->load(); +// +// if ( is_null( $this->mFormerGroups ) ) { +// $db = ( $this->queryFlagsUsed & self::READ_LATEST ) +// ? wfGetDB( DB_MASTER ) +// : wfGetDB( DB_REPLICA ); +// $res = $db->select( 'user_former_groups', +// [ 'ufg_group' ], +// [ 'ufg_user' => $this->mId ], +// __METHOD__ ); +// $this->mFormerGroups = []; +// foreach ( $res as $row ) { +// $this->mFormerGroups[] = $row->ufg_group; +// } +// } +// +// return $this->mFormerGroups; +// } +// +// /** +// * Get the user's edit count. +// * @return int|null Null for anonymous users +// */ +// public function getEditCount() { +// if ( !$this->getId() ) { +// return null; +// } +// +// if ( $this->mEditCount === null ) { +// /* Populate the count, if it has not been populated yet */ +// $dbr = wfGetDB( DB_REPLICA ); +// // check if the user_editcount field has been initialized +// $count = $dbr->selectField( +// 'user', 'user_editcount', +// [ 'user_id' => $this->mId ], +// __METHOD__ +// ); +// +// if ( $count === null ) { +// // it has not been initialized. do so. +// $count = $this->initEditCountInternal(); +// } +// $this->mEditCount = $count; +// } +// return (int)$this->mEditCount; +// } +// +// /** +// * Add the user to the given group. This takes immediate effect. +// * If the user is already in the group, the expiry time will be updated to the new +// * expiry time. (If $expiry is omitted or null, the membership will be altered to +// * never expire.) +// * +// * @param String $group Name of the group to add +// * @param String|null $expiry Optional expiry timestamp in any format acceptable to +// * wfTimestamp(), or null if the group assignment should not expire +// * @return boolean +// */ +// public function addGroup( $group, $expiry = null ) { +// $this->load(); +// $this->loadGroups(); +// +// if ( $expiry ) { +// $expiry = wfTimestamp( TS_MW, $expiry ); +// } +// +// if ( !Hooks::run( 'UserAddGroup', [ $this, &$group, &$expiry ] ) ) { +// return false; +// } +// +// // create the new UserGroupMembership and put it in the DB +// $ugm = new UserGroupMembership( $this->mId, $group, $expiry ); +// if ( !$ugm->insert( true ) ) { +// return false; +// } +// +// $this->mGroupMemberships[$group] = $ugm; +// +// // Refresh the groups caches, and clear the rights cache so it will be +// // refreshed on the next call to $this->getRights(). +// $this->getEffectiveGroups( true ); +// $this->mRights = null; +// +// $this->invalidateCache(); +// +// return true; +// } +// +// /** +// * Remove the user from the given group. +// * This takes immediate effect. +// * @param String $group Name of the group to remove +// * @return boolean +// */ +// public function removeGroup( $group ) { +// $this->load(); +// +// if ( !Hooks::run( 'UserRemoveGroup', [ $this, &$group ] ) ) { +// return false; +// } +// +// $ugm = UserGroupMembership::getMembership( $this->mId, $group ); +// // delete the membership entry +// if ( !$ugm || !$ugm->delete() ) { +// return false; +// } +// +// $this->loadGroups(); +// unset( $this->mGroupMemberships[$group] ); +// +// // Refresh the groups caches, and clear the rights cache so it will be +// // refreshed on the next call to $this->getRights(). +// $this->getEffectiveGroups( true ); +// $this->mRights = null; +// +// $this->invalidateCache(); +// +// return true; +// } +// +// /** +// * Get whether the user is logged in +// * @return boolean +// */ +// public function isLoggedIn() { +// return $this->getId() != 0; +// } +// +// /** +// * Get whether the user is anonymous +// * @return boolean +// */ +// public function isAnon() { +// return !$this->isLoggedIn(); +// } +// +// /** +// * @return boolean Whether this user is flagged as being a bot role account +// * @since 1.28 +// */ +// public function isBot() { +// if ( in_array( 'bot', $this->getGroups() ) && $this->isAllowed( 'bot' ) ) { +// return true; +// } +// +// $isBot = false; +// Hooks::run( "UserIsBot", [ $this, &$isBot ] ); +// +// return $isBot; +// } +// +// /** +// * Check if user is allowed to access a feature / make an action +// * +// * @param String $permissions,... Permissions to test +// * @return boolean True if user is allowed to perform *any* of the given actions +// */ +// public function isAllowedAny() { +// $permissions = func_get_args(); +// foreach ( $permissions as $permission ) { +// if ( $this->isAllowed( $permission ) ) { +// return true; +// } +// } +// return false; +// } +// +// /** +// * +// * @param String $permissions,... Permissions to test +// * @return boolean True if the user is allowed to perform *all* of the given actions +// */ +// public function isAllowedAll() { +// $permissions = func_get_args(); +// foreach ( $permissions as $permission ) { +// if ( !$this->isAllowed( $permission ) ) { +// return false; +// } +// } +// return true; +// } +// +// /** +// * Internal mechanics of testing a permission +// * @param String $action +// * @return boolean +// */ +// public function isAllowed( $action = '' ) { +// if ( $action === '' ) { +// return true; // In the spirit of DWIM +// } +// // Use strict parameter to avoid matching numeric 0 accidentally inserted +// // by misconfiguration: 0 == 'foo' +// return in_array( $action, $this->getRights(), true ); +// } +// +// /** +// * Check whether to enable recent changes patrol features for this user +// * @return boolean True or false +// */ +// public function useRCPatrol() { +// global $wgUseRCPatrol; +// return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' ); +// } +// +// /** +// * Check whether to enable new pages patrol features for this user +// * @return boolean True or false +// */ +// public function useNPPatrol() { +// global $wgUseRCPatrol, $wgUseNPPatrol; +// return ( +// ( $wgUseRCPatrol || $wgUseNPPatrol ) +// && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) ) +// ); +// } +// +// /** +// * Check whether to enable new files patrol features for this user +// * @return boolean True or false +// */ +// public function useFilePatrol() { +// global $wgUseRCPatrol, $wgUseFilePatrol; +// return ( +// ( $wgUseRCPatrol || $wgUseFilePatrol ) +// && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) ) +// ); +// } +// +// /** +// * Get the WebRequest Object to use with this Object +// * +// * @return WebRequest +// */ +// public function getRequest() { +// if ( $this->mRequest ) { +// return $this->mRequest; +// } +// +// global $wgRequest; +// return $wgRequest; +// } +// +// /** +// * Check the watched status of an article. +// * @since 1.22 $checkRights parameter added +// * @param Title $title Title of the article to look at +// * @param boolean $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights. +// * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS. +// * @return boolean +// */ +// public function isWatched( $title, $checkRights = self::CHECK_USER_RIGHTS ) { +// if ( $title->isWatchable() && ( !$checkRights || $this->isAllowed( 'viewmywatchlist' ) ) ) { +// return MediaWikiServices::getInstance()->getWatchedItemStore()->isWatched( $this, $title ); +// } +// return false; +// } +// +// /** +// * Watch an article. +// * @since 1.22 $checkRights parameter added +// * @param Title $title Title of the article to look at +// * @param boolean $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights. +// * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS. +// */ +// public function addWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) { +// if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) { +// MediaWikiServices::getInstance()->getWatchedItemStore()->addWatchBatchForUser( +// $this, +// [ $title->getSubjectPage(), $title->getTalkPage() ] +// ); +// } +// $this->invalidateCache(); +// } +// +// /** +// * Stop watching an article. +// * @since 1.22 $checkRights parameter added +// * @param Title $title Title of the article to look at +// * @param boolean $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights. +// * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS. +// */ +// public function removeWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) { +// if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) { +// $store = MediaWikiServices::getInstance()->getWatchedItemStore(); +// $store->removeWatch( $this, $title->getSubjectPage() ); +// $store->removeWatch( $this, $title->getTalkPage() ); +// } +// $this->invalidateCache(); +// } +// +// /** +// * Clear the user's notification timestamp for the given title. +// * If e-notif e-mails are on, they will receive notification mails on +// * the next change of the page if it's watched etc. +// * @note If the user doesn't have 'editmywatchlist', this will do nothing. +// * @param Title &$title Title of the article to look at +// * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed. +// */ +// public function clearNotification( &$title, $oldid = 0 ) { +// global $wgUseEnotif, $wgShowUpdatedMarker; +// +// // Do nothing if the database is locked to writes +// if ( wfReadOnly() ) { +// return; +// } +// +// // Do nothing if not allowed to edit the watchlist +// if ( !$this->isAllowed( 'editmywatchlist' ) ) { +// return; +// } +// +// // If we're working on user's talk page, we should update the talk page message indicator +// if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) { +// // Avoid PHP 7.1 warning of passing $this by reference +// $user = $this; +// if ( !Hooks::run( 'UserClearNewTalkNotification', [ &$user, $oldid ] ) ) { +// return; +// } +// +// // Try to update the DB post-send and only if needed... +// DeferredUpdates::addCallableUpdate( function () use ( $title, $oldid ) { +// if ( !$this->getNewtalk() ) { +// return; // no notifications to clear +// } +// +// // Delete the last notifications (they stack up) +// $this->setNewtalk( false ); +// +// // If there is a new, unseen, revision, use its timestamp +// $nextid = $oldid +// ? $title->getNextRevisionID( $oldid, Title::GAID_FOR_UPDATE ) +// : null; +// if ( $nextid ) { +// $this->setNewtalk( true, Revision::newFromId( $nextid ) ); +// } +// } ); +// } +// +// if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) { +// return; +// } +// +// if ( $this->isAnon() ) { +// // Nothing else to do... +// return; +// } +// +// // Only update the timestamp if the page is being watched. +// // The query to find out if it is watched is cached both in memcached and per-invocation, +// // and when it does have to be executed, it can be on a replica DB +// // If this is the user's newtalk page, we always update the timestamp +// $force = ''; +// if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) { +// $force = 'force'; +// } +// +// MediaWikiServices::getInstance()->getWatchedItemStore() +// ->resetNotificationTimestamp( $this, $title, $force, $oldid ); +// } +// +// /** +// * Resets all of the given user's page-change notification timestamps. +// * If e-notif e-mails are on, they will receive notification mails on +// * the next change of any watched page. +// * @note If the user doesn't have 'editmywatchlist', this will do nothing. +// */ +// public function clearAllNotifications() { +// global $wgUseEnotif, $wgShowUpdatedMarker; +// // Do nothing if not allowed to edit the watchlist +// if ( wfReadOnly() || !$this->isAllowed( 'editmywatchlist' ) ) { +// return; +// } +// +// if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) { +// $this->setNewtalk( false ); +// return; +// } +// +// $id = $this->getId(); +// if ( !$id ) { +// return; +// } +// +// $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore(); +// $watchedItemStore->resetAllNotificationTimestampsForUser( $this ); +// +// // We also need to clear here the "you have new message" notification for the own +// // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates(). +// } +// +// /** +// * Compute experienced level based on edit count and registration date. +// * +// * @return String 'newcomer', 'learner', or 'experienced' +// */ +// public function getExperienceLevel() { +// global $wgLearnerEdits, +// $wgExperiencedUserEdits, +// $wgLearnerMemberSince, +// $wgExperiencedUserMemberSince; +// +// if ( $this->isAnon() ) { +// return false; +// } +// +// $editCount = $this->getEditCount(); +// $registration = $this->getRegistration(); +// $now = time(); +// $learnerRegistration = wfTimestamp( TS_MW, $now - $wgLearnerMemberSince * 86400 ); +// $experiencedRegistration = wfTimestamp( TS_MW, $now - $wgExperiencedUserMemberSince * 86400 ); +// +// if ( $editCount < $wgLearnerEdits || +// $registration > $learnerRegistration ) { +// return 'newcomer'; +// } +// +// if ( $editCount > $wgExperiencedUserEdits && +// $registration <= $experiencedRegistration +// ) { +// return 'experienced'; +// } +// +// return 'learner'; +// } +// +// /** +// * Persist this user's session (e.g. set cookies) +// * +// * @param WebRequest|null $request WebRequest Object to use; $wgRequest will be used if null +// * is passed. +// * @param boolean|null $secure Whether to force secure/insecure cookies or use default +// * @param boolean $rememberMe Whether to add a Token cookie for elongated sessions +// */ +// public function setCookies( $request = null, $secure = null, $rememberMe = false ) { +// $this->load(); +// if ( $this->mId == 0 ) { +// return; +// } +// +// $session = $this->getRequest()->getSession(); +// if ( $request && $session->getRequest() !== $request ) { +// $session = $session->sessionWithRequest( $request ); +// } +// $delay = $session->delaySave(); +// +// if ( !$session->getUser()->equals( $this ) ) { +// if ( !$session->canSetUser() ) { +// \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) +// ->warning( __METHOD__ . +// ": Cannot save user \"$this\" to a user \"{$session->getUser()}\"'s immutable session" +// ); +// return; +// } +// $session->setUser( $this ); +// } +// +// $session->setRememberUser( $rememberMe ); +// if ( $secure !== null ) { +// $session->setForceHTTPS( $secure ); +// } +// +// $session->persist(); +// +// ScopedCallback::consume( $delay ); +// } +// +// /** +// * Log this user out. +// */ +// public function logout() { +// // Avoid PHP 7.1 warning of passing $this by reference +// $user = $this; +// if ( Hooks::run( 'UserLogout', [ &$user ] ) ) { +// $this->doLogout(); +// } +// } +// +// /** +// * Clear the user's session, and reset the instance cache. +// * @see logout() +// */ +// public function doLogout() { +// $session = $this->getRequest()->getSession(); +// if ( !$session->canSetUser() ) { +// \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) +// ->warning( __METHOD__ . ": Cannot log out of an immutable session" ); +// $error = 'immutable'; +// } elseif ( !$session->getUser()->equals( $this ) ) { +// \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) +// ->warning( __METHOD__ . +// ": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session" +// ); +// // But we still may as well make this user Object anon +// $this->clearInstanceCache( 'defaults' ); +// $error = 'wronguser'; +// } else { +// $this->clearInstanceCache( 'defaults' ); +// $delay = $session->delaySave(); +// $session->unpersist(); // Clear cookies (T127436) +// $session->setLoggedOutTimestamp( time() ); +// $session->setUser( new User ); +// $session->set( 'wsUserID', 0 ); // Other code expects this +// $session->resetAllTokens(); +// ScopedCallback::consume( $delay ); +// $error = false; +// } +// \MediaWiki\Logger\LoggerFactory::getInstance( 'authevents' )->info( 'Logout', [ +// 'event' => 'logout', +// 'successful' => $error === false, +// 'status' => $error ?: 'success', +// ] ); +// } +// +// /** +// * Save this user's settings into the database. +// * @todo Only rarely do all these fields need to be set! +// */ +// public function saveSettings() { +// if ( wfReadOnly() ) { +// // @TODO: caller should deal with this instead! +// // This should really just be an exception. +// MWExceptionHandler::logException( new DBExpectedError( +// null, +// "Could not update user with ID '{$this->mId}'; DB is read-only." +// ) ); +// return; +// } +// +// $this->load(); +// if ( $this->mId == 0 ) { +// return; // anon +// } +// +// // Get a new user_touched that is higher than the old one. +// // This will be used for a CAS check as a last-resort safety +// // check against race conditions and replica DB lag. +// $newTouched = $this->newTouchedTimestamp(); +// +// $dbw = wfGetDB( DB_MASTER ); +// $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $newTouched ) { +// global $wgActorTableSchemaMigrationStage; +// +// $dbw->update( 'user', +// [ /* SET */ +// 'user_name' => $this->mName, +// 'user_real_name' => $this->mRealName, +// 'user_email' => $this->mEmail, +// 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), +// 'user_touched' => $dbw->timestamp( $newTouched ), +// 'user_token' => strval( $this->mToken ), +// 'user_email_token' => $this->mEmailToken, +// 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ), +// ], $this->makeUpdateConditions( $dbw, [ /* WHERE */ +// 'user_id' => $this->mId, +// ] ), $fname +// ); +// +// if ( !$dbw->affectedRows() ) { +// // Maybe the problem was a missed cache update; clear it to be safe +// $this->clearSharedCache( 'refresh' ); +// // User was changed in the meantime or loaded with stale data +// $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'replica'; +// LoggerFactory::getInstance( 'preferences' )->warning( +// "CAS update failed on user_touched for user ID '{user_id}' ({db_flag} read)", +// [ 'user_id' => $this->mId, 'db_flag' => $from ] +// ); +// throw new MWException( "CAS update failed on user_touched. " . +// "The version of the user to be saved is older than the current version." +// ); +// } +// +// if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { +// $dbw->update( +// 'actor', +// [ 'actor_name' => $this->mName ], +// [ 'actor_user' => $this->mId ], +// $fname +// ); +// } +// } ); +// +// $this->mTouched = $newTouched; +// $this->saveOptions(); +// +// Hooks::run( 'UserSaveSettings', [ $this ] ); +// $this->clearSharedCache(); +// $this->getUserPage()->purgeSquid(); +// } +// +// /** +// * If only this user's username is known, and it exists, return the user ID. +// * +// * @param int $flags Bitfield of User:READ_* constants; useful for existence checks +// * @return int +// */ +// public function idForName( $flags = 0 ) { +// $s = trim( $this->getName() ); +// if ( $s === '' ) { +// return 0; +// } +// +// $db = ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) +// ? wfGetDB( DB_MASTER ) +// : wfGetDB( DB_REPLICA ); +// +// $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) +// ? [ 'LOCK IN SHARE MODE' ] +// : []; +// +// $id = $db->selectField( 'user', +// 'user_id', [ 'user_name' => $s ], __METHOD__, $options ); +// +// return (int)$id; +// } +// +// /** +// * Add a user to the database, return the user Object +// * +// * @param String $name Username to add +// * @param array $params Array of Strings Non-default parameters to save to +// * the database as user_* fields: +// * - email: The user's email address. +// * - email_authenticated: The email authentication timestamp. +// * - real_name: The user's real name. +// * - options: An associative array of non-default options. +// * - token: Random authentication token. Do not set. +// * - registration: Registration timestamp. Do not set. +// * +// * @return User|null User Object, or null if the username already exists. +// */ +// public static function createNew( $name, $params = [] ) { +// foreach ( [ 'password', 'newpassword', 'newpass_time', 'password_expires' ] as $field ) { +// if ( isset( $params[$field] ) ) { +// wfDeprecated( __METHOD__ . " with param '$field'", '1.27' ); +// unset( $params[$field] ); +// } +// } +// +// $user = new User; +// $user->load(); +// $user->setToken(); // init token +// if ( isset( $params['options'] ) ) { +// $user->mOptions = $params['options'] + (array)$user->mOptions; +// unset( $params['options'] ); +// } +// $dbw = wfGetDB( DB_MASTER ); +// +// $noPass = PasswordFactory::newInvalidPassword()->toString(); +// +// $fields = [ +// 'user_name' => $name, +// 'user_password' => $noPass, +// 'user_newpassword' => $noPass, +// 'user_email' => $user->mEmail, +// 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ), +// 'user_real_name' => $user->mRealName, +// 'user_token' => strval( $user->mToken ), +// 'user_registration' => $dbw->timestamp( $user->mRegistration ), +// 'user_editcount' => 0, +// 'user_touched' => $dbw->timestamp( $user->newTouchedTimestamp() ), +// ]; +// foreach ( $params as $name => $value ) { +// $fields["user_$name"] = $value; +// } +// +// return $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $fields ) { +// $dbw->insert( 'user', $fields, $fname, [ 'IGNORE' ] ); +// if ( $dbw->affectedRows() ) { +// $newUser = self::newFromId( $dbw->insertId() ); +// $newUser->mName = $fields['user_name']; +// $newUser->updateActorId( $dbw ); +// // Load the user from master to avoid replica lag +// $newUser->load( self::READ_LATEST ); +// } else { +// $newUser = null; +// } +// return $newUser; +// } ); +// } +// +// /** +// * Add this existing user Object to the database. If the user already +// * exists, a fatal status Object is returned, and the user Object is +// * initialised with the data from the database. +// * +// * Previously, this function generated a DB error due to a key conflict +// * if the user already existed. Many extension callers use this function +// * in code along the lines of: +// * +// * $user = User::newFromName( $name ); +// * if ( !$user->isLoggedIn() ) { +// * $user->addToDatabase(); +// * } +// * // do something with $user... +// * +// * However, this was vulnerable to a race condition (T18020). By +// * initialising the user Object if the user exists, we aim to support this +// * calling sequence as far as possible. +// * +// * Note that if the user exists, this function will acquire a write synchronized, +// * so it is still advisable to make the call conditional on isLoggedIn(), +// * and to commit the transaction after calling. +// * +// * @throws MWException +// * @return Status +// */ +// public function addToDatabase() { +// $this->load(); +// if ( !$this->mToken ) { +// $this->setToken(); // init token +// } +// +// if ( !is_string( $this->mName ) ) { +// throw new RuntimeException( "User name field is not set." ); +// } +// +// $this->mTouched = $this->newTouchedTimestamp(); +// +// $dbw = wfGetDB( DB_MASTER ); +// $status = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) { +// $noPass = PasswordFactory::newInvalidPassword()->toString(); +// $dbw->insert( 'user', +// [ +// 'user_name' => $this->mName, +// 'user_password' => $noPass, +// 'user_newpassword' => $noPass, +// 'user_email' => $this->mEmail, +// 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), +// 'user_real_name' => $this->mRealName, +// 'user_token' => strval( $this->mToken ), +// 'user_registration' => $dbw->timestamp( $this->mRegistration ), +// 'user_editcount' => 0, +// 'user_touched' => $dbw->timestamp( $this->mTouched ), +// ], $fname, +// [ 'IGNORE' ] +// ); +// if ( !$dbw->affectedRows() ) { +// // Use locking reads to bypass any REPEATABLE-READ snapshot. +// $this->mId = $dbw->selectField( +// 'user', +// 'user_id', +// [ 'user_name' => $this->mName ], +// $fname, +// [ 'LOCK IN SHARE MODE' ] +// ); +// $loaded = false; +// if ( $this->mId && $this->loadFromDatabase( self::READ_LOCKING ) ) { +// $loaded = true; +// } +// if ( !$loaded ) { +// throw new MWException( $fname . ": hit a key conflict attempting " . +// "to insert user '{$this->mName}' row, but it was not present in select!" ); +// } +// return Status::newFatal( 'userexists' ); +// } +// $this->mId = $dbw->insertId(); +// self::$idCacheByName[$this->mName] = $this->mId; +// $this->updateActorId( $dbw ); +// +// return Status::newGood(); +// } ); +// if ( !$status->isGood() ) { +// return $status; +// } +// +// // Clear instance cache other than user table data and actor, which is already accurate +// $this->clearInstanceCache(); +// +// $this->saveOptions(); +// return Status::newGood(); +// } +// +// /** +// * Update the actor ID after an insert +// * @param IDatabase $dbw Writable database handle +// */ +// private function updateActorId( IDatabase $dbw ) { +// global $wgActorTableSchemaMigrationStage; +// +// if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) { +// $dbw->insert( +// 'actor', +// [ 'actor_user' => $this->mId, 'actor_name' => $this->mName ], +// __METHOD__ +// ); +// $this->mActorId = (int)$dbw->insertId(); +// } +// } +// +// /** +// * If this user is logged-in and blocked, +// * block any IP address they've successfully logged in from. +// * @return boolean A block was spread +// */ +// public function spreadAnyEditBlock() { +// if ( $this->isLoggedIn() && $this->isBlocked() ) { +// return $this->spreadBlock(); +// } +// +// return false; +// } +// +// /** +// * If this (non-anonymous) user is blocked, +// * block the IP address they've successfully logged in from. +// * @return boolean A block was spread +// */ +// protected function spreadBlock() { +// wfDebug( __METHOD__ . "()\n" ); +// $this->load(); +// if ( $this->mId == 0 ) { +// return false; +// } +// +// $userblock = Block::newFromTarget( $this->getName() ); +// if ( !$userblock ) { +// return false; +// } +// +// return (boolean)$userblock->doAutoblock( $this->getRequest()->getIP() ); +// } +// +// /** +// * Get whether the user is explicitly blocked from account creation. +// * @return boolean|Block +// */ +// public function isBlockedFromCreateAccount() { +// $this->getBlockedStatus(); +// if ( $this->mBlock && $this->mBlock->appliesToRight( 'createaccount' ) ) { +// return $this->mBlock; +// } +// +// # T15611: if the IP address the user is trying to create an account from is +// # blocked with createaccount disabled, prevent new account creation there even +// # when the user is logged in +// if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) { +// $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() ); +// } +// return $this->mBlockedFromCreateAccount instanceof Block +// && $this->mBlockedFromCreateAccount->appliesToRight( 'createaccount' ) +// ? $this->mBlockedFromCreateAccount +// : false; +// } +// +// /** +// * Get whether the user is blocked from using Special:Emailuser. +// * @return boolean +// */ +// public function isBlockedFromEmailuser() { +// $this->getBlockedStatus(); +// return $this->mBlock && $this->mBlock->appliesToRight( 'sendemail' ); +// } +// +// /** +// * Get whether the user is blocked from using Special:Upload +// * +// * @since 1.33 +// * @return boolean +// */ +// public function isBlockedFromUpload() { +// $this->getBlockedStatus(); +// return $this->mBlock && $this->mBlock->appliesToRight( 'upload' ); +// } +// +// /** +// * Get whether the user is allowed to create an account. +// * @return boolean +// */ +// public function isAllowedToCreateAccount() { +// return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount(); +// } +// +// /** +// * Get this user's personal page title. +// * +// * @return Title User's personal page title +// */ +// public function getUserPage() { +// return Title::makeTitle( NS_USER, $this->getName() ); +// } +// +// /** +// * Get this user's talk page title. +// * +// * @return Title User's talk page title +// */ +// public function getTalkPage() { +// $title = $this->getUserPage(); +// return $title->getTalkPage(); +// } +// +// /** +// * Determine whether the user is a newbie. Newbies are either +// * anonymous IPs, or the most recently created accounts. +// * @return boolean +// */ +// public function isNewbie() { +// return !$this->isAllowed( 'autoconfirmed' ); +// } +// +// /** +// * Check to see if the given clear-text password is one of the accepted passwords +// * @deprecated since 1.27, use AuthManager instead +// * @param String $password User password +// * @return boolean True if the given password is correct, otherwise False +// */ +// public function checkPassword( $password ) { +// wfDeprecated( __METHOD__, '1.27' ); +// +// $manager = AuthManager::singleton(); +// $reqs = AuthenticationRequest::loadRequestsFromSubmission( +// $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN ), +// [ +// 'username' => $this->getName(), +// 'password' => $password, +// ] +// ); +// $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' ); +// switch ( $res->status ) { +// case AuthenticationResponse::PASS: +// return true; +// case AuthenticationResponse::FAIL: +// // Hope it's not a PreAuthenticationProvider that failed... +// \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) +// ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() ); +// return false; +// default: +// throw new BadMethodCallException( +// 'AuthManager returned a response unsupported by ' . __METHOD__ +// ); +// } +// } +// +// /** +// * Check if the given clear-text password matches the temporary password +// * sent by e-mail for password reset operations. +// * +// * @deprecated since 1.27, use AuthManager instead +// * @param String $plaintext +// * @return boolean True if matches, false otherwise +// */ +// public function checkTemporaryPassword( $plaintext ) { +// wfDeprecated( __METHOD__, '1.27' ); +// // Can't check the temporary password individually. +// return $this->checkPassword( $plaintext ); +// } +// +// /** +// * Initialize (if necessary) and return a session token value +// * which can be used in edit forms to show that the user's +// * login credentials aren't being hijacked with a foreign form +// * submission. +// * +// * @since 1.27 +// * @param String|array $salt Array of Strings Optional function-specific data for hashing +// * @param WebRequest|null $request WebRequest Object to use or null to use $wgRequest +// * @return MediaWiki\Session\Token The new edit token +// */ +// public function getEditTokenObject( $salt = '', $request = null ) { +// if ( $this->isAnon() ) { +// return new LoggedOutEditToken(); +// } +// +// if ( !$request ) { +// $request = $this->getRequest(); +// } +// return $request->getSession()->getToken( $salt ); +// } +// +// /** +// * Initialize (if necessary) and return a session token value +// * which can be used in edit forms to show that the user's +// * login credentials aren't being hijacked with a foreign form +// * submission. +// * +// * The $salt for 'edit' and 'csrf' tokens is the default (empty String). +// * +// * @since 1.19 +// * @param String|array $salt Array of Strings Optional function-specific data for hashing +// * @param WebRequest|null $request WebRequest Object to use or null to use $wgRequest +// * @return String The new edit token +// */ +// public function getEditToken( $salt = '', $request = null ) { +// return $this->getEditTokenObject( $salt, $request )->toString(); +// } +// +// /** +// * Check given value against the token value stored in the session. +// * A match should confirm that the form was submitted from the +// * user's own login session, not a form submission from a third-party +// * site. +// * +// * @param String $val Input value to compare +// * @param String|array $salt Optional function-specific data for hashing +// * @param WebRequest|null $request Object to use or null to use $wgRequest +// * @param int|null $maxage Fail tokens older than this, in seconds +// * @return boolean Whether the token matches +// */ +// public function matchEditToken( $val, $salt = '', $request = null, $maxage = null ) { +// return $this->getEditTokenObject( $salt, $request )->match( $val, $maxage ); +// } +// +// /** +// * Check given value against the token value stored in the session, +// * ignoring the suffix. +// * +// * @param String $val Input value to compare +// * @param String|array $salt Optional function-specific data for hashing +// * @param WebRequest|null $request Object to use or null to use $wgRequest +// * @param int|null $maxage Fail tokens older than this, in seconds +// * @return boolean Whether the token matches +// */ +// public function matchEditTokenNoSuffix( $val, $salt = '', $request = null, $maxage = null ) { +// $val = substr( $val, 0, strspn( $val, '0123456789abcdef' ) ) . Token::SUFFIX; +// return $this->matchEditToken( $val, $salt, $request, $maxage ); +// } +// +// /** +// * Generate a new e-mail confirmation token and send a confirmation/invalidation +// * mail to the user's given address. +// * +// * @param String $type Message to send, either "created", "changed" or "set" +// * @return Status +// */ +// public function sendConfirmationMail( $type = 'created' ) { +// global $wgLang; +// $expiration = null; // gets passed-by-ref and defined in next line. +// $token = $this->confirmationToken( $expiration ); +// $url = $this->confirmationTokenUrl( $token ); +// $invalidateURL = $this->invalidationTokenUrl( $token ); +// $this->saveSettings(); +// +// if ( $type == 'created' || $type === false ) { +// $message = 'confirmemail_body'; +// $type = 'created'; +// } elseif ( $type === true ) { +// $message = 'confirmemail_body_changed'; +// $type = 'changed'; +// } else { +// // Messages: confirmemail_body_changed, confirmemail_body_set +// $message = 'confirmemail_body_' . $type; +// } +// +// $mail = [ +// 'subject' => wfMessage( 'confirmemail_subject' )->text(), +// 'body' => wfMessage( $message, +// $this->getRequest()->getIP(), +// $this->getName(), +// $url, +// $wgLang->userTimeAndDate( $expiration, $this ), +// $invalidateURL, +// $wgLang->userDate( $expiration, $this ), +// $wgLang->userTime( $expiration, $this ) )->text(), +// 'from' => null, +// 'replyTo' => null, +// ]; +// $info = [ +// 'type' => $type, +// 'ip' => $this->getRequest()->getIP(), +// 'confirmURL' => $url, +// 'invalidateURL' => $invalidateURL, +// 'expiration' => $expiration +// ]; +// +// Hooks::run( 'UserSendConfirmationMail', [ $this, &$mail, $info ] ); +// return $this->sendMail( $mail['subject'], $mail['body'], $mail['from'], $mail['replyTo'] ); +// } +// +// /** +// * Send an e-mail to this user's account. Does not check for +// * confirmed status or validity. +// * +// * @param String $subject Message subject +// * @param String $body Message body +// * @param User|null $from Optional sending user; if unspecified, default +// * $wgPasswordSender will be used. +// * @param MailAddress|null $replyto Reply-To address +// * @return Status +// */ +// public function sendMail( $subject, $body, $from = null, $replyto = null ) { +// global $wgPasswordSender; +// +// if ( $from instanceof User ) { +// $sender = MailAddress::newFromUser( $from ); +// } else { +// $sender = new MailAddress( $wgPasswordSender, +// wfMessage( 'emailsender' )->inContentLanguage()->text() ); +// } +// $to = MailAddress::newFromUser( $this ); +// +// return UserMailer::send( $to, $sender, $subject, $body, [ +// 'replyTo' => $replyto, +// ] ); +// } +// +// /** +// * Generate, store, and return a new e-mail confirmation code. +// * A hash (unsalted, since it's used as a key) is stored. +// * +// * @note Call saveSettings() after calling this function to commit +// * this change to the database. +// * +// * @param String &$expiration Accepts the expiration time +// * @return String New token +// */ +// protected function confirmationToken( &$expiration ) { +// global $wgUserEmailConfirmationTokenExpiry; +// $now = time(); +// $expires = $now + $wgUserEmailConfirmationTokenExpiry; +// $expiration = wfTimestamp( TS_MW, $expires ); +// $this->load(); +// $token = MWCryptRand::generateHex( 32 ); +// $hash = md5( $token ); +// $this->mEmailToken = $hash; +// $this->mEmailTokenExpires = $expiration; +// return $token; +// } +// +// /** +// * Return a URL the user can use to confirm their email address. +// * @param String $token Accepts the email confirmation token +// * @return String New token URL +// */ +// protected function confirmationTokenUrl( $token ) { +// return $this->getTokenUrl( 'ConfirmEmail', $token ); +// } +// +// /** +// * Return a URL the user can use to invalidate their email address. +// * @param String $token Accepts the email confirmation token +// * @return String New token URL +// */ +// protected function invalidationTokenUrl( $token ) { +// return $this->getTokenUrl( 'InvalidateEmail', $token ); +// } +// +// /** +// * Internal function to format the e-mail validation/invalidation URLs. +// * This uses a quickie hack to use the +// * hardcoded English names of the Special: pages, for ASCII safety. +// * +// * @note Since these URLs get dropped directly into emails, using the +// * short English names avoids insanely long URL-encoded links, which +// * also sometimes can get corrupted in some browsers/mailers +// * (T8957 with Gmail and Internet Explorer). +// * +// * @param String $page Special page +// * @param String $token +// * @return String Formatted URL +// */ +// protected function getTokenUrl( $page, $token ) { +// // Hack to bypass localization of 'Special:' +// $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" ); +// return $title->getCanonicalURL(); +// } +// +// /** +// * Mark the e-mail address confirmed. +// * +// * @note Call saveSettings() after calling this function to commit the change. +// * +// * @return boolean +// */ +// public function confirmEmail() { +// // Check if it's already confirmed, so we don't touch the database +// // and fire the ConfirmEmailComplete hook on redundant confirmations. +// if ( !$this->isEmailConfirmed() ) { +// $this->setEmailAuthenticationTimestamp( wfTimestampNow() ); +// Hooks::run( 'ConfirmEmailComplete', [ $this ] ); +// } +// return true; +// } +// +// /** +// * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail +// * address if it was already confirmed. +// * +// * @note Call saveSettings() after calling this function to commit the change. +// * @return boolean Returns true +// */ +// public function invalidateEmail() { +// $this->load(); +// $this->mEmailToken = null; +// $this->mEmailTokenExpires = null; +// $this->setEmailAuthenticationTimestamp( null ); +// $this->mEmail = ''; +// Hooks::run( 'InvalidateEmailComplete', [ $this ] ); +// return true; +// } +// +// /** +// * Set the e-mail authentication timestamp. +// * @param String $timestamp TS_MW timestamp +// */ +// public function setEmailAuthenticationTimestamp( $timestamp ) { +// $this->load(); +// $this->mEmailAuthenticated = $timestamp; +// Hooks::run( 'UserSetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] ); +// } +// +// /** +// * Is this user allowed to send e-mails within limits of current +// * site configuration? +// * @return boolean +// */ +// public function canSendEmail() { +// global $wgEnableEmail, $wgEnableUserEmail; +// if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) { +// return false; +// } +// $canSend = $this->isEmailConfirmed(); +// // Avoid PHP 7.1 warning of passing $this by reference +// $user = $this; +// Hooks::run( 'UserCanSendEmail', [ &$user, &$canSend ] ); +// return $canSend; +// } +// +// /** +// * Is this user allowed to receive e-mails within limits of current +// * site configuration? +// * @return boolean +// */ +// public function canReceiveEmail() { +// return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' ); +// } +// +// /** +// * Is this user's e-mail address valid-looking and confirmed within +// * limits of the current site configuration? +// * +// * @note If $wgEmailAuthentication is on, this may require the user to have +// * confirmed their address by returning a code or using a password +// * sent to the address from the wiki. +// * +// * @return boolean +// */ +// public function isEmailConfirmed() { +// global $wgEmailAuthentication; +// $this->load(); +// // Avoid PHP 7.1 warning of passing $this by reference +// $user = $this; +// $confirmed = true; +// if ( Hooks::run( 'EmailConfirmed', [ &$user, &$confirmed ] ) ) { +// if ( $this->isAnon() ) { +// return false; +// } +// if ( !Sanitizer::validateEmail( $this->mEmail ) ) { +// return false; +// } +// if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) { +// return false; +// } +// return true; +// } +// +// return $confirmed; +// } +// +// /** +// * Check whether there is an outstanding request for e-mail confirmation. +// * @return boolean +// */ +// public function isEmailConfirmationPending() { +// global $wgEmailAuthentication; +// return $wgEmailAuthentication && +// !$this->isEmailConfirmed() && +// $this->mEmailToken && +// $this->mEmailTokenExpires > wfTimestamp(); +// } +// +// /** +// * Get the timestamp of account creation. +// * +// * @return String|boolean|null Timestamp of account creation, false for +// * non-existent/anonymous user accounts, or null if existing account +// * but information is not in database. +// */ +// public function getRegistration() { +// if ( $this->isAnon() ) { +// return false; +// } +// $this->load(); +// return $this->mRegistration; +// } +// +// /** +// * Get the timestamp of the first edit +// * +// * @return String|boolean Timestamp of first edit, or false for +// * non-existent/anonymous user accounts. +// */ +// public function getFirstEditTimestamp() { +// return $this->getEditTimestamp( true ); +// } +// +// /** +// * Get the timestamp of the latest edit +// * +// * @since 1.33 +// * @return String|boolean Timestamp of first edit, or false for +// * non-existent/anonymous user accounts. +// */ +// public function getLatestEditTimestamp() { +// return $this->getEditTimestamp( false ); +// } +// +// /** +// * Get the timestamp of the first or latest edit +// * +// * @param boolean $first True for the first edit, false for the latest one +// * @return String|boolean Timestamp of first or latest edit, or false for +// * non-existent/anonymous user accounts. +// */ +// private function getEditTimestamp( $first ) { +// if ( $this->getId() == 0 ) { +// return false; // anons +// } +// $dbr = wfGetDB( DB_REPLICA ); +// $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this ); +// $tsField = isset( $actorWhere['tables']['temp_rev_user'] ) +// ? 'revactor_timestamp' : 'rev_timestamp'; +// $sortOrder = $first ? 'ASC' : 'DESC'; +// $time = $dbr->selectField( +// [ 'revision' ] + $actorWhere['tables'], +// $tsField, +// [ $actorWhere['conds'] ], +// __METHOD__, +// [ 'ORDER BY' => "$tsField $sortOrder" ], +// $actorWhere['joins'] +// ); +// if ( !$time ) { +// return false; // no edits +// } +// return wfTimestamp( TS_MW, $time ); +// } +// +// /** +// * Get the permissions associated with a given list of groups +// * +// * @param array $groups Array of Strings List of @gplx.Internal protected group names +// * @return array Array of Strings List of permission key names for given groups combined +// */ +// public static function getGroupPermissions( $groups ) { +// global $wgGroupPermissions, $wgRevokePermissions; +// $rights = []; +// // grant every granted permission first +// foreach ( $groups as $group ) { +// if ( isset( $wgGroupPermissions[$group] ) ) { +// $rights = array_merge( $rights, +// // array_filter removes empty items +// array_keys( array_filter( $wgGroupPermissions[$group] ) ) ); +// } +// } +// // now revoke the revoked permissions +// foreach ( $groups as $group ) { +// if ( isset( $wgRevokePermissions[$group] ) ) { +// $rights = array_diff( $rights, +// array_keys( array_filter( $wgRevokePermissions[$group] ) ) ); +// } +// } +// return array_unique( $rights ); +// } +// +// /** +// * Get all the groups who have a given permission +// * +// * @param String $role Role to check +// * @return array Array of Strings List of @gplx.Internal protected group names with the given permission +// */ +// public static function getGroupsWithPermission( $role ) { +// global $wgGroupPermissions; +// $allowedGroups = []; +// foreach ( array_keys( $wgGroupPermissions ) as $group ) { +// if ( self::groupHasPermission( $group, $role ) ) { +// $allowedGroups[] = $group; +// } +// } +// return $allowedGroups; +// } +// +// /** +// * Check, if the given group has the given permission +// * +// * If you're wanting to check whether all users have a permission, use +// * User::isEveryoneAllowed() instead. That properly checks if it's revoked +// * from anyone. +// * +// * @since 1.21 +// * @param String $group Group to check +// * @param String $role Role to check +// * @return boolean +// */ +// public static function groupHasPermission( $group, $role ) { +// global $wgGroupPermissions, $wgRevokePermissions; +// return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role] +// && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] ); +// } +// +// /** +// * Check if all users may be assumed to have the given permission +// * +// * We generally assume so if the right is granted to '*' and isn't revoked +// * on any group. It doesn't attempt to take grants or other extension +// * limitations on rights into account in the general case, though, as that +// * would require it to always return false and defeat the purpose. +// * Specifically, session-based rights restrictions (such as OAuth or bot +// * passwords) are applied based on the current session. +// * +// * @since 1.22 +// * @param String $right Right to check +// * @return boolean +// */ +// public static function isEveryoneAllowed( $right ) { +// global $wgGroupPermissions, $wgRevokePermissions; +// static $cache = []; +// +// // Use the cached results, except in unit tests which rely on +// // being able change the permission mid-request +// if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) { +// return $cache[$right]; +// } +// +// if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) { +// $cache[$right] = false; +// return false; +// } +// +// // If it's revoked anywhere, then everyone doesn't have it +// foreach ( $wgRevokePermissions as $rights ) { +// if ( isset( $rights[$right] ) && $rights[$right] ) { +// $cache[$right] = false; +// return false; +// } +// } +// +// // Remove any rights that aren't allowed to the global-session user, +// // unless there are no sessions for this endpoint. +// if ( !defined( 'MW_NO_SESSION' ) ) { +// $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights(); +// if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) { +// $cache[$right] = false; +// return false; +// } +// } +// +// // Allow extensions to say false +// if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) { +// $cache[$right] = false; +// return false; +// } +// +// $cache[$right] = true; +// return true; +// } +// +// /** +// * Return the set of defined explicit groups. +// * The implicit groups (by default *, 'user' and 'autoconfirmed') +// * are not included, as they are defined automatically, not in the database. +// * @return array Array of @gplx.Internal protected group names +// */ +// public static function getAllGroups() { +// global $wgGroupPermissions, $wgRevokePermissions; +// return array_values( array_diff( +// array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ), +// self::getImplicitGroups() +// ) ); +// } +// +// /** +// * Get a list of all available permissions. +// * @return String[] Array of permission names +// */ +// public static function getAllRights() { +// if ( self::$mAllRights === false ) { +// global $wgAvailableRights; +// if ( count( $wgAvailableRights ) ) { +// self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) ); +// } else { +// self::$mAllRights = self::$mCoreRights; +// } +// Hooks::run( 'UserGetAllRights', [ &self::$mAllRights ] ); +// } +// return self::$mAllRights; +// } +// +// /** +// * Get a list of implicit groups +// * TODO: Should we deprecate this? It's trivial, but we don't want to encourage use of globals. +// * +// * @return array Array of Strings Array of @gplx.Internal protected group names +// */ +// public static function getImplicitGroups() { +// global $wgImplicitGroups; +// return $wgImplicitGroups; +// } +// +// /** +// * Get the title of a page describing a particular group +// * @deprecated since 1.29 Use UserGroupMembership::getGroupPage instead +// * +// * @param String $group Internal group name +// * @return Title|boolean Title of the page if it exists, false otherwise +// */ +// public static function getGroupPage( $group ) { +// wfDeprecated( __METHOD__, '1.29' ); +// return UserGroupMembership::getGroupPage( $group ); +// } +// +// /** +// * Create a link to the group in HTML, if available; +// * else return the group name. +// * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or +// * make the link yourself if you need custom text +// * +// * @param String $group Internal name of the group +// * @param String $text The text of the link +// * @return String HTML link to the group +// */ +// public static function makeGroupLinkHTML( $group, $text = '' ) { +// wfDeprecated( __METHOD__, '1.29' ); +// +// if ( $text == '' ) { +// $text = UserGroupMembership::getGroupName( $group ); +// } +// $title = UserGroupMembership::getGroupPage( $group ); +// if ( $title ) { +// return MediaWikiServices::getInstance() +// ->getLinkRenderer()->makeLink( $title, $text ); +// } +// +// return htmlspecialchars( $text ); +// } +// +// /** +// * Create a link to the group in Wikitext, if available; +// * else return the group name. +// * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or +// * make the link yourself if you need custom text +// * +// * @param String $group Internal name of the group +// * @param String $text The text of the link +// * @return String Wikilink to the group +// */ +// public static function makeGroupLinkWiki( $group, $text = '' ) { +// wfDeprecated( __METHOD__, '1.29' ); +// +// if ( $text == '' ) { +// $text = UserGroupMembership::getGroupName( $group ); +// } +// $title = UserGroupMembership::getGroupPage( $group ); +// if ( $title ) { +// $page = $title->getFullText(); +// return "[[$page|$text]]"; +// } +// +// return $text; +// } +// +// /** +// * Returns an array of the groups that a particular group can add/remove. +// * +// * @param String $group The group to check for whether it can add/remove +// * @return array Array( 'add' => array( addablegroups ), +// * 'remove' => array( removablegroups ), +// * 'add-self' => array( addablegroups to self), +// * 'remove-self' => array( removable groups from self) ) +// */ +// public static function changeableByGroup( $group ) { +// global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; +// +// $groups = [ +// 'add' => [], +// 'remove' => [], +// 'add-self' => [], +// 'remove-self' => [] +// ]; +// +// if ( empty( $wgAddGroups[$group] ) ) { +// // Don't add anything to $groups +// } elseif ( $wgAddGroups[$group] === true ) { +// // You get everything +// $groups['add'] = self::getAllGroups(); +// } elseif ( is_array( $wgAddGroups[$group] ) ) { +// $groups['add'] = $wgAddGroups[$group]; +// } +// +// // Same thing for remove +// if ( empty( $wgRemoveGroups[$group] ) ) { +// // Do nothing +// } elseif ( $wgRemoveGroups[$group] === true ) { +// $groups['remove'] = self::getAllGroups(); +// } elseif ( is_array( $wgRemoveGroups[$group] ) ) { +// $groups['remove'] = $wgRemoveGroups[$group]; +// } +// +// // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility +// if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) { +// foreach ( $wgGroupsAddToSelf as $key => $value ) { +// if ( is_int( $key ) ) { +// $wgGroupsAddToSelf['user'][] = $value; +// } +// } +// } +// +// if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) { +// foreach ( $wgGroupsRemoveFromSelf as $key => $value ) { +// if ( is_int( $key ) ) { +// $wgGroupsRemoveFromSelf['user'][] = $value; +// } +// } +// } +// +// // Now figure out what groups the user can add to him/herself +// if ( empty( $wgGroupsAddToSelf[$group] ) ) { +// // Do nothing +// } elseif ( $wgGroupsAddToSelf[$group] === true ) { +// // No idea WHY this would be used, but it's there +// $groups['add-self'] = self::getAllGroups(); +// } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) { +// $groups['add-self'] = $wgGroupsAddToSelf[$group]; +// } +// +// if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) { +// // Do nothing +// } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) { +// $groups['remove-self'] = self::getAllGroups(); +// } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) { +// $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group]; +// } +// +// return $groups; +// } +// +// /** +// * Returns an array of groups that this user can add and remove +// * @return array Array( 'add' => array( addablegroups ), +// * 'remove' => array( removablegroups ), +// * 'add-self' => array( addablegroups to self), +// * 'remove-self' => array( removable groups from self) ) +// */ +// public function changeableGroups() { +// if ( $this->isAllowed( 'userrights' ) ) { +// // This group gives the right to modify everything (reverse- +// // compatibility with old "userrights lets you change +// // everything") +// // Using array_merge to make the groups reindexed +// $all = array_merge( self::getAllGroups() ); +// return [ +// 'add' => $all, +// 'remove' => $all, +// 'add-self' => [], +// 'remove-self' => [] +// ]; +// } +// +// // Okay, it's not so simple, we will have to go through the arrays +// $groups = [ +// 'add' => [], +// 'remove' => [], +// 'add-self' => [], +// 'remove-self' => [] +// ]; +// $addergroups = $this->getEffectiveGroups(); +// +// foreach ( $addergroups as $addergroup ) { +// $groups = array_merge_recursive( +// $groups, $this->changeableByGroup( $addergroup ) +// ); +// $groups['add'] = array_unique( $groups['add'] ); +// $groups['remove'] = array_unique( $groups['remove'] ); +// $groups['add-self'] = array_unique( $groups['add-self'] ); +// $groups['remove-self'] = array_unique( $groups['remove-self'] ); +// } +// return $groups; +// } +// +// /** +// * Schedule a deferred update to update the user's edit count +// */ +// public function incEditCount() { +// if ( $this->isAnon() ) { +// return; // sanity +// } +// +// DeferredUpdates::addUpdate( +// new UserEditCountUpdate( $this, 1 ), +// DeferredUpdates::POSTSEND +// ); +// } +// +// /** +// * This method should not be called outside User/UserEditCountUpdate +// * +// * @param int $count +// */ +// public function setEditCountInternal( $count ) { +// $this->mEditCount = $count; +// } +// +// /** +// * Initialize user_editcount from data out of the revision table +// * +// * This method should not be called outside User/UserEditCountUpdate +// * +// * @return int Number of edits +// */ +// public function initEditCountInternal() { +// // Pull from a replica DB to be less cruel to servers +// // Accuracy isn't the point anyway here +// $dbr = wfGetDB( DB_REPLICA ); +// $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this ); +// $count = (int)$dbr->selectField( +// [ 'revision' ] + $actorWhere['tables'], +// 'COUNT(*)', +// [ $actorWhere['conds'] ], +// __METHOD__, +// [], +// $actorWhere['joins'] +// ); +// +// $dbw = wfGetDB( DB_MASTER ); +// $dbw->update( +// 'user', +// [ 'user_editcount' => $count ], +// [ +// 'user_id' => $this->getId(), +// 'user_editcount IS NULL OR user_editcount < ' . (int)$count +// ], +// __METHOD__ +// ); +// +// return $count; +// } +// +// /** +// * Get the description of a given right +// * +// * @since 1.29 +// * @param String $right Right to query +// * @return String Localized description of the right +// */ +// public static function getRightDescription( $right ) { +// $key = "right-$right"; +// $msg = wfMessage( $key ); +// return $msg->isDisabled() ? $right : $msg->text(); +// } +// +// /** +// * Get the name of a given grant +// * +// * @since 1.29 +// * @param String $grant Grant to query +// * @return String Localized name of the grant +// */ +// public static function getGrantName( $grant ) { +// $key = "grant-$grant"; +// $msg = wfMessage( $key ); +// return $msg->isDisabled() ? $grant : $msg->text(); +// } +// +// /** +// * Add a newuser log entry for this user. +// * Before 1.19 the return value was always true. +// * +// * @deprecated since 1.27, AuthManager handles logging +// * @param String|boolean $action Account creation type. +// * - String, one of the following values: +// * - 'create' for an anonymous user creating an account for himself. +// * This will force the action's performer to be the created user itself, +// * no matter the value of $wgUser +// * - 'create2' for a logged in user creating an account for someone else +// * - 'byemail' when the created user will receive its password by e-mail +// * - 'autocreate' when the user is automatically created (such as by CentralAuth). +// * - Boolean means whether the account was created by e-mail (deprecated): +// * - true will be converted to 'byemail' +// * - false will be converted to 'create' if this Object is the same as +// * $wgUser and to 'create2' otherwise +// * @param String $reason User supplied reason +// * @return boolean true +// */ +// public function addNewUserLogEntry( $action = false, $reason = '' ) { +// return true; // disabled +// } +// +// /** +// * Add an autocreate newuser log entry for this user +// * Used by things like CentralAuth and perhaps other authplugins. +// * Consider calling addNewUserLogEntry() directly instead. +// * +// * @deprecated since 1.27, AuthManager handles logging +// * @return boolean +// */ +// public function addNewUserLogEntryAutoCreate() { +// $this->addNewUserLogEntry( 'autocreate' ); +// +// return true; +// } +// +// /** +// * Load the user options either from cache, the database or an array +// * +// * @param array|null $data Rows for the current user out of the user_properties table +// */ +// protected function loadOptions( $data = null ) { +// $this->load(); +// +// if ( $this->mOptionsLoaded ) { +// return; +// } +// +// $this->mOptions = self::getDefaultOptions(); +// +// if ( !$this->getId() ) { +// // For unlogged-in users, load language/variant options from request. +// // There's no need to do it for logged-in users: they can set preferences, +// // and handling of page content is done by $pageLang->getPreferredVariant() and such, +// // so don't override user's choice (especially when the user chooses site default). +// $variant = MediaWikiServices::getInstance()->getContentLanguage()->getDefaultVariant(); +// $this->mOptions['variant'] = $variant; +// $this->mOptions['language'] = $variant; +// $this->mOptionsLoaded = true; +// return; +// } +// +// // Maybe load from the Object +// if ( !is_null( $this->mOptionOverrides ) ) { +// wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" ); +// foreach ( $this->mOptionOverrides as $key => $value ) { +// $this->mOptions[$key] = $value; +// } +// } else { +// if ( !is_array( $data ) ) { +// wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" ); +// // Load from database +// $dbr = ( $this->queryFlagsUsed & self::READ_LATEST ) +// ? wfGetDB( DB_MASTER ) +// : wfGetDB( DB_REPLICA ); +// +// $res = $dbr->select( +// 'user_properties', +// [ 'up_property', 'up_value' ], +// [ 'up_user' => $this->getId() ], +// __METHOD__ +// ); +// +// $this->mOptionOverrides = []; +// $data = []; +// foreach ( $res as $row ) { +// // Convert '0' to 0. PHP's boolean conversion considers them both +// // false, but e.g. JavaScript considers the former as true. +// // @todo: T54542 Somehow determine the desired type (String/int/boolean) +// // and convert all values here. +// if ( $row->up_value === '0' ) { +// $row->up_value = 0; +// } +// $data[$row->up_property] = $row->up_value; +// } +// } +// +// foreach ( $data as $property => $value ) { +// $this->mOptionOverrides[$property] = $value; +// $this->mOptions[$property] = $value; +// } +// } +// +// // Replace deprecated language codes +// $this->mOptions['language'] = LanguageCode::replaceDeprecatedCodes( +// $this->mOptions['language'] +// ); +// +// $this->mOptionsLoaded = true; +// +// Hooks::run( 'UserLoadOptions', [ $this, &$this->mOptions ] ); +// } +// +// /** +// * Saves the non-default options for this user, as previously set e.g. via +// * setOption(), in the database's "user_properties" (preferences) table. +// * Usually used via saveSettings(). +// */ +// protected function saveOptions() { +// $this->loadOptions(); +// +// // Not using getOptions(), to keep hidden preferences in database +// $saveOptions = $this->mOptions; +// +// // Allow hooks to abort, for instance to save to a global profile. +// // Reset options to default state before saving. +// if ( !Hooks::run( 'UserSaveOptions', [ $this, &$saveOptions ] ) ) { +// return; +// } +// +// $userId = $this->getId(); +// +// $insert_rows = []; // all the new preference rows +// foreach ( $saveOptions as $key => $value ) { +// // Don't bother storing default values +// $defaultOption = self::getDefaultOption( $key ); +// if ( ( $defaultOption === null && $value !== false && $value !== null ) +// || $value != $defaultOption +// ) { +// $insert_rows[] = [ +// 'up_user' => $userId, +// 'up_property' => $key, +// 'up_value' => $value, +// ]; +// } +// } +// +// $dbw = wfGetDB( DB_MASTER ); +// +// $res = $dbw->select( 'user_properties', +// [ 'up_property', 'up_value' ], [ 'up_user' => $userId ], __METHOD__ ); +// +// // Find prior rows that need to be removed or updated. These rows will +// // all be deleted (the latter so that INSERT IGNORE applies the new values). +// $keysDelete = []; +// foreach ( $res as $row ) { +// if ( !isset( $saveOptions[$row->up_property] ) +// || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0 +// ) { +// $keysDelete[] = $row->up_property; +// } +// } +// +// if ( count( $keysDelete ) ) { +// // Do the DELETE by PRIMARY KEY for prior rows. +// // In the past a very large portion of calls to this function are for setting +// // 'rememberpassword' for new accounts (a preference that has since been removed). +// // Doing a blanket per-user DELETE for new accounts with no rows in the table +// // caused gap locks on [max user ID,+infinity) which caused high contention since +// // updates would pile up on each other as they are for higher (newer) user IDs. +// // It might not be necessary these days, but it shouldn't hurt either. +// $dbw->delete( 'user_properties', +// [ 'up_user' => $userId, 'up_property' => $keysDelete ], __METHOD__ ); +// } +// // Insert the new preference rows +// $dbw->insert( 'user_properties', $insert_rows, __METHOD__, [ 'IGNORE' ] ); +// } +// +// /** +// * Return the list of user fields that should be selected to create +// * a new user Object. +// * @deprecated since 1.31, use self::getQueryInfo() instead. +// * @return array +// */ +// public static function selectFields() { +// wfDeprecated( __METHOD__, '1.31' ); +// return [ +// 'user_id', +// 'user_name', +// 'user_real_name', +// 'user_email', +// 'user_touched', +// 'user_token', +// 'user_email_authenticated', +// 'user_email_token', +// 'user_email_token_expires', +// 'user_registration', +// 'user_editcount', +// ]; +// } +// +// /** +// * Return the tables, fields, and join conditions to be selected to create +// * a new user Object. +// * @since 1.31 +// * @return array With three keys: +// * - tables: (String[]) to include in the `$table` to `IDatabase->select()` +// * - fields: (String[]) to include in the `$vars` to `IDatabase->select()` +// * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` +// */ +// public static function getQueryInfo() { +// global $wgActorTableSchemaMigrationStage; +// +// $ret = [ +// 'tables' => [ 'user' ], +// 'fields' => [ +// 'user_id', +// 'user_name', +// 'user_real_name', +// 'user_email', +// 'user_touched', +// 'user_token', +// 'user_email_authenticated', +// 'user_email_token', +// 'user_email_token_expires', +// 'user_registration', +// 'user_editcount', +// ], +// 'joins' => [], +// ]; +// +// // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW, +// // but it does little harm and might be needed for write callers loading a User. +// if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) { +// $ret['tables']['user_actor'] = 'actor'; +// $ret['fields'][] = 'user_actor.actor_id'; +// $ret['joins']['user_actor'] = [ +// ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) ? 'JOIN' : 'LEFT JOIN', +// [ 'user_actor.actor_user = user_id' ] +// ]; +// } +// +// return $ret; +// } +// +// /** +// * Factory function for fatal permission-denied errors +// * +// * @since 1.22 +// * @param String $permission User right required +// * @return Status +// */ +// static function newFatalPermissionDeniedStatus( $permission ) { +// global $wgLang; +// +// $groups = []; +// foreach ( self::getGroupsWithPermission( $permission ) as $group ) { +// $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' ); +// } +// +// if ( $groups ) { +// return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) ); +// } +// +// return Status::newFatal( 'badaccess-group0' ); +// } +// +// /** +// * Get a new instance of this user that was loaded from the master via a locking read +// * +// * Use this instead of the main context User when updating that user. This avoids races +// * where that user was loaded from a replica DB or even the master but without proper locks. +// * +// * @return User|null Returns null if the user was not found in the DB +// * @since 1.27 +// */ +// public function getInstanceForUpdate() { +// if ( !$this->getId() ) { +// return null; // anon +// } +// +// $user = self::newFromId( $this->getId() ); +// if ( !$user->loadFromId( self::READ_EXCLUSIVE ) ) { +// return null; +// } +// +// return $user; +// } +// +// /** +// * Checks if two user objects point to the same user. +// * +// * @since 1.25 ; takes a UserIdentity instead of a User since 1.32 +// * @param UserIdentity $user +// * @return boolean +// */ +// public function equals( UserIdentity $user ) { +// // XXX it's not clear whether central ID providers are supposed to obey this +// return $this->getName() === $user->getName(); +// } +// +// /** +// * Checks if usertalk is allowed +// * +// * @return boolean +// */ +// public function isAllowUsertalk() { +// return $this->mAllowUsertalk; +// } +// +} diff --git a/400_xowa/src/gplx/xowa/xtns/scribunto/libs/Scrib_lib_wikibase.java b/400_xowa/src/gplx/xowa/xtns/scribunto/libs/Scrib_lib_wikibase.java index bef723022..7b61a5231 100644 --- a/400_xowa/src/gplx/xowa/xtns/scribunto/libs/Scrib_lib_wikibase.java +++ b/400_xowa/src/gplx/xowa/xtns/scribunto/libs/Scrib_lib_wikibase.java @@ -18,6 +18,8 @@ import gplx.langs.jsons.*; import gplx.xowa.xtns.wbases.*; import gplx.xowa.xtns import gplx.xowa.wikis.domains.*; import gplx.xowa.xtns.scribunto.procs.*; import gplx.xowa.xtns.wbases.core.*; import gplx.xowa.mediawiki.extensions.Wikibase.client.includes.*; import gplx.xowa.mediawiki.extensions.Wikibase.client.includes.dataAccess.scribunto.*; +import gplx.xowa.mediawiki.*; +import gplx.xowa.mediawiki.extensions.Wikibase.lib.includes.Store.*; // REF.MW:https://github.com/wikimedia/mediawiki-extensions-Wikibase/blob/master/client/includes/DataAccess/Scribunto/Scribunto_LuaWikibaseLibrary.php public class Scrib_lib_wikibase implements Scrib_lib { private final Scrib_core core; @@ -29,6 +31,57 @@ public class Scrib_lib_wikibase implements Scrib_lib { public String Key() {return "mw.wikibase";} public Scrib_lua_mod Mod() {return mod;} private Scrib_lua_mod mod; public Scrib_proc_mgr Procs() {return procs;} private final Scrib_proc_mgr procs = new Scrib_proc_mgr(); + +// /** +// * @var WikibaseLanguageIndependentLuaBindings|null +// */ +// private $languageIndependentLuaBindings = null; +// +// /** +// * @var WikibaseLanguageDependentLuaBindings|null +// */ +// private $languageDependentLuaBindings = null; +// +// /** +// * @var EntityAccessor|null +// */ +// private $entityAccessor = null; +// +// /** +// * @var SnakSerializationRenderer[] +// */ +// private $snakSerializationRenderers = []; +// +// /** +// * @var LanguageFallbackChain|null +// */ +// private $fallbackChain = null; +// +// /** +// * @var ParserOutputUsageAccumulator|null +// */ +// private $usageAccumulator = null; +// +// /** +// * @var PropertyIdResolver|null +// */ +// private $propertyIdResolver = null; + + /** + * @var PropertyOrderProvider|null + */ +// private XomwPropertyOrderProvider propertyOrderProvider = null; + +// /** +// * @var EntityIdParser|null +// */ +// private $entityIdParser = null; +// +// /** +// * @var RepoLinker|null +// */ +// private $repoLinker = null; + public Scrib_lib Init() { procs.Init_by_lib(this, Proc_names); this.wdata_mgr = core.App().Wiki_mgr().Wdata_mgr(); @@ -184,7 +237,7 @@ public class Scrib_lib_wikibase implements Scrib_lib { } public boolean GetEntityUrl(Scrib_proc_args args, Scrib_proc_rslt rslt) { byte[] entityId = args.Pull_bry(0); - byte[] entity_url = Wbase_client.getDefaultInstance().RepoLinker().getEntityUrl(entityId); + byte[] entity_url = WikibaseClient.getDefaultInstance().RepoLinker().getEntityUrl(entityId); return rslt.Init_obj(entity_url); } public boolean GetEntityStatements(Scrib_proc_args args, Scrib_proc_rslt rslt) { @@ -277,8 +330,44 @@ public function formatValues( $snaksSerialization ) { public boolean GetPropertyOrder(Scrib_proc_args args, Scrib_proc_rslt rslt) { throw Err_.new_("wbase", "getPropertyOrder not implemented", "url", core.Page().Url().To_str()); } + + // TEST: + // * 0 propertyIds + // * same membership, but unsorted + // * more in lhs + // * more in rhs public boolean OrderProperties(Scrib_proc_args args, Scrib_proc_rslt rslt) { - throw Err_.new_("wbase", "orderProperties not implemented", "url", core.Page().Url().To_str()); + Keyval[] propertyIds = args.Pull_kv_ary_safe(0); + +// if (propertyIds.length == 0) { +// return rslt.Init_obj(propertyIds); +// } +// +// XophpArray orderedPropertiesPart = XophpArray.New(); +// XophpArray unorderedProperties = XophpArray.New(); +// +// // item is [{P1,1}] +// XophpArray propertyOrder = this.getPropertyOrderProvider().getPropertyOrder(); +// foreach (Keyval propertyIdKv in propertyIds) { +// // item is [{0,P1}] +// String propertyId = propertyIdKv.Val_to_str_or_empty(); +// if (propertyOrder.isset(propertyId)) { +// int propertyOrderSort = propertyOrder.Get_by_int(propertyId); +// orderedPropertiesPart.Set(propertyOrderSort, propertyId); +// } else { +// unorderedProperties.Add(propertyId); +// } +// } +// ksort( orderedPropertiesPart ); +// orderedProperties = XophpArray_.array_merge(orderedPropertiesPart, unorderedProperties); + + // Lua tables start at 1 +// XophpArray orderedPropertiesResult = XophpArray_.array_combine( +// range(1, count(orderedProperties)), XophpArray_.array_values(orderedProperties) +// ); +// return rslt.Init_obj(orderedPropertiesResult.To_kv_ary()); + return rslt.Init_obj(propertyIds); +// throw Err_.new_("wbase", "orderProperties not implemented", "url", core.Page().Url().To_str()); } public boolean GetLabel(Scrib_proc_args args, Scrib_proc_rslt rslt) { Wdata_doc wdoc = Get_wdoc_or_null(args, core, "GetLabel", true); @@ -359,6 +448,17 @@ public function formatValues( $snaksSerialization ) { if (wdoc == null && logMissing) Wdata_wiki_mgr.Log_missing_qid(core.Ctx(), type, xid_bry); return wdoc; } + + /** + * @return PropertyOrderProvider + */ +// private XomwPropertyOrderProvider getPropertyOrderProvider() { +// if (!XophpObject_.is_true(this.propertyOrderProvider)) { +// WikibaseClient wikibaseClient = WikibaseClient.getDefaultInstance(); +// this.propertyOrderProvider = wikibaseClient.getPropertyOrderProvider(); +// } +// return this.propertyOrderProvider; +// } } /* FOOTNOTE:GetEntityModuleName diff --git a/400_xowa/src/gplx/xowa/xtns/scribunto/libs/Scrib_lib_wikibase_entity.java b/400_xowa/src/gplx/xowa/xtns/scribunto/libs/Scrib_lib_wikibase_entity.java index dcfe48fc5..3545af76a 100644 --- a/400_xowa/src/gplx/xowa/xtns/scribunto/libs/Scrib_lib_wikibase_entity.java +++ b/400_xowa/src/gplx/xowa/xtns/scribunto/libs/Scrib_lib_wikibase_entity.java @@ -61,8 +61,7 @@ public class Scrib_lib_wikibase_entity implements Scrib_lib { // REF.MW:https:// return rslt.Init_obj(core.Wiki().Lang().Key_bry()); } public boolean FormatStatements(Scrib_proc_args args, Scrib_proc_rslt rslt) { - throw Err_.new_unimplemented(); -// return FormatPropertyValues(args, rslt); // NOTE: implementation should be like Visit_entity but return [[A]] instead of + return FormatPropertyValues(args, rslt); // NOTE: implementation should be like Visit_entity but return [[A]] instead of } public boolean FormatPropertyValues(Scrib_proc_args args, Scrib_proc_rslt rslt) { // get qid / pid