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