Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4114963
ChangeTagsStore.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
31 KB
Referenced Files
None
Subscribers
None
ChangeTagsStore.php
View Options
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace
MediaWiki\ChangeTags
;
use
InvalidArgumentException
;
use
ManualLogEntry
;
use
MediaWiki\Config\ServiceOptions
;
use
MediaWiki\HookContainer\HookContainer
;
use
MediaWiki\HookContainer\HookRunner
;
use
MediaWiki\MainConfigNames
;
use
MediaWiki\Status\Status
;
use
MediaWiki\Storage\NameTableAccessException
;
use
MediaWiki\Storage\NameTableStore
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\UserFactory
;
use
MediaWiki\User\UserIdentity
;
use
Psr\Log\LoggerInterface
;
use
RecentChange
;
use
Wikimedia\ObjectCache\WANObjectCache
;
use
Wikimedia\Rdbms\Database
;
use
Wikimedia\Rdbms\IConnectionProvider
;
use
Wikimedia\Rdbms\IReadableDatabase
;
use
Wikimedia\Rdbms\RawSQLValue
;
use
Wikimedia\Rdbms\SelectQueryBuilder
;
/**
* Read-write access to the change_tags table.
*
* @since 1.41
* @ingroup ChangeTags
*/
class
ChangeTagsStore
{
/**
* Name of change_tag table
*/
private
const
CHANGE_TAG
=
'change_tag'
;
/**
* Name of change_tag_def table
*/
private
const
CHANGE_TAG_DEF
=
'change_tag_def'
;
public
const
DISPLAY_TABLE_ALIAS
=
'changetagdisplay'
;
/**
* @internal For use by ServiceWiring
*/
public
const
CONSTRUCTOR_OPTIONS
=
[
MainConfigNames
::
SoftwareTags
,
MainConfigNames
::
UseTagFilter
,
];
/**
* A list of tags defined and used by MediaWiki itself.
*/
private
const
DEFINED_SOFTWARE_TAGS
=
[
'mw-contentmodelchange'
,
'mw-new-redirect'
,
'mw-removed-redirect'
,
'mw-changed-redirect-target'
,
'mw-blank'
,
'mw-replace'
,
'mw-rollback'
,
'mw-undo'
,
'mw-manual-revert'
,
'mw-reverted'
,
'mw-server-side-upload'
,
];
private
IConnectionProvider
$dbProvider
;
private
LoggerInterface
$logger
;
private
ServiceOptions
$options
;
private
NameTableStore
$changeTagDefStore
;
private
WANObjectCache
$wanCache
;
private
HookRunner
$hookRunner
;
private
UserFactory
$userFactory
;
private
HookContainer
$hookContainer
;
public
function
__construct
(
IConnectionProvider
$dbProvider
,
NameTableStore
$changeTagDefStore
,
WANObjectCache
$wanCache
,
HookContainer
$hookContainer
,
LoggerInterface
$logger
,
UserFactory
$userFactory
,
ServiceOptions
$options
)
{
$options
->
assertRequiredOptions
(
self
::
CONSTRUCTOR_OPTIONS
);
$this
->
dbProvider
=
$dbProvider
;
$this
->
logger
=
$logger
;
$this
->
options
=
$options
;
$this
->
changeTagDefStore
=
$changeTagDefStore
;
$this
->
wanCache
=
$wanCache
;
$this
->
hookContainer
=
$hookContainer
;
$this
->
userFactory
=
$userFactory
;
$this
->
hookRunner
=
new
HookRunner
(
$hookContainer
);
}
/**
* Loads defined core tags, checks for invalid types (if not array),
* and filters for supported and enabled (if $all is false) tags only.
*
* @param bool $all If true, return all valid defined tags. Otherwise, return only enabled ones.
* @return array Array of all defined/enabled tags.
*/
public
function
getSoftwareTags
(
$all
=
false
):
array
{
$coreTags
=
$this
->
options
->
get
(
MainConfigNames
::
SoftwareTags
);
if
(
!
is_array
(
$coreTags
)
)
{
$this
->
logger
->
warning
(
'wgSoftwareTags should be associative array of enabled tags.
Please refer to documentation for the list of tags you can enable'
);
return
[];
}
$availableSoftwareTags
=
!
$all
?
array_keys
(
array_filter
(
$coreTags
)
)
:
array_keys
(
$coreTags
);
return
array_intersect
(
$availableSoftwareTags
,
self
::
DEFINED_SOFTWARE_TAGS
);
}
/**
* Return all the tags associated with the given recent change ID,
* revision ID, and/or log entry ID, along with any data stored with the tag.
*
* @param IReadableDatabase $db the database to query
* @param int|null $rc_id
* @param int|null $rev_id
* @param int|null $log_id
* @return string[] Tag name => data. Data format is tag-specific.
* @since 1.41
*/
public
function
getTagsWithData
(
IReadableDatabase
$db
,
$rc_id
=
null
,
$rev_id
=
null
,
$log_id
=
null
):
array
{
if
(
!
$rc_id
&&
!
$rev_id
&&
!
$log_id
)
{
throw
new
InvalidArgumentException
(
'At least one of: RCID, revision ID, and log ID MUST be '
.
'specified when loading tags from a change!'
);
}
$conds
=
array_filter
(
[
'ct_rc_id'
=>
$rc_id
,
'ct_rev_id'
=>
$rev_id
,
'ct_log_id'
=>
$log_id
,
]
);
$result
=
$db
->
newSelectQueryBuilder
()
->
select
(
[
'ct_tag_id'
,
'ct_params'
]
)
->
from
(
self
::
CHANGE_TAG
)
->
where
(
$conds
)
->
caller
(
__METHOD__
)
->
fetchResultSet
();
$tags
=
[];
foreach
(
$result
as
$row
)
{
$tagName
=
$this
->
changeTagDefStore
->
getName
(
(
int
)
$row
->
ct_tag_id
);
$tags
[
$tagName
]
=
$row
->
ct_params
;
}
return
$tags
;
}
/**
* Make the tag summary subquery based on the given tables and return it.
*
* @param string|array $tables Table names, see Database::select
*
* @return string tag summary subqeury
*/
public
function
makeTagSummarySubquery
(
$tables
)
{
// Normalize to arrays
$tables
=
(
array
)
$tables
;
// Figure out which ID field to use
if
(
in_array
(
'recentchanges'
,
$tables
)
)
{
$join_cond
=
'ct_rc_id=rc_id'
;
}
elseif
(
in_array
(
'logging'
,
$tables
)
)
{
$join_cond
=
'ct_log_id=log_id'
;
}
elseif
(
in_array
(
'revision'
,
$tables
)
)
{
$join_cond
=
'ct_rev_id=rev_id'
;
}
elseif
(
in_array
(
'archive'
,
$tables
)
)
{
$join_cond
=
'ct_rev_id=ar_rev_id'
;
}
else
{
throw
new
InvalidArgumentException
(
'Unable to determine appropriate JOIN condition for tagging.'
);
}
return
$this
->
dbProvider
->
getReplicaDatabase
()
->
newSelectQueryBuilder
()
->
table
(
self
::
CHANGE_TAG
)
->
join
(
self
::
CHANGE_TAG_DEF
,
null
,
'ct_tag_id=ctd_id'
)
->
field
(
'ctd_name'
)
->
where
(
$join_cond
)
->
buildGroupConcatField
(
','
);
}
/**
* Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid.
* Extensions should NOT use this function; they can use the ListDefinedTags
* hook instead.
*
* @param string $tag Tag to create
* @since 1.41
*/
public
function
defineTag
(
$tag
)
{
$dbw
=
$this
->
dbProvider
->
getPrimaryDatabase
();
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
self
::
CHANGE_TAG_DEF
)
->
row
(
[
'ctd_name'
=>
$tag
,
'ctd_user_defined'
=>
1
,
'ctd_count'
=>
0
]
)
->
onDuplicateKeyUpdate
()
->
uniqueIndexFields
(
[
'ctd_name'
]
)
->
set
(
[
'ctd_user_defined'
=>
1
]
)
->
caller
(
__METHOD__
)->
execute
();
// clear the memcache of defined tags
$this
->
purgeTagCacheAll
();
}
/**
* Update ctd_user_defined = 0 field in change_tag_def.
* The tag may remain in use by extensions, and may still show up as 'defined'
* if an extension is setting it from the ListDefinedTags hook.
*
* @param string $tag Tag to remove
* @since 1.41
*/
public
function
undefineTag
(
$tag
)
{
$dbw
=
$this
->
dbProvider
->
getPrimaryDatabase
();
$dbw
->
newUpdateQueryBuilder
()
->
update
(
self
::
CHANGE_TAG_DEF
)
->
set
(
[
'ctd_user_defined'
=>
0
]
)
->
where
(
[
'ctd_name'
=>
$tag
]
)
->
caller
(
__METHOD__
)->
execute
();
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
self
::
CHANGE_TAG_DEF
)
->
where
(
[
'ctd_name'
=>
$tag
,
'ctd_count'
=>
0
]
)
->
caller
(
__METHOD__
)->
execute
();
// clear the memcache of defined tags
$this
->
purgeTagCacheAll
();
}
/**
* Writes a tag action into the tag management log.
*
* @param string $action
* @param string $tag
* @param string $reason
* @param UserIdentity $user Who to attribute the action to
* @param int|null $tagCount For deletion only, how many usages the tag had before
* it was deleted.
* @param array $logEntryTags Change tags to apply to the entry
* that will be created in the tag management log
* @return int ID of the inserted log entry
* @since 1.41
*/
public
function
logTagManagementAction
(
string
$action
,
string
$tag
,
string
$reason
,
UserIdentity
$user
,
$tagCount
=
null
,
array
$logEntryTags
=
[]
)
{
$dbw
=
$this
->
dbProvider
->
getPrimaryDatabase
();
$logEntry
=
new
ManualLogEntry
(
'managetags'
,
$action
);
$logEntry
->
setPerformer
(
$user
);
// target page is not relevant, but it has to be set, so we just put in
// the title of Special:Tags
$logEntry
->
setTarget
(
Title
::
newFromText
(
'Special:Tags'
)
);
$logEntry
->
setComment
(
$reason
);
$params
=
[
'4::tag'
=>
$tag
];
if
(
$tagCount
!==
null
)
{
$params
[
'5:number:count'
]
=
$tagCount
;
}
$logEntry
->
setParameters
(
$params
);
$logEntry
->
setRelations
(
[
'Tag'
=>
$tag
]
);
$logEntry
->
addTags
(
$logEntryTags
);
$logId
=
$logEntry
->
insert
(
$dbw
);
$logEntry
->
publish
(
$logId
);
return
$logId
;
}
/**
* Permanently removes all traces of a tag from the DB. Good for removing
* misspelt or temporary tags.
*
* This function should be directly called by maintenance scripts only, never
* by user-facing code. See deleteTagWithChecks() for functionality that can
* safely be exposed to users.
*
* @param string $tag Tag to remove
* @return Status The returned status will be good unless a hook changed it
* @since 1.41
*/
public
function
deleteTagEverywhere
(
$tag
)
{
$dbw
=
$this
->
dbProvider
->
getPrimaryDatabase
();
$dbw
->
startAtomic
(
__METHOD__
);
// fetch tag id, this must be done before calling undefineTag(), see T225564
$tagId
=
$this
->
changeTagDefStore
->
getId
(
$tag
);
// set ctd_user_defined = 0
$this
->
undefineTag
(
$tag
);
// delete from change_tag
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
self
::
CHANGE_TAG
)
->
where
(
[
'ct_tag_id'
=>
$tagId
]
)
->
caller
(
__METHOD__
)->
execute
();
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
self
::
CHANGE_TAG_DEF
)
->
where
(
[
'ctd_name'
=>
$tag
]
)
->
caller
(
__METHOD__
)->
execute
();
$dbw
->
endAtomic
(
__METHOD__
);
// give extensions a chance
$status
=
Status
::
newGood
();
$this
->
hookRunner
->
onChangeTagAfterDelete
(
$tag
,
$status
);
// let's not allow error results, as the actual tag deletion succeeded
if
(
!
$status
->
isOK
()
)
{
$this
->
logger
->
debug
(
'ChangeTagAfterDelete error condition downgraded to warning'
);
$status
->
setOK
(
true
);
}
// clear the memcache of defined tags
$this
->
purgeTagCacheAll
();
return
$status
;
}
/**
* Invalidates the short-term cache of defined tags used by the
* list*DefinedTags functions, as well as the tag statistics cache.
* @since 1.41
*/
public
function
purgeTagCacheAll
()
{
$this
->
wanCache
->
touchCheckKey
(
$this
->
wanCache
->
makeKey
(
'active-tags'
)
);
$this
->
wanCache
->
touchCheckKey
(
$this
->
wanCache
->
makeKey
(
'valid-tags-db'
)
);
$this
->
wanCache
->
touchCheckKey
(
$this
->
wanCache
->
makeKey
(
'valid-tags-hook'
)
);
$this
->
wanCache
->
touchCheckKey
(
$this
->
wanCache
->
makeKey
(
'tags-usage-statistics'
)
);
$this
->
changeTagDefStore
->
reloadMap
();
}
/**
* Returns a map of any tags used on the wiki to number of edits
* tagged with them, ordered descending by the hitcount.
* This does not include tags defined somewhere that have never been applied.
* @return array Array of string => int
*/
public
function
tagUsageStatistics
():
array
{
$fname
=
__METHOD__
;
$dbProvider
=
$this
->
dbProvider
;
return
$this
->
wanCache
->
getWithSetCallback
(
$this
->
wanCache
->
makeKey
(
'tags-usage-statistics'
),
WANObjectCache
::
TTL_MINUTE
*
5
,
static
function
(
$oldValue
,
&
$ttl
,
array
&
$setOpts
)
use
(
$fname
,
$dbProvider
)
{
$dbr
=
$dbProvider
->
getReplicaDatabase
();
$res
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
[
'ctd_name'
,
'ctd_count'
]
)
->
from
(
self
::
CHANGE_TAG_DEF
)
->
orderBy
(
'ctd_count'
,
SelectQueryBuilder
::
SORT_DESC
)
->
caller
(
$fname
)
->
fetchResultSet
();
$out
=
[];
foreach
(
$res
as
$row
)
{
$out
[
$row
->
ctd_name
]
=
$row
->
ctd_count
;
}
return
$out
;
},
[
'checkKeys'
=>
[
$this
->
wanCache
->
makeKey
(
'tags-usage-statistics'
)
],
'lockTSE'
=>
WANObjectCache
::
TTL_MINUTE
*
5
,
'pcTTL'
=>
WANObjectCache
::
TTL_PROC_LONG
]
);
}
/**
* Lists tags explicitly defined in the `change_tag_def` table of the database.
*
* Tries memcached first.
*
* @return string[] Array of strings: tags
* @since 1.25
*/
public
function
listExplicitlyDefinedTags
()
{
$fname
=
__METHOD__
;
$dbProvider
=
$this
->
dbProvider
;
return
$this
->
wanCache
->
getWithSetCallback
(
$this
->
wanCache
->
makeKey
(
'valid-tags-db'
),
WANObjectCache
::
TTL_MINUTE
*
5
,
static
function
(
$oldValue
,
&
$ttl
,
array
&
$setOpts
)
use
(
$fname
,
$dbProvider
)
{
$dbr
=
$dbProvider
->
getReplicaDatabase
();
$setOpts
+=
Database
::
getCacheSetOptions
(
$dbr
);
$tags
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'ctd_name'
)
->
from
(
self
::
CHANGE_TAG_DEF
)
->
where
(
[
'ctd_user_defined'
=>
1
]
)
->
caller
(
$fname
)
->
fetchFieldValues
();
return
array_unique
(
$tags
);
},
[
'checkKeys'
=>
[
$this
->
wanCache
->
makeKey
(
'valid-tags-db'
)
],
'lockTSE'
=>
WANObjectCache
::
TTL_MINUTE
*
5
,
'pcTTL'
=>
WANObjectCache
::
TTL_PROC_LONG
]
);
}
/**
* Lists tags defined by core or extensions using the ListDefinedTags hook.
* Extensions need only define those tags they deem to be in active use.
*
* Tries memcached first.
*
* @return string[] Array of strings: tags
* @since 1.25
*/
public
function
listSoftwareDefinedTags
()
{
// core defined tags
$tags
=
$this
->
getSoftwareTags
(
true
);
if
(
!
$this
->
hookContainer
->
isRegistered
(
'ListDefinedTags'
)
)
{
return
$tags
;
}
$hookRunner
=
$this
->
hookRunner
;
$dbProvider
=
$this
->
dbProvider
;
return
$this
->
wanCache
->
getWithSetCallback
(
$this
->
wanCache
->
makeKey
(
'valid-tags-hook'
),
WANObjectCache
::
TTL_MINUTE
*
5
,
static
function
(
$oldValue
,
&
$ttl
,
array
&
$setOpts
)
use
(
$tags
,
$hookRunner
,
$dbProvider
)
{
$setOpts
+=
Database
::
getCacheSetOptions
(
$dbProvider
->
getReplicaDatabase
()
);
$hookRunner
->
onListDefinedTags
(
$tags
);
return
array_unique
(
$tags
);
},
[
'checkKeys'
=>
[
$this
->
wanCache
->
makeKey
(
'valid-tags-hook'
)
],
'lockTSE'
=>
WANObjectCache
::
TTL_MINUTE
*
5
,
'pcTTL'
=>
WANObjectCache
::
TTL_PROC_LONG
]
);
}
/**
* Return all the tags associated with the given recent change ID,
* revision ID, and/or log entry ID.
*
* @param IReadableDatabase $db the database to query
* @param int|null $rc_id
* @param int|null $rev_id
* @param int|null $log_id
* @return string[]
*/
public
function
getTags
(
IReadableDatabase
$db
,
$rc_id
=
null
,
$rev_id
=
null
,
$log_id
=
null
)
{
return
array_keys
(
$this
->
getTagsWithData
(
$db
,
$rc_id
,
$rev_id
,
$log_id
)
);
}
/**
* Basically lists defined tags which count even if they aren't applied to anything.
* It returns a union of the results of listExplicitlyDefinedTags() and
* listSoftwareDefinedTags()
*
* @return string[] Array of strings: tags
*/
public
function
listDefinedTags
()
{
$tags1
=
$this
->
listExplicitlyDefinedTags
();
$tags2
=
$this
->
listSoftwareDefinedTags
();
return
array_values
(
array_unique
(
array_merge
(
$tags1
,
$tags2
)
)
);
}
/**
* Add and remove tags to/from a change given its rc_id, rev_id and/or log_id,
* without verifying that the tags exist or are valid. If a tag is present in
* both $tagsToAdd and $tagsToRemove, it will be removed.
*
* This function should only be used by extensions to manipulate tags they
* have registered using the ListDefinedTags hook. When dealing with user
* input, call updateTagsWithChecks() instead.
*
* @param string|array|null $tagsToAdd Tags to add to the change
* @param string|array|null $tagsToRemove Tags to remove from the change
* @param int|null &$rc_id The rc_id of the change to add the tags to.
* Pass a variable whose value is null if the rc_id is not relevant or unknown.
* @param int|null &$rev_id The rev_id of the change to add the tags to.
* Pass a variable whose value is null if the rev_id is not relevant or unknown.
* @param int|null &$log_id The log_id of the change to add the tags to.
* Pass a variable whose value is null if the log_id is not relevant or unknown.
* @param string|null $params Params to put in the ct_params field of table
* 'change_tag' when adding tags
* @param RecentChange|null $rc Recent change being tagged, in case the tagging accompanies
* the action
* @param UserIdentity|null $user Tagging user, in case the tagging is subsequent to the tagged action
*
* @return array Index 0 is an array of tags actually added, index 1 is an
* array of tags actually removed, index 2 is an array of tags present on the
* revision or log entry before any changes were made
*/
public
function
updateTags
(
$tagsToAdd
,
$tagsToRemove
,
&
$rc_id
=
null
,
&
$rev_id
=
null
,
&
$log_id
=
null
,
$params
=
null
,
?
RecentChange
$rc
=
null
,
?
UserIdentity
$user
=
null
)
{
$tagsToAdd
=
array_filter
(
(
array
)
$tagsToAdd
,
// Make sure we're submitting all tags...
static
function
(
$value
)
{
return
(
$value
??
''
)
!==
''
;
}
);
$tagsToRemove
=
array_filter
(
(
array
)
$tagsToRemove
,
static
function
(
$value
)
{
return
(
$value
??
''
)
!==
''
;
}
);
if
(
!
$rc_id
&&
!
$rev_id
&&
!
$log_id
)
{
throw
new
InvalidArgumentException
(
'At least one of: RCID, revision ID, and log ID MUST be '
.
'specified when adding or removing a tag from a change!'
);
}
$dbw
=
$this
->
dbProvider
->
getPrimaryDatabase
();
// Might as well look for rcids and so on.
if
(
!
$rc_id
)
{
// Info might be out of date, somewhat fractionally, on replica DB.
// LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
// so use that relation to avoid full table scans.
if
(
$log_id
)
{
$rc_id
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'rc_id'
)
->
from
(
'logging'
)
->
join
(
'recentchanges'
,
null
,
[
'rc_timestamp = log_timestamp'
,
'rc_logid = log_id'
]
)
->
where
(
[
'log_id'
=>
$log_id
]
)
->
caller
(
__METHOD__
)
->
fetchField
();
}
elseif
(
$rev_id
)
{
$rc_id
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'rc_id'
)
->
from
(
'revision'
)
->
join
(
'recentchanges'
,
null
,
[
'rc_this_oldid = rev_id'
]
)
->
where
(
[
'rev_id'
=>
$rev_id
]
)
->
caller
(
__METHOD__
)
->
fetchField
();
}
}
elseif
(
!
$log_id
&&
!
$rev_id
)
{
// Info might be out of date, somewhat fractionally, on replica DB.
$log_id
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'rc_logid'
)
->
from
(
'recentchanges'
)
->
where
(
[
'rc_id'
=>
$rc_id
]
)
->
caller
(
__METHOD__
)
->
fetchField
();
$rev_id
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'rc_this_oldid'
)
->
from
(
'recentchanges'
)
->
where
(
[
'rc_id'
=>
$rc_id
]
)
->
caller
(
__METHOD__
)
->
fetchField
();
}
if
(
$log_id
&&
!
$rev_id
)
{
$rev_id
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'ls_value'
)
->
from
(
'log_search'
)
->
where
(
[
'ls_field'
=>
'associated_rev_id'
,
'ls_log_id'
=>
$log_id
]
)
->
caller
(
__METHOD__
)
->
fetchField
();
}
elseif
(
!
$log_id
&&
$rev_id
)
{
$log_id
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'ls_log_id'
)
->
from
(
'log_search'
)
->
where
(
[
'ls_field'
=>
'associated_rev_id'
,
'ls_value'
=>
(
string
)
$rev_id
]
)
->
caller
(
__METHOD__
)
->
fetchField
();
}
$prevTags
=
$this
->
getTags
(
$dbw
,
$rc_id
,
$rev_id
,
$log_id
);
// add tags
$tagsToAdd
=
array_values
(
array_diff
(
$tagsToAdd
,
$prevTags
)
);
$newTags
=
array_unique
(
array_merge
(
$prevTags
,
$tagsToAdd
)
);
// remove tags
$tagsToRemove
=
array_values
(
array_intersect
(
$tagsToRemove
,
$newTags
)
);
$newTags
=
array_values
(
array_diff
(
$newTags
,
$tagsToRemove
)
);
sort
(
$prevTags
);
sort
(
$newTags
);
if
(
$prevTags
==
$newTags
)
{
return
[
[],
[],
$prevTags
];
}
// insert a row into change_tag for each new tag
if
(
count
(
$tagsToAdd
)
)
{
$changeTagMapping
=
[];
foreach
(
$tagsToAdd
as
$tag
)
{
$changeTagMapping
[
$tag
]
=
$this
->
changeTagDefStore
->
acquireId
(
$tag
);
}
$fname
=
__METHOD__
;
// T207881: update the counts at the end of the transaction
$dbw
->
onTransactionPreCommitOrIdle
(
static
function
()
use
(
$dbw
,
$tagsToAdd
,
$fname
)
{
$dbw
->
newUpdateQueryBuilder
()
->
update
(
self
::
CHANGE_TAG_DEF
)
->
set
(
[
'ctd_count'
=>
new
RawSQLValue
(
'ctd_count + 1'
)
]
)
->
where
(
[
'ctd_name'
=>
$tagsToAdd
]
)
->
caller
(
$fname
)->
execute
();
},
$fname
);
$tagsRows
=
[];
foreach
(
$tagsToAdd
as
$tag
)
{
// Filter so we don't insert NULLs as zero accidentally.
// Keep in mind that $rc_id === null means "I don't care/know about the
// rc_id, just delete $tag on this revision/log entry". It doesn't
// mean "only delete tags on this revision/log WHERE rc_id IS NULL".
$tagsRows
[]
=
array_filter
(
[
'ct_rc_id'
=>
$rc_id
,
'ct_log_id'
=>
$log_id
,
'ct_rev_id'
=>
$rev_id
,
'ct_params'
=>
$params
,
'ct_tag_id'
=>
$changeTagMapping
[
$tag
]
??
null
,
]
);
}
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
self
::
CHANGE_TAG
)
->
ignore
()
->
rows
(
$tagsRows
)
->
caller
(
__METHOD__
)->
execute
();
}
// delete from change_tag
if
(
count
(
$tagsToRemove
)
)
{
$fname
=
__METHOD__
;
foreach
(
$tagsToRemove
as
$tag
)
{
$conds
=
array_filter
(
[
'ct_rc_id'
=>
$rc_id
,
'ct_log_id'
=>
$log_id
,
'ct_rev_id'
=>
$rev_id
,
'ct_tag_id'
=>
$this
->
changeTagDefStore
->
getId
(
$tag
),
]
);
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
self
::
CHANGE_TAG
)
->
where
(
$conds
)
->
caller
(
__METHOD__
)->
execute
();
if
(
$dbw
->
affectedRows
()
)
{
// T207881: update the counts at the end of the transaction
$dbw
->
onTransactionPreCommitOrIdle
(
static
function
()
use
(
$dbw
,
$tag
,
$fname
)
{
$dbw
->
newUpdateQueryBuilder
()
->
update
(
self
::
CHANGE_TAG_DEF
)
->
set
(
[
'ctd_count'
=>
new
RawSQLValue
(
'ctd_count - 1'
)
]
)
->
where
(
[
'ctd_name'
=>
$tag
]
)
->
caller
(
$fname
)->
execute
();
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
self
::
CHANGE_TAG_DEF
)
->
where
(
[
'ctd_name'
=>
$tag
,
'ctd_count'
=>
0
,
'ctd_user_defined'
=>
0
]
)
->
caller
(
$fname
)->
execute
();
},
$fname
);
}
}
}
$userObj
=
$user
?
$this
->
userFactory
->
newFromUserIdentity
(
$user
)
:
null
;
$this
->
hookRunner
->
onChangeTagsAfterUpdateTags
(
$tagsToAdd
,
$tagsToRemove
,
$prevTags
,
$rc_id
,
$rev_id
,
$log_id
,
$params
,
$rc
,
$userObj
);
return
[
$tagsToAdd
,
$tagsToRemove
,
$prevTags
];
}
/**
* Add tags to a change given its rc_id, rev_id and/or log_id
*
* @param string|string[] $tags Tags to add to the change
* @param int|null $rc_id The rc_id of the change to add the tags to
* @param int|null $rev_id The rev_id of the change to add the tags to
* @param int|null $log_id The log_id of the change to add the tags to
* @param string|null $params Params to put in the ct_params field of table 'change_tag'
* @param RecentChange|null $rc Recent change, in case the tagging accompanies the action
* (this should normally be the case)
*
* @return bool False if no changes are made, otherwise true
*/
public
function
addTags
(
$tags
,
$rc_id
=
null
,
$rev_id
=
null
,
$log_id
=
null
,
$params
=
null
,
?
RecentChange
$rc
=
null
)
{
$result
=
$this
->
updateTags
(
$tags
,
null
,
$rc_id
,
$rev_id
,
$log_id
,
$params
,
$rc
);
return
(
bool
)
$result
[
0
];
}
/**
* Lists those tags which core or extensions report as being "active".
*
* @return array
* @since 1.41
*/
public
function
listSoftwareActivatedTags
()
{
// core active tags
$tags
=
$this
->
getSoftwareTags
();
if
(
!
$this
->
hookContainer
->
isRegistered
(
'ChangeTagsListActive'
)
)
{
return
$tags
;
}
$hookRunner
=
$this
->
hookRunner
;
$dbProvider
=
$this
->
dbProvider
;
return
$this
->
wanCache
->
getWithSetCallback
(
$this
->
wanCache
->
makeKey
(
'active-tags'
),
WANObjectCache
::
TTL_MINUTE
*
5
,
static
function
(
$oldValue
,
&
$ttl
,
array
&
$setOpts
)
use
(
$tags
,
$hookRunner
,
$dbProvider
)
{
$setOpts
+=
Database
::
getCacheSetOptions
(
$dbProvider
->
getReplicaDatabase
()
);
// Ask extensions which tags they consider active
$hookRunner
->
onChangeTagsListActive
(
$tags
);
return
$tags
;
},
[
'checkKeys'
=>
[
$this
->
wanCache
->
makeKey
(
'active-tags'
)
],
'lockTSE'
=>
WANObjectCache
::
TTL_MINUTE
*
5
,
'pcTTL'
=>
WANObjectCache
::
TTL_PROC_LONG
]
);
}
/**
* Applies all tags-related changes to a query.
* Handles selecting tags, and filtering.
* Needs $tables to be set up properly, so we can figure out which join conditions to use.
*
* WARNING: If $filter_tag contains more than one tag and $exclude is false, this function
* will add DISTINCT, which may cause performance problems for your query unless you put
* the ID field of your table at the end of the ORDER BY, and set a GROUP BY equal to the
* ORDER BY. For example, if you had ORDER BY foo_timestamp DESC, you will now need
* GROUP BY foo_timestamp, foo_id ORDER BY foo_timestamp DESC, foo_id DESC.
*
* @deprecated since 1.41 use ChangeTagsStore::modifyDisplayQueryBuilder instead
*
* @param string|array &$tables Table names, see Database::select
* @param string|array &$fields Fields used in query, see Database::select
* @param string|array &$conds Conditions used in query, see Database::select
* @param array &$join_conds Join conditions, see Database::select
* @param string|array &$options Options, see Database::select
* @param string|array|false|null $filter_tag Tag(s) to select on (OR)
* @param bool $exclude If true, exclude tag(s) from $filter_tag (NOR)
*
*/
public
function
modifyDisplayQuery
(
&
$tables
,
&
$fields
,
&
$conds
,
&
$join_conds
,
&
$options
,
$filter_tag
=
''
,
bool
$exclude
=
false
)
{
$useTagFilter
=
$this
->
options
->
get
(
MainConfigNames
::
UseTagFilter
);
// Normalize to arrays
$tables
=
(
array
)
$tables
;
$fields
=
(
array
)
$fields
;
$conds
=
(
array
)
$conds
;
$options
=
(
array
)
$options
;
$fields
[
'ts_tags'
]
=
$this
->
makeTagSummarySubquery
(
$tables
);
// We use an alias and qualify the conditions in case there are
// multiple joins to this table.
// In particular for compatibility with the RC filters that extension Translate does.
// Figure out which ID field to use
if
(
in_array
(
'recentchanges'
,
$tables
)
)
{
$join_cond
=
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_rc_id=rc_id'
;
}
elseif
(
in_array
(
'logging'
,
$tables
)
)
{
$join_cond
=
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_log_id=log_id'
;
}
elseif
(
in_array
(
'revision'
,
$tables
)
)
{
$join_cond
=
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_rev_id=rev_id'
;
}
elseif
(
in_array
(
'archive'
,
$tables
)
)
{
$join_cond
=
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_rev_id=ar_rev_id'
;
}
else
{
throw
new
InvalidArgumentException
(
'Unable to determine appropriate JOIN condition for tagging.'
);
}
if
(
!
$useTagFilter
)
{
return
;
}
if
(
!
is_array
(
$filter_tag
)
)
{
// some callers provide false or null
$filter_tag
=
(
string
)
$filter_tag
;
}
if
(
$filter_tag
!==
[]
&&
$filter_tag
!==
''
)
{
// Somebody wants to filter on a tag.
// Add an INNER JOIN on change_tag
$filterTagIds
=
[];
foreach
(
(
array
)
$filter_tag
as
$filterTagName
)
{
try
{
$filterTagIds
[]
=
$this
->
changeTagDefStore
->
getId
(
$filterTagName
);
}
catch
(
NameTableAccessException
$exception
)
{
}
}
if
(
$exclude
)
{
if
(
$filterTagIds
!==
[]
)
{
$tables
[
self
::
DISPLAY_TABLE_ALIAS
]
=
self
::
CHANGE_TAG
;
$join_conds
[
self
::
DISPLAY_TABLE_ALIAS
]
=
[
'LEFT JOIN'
,
[
$join_cond
,
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_tag_id'
=>
$filterTagIds
]
];
$conds
[
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_tag_id'
]
=
null
;
}
}
else
{
$tables
[
self
::
DISPLAY_TABLE_ALIAS
]
=
self
::
CHANGE_TAG
;
$join_conds
[
self
::
DISPLAY_TABLE_ALIAS
]
=
[
'JOIN'
,
$join_cond
];
if
(
$filterTagIds
!==
[]
)
{
$conds
[
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_tag_id'
]
=
$filterTagIds
;
}
else
{
// all tags were invalid, return nothing
$conds
[]
=
'0=1'
;
}
if
(
is_array
(
$filter_tag
)
&&
count
(
$filter_tag
)
>
1
&&
!
in_array
(
'DISTINCT'
,
$options
)
)
{
$options
[]
=
'DISTINCT'
;
}
}
}
}
/**
* Applies all tags-related changes to a query builder object.
*
* Handles selecting tags, and filtering.
*
* WARNING: If $filter_tag contains more than one tag and $exclude is false, this function
* will add DISTINCT, which may cause performance problems for your query unless you put
* the ID field of your table at the end of the ORDER BY, and set a GROUP BY equal to the
* ORDER BY. For example, if you had ORDER BY foo_timestamp DESC, you will now need
* GROUP BY foo_timestamp, foo_id ORDER BY foo_timestamp DESC, foo_id DESC.
*
* @param SelectQueryBuilder $queryBuilder Query builder to add the join
* @param string $table Table name. Must be either of 'recentchanges', 'logging', 'revision', or 'archive'
* @param string|array|false|null $filter_tag Tag(s) to select on (OR)
* @param bool $exclude If true, exclude tag(s) from $filter_tag (NOR)
*
*/
public
function
modifyDisplayQueryBuilder
(
SelectQueryBuilder
$queryBuilder
,
$table
,
$filter_tag
=
''
,
bool
$exclude
=
false
)
{
$useTagFilter
=
$this
->
options
->
get
(
MainConfigNames
::
UseTagFilter
);
$queryBuilder
->
field
(
$this
->
makeTagSummarySubquery
(
[
$table
]
),
'ts_tags'
);
// We use an alias and qualify the conditions in case there are
// multiple joins to this table.
// In particular for compatibility with the RC filters that extension Translate does.
// Figure out which ID field to use
if
(
$table
===
'recentchanges'
)
{
$join_cond
=
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_rc_id=rc_id'
;
}
elseif
(
$table
===
'logging'
)
{
$join_cond
=
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_log_id=log_id'
;
}
elseif
(
$table
===
'revision'
)
{
$join_cond
=
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_rev_id=rev_id'
;
}
elseif
(
$table
===
'archive'
)
{
$join_cond
=
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_rev_id=ar_rev_id'
;
}
else
{
throw
new
InvalidArgumentException
(
'Unable to determine appropriate JOIN condition for tagging.'
);
}
if
(
!
$useTagFilter
)
{
return
;
}
if
(
!
is_array
(
$filter_tag
)
)
{
// some callers provide false or null
$filter_tag
=
(
string
)
$filter_tag
;
}
if
(
$filter_tag
!==
[]
&&
$filter_tag
!==
''
)
{
// Somebody wants to filter on a tag.
// Add an INNER JOIN on change_tag
$filterTagIds
=
[];
foreach
(
(
array
)
$filter_tag
as
$filterTagName
)
{
try
{
$filterTagIds
[]
=
$this
->
changeTagDefStore
->
getId
(
$filterTagName
);
}
catch
(
NameTableAccessException
$exception
)
{
}
}
if
(
$exclude
)
{
if
(
$filterTagIds
!==
[]
)
{
$queryBuilder
->
leftJoin
(
self
::
CHANGE_TAG
,
self
::
DISPLAY_TABLE_ALIAS
,
[
$join_cond
,
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_tag_id'
=>
$filterTagIds
]
);
$queryBuilder
->
where
(
[
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_tag_id'
=>
null
]
);
}
}
else
{
$queryBuilder
->
join
(
self
::
CHANGE_TAG
,
self
::
DISPLAY_TABLE_ALIAS
,
$join_cond
);
if
(
$filterTagIds
!==
[]
)
{
$queryBuilder
->
where
(
[
self
::
DISPLAY_TABLE_ALIAS
.
'.ct_tag_id'
=>
$filterTagIds
]
);
}
else
{
// all tags were invalid, return nothing
$queryBuilder
->
where
(
'0=1'
);
}
if
(
is_array
(
$filter_tag
)
&&
count
(
$filter_tag
)
>
1
)
{
$queryBuilder
->
distinct
();
}
}
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Wed, Aug 19, 00:37 (5 d, 5 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
53/54/aa1b154e27f2ecce37e3738aa5fd
Default Alt Text
ChangeTagsStore.php (31 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment