User.php 157 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345
  1. <?php
  2. /**
  3. * Implements the User class for the %MediaWiki software.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. use MediaWiki\Block\AbstractBlock;
  23. use MediaWiki\Block\DatabaseBlock;
  24. use MediaWiki\Block\SystemBlock;
  25. use MediaWiki\MediaWikiServices;
  26. use MediaWiki\Session\SessionManager;
  27. use MediaWiki\Session\Token;
  28. use MediaWiki\Auth\AuthManager;
  29. use MediaWiki\Auth\AuthenticationResponse;
  30. use MediaWiki\Auth\AuthenticationRequest;
  31. use MediaWiki\User\UserIdentity;
  32. use MediaWiki\Logger\LoggerFactory;
  33. use Wikimedia\Assert\Assert;
  34. use Wikimedia\IPSet;
  35. use Wikimedia\ScopedCallback;
  36. use Wikimedia\Rdbms\Database;
  37. use Wikimedia\Rdbms\DBExpectedError;
  38. use Wikimedia\Rdbms\IDatabase;
  39. /**
  40. * The User object encapsulates all of the user-specific settings (user_id,
  41. * name, rights, email address, options, last login time). Client
  42. * classes use the getXXX() functions to access these fields. These functions
  43. * do all the work of determining whether the user is logged in,
  44. * whether the requested option can be satisfied from cookies or
  45. * whether a database query is needed. Most of the settings needed
  46. * for rendering normal pages are set in the cookie to minimize use
  47. * of the database.
  48. */
  49. class User implements IDBAccessObject, UserIdentity {
  50. /**
  51. * Number of characters required for the user_token field.
  52. */
  53. const TOKEN_LENGTH = 32;
  54. /**
  55. * An invalid string value for the user_token field.
  56. */
  57. const INVALID_TOKEN = '*** INVALID ***';
  58. /**
  59. * Version number to tag cached versions of serialized User objects. Should be increased when
  60. * {@link $mCacheVars} or one of it's members changes.
  61. */
  62. const VERSION = 14;
  63. /**
  64. * Exclude user options that are set to their default value.
  65. * @since 1.25
  66. */
  67. const GETOPTIONS_EXCLUDE_DEFAULTS = 1;
  68. /**
  69. * @since 1.27
  70. */
  71. const CHECK_USER_RIGHTS = true;
  72. /**
  73. * @since 1.27
  74. */
  75. const IGNORE_USER_RIGHTS = false;
  76. /**
  77. * Array of Strings List of member variables which are saved to the
  78. * shared cache (memcached). Any operation which changes the
  79. * corresponding database fields must call a cache-clearing function.
  80. * @showinitializer
  81. * @var string[]
  82. */
  83. protected static $mCacheVars = [
  84. // user table
  85. 'mId',
  86. 'mName',
  87. 'mRealName',
  88. 'mEmail',
  89. 'mTouched',
  90. 'mToken',
  91. 'mEmailAuthenticated',
  92. 'mEmailToken',
  93. 'mEmailTokenExpires',
  94. 'mRegistration',
  95. 'mEditCount',
  96. // user_groups table
  97. 'mGroupMemberships',
  98. // user_properties table
  99. 'mOptionOverrides',
  100. // actor table
  101. 'mActorId',
  102. ];
  103. /** Cache variables */
  104. // @{
  105. /** @var int */
  106. public $mId;
  107. /** @var string */
  108. public $mName;
  109. /** @var int|null */
  110. protected $mActorId;
  111. /** @var string */
  112. public $mRealName;
  113. /** @var string */
  114. public $mEmail;
  115. /** @var string TS_MW timestamp from the DB */
  116. public $mTouched;
  117. /** @var string TS_MW timestamp from cache */
  118. protected $mQuickTouched;
  119. /** @var string */
  120. protected $mToken;
  121. /** @var string */
  122. public $mEmailAuthenticated;
  123. /** @var string */
  124. protected $mEmailToken;
  125. /** @var string */
  126. protected $mEmailTokenExpires;
  127. /** @var string */
  128. protected $mRegistration;
  129. /** @var int */
  130. protected $mEditCount;
  131. /** @var UserGroupMembership[] Associative array of (group name => UserGroupMembership object) */
  132. protected $mGroupMemberships;
  133. /** @var array */
  134. protected $mOptionOverrides;
  135. // @}
  136. // @{
  137. /**
  138. * @var bool Whether the cache variables have been loaded.
  139. */
  140. public $mOptionsLoaded;
  141. /**
  142. * @var array|bool Array with already loaded items or true if all items have been loaded.
  143. */
  144. protected $mLoadedItems = [];
  145. // @}
  146. /**
  147. * @var string Initialization data source if mLoadedItems!==true. May be one of:
  148. * - 'defaults' anonymous user initialised from class defaults
  149. * - 'name' initialise from mName
  150. * - 'id' initialise from mId
  151. * - 'actor' initialise from mActorId
  152. * - 'session' log in from session if possible
  153. *
  154. * Use the User::newFrom*() family of functions to set this.
  155. */
  156. public $mFrom;
  157. /**
  158. * Lazy-initialized variables, invalidated with clearInstanceCache
  159. */
  160. /** @var int|bool */
  161. protected $mNewtalk;
  162. /** @var string */
  163. protected $mDatePreference;
  164. /** @var string */
  165. public $mBlockedby;
  166. /** @var string */
  167. protected $mHash;
  168. /** @var string */
  169. protected $mBlockreason;
  170. /** @var array */
  171. protected $mEffectiveGroups;
  172. /** @var array */
  173. protected $mImplicitGroups;
  174. /** @var array */
  175. protected $mFormerGroups;
  176. /** @var AbstractBlock */
  177. protected $mGlobalBlock;
  178. /** @var bool */
  179. protected $mLocked;
  180. /** @var bool */
  181. public $mHideName;
  182. /** @var array */
  183. public $mOptions;
  184. /** @var WebRequest */
  185. private $mRequest;
  186. /** @var AbstractBlock */
  187. public $mBlock;
  188. /** @var bool */
  189. protected $mAllowUsertalk;
  190. /** @var AbstractBlock|bool */
  191. private $mBlockedFromCreateAccount = false;
  192. /** @var int User::READ_* constant bitfield used to load data */
  193. protected $queryFlagsUsed = self::READ_NORMAL;
  194. /** @var int[] */
  195. public static $idCacheByName = [];
  196. /**
  197. * Lightweight constructor for an anonymous user.
  198. * Use the User::newFrom* factory functions for other kinds of users.
  199. *
  200. * @see newFromName()
  201. * @see newFromId()
  202. * @see newFromActorId()
  203. * @see newFromConfirmationCode()
  204. * @see newFromSession()
  205. * @see newFromRow()
  206. */
  207. public function __construct() {
  208. $this->clearInstanceCache( 'defaults' );
  209. }
  210. /**
  211. * @return string
  212. */
  213. public function __toString() {
  214. return (string)$this->getName();
  215. }
  216. public function &__get( $name ) {
  217. // A shortcut for $mRights deprecation phase
  218. if ( $name === 'mRights' ) {
  219. $copy = $this->getRights();
  220. return $copy;
  221. } elseif ( !property_exists( $this, $name ) ) {
  222. // T227688 - do not break $u->foo['bar'] = 1
  223. wfLogWarning( 'tried to get non-existent property' );
  224. $this->$name = null;
  225. return $this->$name;
  226. } else {
  227. wfLogWarning( 'tried to get non-visible property' );
  228. $null = null;
  229. return $null;
  230. }
  231. }
  232. public function __set( $name, $value ) {
  233. // A shortcut for $mRights deprecation phase, only known legitimate use was for
  234. // testing purposes, other uses seem bad in principle
  235. if ( $name === 'mRights' ) {
  236. MediaWikiServices::getInstance()->getPermissionManager()->overrideUserRightsForTesting(
  237. $this,
  238. is_null( $value ) ? [] : $value
  239. );
  240. } elseif ( !property_exists( $this, $name ) ) {
  241. $this->$name = $value;
  242. } else {
  243. wfLogWarning( 'tried to set non-visible property' );
  244. }
  245. }
  246. /**
  247. * Test if it's safe to load this User object.
  248. *
  249. * You should typically check this before using $wgUser or
  250. * RequestContext::getUser in a method that might be called before the
  251. * system has been fully initialized. If the object is unsafe, you should
  252. * use an anonymous user:
  253. * \code
  254. * $user = $wgUser->isSafeToLoad() ? $wgUser : new User;
  255. * \endcode
  256. *
  257. * @since 1.27
  258. * @return bool
  259. */
  260. public function isSafeToLoad() {
  261. global $wgFullyInitialised;
  262. // The user is safe to load if:
  263. // * MW_NO_SESSION is undefined AND $wgFullyInitialised is true (safe to use session data)
  264. // * mLoadedItems === true (already loaded)
  265. // * mFrom !== 'session' (sessions not involved at all)
  266. return ( !defined( 'MW_NO_SESSION' ) && $wgFullyInitialised ) ||
  267. $this->mLoadedItems === true || $this->mFrom !== 'session';
  268. }
  269. /**
  270. * Load the user table data for this object from the source given by mFrom.
  271. *
  272. * @param int $flags User::READ_* constant bitfield
  273. */
  274. public function load( $flags = self::READ_NORMAL ) {
  275. global $wgFullyInitialised;
  276. if ( $this->mLoadedItems === true ) {
  277. return;
  278. }
  279. // Set it now to avoid infinite recursion in accessors
  280. $oldLoadedItems = $this->mLoadedItems;
  281. $this->mLoadedItems = true;
  282. $this->queryFlagsUsed = $flags;
  283. // If this is called too early, things are likely to break.
  284. if ( !$wgFullyInitialised && $this->mFrom === 'session' ) {
  285. \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
  286. ->warning( 'User::loadFromSession called before the end of Setup.php', [
  287. 'exception' => new Exception( 'User::loadFromSession called before the end of Setup.php' ),
  288. ] );
  289. $this->loadDefaults();
  290. $this->mLoadedItems = $oldLoadedItems;
  291. return;
  292. }
  293. switch ( $this->mFrom ) {
  294. case 'defaults':
  295. $this->loadDefaults();
  296. break;
  297. case 'id':
  298. // Make sure this thread sees its own changes, if the ID isn't 0
  299. if ( $this->mId != 0 ) {
  300. $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
  301. if ( $lb->hasOrMadeRecentMasterChanges() ) {
  302. $flags |= self::READ_LATEST;
  303. $this->queryFlagsUsed = $flags;
  304. }
  305. }
  306. $this->loadFromId( $flags );
  307. break;
  308. case 'actor':
  309. case 'name':
  310. // Make sure this thread sees its own changes
  311. $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
  312. if ( $lb->hasOrMadeRecentMasterChanges() ) {
  313. $flags |= self::READ_LATEST;
  314. $this->queryFlagsUsed = $flags;
  315. }
  316. list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
  317. $row = wfGetDB( $index )->selectRow(
  318. 'actor',
  319. [ 'actor_id', 'actor_user', 'actor_name' ],
  320. $this->mFrom === 'name' ? [ 'actor_name' => $this->mName ] : [ 'actor_id' => $this->mActorId ],
  321. __METHOD__,
  322. $options
  323. );
  324. if ( !$row ) {
  325. // Ugh.
  326. $this->loadDefaults( $this->mFrom === 'name' ? $this->mName : false );
  327. } elseif ( $row->actor_user ) {
  328. $this->mId = $row->actor_user;
  329. $this->loadFromId( $flags );
  330. } else {
  331. $this->loadDefaults( $row->actor_name, $row->actor_id );
  332. }
  333. break;
  334. case 'session':
  335. if ( !$this->loadFromSession() ) {
  336. // Loading from session failed. Load defaults.
  337. $this->loadDefaults();
  338. }
  339. Hooks::run( 'UserLoadAfterLoadFromSession', [ $this ] );
  340. break;
  341. default:
  342. throw new UnexpectedValueException(
  343. "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
  344. }
  345. }
  346. /**
  347. * Load user table data, given mId has already been set.
  348. * @param int $flags User::READ_* constant bitfield
  349. * @return bool False if the ID does not exist, true otherwise
  350. */
  351. public function loadFromId( $flags = self::READ_NORMAL ) {
  352. if ( $this->mId == 0 ) {
  353. // Anonymous users are not in the database (don't need cache)
  354. $this->loadDefaults();
  355. return false;
  356. }
  357. // Try cache (unless this needs data from the master DB).
  358. // NOTE: if this thread called saveSettings(), the cache was cleared.
  359. $latest = DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST );
  360. if ( $latest ) {
  361. if ( !$this->loadFromDatabase( $flags ) ) {
  362. // Can't load from ID
  363. return false;
  364. }
  365. } else {
  366. $this->loadFromCache();
  367. }
  368. $this->mLoadedItems = true;
  369. $this->queryFlagsUsed = $flags;
  370. return true;
  371. }
  372. /**
  373. * @since 1.27
  374. * @param string $dbDomain
  375. * @param int $userId
  376. */
  377. public static function purge( $dbDomain, $userId ) {
  378. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  379. $key = $cache->makeGlobalKey( 'user', 'id', $dbDomain, $userId );
  380. $cache->delete( $key );
  381. }
  382. /**
  383. * @since 1.27
  384. * @param WANObjectCache $cache
  385. * @return string
  386. */
  387. protected function getCacheKey( WANObjectCache $cache ) {
  388. $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
  389. return $cache->makeGlobalKey( 'user', 'id', $lbFactory->getLocalDomainID(), $this->mId );
  390. }
  391. /**
  392. * @param WANObjectCache $cache
  393. * @return string[]
  394. * @since 1.28
  395. */
  396. public function getMutableCacheKeys( WANObjectCache $cache ) {
  397. $id = $this->getId();
  398. return $id ? [ $this->getCacheKey( $cache ) ] : [];
  399. }
  400. /**
  401. * Load user data from shared cache, given mId has already been set.
  402. *
  403. * @return bool True
  404. * @since 1.25
  405. */
  406. protected function loadFromCache() {
  407. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  408. $data = $cache->getWithSetCallback(
  409. $this->getCacheKey( $cache ),
  410. $cache::TTL_HOUR,
  411. function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
  412. $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
  413. wfDebug( "User: cache miss for user {$this->mId}\n" );
  414. $this->loadFromDatabase( self::READ_NORMAL );
  415. $this->loadGroups();
  416. $this->loadOptions();
  417. $data = [];
  418. foreach ( self::$mCacheVars as $name ) {
  419. $data[$name] = $this->$name;
  420. }
  421. $ttl = $cache->adaptiveTTL( wfTimestamp( TS_UNIX, $this->mTouched ), $ttl );
  422. // if a user group membership is about to expire, the cache needs to
  423. // expire at that time (T163691)
  424. foreach ( $this->mGroupMemberships as $ugm ) {
  425. if ( $ugm->getExpiry() ) {
  426. $secondsUntilExpiry = wfTimestamp( TS_UNIX, $ugm->getExpiry() ) - time();
  427. if ( $secondsUntilExpiry > 0 && $secondsUntilExpiry < $ttl ) {
  428. $ttl = $secondsUntilExpiry;
  429. }
  430. }
  431. }
  432. return $data;
  433. },
  434. [ 'pcTTL' => $cache::TTL_PROC_LONG, 'version' => self::VERSION ]
  435. );
  436. // Restore from cache
  437. foreach ( self::$mCacheVars as $name ) {
  438. $this->$name = $data[$name];
  439. }
  440. return true;
  441. }
  442. /** @name newFrom*() static factory methods */
  443. // @{
  444. /**
  445. * Static factory method for creation from username.
  446. *
  447. * This is slightly less efficient than newFromId(), so use newFromId() if
  448. * you have both an ID and a name handy.
  449. *
  450. * @param string $name Username, validated by Title::newFromText()
  451. * @param string|bool $validate Validate username. Takes the same parameters as
  452. * User::getCanonicalName(), except that true is accepted as an alias
  453. * for 'valid', for BC.
  454. *
  455. * @return User|bool User object, or false if the username is invalid
  456. * (e.g. if it contains illegal characters or is an IP address). If the
  457. * username is not present in the database, the result will be a user object
  458. * with a name, zero user ID and default settings.
  459. */
  460. public static function newFromName( $name, $validate = 'valid' ) {
  461. if ( $validate === true ) {
  462. $validate = 'valid';
  463. }
  464. $name = self::getCanonicalName( $name, $validate );
  465. if ( $name === false ) {
  466. return false;
  467. }
  468. // Create unloaded user object
  469. $u = new User;
  470. $u->mName = $name;
  471. $u->mFrom = 'name';
  472. $u->setItemLoaded( 'name' );
  473. return $u;
  474. }
  475. /**
  476. * Static factory method for creation from a given user ID.
  477. *
  478. * @param int $id Valid user ID
  479. * @return User The corresponding User object
  480. */
  481. public static function newFromId( $id ) {
  482. $u = new User;
  483. $u->mId = $id;
  484. $u->mFrom = 'id';
  485. $u->setItemLoaded( 'id' );
  486. return $u;
  487. }
  488. /**
  489. * Static factory method for creation from a given actor ID.
  490. *
  491. * @since 1.31
  492. * @param int $id Valid actor ID
  493. * @return User The corresponding User object
  494. */
  495. public static function newFromActorId( $id ) {
  496. $u = new User;
  497. $u->mActorId = $id;
  498. $u->mFrom = 'actor';
  499. $u->setItemLoaded( 'actor' );
  500. return $u;
  501. }
  502. /**
  503. * Returns a User object corresponding to the given UserIdentity.
  504. *
  505. * @since 1.32
  506. *
  507. * @param UserIdentity $identity
  508. *
  509. * @return User
  510. */
  511. public static function newFromIdentity( UserIdentity $identity ) {
  512. if ( $identity instanceof User ) {
  513. return $identity;
  514. }
  515. return self::newFromAnyId(
  516. $identity->getId() === 0 ? null : $identity->getId(),
  517. $identity->getName() === '' ? null : $identity->getName(),
  518. $identity->getActorId() === 0 ? null : $identity->getActorId()
  519. );
  520. }
  521. /**
  522. * Static factory method for creation from an ID, name, and/or actor ID
  523. *
  524. * This does not check that the ID, name, and actor ID all correspond to
  525. * the same user.
  526. *
  527. * @since 1.31
  528. * @param int|null $userId User ID, if known
  529. * @param string|null $userName User name, if known
  530. * @param int|null $actorId Actor ID, if known
  531. * @param bool|string $dbDomain remote wiki to which the User/Actor ID applies, or false if none
  532. * @return User
  533. */
  534. public static function newFromAnyId( $userId, $userName, $actorId, $dbDomain = false ) {
  535. // Stop-gap solution for the problem described in T222212.
  536. // Force the User ID and Actor ID to zero for users loaded from the database
  537. // of another wiki, to prevent subtle data corruption and confusing failure modes.
  538. if ( $dbDomain !== false ) {
  539. $userId = 0;
  540. $actorId = 0;
  541. }
  542. $user = new User;
  543. $user->mFrom = 'defaults';
  544. if ( $actorId !== null ) {
  545. $user->mActorId = (int)$actorId;
  546. if ( $user->mActorId !== 0 ) {
  547. $user->mFrom = 'actor';
  548. }
  549. $user->setItemLoaded( 'actor' );
  550. }
  551. if ( $userName !== null && $userName !== '' ) {
  552. $user->mName = $userName;
  553. $user->mFrom = 'name';
  554. $user->setItemLoaded( 'name' );
  555. }
  556. if ( $userId !== null ) {
  557. $user->mId = (int)$userId;
  558. if ( $user->mId !== 0 ) {
  559. $user->mFrom = 'id';
  560. }
  561. $user->setItemLoaded( 'id' );
  562. }
  563. if ( $user->mFrom === 'defaults' ) {
  564. throw new InvalidArgumentException(
  565. 'Cannot create a user with no name, no ID, and no actor ID'
  566. );
  567. }
  568. return $user;
  569. }
  570. /**
  571. * Factory method to fetch whichever user has a given email confirmation code.
  572. * This code is generated when an account is created or its e-mail address
  573. * has changed.
  574. *
  575. * If the code is invalid or has expired, returns NULL.
  576. *
  577. * @param string $code Confirmation code
  578. * @param int $flags User::READ_* bitfield
  579. * @return User|null
  580. */
  581. public static function newFromConfirmationCode( $code, $flags = 0 ) {
  582. $db = ( $flags & self::READ_LATEST ) == self::READ_LATEST
  583. ? wfGetDB( DB_MASTER )
  584. : wfGetDB( DB_REPLICA );
  585. $id = $db->selectField(
  586. 'user',
  587. 'user_id',
  588. [
  589. 'user_email_token' => md5( $code ),
  590. 'user_email_token_expires > ' . $db->addQuotes( $db->timestamp() ),
  591. ]
  592. );
  593. return $id ? self::newFromId( $id ) : null;
  594. }
  595. /**
  596. * Create a new user object using data from session. If the login
  597. * credentials are invalid, the result is an anonymous user.
  598. *
  599. * @param WebRequest|null $request Object to use; $wgRequest will be used if omitted.
  600. * @return User
  601. */
  602. public static function newFromSession( WebRequest $request = null ) {
  603. $user = new User;
  604. $user->mFrom = 'session';
  605. $user->mRequest = $request;
  606. return $user;
  607. }
  608. /**
  609. * Create a new user object from a user row.
  610. * The row should have the following fields from the user table in it:
  611. * - either user_name or user_id to load further data if needed (or both)
  612. * - user_real_name
  613. * - all other fields (email, etc.)
  614. * It is useless to provide the remaining fields if either user_id,
  615. * user_name and user_real_name are not provided because the whole row
  616. * will be loaded once more from the database when accessing them.
  617. *
  618. * @param stdClass $row A row from the user table
  619. * @param array|null $data Further data to load into the object
  620. * (see User::loadFromRow for valid keys)
  621. * @return User
  622. */
  623. public static function newFromRow( $row, $data = null ) {
  624. $user = new User;
  625. $user->loadFromRow( $row, $data );
  626. return $user;
  627. }
  628. /**
  629. * Static factory method for creation of a "system" user from username.
  630. *
  631. * A "system" user is an account that's used to attribute logged actions
  632. * taken by MediaWiki itself, as opposed to a bot or human user. Examples
  633. * might include the 'Maintenance script' or 'Conversion script' accounts
  634. * used by various scripts in the maintenance/ directory or accounts such
  635. * as 'MediaWiki message delivery' used by the MassMessage extension.
  636. *
  637. * This can optionally create the user if it doesn't exist, and "steal" the
  638. * account if it does exist.
  639. *
  640. * "Stealing" an existing user is intended to make it impossible for normal
  641. * authentication processes to use the account, effectively disabling the
  642. * account for normal use:
  643. * - Email is invalidated, to prevent account recovery by emailing a
  644. * temporary password and to disassociate the account from the existing
  645. * human.
  646. * - The token is set to a magic invalid value, to kill existing sessions
  647. * and to prevent $this->setToken() calls from resetting the token to a
  648. * valid value.
  649. * - SessionManager is instructed to prevent new sessions for the user, to
  650. * do things like deauthorizing OAuth consumers.
  651. * - AuthManager is instructed to revoke access, to invalidate or remove
  652. * passwords and other credentials.
  653. *
  654. * @param string $name Username
  655. * @param array $options Options are:
  656. * - validate: As for User::getCanonicalName(), default 'valid'
  657. * - create: Whether to create the user if it doesn't already exist, default true
  658. * - steal: Whether to "disable" the account for normal use if it already
  659. * exists, default false
  660. * @return User|null
  661. * @since 1.27
  662. */
  663. public static function newSystemUser( $name, $options = [] ) {
  664. $options += [
  665. 'validate' => 'valid',
  666. 'create' => true,
  667. 'steal' => false,
  668. ];
  669. $name = self::getCanonicalName( $name, $options['validate'] );
  670. if ( $name === false ) {
  671. return null;
  672. }
  673. $dbr = wfGetDB( DB_REPLICA );
  674. $userQuery = self::getQueryInfo();
  675. $row = $dbr->selectRow(
  676. $userQuery['tables'],
  677. $userQuery['fields'],
  678. [ 'user_name' => $name ],
  679. __METHOD__,
  680. [],
  681. $userQuery['joins']
  682. );
  683. if ( !$row ) {
  684. // Try the master database...
  685. $dbw = wfGetDB( DB_MASTER );
  686. $row = $dbw->selectRow(
  687. $userQuery['tables'],
  688. $userQuery['fields'],
  689. [ 'user_name' => $name ],
  690. __METHOD__,
  691. [],
  692. $userQuery['joins']
  693. );
  694. }
  695. if ( !$row ) {
  696. // No user. Create it?
  697. return $options['create']
  698. ? self::createNew( $name, [ 'token' => self::INVALID_TOKEN ] )
  699. : null;
  700. }
  701. $user = self::newFromRow( $row );
  702. // A user is considered to exist as a non-system user if it can
  703. // authenticate, or has an email set, or has a non-invalid token.
  704. if ( $user->mEmail || $user->mToken !== self::INVALID_TOKEN ||
  705. AuthManager::singleton()->userCanAuthenticate( $name )
  706. ) {
  707. // User exists. Steal it?
  708. if ( !$options['steal'] ) {
  709. return null;
  710. }
  711. AuthManager::singleton()->revokeAccessForUser( $name );
  712. $user->invalidateEmail();
  713. $user->mToken = self::INVALID_TOKEN;
  714. $user->saveSettings();
  715. SessionManager::singleton()->preventSessionsForUser( $user->getName() );
  716. }
  717. return $user;
  718. }
  719. // @}
  720. /**
  721. * Get the username corresponding to a given user ID
  722. * @param int $id User ID
  723. * @return string|bool The corresponding username
  724. */
  725. public static function whoIs( $id ) {
  726. return UserCache::singleton()->getProp( $id, 'name' );
  727. }
  728. /**
  729. * Get the real name of a user given their user ID
  730. *
  731. * @param int $id User ID
  732. * @return string|bool The corresponding user's real name
  733. */
  734. public static function whoIsReal( $id ) {
  735. return UserCache::singleton()->getProp( $id, 'real_name' );
  736. }
  737. /**
  738. * Get database id given a user name
  739. * @param string $name Username
  740. * @param int $flags User::READ_* constant bitfield
  741. * @return int|null The corresponding user's ID, or null if user is nonexistent
  742. */
  743. public static function idFromName( $name, $flags = self::READ_NORMAL ) {
  744. // Don't explode on self::$idCacheByName[$name] if $name is not a string but e.g. a User object
  745. $name = (string)$name;
  746. $nt = Title::makeTitleSafe( NS_USER, $name );
  747. if ( is_null( $nt ) ) {
  748. // Illegal name
  749. return null;
  750. }
  751. if ( !( $flags & self::READ_LATEST ) && array_key_exists( $name, self::$idCacheByName ) ) {
  752. return is_null( self::$idCacheByName[$name] ) ? null : (int)self::$idCacheByName[$name];
  753. }
  754. list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
  755. $db = wfGetDB( $index );
  756. $s = $db->selectRow(
  757. 'user',
  758. [ 'user_id' ],
  759. [ 'user_name' => $nt->getText() ],
  760. __METHOD__,
  761. $options
  762. );
  763. if ( $s === false ) {
  764. $result = null;
  765. } else {
  766. $result = (int)$s->user_id;
  767. }
  768. if ( count( self::$idCacheByName ) >= 1000 ) {
  769. self::$idCacheByName = [];
  770. }
  771. self::$idCacheByName[$name] = $result;
  772. return $result;
  773. }
  774. /**
  775. * Reset the cache used in idFromName(). For use in tests.
  776. */
  777. public static function resetIdByNameCache() {
  778. self::$idCacheByName = [];
  779. }
  780. /**
  781. * Does the string match an anonymous IP address?
  782. *
  783. * This function exists for username validation, in order to reject
  784. * usernames which are similar in form to IP addresses. Strings such
  785. * as 300.300.300.300 will return true because it looks like an IP
  786. * address, despite not being strictly valid.
  787. *
  788. * We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP
  789. * address because the usemod software would "cloak" anonymous IP
  790. * addresses like this, if we allowed accounts like this to be created
  791. * new users could get the old edits of these anonymous users.
  792. *
  793. * @param string $name Name to match
  794. * @return bool
  795. */
  796. public static function isIP( $name ) {
  797. return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name )
  798. || IP::isIPv6( $name );
  799. }
  800. /**
  801. * Is the user an IP range?
  802. *
  803. * @since 1.30
  804. * @return bool
  805. */
  806. public function isIPRange() {
  807. return IP::isValidRange( $this->mName );
  808. }
  809. /**
  810. * Is the input a valid username?
  811. *
  812. * Checks if the input is a valid username, we don't want an empty string,
  813. * an IP address, anything that contains slashes (would mess up subpages),
  814. * is longer than the maximum allowed username size or doesn't begin with
  815. * a capital letter.
  816. *
  817. * @param string $name Name to match
  818. * @return bool
  819. */
  820. public static function isValidUserName( $name ) {
  821. global $wgMaxNameChars;
  822. if ( $name == ''
  823. || self::isIP( $name )
  824. || strpos( $name, '/' ) !== false
  825. || strlen( $name ) > $wgMaxNameChars
  826. || $name != MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name )
  827. ) {
  828. return false;
  829. }
  830. // Ensure that the name can't be misresolved as a different title,
  831. // such as with extra namespace keys at the start.
  832. $parsed = Title::newFromText( $name );
  833. if ( is_null( $parsed )
  834. || $parsed->getNamespace()
  835. || strcmp( $name, $parsed->getPrefixedText() ) ) {
  836. return false;
  837. }
  838. // Check an additional blacklist of troublemaker characters.
  839. // Should these be merged into the title char list?
  840. $unicodeBlacklist = '/[' .
  841. '\x{0080}-\x{009f}' . # iso-8859-1 control chars
  842. '\x{00a0}' . # non-breaking space
  843. '\x{2000}-\x{200f}' . # various whitespace
  844. '\x{2028}-\x{202f}' . # breaks and control chars
  845. '\x{3000}' . # ideographic space
  846. '\x{e000}-\x{f8ff}' . # private use
  847. ']/u';
  848. if ( preg_match( $unicodeBlacklist, $name ) ) {
  849. return false;
  850. }
  851. return true;
  852. }
  853. /**
  854. * Usernames which fail to pass this function will be blocked
  855. * from user login and new account registrations, but may be used
  856. * internally by batch processes.
  857. *
  858. * If an account already exists in this form, login will be blocked
  859. * by a failure to pass this function.
  860. *
  861. * @param string $name Name to match
  862. * @return bool
  863. */
  864. public static function isUsableName( $name ) {
  865. global $wgReservedUsernames;
  866. // Must be a valid username, obviously ;)
  867. if ( !self::isValidUserName( $name ) ) {
  868. return false;
  869. }
  870. static $reservedUsernames = false;
  871. if ( !$reservedUsernames ) {
  872. $reservedUsernames = $wgReservedUsernames;
  873. Hooks::run( 'UserGetReservedNames', [ &$reservedUsernames ] );
  874. }
  875. // Certain names may be reserved for batch processes.
  876. foreach ( $reservedUsernames as $reserved ) {
  877. if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
  878. $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->plain();
  879. }
  880. if ( $reserved == $name ) {
  881. return false;
  882. }
  883. }
  884. return true;
  885. }
  886. /**
  887. * Return the users who are members of the given group(s). In case of multiple groups,
  888. * users who are members of at least one of them are returned.
  889. *
  890. * @param string|array $groups A single group name or an array of group names
  891. * @param int $limit Max number of users to return. The actual limit will never exceed 5000
  892. * records; larger values are ignored.
  893. * @param int|null $after ID the user to start after
  894. * @return UserArrayFromResult
  895. */
  896. public static function findUsersByGroup( $groups, $limit = 5000, $after = null ) {
  897. if ( $groups === [] ) {
  898. return UserArrayFromResult::newFromIDs( [] );
  899. }
  900. $groups = array_unique( (array)$groups );
  901. $limit = min( 5000, $limit );
  902. $conds = [ 'ug_group' => $groups ];
  903. if ( $after !== null ) {
  904. $conds[] = 'ug_user > ' . (int)$after;
  905. }
  906. $dbr = wfGetDB( DB_REPLICA );
  907. $ids = $dbr->selectFieldValues(
  908. 'user_groups',
  909. 'ug_user',
  910. $conds,
  911. __METHOD__,
  912. [
  913. 'DISTINCT' => true,
  914. 'ORDER BY' => 'ug_user',
  915. 'LIMIT' => $limit,
  916. ]
  917. ) ?: [];
  918. return UserArray::newFromIDs( $ids );
  919. }
  920. /**
  921. * Usernames which fail to pass this function will be blocked
  922. * from new account registrations, but may be used internally
  923. * either by batch processes or by user accounts which have
  924. * already been created.
  925. *
  926. * Additional blacklisting may be added here rather than in
  927. * isValidUserName() to avoid disrupting existing accounts.
  928. *
  929. * @param string $name String to match
  930. * @return bool
  931. */
  932. public static function isCreatableName( $name ) {
  933. global $wgInvalidUsernameCharacters;
  934. // Ensure that the username isn't longer than 235 bytes, so that
  935. // (at least for the builtin skins) user javascript and css files
  936. // will work. (T25080)
  937. if ( strlen( $name ) > 235 ) {
  938. wfDebugLog( 'username', __METHOD__ .
  939. ": '$name' invalid due to length" );
  940. return false;
  941. }
  942. // Preg yells if you try to give it an empty string
  943. if ( $wgInvalidUsernameCharacters !== '' &&
  944. preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name )
  945. ) {
  946. wfDebugLog( 'username', __METHOD__ .
  947. ": '$name' invalid due to wgInvalidUsernameCharacters" );
  948. return false;
  949. }
  950. return self::isUsableName( $name );
  951. }
  952. /**
  953. * Is the input a valid password for this user?
  954. *
  955. * @param string $password Desired password
  956. * @return bool
  957. */
  958. public function isValidPassword( $password ) {
  959. // simple boolean wrapper for checkPasswordValidity
  960. return $this->checkPasswordValidity( $password )->isGood();
  961. }
  962. /**
  963. * Check if this is a valid password for this user
  964. *
  965. * Returns a Status object with a set of messages describing
  966. * problems with the password. If the return status is fatal,
  967. * the action should be refused and the password should not be
  968. * checked at all (this is mainly meant for DoS mitigation).
  969. * If the return value is OK but not good, the password can be checked,
  970. * but the user should not be able to set their password to this.
  971. * The value of the returned Status object will be an array which
  972. * can have the following fields:
  973. * - forceChange (bool): if set to true, the user should not be
  974. * allowed to log with this password unless they change it during
  975. * the login process (see ResetPasswordSecondaryAuthenticationProvider).
  976. * - suggestChangeOnLogin (bool): if set to true, the user should be prompted for
  977. * a password change on login.
  978. *
  979. * @param string $password Desired password
  980. * @return Status
  981. * @since 1.23
  982. */
  983. public function checkPasswordValidity( $password ) {
  984. global $wgPasswordPolicy;
  985. $upp = new UserPasswordPolicy(
  986. $wgPasswordPolicy['policies'],
  987. $wgPasswordPolicy['checks']
  988. );
  989. $status = Status::newGood( [] );
  990. $result = false; // init $result to false for the internal checks
  991. if ( !Hooks::run( 'isValidPassword', [ $password, &$result, $this ] ) ) {
  992. $status->error( $result );
  993. return $status;
  994. }
  995. if ( $result === false ) {
  996. $status->merge( $upp->checkUserPassword( $this, $password ), true );
  997. return $status;
  998. }
  999. if ( $result === true ) {
  1000. return $status;
  1001. }
  1002. $status->error( $result );
  1003. return $status; // the isValidPassword hook set a string $result and returned true
  1004. }
  1005. /**
  1006. * Given unvalidated user input, return a canonical username, or false if
  1007. * the username is invalid.
  1008. * @param string $name User input
  1009. * @param string|bool $validate Type of validation to use:
  1010. * - false No validation
  1011. * - 'valid' Valid for batch processes
  1012. * - 'usable' Valid for batch processes and login
  1013. * - 'creatable' Valid for batch processes, login and account creation
  1014. *
  1015. * @throws InvalidArgumentException
  1016. * @return bool|string
  1017. */
  1018. public static function getCanonicalName( $name, $validate = 'valid' ) {
  1019. // Force usernames to capital
  1020. $name = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name );
  1021. # Reject names containing '#'; these will be cleaned up
  1022. # with title normalisation, but then it's too late to
  1023. # check elsewhere
  1024. if ( strpos( $name, '#' ) !== false ) {
  1025. return false;
  1026. }
  1027. // Clean up name according to title rules,
  1028. // but only when validation is requested (T14654)
  1029. $t = ( $validate !== false ) ?
  1030. Title::newFromText( $name, NS_USER ) : Title::makeTitle( NS_USER, $name );
  1031. // Check for invalid titles
  1032. if ( is_null( $t ) || $t->getNamespace() !== NS_USER || $t->isExternal() ) {
  1033. return false;
  1034. }
  1035. $name = $t->getText();
  1036. switch ( $validate ) {
  1037. case false:
  1038. break;
  1039. case 'valid':
  1040. if ( !self::isValidUserName( $name ) ) {
  1041. $name = false;
  1042. }
  1043. break;
  1044. case 'usable':
  1045. if ( !self::isUsableName( $name ) ) {
  1046. $name = false;
  1047. }
  1048. break;
  1049. case 'creatable':
  1050. if ( !self::isCreatableName( $name ) ) {
  1051. $name = false;
  1052. }
  1053. break;
  1054. default:
  1055. throw new InvalidArgumentException(
  1056. 'Invalid parameter value for $validate in ' . __METHOD__ );
  1057. }
  1058. return $name;
  1059. }
  1060. /**
  1061. * Set cached properties to default.
  1062. *
  1063. * @note This no longer clears uncached lazy-initialised properties;
  1064. * the constructor does that instead.
  1065. *
  1066. * @param string|bool $name
  1067. * @param int|null $actorId
  1068. */
  1069. public function loadDefaults( $name = false, $actorId = null ) {
  1070. $this->mId = 0;
  1071. $this->mName = $name;
  1072. $this->mActorId = $actorId;
  1073. $this->mRealName = '';
  1074. $this->mEmail = '';
  1075. $this->mOptionOverrides = null;
  1076. $this->mOptionsLoaded = false;
  1077. $loggedOut = $this->mRequest && !defined( 'MW_NO_SESSION' )
  1078. ? $this->mRequest->getSession()->getLoggedOutTimestamp() : 0;
  1079. if ( $loggedOut !== 0 ) {
  1080. $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
  1081. } else {
  1082. $this->mTouched = '1'; # Allow any pages to be cached
  1083. }
  1084. $this->mToken = null; // Don't run cryptographic functions till we need a token
  1085. $this->mEmailAuthenticated = null;
  1086. $this->mEmailToken = '';
  1087. $this->mEmailTokenExpires = null;
  1088. $this->mRegistration = wfTimestamp( TS_MW );
  1089. $this->mGroupMemberships = [];
  1090. Hooks::run( 'UserLoadDefaults', [ $this, $name ] );
  1091. }
  1092. /**
  1093. * Return whether an item has been loaded.
  1094. *
  1095. * @param string $item Item to check. Current possibilities:
  1096. * - id
  1097. * - name
  1098. * - realname
  1099. * @param string $all 'all' to check if the whole object has been loaded
  1100. * or any other string to check if only the item is available (e.g.
  1101. * for optimisation)
  1102. * @return bool
  1103. */
  1104. public function isItemLoaded( $item, $all = 'all' ) {
  1105. return ( $this->mLoadedItems === true && $all === 'all' ) ||
  1106. ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
  1107. }
  1108. /**
  1109. * Set that an item has been loaded
  1110. *
  1111. * @param string $item
  1112. */
  1113. protected function setItemLoaded( $item ) {
  1114. if ( is_array( $this->mLoadedItems ) ) {
  1115. $this->mLoadedItems[$item] = true;
  1116. }
  1117. }
  1118. /**
  1119. * Load user data from the session.
  1120. *
  1121. * @return bool True if the user is logged in, false otherwise.
  1122. */
  1123. private function loadFromSession() {
  1124. // MediaWiki\Session\Session already did the necessary authentication of the user
  1125. // returned here, so just use it if applicable.
  1126. $session = $this->getRequest()->getSession();
  1127. $user = $session->getUser();
  1128. if ( $user->isLoggedIn() ) {
  1129. $this->loadFromUserObject( $user );
  1130. // If this user is autoblocked, set a cookie to track the block. This has to be done on
  1131. // every session load, because an autoblocked editor might not edit again from the same
  1132. // IP address after being blocked.
  1133. MediaWikiServices::getInstance()->getBlockManager()->trackBlockWithCookie( $this );
  1134. // Other code expects these to be set in the session, so set them.
  1135. $session->set( 'wsUserID', $this->getId() );
  1136. $session->set( 'wsUserName', $this->getName() );
  1137. $session->set( 'wsToken', $this->getToken() );
  1138. return true;
  1139. }
  1140. return false;
  1141. }
  1142. /**
  1143. * Set the 'BlockID' cookie depending on block type and user authentication status.
  1144. *
  1145. * @deprecated since 1.34 Use BlockManager::trackBlockWithCookie instead
  1146. */
  1147. public function trackBlockWithCookie() {
  1148. MediaWikiServices::getInstance()->getBlockManager()->trackBlockWithCookie( $this );
  1149. }
  1150. /**
  1151. * Load user and user_group data from the database.
  1152. * $this->mId must be set, this is how the user is identified.
  1153. *
  1154. * @param int $flags User::READ_* constant bitfield
  1155. * @return bool True if the user exists, false if the user is anonymous
  1156. */
  1157. public function loadFromDatabase( $flags = self::READ_LATEST ) {
  1158. // Paranoia
  1159. $this->mId = intval( $this->mId );
  1160. if ( !$this->mId ) {
  1161. // Anonymous users are not in the database
  1162. $this->loadDefaults();
  1163. return false;
  1164. }
  1165. list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
  1166. $db = wfGetDB( $index );
  1167. $userQuery = self::getQueryInfo();
  1168. $s = $db->selectRow(
  1169. $userQuery['tables'],
  1170. $userQuery['fields'],
  1171. [ 'user_id' => $this->mId ],
  1172. __METHOD__,
  1173. $options,
  1174. $userQuery['joins']
  1175. );
  1176. $this->queryFlagsUsed = $flags;
  1177. Hooks::run( 'UserLoadFromDatabase', [ $this, &$s ] );
  1178. if ( $s !== false ) {
  1179. // Initialise user table data
  1180. $this->loadFromRow( $s );
  1181. $this->mGroupMemberships = null; // deferred
  1182. $this->getEditCount(); // revalidation for nulls
  1183. return true;
  1184. }
  1185. // Invalid user_id
  1186. $this->mId = 0;
  1187. $this->loadDefaults();
  1188. return false;
  1189. }
  1190. /**
  1191. * Initialize this object from a row from the user table.
  1192. *
  1193. * @param stdClass $row Row from the user table to load.
  1194. * @param array|null $data Further user data to load into the object
  1195. *
  1196. * user_groups Array of arrays or stdClass result rows out of the user_groups
  1197. * table. Previously you were supposed to pass an array of strings
  1198. * here, but we also need expiry info nowadays, so an array of
  1199. * strings is ignored.
  1200. * user_properties Array with properties out of the user_properties table
  1201. */
  1202. protected function loadFromRow( $row, $data = null ) {
  1203. if ( !is_object( $row ) ) {
  1204. throw new InvalidArgumentException( '$row must be an object' );
  1205. }
  1206. $all = true;
  1207. $this->mGroupMemberships = null; // deferred
  1208. if ( isset( $row->actor_id ) ) {
  1209. $this->mActorId = (int)$row->actor_id;
  1210. if ( $this->mActorId !== 0 ) {
  1211. $this->mFrom = 'actor';
  1212. }
  1213. $this->setItemLoaded( 'actor' );
  1214. } else {
  1215. $all = false;
  1216. }
  1217. if ( isset( $row->user_name ) && $row->user_name !== '' ) {
  1218. $this->mName = $row->user_name;
  1219. $this->mFrom = 'name';
  1220. $this->setItemLoaded( 'name' );
  1221. } else {
  1222. $all = false;
  1223. }
  1224. if ( isset( $row->user_real_name ) ) {
  1225. $this->mRealName = $row->user_real_name;
  1226. $this->setItemLoaded( 'realname' );
  1227. } else {
  1228. $all = false;
  1229. }
  1230. if ( isset( $row->user_id ) ) {
  1231. $this->mId = intval( $row->user_id );
  1232. if ( $this->mId !== 0 ) {
  1233. $this->mFrom = 'id';
  1234. }
  1235. $this->setItemLoaded( 'id' );
  1236. } else {
  1237. $all = false;
  1238. }
  1239. if ( isset( $row->user_id ) && isset( $row->user_name ) && $row->user_name !== '' ) {
  1240. self::$idCacheByName[$row->user_name] = $row->user_id;
  1241. }
  1242. if ( isset( $row->user_editcount ) ) {
  1243. $this->mEditCount = $row->user_editcount;
  1244. } else {
  1245. $all = false;
  1246. }
  1247. if ( isset( $row->user_touched ) ) {
  1248. $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
  1249. } else {
  1250. $all = false;
  1251. }
  1252. if ( isset( $row->user_token ) ) {
  1253. // The definition for the column is binary(32), so trim the NULs
  1254. // that appends. The previous definition was char(32), so trim
  1255. // spaces too.
  1256. $this->mToken = rtrim( $row->user_token, " \0" );
  1257. if ( $this->mToken === '' ) {
  1258. $this->mToken = null;
  1259. }
  1260. } else {
  1261. $all = false;
  1262. }
  1263. if ( isset( $row->user_email ) ) {
  1264. $this->mEmail = $row->user_email;
  1265. $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
  1266. $this->mEmailToken = $row->user_email_token;
  1267. $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
  1268. $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
  1269. } else {
  1270. $all = false;
  1271. }
  1272. if ( $all ) {
  1273. $this->mLoadedItems = true;
  1274. }
  1275. if ( is_array( $data ) ) {
  1276. if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
  1277. if ( $data['user_groups'] === [] ) {
  1278. $this->mGroupMemberships = [];
  1279. } else {
  1280. $firstGroup = reset( $data['user_groups'] );
  1281. if ( is_array( $firstGroup ) || is_object( $firstGroup ) ) {
  1282. $this->mGroupMemberships = [];
  1283. foreach ( $data['user_groups'] as $row ) {
  1284. $ugm = UserGroupMembership::newFromRow( (object)$row );
  1285. $this->mGroupMemberships[$ugm->getGroup()] = $ugm;
  1286. }
  1287. }
  1288. }
  1289. }
  1290. if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
  1291. $this->loadOptions( $data['user_properties'] );
  1292. }
  1293. }
  1294. }
  1295. /**
  1296. * Load the data for this user object from another user object.
  1297. *
  1298. * @param User $user
  1299. */
  1300. protected function loadFromUserObject( $user ) {
  1301. $user->load();
  1302. foreach ( self::$mCacheVars as $var ) {
  1303. $this->$var = $user->$var;
  1304. }
  1305. }
  1306. /**
  1307. * Load the groups from the database if they aren't already loaded.
  1308. */
  1309. private function loadGroups() {
  1310. if ( is_null( $this->mGroupMemberships ) ) {
  1311. $db = ( $this->queryFlagsUsed & self::READ_LATEST )
  1312. ? wfGetDB( DB_MASTER )
  1313. : wfGetDB( DB_REPLICA );
  1314. $this->mGroupMemberships = UserGroupMembership::getMembershipsForUser(
  1315. $this->mId, $db );
  1316. }
  1317. }
  1318. /**
  1319. * Add the user to the group if he/she meets given criteria.
  1320. *
  1321. * Contrary to autopromotion by \ref $wgAutopromote, the group will be
  1322. * possible to remove manually via Special:UserRights. In such case it
  1323. * will not be re-added automatically. The user will also not lose the
  1324. * group if they no longer meet the criteria.
  1325. *
  1326. * @param string $event Key in $wgAutopromoteOnce (each one has groups/criteria)
  1327. *
  1328. * @return array Array of groups the user has been promoted to.
  1329. *
  1330. * @see $wgAutopromoteOnce
  1331. */
  1332. public function addAutopromoteOnceGroups( $event ) {
  1333. global $wgAutopromoteOnceLogInRC;
  1334. if ( wfReadOnly() || !$this->getId() ) {
  1335. return [];
  1336. }
  1337. $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
  1338. if ( $toPromote === [] ) {
  1339. return [];
  1340. }
  1341. if ( !$this->checkAndSetTouched() ) {
  1342. return []; // raced out (bug T48834)
  1343. }
  1344. $oldGroups = $this->getGroups(); // previous groups
  1345. $oldUGMs = $this->getGroupMemberships();
  1346. foreach ( $toPromote as $group ) {
  1347. $this->addGroup( $group );
  1348. }
  1349. $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
  1350. $newUGMs = $this->getGroupMemberships();
  1351. // update groups in external authentication database
  1352. Hooks::run( 'UserGroupsChanged', [ $this, $toPromote, [], false, false, $oldUGMs, $newUGMs ] );
  1353. $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
  1354. $logEntry->setPerformer( $this );
  1355. $logEntry->setTarget( $this->getUserPage() );
  1356. $logEntry->setParameters( [
  1357. '4::oldgroups' => $oldGroups,
  1358. '5::newgroups' => $newGroups,
  1359. ] );
  1360. $logid = $logEntry->insert();
  1361. if ( $wgAutopromoteOnceLogInRC ) {
  1362. $logEntry->publish( $logid );
  1363. }
  1364. return $toPromote;
  1365. }
  1366. /**
  1367. * Builds update conditions. Additional conditions may be added to $conditions to
  1368. * protected against race conditions using a compare-and-set (CAS) mechanism
  1369. * based on comparing $this->mTouched with the user_touched field.
  1370. *
  1371. * @param IDatabase $db
  1372. * @param array $conditions WHERE conditions for use with Database::update
  1373. * @return array WHERE conditions for use with Database::update
  1374. */
  1375. protected function makeUpdateConditions( IDatabase $db, array $conditions ) {
  1376. if ( $this->mTouched ) {
  1377. // CAS check: only update if the row wasn't changed sicne it was loaded.
  1378. $conditions['user_touched'] = $db->timestamp( $this->mTouched );
  1379. }
  1380. return $conditions;
  1381. }
  1382. /**
  1383. * Bump user_touched if it didn't change since this object was loaded
  1384. *
  1385. * On success, the mTouched field is updated.
  1386. * The user serialization cache is always cleared.
  1387. *
  1388. * @return bool Whether user_touched was actually updated
  1389. * @since 1.26
  1390. */
  1391. protected function checkAndSetTouched() {
  1392. $this->load();
  1393. if ( !$this->mId ) {
  1394. return false; // anon
  1395. }
  1396. // Get a new user_touched that is higher than the old one
  1397. $newTouched = $this->newTouchedTimestamp();
  1398. $dbw = wfGetDB( DB_MASTER );
  1399. $dbw->update( 'user',
  1400. [ 'user_touched' => $dbw->timestamp( $newTouched ) ],
  1401. $this->makeUpdateConditions( $dbw, [
  1402. 'user_id' => $this->mId,
  1403. ] ),
  1404. __METHOD__
  1405. );
  1406. $success = ( $dbw->affectedRows() > 0 );
  1407. if ( $success ) {
  1408. $this->mTouched = $newTouched;
  1409. $this->clearSharedCache( 'changed' );
  1410. } else {
  1411. // Clears on failure too since that is desired if the cache is stale
  1412. $this->clearSharedCache( 'refresh' );
  1413. }
  1414. return $success;
  1415. }
  1416. /**
  1417. * Clear various cached data stored in this object. The cache of the user table
  1418. * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
  1419. *
  1420. * @param bool|string $reloadFrom Reload user and user_groups table data from a
  1421. * given source. May be "name", "id", "actor", "defaults", "session", or false for no reload.
  1422. */
  1423. public function clearInstanceCache( $reloadFrom = false ) {
  1424. global $wgFullyInitialised;
  1425. $this->mNewtalk = -1;
  1426. $this->mDatePreference = null;
  1427. $this->mBlockedby = -1; # Unset
  1428. $this->mHash = false;
  1429. $this->mEffectiveGroups = null;
  1430. $this->mImplicitGroups = null;
  1431. $this->mGroupMemberships = null;
  1432. $this->mOptions = null;
  1433. $this->mOptionsLoaded = false;
  1434. $this->mEditCount = null;
  1435. // Replacement of former `$this->mRights = null` line
  1436. if ( $wgFullyInitialised && $this->mFrom ) {
  1437. MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache(
  1438. $this
  1439. );
  1440. }
  1441. if ( $reloadFrom ) {
  1442. $this->mLoadedItems = [];
  1443. $this->mFrom = $reloadFrom;
  1444. }
  1445. }
  1446. /** @var array|null */
  1447. private static $defOpt = null;
  1448. /** @var string|null */
  1449. private static $defOptLang = null;
  1450. /**
  1451. * Reset the process cache of default user options. This is only necessary
  1452. * if the wiki configuration has changed since defaults were calculated,
  1453. * and as such should only be performed inside the testing suite that
  1454. * regularly changes wiki configuration.
  1455. */
  1456. public static function resetGetDefaultOptionsForTestsOnly() {
  1457. Assert::invariant( defined( 'MW_PHPUNIT_TEST' ), 'Unit tests only' );
  1458. self::$defOpt = null;
  1459. self::$defOptLang = null;
  1460. }
  1461. /**
  1462. * Combine the language default options with any site-specific options
  1463. * and add the default language variants.
  1464. *
  1465. * @return array Array of String options
  1466. */
  1467. public static function getDefaultOptions() {
  1468. global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgDefaultSkin;
  1469. $contLang = MediaWikiServices::getInstance()->getContentLanguage();
  1470. if ( self::$defOpt !== null && self::$defOptLang === $contLang->getCode() ) {
  1471. // The content language does not change (and should not change) mid-request, but the
  1472. // unit tests change it anyway, and expect this method to return values relevant to the
  1473. // current content language.
  1474. return self::$defOpt;
  1475. }
  1476. self::$defOpt = $wgDefaultUserOptions;
  1477. // Default language setting
  1478. self::$defOptLang = $contLang->getCode();
  1479. self::$defOpt['language'] = self::$defOptLang;
  1480. foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
  1481. if ( $langCode === $contLang->getCode() ) {
  1482. self::$defOpt['variant'] = $langCode;
  1483. } else {
  1484. self::$defOpt["variant-$langCode"] = $langCode;
  1485. }
  1486. }
  1487. // NOTE: don't use SearchEngineConfig::getSearchableNamespaces here,
  1488. // since extensions may change the set of searchable namespaces depending
  1489. // on user groups/permissions.
  1490. foreach ( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
  1491. self::$defOpt['searchNs' . $nsnum] = (bool)$val;
  1492. }
  1493. self::$defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
  1494. Hooks::run( 'UserGetDefaultOptions', [ &self::$defOpt ] );
  1495. return self::$defOpt;
  1496. }
  1497. /**
  1498. * Get a given default option value.
  1499. *
  1500. * @param string $opt Name of option to retrieve
  1501. * @return string Default option value
  1502. */
  1503. public static function getDefaultOption( $opt ) {
  1504. $defOpts = self::getDefaultOptions();
  1505. return $defOpts[$opt] ?? null;
  1506. }
  1507. /**
  1508. * Get blocking information
  1509. *
  1510. * TODO: Move this into the BlockManager, along with block-related properties.
  1511. *
  1512. * @param bool $fromReplica Whether to check the replica DB first.
  1513. * To improve performance, non-critical checks are done against replica DBs.
  1514. * Check when actually saving should be done against master.
  1515. */
  1516. private function getBlockedStatus( $fromReplica = true ) {
  1517. if ( $this->mBlockedby != -1 ) {
  1518. return;
  1519. }
  1520. wfDebug( __METHOD__ . ": checking...\n" );
  1521. // Initialize data...
  1522. // Otherwise something ends up stomping on $this->mBlockedby when
  1523. // things get lazy-loaded later, causing false positive block hits
  1524. // due to -1 !== 0. Probably session-related... Nothing should be
  1525. // overwriting mBlockedby, surely?
  1526. $this->load();
  1527. // TODO: Block checking shouldn't really be done from the User object. Block
  1528. // checking can involve checking for IP blocks, cookie blocks, and/or XFF blocks,
  1529. // which need more knowledge of the request context than the User should have.
  1530. // Since we do currently check blocks from the User, we have to do the following
  1531. // here:
  1532. // - Check if this is the user associated with the main request
  1533. // - If so, pass the relevant request information to the block manager
  1534. $request = null;
  1535. // The session user is set up towards the end of Setup.php. Until then,
  1536. // assume it's a logged-out user.
  1537. $sessionUser = RequestContext::getMain()->getUser();
  1538. $globalUserName = $sessionUser->isSafeToLoad()
  1539. ? $sessionUser->getName()
  1540. : IP::sanitizeIP( $sessionUser->getRequest()->getIP() );
  1541. if ( $this->getName() === $globalUserName ) {
  1542. // This is the global user, so we need to pass the request
  1543. $request = $this->getRequest();
  1544. }
  1545. // @phan-suppress-next-line PhanAccessMethodInternal It's the only allowed use
  1546. $block = MediaWikiServices::getInstance()->getBlockManager()->getUserBlock(
  1547. $this,
  1548. $request,
  1549. $fromReplica
  1550. );
  1551. if ( $block ) {
  1552. $this->mBlock = $block;
  1553. $this->mBlockedby = $block->getByName();
  1554. $this->mBlockreason = $block->getReason();
  1555. $this->mHideName = $block->getHideName();
  1556. $this->mAllowUsertalk = $block->isUsertalkEditAllowed();
  1557. } else {
  1558. $this->mBlock = null;
  1559. $this->mBlockedby = '';
  1560. $this->mBlockreason = '';
  1561. $this->mHideName = 0;
  1562. $this->mAllowUsertalk = false;
  1563. }
  1564. // Avoid PHP 7.1 warning of passing $this by reference
  1565. $thisUser = $this;
  1566. // Extensions
  1567. Hooks::run( 'GetBlockedStatus', [ &$thisUser ], '1.34' );
  1568. }
  1569. /**
  1570. * Whether the given IP is in a DNS blacklist.
  1571. *
  1572. * @deprecated since 1.34 Use BlockManager::isDnsBlacklisted.
  1573. * @param string $ip IP to check
  1574. * @param bool $checkWhitelist Whether to check the whitelist first
  1575. * @return bool True if blacklisted.
  1576. */
  1577. public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
  1578. return MediaWikiServices::getInstance()->getBlockManager()
  1579. ->isDnsBlacklisted( $ip, $checkWhitelist );
  1580. }
  1581. /**
  1582. * Whether the given IP is in a given DNS blacklist.
  1583. *
  1584. * @deprecated since 1.34 Check via BlockManager::isDnsBlacklisted instead.
  1585. * @param string $ip IP to check
  1586. * @param string|array $bases Array of Strings: URL of the DNS blacklist
  1587. * @return bool True if blacklisted.
  1588. */
  1589. public function inDnsBlacklist( $ip, $bases ) {
  1590. wfDeprecated( __METHOD__, '1.34' );
  1591. $found = false;
  1592. // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
  1593. if ( IP::isIPv4( $ip ) ) {
  1594. // Reverse IP, T23255
  1595. $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
  1596. foreach ( (array)$bases as $base ) {
  1597. // Make hostname
  1598. // If we have an access key, use that too (ProjectHoneypot, etc.)
  1599. $basename = $base;
  1600. if ( is_array( $base ) ) {
  1601. if ( count( $base ) >= 2 ) {
  1602. // Access key is 1, base URL is 0
  1603. $host = "{$base[1]}.$ipReversed.{$base[0]}";
  1604. } else {
  1605. $host = "$ipReversed.{$base[0]}";
  1606. }
  1607. $basename = $base[0];
  1608. } else {
  1609. $host = "$ipReversed.$base";
  1610. }
  1611. // Send query
  1612. $ipList = gethostbynamel( $host );
  1613. if ( $ipList ) {
  1614. wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
  1615. $found = true;
  1616. break;
  1617. }
  1618. wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
  1619. }
  1620. }
  1621. return $found;
  1622. }
  1623. /**
  1624. * Check if an IP address is in the local proxy list
  1625. *
  1626. * @deprecated since 1.34 Use BlockManager::getUserBlock instead.
  1627. * @param string $ip
  1628. * @return bool
  1629. */
  1630. public static function isLocallyBlockedProxy( $ip ) {
  1631. wfDeprecated( __METHOD__, '1.34' );
  1632. global $wgProxyList;
  1633. if ( !$wgProxyList ) {
  1634. return false;
  1635. }
  1636. if ( !is_array( $wgProxyList ) ) {
  1637. // Load values from the specified file
  1638. $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
  1639. }
  1640. $resultProxyList = [];
  1641. $deprecatedIPEntries = [];
  1642. // backward compatibility: move all ip addresses in keys to values
  1643. foreach ( $wgProxyList as $key => $value ) {
  1644. $keyIsIP = IP::isIPAddress( $key );
  1645. $valueIsIP = IP::isIPAddress( $value );
  1646. if ( $keyIsIP && !$valueIsIP ) {
  1647. $deprecatedIPEntries[] = $key;
  1648. $resultProxyList[] = $key;
  1649. } elseif ( $keyIsIP && $valueIsIP ) {
  1650. $deprecatedIPEntries[] = $key;
  1651. $resultProxyList[] = $key;
  1652. $resultProxyList[] = $value;
  1653. } else {
  1654. $resultProxyList[] = $value;
  1655. }
  1656. }
  1657. if ( $deprecatedIPEntries ) {
  1658. wfDeprecated(
  1659. 'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' .
  1660. implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' );
  1661. }
  1662. $proxyListIPSet = new IPSet( $resultProxyList );
  1663. return $proxyListIPSet->match( $ip );
  1664. }
  1665. /**
  1666. * Is this user subject to rate limiting?
  1667. *
  1668. * @return bool True if rate limited
  1669. */
  1670. public function isPingLimitable() {
  1671. global $wgRateLimitsExcludedIPs;
  1672. if ( IP::isInRanges( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
  1673. // No other good way currently to disable rate limits
  1674. // for specific IPs. :P
  1675. // But this is a crappy hack and should die.
  1676. return false;
  1677. }
  1678. return !$this->isAllowed( 'noratelimit' );
  1679. }
  1680. /**
  1681. * Primitive rate limits: enforce maximum actions per time period
  1682. * to put a brake on flooding.
  1683. *
  1684. * The method generates both a generic profiling point and a per action one
  1685. * (suffix being "-$action".
  1686. *
  1687. * @note When using a shared cache like memcached, IP-address
  1688. * last-hit counters will be shared across wikis.
  1689. *
  1690. * @param string $action Action to enforce; 'edit' if unspecified
  1691. * @param int $incrBy Positive amount to increment counter by [defaults to 1]
  1692. * @return bool True if a rate limiter was tripped
  1693. */
  1694. public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
  1695. // Avoid PHP 7.1 warning of passing $this by reference
  1696. $user = $this;
  1697. // Call the 'PingLimiter' hook
  1698. $result = false;
  1699. if ( !Hooks::run( 'PingLimiter', [ &$user, $action, &$result, $incrBy ] ) ) {
  1700. return $result;
  1701. }
  1702. global $wgRateLimits;
  1703. if ( !isset( $wgRateLimits[$action] ) ) {
  1704. return false;
  1705. }
  1706. $limits = array_merge(
  1707. [ '&can-bypass' => true ],
  1708. $wgRateLimits[$action]
  1709. );
  1710. // Some groups shouldn't trigger the ping limiter, ever
  1711. if ( $limits['&can-bypass'] && !$this->isPingLimitable() ) {
  1712. return false;
  1713. }
  1714. $keys = [];
  1715. $id = $this->getId();
  1716. $userLimit = false;
  1717. $isNewbie = $this->isNewbie();
  1718. $cache = ObjectCache::getLocalClusterInstance();
  1719. if ( $id == 0 ) {
  1720. // limits for anons
  1721. if ( isset( $limits['anon'] ) ) {
  1722. $keys[$cache->makeKey( 'limiter', $action, 'anon' )] = $limits['anon'];
  1723. }
  1724. } elseif ( isset( $limits['user'] ) ) {
  1725. // limits for logged-in users
  1726. $userLimit = $limits['user'];
  1727. }
  1728. // limits for anons and for newbie logged-in users
  1729. if ( $isNewbie ) {
  1730. // ip-based limits
  1731. if ( isset( $limits['ip'] ) ) {
  1732. $ip = $this->getRequest()->getIP();
  1733. $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
  1734. }
  1735. // subnet-based limits
  1736. if ( isset( $limits['subnet'] ) ) {
  1737. $ip = $this->getRequest()->getIP();
  1738. $subnet = IP::getSubnet( $ip );
  1739. if ( $subnet !== false ) {
  1740. $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
  1741. }
  1742. }
  1743. }
  1744. // Check for group-specific permissions
  1745. // If more than one group applies, use the group with the highest limit ratio (max/period)
  1746. foreach ( $this->getGroups() as $group ) {
  1747. if ( isset( $limits[$group] ) ) {
  1748. if ( $userLimit === false
  1749. || $limits[$group][0] / $limits[$group][1] > $userLimit[0] / $userLimit[1]
  1750. ) {
  1751. $userLimit = $limits[$group];
  1752. }
  1753. }
  1754. }
  1755. // limits for newbie logged-in users (override all the normal user limits)
  1756. if ( $id !== 0 && $isNewbie && isset( $limits['newbie'] ) ) {
  1757. $userLimit = $limits['newbie'];
  1758. }
  1759. // Set the user limit key
  1760. if ( $userLimit !== false ) {
  1761. // phan is confused because &can-bypass's value is a bool, so it assumes
  1762. // that $userLimit is also a bool here.
  1763. // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
  1764. list( $max, $period ) = $userLimit;
  1765. wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" );
  1766. $keys[$cache->makeKey( 'limiter', $action, 'user', $id )] = $userLimit;
  1767. }
  1768. // ip-based limits for all ping-limitable users
  1769. if ( isset( $limits['ip-all'] ) ) {
  1770. $ip = $this->getRequest()->getIP();
  1771. // ignore if user limit is more permissive
  1772. if ( $isNewbie || $userLimit === false
  1773. || $limits['ip-all'][0] / $limits['ip-all'][1] > $userLimit[0] / $userLimit[1] ) {
  1774. $keys["mediawiki:limiter:$action:ip-all:$ip"] = $limits['ip-all'];
  1775. }
  1776. }
  1777. // subnet-based limits for all ping-limitable users
  1778. if ( isset( $limits['subnet-all'] ) ) {
  1779. $ip = $this->getRequest()->getIP();
  1780. $subnet = IP::getSubnet( $ip );
  1781. if ( $subnet !== false ) {
  1782. // ignore if user limit is more permissive
  1783. if ( $isNewbie || $userLimit === false
  1784. || $limits['ip-all'][0] / $limits['ip-all'][1]
  1785. > $userLimit[0] / $userLimit[1] ) {
  1786. $keys["mediawiki:limiter:$action:subnet-all:$subnet"] = $limits['subnet-all'];
  1787. }
  1788. }
  1789. }
  1790. $triggered = false;
  1791. foreach ( $keys as $key => $limit ) {
  1792. // phan is confused because &can-bypass's value is a bool, so it assumes
  1793. // that $userLimit is also a bool here.
  1794. // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
  1795. list( $max, $period ) = $limit;
  1796. $summary = "(limit $max in {$period}s)";
  1797. $count = $cache->get( $key );
  1798. // Already pinged?
  1799. if ( $count && $count >= $max ) {
  1800. wfDebugLog( 'ratelimit', "User '{$this->getName()}' " .
  1801. "(IP {$this->getRequest()->getIP()}) tripped $key at $count $summary" );
  1802. $triggered = true;
  1803. } else {
  1804. wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
  1805. if ( $incrBy > 0 ) {
  1806. $cache->add( $key, 0, intval( $period ) ); // first ping
  1807. }
  1808. }
  1809. if ( $incrBy > 0 ) {
  1810. $cache->incrWithInit( $key, (int)$period, $incrBy, $incrBy );
  1811. }
  1812. }
  1813. return $triggered;
  1814. }
  1815. /**
  1816. * Check if user is blocked
  1817. *
  1818. * @deprecated since 1.34, use User::getBlock() or
  1819. * PermissionManager::isBlockedFrom() or
  1820. * PermissionManager::userCan() instead.
  1821. *
  1822. * @param bool $fromReplica Whether to check the replica DB instead of
  1823. * the master. Hacked from false due to horrible probs on site.
  1824. * @return bool True if blocked, false otherwise
  1825. */
  1826. public function isBlocked( $fromReplica = true ) {
  1827. return $this->getBlock( $fromReplica ) instanceof AbstractBlock &&
  1828. $this->getBlock()->appliesToRight( 'edit' );
  1829. }
  1830. /**
  1831. * Get the block affecting the user, or null if the user is not blocked
  1832. *
  1833. * @param bool $fromReplica Whether to check the replica DB instead of the master
  1834. * @return AbstractBlock|null
  1835. */
  1836. public function getBlock( $fromReplica = true ) {
  1837. $this->getBlockedStatus( $fromReplica );
  1838. return $this->mBlock instanceof AbstractBlock ? $this->mBlock : null;
  1839. }
  1840. /**
  1841. * Check if user is blocked from editing a particular article
  1842. *
  1843. * @param Title $title Title to check
  1844. * @param bool $fromReplica Whether to check the replica DB instead of the master
  1845. * @return bool
  1846. *
  1847. * @deprecated since 1.33,
  1848. * use MediaWikiServices::getInstance()->getPermissionManager()->isBlockedFrom(..)
  1849. *
  1850. */
  1851. public function isBlockedFrom( $title, $fromReplica = false ) {
  1852. return MediaWikiServices::getInstance()->getPermissionManager()
  1853. ->isBlockedFrom( $this, $title, $fromReplica );
  1854. }
  1855. /**
  1856. * If user is blocked, return the name of the user who placed the block
  1857. * @return string Name of blocker
  1858. */
  1859. public function blockedBy() {
  1860. $this->getBlockedStatus();
  1861. return $this->mBlockedby;
  1862. }
  1863. /**
  1864. * If user is blocked, return the specified reason for the block
  1865. * @return string Blocking reason
  1866. */
  1867. public function blockedFor() {
  1868. $this->getBlockedStatus();
  1869. return $this->mBlockreason;
  1870. }
  1871. /**
  1872. * If user is blocked, return the ID for the block
  1873. * @return int Block ID
  1874. */
  1875. public function getBlockId() {
  1876. $this->getBlockedStatus();
  1877. return ( $this->mBlock ? $this->mBlock->getId() : false );
  1878. }
  1879. /**
  1880. * Check if user is blocked on all wikis.
  1881. * Do not use for actual edit permission checks!
  1882. * This is intended for quick UI checks.
  1883. *
  1884. * @param string $ip IP address, uses current client if none given
  1885. * @return bool True if blocked, false otherwise
  1886. */
  1887. public function isBlockedGlobally( $ip = '' ) {
  1888. return $this->getGlobalBlock( $ip ) instanceof AbstractBlock;
  1889. }
  1890. /**
  1891. * Check if user is blocked on all wikis.
  1892. * Do not use for actual edit permission checks!
  1893. * This is intended for quick UI checks.
  1894. *
  1895. * @param string $ip IP address, uses current client if none given
  1896. * @return AbstractBlock|null Block object if blocked, null otherwise
  1897. * @throws FatalError
  1898. * @throws MWException
  1899. */
  1900. public function getGlobalBlock( $ip = '' ) {
  1901. if ( $this->mGlobalBlock !== null ) {
  1902. return $this->mGlobalBlock ?: null;
  1903. }
  1904. // User is already an IP?
  1905. if ( IP::isIPAddress( $this->getName() ) ) {
  1906. $ip = $this->getName();
  1907. } elseif ( !$ip ) {
  1908. $ip = $this->getRequest()->getIP();
  1909. }
  1910. // Avoid PHP 7.1 warning of passing $this by reference
  1911. $user = $this;
  1912. $blocked = false;
  1913. $block = null;
  1914. Hooks::run( 'UserIsBlockedGlobally', [ &$user, $ip, &$blocked, &$block ] );
  1915. if ( $blocked && $block === null ) {
  1916. // back-compat: UserIsBlockedGlobally didn't have $block param first
  1917. $block = new SystemBlock( [
  1918. 'address' => $ip,
  1919. 'systemBlock' => 'global-block'
  1920. ] );
  1921. }
  1922. $this->mGlobalBlock = $blocked ? $block : false;
  1923. return $this->mGlobalBlock ?: null;
  1924. }
  1925. /**
  1926. * Check if user account is locked
  1927. *
  1928. * @return bool True if locked, false otherwise
  1929. */
  1930. public function isLocked() {
  1931. if ( $this->mLocked !== null ) {
  1932. return $this->mLocked;
  1933. }
  1934. // Reset for hook
  1935. $this->mLocked = false;
  1936. Hooks::run( 'UserIsLocked', [ $this, &$this->mLocked ] );
  1937. return $this->mLocked;
  1938. }
  1939. /**
  1940. * Check if user account is hidden
  1941. *
  1942. * @return bool True if hidden, false otherwise
  1943. */
  1944. public function isHidden() {
  1945. if ( $this->mHideName !== null ) {
  1946. return (bool)$this->mHideName;
  1947. }
  1948. $this->getBlockedStatus();
  1949. if ( !$this->mHideName ) {
  1950. // Reset for hook
  1951. $this->mHideName = false;
  1952. Hooks::run( 'UserIsHidden', [ $this, &$this->mHideName ], '1.34' );
  1953. }
  1954. return (bool)$this->mHideName;
  1955. }
  1956. /**
  1957. * Get the user's ID.
  1958. * @return int The user's ID; 0 if the user is anonymous or nonexistent
  1959. */
  1960. public function getId() {
  1961. if ( $this->mId === null && $this->mName !== null &&
  1962. ( self::isIP( $this->mName ) || ExternalUserNames::isExternal( $this->mName ) )
  1963. ) {
  1964. // Special case, we know the user is anonymous
  1965. return 0;
  1966. }
  1967. if ( !$this->isItemLoaded( 'id' ) ) {
  1968. // Don't load if this was initialized from an ID
  1969. $this->load();
  1970. }
  1971. return (int)$this->mId;
  1972. }
  1973. /**
  1974. * Set the user and reload all fields according to a given ID
  1975. * @param int $v User ID to reload
  1976. */
  1977. public function setId( $v ) {
  1978. $this->mId = $v;
  1979. $this->clearInstanceCache( 'id' );
  1980. }
  1981. /**
  1982. * Get the user name, or the IP of an anonymous user
  1983. * @return string User's name or IP address
  1984. */
  1985. public function getName() {
  1986. if ( $this->isItemLoaded( 'name', 'only' ) ) {
  1987. // Special case optimisation
  1988. return $this->mName;
  1989. }
  1990. $this->load();
  1991. if ( $this->mName === false ) {
  1992. // Clean up IPs
  1993. $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
  1994. }
  1995. return $this->mName;
  1996. }
  1997. /**
  1998. * Set the user name.
  1999. *
  2000. * This does not reload fields from the database according to the given
  2001. * name. Rather, it is used to create a temporary "nonexistent user" for
  2002. * later addition to the database. It can also be used to set the IP
  2003. * address for an anonymous user to something other than the current
  2004. * remote IP.
  2005. *
  2006. * @note User::newFromName() has roughly the same function, when the named user
  2007. * does not exist.
  2008. * @param string $str New user name to set
  2009. */
  2010. public function setName( $str ) {
  2011. $this->load();
  2012. $this->mName = $str;
  2013. }
  2014. /**
  2015. * Get the user's actor ID.
  2016. * @since 1.31
  2017. * @param IDatabase|null $dbw Assign a new actor ID, using this DB handle, if none exists
  2018. * @return int The actor's ID, or 0 if no actor ID exists and $dbw was null
  2019. */
  2020. public function getActorId( IDatabase $dbw = null ) {
  2021. if ( !$this->isItemLoaded( 'actor' ) ) {
  2022. $this->load();
  2023. }
  2024. if ( !$this->mActorId && $dbw ) {
  2025. $q = [
  2026. 'actor_user' => $this->getId() ?: null,
  2027. 'actor_name' => (string)$this->getName(),
  2028. ];
  2029. if ( $q['actor_user'] === null && self::isUsableName( $q['actor_name'] ) ) {
  2030. throw new CannotCreateActorException(
  2031. 'Cannot create an actor for a usable name that is not an existing user'
  2032. );
  2033. }
  2034. if ( $q['actor_name'] === '' ) {
  2035. throw new CannotCreateActorException( 'Cannot create an actor for a user with no name' );
  2036. }
  2037. $dbw->insert( 'actor', $q, __METHOD__, [ 'IGNORE' ] );
  2038. if ( $dbw->affectedRows() ) {
  2039. $this->mActorId = (int)$dbw->insertId();
  2040. } else {
  2041. // Outdated cache?
  2042. // Use LOCK IN SHARE MODE to bypass any MySQL REPEATABLE-READ snapshot.
  2043. $this->mActorId = (int)$dbw->selectField(
  2044. 'actor',
  2045. 'actor_id',
  2046. $q,
  2047. __METHOD__,
  2048. [ 'LOCK IN SHARE MODE' ]
  2049. );
  2050. if ( !$this->mActorId ) {
  2051. throw new CannotCreateActorException(
  2052. "Cannot create actor ID for user_id={$this->getId()} user_name={$this->getName()}"
  2053. );
  2054. }
  2055. }
  2056. $this->invalidateCache();
  2057. $this->setItemLoaded( 'actor' );
  2058. }
  2059. return (int)$this->mActorId;
  2060. }
  2061. /**
  2062. * Get the user's name escaped by underscores.
  2063. * @return string Username escaped by underscores.
  2064. */
  2065. public function getTitleKey() {
  2066. return str_replace( ' ', '_', $this->getName() );
  2067. }
  2068. /**
  2069. * Check if the user has new messages.
  2070. * @return bool True if the user has new messages
  2071. */
  2072. public function getNewtalk() {
  2073. $this->load();
  2074. // Load the newtalk status if it is unloaded (mNewtalk=-1)
  2075. if ( $this->mNewtalk === -1 ) {
  2076. $this->mNewtalk = false; # reset talk page status
  2077. // Check memcached separately for anons, who have no
  2078. // entire User object stored in there.
  2079. if ( !$this->mId ) {
  2080. global $wgDisableAnonTalk;
  2081. if ( $wgDisableAnonTalk ) {
  2082. // Anon newtalk disabled by configuration.
  2083. $this->mNewtalk = false;
  2084. } else {
  2085. $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
  2086. }
  2087. } else {
  2088. $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
  2089. }
  2090. }
  2091. return (bool)$this->mNewtalk;
  2092. }
  2093. /**
  2094. * Return the data needed to construct links for new talk page message
  2095. * alerts. If there are new messages, this will return an associative array
  2096. * with the following data:
  2097. * wiki: The database name of the wiki
  2098. * link: Root-relative link to the user's talk page
  2099. * rev: The last talk page revision that the user has seen or null. This
  2100. * is useful for building diff links.
  2101. * If there are no new messages, it returns an empty array.
  2102. * @note This function was designed to accomodate multiple talk pages, but
  2103. * currently only returns a single link and revision.
  2104. * @return array
  2105. */
  2106. public function getNewMessageLinks() {
  2107. // Avoid PHP 7.1 warning of passing $this by reference
  2108. $user = $this;
  2109. $talks = [];
  2110. if ( !Hooks::run( 'UserRetrieveNewTalks', [ &$user, &$talks ] ) ) {
  2111. return $talks;
  2112. }
  2113. if ( !$this->getNewtalk() ) {
  2114. return [];
  2115. }
  2116. $utp = $this->getTalkPage();
  2117. $dbr = wfGetDB( DB_REPLICA );
  2118. // Get the "last viewed rev" timestamp from the oldest message notification
  2119. $timestamp = $dbr->selectField( 'user_newtalk',
  2120. 'MIN(user_last_timestamp)',
  2121. $this->isAnon() ? [ 'user_ip' => $this->getName() ] : [ 'user_id' => $this->getId() ],
  2122. __METHOD__ );
  2123. $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
  2124. return [
  2125. [
  2126. 'wiki' => WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() ),
  2127. 'link' => $utp->getLocalURL(),
  2128. 'rev' => $rev
  2129. ]
  2130. ];
  2131. }
  2132. /**
  2133. * Get the revision ID for the last talk page revision viewed by the talk
  2134. * page owner.
  2135. * @return int|null Revision ID or null
  2136. */
  2137. public function getNewMessageRevisionId() {
  2138. $newMessageRevisionId = null;
  2139. $newMessageLinks = $this->getNewMessageLinks();
  2140. // Note: getNewMessageLinks() never returns more than a single link
  2141. // and it is always for the same wiki, but we double-check here in
  2142. // case that changes some time in the future.
  2143. if ( $newMessageLinks && count( $newMessageLinks ) === 1
  2144. && WikiMap::isCurrentWikiId( $newMessageLinks[0]['wiki'] )
  2145. && $newMessageLinks[0]['rev']
  2146. ) {
  2147. /** @var Revision $newMessageRevision */
  2148. $newMessageRevision = $newMessageLinks[0]['rev'];
  2149. $newMessageRevisionId = $newMessageRevision->getId();
  2150. }
  2151. return $newMessageRevisionId;
  2152. }
  2153. /**
  2154. * Internal uncached check for new messages
  2155. *
  2156. * @see getNewtalk()
  2157. * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
  2158. * @param string|int $id User's IP address for anonymous users, User ID otherwise
  2159. * @return bool True if the user has new messages
  2160. */
  2161. protected function checkNewtalk( $field, $id ) {
  2162. $dbr = wfGetDB( DB_REPLICA );
  2163. $ok = $dbr->selectField( 'user_newtalk', $field, [ $field => $id ], __METHOD__ );
  2164. return $ok !== false;
  2165. }
  2166. /**
  2167. * Add or update the new messages flag
  2168. * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
  2169. * @param string|int $id User's IP address for anonymous users, User ID otherwise
  2170. * @param Revision|null $curRev New, as yet unseen revision of the user talk page. Ignored if null.
  2171. * @return bool True if successful, false otherwise
  2172. */
  2173. protected function updateNewtalk( $field, $id, $curRev = null ) {
  2174. // Get timestamp of the talk page revision prior to the current one
  2175. $prevRev = $curRev ? $curRev->getPrevious() : false;
  2176. $ts = $prevRev ? $prevRev->getTimestamp() : null;
  2177. // Mark the user as having new messages since this revision
  2178. $dbw = wfGetDB( DB_MASTER );
  2179. $dbw->insert( 'user_newtalk',
  2180. [ $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ],
  2181. __METHOD__,
  2182. [ 'IGNORE' ] );
  2183. if ( $dbw->affectedRows() ) {
  2184. wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
  2185. return true;
  2186. }
  2187. wfDebug( __METHOD__ . " already set ($field, $id)\n" );
  2188. return false;
  2189. }
  2190. /**
  2191. * Clear the new messages flag for the given user
  2192. * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
  2193. * @param string|int $id User's IP address for anonymous users, User ID otherwise
  2194. * @return bool True if successful, false otherwise
  2195. */
  2196. protected function deleteNewtalk( $field, $id ) {
  2197. $dbw = wfGetDB( DB_MASTER );
  2198. $dbw->delete( 'user_newtalk',
  2199. [ $field => $id ],
  2200. __METHOD__ );
  2201. if ( $dbw->affectedRows() ) {
  2202. wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
  2203. return true;
  2204. }
  2205. wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
  2206. return false;
  2207. }
  2208. /**
  2209. * Update the 'You have new messages!' status.
  2210. * @param bool $val Whether the user has new messages
  2211. * @param Revision|null $curRev New, as yet unseen revision of the user talk
  2212. * page. Ignored if null or !$val.
  2213. */
  2214. public function setNewtalk( $val, $curRev = null ) {
  2215. if ( wfReadOnly() ) {
  2216. return;
  2217. }
  2218. $this->load();
  2219. $this->mNewtalk = $val;
  2220. if ( $this->isAnon() ) {
  2221. $field = 'user_ip';
  2222. $id = $this->getName();
  2223. } else {
  2224. $field = 'user_id';
  2225. $id = $this->getId();
  2226. }
  2227. if ( $val ) {
  2228. $changed = $this->updateNewtalk( $field, $id, $curRev );
  2229. } else {
  2230. $changed = $this->deleteNewtalk( $field, $id );
  2231. }
  2232. if ( $changed ) {
  2233. $this->invalidateCache();
  2234. }
  2235. }
  2236. /**
  2237. * Generate a current or new-future timestamp to be stored in the
  2238. * user_touched field when we update things.
  2239. *
  2240. * @return string Timestamp in TS_MW format
  2241. */
  2242. private function newTouchedTimestamp() {
  2243. $time = time();
  2244. if ( $this->mTouched ) {
  2245. $time = max( $time, wfTimestamp( TS_UNIX, $this->mTouched ) + 1 );
  2246. }
  2247. return wfTimestamp( TS_MW, $time );
  2248. }
  2249. /**
  2250. * Clear user data from memcached
  2251. *
  2252. * Use after applying updates to the database; caller's
  2253. * responsibility to update user_touched if appropriate.
  2254. *
  2255. * Called implicitly from invalidateCache() and saveSettings().
  2256. *
  2257. * @param string $mode Use 'refresh' to clear now or 'changed' to clear before DB commit
  2258. */
  2259. public function clearSharedCache( $mode = 'refresh' ) {
  2260. if ( !$this->getId() ) {
  2261. return;
  2262. }
  2263. $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
  2264. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  2265. $key = $this->getCacheKey( $cache );
  2266. if ( $mode === 'refresh' ) {
  2267. $cache->delete( $key, 1 ); // low tombstone/"hold-off" TTL
  2268. } else {
  2269. $lb->getConnectionRef( DB_MASTER )->onTransactionPreCommitOrIdle(
  2270. function () use ( $cache, $key ) {
  2271. $cache->delete( $key );
  2272. },
  2273. __METHOD__
  2274. );
  2275. }
  2276. }
  2277. /**
  2278. * Immediately touch the user data cache for this account
  2279. *
  2280. * Calls touch() and removes account data from memcached
  2281. */
  2282. public function invalidateCache() {
  2283. $this->touch();
  2284. $this->clearSharedCache( 'changed' );
  2285. }
  2286. /**
  2287. * Update the "touched" timestamp for the user
  2288. *
  2289. * This is useful on various login/logout events when making sure that
  2290. * a browser or proxy that has multiple tenants does not suffer cache
  2291. * pollution where the new user sees the old users content. The value
  2292. * of getTouched() is checked when determining 304 vs 200 responses.
  2293. * Unlike invalidateCache(), this preserves the User object cache and
  2294. * avoids database writes.
  2295. *
  2296. * @since 1.25
  2297. */
  2298. public function touch() {
  2299. $id = $this->getId();
  2300. if ( $id ) {
  2301. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  2302. $key = $cache->makeKey( 'user-quicktouched', 'id', $id );
  2303. $cache->touchCheckKey( $key );
  2304. $this->mQuickTouched = null;
  2305. }
  2306. }
  2307. /**
  2308. * Validate the cache for this account.
  2309. * @param string $timestamp A timestamp in TS_MW format
  2310. * @return bool
  2311. */
  2312. public function validateCache( $timestamp ) {
  2313. return ( $timestamp >= $this->getTouched() );
  2314. }
  2315. /**
  2316. * Get the user touched timestamp
  2317. *
  2318. * Use this value only to validate caches via inequalities
  2319. * such as in the case of HTTP If-Modified-Since response logic
  2320. *
  2321. * @return string TS_MW Timestamp
  2322. */
  2323. public function getTouched() {
  2324. $this->load();
  2325. if ( $this->mId ) {
  2326. if ( $this->mQuickTouched === null ) {
  2327. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  2328. $key = $cache->makeKey( 'user-quicktouched', 'id', $this->mId );
  2329. $this->mQuickTouched = wfTimestamp( TS_MW, $cache->getCheckKeyTime( $key ) );
  2330. }
  2331. return max( $this->mTouched, $this->mQuickTouched );
  2332. }
  2333. return $this->mTouched;
  2334. }
  2335. /**
  2336. * Get the user_touched timestamp field (time of last DB updates)
  2337. * @return string TS_MW Timestamp
  2338. * @since 1.26
  2339. */
  2340. public function getDBTouched() {
  2341. $this->load();
  2342. return $this->mTouched;
  2343. }
  2344. /**
  2345. * Set the password and reset the random token.
  2346. * Calls through to authentication plugin if necessary;
  2347. * will have no effect if the auth plugin refuses to
  2348. * pass the change through or if the legal password
  2349. * checks fail.
  2350. *
  2351. * As a special case, setting the password to null
  2352. * wipes it, so the account cannot be logged in until
  2353. * a new password is set, for instance via e-mail.
  2354. *
  2355. * @deprecated since 1.27, use AuthManager instead
  2356. * @param string $str New password to set
  2357. * @throws PasswordError On failure
  2358. * @return bool
  2359. */
  2360. public function setPassword( $str ) {
  2361. wfDeprecated( __METHOD__, '1.27' );
  2362. return $this->setPasswordInternal( $str );
  2363. }
  2364. /**
  2365. * Set the password and reset the random token unconditionally.
  2366. *
  2367. * @deprecated since 1.27, use AuthManager instead
  2368. * @param string|null $str New password to set or null to set an invalid
  2369. * password hash meaning that the user will not be able to log in
  2370. * through the web interface.
  2371. */
  2372. public function setInternalPassword( $str ) {
  2373. wfDeprecated( __METHOD__, '1.27' );
  2374. $this->setPasswordInternal( $str );
  2375. }
  2376. /**
  2377. * Actually set the password and such
  2378. * @since 1.27 cannot set a password for a user not in the database
  2379. * @param string|null $str New password to set or null to set an invalid
  2380. * password hash meaning that the user will not be able to log in
  2381. * through the web interface.
  2382. * @return bool Success
  2383. */
  2384. private function setPasswordInternal( $str ) {
  2385. $manager = AuthManager::singleton();
  2386. // If the user doesn't exist yet, fail
  2387. if ( !$manager->userExists( $this->getName() ) ) {
  2388. throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
  2389. }
  2390. $status = $this->changeAuthenticationData( [
  2391. 'username' => $this->getName(),
  2392. 'password' => $str,
  2393. 'retype' => $str,
  2394. ] );
  2395. if ( !$status->isGood() ) {
  2396. \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
  2397. ->info( __METHOD__ . ': Password change rejected: '
  2398. . $status->getWikiText( null, null, 'en' ) );
  2399. return false;
  2400. }
  2401. $this->setOption( 'watchlisttoken', false );
  2402. SessionManager::singleton()->invalidateSessionsForUser( $this );
  2403. return true;
  2404. }
  2405. /**
  2406. * Changes credentials of the user.
  2407. *
  2408. * This is a convenience wrapper around AuthManager::changeAuthenticationData.
  2409. * Note that this can return a status that isOK() but not isGood() on certain types of failures,
  2410. * e.g. when no provider handled the change.
  2411. *
  2412. * @param array $data A set of authentication data in fieldname => value format. This is the
  2413. * same data you would pass the changeauthenticationdata API - 'username', 'password' etc.
  2414. * @return Status
  2415. * @since 1.27
  2416. */
  2417. public function changeAuthenticationData( array $data ) {
  2418. $manager = AuthManager::singleton();
  2419. $reqs = $manager->getAuthenticationRequests( AuthManager::ACTION_CHANGE, $this );
  2420. $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
  2421. $status = Status::newGood( 'ignored' );
  2422. foreach ( $reqs as $req ) {
  2423. $status->merge( $manager->allowsAuthenticationDataChange( $req ), true );
  2424. }
  2425. if ( $status->getValue() === 'ignored' ) {
  2426. $status->warning( 'authenticationdatachange-ignored' );
  2427. }
  2428. if ( $status->isGood() ) {
  2429. foreach ( $reqs as $req ) {
  2430. $manager->changeAuthenticationData( $req );
  2431. }
  2432. }
  2433. return $status;
  2434. }
  2435. /**
  2436. * Get the user's current token.
  2437. * @param bool $forceCreation Force the generation of a new token if the
  2438. * user doesn't have one (default=true for backwards compatibility).
  2439. * @return string|null Token
  2440. */
  2441. public function getToken( $forceCreation = true ) {
  2442. global $wgAuthenticationTokenVersion;
  2443. $this->load();
  2444. if ( !$this->mToken && $forceCreation ) {
  2445. $this->setToken();
  2446. }
  2447. if ( !$this->mToken ) {
  2448. // The user doesn't have a token, return null to indicate that.
  2449. return null;
  2450. }
  2451. if ( $this->mToken === self::INVALID_TOKEN ) {
  2452. // We return a random value here so existing token checks are very
  2453. // likely to fail.
  2454. return MWCryptRand::generateHex( self::TOKEN_LENGTH );
  2455. }
  2456. if ( $wgAuthenticationTokenVersion === null ) {
  2457. // $wgAuthenticationTokenVersion not in use, so return the raw secret
  2458. return $this->mToken;
  2459. }
  2460. // $wgAuthenticationTokenVersion in use, so hmac it.
  2461. $ret = MWCryptHash::hmac( $wgAuthenticationTokenVersion, $this->mToken, false );
  2462. // The raw hash can be overly long. Shorten it up.
  2463. $len = max( 32, self::TOKEN_LENGTH );
  2464. if ( strlen( $ret ) < $len ) {
  2465. // Should never happen, even md5 is 128 bits
  2466. throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' );
  2467. }
  2468. return substr( $ret, -$len );
  2469. }
  2470. /**
  2471. * Set the random token (used for persistent authentication)
  2472. * Called from loadDefaults() among other places.
  2473. *
  2474. * @param string|bool $token If specified, set the token to this value
  2475. */
  2476. public function setToken( $token = false ) {
  2477. $this->load();
  2478. if ( $this->mToken === self::INVALID_TOKEN ) {
  2479. \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
  2480. ->debug( __METHOD__ . ": Ignoring attempt to set token for system user \"$this\"" );
  2481. } elseif ( !$token ) {
  2482. $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
  2483. } else {
  2484. $this->mToken = $token;
  2485. }
  2486. }
  2487. /**
  2488. * Get the user's e-mail address
  2489. * @return string User's email address
  2490. */
  2491. public function getEmail() {
  2492. $this->load();
  2493. Hooks::run( 'UserGetEmail', [ $this, &$this->mEmail ] );
  2494. return $this->mEmail;
  2495. }
  2496. /**
  2497. * Get the timestamp of the user's e-mail authentication
  2498. * @return string TS_MW timestamp
  2499. */
  2500. public function getEmailAuthenticationTimestamp() {
  2501. $this->load();
  2502. Hooks::run( 'UserGetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] );
  2503. return $this->mEmailAuthenticated;
  2504. }
  2505. /**
  2506. * Set the user's e-mail address
  2507. * @param string $str New e-mail address
  2508. */
  2509. public function setEmail( $str ) {
  2510. $this->load();
  2511. if ( $str == $this->mEmail ) {
  2512. return;
  2513. }
  2514. $this->invalidateEmail();
  2515. $this->mEmail = $str;
  2516. Hooks::run( 'UserSetEmail', [ $this, &$this->mEmail ] );
  2517. }
  2518. /**
  2519. * Set the user's e-mail address and a confirmation mail if needed.
  2520. *
  2521. * @since 1.20
  2522. * @param string $str New e-mail address
  2523. * @return Status
  2524. */
  2525. public function setEmailWithConfirmation( $str ) {
  2526. global $wgEnableEmail, $wgEmailAuthentication;
  2527. if ( !$wgEnableEmail ) {
  2528. return Status::newFatal( 'emaildisabled' );
  2529. }
  2530. $oldaddr = $this->getEmail();
  2531. if ( $str === $oldaddr ) {
  2532. return Status::newGood( true );
  2533. }
  2534. $type = $oldaddr != '' ? 'changed' : 'set';
  2535. $notificationResult = null;
  2536. if ( $wgEmailAuthentication && $type === 'changed' ) {
  2537. // Send the user an email notifying the user of the change in registered
  2538. // email address on their previous email address
  2539. $change = $str != '' ? 'changed' : 'removed';
  2540. $notificationResult = $this->sendMail(
  2541. wfMessage( 'notificationemail_subject_' . $change )->text(),
  2542. wfMessage( 'notificationemail_body_' . $change,
  2543. $this->getRequest()->getIP(),
  2544. $this->getName(),
  2545. $str )->text()
  2546. );
  2547. }
  2548. $this->setEmail( $str );
  2549. if ( $str !== '' && $wgEmailAuthentication ) {
  2550. // Send a confirmation request to the new address if needed
  2551. $result = $this->sendConfirmationMail( $type );
  2552. if ( $notificationResult !== null ) {
  2553. $result->merge( $notificationResult );
  2554. }
  2555. if ( $result->isGood() ) {
  2556. // Say to the caller that a confirmation and notification mail has been sent
  2557. $result->value = 'eauth';
  2558. }
  2559. } else {
  2560. $result = Status::newGood( true );
  2561. }
  2562. return $result;
  2563. }
  2564. /**
  2565. * Get the user's real name
  2566. * @return string User's real name
  2567. */
  2568. public function getRealName() {
  2569. if ( !$this->isItemLoaded( 'realname' ) ) {
  2570. $this->load();
  2571. }
  2572. return $this->mRealName;
  2573. }
  2574. /**
  2575. * Set the user's real name
  2576. * @param string $str New real name
  2577. */
  2578. public function setRealName( $str ) {
  2579. $this->load();
  2580. $this->mRealName = $str;
  2581. }
  2582. /**
  2583. * Get the user's current setting for a given option.
  2584. *
  2585. * @param string $oname The option to check
  2586. * @param string|array|null $defaultOverride A default value returned if the option does not exist
  2587. * @param bool $ignoreHidden Whether to ignore the effects of $wgHiddenPrefs
  2588. * @return string|array|int|null User's current value for the option
  2589. * @see getBoolOption()
  2590. * @see getIntOption()
  2591. */
  2592. public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
  2593. global $wgHiddenPrefs;
  2594. $this->loadOptions();
  2595. # We want 'disabled' preferences to always behave as the default value for
  2596. # users, even if they have set the option explicitly in their settings (ie they
  2597. # set it, and then it was disabled removing their ability to change it). But
  2598. # we don't want to erase the preferences in the database in case the preference
  2599. # is re-enabled again. So don't touch $mOptions, just override the returned value
  2600. if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) {
  2601. return self::getDefaultOption( $oname );
  2602. }
  2603. if ( array_key_exists( $oname, $this->mOptions ) ) {
  2604. return $this->mOptions[$oname];
  2605. }
  2606. return $defaultOverride;
  2607. }
  2608. /**
  2609. * Get all user's options
  2610. *
  2611. * @param int $flags Bitwise combination of:
  2612. * User::GETOPTIONS_EXCLUDE_DEFAULTS Exclude user options that are set
  2613. * to the default value. (Since 1.25)
  2614. * @return array
  2615. */
  2616. public function getOptions( $flags = 0 ) {
  2617. global $wgHiddenPrefs;
  2618. $this->loadOptions();
  2619. $options = $this->mOptions;
  2620. # We want 'disabled' preferences to always behave as the default value for
  2621. # users, even if they have set the option explicitly in their settings (ie they
  2622. # set it, and then it was disabled removing their ability to change it). But
  2623. # we don't want to erase the preferences in the database in case the preference
  2624. # is re-enabled again. So don't touch $mOptions, just override the returned value
  2625. foreach ( $wgHiddenPrefs as $pref ) {
  2626. $default = self::getDefaultOption( $pref );
  2627. if ( $default !== null ) {
  2628. $options[$pref] = $default;
  2629. }
  2630. }
  2631. if ( $flags & self::GETOPTIONS_EXCLUDE_DEFAULTS ) {
  2632. $options = array_diff_assoc( $options, self::getDefaultOptions() );
  2633. }
  2634. return $options;
  2635. }
  2636. /**
  2637. * Get the user's current setting for a given option, as a boolean value.
  2638. *
  2639. * @param string $oname The option to check
  2640. * @return bool User's current value for the option
  2641. * @see getOption()
  2642. */
  2643. public function getBoolOption( $oname ) {
  2644. return (bool)$this->getOption( $oname );
  2645. }
  2646. /**
  2647. * Get the user's current setting for a given option, as an integer value.
  2648. *
  2649. * @param string $oname The option to check
  2650. * @param int $defaultOverride A default value returned if the option does not exist
  2651. * @return int User's current value for the option
  2652. * @see getOption()
  2653. */
  2654. public function getIntOption( $oname, $defaultOverride = 0 ) {
  2655. $val = $this->getOption( $oname );
  2656. if ( $val == '' ) {
  2657. $val = $defaultOverride;
  2658. }
  2659. return intval( $val );
  2660. }
  2661. /**
  2662. * Set the given option for a user.
  2663. *
  2664. * You need to call saveSettings() to actually write to the database.
  2665. *
  2666. * @param string $oname The option to set
  2667. * @param mixed $val New value to set
  2668. */
  2669. public function setOption( $oname, $val ) {
  2670. $this->loadOptions();
  2671. // Explicitly NULL values should refer to defaults
  2672. if ( is_null( $val ) ) {
  2673. $val = self::getDefaultOption( $oname );
  2674. }
  2675. $this->mOptions[$oname] = $val;
  2676. }
  2677. /**
  2678. * Get a token stored in the preferences (like the watchlist one),
  2679. * resetting it if it's empty (and saving changes).
  2680. *
  2681. * @param string $oname The option name to retrieve the token from
  2682. * @return string|bool User's current value for the option, or false if this option is disabled.
  2683. * @see resetTokenFromOption()
  2684. * @see getOption()
  2685. * @deprecated since 1.26 Applications should use the OAuth extension
  2686. */
  2687. public function getTokenFromOption( $oname ) {
  2688. global $wgHiddenPrefs;
  2689. $id = $this->getId();
  2690. if ( !$id || in_array( $oname, $wgHiddenPrefs ) ) {
  2691. return false;
  2692. }
  2693. $token = $this->getOption( $oname );
  2694. if ( !$token ) {
  2695. // Default to a value based on the user token to avoid space
  2696. // wasted on storing tokens for all users. When this option
  2697. // is set manually by the user, only then is it stored.
  2698. $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() );
  2699. }
  2700. return $token;
  2701. }
  2702. /**
  2703. * Reset a token stored in the preferences (like the watchlist one).
  2704. * *Does not* save user's preferences (similarly to setOption()).
  2705. *
  2706. * @param string $oname The option name to reset the token in
  2707. * @return string|bool New token value, or false if this option is disabled.
  2708. * @see getTokenFromOption()
  2709. * @see setOption()
  2710. */
  2711. public function resetTokenFromOption( $oname ) {
  2712. global $wgHiddenPrefs;
  2713. if ( in_array( $oname, $wgHiddenPrefs ) ) {
  2714. return false;
  2715. }
  2716. $token = MWCryptRand::generateHex( 40 );
  2717. $this->setOption( $oname, $token );
  2718. return $token;
  2719. }
  2720. /**
  2721. * Return a list of the types of user options currently returned by
  2722. * User::getOptionKinds().
  2723. *
  2724. * Currently, the option kinds are:
  2725. * - 'registered' - preferences which are registered in core MediaWiki or
  2726. * by extensions using the UserGetDefaultOptions hook.
  2727. * - 'registered-multiselect' - as above, using the 'multiselect' type.
  2728. * - 'registered-checkmatrix' - as above, using the 'checkmatrix' type.
  2729. * - 'userjs' - preferences with names starting with 'userjs-', intended to
  2730. * be used by user scripts.
  2731. * - 'special' - "preferences" that are not accessible via User::getOptions
  2732. * or User::setOptions.
  2733. * - 'unused' - preferences about which MediaWiki doesn't know anything.
  2734. * These are usually legacy options, removed in newer versions.
  2735. *
  2736. * The API (and possibly others) use this function to determine the possible
  2737. * option types for validation purposes, so make sure to update this when a
  2738. * new option kind is added.
  2739. *
  2740. * @see User::getOptionKinds
  2741. * @return array Option kinds
  2742. */
  2743. public static function listOptionKinds() {
  2744. return [
  2745. 'registered',
  2746. 'registered-multiselect',
  2747. 'registered-checkmatrix',
  2748. 'userjs',
  2749. 'special',
  2750. 'unused'
  2751. ];
  2752. }
  2753. /**
  2754. * Return an associative array mapping preferences keys to the kind of a preference they're
  2755. * used for. Different kinds are handled differently when setting or reading preferences.
  2756. *
  2757. * See User::listOptionKinds for the list of valid option types that can be provided.
  2758. *
  2759. * @see User::listOptionKinds
  2760. * @param IContextSource $context
  2761. * @param array|null $options Assoc. array with options keys to check as keys.
  2762. * Defaults to $this->mOptions.
  2763. * @return array The key => kind mapping data
  2764. */
  2765. public function getOptionKinds( IContextSource $context, $options = null ) {
  2766. $this->loadOptions();
  2767. if ( $options === null ) {
  2768. $options = $this->mOptions;
  2769. }
  2770. $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
  2771. $prefs = $preferencesFactory->getFormDescriptor( $this, $context );
  2772. $mapping = [];
  2773. // Pull out the "special" options, so they don't get converted as
  2774. // multiselect or checkmatrix.
  2775. $specialOptions = array_fill_keys( $preferencesFactory->getSaveBlacklist(), true );
  2776. foreach ( $specialOptions as $name => $value ) {
  2777. unset( $prefs[$name] );
  2778. }
  2779. // Multiselect and checkmatrix options are stored in the database with
  2780. // one key per option, each having a boolean value. Extract those keys.
  2781. $multiselectOptions = [];
  2782. foreach ( $prefs as $name => $info ) {
  2783. if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
  2784. ( isset( $info['class'] ) && $info['class'] == HTMLMultiSelectField::class ) ) {
  2785. $opts = HTMLFormField::flattenOptions( $info['options'] );
  2786. $prefix = $info['prefix'] ?? $name;
  2787. foreach ( $opts as $value ) {
  2788. $multiselectOptions["$prefix$value"] = true;
  2789. }
  2790. unset( $prefs[$name] );
  2791. }
  2792. }
  2793. $checkmatrixOptions = [];
  2794. foreach ( $prefs as $name => $info ) {
  2795. if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
  2796. ( isset( $info['class'] ) && $info['class'] == HTMLCheckMatrix::class ) ) {
  2797. $columns = HTMLFormField::flattenOptions( $info['columns'] );
  2798. $rows = HTMLFormField::flattenOptions( $info['rows'] );
  2799. $prefix = $info['prefix'] ?? $name;
  2800. foreach ( $columns as $column ) {
  2801. foreach ( $rows as $row ) {
  2802. $checkmatrixOptions["$prefix$column-$row"] = true;
  2803. }
  2804. }
  2805. unset( $prefs[$name] );
  2806. }
  2807. }
  2808. // $value is ignored
  2809. foreach ( $options as $key => $value ) {
  2810. if ( isset( $prefs[$key] ) ) {
  2811. $mapping[$key] = 'registered';
  2812. } elseif ( isset( $multiselectOptions[$key] ) ) {
  2813. $mapping[$key] = 'registered-multiselect';
  2814. } elseif ( isset( $checkmatrixOptions[$key] ) ) {
  2815. $mapping[$key] = 'registered-checkmatrix';
  2816. } elseif ( isset( $specialOptions[$key] ) ) {
  2817. $mapping[$key] = 'special';
  2818. } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
  2819. $mapping[$key] = 'userjs';
  2820. } else {
  2821. $mapping[$key] = 'unused';
  2822. }
  2823. }
  2824. return $mapping;
  2825. }
  2826. /**
  2827. * Reset certain (or all) options to the site defaults
  2828. *
  2829. * The optional parameter determines which kinds of preferences will be reset.
  2830. * Supported values are everything that can be reported by getOptionKinds()
  2831. * and 'all', which forces a reset of *all* preferences and overrides everything else.
  2832. *
  2833. * @param array|string $resetKinds Which kinds of preferences to reset. Defaults to
  2834. * [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ]
  2835. * for backwards-compatibility.
  2836. * @param IContextSource|null $context Context source used when $resetKinds
  2837. * does not contain 'all', passed to getOptionKinds().
  2838. * Defaults to RequestContext::getMain() when null.
  2839. */
  2840. public function resetOptions(
  2841. $resetKinds = [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ],
  2842. IContextSource $context = null
  2843. ) {
  2844. $this->load();
  2845. $defaultOptions = self::getDefaultOptions();
  2846. if ( !is_array( $resetKinds ) ) {
  2847. $resetKinds = [ $resetKinds ];
  2848. }
  2849. if ( in_array( 'all', $resetKinds ) ) {
  2850. $newOptions = $defaultOptions;
  2851. } else {
  2852. if ( $context === null ) {
  2853. $context = RequestContext::getMain();
  2854. }
  2855. $optionKinds = $this->getOptionKinds( $context );
  2856. $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
  2857. $newOptions = [];
  2858. // Use default values for the options that should be deleted, and
  2859. // copy old values for the ones that shouldn't.
  2860. foreach ( $this->mOptions as $key => $value ) {
  2861. if ( in_array( $optionKinds[$key], $resetKinds ) ) {
  2862. if ( array_key_exists( $key, $defaultOptions ) ) {
  2863. $newOptions[$key] = $defaultOptions[$key];
  2864. }
  2865. } else {
  2866. $newOptions[$key] = $value;
  2867. }
  2868. }
  2869. }
  2870. Hooks::run( 'UserResetAllOptions', [ $this, &$newOptions, $this->mOptions, $resetKinds ] );
  2871. $this->mOptions = $newOptions;
  2872. $this->mOptionsLoaded = true;
  2873. }
  2874. /**
  2875. * Get the user's preferred date format.
  2876. * @return string User's preferred date format
  2877. */
  2878. public function getDatePreference() {
  2879. // Important migration for old data rows
  2880. if ( is_null( $this->mDatePreference ) ) {
  2881. global $wgLang;
  2882. $value = $this->getOption( 'date' );
  2883. $map = $wgLang->getDatePreferenceMigrationMap();
  2884. if ( isset( $map[$value] ) ) {
  2885. $value = $map[$value];
  2886. }
  2887. $this->mDatePreference = $value;
  2888. }
  2889. return $this->mDatePreference;
  2890. }
  2891. /**
  2892. * Determine based on the wiki configuration and the user's options,
  2893. * whether this user must be over HTTPS no matter what.
  2894. *
  2895. * @return bool
  2896. */
  2897. public function requiresHTTPS() {
  2898. global $wgSecureLogin;
  2899. if ( !$wgSecureLogin ) {
  2900. return false;
  2901. }
  2902. $https = $this->getBoolOption( 'prefershttps' );
  2903. Hooks::run( 'UserRequiresHTTPS', [ $this, &$https ] );
  2904. if ( $https ) {
  2905. $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
  2906. }
  2907. return $https;
  2908. }
  2909. /**
  2910. * Get the user preferred stub threshold
  2911. *
  2912. * @return int
  2913. */
  2914. public function getStubThreshold() {
  2915. global $wgMaxArticleSize; # Maximum article size, in Kb
  2916. $threshold = $this->getIntOption( 'stubthreshold' );
  2917. if ( $threshold > $wgMaxArticleSize * 1024 ) {
  2918. // If they have set an impossible value, disable the preference
  2919. // so we can use the parser cache again.
  2920. $threshold = 0;
  2921. }
  2922. return $threshold;
  2923. }
  2924. /**
  2925. * Get the permissions this user has.
  2926. * @return string[] permission names
  2927. *
  2928. * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
  2929. * ->getUserPermissions(..) instead
  2930. *
  2931. */
  2932. public function getRights() {
  2933. return MediaWikiServices::getInstance()->getPermissionManager()->getUserPermissions( $this );
  2934. }
  2935. /**
  2936. * Get the list of explicit group memberships this user has.
  2937. * The implicit * and user groups are not included.
  2938. *
  2939. * @return string[] Array of internal group names (sorted since 1.33)
  2940. */
  2941. public function getGroups() {
  2942. $this->load();
  2943. $this->loadGroups();
  2944. return array_keys( $this->mGroupMemberships );
  2945. }
  2946. /**
  2947. * Get the list of explicit group memberships this user has, stored as
  2948. * UserGroupMembership objects. Implicit groups are not included.
  2949. *
  2950. * @return UserGroupMembership[] Associative array of (group name => UserGroupMembership object)
  2951. * @since 1.29
  2952. */
  2953. public function getGroupMemberships() {
  2954. $this->load();
  2955. $this->loadGroups();
  2956. return $this->mGroupMemberships;
  2957. }
  2958. /**
  2959. * Get the list of implicit group memberships this user has.
  2960. * This includes all explicit groups, plus 'user' if logged in,
  2961. * '*' for all accounts, and autopromoted groups
  2962. * @param bool $recache Whether to avoid the cache
  2963. * @return array Array of String internal group names
  2964. */
  2965. public function getEffectiveGroups( $recache = false ) {
  2966. if ( $recache || is_null( $this->mEffectiveGroups ) ) {
  2967. $this->mEffectiveGroups = array_unique( array_merge(
  2968. $this->getGroups(), // explicit groups
  2969. $this->getAutomaticGroups( $recache ) // implicit groups
  2970. ) );
  2971. // Avoid PHP 7.1 warning of passing $this by reference
  2972. $user = $this;
  2973. // Hook for additional groups
  2974. Hooks::run( 'UserEffectiveGroups', [ &$user, &$this->mEffectiveGroups ] );
  2975. // Force reindexation of groups when a hook has unset one of them
  2976. $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
  2977. }
  2978. return $this->mEffectiveGroups;
  2979. }
  2980. /**
  2981. * Get the list of implicit group memberships this user has.
  2982. * This includes 'user' if logged in, '*' for all accounts,
  2983. * and autopromoted groups
  2984. * @param bool $recache Whether to avoid the cache
  2985. * @return array Array of String internal group names
  2986. */
  2987. public function getAutomaticGroups( $recache = false ) {
  2988. if ( $recache || is_null( $this->mImplicitGroups ) ) {
  2989. $this->mImplicitGroups = [ '*' ];
  2990. if ( $this->getId() ) {
  2991. $this->mImplicitGroups[] = 'user';
  2992. $this->mImplicitGroups = array_unique( array_merge(
  2993. $this->mImplicitGroups,
  2994. Autopromote::getAutopromoteGroups( $this )
  2995. ) );
  2996. }
  2997. if ( $recache ) {
  2998. // Assure data consistency with rights/groups,
  2999. // as getEffectiveGroups() depends on this function
  3000. $this->mEffectiveGroups = null;
  3001. }
  3002. }
  3003. return $this->mImplicitGroups;
  3004. }
  3005. /**
  3006. * Returns the groups the user has belonged to.
  3007. *
  3008. * The user may still belong to the returned groups. Compare with getGroups().
  3009. *
  3010. * The function will not return groups the user had belonged to before MW 1.17
  3011. *
  3012. * @return array Names of the groups the user has belonged to.
  3013. */
  3014. public function getFormerGroups() {
  3015. $this->load();
  3016. if ( is_null( $this->mFormerGroups ) ) {
  3017. $db = ( $this->queryFlagsUsed & self::READ_LATEST )
  3018. ? wfGetDB( DB_MASTER )
  3019. : wfGetDB( DB_REPLICA );
  3020. $res = $db->select( 'user_former_groups',
  3021. [ 'ufg_group' ],
  3022. [ 'ufg_user' => $this->mId ],
  3023. __METHOD__ );
  3024. $this->mFormerGroups = [];
  3025. foreach ( $res as $row ) {
  3026. $this->mFormerGroups[] = $row->ufg_group;
  3027. }
  3028. }
  3029. return $this->mFormerGroups;
  3030. }
  3031. /**
  3032. * Get the user's edit count.
  3033. * @return int|null Null for anonymous users
  3034. */
  3035. public function getEditCount() {
  3036. if ( !$this->getId() ) {
  3037. return null;
  3038. }
  3039. if ( $this->mEditCount === null ) {
  3040. /* Populate the count, if it has not been populated yet */
  3041. $dbr = wfGetDB( DB_REPLICA );
  3042. // check if the user_editcount field has been initialized
  3043. $count = $dbr->selectField(
  3044. 'user', 'user_editcount',
  3045. [ 'user_id' => $this->mId ],
  3046. __METHOD__
  3047. );
  3048. if ( $count === null ) {
  3049. // it has not been initialized. do so.
  3050. $count = $this->initEditCountInternal( $dbr );
  3051. }
  3052. $this->mEditCount = $count;
  3053. }
  3054. return (int)$this->mEditCount;
  3055. }
  3056. /**
  3057. * Add the user to the given group. This takes immediate effect.
  3058. * If the user is already in the group, the expiry time will be updated to the new
  3059. * expiry time. (If $expiry is omitted or null, the membership will be altered to
  3060. * never expire.)
  3061. *
  3062. * @param string $group Name of the group to add
  3063. * @param string|null $expiry Optional expiry timestamp in any format acceptable to
  3064. * wfTimestamp(), or null if the group assignment should not expire
  3065. * @return bool
  3066. */
  3067. public function addGroup( $group, $expiry = null ) {
  3068. $this->load();
  3069. $this->loadGroups();
  3070. if ( $expiry ) {
  3071. $expiry = wfTimestamp( TS_MW, $expiry );
  3072. }
  3073. if ( !Hooks::run( 'UserAddGroup', [ $this, &$group, &$expiry ] ) ) {
  3074. return false;
  3075. }
  3076. // create the new UserGroupMembership and put it in the DB
  3077. $ugm = new UserGroupMembership( $this->mId, $group, $expiry );
  3078. if ( !$ugm->insert( true ) ) {
  3079. return false;
  3080. }
  3081. $this->mGroupMemberships[$group] = $ugm;
  3082. // Refresh the groups caches, and clear the rights cache so it will be
  3083. // refreshed on the next call to $this->getRights().
  3084. $this->getEffectiveGroups( true );
  3085. MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache( $this );
  3086. $this->invalidateCache();
  3087. return true;
  3088. }
  3089. /**
  3090. * Remove the user from the given group.
  3091. * This takes immediate effect.
  3092. * @param string $group Name of the group to remove
  3093. * @return bool
  3094. */
  3095. public function removeGroup( $group ) {
  3096. $this->load();
  3097. if ( !Hooks::run( 'UserRemoveGroup', [ $this, &$group ] ) ) {
  3098. return false;
  3099. }
  3100. $ugm = UserGroupMembership::getMembership( $this->mId, $group );
  3101. // delete the membership entry
  3102. if ( !$ugm || !$ugm->delete() ) {
  3103. return false;
  3104. }
  3105. $this->loadGroups();
  3106. unset( $this->mGroupMemberships[$group] );
  3107. // Refresh the groups caches, and clear the rights cache so it will be
  3108. // refreshed on the next call to $this->getRights().
  3109. $this->getEffectiveGroups( true );
  3110. MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache( $this );
  3111. $this->invalidateCache();
  3112. return true;
  3113. }
  3114. /**
  3115. * Alias of isLoggedIn() with a name that describes its actual functionality. UserIdentity has
  3116. * only this new name and not the old isLoggedIn() variant.
  3117. *
  3118. * @return bool True if user is registered on this wiki, i.e., has a user ID. False if user is
  3119. * anonymous or has no local account (which can happen when importing). This is equivalent to
  3120. * getId() != 0 and is provided for code readability.
  3121. * @since 1.34
  3122. */
  3123. public function isRegistered() {
  3124. return $this->getId() != 0;
  3125. }
  3126. /**
  3127. * Get whether the user is logged in
  3128. * @return bool
  3129. */
  3130. public function isLoggedIn() {
  3131. return $this->isRegistered();
  3132. }
  3133. /**
  3134. * Get whether the user is anonymous
  3135. * @return bool
  3136. */
  3137. public function isAnon() {
  3138. return !$this->isRegistered();
  3139. }
  3140. /**
  3141. * @return bool Whether this user is flagged as being a bot role account
  3142. * @since 1.28
  3143. */
  3144. public function isBot() {
  3145. if ( in_array( 'bot', $this->getGroups() ) && $this->isAllowed( 'bot' ) ) {
  3146. return true;
  3147. }
  3148. $isBot = false;
  3149. Hooks::run( "UserIsBot", [ $this, &$isBot ] );
  3150. return $isBot;
  3151. }
  3152. /**
  3153. * Check if user is allowed to access a feature / make an action
  3154. *
  3155. * @deprecated since 1.34, use MediaWikiServices::getInstance()
  3156. * ->getPermissionManager()->userHasAnyRights(...) instead
  3157. *
  3158. * @param string ...$permissions Permissions to test
  3159. * @return bool True if user is allowed to perform *any* of the given actions
  3160. */
  3161. public function isAllowedAny( ...$permissions ) {
  3162. return MediaWikiServices::getInstance()
  3163. ->getPermissionManager()
  3164. ->userHasAnyRight( $this, ...$permissions );
  3165. }
  3166. /**
  3167. * @deprecated since 1.34, use MediaWikiServices::getInstance()
  3168. * ->getPermissionManager()->userHasAllRights(...) instead
  3169. * @param string ...$permissions Permissions to test
  3170. * @return bool True if the user is allowed to perform *all* of the given actions
  3171. */
  3172. public function isAllowedAll( ...$permissions ) {
  3173. return MediaWikiServices::getInstance()
  3174. ->getPermissionManager()
  3175. ->userHasAllRights( $this, ...$permissions );
  3176. }
  3177. /**
  3178. * Internal mechanics of testing a permission
  3179. *
  3180. * @deprecated since 1.34, use MediaWikiServices::getInstance()
  3181. * ->getPermissionManager()->userHasRight(...) instead
  3182. *
  3183. * @param string $action
  3184. *
  3185. * @return bool
  3186. */
  3187. public function isAllowed( $action = '' ) {
  3188. return MediaWikiServices::getInstance()->getPermissionManager()
  3189. ->userHasRight( $this, $action );
  3190. }
  3191. /**
  3192. * Check whether to enable recent changes patrol features for this user
  3193. * @return bool True or false
  3194. */
  3195. public function useRCPatrol() {
  3196. global $wgUseRCPatrol;
  3197. return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
  3198. }
  3199. /**
  3200. * Check whether to enable new pages patrol features for this user
  3201. * @return bool True or false
  3202. */
  3203. public function useNPPatrol() {
  3204. global $wgUseRCPatrol, $wgUseNPPatrol;
  3205. return (
  3206. ( $wgUseRCPatrol || $wgUseNPPatrol )
  3207. && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
  3208. );
  3209. }
  3210. /**
  3211. * Check whether to enable new files patrol features for this user
  3212. * @return bool True or false
  3213. */
  3214. public function useFilePatrol() {
  3215. global $wgUseRCPatrol, $wgUseFilePatrol;
  3216. return (
  3217. ( $wgUseRCPatrol || $wgUseFilePatrol )
  3218. && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
  3219. );
  3220. }
  3221. /**
  3222. * Get the WebRequest object to use with this object
  3223. *
  3224. * @return WebRequest
  3225. */
  3226. public function getRequest() {
  3227. if ( $this->mRequest ) {
  3228. return $this->mRequest;
  3229. }
  3230. global $wgRequest;
  3231. return $wgRequest;
  3232. }
  3233. /**
  3234. * Check the watched status of an article.
  3235. * @since 1.22 $checkRights parameter added
  3236. * @param Title $title Title of the article to look at
  3237. * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
  3238. * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
  3239. * @return bool
  3240. */
  3241. public function isWatched( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
  3242. if ( $title->isWatchable() && ( !$checkRights || $this->isAllowed( 'viewmywatchlist' ) ) ) {
  3243. return MediaWikiServices::getInstance()->getWatchedItemStore()->isWatched( $this, $title );
  3244. }
  3245. return false;
  3246. }
  3247. /**
  3248. * Watch an article.
  3249. * @since 1.22 $checkRights parameter added
  3250. * @param Title $title Title of the article to look at
  3251. * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
  3252. * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
  3253. */
  3254. public function addWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
  3255. if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
  3256. MediaWikiServices::getInstance()->getWatchedItemStore()->addWatchBatchForUser(
  3257. $this,
  3258. [ $title->getSubjectPage(), $title->getTalkPage() ]
  3259. );
  3260. }
  3261. $this->invalidateCache();
  3262. }
  3263. /**
  3264. * Stop watching an article.
  3265. * @since 1.22 $checkRights parameter added
  3266. * @param Title $title Title of the article to look at
  3267. * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
  3268. * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
  3269. */
  3270. public function removeWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
  3271. if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
  3272. $store = MediaWikiServices::getInstance()->getWatchedItemStore();
  3273. $store->removeWatch( $this, $title->getSubjectPage() );
  3274. $store->removeWatch( $this, $title->getTalkPage() );
  3275. }
  3276. $this->invalidateCache();
  3277. }
  3278. /**
  3279. * Clear the user's notification timestamp for the given title.
  3280. * If e-notif e-mails are on, they will receive notification mails on
  3281. * the next change of the page if it's watched etc.
  3282. * @note If the user doesn't have 'editmywatchlist', this will do nothing.
  3283. * @param Title &$title Title of the article to look at
  3284. * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
  3285. */
  3286. public function clearNotification( &$title, $oldid = 0 ) {
  3287. global $wgUseEnotif, $wgShowUpdatedMarker;
  3288. // Do nothing if the database is locked to writes
  3289. if ( wfReadOnly() ) {
  3290. return;
  3291. }
  3292. // Do nothing if not allowed to edit the watchlist
  3293. if ( !$this->isAllowed( 'editmywatchlist' ) ) {
  3294. return;
  3295. }
  3296. // If we're working on user's talk page, we should update the talk page message indicator
  3297. if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
  3298. // Avoid PHP 7.1 warning of passing $this by reference
  3299. $user = $this;
  3300. if ( !Hooks::run( 'UserClearNewTalkNotification', [ &$user, $oldid ] ) ) {
  3301. return;
  3302. }
  3303. // Try to update the DB post-send and only if needed...
  3304. DeferredUpdates::addCallableUpdate( function () use ( $title, $oldid ) {
  3305. if ( !$this->getNewtalk() ) {
  3306. return; // no notifications to clear
  3307. }
  3308. // Delete the last notifications (they stack up)
  3309. $this->setNewtalk( false );
  3310. // If there is a new, unseen, revision, use its timestamp
  3311. if ( $oldid ) {
  3312. $rl = MediaWikiServices::getInstance()->getRevisionLookup();
  3313. $oldRev = $rl->getRevisionById( $oldid, Title::READ_LATEST );
  3314. if ( $oldRev ) {
  3315. $newRev = $rl->getNextRevision( $oldRev );
  3316. if ( $newRev ) {
  3317. // TODO: actually no need to wrap in a revision,
  3318. // setNewtalk really only needs a RevRecord
  3319. $this->setNewtalk( true, new Revision( $newRev ) );
  3320. }
  3321. }
  3322. }
  3323. } );
  3324. }
  3325. if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
  3326. return;
  3327. }
  3328. if ( $this->isAnon() ) {
  3329. // Nothing else to do...
  3330. return;
  3331. }
  3332. // Only update the timestamp if the page is being watched.
  3333. // The query to find out if it is watched is cached both in memcached and per-invocation,
  3334. // and when it does have to be executed, it can be on a replica DB
  3335. // If this is the user's newtalk page, we always update the timestamp
  3336. $force = '';
  3337. if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
  3338. $force = 'force';
  3339. }
  3340. MediaWikiServices::getInstance()->getWatchedItemStore()
  3341. ->resetNotificationTimestamp( $this, $title, $force, $oldid );
  3342. }
  3343. /**
  3344. * Resets all of the given user's page-change notification timestamps.
  3345. * If e-notif e-mails are on, they will receive notification mails on
  3346. * the next change of any watched page.
  3347. * @note If the user doesn't have 'editmywatchlist', this will do nothing.
  3348. */
  3349. public function clearAllNotifications() {
  3350. global $wgUseEnotif, $wgShowUpdatedMarker;
  3351. // Do nothing if not allowed to edit the watchlist
  3352. if ( wfReadOnly() || !$this->isAllowed( 'editmywatchlist' ) ) {
  3353. return;
  3354. }
  3355. if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
  3356. $this->setNewtalk( false );
  3357. return;
  3358. }
  3359. $id = $this->getId();
  3360. if ( !$id ) {
  3361. return;
  3362. }
  3363. $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
  3364. $watchedItemStore->resetAllNotificationTimestampsForUser( $this );
  3365. // We also need to clear here the "you have new message" notification for the own
  3366. // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
  3367. }
  3368. /**
  3369. * Compute experienced level based on edit count and registration date.
  3370. *
  3371. * @return string 'newcomer', 'learner', or 'experienced'
  3372. */
  3373. public function getExperienceLevel() {
  3374. global $wgLearnerEdits,
  3375. $wgExperiencedUserEdits,
  3376. $wgLearnerMemberSince,
  3377. $wgExperiencedUserMemberSince;
  3378. if ( $this->isAnon() ) {
  3379. return false;
  3380. }
  3381. $editCount = $this->getEditCount();
  3382. $registration = $this->getRegistration();
  3383. $now = time();
  3384. $learnerRegistration = wfTimestamp( TS_MW, $now - $wgLearnerMemberSince * 86400 );
  3385. $experiencedRegistration = wfTimestamp( TS_MW, $now - $wgExperiencedUserMemberSince * 86400 );
  3386. if ( $editCount < $wgLearnerEdits ||
  3387. $registration > $learnerRegistration ) {
  3388. return 'newcomer';
  3389. }
  3390. if ( $editCount > $wgExperiencedUserEdits &&
  3391. $registration <= $experiencedRegistration
  3392. ) {
  3393. return 'experienced';
  3394. }
  3395. return 'learner';
  3396. }
  3397. /**
  3398. * Persist this user's session (e.g. set cookies)
  3399. *
  3400. * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
  3401. * is passed.
  3402. * @param bool|null $secure Whether to force secure/insecure cookies or use default
  3403. * @param bool $rememberMe Whether to add a Token cookie for elongated sessions
  3404. */
  3405. public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
  3406. $this->load();
  3407. if ( $this->mId == 0 ) {
  3408. return;
  3409. }
  3410. $session = $this->getRequest()->getSession();
  3411. if ( $request && $session->getRequest() !== $request ) {
  3412. $session = $session->sessionWithRequest( $request );
  3413. }
  3414. $delay = $session->delaySave();
  3415. if ( !$session->getUser()->equals( $this ) ) {
  3416. if ( !$session->canSetUser() ) {
  3417. \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
  3418. ->warning( __METHOD__ .
  3419. ": Cannot save user \"$this\" to a user \"{$session->getUser()}\"'s immutable session"
  3420. );
  3421. return;
  3422. }
  3423. $session->setUser( $this );
  3424. }
  3425. $session->setRememberUser( $rememberMe );
  3426. if ( $secure !== null ) {
  3427. $session->setForceHTTPS( $secure );
  3428. }
  3429. $session->persist();
  3430. ScopedCallback::consume( $delay );
  3431. }
  3432. /**
  3433. * Log this user out.
  3434. */
  3435. public function logout() {
  3436. // Avoid PHP 7.1 warning of passing $this by reference
  3437. $user = $this;
  3438. if ( Hooks::run( 'UserLogout', [ &$user ] ) ) {
  3439. $this->doLogout();
  3440. }
  3441. }
  3442. /**
  3443. * Clear the user's session, and reset the instance cache.
  3444. * @see logout()
  3445. */
  3446. public function doLogout() {
  3447. $session = $this->getRequest()->getSession();
  3448. if ( !$session->canSetUser() ) {
  3449. \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
  3450. ->warning( __METHOD__ . ": Cannot log out of an immutable session" );
  3451. $error = 'immutable';
  3452. } elseif ( !$session->getUser()->equals( $this ) ) {
  3453. \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
  3454. ->warning( __METHOD__ .
  3455. ": Cannot log user \"$this\" out of a user \"{$session->getUser()}\"'s session"
  3456. );
  3457. // But we still may as well make this user object anon
  3458. $this->clearInstanceCache( 'defaults' );
  3459. $error = 'wronguser';
  3460. } else {
  3461. $this->clearInstanceCache( 'defaults' );
  3462. $delay = $session->delaySave();
  3463. $session->unpersist(); // Clear cookies (T127436)
  3464. $session->setLoggedOutTimestamp( time() );
  3465. $session->setUser( new User );
  3466. $session->set( 'wsUserID', 0 ); // Other code expects this
  3467. $session->resetAllTokens();
  3468. ScopedCallback::consume( $delay );
  3469. $error = false;
  3470. }
  3471. \MediaWiki\Logger\LoggerFactory::getInstance( 'authevents' )->info( 'Logout', [
  3472. 'event' => 'logout',
  3473. 'successful' => $error === false,
  3474. 'status' => $error ?: 'success',
  3475. ] );
  3476. }
  3477. /**
  3478. * Save this user's settings into the database.
  3479. * @todo Only rarely do all these fields need to be set!
  3480. */
  3481. public function saveSettings() {
  3482. if ( wfReadOnly() ) {
  3483. // @TODO: caller should deal with this instead!
  3484. // This should really just be an exception.
  3485. MWExceptionHandler::logException( new DBExpectedError(
  3486. null,
  3487. "Could not update user with ID '{$this->mId}'; DB is read-only."
  3488. ) );
  3489. return;
  3490. }
  3491. $this->load();
  3492. if ( $this->mId == 0 ) {
  3493. return; // anon
  3494. }
  3495. // Get a new user_touched that is higher than the old one.
  3496. // This will be used for a CAS check as a last-resort safety
  3497. // check against race conditions and replica DB lag.
  3498. $newTouched = $this->newTouchedTimestamp();
  3499. $dbw = wfGetDB( DB_MASTER );
  3500. $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $newTouched ) {
  3501. $dbw->update( 'user',
  3502. [ /* SET */
  3503. 'user_name' => $this->mName,
  3504. 'user_real_name' => $this->mRealName,
  3505. 'user_email' => $this->mEmail,
  3506. 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
  3507. 'user_touched' => $dbw->timestamp( $newTouched ),
  3508. 'user_token' => strval( $this->mToken ),
  3509. 'user_email_token' => $this->mEmailToken,
  3510. 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
  3511. ], $this->makeUpdateConditions( $dbw, [ /* WHERE */
  3512. 'user_id' => $this->mId,
  3513. ] ), $fname
  3514. );
  3515. if ( !$dbw->affectedRows() ) {
  3516. // Maybe the problem was a missed cache update; clear it to be safe
  3517. $this->clearSharedCache( 'refresh' );
  3518. // User was changed in the meantime or loaded with stale data
  3519. $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'replica';
  3520. LoggerFactory::getInstance( 'preferences' )->warning(
  3521. "CAS update failed on user_touched for user ID '{user_id}' ({db_flag} read)",
  3522. [ 'user_id' => $this->mId, 'db_flag' => $from ]
  3523. );
  3524. throw new MWException( "CAS update failed on user_touched. " .
  3525. "The version of the user to be saved is older than the current version."
  3526. );
  3527. }
  3528. $dbw->update(
  3529. 'actor',
  3530. [ 'actor_name' => $this->mName ],
  3531. [ 'actor_user' => $this->mId ],
  3532. $fname
  3533. );
  3534. } );
  3535. $this->mTouched = $newTouched;
  3536. $this->saveOptions();
  3537. Hooks::run( 'UserSaveSettings', [ $this ] );
  3538. $this->clearSharedCache( 'changed' );
  3539. $this->getUserPage()->purgeSquid();
  3540. }
  3541. /**
  3542. * If only this user's username is known, and it exists, return the user ID.
  3543. *
  3544. * @param int $flags Bitfield of User:READ_* constants; useful for existence checks
  3545. * @return int
  3546. */
  3547. public function idForName( $flags = 0 ) {
  3548. $s = trim( $this->getName() );
  3549. if ( $s === '' ) {
  3550. return 0;
  3551. }
  3552. $db = ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
  3553. ? wfGetDB( DB_MASTER )
  3554. : wfGetDB( DB_REPLICA );
  3555. $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING )
  3556. ? [ 'LOCK IN SHARE MODE' ]
  3557. : [];
  3558. $id = $db->selectField( 'user',
  3559. 'user_id', [ 'user_name' => $s ], __METHOD__, $options );
  3560. return (int)$id;
  3561. }
  3562. /**
  3563. * Add a user to the database, return the user object
  3564. *
  3565. * @param string $name Username to add
  3566. * @param array $params Array of Strings Non-default parameters to save to
  3567. * the database as user_* fields:
  3568. * - email: The user's email address.
  3569. * - email_authenticated: The email authentication timestamp.
  3570. * - real_name: The user's real name.
  3571. * - options: An associative array of non-default options.
  3572. * - token: Random authentication token. Do not set.
  3573. * - registration: Registration timestamp. Do not set.
  3574. *
  3575. * @return User|null User object, or null if the username already exists.
  3576. */
  3577. public static function createNew( $name, $params = [] ) {
  3578. foreach ( [ 'password', 'newpassword', 'newpass_time', 'password_expires' ] as $field ) {
  3579. if ( isset( $params[$field] ) ) {
  3580. wfDeprecated( __METHOD__ . " with param '$field'", '1.27' );
  3581. unset( $params[$field] );
  3582. }
  3583. }
  3584. $user = new User;
  3585. $user->load();
  3586. $user->setToken(); // init token
  3587. if ( isset( $params['options'] ) ) {
  3588. $user->mOptions = $params['options'] + (array)$user->mOptions;
  3589. unset( $params['options'] );
  3590. }
  3591. $dbw = wfGetDB( DB_MASTER );
  3592. $noPass = PasswordFactory::newInvalidPassword()->toString();
  3593. $fields = [
  3594. 'user_name' => $name,
  3595. 'user_password' => $noPass,
  3596. 'user_newpassword' => $noPass,
  3597. 'user_email' => $user->mEmail,
  3598. 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
  3599. 'user_real_name' => $user->mRealName,
  3600. 'user_token' => strval( $user->mToken ),
  3601. 'user_registration' => $dbw->timestamp( $user->mRegistration ),
  3602. 'user_editcount' => 0,
  3603. 'user_touched' => $dbw->timestamp( $user->newTouchedTimestamp() ),
  3604. ];
  3605. foreach ( $params as $name => $value ) {
  3606. $fields["user_$name"] = $value;
  3607. }
  3608. return $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $fields ) {
  3609. $dbw->insert( 'user', $fields, $fname, [ 'IGNORE' ] );
  3610. if ( $dbw->affectedRows() ) {
  3611. $newUser = self::newFromId( $dbw->insertId() );
  3612. $newUser->mName = $fields['user_name'];
  3613. $newUser->updateActorId( $dbw );
  3614. // Load the user from master to avoid replica lag
  3615. $newUser->load( self::READ_LATEST );
  3616. } else {
  3617. $newUser = null;
  3618. }
  3619. return $newUser;
  3620. } );
  3621. }
  3622. /**
  3623. * Add this existing user object to the database. If the user already
  3624. * exists, a fatal status object is returned, and the user object is
  3625. * initialised with the data from the database.
  3626. *
  3627. * Previously, this function generated a DB error due to a key conflict
  3628. * if the user already existed. Many extension callers use this function
  3629. * in code along the lines of:
  3630. *
  3631. * $user = User::newFromName( $name );
  3632. * if ( !$user->isLoggedIn() ) {
  3633. * $user->addToDatabase();
  3634. * }
  3635. * // do something with $user...
  3636. *
  3637. * However, this was vulnerable to a race condition (T18020). By
  3638. * initialising the user object if the user exists, we aim to support this
  3639. * calling sequence as far as possible.
  3640. *
  3641. * Note that if the user exists, this function will acquire a write lock,
  3642. * so it is still advisable to make the call conditional on isLoggedIn(),
  3643. * and to commit the transaction after calling.
  3644. *
  3645. * @throws MWException
  3646. * @return Status
  3647. */
  3648. public function addToDatabase() {
  3649. $this->load();
  3650. if ( !$this->mToken ) {
  3651. $this->setToken(); // init token
  3652. }
  3653. if ( !is_string( $this->mName ) ) {
  3654. throw new RuntimeException( "User name field is not set." );
  3655. }
  3656. $this->mTouched = $this->newTouchedTimestamp();
  3657. $dbw = wfGetDB( DB_MASTER );
  3658. $status = $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) {
  3659. $noPass = PasswordFactory::newInvalidPassword()->toString();
  3660. $dbw->insert( 'user',
  3661. [
  3662. 'user_name' => $this->mName,
  3663. 'user_password' => $noPass,
  3664. 'user_newpassword' => $noPass,
  3665. 'user_email' => $this->mEmail,
  3666. 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
  3667. 'user_real_name' => $this->mRealName,
  3668. 'user_token' => strval( $this->mToken ),
  3669. 'user_registration' => $dbw->timestamp( $this->mRegistration ),
  3670. 'user_editcount' => 0,
  3671. 'user_touched' => $dbw->timestamp( $this->mTouched ),
  3672. ], $fname,
  3673. [ 'IGNORE' ]
  3674. );
  3675. if ( !$dbw->affectedRows() ) {
  3676. // Use locking reads to bypass any REPEATABLE-READ snapshot.
  3677. $this->mId = $dbw->selectField(
  3678. 'user',
  3679. 'user_id',
  3680. [ 'user_name' => $this->mName ],
  3681. $fname,
  3682. [ 'LOCK IN SHARE MODE' ]
  3683. );
  3684. $loaded = false;
  3685. if ( $this->mId && $this->loadFromDatabase( self::READ_LOCKING ) ) {
  3686. $loaded = true;
  3687. }
  3688. if ( !$loaded ) {
  3689. throw new MWException( $fname . ": hit a key conflict attempting " .
  3690. "to insert user '{$this->mName}' row, but it was not present in select!" );
  3691. }
  3692. return Status::newFatal( 'userexists' );
  3693. }
  3694. $this->mId = $dbw->insertId();
  3695. self::$idCacheByName[$this->mName] = $this->mId;
  3696. $this->updateActorId( $dbw );
  3697. return Status::newGood();
  3698. } );
  3699. if ( !$status->isGood() ) {
  3700. return $status;
  3701. }
  3702. // Clear instance cache other than user table data and actor, which is already accurate
  3703. $this->clearInstanceCache();
  3704. $this->saveOptions();
  3705. return Status::newGood();
  3706. }
  3707. /**
  3708. * Update the actor ID after an insert
  3709. * @param IDatabase $dbw Writable database handle
  3710. */
  3711. private function updateActorId( IDatabase $dbw ) {
  3712. $dbw->insert(
  3713. 'actor',
  3714. [ 'actor_user' => $this->mId, 'actor_name' => $this->mName ],
  3715. __METHOD__
  3716. );
  3717. $this->mActorId = (int)$dbw->insertId();
  3718. }
  3719. /**
  3720. * If this user is logged-in and blocked,
  3721. * block any IP address they've successfully logged in from.
  3722. * @return bool A block was spread
  3723. */
  3724. public function spreadAnyEditBlock() {
  3725. if ( $this->isLoggedIn() && $this->getBlock() ) {
  3726. return $this->spreadBlock();
  3727. }
  3728. return false;
  3729. }
  3730. /**
  3731. * If this (non-anonymous) user is blocked,
  3732. * block the IP address they've successfully logged in from.
  3733. * @return bool A block was spread
  3734. */
  3735. protected function spreadBlock() {
  3736. wfDebug( __METHOD__ . "()\n" );
  3737. $this->load();
  3738. if ( $this->mId == 0 ) {
  3739. return false;
  3740. }
  3741. $userblock = DatabaseBlock::newFromTarget( $this->getName() );
  3742. if ( !$userblock ) {
  3743. return false;
  3744. }
  3745. return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
  3746. }
  3747. /**
  3748. * Get whether the user is explicitly blocked from account creation.
  3749. * @return bool|AbstractBlock
  3750. */
  3751. public function isBlockedFromCreateAccount() {
  3752. $this->getBlockedStatus();
  3753. if ( $this->mBlock && $this->mBlock->appliesToRight( 'createaccount' ) ) {
  3754. return $this->mBlock;
  3755. }
  3756. # T15611: if the IP address the user is trying to create an account from is
  3757. # blocked with createaccount disabled, prevent new account creation there even
  3758. # when the user is logged in
  3759. if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
  3760. $this->mBlockedFromCreateAccount = DatabaseBlock::newFromTarget(
  3761. null, $this->getRequest()->getIP()
  3762. );
  3763. }
  3764. return $this->mBlockedFromCreateAccount instanceof AbstractBlock
  3765. && $this->mBlockedFromCreateAccount->appliesToRight( 'createaccount' )
  3766. ? $this->mBlockedFromCreateAccount
  3767. : false;
  3768. }
  3769. /**
  3770. * Get whether the user is blocked from using Special:Emailuser.
  3771. * @return bool
  3772. */
  3773. public function isBlockedFromEmailuser() {
  3774. $this->getBlockedStatus();
  3775. return $this->mBlock && $this->mBlock->appliesToRight( 'sendemail' );
  3776. }
  3777. /**
  3778. * Get whether the user is blocked from using Special:Upload
  3779. *
  3780. * @since 1.33
  3781. * @return bool
  3782. */
  3783. public function isBlockedFromUpload() {
  3784. $this->getBlockedStatus();
  3785. return $this->mBlock && $this->mBlock->appliesToRight( 'upload' );
  3786. }
  3787. /**
  3788. * Get whether the user is allowed to create an account.
  3789. * @return bool
  3790. */
  3791. public function isAllowedToCreateAccount() {
  3792. return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
  3793. }
  3794. /**
  3795. * Get this user's personal page title.
  3796. *
  3797. * @return Title User's personal page title
  3798. */
  3799. public function getUserPage() {
  3800. return Title::makeTitle( NS_USER, $this->getName() );
  3801. }
  3802. /**
  3803. * Get this user's talk page title.
  3804. *
  3805. * @return Title User's talk page title
  3806. */
  3807. public function getTalkPage() {
  3808. $title = $this->getUserPage();
  3809. return $title->getTalkPage();
  3810. }
  3811. /**
  3812. * Determine whether the user is a newbie. Newbies are either
  3813. * anonymous IPs, or the most recently created accounts.
  3814. * @return bool
  3815. */
  3816. public function isNewbie() {
  3817. return !$this->isAllowed( 'autoconfirmed' );
  3818. }
  3819. /**
  3820. * Check to see if the given clear-text password is one of the accepted passwords
  3821. * @deprecated since 1.27, use AuthManager instead
  3822. * @param string $password User password
  3823. * @return bool True if the given password is correct, otherwise False
  3824. */
  3825. public function checkPassword( $password ) {
  3826. wfDeprecated( __METHOD__, '1.27' );
  3827. $manager = AuthManager::singleton();
  3828. $reqs = AuthenticationRequest::loadRequestsFromSubmission(
  3829. $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN ),
  3830. [
  3831. 'username' => $this->getName(),
  3832. 'password' => $password,
  3833. ]
  3834. );
  3835. $res = $manager->beginAuthentication( $reqs, 'null:' );
  3836. switch ( $res->status ) {
  3837. case AuthenticationResponse::PASS:
  3838. return true;
  3839. case AuthenticationResponse::FAIL:
  3840. // Hope it's not a PreAuthenticationProvider that failed...
  3841. LoggerFactory::getInstance( 'authentication' )
  3842. ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() );
  3843. return false;
  3844. default:
  3845. throw new BadMethodCallException(
  3846. 'AuthManager returned a response unsupported by ' . __METHOD__
  3847. );
  3848. }
  3849. }
  3850. /**
  3851. * Check if the given clear-text password matches the temporary password
  3852. * sent by e-mail for password reset operations.
  3853. *
  3854. * @deprecated since 1.27, use AuthManager instead
  3855. * @param string $plaintext
  3856. * @return bool True if matches, false otherwise
  3857. */
  3858. public function checkTemporaryPassword( $plaintext ) {
  3859. wfDeprecated( __METHOD__, '1.27' );
  3860. // Can't check the temporary password individually.
  3861. return $this->checkPassword( $plaintext );
  3862. }
  3863. /**
  3864. * Initialize (if necessary) and return a session token value
  3865. * which can be used in edit forms to show that the user's
  3866. * login credentials aren't being hijacked with a foreign form
  3867. * submission.
  3868. *
  3869. * @since 1.27
  3870. * @param string|array $salt Array of Strings Optional function-specific data for hashing
  3871. * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
  3872. * @return MediaWiki\Session\Token The new edit token
  3873. */
  3874. public function getEditTokenObject( $salt = '', $request = null ) {
  3875. if ( $this->isAnon() ) {
  3876. return new LoggedOutEditToken();
  3877. }
  3878. if ( !$request ) {
  3879. $request = $this->getRequest();
  3880. }
  3881. return $request->getSession()->getToken( $salt );
  3882. }
  3883. /**
  3884. * Initialize (if necessary) and return a session token value
  3885. * which can be used in edit forms to show that the user's
  3886. * login credentials aren't being hijacked with a foreign form
  3887. * submission.
  3888. *
  3889. * The $salt for 'edit' and 'csrf' tokens is the default (empty string).
  3890. *
  3891. * @since 1.19
  3892. * @param string|array $salt Array of Strings Optional function-specific data for hashing
  3893. * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
  3894. * @return string The new edit token
  3895. */
  3896. public function getEditToken( $salt = '', $request = null ) {
  3897. return $this->getEditTokenObject( $salt, $request )->toString();
  3898. }
  3899. /**
  3900. * Check given value against the token value stored in the session.
  3901. * A match should confirm that the form was submitted from the
  3902. * user's own login session, not a form submission from a third-party
  3903. * site.
  3904. *
  3905. * @param string $val Input value to compare
  3906. * @param string|array $salt Optional function-specific data for hashing
  3907. * @param WebRequest|null $request Object to use or null to use $wgRequest
  3908. * @param int|null $maxage Fail tokens older than this, in seconds
  3909. * @return bool Whether the token matches
  3910. */
  3911. public function matchEditToken( $val, $salt = '', $request = null, $maxage = null ) {
  3912. return $this->getEditTokenObject( $salt, $request )->match( $val, $maxage );
  3913. }
  3914. /**
  3915. * Check given value against the token value stored in the session,
  3916. * ignoring the suffix.
  3917. *
  3918. * @param string $val Input value to compare
  3919. * @param string|array $salt Optional function-specific data for hashing
  3920. * @param WebRequest|null $request Object to use or null to use $wgRequest
  3921. * @param int|null $maxage Fail tokens older than this, in seconds
  3922. * @return bool Whether the token matches
  3923. */
  3924. public function matchEditTokenNoSuffix( $val, $salt = '', $request = null, $maxage = null ) {
  3925. $val = substr( $val, 0, strspn( $val, '0123456789abcdef' ) ) . Token::SUFFIX;
  3926. return $this->matchEditToken( $val, $salt, $request, $maxage );
  3927. }
  3928. /**
  3929. * Generate a new e-mail confirmation token and send a confirmation/invalidation
  3930. * mail to the user's given address.
  3931. *
  3932. * @param string $type Message to send, either "created", "changed" or "set"
  3933. * @return Status
  3934. */
  3935. public function sendConfirmationMail( $type = 'created' ) {
  3936. global $wgLang;
  3937. $expiration = null; // gets passed-by-ref and defined in next line.
  3938. $token = $this->confirmationToken( $expiration );
  3939. $url = $this->confirmationTokenUrl( $token );
  3940. $invalidateURL = $this->invalidationTokenUrl( $token );
  3941. $this->saveSettings();
  3942. if ( $type == 'created' || $type === false ) {
  3943. $message = 'confirmemail_body';
  3944. $type = 'created';
  3945. } elseif ( $type === true ) {
  3946. $message = 'confirmemail_body_changed';
  3947. $type = 'changed';
  3948. } else {
  3949. // Messages: confirmemail_body_changed, confirmemail_body_set
  3950. $message = 'confirmemail_body_' . $type;
  3951. }
  3952. $mail = [
  3953. 'subject' => wfMessage( 'confirmemail_subject' )->text(),
  3954. 'body' => wfMessage( $message,
  3955. $this->getRequest()->getIP(),
  3956. $this->getName(),
  3957. $url,
  3958. $wgLang->userTimeAndDate( $expiration, $this ),
  3959. $invalidateURL,
  3960. $wgLang->userDate( $expiration, $this ),
  3961. $wgLang->userTime( $expiration, $this ) )->text(),
  3962. 'from' => null,
  3963. 'replyTo' => null,
  3964. ];
  3965. $info = [
  3966. 'type' => $type,
  3967. 'ip' => $this->getRequest()->getIP(),
  3968. 'confirmURL' => $url,
  3969. 'invalidateURL' => $invalidateURL,
  3970. 'expiration' => $expiration
  3971. ];
  3972. Hooks::run( 'UserSendConfirmationMail', [ $this, &$mail, $info ] );
  3973. return $this->sendMail( $mail['subject'], $mail['body'], $mail['from'], $mail['replyTo'] );
  3974. }
  3975. /**
  3976. * Send an e-mail to this user's account. Does not check for
  3977. * confirmed status or validity.
  3978. *
  3979. * @param string $subject Message subject
  3980. * @param string $body Message body
  3981. * @param User|null $from Optional sending user; if unspecified, default
  3982. * $wgPasswordSender will be used.
  3983. * @param MailAddress|null $replyto Reply-To address
  3984. * @return Status
  3985. */
  3986. public function sendMail( $subject, $body, $from = null, $replyto = null ) {
  3987. global $wgPasswordSender;
  3988. if ( $from instanceof User ) {
  3989. $sender = MailAddress::newFromUser( $from );
  3990. } else {
  3991. $sender = new MailAddress( $wgPasswordSender,
  3992. wfMessage( 'emailsender' )->inContentLanguage()->text() );
  3993. }
  3994. $to = MailAddress::newFromUser( $this );
  3995. return UserMailer::send( $to, $sender, $subject, $body, [
  3996. 'replyTo' => $replyto,
  3997. ] );
  3998. }
  3999. /**
  4000. * Generate, store, and return a new e-mail confirmation code.
  4001. * A hash (unsalted, since it's used as a key) is stored.
  4002. *
  4003. * @note Call saveSettings() after calling this function to commit
  4004. * this change to the database.
  4005. *
  4006. * @param string &$expiration Accepts the expiration time
  4007. * @return string New token
  4008. */
  4009. protected function confirmationToken( &$expiration ) {
  4010. global $wgUserEmailConfirmationTokenExpiry;
  4011. $now = time();
  4012. $expires = $now + $wgUserEmailConfirmationTokenExpiry;
  4013. $expiration = wfTimestamp( TS_MW, $expires );
  4014. $this->load();
  4015. $token = MWCryptRand::generateHex( 32 );
  4016. $hash = md5( $token );
  4017. $this->mEmailToken = $hash;
  4018. $this->mEmailTokenExpires = $expiration;
  4019. return $token;
  4020. }
  4021. /**
  4022. * Return a URL the user can use to confirm their email address.
  4023. * @param string $token Accepts the email confirmation token
  4024. * @return string New token URL
  4025. */
  4026. protected function confirmationTokenUrl( $token ) {
  4027. return $this->getTokenUrl( 'ConfirmEmail', $token );
  4028. }
  4029. /**
  4030. * Return a URL the user can use to invalidate their email address.
  4031. * @param string $token Accepts the email confirmation token
  4032. * @return string New token URL
  4033. */
  4034. protected function invalidationTokenUrl( $token ) {
  4035. return $this->getTokenUrl( 'InvalidateEmail', $token );
  4036. }
  4037. /**
  4038. * Internal function to format the e-mail validation/invalidation URLs.
  4039. * This uses a quickie hack to use the
  4040. * hardcoded English names of the Special: pages, for ASCII safety.
  4041. *
  4042. * @note Since these URLs get dropped directly into emails, using the
  4043. * short English names avoids insanely long URL-encoded links, which
  4044. * also sometimes can get corrupted in some browsers/mailers
  4045. * (T8957 with Gmail and Internet Explorer).
  4046. *
  4047. * @param string $page Special page
  4048. * @param string $token
  4049. * @return string Formatted URL
  4050. */
  4051. protected function getTokenUrl( $page, $token ) {
  4052. // Hack to bypass localization of 'Special:'
  4053. $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
  4054. return $title->getCanonicalURL();
  4055. }
  4056. /**
  4057. * Mark the e-mail address confirmed.
  4058. *
  4059. * @note Call saveSettings() after calling this function to commit the change.
  4060. *
  4061. * @return bool
  4062. */
  4063. public function confirmEmail() {
  4064. // Check if it's already confirmed, so we don't touch the database
  4065. // and fire the ConfirmEmailComplete hook on redundant confirmations.
  4066. if ( !$this->isEmailConfirmed() ) {
  4067. $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
  4068. Hooks::run( 'ConfirmEmailComplete', [ $this ] );
  4069. }
  4070. return true;
  4071. }
  4072. /**
  4073. * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
  4074. * address if it was already confirmed.
  4075. *
  4076. * @note Call saveSettings() after calling this function to commit the change.
  4077. * @return bool Returns true
  4078. */
  4079. public function invalidateEmail() {
  4080. $this->load();
  4081. $this->mEmailToken = null;
  4082. $this->mEmailTokenExpires = null;
  4083. $this->setEmailAuthenticationTimestamp( null );
  4084. $this->mEmail = '';
  4085. Hooks::run( 'InvalidateEmailComplete', [ $this ] );
  4086. return true;
  4087. }
  4088. /**
  4089. * Set the e-mail authentication timestamp.
  4090. * @param string $timestamp TS_MW timestamp
  4091. */
  4092. public function setEmailAuthenticationTimestamp( $timestamp ) {
  4093. $this->load();
  4094. $this->mEmailAuthenticated = $timestamp;
  4095. Hooks::run( 'UserSetEmailAuthenticationTimestamp', [ $this, &$this->mEmailAuthenticated ] );
  4096. }
  4097. /**
  4098. * Is this user allowed to send e-mails within limits of current
  4099. * site configuration?
  4100. * @return bool
  4101. */
  4102. public function canSendEmail() {
  4103. global $wgEnableEmail, $wgEnableUserEmail;
  4104. if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
  4105. return false;
  4106. }
  4107. $canSend = $this->isEmailConfirmed();
  4108. // Avoid PHP 7.1 warning of passing $this by reference
  4109. $user = $this;
  4110. Hooks::run( 'UserCanSendEmail', [ &$user, &$canSend ] );
  4111. return $canSend;
  4112. }
  4113. /**
  4114. * Is this user allowed to receive e-mails within limits of current
  4115. * site configuration?
  4116. * @return bool
  4117. */
  4118. public function canReceiveEmail() {
  4119. return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
  4120. }
  4121. /**
  4122. * Is this user's e-mail address valid-looking and confirmed within
  4123. * limits of the current site configuration?
  4124. *
  4125. * @note If $wgEmailAuthentication is on, this may require the user to have
  4126. * confirmed their address by returning a code or using a password
  4127. * sent to the address from the wiki.
  4128. *
  4129. * @return bool
  4130. */
  4131. public function isEmailConfirmed() {
  4132. global $wgEmailAuthentication;
  4133. $this->load();
  4134. // Avoid PHP 7.1 warning of passing $this by reference
  4135. $user = $this;
  4136. $confirmed = true;
  4137. if ( Hooks::run( 'EmailConfirmed', [ &$user, &$confirmed ] ) ) {
  4138. if ( $this->isAnon() ) {
  4139. return false;
  4140. }
  4141. if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
  4142. return false;
  4143. }
  4144. if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
  4145. return false;
  4146. }
  4147. return true;
  4148. }
  4149. return $confirmed;
  4150. }
  4151. /**
  4152. * Check whether there is an outstanding request for e-mail confirmation.
  4153. * @return bool
  4154. */
  4155. public function isEmailConfirmationPending() {
  4156. global $wgEmailAuthentication;
  4157. return $wgEmailAuthentication &&
  4158. !$this->isEmailConfirmed() &&
  4159. $this->mEmailToken &&
  4160. $this->mEmailTokenExpires > wfTimestamp();
  4161. }
  4162. /**
  4163. * Get the timestamp of account creation.
  4164. *
  4165. * @return string|bool|null Timestamp of account creation, false for
  4166. * non-existent/anonymous user accounts, or null if existing account
  4167. * but information is not in database.
  4168. */
  4169. public function getRegistration() {
  4170. if ( $this->isAnon() ) {
  4171. return false;
  4172. }
  4173. $this->load();
  4174. return $this->mRegistration;
  4175. }
  4176. /**
  4177. * Get the timestamp of the first edit
  4178. *
  4179. * @return string|bool Timestamp of first edit, or false for
  4180. * non-existent/anonymous user accounts.
  4181. */
  4182. public function getFirstEditTimestamp() {
  4183. return $this->getEditTimestamp( true );
  4184. }
  4185. /**
  4186. * Get the timestamp of the latest edit
  4187. *
  4188. * @since 1.33
  4189. * @return string|bool Timestamp of first edit, or false for
  4190. * non-existent/anonymous user accounts.
  4191. */
  4192. public function getLatestEditTimestamp() {
  4193. return $this->getEditTimestamp( false );
  4194. }
  4195. /**
  4196. * Get the timestamp of the first or latest edit
  4197. *
  4198. * @param bool $first True for the first edit, false for the latest one
  4199. * @return string|bool Timestamp of first or latest edit, or false for
  4200. * non-existent/anonymous user accounts.
  4201. */
  4202. private function getEditTimestamp( $first ) {
  4203. if ( $this->getId() == 0 ) {
  4204. return false; // anons
  4205. }
  4206. $dbr = wfGetDB( DB_REPLICA );
  4207. $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
  4208. $tsField = isset( $actorWhere['tables']['temp_rev_user'] )
  4209. ? 'revactor_timestamp' : 'rev_timestamp';
  4210. $sortOrder = $first ? 'ASC' : 'DESC';
  4211. $time = $dbr->selectField(
  4212. [ 'revision' ] + $actorWhere['tables'],
  4213. $tsField,
  4214. [ $actorWhere['conds'] ],
  4215. __METHOD__,
  4216. [ 'ORDER BY' => "$tsField $sortOrder" ],
  4217. $actorWhere['joins']
  4218. );
  4219. if ( !$time ) {
  4220. return false; // no edits
  4221. }
  4222. return wfTimestamp( TS_MW, $time );
  4223. }
  4224. /**
  4225. * Get the permissions associated with a given list of groups
  4226. *
  4227. * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
  4228. * ->getGroupPermissions() instead
  4229. *
  4230. * @param array $groups Array of Strings List of internal group names
  4231. * @return array Array of Strings List of permission key names for given groups combined
  4232. */
  4233. public static function getGroupPermissions( $groups ) {
  4234. return MediaWikiServices::getInstance()->getPermissionManager()->getGroupPermissions( $groups );
  4235. }
  4236. /**
  4237. * Get all the groups who have a given permission
  4238. *
  4239. * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
  4240. * ->getGroupsWithPermission() instead
  4241. *
  4242. * @param string $role Role to check
  4243. * @return array Array of Strings List of internal group names with the given permission
  4244. */
  4245. public static function getGroupsWithPermission( $role ) {
  4246. return MediaWikiServices::getInstance()->getPermissionManager()->getGroupsWithPermission( $role );
  4247. }
  4248. /**
  4249. * Check, if the given group has the given permission
  4250. *
  4251. * If you're wanting to check whether all users have a permission, use
  4252. * User::isEveryoneAllowed() instead. That properly checks if it's revoked
  4253. * from anyone.
  4254. *
  4255. * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
  4256. * ->groupHasPermission(..) instead
  4257. *
  4258. * @since 1.21
  4259. * @param string $group Group to check
  4260. * @param string $role Role to check
  4261. * @return bool
  4262. */
  4263. public static function groupHasPermission( $group, $role ) {
  4264. return MediaWikiServices::getInstance()->getPermissionManager()
  4265. ->groupHasPermission( $group, $role );
  4266. }
  4267. /**
  4268. * Check if all users may be assumed to have the given permission
  4269. *
  4270. * We generally assume so if the right is granted to '*' and isn't revoked
  4271. * on any group. It doesn't attempt to take grants or other extension
  4272. * limitations on rights into account in the general case, though, as that
  4273. * would require it to always return false and defeat the purpose.
  4274. * Specifically, session-based rights restrictions (such as OAuth or bot
  4275. * passwords) are applied based on the current session.
  4276. *
  4277. * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
  4278. * ->isEveryoneAllowed() instead
  4279. *
  4280. * @param string $right Right to check
  4281. *
  4282. * @return bool
  4283. * @since 1.22
  4284. */
  4285. public static function isEveryoneAllowed( $right ) {
  4286. return MediaWikiServices::getInstance()->getPermissionManager()->isEveryoneAllowed( $right );
  4287. }
  4288. /**
  4289. * Return the set of defined explicit groups.
  4290. * The implicit groups (by default *, 'user' and 'autoconfirmed')
  4291. * are not included, as they are defined automatically, not in the database.
  4292. * @return array Array of internal group names
  4293. */
  4294. public static function getAllGroups() {
  4295. global $wgGroupPermissions, $wgRevokePermissions;
  4296. return array_values( array_diff(
  4297. array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
  4298. self::getImplicitGroups()
  4299. ) );
  4300. }
  4301. /**
  4302. * Get a list of all available permissions.
  4303. *
  4304. * @deprecated since 1.34, use PermissionManager::getAllPermissions() instead
  4305. *
  4306. * @return string[] Array of permission names
  4307. */
  4308. public static function getAllRights() {
  4309. return MediaWikiServices::getInstance()->getPermissionManager()->getAllPermissions();
  4310. }
  4311. /**
  4312. * Get a list of implicit groups
  4313. * TODO: Should we deprecate this? It's trivial, but we don't want to encourage use of globals.
  4314. *
  4315. * @return array Array of Strings Array of internal group names
  4316. */
  4317. public static function getImplicitGroups() {
  4318. global $wgImplicitGroups;
  4319. return $wgImplicitGroups;
  4320. }
  4321. /**
  4322. * Returns an array of the groups that a particular group can add/remove.
  4323. *
  4324. * @param string $group The group to check for whether it can add/remove
  4325. * @return array [ 'add' => [ addablegroups ],
  4326. * 'remove' => [ removablegroups ],
  4327. * 'add-self' => [ addablegroups to self ],
  4328. * 'remove-self' => [ removable groups from self ] ]
  4329. */
  4330. public static function changeableByGroup( $group ) {
  4331. global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
  4332. $groups = [
  4333. 'add' => [],
  4334. 'remove' => [],
  4335. 'add-self' => [],
  4336. 'remove-self' => []
  4337. ];
  4338. if ( empty( $wgAddGroups[$group] ) ) {
  4339. // Don't add anything to $groups
  4340. } elseif ( $wgAddGroups[$group] === true ) {
  4341. // You get everything
  4342. $groups['add'] = self::getAllGroups();
  4343. } elseif ( is_array( $wgAddGroups[$group] ) ) {
  4344. $groups['add'] = $wgAddGroups[$group];
  4345. }
  4346. // Same thing for remove
  4347. if ( empty( $wgRemoveGroups[$group] ) ) {
  4348. // Do nothing
  4349. } elseif ( $wgRemoveGroups[$group] === true ) {
  4350. $groups['remove'] = self::getAllGroups();
  4351. } elseif ( is_array( $wgRemoveGroups[$group] ) ) {
  4352. $groups['remove'] = $wgRemoveGroups[$group];
  4353. }
  4354. // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
  4355. if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) {
  4356. foreach ( $wgGroupsAddToSelf as $key => $value ) {
  4357. if ( is_int( $key ) ) {
  4358. $wgGroupsAddToSelf['user'][] = $value;
  4359. }
  4360. }
  4361. }
  4362. if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) {
  4363. foreach ( $wgGroupsRemoveFromSelf as $key => $value ) {
  4364. if ( is_int( $key ) ) {
  4365. $wgGroupsRemoveFromSelf['user'][] = $value;
  4366. }
  4367. }
  4368. }
  4369. // Now figure out what groups the user can add to him/herself
  4370. if ( empty( $wgGroupsAddToSelf[$group] ) ) {
  4371. // Do nothing
  4372. } elseif ( $wgGroupsAddToSelf[$group] === true ) {
  4373. // No idea WHY this would be used, but it's there
  4374. $groups['add-self'] = self::getAllGroups();
  4375. } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
  4376. $groups['add-self'] = $wgGroupsAddToSelf[$group];
  4377. }
  4378. if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
  4379. // Do nothing
  4380. } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
  4381. $groups['remove-self'] = self::getAllGroups();
  4382. } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
  4383. $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
  4384. }
  4385. return $groups;
  4386. }
  4387. /**
  4388. * Returns an array of groups that this user can add and remove
  4389. * @return array [ 'add' => [ addablegroups ],
  4390. * 'remove' => [ removablegroups ],
  4391. * 'add-self' => [ addablegroups to self ],
  4392. * 'remove-self' => [ removable groups from self ] ]
  4393. */
  4394. public function changeableGroups() {
  4395. if ( $this->isAllowed( 'userrights' ) ) {
  4396. // This group gives the right to modify everything (reverse-
  4397. // compatibility with old "userrights lets you change
  4398. // everything")
  4399. // Using array_merge to make the groups reindexed
  4400. $all = array_merge( self::getAllGroups() );
  4401. return [
  4402. 'add' => $all,
  4403. 'remove' => $all,
  4404. 'add-self' => [],
  4405. 'remove-self' => []
  4406. ];
  4407. }
  4408. // Okay, it's not so simple, we will have to go through the arrays
  4409. $groups = [
  4410. 'add' => [],
  4411. 'remove' => [],
  4412. 'add-self' => [],
  4413. 'remove-self' => []
  4414. ];
  4415. $addergroups = $this->getEffectiveGroups();
  4416. foreach ( $addergroups as $addergroup ) {
  4417. $groups = array_merge_recursive(
  4418. $groups, $this->changeableByGroup( $addergroup )
  4419. );
  4420. $groups['add'] = array_unique( $groups['add'] );
  4421. $groups['remove'] = array_unique( $groups['remove'] );
  4422. $groups['add-self'] = array_unique( $groups['add-self'] );
  4423. $groups['remove-self'] = array_unique( $groups['remove-self'] );
  4424. }
  4425. return $groups;
  4426. }
  4427. /**
  4428. * Schedule a deferred update to update the user's edit count
  4429. */
  4430. public function incEditCount() {
  4431. if ( $this->isAnon() ) {
  4432. return; // sanity
  4433. }
  4434. DeferredUpdates::addUpdate(
  4435. new UserEditCountUpdate( $this, 1 ),
  4436. DeferredUpdates::POSTSEND
  4437. );
  4438. }
  4439. /**
  4440. * This method should not be called outside User/UserEditCountUpdate
  4441. *
  4442. * @param int $count
  4443. */
  4444. public function setEditCountInternal( $count ) {
  4445. $this->mEditCount = $count;
  4446. }
  4447. /**
  4448. * Initialize user_editcount from data out of the revision table
  4449. *
  4450. * @internal This method should not be called outside User/UserEditCountUpdate
  4451. * @param IDatabase $dbr Replica database
  4452. * @return int Number of edits
  4453. */
  4454. public function initEditCountInternal( IDatabase $dbr ) {
  4455. // Pull from a replica DB to be less cruel to servers
  4456. // Accuracy isn't the point anyway here
  4457. $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
  4458. $count = (int)$dbr->selectField(
  4459. [ 'revision' ] + $actorWhere['tables'],
  4460. 'COUNT(*)',
  4461. [ $actorWhere['conds'] ],
  4462. __METHOD__,
  4463. [],
  4464. $actorWhere['joins']
  4465. );
  4466. $dbw = wfGetDB( DB_MASTER );
  4467. $dbw->update(
  4468. 'user',
  4469. [ 'user_editcount' => $count ],
  4470. [
  4471. 'user_id' => $this->getId(),
  4472. 'user_editcount IS NULL OR user_editcount < ' . (int)$count
  4473. ],
  4474. __METHOD__
  4475. );
  4476. return $count;
  4477. }
  4478. /**
  4479. * Get the description of a given right
  4480. *
  4481. * @since 1.29
  4482. * @param string $right Right to query
  4483. * @return string Localized description of the right
  4484. */
  4485. public static function getRightDescription( $right ) {
  4486. $key = "right-$right";
  4487. $msg = wfMessage( $key );
  4488. return $msg->isDisabled() ? $right : $msg->text();
  4489. }
  4490. /**
  4491. * Get the name of a given grant
  4492. *
  4493. * @since 1.29
  4494. * @param string $grant Grant to query
  4495. * @return string Localized name of the grant
  4496. */
  4497. public static function getGrantName( $grant ) {
  4498. $key = "grant-$grant";
  4499. $msg = wfMessage( $key );
  4500. return $msg->isDisabled() ? $grant : $msg->text();
  4501. }
  4502. /**
  4503. * Add a newuser log entry for this user.
  4504. * Before 1.19 the return value was always true.
  4505. *
  4506. * @deprecated since 1.27, AuthManager handles logging
  4507. * @param string|bool $action Account creation type.
  4508. * - String, one of the following values:
  4509. * - 'create' for an anonymous user creating an account for himself.
  4510. * This will force the action's performer to be the created user itself,
  4511. * no matter the value of $wgUser
  4512. * - 'create2' for a logged in user creating an account for someone else
  4513. * - 'byemail' when the created user will receive its password by e-mail
  4514. * - 'autocreate' when the user is automatically created (such as by CentralAuth).
  4515. * - Boolean means whether the account was created by e-mail (deprecated):
  4516. * - true will be converted to 'byemail'
  4517. * - false will be converted to 'create' if this object is the same as
  4518. * $wgUser and to 'create2' otherwise
  4519. * @param string $reason User supplied reason
  4520. * @return bool true
  4521. */
  4522. public function addNewUserLogEntry( $action = false, $reason = '' ) {
  4523. return true; // disabled
  4524. }
  4525. /**
  4526. * Add an autocreate newuser log entry for this user
  4527. * Used by things like CentralAuth and perhaps other authplugins.
  4528. * Consider calling addNewUserLogEntry() directly instead.
  4529. *
  4530. * @deprecated since 1.27, AuthManager handles logging
  4531. * @return bool
  4532. */
  4533. public function addNewUserLogEntryAutoCreate() {
  4534. wfDeprecated( __METHOD__, '1.27' );
  4535. $this->addNewUserLogEntry( 'autocreate' );
  4536. return true;
  4537. }
  4538. /**
  4539. * Load the user options either from cache, the database or an array
  4540. *
  4541. * @param array|null $data Rows for the current user out of the user_properties table
  4542. */
  4543. protected function loadOptions( $data = null ) {
  4544. $this->load();
  4545. if ( $this->mOptionsLoaded ) {
  4546. return;
  4547. }
  4548. $this->mOptions = self::getDefaultOptions();
  4549. if ( !$this->getId() ) {
  4550. // For unlogged-in users, load language/variant options from request.
  4551. // There's no need to do it for logged-in users: they can set preferences,
  4552. // and handling of page content is done by $pageLang->getPreferredVariant() and such,
  4553. // so don't override user's choice (especially when the user chooses site default).
  4554. $variant = MediaWikiServices::getInstance()->getContentLanguage()->getDefaultVariant();
  4555. $this->mOptions['variant'] = $variant;
  4556. $this->mOptions['language'] = $variant;
  4557. $this->mOptionsLoaded = true;
  4558. return;
  4559. }
  4560. // Maybe load from the object
  4561. if ( !is_null( $this->mOptionOverrides ) ) {
  4562. wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
  4563. foreach ( $this->mOptionOverrides as $key => $value ) {
  4564. $this->mOptions[$key] = $value;
  4565. }
  4566. } else {
  4567. if ( !is_array( $data ) ) {
  4568. wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
  4569. // Load from database
  4570. $dbr = ( $this->queryFlagsUsed & self::READ_LATEST )
  4571. ? wfGetDB( DB_MASTER )
  4572. : wfGetDB( DB_REPLICA );
  4573. $res = $dbr->select(
  4574. 'user_properties',
  4575. [ 'up_property', 'up_value' ],
  4576. [ 'up_user' => $this->getId() ],
  4577. __METHOD__
  4578. );
  4579. $this->mOptionOverrides = [];
  4580. $data = [];
  4581. foreach ( $res as $row ) {
  4582. // Convert '0' to 0. PHP's boolean conversion considers them both
  4583. // false, but e.g. JavaScript considers the former as true.
  4584. // @todo: T54542 Somehow determine the desired type (string/int/bool)
  4585. // and convert all values here.
  4586. if ( $row->up_value === '0' ) {
  4587. $row->up_value = 0;
  4588. }
  4589. $data[$row->up_property] = $row->up_value;
  4590. }
  4591. }
  4592. foreach ( $data as $property => $value ) {
  4593. $this->mOptionOverrides[$property] = $value;
  4594. $this->mOptions[$property] = $value;
  4595. }
  4596. }
  4597. // Replace deprecated language codes
  4598. $this->mOptions['language'] = LanguageCode::replaceDeprecatedCodes(
  4599. $this->mOptions['language']
  4600. );
  4601. $this->mOptionsLoaded = true;
  4602. Hooks::run( 'UserLoadOptions', [ $this, &$this->mOptions ] );
  4603. }
  4604. /**
  4605. * Saves the non-default options for this user, as previously set e.g. via
  4606. * setOption(), in the database's "user_properties" (preferences) table.
  4607. * Usually used via saveSettings().
  4608. */
  4609. protected function saveOptions() {
  4610. $this->loadOptions();
  4611. // Not using getOptions(), to keep hidden preferences in database
  4612. $saveOptions = $this->mOptions;
  4613. // Allow hooks to abort, for instance to save to a global profile.
  4614. // Reset options to default state before saving.
  4615. if ( !Hooks::run( 'UserSaveOptions', [ $this, &$saveOptions ] ) ) {
  4616. return;
  4617. }
  4618. $userId = $this->getId();
  4619. $insert_rows = []; // all the new preference rows
  4620. foreach ( $saveOptions as $key => $value ) {
  4621. // Don't bother storing default values
  4622. $defaultOption = self::getDefaultOption( $key );
  4623. if ( ( $defaultOption === null && $value !== false && $value !== null )
  4624. || $value != $defaultOption
  4625. ) {
  4626. $insert_rows[] = [
  4627. 'up_user' => $userId,
  4628. 'up_property' => $key,
  4629. 'up_value' => $value,
  4630. ];
  4631. }
  4632. }
  4633. $dbw = wfGetDB( DB_MASTER );
  4634. $res = $dbw->select( 'user_properties',
  4635. [ 'up_property', 'up_value' ], [ 'up_user' => $userId ], __METHOD__ );
  4636. // Find prior rows that need to be removed or updated. These rows will
  4637. // all be deleted (the latter so that INSERT IGNORE applies the new values).
  4638. $keysDelete = [];
  4639. foreach ( $res as $row ) {
  4640. if ( !isset( $saveOptions[$row->up_property] )
  4641. || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0
  4642. ) {
  4643. $keysDelete[] = $row->up_property;
  4644. }
  4645. }
  4646. if ( count( $keysDelete ) ) {
  4647. // Do the DELETE by PRIMARY KEY for prior rows.
  4648. // In the past a very large portion of calls to this function are for setting
  4649. // 'rememberpassword' for new accounts (a preference that has since been removed).
  4650. // Doing a blanket per-user DELETE for new accounts with no rows in the table
  4651. // caused gap locks on [max user ID,+infinity) which caused high contention since
  4652. // updates would pile up on each other as they are for higher (newer) user IDs.
  4653. // It might not be necessary these days, but it shouldn't hurt either.
  4654. $dbw->delete( 'user_properties',
  4655. [ 'up_user' => $userId, 'up_property' => $keysDelete ], __METHOD__ );
  4656. }
  4657. // Insert the new preference rows
  4658. $dbw->insert( 'user_properties', $insert_rows, __METHOD__, [ 'IGNORE' ] );
  4659. }
  4660. /**
  4661. * Return the list of user fields that should be selected to create
  4662. * a new user object.
  4663. * @deprecated since 1.31, use self::getQueryInfo() instead.
  4664. * @return array
  4665. */
  4666. public static function selectFields() {
  4667. wfDeprecated( __METHOD__, '1.31' );
  4668. return [
  4669. 'user_id',
  4670. 'user_name',
  4671. 'user_real_name',
  4672. 'user_email',
  4673. 'user_touched',
  4674. 'user_token',
  4675. 'user_email_authenticated',
  4676. 'user_email_token',
  4677. 'user_email_token_expires',
  4678. 'user_registration',
  4679. 'user_editcount',
  4680. ];
  4681. }
  4682. /**
  4683. * Return the tables, fields, and join conditions to be selected to create
  4684. * a new user object.
  4685. * @since 1.31
  4686. * @return array With three keys:
  4687. * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
  4688. * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
  4689. * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
  4690. */
  4691. public static function getQueryInfo() {
  4692. $ret = [
  4693. 'tables' => [ 'user', 'user_actor' => 'actor' ],
  4694. 'fields' => [
  4695. 'user_id',
  4696. 'user_name',
  4697. 'user_real_name',
  4698. 'user_email',
  4699. 'user_touched',
  4700. 'user_token',
  4701. 'user_email_authenticated',
  4702. 'user_email_token',
  4703. 'user_email_token_expires',
  4704. 'user_registration',
  4705. 'user_editcount',
  4706. 'user_actor.actor_id',
  4707. ],
  4708. 'joins' => [
  4709. 'user_actor' => [ 'JOIN', 'user_actor.actor_user = user_id' ],
  4710. ],
  4711. ];
  4712. return $ret;
  4713. }
  4714. /**
  4715. * Factory function for fatal permission-denied errors
  4716. *
  4717. * @since 1.22
  4718. * @param string $permission User right required
  4719. * @return Status
  4720. */
  4721. static function newFatalPermissionDeniedStatus( $permission ) {
  4722. global $wgLang;
  4723. $groups = [];
  4724. foreach ( MediaWikiServices::getInstance()
  4725. ->getPermissionManager()
  4726. ->getGroupsWithPermission( $permission ) as $group ) {
  4727. $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' );
  4728. }
  4729. if ( $groups ) {
  4730. return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
  4731. }
  4732. return Status::newFatal( 'badaccess-group0' );
  4733. }
  4734. /**
  4735. * Get a new instance of this user that was loaded from the master via a locking read
  4736. *
  4737. * Use this instead of the main context User when updating that user. This avoids races
  4738. * where that user was loaded from a replica DB or even the master but without proper locks.
  4739. *
  4740. * @return User|null Returns null if the user was not found in the DB
  4741. * @since 1.27
  4742. */
  4743. public function getInstanceForUpdate() {
  4744. if ( !$this->getId() ) {
  4745. return null; // anon
  4746. }
  4747. $user = self::newFromId( $this->getId() );
  4748. if ( !$user->loadFromId( self::READ_EXCLUSIVE ) ) {
  4749. return null;
  4750. }
  4751. return $user;
  4752. }
  4753. /**
  4754. * Checks if two user objects point to the same user.
  4755. *
  4756. * @since 1.25 ; takes a UserIdentity instead of a User since 1.32
  4757. * @param UserIdentity $user
  4758. * @return bool
  4759. */
  4760. public function equals( UserIdentity $user ) {
  4761. // XXX it's not clear whether central ID providers are supposed to obey this
  4762. return $this->getName() === $user->getName();
  4763. }
  4764. /**
  4765. * Checks if usertalk is allowed
  4766. *
  4767. * @return bool
  4768. */
  4769. public function isAllowUsertalk() {
  4770. return $this->mAllowUsertalk;
  4771. }
  4772. }