Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1430559
PageHistoryCountHandler.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
21 KB
Referenced Files
None
Subscribers
None
PageHistoryCountHandler.php
View Options
<?php
namespace
MediaWiki\Rest\Handler
;
use
ChangeTags
;
use
MediaWiki\Page\ExistingPageRecord
;
use
MediaWiki\Page\PageLookup
;
use
MediaWiki\Permissions\GroupPermissionsLookup
;
use
MediaWiki\Rest\Handler\Helper\PageRedirectHelper
;
use
MediaWiki\Rest\Handler\Helper\PageRestHelperFactory
;
use
MediaWiki\Rest\LocalizedHttpException
;
use
MediaWiki\Rest\Response
;
use
MediaWiki\Rest\SimpleHandler
;
use
MediaWiki\Revision\RevisionRecord
;
use
MediaWiki\Revision\RevisionStore
;
use
MediaWiki\Storage\NameTableAccessException
;
use
MediaWiki\Storage\NameTableStore
;
use
MediaWiki\Storage\NameTableStoreFactory
;
use
MediaWiki\User\TempUser\TempUserConfig
;
use
Wikimedia\Message\MessageValue
;
use
Wikimedia\Message\ParamType
;
use
Wikimedia\Message\ScalarParam
;
use
Wikimedia\ObjectCache\WANObjectCache
;
use
Wikimedia\ParamValidator\ParamValidator
;
use
Wikimedia\Rdbms\IConnectionProvider
;
use
Wikimedia\Rdbms\IExpression
;
use
Wikimedia\Rdbms\RawSQLExpression
;
/**
* Handler class for Core REST API endpoints that perform operations on revisions
*/
class
PageHistoryCountHandler
extends
SimpleHandler
{
/** The maximum number of counts to return per type of revision */
private
const
COUNT_LIMITS
=
[
'anonymous'
=>
10000
,
'temporary'
=>
10000
,
'bot'
=>
10000
,
'editors'
=>
25000
,
'edits'
=>
30000
,
'minor'
=>
1000
,
'reverted'
=>
30000
];
private
const
DEPRECATED_COUNT_TYPES
=
[
'anonedits'
=>
'anonymous'
,
'botedits'
=>
'bot'
,
'revertededits'
=>
'reverted'
];
private
const
MAX_AGE_200
=
60
;
private
RevisionStore
$revisionStore
;
private
NameTableStore
$changeTagDefStore
;
private
GroupPermissionsLookup
$groupPermissionsLookup
;
private
IConnectionProvider
$dbProvider
;
private
PageLookup
$pageLookup
;
private
WANObjectCache
$cache
;
private
PageRestHelperFactory
$helperFactory
;
private
TempUserConfig
$tempUserConfig
;
/** @var RevisionRecord|false|null */
private
$revision
=
false
;
/** @var array|null */
private
$lastModifiedTimes
;
/** @var ExistingPageRecord|false|null */
private
$page
=
false
;
/**
* @param RevisionStore $revisionStore
* @param NameTableStoreFactory $nameTableStoreFactory
* @param GroupPermissionsLookup $groupPermissionsLookup
* @param IConnectionProvider $dbProvider
* @param WANObjectCache $cache
* @param PageLookup $pageLookup
* @param PageRestHelperFactory $helperFactory
* @param TempUserConfig $tempUserConfig
*/
public
function
__construct
(
RevisionStore
$revisionStore
,
NameTableStoreFactory
$nameTableStoreFactory
,
GroupPermissionsLookup
$groupPermissionsLookup
,
IConnectionProvider
$dbProvider
,
WANObjectCache
$cache
,
PageLookup
$pageLookup
,
PageRestHelperFactory
$helperFactory
,
TempUserConfig
$tempUserConfig
)
{
$this
->
revisionStore
=
$revisionStore
;
$this
->
changeTagDefStore
=
$nameTableStoreFactory
->
getChangeTagDef
();
$this
->
groupPermissionsLookup
=
$groupPermissionsLookup
;
$this
->
dbProvider
=
$dbProvider
;
$this
->
cache
=
$cache
;
$this
->
pageLookup
=
$pageLookup
;
$this
->
helperFactory
=
$helperFactory
;
$this
->
tempUserConfig
=
$tempUserConfig
;
}
private
function
getRedirectHelper
():
PageRedirectHelper
{
return
$this
->
helperFactory
->
newPageRedirectHelper
(
$this
->
getResponseFactory
(),
$this
->
getRouter
(),
$this
->
getPath
(),
$this
->
getRequest
()
);
}
private
function
normalizeType
(
$type
)
{
return
self
::
DEPRECATED_COUNT_TYPES
[
$type
]
??
$type
;
}
/**
* Validates that the provided parameter combination is supported.
*
* @param string $type
* @throws LocalizedHttpException
*/
private
function
validateParameterCombination
(
$type
)
{
$params
=
$this
->
getValidatedParams
();
if
(
!
$params
)
{
return
;
}
if
(
$params
[
'from'
]
||
$params
[
'to'
]
)
{
if
(
$type
===
'edits'
||
$type
===
'editors'
)
{
if
(
!
$params
[
'from'
]
||
!
$params
[
'to'
]
)
{
throw
new
LocalizedHttpException
(
new
MessageValue
(
'rest-pagehistorycount-parameters-invalid'
),
400
);
}
}
else
{
throw
new
LocalizedHttpException
(
new
MessageValue
(
'rest-pagehistorycount-parameters-invalid'
),
400
);
}
}
}
/**
* @param string $title the title of the page to load history for
* @param string $type the validated count type
* @return Response
* @throws LocalizedHttpException
*/
public
function
run
(
$title
,
$type
)
{
$normalizedType
=
$this
->
normalizeType
(
$type
);
$this
->
validateParameterCombination
(
$normalizedType
);
$params
=
$this
->
getValidatedParams
();
$page
=
$this
->
getPage
();
if
(
!
$page
)
{
throw
new
LocalizedHttpException
(
new
MessageValue
(
'rest-nonexistent-title'
,
[
new
ScalarParam
(
ParamType
::
PLAINTEXT
,
$title
)
]
),
404
);
}
if
(
!
$this
->
getAuthority
()->
authorizeRead
(
'read'
,
$page
)
)
{
throw
new
LocalizedHttpException
(
new
MessageValue
(
'rest-permission-denied-title'
,
[
new
ScalarParam
(
ParamType
::
PLAINTEXT
,
$title
)
]
),
403
);
}
'@phan-var
\M
ediaWiki
\P
age
\E
xistingPageRecord $page'
;
$redirectResponse
=
$this
->
getRedirectHelper
()->
createNormalizationRedirectResponseIfNeeded
(
$page
,
$params
[
'title'
]
??
null
);
if
(
$redirectResponse
!==
null
)
{
return
$redirectResponse
;
}
$count
=
$this
->
getCount
(
$normalizedType
);
$countLimit
=
self
::
COUNT_LIMITS
[
$normalizedType
];
$response
=
$this
->
getResponseFactory
()->
createJson
(
[
'count'
=>
$count
>
$countLimit
?
$countLimit
:
$count
,
'limit'
=>
$count
>
$countLimit
]
);
$response
->
setHeader
(
'Cache-Control'
,
'max-age='
.
self
::
MAX_AGE_200
);
// Inform clients who use a deprecated "type" value, so they can adjust
if
(
isset
(
self
::
DEPRECATED_COUNT_TYPES
[
$type
]
)
)
{
$docs
=
'<https://www.mediawiki.org/wiki/API:REST/History_API'
.
'#Get_page_history_counts>; rel="deprecation"'
;
$response
->
setHeader
(
'Deprecation'
,
'version="v1"'
);
$response
->
setHeader
(
'Link'
,
$docs
);
}
return
$response
;
}
/**
* @param string $type the validated count type
* @return int the article count
* @throws LocalizedHttpException
*/
private
function
getCount
(
$type
)
{
$pageId
=
$this
->
getPage
()->
getId
();
switch
(
$type
)
{
case
'anonymous'
:
return
$this
->
getCachedCount
(
$type
,
function
(
?
RevisionRecord
$fromRev
=
null
)
use
(
$pageId
)
{
return
$this
->
getAnonCount
(
$pageId
,
$fromRev
);
}
);
case
'temporary'
:
return
$this
->
getCachedCount
(
$type
,
function
(
?
RevisionRecord
$fromRev
=
null
)
use
(
$pageId
)
{
return
$this
->
getTempCount
(
$pageId
,
$fromRev
);
}
);
case
'bot'
:
return
$this
->
getCachedCount
(
$type
,
function
(
?
RevisionRecord
$fromRev
=
null
)
use
(
$pageId
)
{
return
$this
->
getBotCount
(
$pageId
,
$fromRev
);
}
);
case
'editors'
:
$from
=
$this
->
getValidatedParams
()[
'from'
]
??
null
;
$to
=
$this
->
getValidatedParams
()[
'to'
]
??
null
;
if
(
$from
||
$to
)
{
return
$this
->
getEditorsCount
(
$pageId
,
$from
?
$this
->
getRevisionOrThrow
(
$from
)
:
null
,
$to
?
$this
->
getRevisionOrThrow
(
$to
)
:
null
);
}
else
{
return
$this
->
getCachedCount
(
$type
,
function
(
?
RevisionRecord
$fromRev
=
null
)
use
(
$pageId
)
{
return
$this
->
getEditorsCount
(
$pageId
,
$fromRev
);
}
);
}
case
'edits'
:
$from
=
$this
->
getValidatedParams
()[
'from'
]
??
null
;
$to
=
$this
->
getValidatedParams
()[
'to'
]
??
null
;
if
(
$from
||
$to
)
{
return
$this
->
getEditsCount
(
$pageId
,
$from
?
$this
->
getRevisionOrThrow
(
$from
)
:
null
,
$to
?
$this
->
getRevisionOrThrow
(
$to
)
:
null
);
}
else
{
return
$this
->
getCachedCount
(
$type
,
function
(
?
RevisionRecord
$fromRev
=
null
)
use
(
$pageId
)
{
return
$this
->
getEditsCount
(
$pageId
,
$fromRev
);
}
);
}
case
'reverted'
:
return
$this
->
getCachedCount
(
$type
,
function
(
?
RevisionRecord
$fromRev
=
null
)
use
(
$pageId
)
{
return
$this
->
getRevertedCount
(
$pageId
,
$fromRev
);
}
);
case
'minor'
:
// The query for minor counts is inefficient for the database for pages with many revisions.
// If the specified title contains more revisions than allowed, we will return an error.
$editsCount
=
$this
->
getCachedCount
(
'edits'
,
function
(
?
RevisionRecord
$fromRev
=
null
)
use
(
$pageId
)
{
return
$this
->
getEditsCount
(
$pageId
,
$fromRev
);
}
);
if
(
$editsCount
>
self
::
COUNT_LIMITS
[
$type
]
*
2
)
{
throw
new
LocalizedHttpException
(
new
MessageValue
(
'rest-pagehistorycount-too-many-revisions'
),
500
);
}
return
$this
->
getCachedCount
(
$type
,
function
(
?
RevisionRecord
$fromRev
=
null
)
use
(
$pageId
)
{
return
$this
->
getMinorCount
(
$pageId
,
$fromRev
);
}
);
default
:
throw
new
LocalizedHttpException
(
new
MessageValue
(
'rest-pagehistorycount-type-unrecognized'
,
[
new
ScalarParam
(
ParamType
::
PLAINTEXT
,
$type
)
]
),
500
);
}
}
/**
* @return RevisionRecord|null current revision or false if unable to retrieve revision
*/
private
function
getCurrentRevision
():
?
RevisionRecord
{
if
(
$this
->
revision
===
false
)
{
$page
=
$this
->
getPage
();
if
(
$page
)
{
$this
->
revision
=
$this
->
revisionStore
->
getKnownCurrentRevision
(
$page
)
?:
null
;
}
else
{
$this
->
revision
=
null
;
}
}
return
$this
->
revision
;
}
/**
* @return ExistingPageRecord|null
*/
private
function
getPage
():
?
ExistingPageRecord
{
if
(
$this
->
page
===
false
)
{
$this
->
page
=
$this
->
pageLookup
->
getExistingPageByText
(
$this
->
getValidatedParams
()[
'title'
]
);
}
return
$this
->
page
;
}
/**
* Returns latest of 2 timestamps:
* 1. Current revision
* 2. OR entry from the DB logging table for the given page
* @return int|null
*/
protected
function
getLastModified
()
{
$lastModifiedTimes
=
$this
->
getLastModifiedTimes
();
if
(
$lastModifiedTimes
)
{
return
max
(
array_values
(
$lastModifiedTimes
)
);
}
return
null
;
}
/**
* Returns array with 2 timestamps:
* 1. Current revision
* 2. OR entry from the DB logging table for the given page
* @return array|null
*/
protected
function
getLastModifiedTimes
()
{
$currentRev
=
$this
->
getCurrentRevision
();
if
(
!
$currentRev
)
{
return
null
;
}
if
(
$this
->
lastModifiedTimes
===
null
)
{
$currentRevTime
=
(
int
)
wfTimestampOrNull
(
TS_UNIX
,
$currentRev
->
getTimestamp
()
);
$loggingTableTime
=
$this
->
loggingTableTime
(
$currentRev
->
getPageId
()
);
$this
->
lastModifiedTimes
=
[
'currentRevTS'
=>
$currentRevTime
,
'dependencyModTS'
=>
$loggingTableTime
];
}
return
$this
->
lastModifiedTimes
;
}
/**
* Return timestamp of latest entry in logging table for given page id
* @param int $pageId
* @return int|null
*/
private
function
loggingTableTime
(
$pageId
)
{
$res
=
$this
->
dbProvider
->
getReplicaDatabase
()->
newSelectQueryBuilder
()
->
select
(
'MAX(log_timestamp)'
)
->
from
(
'logging'
)
->
where
(
[
'log_page'
=>
$pageId
]
)
->
caller
(
__METHOD__
)->
fetchField
();
return
$res
?
(
int
)
wfTimestamp
(
TS_UNIX
,
$res
)
:
null
;
}
/**
* Choosing to not implement etags in this handler.
* Generating an etag when getting revision counts must account for things like visibility settings
* (e.g. rev_deleted bit) which requires hitting the database anyway. The response for these
* requests are so small that we wouldn't be gaining much efficiency.
* Etags are strong validators and if provided would take precedence over
* last modified time, a weak validator. We want to ensure only last modified time is used
* since it is more efficient than using etags for this particular case.
* @return null
*/
protected
function
getEtag
()
{
return
null
;
}
/**
* @param string $type
* @param callable $fetchCount
* @return int
*/
private
function
getCachedCount
(
$type
,
callable
$fetchCount
)
{
$pageId
=
$this
->
getPage
()->
getId
();
return
$this
->
cache
->
getWithSetCallback
(
$this
->
cache
->
makeKey
(
'rest'
,
'pagehistorycount'
,
$pageId
,
$type
),
WANObjectCache
::
TTL_WEEK
,
function
(
$oldValue
)
use
(
$fetchCount
)
{
$currentRev
=
$this
->
getCurrentRevision
();
if
(
$oldValue
)
{
// Last modified timestamp was NOT a dependency change (e.g. revdel)
$doIncrementalUpdate
=
(
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
$this
->
getLastModified
()
!=
$this
->
getLastModifiedTimes
()[
'dependencyModTS'
]
);
if
(
$doIncrementalUpdate
)
{
$rev
=
$this
->
revisionStore
->
getRevisionById
(
$oldValue
[
'revision'
]
);
if
(
$rev
)
{
$additionalCount
=
$fetchCount
(
$rev
);
return
[
'revision'
=>
$currentRev
->
getId
(),
'count'
=>
$oldValue
[
'count'
]
+
$additionalCount
,
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
'dependencyModTS'
=>
$this
->
getLastModifiedTimes
()[
'dependencyModTS'
]
];
}
}
}
// Nothing was previously stored, or incremental update was done for too long,
// recalculate from scratch.
return
[
'revision'
=>
$currentRev
->
getId
(),
'count'
=>
$fetchCount
(),
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
'dependencyModTS'
=>
$this
->
getLastModifiedTimes
()[
'dependencyModTS'
]
];
},
[
'touchedCallback'
=>
function
(){
return
$this
->
getLastModified
();
},
'version'
=>
2
,
'lockTSE'
=>
WANObjectCache
::
TTL_MINUTE
*
5
]
)[
'count'
];
}
/**
* @param int $pageId the id of the page to load history for
* @param RevisionRecord|null $fromRev
* @return int the count
*/
protected
function
getAnonCount
(
$pageId
,
?
RevisionRecord
$fromRev
=
null
)
{
$dbr
=
$this
->
dbProvider
->
getReplicaDatabase
();
$queryBuilder
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'1'
)
->
from
(
'revision'
)
->
join
(
'actor'
,
null
,
'rev_actor = actor_id'
)
->
where
(
[
'rev_page'
=>
$pageId
,
'actor_user'
=>
null
,
$dbr
->
bitAnd
(
'rev_deleted'
,
RevisionRecord
::
DELETED_TEXT
|
RevisionRecord
::
DELETED_USER
)
=>
0
,
]
)
->
limit
(
self
::
COUNT_LIMITS
[
'anonymous'
]
+
1
);
// extra to detect truncation
if
(
$fromRev
)
{
$queryBuilder
->
andWhere
(
$dbr
->
buildComparison
(
'>'
,
[
'rev_timestamp'
=>
$dbr
->
timestamp
(
$fromRev
->
getTimestamp
()
),
'rev_id'
=>
$fromRev
->
getId
(),
]
)
);
}
return
$queryBuilder
->
caller
(
__METHOD__
)->
fetchRowCount
();
}
/**
* @param int $pageId the id of the page to load history for
* @param RevisionRecord|null $fromRev
* @return int the count
*/
protected
function
getTempCount
(
$pageId
,
?
RevisionRecord
$fromRev
=
null
)
{
if
(
!
$this
->
tempUserConfig
->
isKnown
()
)
{
return
0
;
}
$dbr
=
$this
->
dbProvider
->
getReplicaDatabase
();
$queryBuilder
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'1'
)
->
from
(
'revision'
)
->
join
(
'actor'
,
null
,
'rev_actor = actor_id'
)
->
where
(
[
'rev_page'
=>
$pageId
,
$this
->
tempUserConfig
->
getMatchCondition
(
$dbr
,
'actor_name'
,
IExpression
::
LIKE
),
]
)
->
andWhere
(
[
$dbr
->
bitAnd
(
'rev_deleted'
,
RevisionRecord
::
DELETED_TEXT
|
RevisionRecord
::
DELETED_USER
)
=>
0
]
)
->
limit
(
self
::
COUNT_LIMITS
[
'temporary'
]
+
1
);
// extra to detect truncation
if
(
$fromRev
)
{
$queryBuilder
->
andWhere
(
$dbr
->
buildComparison
(
'>'
,
[
'rev_timestamp'
=>
$dbr
->
timestamp
(
$fromRev
->
getTimestamp
()
),
'rev_id'
=>
$fromRev
->
getId
(),
]
)
);
}
return
$queryBuilder
->
caller
(
__METHOD__
)->
fetchRowCount
();
}
/**
* @param int $pageId the id of the page to load history for
* @param RevisionRecord|null $fromRev
* @return int the count
*/
protected
function
getBotCount
(
$pageId
,
?
RevisionRecord
$fromRev
=
null
)
{
$dbr
=
$this
->
dbProvider
->
getReplicaDatabase
();
$queryBuilder
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'1'
)
->
from
(
'revision'
)
->
join
(
'actor'
,
'actor_rev_user'
,
'actor_rev_user.actor_id = rev_actor'
)
->
where
(
[
'rev_page'
=>
intval
(
$pageId
)
]
)
->
andWhere
(
[
$dbr
->
bitAnd
(
'rev_deleted'
,
RevisionRecord
::
DELETED_TEXT
|
RevisionRecord
::
DELETED_USER
)
=>
0
]
)
->
limit
(
self
::
COUNT_LIMITS
[
'bot'
]
+
1
);
// extra to detect truncation
$subquery
=
$queryBuilder
->
newSubquery
()
->
select
(
'1'
)
->
from
(
'user_groups'
)
->
where
(
[
'actor_rev_user.actor_user = ug_user'
,
'ug_group'
=>
$this
->
groupPermissionsLookup
->
getGroupsWithPermission
(
'bot'
),
$dbr
->
expr
(
'ug_expiry'
,
'='
,
null
)->
or
(
'ug_expiry'
,
'>='
,
$dbr
->
timestamp
()
)
]
);
$queryBuilder
->
andWhere
(
new
RawSQLExpression
(
'EXISTS('
.
$subquery
->
getSQL
()
.
')'
)
);
if
(
$fromRev
)
{
$queryBuilder
->
andWhere
(
$dbr
->
buildComparison
(
'>'
,
[
'rev_timestamp'
=>
$dbr
->
timestamp
(
$fromRev
->
getTimestamp
()
),
'rev_id'
=>
$fromRev
->
getId
(),
]
)
);
}
return
$queryBuilder
->
caller
(
__METHOD__
)->
fetchRowCount
();
}
/**
* @param int $pageId the id of the page to load history for
* @param RevisionRecord|null $fromRev
* @param RevisionRecord|null $toRev
* @return int the count
*/
protected
function
getEditorsCount
(
$pageId
,
?
RevisionRecord
$fromRev
=
null
,
?
RevisionRecord
$toRev
=
null
)
{
[
$fromRev
,
$toRev
]
=
$this
->
orderRevisions
(
$fromRev
,
$toRev
);
return
$this
->
revisionStore
->
countAuthorsBetween
(
$pageId
,
$fromRev
,
$toRev
,
$this
->
getAuthority
(),
self
::
COUNT_LIMITS
[
'editors'
]
);
}
/**
* @param int $pageId the id of the page to load history for
* @param RevisionRecord|null $fromRev
* @return int the count
*/
protected
function
getRevertedCount
(
$pageId
,
?
RevisionRecord
$fromRev
=
null
)
{
$tagIds
=
[];
foreach
(
ChangeTags
::
REVERT_TAGS
as
$tagName
)
{
try
{
$tagIds
[]
=
$this
->
changeTagDefStore
->
getId
(
$tagName
);
}
catch
(
NameTableAccessException
$e
)
{
// If no revisions are tagged with a name, no tag id will be present
}
}
if
(
!
$tagIds
)
{
return
0
;
}
$dbr
=
$this
->
dbProvider
->
getReplicaDatabase
();
$queryBuilder
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'1'
)
->
from
(
'revision'
)
->
join
(
'change_tag'
,
null
,
'ct_rev_id = rev_id'
)
->
where
(
[
'rev_page'
=>
$pageId
,
'ct_tag_id'
=>
$tagIds
,
$dbr
->
bitAnd
(
'rev_deleted'
,
RevisionRecord
::
DELETED_TEXT
)
.
" = 0"
]
)
->
groupBy
(
'rev_id'
)
->
limit
(
self
::
COUNT_LIMITS
[
'reverted'
]
+
1
);
// extra to detect truncation
if
(
$fromRev
)
{
$queryBuilder
->
andWhere
(
$dbr
->
buildComparison
(
'>'
,
[
'rev_timestamp'
=>
$dbr
->
timestamp
(
$fromRev
->
getTimestamp
()
),
'rev_id'
=>
$fromRev
->
getId
(),
]
)
);
}
return
$queryBuilder
->
caller
(
__METHOD__
)->
fetchRowCount
();
}
/**
* @param int $pageId the id of the page to load history for
* @param RevisionRecord|null $fromRev
* @return int the count
*/
protected
function
getMinorCount
(
$pageId
,
?
RevisionRecord
$fromRev
=
null
)
{
$dbr
=
$this
->
dbProvider
->
getReplicaDatabase
();
$queryBuilder
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'1'
)
->
from
(
'revision'
)
->
where
(
[
'rev_page'
=>
$pageId
,
$dbr
->
expr
(
'rev_minor_edit'
,
'!='
,
0
),
$dbr
->
bitAnd
(
'rev_deleted'
,
RevisionRecord
::
DELETED_TEXT
)
.
" = 0"
]
)
->
limit
(
self
::
COUNT_LIMITS
[
'minor'
]
+
1
);
// extra to detect truncation
if
(
$fromRev
)
{
$queryBuilder
->
andWhere
(
$dbr
->
buildComparison
(
'>'
,
[
'rev_timestamp'
=>
$dbr
->
timestamp
(
$fromRev
->
getTimestamp
()
),
'rev_id'
=>
$fromRev
->
getId
(),
]
)
);
}
return
$queryBuilder
->
caller
(
__METHOD__
)->
fetchRowCount
();
}
/**
* @param int $pageId the id of the page to load history for
* @param RevisionRecord|null $fromRev
* @param RevisionRecord|null $toRev
* @return int the count
*/
protected
function
getEditsCount
(
$pageId
,
?
RevisionRecord
$fromRev
=
null
,
?
RevisionRecord
$toRev
=
null
)
{
[
$fromRev
,
$toRev
]
=
$this
->
orderRevisions
(
$fromRev
,
$toRev
);
return
$this
->
revisionStore
->
countRevisionsBetween
(
$pageId
,
$fromRev
,
$toRev
,
self
::
COUNT_LIMITS
[
'edits'
]
// Will be increased by 1 to detect truncation
);
}
/**
* @param int $revId
* @return RevisionRecord
* @throws LocalizedHttpException
*/
private
function
getRevisionOrThrow
(
$revId
)
{
$rev
=
$this
->
revisionStore
->
getRevisionById
(
$revId
);
if
(
!
$rev
)
{
throw
new
LocalizedHttpException
(
new
MessageValue
(
'rest-nonexistent-revision'
,
[
$revId
]
),
404
);
}
return
$rev
;
}
/**
* Reorders revisions if they are present
* @param RevisionRecord|null $fromRev
* @param RevisionRecord|null $toRev
* @return array
* @phan-return array{0:RevisionRecord|null,1:RevisionRecord|null}
*/
private
function
orderRevisions
(
?
RevisionRecord
$fromRev
=
null
,
?
RevisionRecord
$toRev
=
null
)
{
if
(
$fromRev
&&
$toRev
&&
(
$fromRev
->
getTimestamp
()
>
$toRev
->
getTimestamp
()
||
(
$fromRev
->
getTimestamp
()
===
$toRev
->
getTimestamp
()
&&
$fromRev
->
getId
()
>
$toRev
->
getId
()
)
)
)
{
return
[
$toRev
,
$fromRev
];
}
return
[
$fromRev
,
$toRev
];
}
public
function
needsWriteAccess
()
{
return
false
;
}
public
function
getParamSettings
()
{
return
[
'title'
=>
[
self
::
PARAM_SOURCE
=>
'path'
,
ParamValidator
::
PARAM_TYPE
=>
'string'
,
ParamValidator
::
PARAM_REQUIRED
=>
true
,
],
'type'
=>
[
self
::
PARAM_SOURCE
=>
'path'
,
ParamValidator
::
PARAM_TYPE
=>
array_merge
(
array_keys
(
self
::
COUNT_LIMITS
),
array_keys
(
self
::
DEPRECATED_COUNT_TYPES
)
),
ParamValidator
::
PARAM_REQUIRED
=>
true
,
],
'from'
=>
[
self
::
PARAM_SOURCE
=>
'query'
,
ParamValidator
::
PARAM_TYPE
=>
'integer'
,
ParamValidator
::
PARAM_REQUIRED
=>
false
],
'to'
=>
[
self
::
PARAM_SOURCE
=>
'query'
,
ParamValidator
::
PARAM_TYPE
=>
'integer'
,
ParamValidator
::
PARAM_REQUIRED
=>
false
]
];
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 19:14 (5 h, 55 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
75/2a/6194487cb4d4c3e597415b50b9f2
Default Alt Text
PageHistoryCountHandler.php (21 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment