Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F2751330
AbuseLogger.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
12 KB
Referenced Files
None
Subscribers
None
AbuseLogger.php
View Options
<?php
namespace
MediaWiki\Extension\AbuseFilter
;
use
InvalidArgumentException
;
use
MediaWiki\CheckUser\Hooks
;
use
MediaWiki\Config\ServiceOptions
;
use
MediaWiki\Deferred\DeferredUpdates
;
use
MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerFactory
;
use
MediaWiki\Extension\AbuseFilter\Variables\VariableHolder
;
use
MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore
;
use
MediaWiki\Extension\AbuseFilter\Variables\VariablesManager
;
use
MediaWiki\Logging\ManualLogEntry
;
use
MediaWiki\Registration\ExtensionRegistry
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\User
;
use
MediaWiki\User\UserIdentityValue
;
use
Profiler
;
use
Wikimedia\Rdbms\IDatabase
;
use
Wikimedia\Rdbms\LBFactory
;
use
Wikimedia\ScopedCallback
;
class
AbuseLogger
{
private
Title
$title
;
private
User
$user
;
private
VariableHolder
$vars
;
private
string
$action
;
/** @var string[][] A list of variable dumps generated by {@link self::storeVarDump} for de-duplication. */
private
array
$varDumps
=
[];
private
CentralDBManager
$centralDBManager
;
private
FilterLookup
$filterLookup
;
private
VariablesBlobStore
$varBlobStore
;
private
VariablesManager
$varManager
;
private
EditRevUpdater
$editRevUpdater
;
private
LBFactory
$lbFactory
;
private
ServiceOptions
$options
;
private
RuleCheckerFactory
$ruleCheckerFactory
;
private
AbuseFilterPermissionManager
$afPermissionManager
;
private
string
$wikiID
;
private
string
$requestIP
;
/**
* @param CentralDBManager $centralDBManager
* @param FilterLookup $filterLookup
* @param VariablesBlobStore $varBlobStore
* @param VariablesManager $varManager
* @param EditRevUpdater $editRevUpdater
* @param LBFactory $lbFactory
* @param RuleCheckerFactory $ruleCheckerFactory
* @param AbuseFilterPermissionManager $afPermissionManager
* @param ServiceOptions $options
* @param string $wikiID
* @param string $requestIP
* @param Title $title
* @param User $user
* @param VariableHolder $vars
* @internal Use {@link AbuseLoggerFactory::newLogger} instead
*/
public
function
__construct
(
CentralDBManager
$centralDBManager
,
FilterLookup
$filterLookup
,
VariablesBlobStore
$varBlobStore
,
VariablesManager
$varManager
,
EditRevUpdater
$editRevUpdater
,
LBFactory
$lbFactory
,
RuleCheckerFactory
$ruleCheckerFactory
,
AbuseFilterPermissionManager
$afPermissionManager
,
ServiceOptions
$options
,
string
$wikiID
,
string
$requestIP
,
Title
$title
,
User
$user
,
VariableHolder
$vars
)
{
if
(
!
$vars
->
varIsSet
(
'action'
)
)
{
throw
new
InvalidArgumentException
(
"The 'action' variable is not set."
);
}
$this
->
centralDBManager
=
$centralDBManager
;
$this
->
filterLookup
=
$filterLookup
;
$this
->
varBlobStore
=
$varBlobStore
;
$this
->
varManager
=
$varManager
;
$this
->
editRevUpdater
=
$editRevUpdater
;
$this
->
lbFactory
=
$lbFactory
;
$this
->
afPermissionManager
=
$afPermissionManager
;
$this
->
ruleCheckerFactory
=
$ruleCheckerFactory
;
$this
->
options
=
$options
;
$this
->
wikiID
=
$wikiID
;
$this
->
requestIP
=
$requestIP
;
$this
->
title
=
$title
;
$this
->
user
=
$user
;
$this
->
vars
=
$vars
;
$this
->
action
=
$vars
->
getComputedVariable
(
'action'
)->
toString
();
}
/**
* Create and publish log entries for taken actions
*
* @param array[] $actionsTaken
* @return array Shape is [ 'local' => int[], 'global' => int[] ], IDs of logged filters
* @phan-return array{local:int[],global:int[]}
*/
public
function
addLogEntries
(
array
$actionsTaken
):
array
{
$dbw
=
$this
->
lbFactory
->
getPrimaryDatabase
();
$logTemplate
=
$this
->
buildLogTemplate
();
$centralLogTemplate
=
[
'afl_wiki'
=>
$this
->
wikiID
,
];
$logRows
=
[];
$centralLogRows
=
[];
$loggedLocalFilters
=
[];
$loggedGlobalFilters
=
[];
foreach
(
$actionsTaken
as
$filter
=>
$actions
)
{
[
$filterID
,
$global
]
=
GlobalNameUtils
::
splitGlobalName
(
$filter
);
$thisLog
=
$logTemplate
;
$thisLog
[
'afl_filter_id'
]
=
$filterID
;
$thisLog
[
'afl_global'
]
=
(
int
)
$global
;
$thisLog
[
'afl_actions'
]
=
implode
(
','
,
$actions
);
// Don't log if we were only throttling.
// TODO This check should be removed or rewritten using Consequence objects
if
(
$thisLog
[
'afl_actions'
]
!==
'throttle'
)
{
$logRows
[]
=
$thisLog
;
// Global logging
if
(
$global
)
{
$centralLog
=
$thisLog
+
$centralLogTemplate
;
$centralLog
[
'afl_filter_id'
]
=
$filterID
;
$centralLog
[
'afl_global'
]
=
0
;
$centralLog
[
'afl_title'
]
=
$this
->
title
->
getPrefixedText
();
$centralLog
[
'afl_namespace'
]
=
0
;
$centralLogRows
[]
=
$centralLog
;
$loggedGlobalFilters
[]
=
$filterID
;
}
else
{
$loggedLocalFilters
[]
=
$filterID
;
}
}
}
if
(
!
count
(
$logRows
)
)
{
return
[
'local'
=>
[],
'global'
=>
[]
];
}
$localLogIDs
=
$this
->
insertLocalLogEntries
(
$logRows
,
$dbw
);
$globalLogIDs
=
[];
if
(
count
(
$loggedGlobalFilters
)
)
{
$fdb
=
$this
->
centralDBManager
->
getConnection
(
DB_PRIMARY
);
$globalLogIDs
=
$this
->
insertCentralLogEntries
(
$centralLogRows
,
$fdb
);
}
$this
->
editRevUpdater
->
setLogIdsForTarget
(
$this
->
title
,
[
'local'
=>
$localLogIDs
,
'global'
=>
$globalLogIDs
]
);
return
[
'local'
=>
$loggedLocalFilters
,
'global'
=>
$loggedGlobalFilters
];
}
/**
* Creates a template to use for logging taken actions
*
* @return array
*/
private
function
buildLogTemplate
():
array
{
// If $this->user isn't safe to load (e.g. a failure during
// AbortAutoAccount), create a dummy anonymous user instead.
$user
=
$this
->
user
->
isSafeToLoad
()
?
$this
->
user
:
new
User
;
// Create a template
$logTemplate
=
[
'afl_user'
=>
$user
->
getId
(),
'afl_user_text'
=>
$user
->
getName
(),
'afl_timestamp'
=>
$this
->
lbFactory
->
getReplicaDatabase
()->
timestamp
(),
'afl_namespace'
=>
$this
->
title
->
getNamespace
(),
'afl_title'
=>
$this
->
title
->
getDBkey
(),
'afl_action'
=>
$this
->
action
,
'afl_ip'
=>
$this
->
options
->
get
(
'AbuseFilterLogIP'
)
?
$this
->
requestIP
:
''
];
// Hack to avoid revealing IPs of people creating accounts
if
(
(
$this
->
action
===
'createaccount'
||
$this
->
action
===
'autocreateaccount'
)
&&
!
$user
->
getId
()
)
{
$logTemplate
[
'afl_user_text'
]
=
$this
->
vars
->
getComputedVariable
(
'accountname'
)->
toString
();
}
return
$logTemplate
;
}
/**
* @param array $data
* @return ManualLogEntry
*/
private
function
newLocalLogEntryFromData
(
array
$data
):
ManualLogEntry
{
// Give grep a chance to find the usages:
// logentry-abusefilter-hit
$entry
=
new
ManualLogEntry
(
'abusefilter'
,
'hit'
);
$user
=
new
UserIdentityValue
(
$data
[
'afl_user'
],
$data
[
'afl_user_text'
]
);
$entry
->
setPerformer
(
$user
);
$entry
->
setTarget
(
$this
->
title
);
$filterName
=
GlobalNameUtils
::
buildGlobalName
(
$data
[
'afl_filter_id'
],
$data
[
'afl_global'
]
===
1
);
// Additional info
$entry
->
setParameters
(
[
'action'
=>
$data
[
'afl_action'
],
'filter'
=>
$filterName
,
'actions'
=>
$data
[
'afl_actions'
],
'log'
=>
$data
[
'afl_id'
],
]
);
return
$entry
;
}
/**
* @param array[] $logRows
* @param IDatabase $dbw
* @return int[]
*/
private
function
insertLocalLogEntries
(
array
$logRows
,
IDatabase
$dbw
):
array
{
$loggedIDs
=
[];
foreach
(
$logRows
as
$data
)
{
$data
[
'afl_var_dump'
]
=
$this
->
storeVarDump
(
$data
[
'afl_filter_id'
],
(
bool
)
$data
[
'afl_global'
],
false
);
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'abuse_filter_log'
)
->
row
(
$data
)
->
caller
(
__METHOD__
)
->
execute
();
$loggedIDs
[]
=
$data
[
'afl_id'
]
=
$dbw
->
insertId
();
// Send data to CheckUser if installed and we
// aren't already sending a notification to recentchanges
if
(
ExtensionRegistry
::
getInstance
()->
isLoaded
(
'CheckUser'
)
&&
!
str_contains
(
$this
->
options
->
get
(
'AbuseFilterNotifications'
)
?:
''
,
'rc'
)
)
{
$entry
=
$this
->
newLocalLogEntryFromData
(
$data
);
$user
=
$entry
->
getPerformerIdentity
();
// Invert the hack from ::buildLogTemplate because CheckUser attempts
// to assign an actor id to the non-existing user
if
(
(
$this
->
action
===
'createaccount'
||
$this
->
action
===
'autocreateaccount'
)
&&
!
$user
->
getId
()
)
{
$entry
->
setPerformer
(
new
UserIdentityValue
(
0
,
$this
->
requestIP
)
);
}
$rc
=
$entry
->
getRecentChange
();
// We need to send the entries on POSTSEND to ensure that the user definitely exists, as a temporary
// account being created by this edit may not exist until after AbuseFilter processes the edit.
DeferredUpdates
::
addCallableUpdate
(
static
function
()
use
(
$rc
)
{
// Silence the TransactionProfiler warnings for performing write queries (T359648).
$trxProfiler
=
Profiler
::
instance
()->
getTransactionProfiler
();
$scope
=
$trxProfiler
->
silenceForScope
(
$trxProfiler
::
EXPECTATION_REPLICAS_ONLY
);
Hooks
::
updateCheckUserData
(
$rc
);
ScopedCallback
::
consume
(
$scope
);
}
);
}
if
(
$this
->
options
->
get
(
'AbuseFilterNotifications'
)
!==
false
)
{
$filterID
=
$data
[
'afl_filter_id'
];
$global
=
$data
[
'afl_global'
];
if
(
!
$this
->
options
->
get
(
'AbuseFilterNotificationsPrivate'
)
&&
$this
->
filterLookup
->
getFilter
(
$filterID
,
$global
)->
isHidden
()
)
{
continue
;
}
$entry
=
$this
->
newLocalLogEntryFromData
(
$data
);
$this
->
publishEntry
(
$dbw
,
$entry
);
}
}
return
$loggedIDs
;
}
/**
* @param array[] $centralLogRows
* @param IDatabase $fdb
* @return int[]
*/
private
function
insertCentralLogEntries
(
array
$centralLogRows
,
IDatabase
$fdb
):
array
{
$this
->
varManager
->
computeDBVars
(
$this
->
vars
);
foreach
(
$centralLogRows
as
$index
=>
$data
)
{
$centralLogRows
[
$index
][
'afl_var_dump'
]
=
$this
->
storeVarDump
(
$data
[
'afl_filter_id'
],
// All the filters logged centrally are global. Note, this must not use `afl_global`, because that is
// in the perspective of the central wiki, hence false: what we consider global on the current wiki is
// local to the central wiki.
true
,
true
);
}
$loggedIDs
=
[];
foreach
(
$centralLogRows
as
$row
)
{
$fdb
->
newInsertQueryBuilder
()
->
insertInto
(
'abuse_filter_log'
)
->
row
(
$row
)
->
caller
(
__METHOD__
)
->
execute
();
$loggedIDs
[]
=
$fdb
->
insertId
();
}
return
$loggedIDs
;
}
/**
* Returns the BlobStore address for use as the value of the afl_var_dump column for an AbuseFilter log entry.
*
* This method removes protected variables from the var dump that are not used in the filter
* associated with the AbuseFilter log to be created. It also de-duplicates var dumps where
* this is possible.
*
* @param int $filterId The filter associated with the AbuseFilter log entry
* @param bool $isGlobalFilter If the filter associated with the AbuseFilter log entry is global
* @param bool $useCentralDB Whether the dump should be stored in the central database
* @return string
*/
private
function
storeVarDump
(
int
$filterId
,
bool
$isGlobalFilter
,
bool
$useCentralDB
):
string
{
// Generate a key for the varDumps instance cache used to de-duplicate var dumps where possible.
// The key for this cache is the protected variables used in the filter along with whether the
// var dump is global.
$filter
=
$this
->
filterLookup
->
getFilter
(
$filterId
,
$isGlobalFilter
);
$usedVariables
=
$this
->
ruleCheckerFactory
->
newRuleChecker
()->
getUsedVars
(
$filter
->
getRules
()
);
$usedProtectedVariables
=
$this
->
afPermissionManager
->
getUsedProtectedVariables
(
$usedVariables
);
if
(
count
(
$usedProtectedVariables
)
)
{
sort
(
$usedProtectedVariables
);
$variablesKey
=
implode
(
','
,
$usedProtectedVariables
);
}
else
{
$variablesKey
=
0
;
}
$centralDBKey
=
(
int
)
$useCentralDB
;
// Create a new var dump if the instance cache does not have this key.
if
(
!
array_key_exists
(
$centralDBKey
,
$this
->
varDumps
)
||
!
array_key_exists
(
$variablesKey
,
$this
->
varDumps
[
$centralDBKey
]
)
)
{
// Filter out all protected variables that are not used in the current filter. Any other filter with
// the same list of protected filters will also use this var dump
$filteredVars
=
VariableHolder
::
newFromArray
(
$this
->
vars
->
getVars
()
);
$protectedVariables
=
$this
->
afPermissionManager
->
getProtectedVariables
();
foreach
(
array_keys
(
$filteredVars
->
getVars
()
)
as
$varName
)
{
if
(
in_array
(
$varName
,
$protectedVariables
)
&&
!
in_array
(
$varName
,
$usedProtectedVariables
)
)
{
$filteredVars
->
removeVar
(
$varName
);
}
}
$this
->
varDumps
[
$centralDBKey
][
$variablesKey
]
=
$this
->
varBlobStore
->
storeVarDump
(
$filteredVars
,
$useCentralDB
);
}
return
$this
->
varDumps
[
$centralDBKey
][
$variablesKey
];
}
/**
* Like ManualLogEntry::publish, but doesn't require an ID (which we don't have) and skips the
* tagging part
*
* @param IDatabase $dbw To cancel the callback if the log insertion fails
* @param ManualLogEntry $entry
*/
private
function
publishEntry
(
IDatabase
$dbw
,
ManualLogEntry
$entry
):
void
{
DeferredUpdates
::
addCallableUpdate
(
function
()
use
(
$entry
)
{
$rc
=
$entry
->
getRecentChange
();
$to
=
$this
->
options
->
get
(
'AbuseFilterNotifications'
);
if
(
$to
===
'rc'
||
$to
===
'rcandudp'
)
{
$rc
->
save
(
$rc
::
SEND_NONE
);
}
if
(
$to
===
'udp'
||
$to
===
'rcandudp'
)
{
$rc
->
notifyRCFeeds
();
}
},
DeferredUpdates
::
POSTSEND
,
$dbw
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Fri, Jul 3, 18:19 (1 d, 12 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
01/fc/4ed1bc460bb202af37f8187af3e0
Default Alt Text
AbuseLogger.php (12 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment