1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-26 07:20:57 +01:00
phorge-phorge/src/applications/notification/query/PhabricatorNotificationQuery.php
epriestley a4a1143b18 Fix an issue where internal paging of notifications could fail if some notifications are not visible
Summary:
Ref T13266. See <https://discourse.phabricator-community.org/t/notification-page-throws-unrecoverable-fatal-error/2651/>.

The "notifications" query currently uses offset paging for no apparent reason (just a legacy issue?), so some of the paging code is only reachable internally.

  - Stop it from using offset paging, since modern cursor paging is fine here (and Feed has used cursor paging for a long time).
  - Fix the non-offset paging to work like Feed.

Also:

  - Remove a couple of stub methods with no callsites after cursor refactoring.

Test Plan:
  - Set things up so I had more than 100 notifications and some in the first 100 were policy filtered, to reproduce the issue (I just made `FeedStory` return `NO_ONE` as a visibility policy).
  - Applied the patch, notifications now page cleanly.
  - Verified that "Next Page" took me to the right place in the result list.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: hskiba

Maniphest Tasks: T13266

Differential Revision: https://secure.phabricator.com/D20455
2019-04-23 11:45:04 -07:00

169 lines
4 KiB
PHP

<?php
/**
* @task config Configuring the Query
* @task exec Query Execution
*/
final class PhabricatorNotificationQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $userPHIDs;
private $keys;
private $unread;
/* -( Configuring the Query )---------------------------------------------- */
public function withUserPHIDs(array $user_phids) {
$this->userPHIDs = $user_phids;
return $this;
}
public function withKeys(array $keys) {
$this->keys = $keys;
return $this;
}
/**
* Filter results by read/unread status. Note that `true` means to return
* only unread notifications, while `false` means to return only //read//
* notifications. The default is `null`, which returns both.
*
* @param mixed True or false to filter results by read status. Null to remove
* the filter.
* @return this
* @task config
*/
public function withUnread($unread) {
$this->unread = $unread;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
protected function loadPage() {
$story_table = new PhabricatorFeedStoryData();
$notification_table = new PhabricatorFeedStoryNotification();
$conn = $story_table->establishConnection('r');
$data = queryfx_all(
$conn,
'SELECT story.*, notification.hasViewed FROM %R notification
JOIN %R story ON notification.chronologicalKey = story.chronologicalKey
%Q
ORDER BY notification.chronologicalKey DESC
%Q',
$notification_table,
$story_table,
$this->buildWhereClause($conn),
$this->buildLimitClause($conn));
$viewed_map = ipull($data, 'hasViewed', 'chronologicalKey');
$stories = PhabricatorFeedStory::loadAllFromRows(
$data,
$this->getViewer());
foreach ($stories as $key => $story) {
$story->setHasViewed($viewed_map[$key]);
}
return $stories;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->userPHIDs !== null) {
$where[] = qsprintf(
$conn,
'notification.userPHID IN (%Ls)',
$this->userPHIDs);
}
if ($this->unread !== null) {
$where[] = qsprintf(
$conn,
'notification.hasViewed = %d',
(int)!$this->unread);
}
if ($this->keys !== null) {
$where[] = qsprintf(
$conn,
'notification.chronologicalKey IN (%Ls)',
$this->keys);
}
return $where;
}
protected function willFilterPage(array $stories) {
foreach ($stories as $key => $story) {
if (!$story->isVisibleInNotifications()) {
unset($stories[$key]);
}
}
return $stories;
}
protected function getDefaultOrderVector() {
return array('key');
}
public function getBuiltinOrders() {
return array(
'newest' => array(
'vector' => array('key'),
'name' => pht('Creation (Newest First)'),
'aliases' => array('created'),
),
'oldest' => array(
'vector' => array('-key'),
'name' => pht('Creation (Oldest First)'),
),
);
}
public function getOrderableColumns() {
return array(
'key' => array(
'table' => 'notification',
'column' => 'chronologicalKey',
'type' => 'string',
'unique' => true,
),
);
}
protected function applyExternalCursorConstraintsToQuery(
PhabricatorCursorPagedPolicyAwareQuery $subquery,
$cursor) {
$subquery->withKeys(array($cursor));
}
protected function newExternalCursorStringForResult($object) {
return $object->getChronologicalKey();
}
protected function newPagingMapFromPartialObject($object) {
return array(
'key' => $object->getChronologicalKey(),
);
}
protected function getPrimaryTableAlias() {
return 'notification';
}
public function getQueryApplicationClass() {
return 'PhabricatorNotificationsApplication';
}
}