Limit artwork results at the DB level

This commit is contained in:
Alex Cabal 2024-01-28 14:04:29 -06:00
parent d7275074b8
commit a9dcdcde94
4 changed files with 190 additions and 111 deletions

View file

@ -0,0 +1,5 @@
<?
namespace Exceptions;
class PageOutOfBoundsException extends AppException{
}

View file

@ -36,7 +36,13 @@ class Formatter{
return htmlspecialchars(trim($text ?? ''), ENT_QUOTES, 'utf-8'); return htmlspecialchars(trim($text ?? ''), ENT_QUOTES, 'utf-8');
} }
public static function EscapeXhtmlQueryString(?string $text): string{
return str_replace('&', '&amp;', trim($text ?? ''));
}
public static function EscapeXml(?string $text): string{ public static function EscapeXml(?string $text): string{
// Accepts a query string that has already been url-encoded. For example,
// ?foo=bar+baz&x=y
return htmlspecialchars(trim($text ?? ''), ENT_QUOTES|ENT_XML1, 'utf-8'); return htmlspecialchars(trim($text ?? ''), ENT_QUOTES|ENT_XML1, 'utf-8');
} }

View file

@ -160,9 +160,13 @@ class Library{
* @param string $query * @param string $query
* @param string $status * @param string $status
* @param string $sort * @param string $sort
* @return array<Artwork> * @return Array<mixed>
*/ */
public static function FilterArtwork(string $query = null, string $status = null, string $sort = null, int $submitterUserId = null): array{ public static function FilterArtwork(string $query = null, string $status = null, string $sort = null, int $submitterUserId = null, int $page = 1, int $perPage = ARTWORK_PER_PAGE): array{
// Returns an array of:
// ['artworks'] => Array<Artwork>,
// ['artworksCount'] => int
//
// $status is either the string value of an ArtworkStatus enum, or one of these special statuses: // $status is either the string value of an ArtworkStatus enum, or one of these special statuses:
// null: same as "all" // null: same as "all"
// "all": Show all approved and in use artwork // "all": Show all approved and in use artwork
@ -216,6 +220,29 @@ class Library{
// Remove diacritics and non-alphanumeric characters, but preserve apostrophes // Remove diacritics and non-alphanumeric characters, but preserve apostrophes
$query = trim(preg_replace('|[^a-zA-Z0-9\' ]|ius', ' ', Formatter::RemoveDiacritics($query ?? ''))); $query = trim(preg_replace('|[^a-zA-Z0-9\' ]|ius', ' ', Formatter::RemoveDiacritics($query ?? '')));
// We use replace() below because if there's multiple contributors separated by an underscore,
// the underscore won't count as word boundary and we won't get a match.
// See https://github.com/standardebooks/web/pull/325
$limit = $perPage;
$offset = (($page - 1) * $perPage);
if($query == ''){
$artworksCount = Db::QueryInt('
SELECT count(*)
from Artworks art
where ' . $statusCondition, $params);
$params[] = $limit;
$params[] = $offset;
$artworks = Db::Query('
SELECT *
from Artworks art
where ' . $statusCondition . '
limit ?
offset ?', $params, 'Artwork');
}
else{
// Split the query on word boundaries followed by spaces. This keeps words with apostrophes intact. // Split the query on word boundaries followed by spaces. This keeps words with apostrophes intact.
$tokenArray = preg_split('/\b\s+/', $query, -1, PREG_SPLIT_NO_EMPTY); $tokenArray = preg_split('/\b\s+/', $query, -1, PREG_SPLIT_NO_EMPTY);
@ -228,9 +255,29 @@ class Library{
$params[] = $tokenizedQuery; // aan.Name $params[] = $tokenizedQuery; // aan.Name
$params[] = $tokenizedQuery; // t.Name $params[] = $tokenizedQuery; // t.Name
// We use replace() below because if there's multiple contributors separated by an underscore, $artworksCount = Db::QueryInt('
// the underscore won't count as word boundary and we won't get a match. SELECT
// See https://github.com/standardebooks/web/pull/325 count(*)
from
(SELECT distinct
ArtworkId
from
Artworks art
inner join Artists a USING (ArtistId)
left join ArtistAlternateNames aan USING (ArtistId)
left join ArtworkTags at USING (ArtworkId)
left join Tags t USING (TagId)
where
' . $statusCondition . '
and (art.Name regexp ?
or replace(art.EbookUrl, "_", " ") regexp ?
or a.Name regexp ?
or aan.Name regexp ?
or t.Name regexp ?)
group by art.ArtworkId) x', $params);
$params[] = $limit;
$params[] = $offset;
$artworks = Db::Query(' $artworks = Db::Query('
SELECT art.* SELECT art.*
@ -246,9 +293,12 @@ class Library{
or aan.Name regexp ? or aan.Name regexp ?
or t.Name regexp ?) or t.Name regexp ?)
group by art.ArtworkId group by art.ArtworkId
order by ' . $orderBy, $params, 'Artwork'); order by ' . $orderBy . '
limit ?
offset ?', $params, 'Artwork');
}
return $artworks; return ['artworks' => $artworks, 'artworksCount' => $artworksCount];
} }
/** /**

View file

@ -1,9 +1,9 @@
<? <?
$page = HttpInput::Int(GET, 'page') ?? 1; $page = HttpInput::Int(GET, 'page') ?? 1;
$perPage = HttpInput::Int(GET, 'per-page') ?? ARTWORK_PER_PAGE; $perPage = HttpInput::Int(GET, 'per-page') ?? ARTWORK_PER_PAGE;
$query = HttpInput::Str(GET, 'query') ?? ''; $query = HttpInput::Str(GET, 'query');
$queryEbookUrl = HttpInput::Str(GET, 'query-ebook-url'); $queryEbookUrl = HttpInput::Str(GET, 'query-ebook-url');
$status = HttpInput::Str(GET, 'status') ?? null; $status = HttpInput::Str(GET, 'status');
$filterArtworkStatus = $status; $filterArtworkStatus = $status;
$sort = HttpInput::Str(GET, 'sort'); $sort = HttpInput::Str(GET, 'sort');
$pages = 0; $pages = 0;
@ -15,90 +15,108 @@ $isReviewerView = $GLOBALS['User']?->Benefits?->CanReviewArtwork ?? false;
$submitterUserId = $GLOBALS['User']?->Benefits?->CanUploadArtwork ? $GLOBALS['User']->UserId : null; $submitterUserId = $GLOBALS['User']?->Benefits?->CanUploadArtwork ? $GLOBALS['User']->UserId : null;
$isSubmitterView = !$isReviewerView && $submitterUserId !== null; $isSubmitterView = !$isReviewerView && $submitterUserId !== null;
if($page <= 0){ try{
if($page <= 0){
$page = 1; $page = 1;
} }
if($perPage != ARTWORK_PER_PAGE && $perPage != 40 && $perPage != 80){ if($perPage != ARTWORK_PER_PAGE && $perPage != 40 && $perPage != 80){
$perPage = ARTWORK_PER_PAGE; $perPage = ARTWORK_PER_PAGE;
} }
// If we're passed string values that are the same as the defaults, // If we're passed string values that are the same as the defaults,
// set them to null so that we can have cleaner query strings in the navigation footer // set them to null so that we can have cleaner query strings in the navigation footer
if($sort !== null){ if($sort !== null){
$sort = mb_strtolower($sort); $sort = mb_strtolower($sort);
} }
if($sort == 'created-newest'){ if($sort == 'created-newest'){
$sort = null; $sort = null;
} }
if($isReviewerView){ if($isReviewerView){
if($status == 'all' || $status === null){ if($status == 'all' || $status === null){
$filterArtworkStatus = 'all-admin'; $filterArtworkStatus = 'all-admin';
} }
} }
if($isSubmitterView){ if($isSubmitterView){
if($status == 'all' || $status === null){ if($status == 'all' || $status === null){
$filterArtworkStatus = 'all-submitter'; $filterArtworkStatus = 'all-submitter';
} }
if($status == 'unverified'){ if($status == 'unverified'){
$filterArtworkStatus = 'unverified-submitter'; $filterArtworkStatus = 'unverified-submitter';
} }
} }
if(!$isReviewerView && !$isSubmitterView && !in_array($status, array('all', ArtworkStatus::Approved->value, 'in-use'))){ if(!$isReviewerView && !$isSubmitterView && !in_array($status, array('all', ArtworkStatus::Approved->value, 'in-use'))){
$status = ArtworkStatus::Approved->value; $status = ArtworkStatus::Approved->value;
$filterArtworkStatus = $status; $filterArtworkStatus = $status;
} }
if($isReviewerView && !in_array($status, array('all', ArtworkStatus::Unverified->value, ArtworkStatus::Declined->value, ArtworkStatus::Approved->value, 'in-use')) if($isReviewerView && !in_array($status, array('all', ArtworkStatus::Unverified->value, ArtworkStatus::Declined->value, ArtworkStatus::Approved->value, 'in-use'))
&& !in_array($filterArtworkStatus, array('all-admin', ArtworkStatus::Unverified->value, ArtworkStatus::Declined->value, ArtworkStatus::Approved->value, 'in-use'))){ && !in_array($filterArtworkStatus, array('all-admin', ArtworkStatus::Unverified->value, ArtworkStatus::Declined->value, ArtworkStatus::Approved->value, 'in-use'))){
$status = ArtworkStatus::Approved->value; $status = ArtworkStatus::Approved->value;
$filterArtworkStatus = $status; $filterArtworkStatus = $status;
} }
if($isSubmitterView && !in_array($status, array('all', ArtworkStatus::Unverified->value, ArtworkStatus::Approved->value, 'in-use')) if($isSubmitterView && !in_array($status, array('all', ArtworkStatus::Unverified->value, ArtworkStatus::Approved->value, 'in-use'))
&& !in_array($filterArtworkStatus, array('all-submitter', 'unverified-submitter', ArtworkStatus::Approved->value, 'in-use'))){ && !in_array($filterArtworkStatus, array('all-submitter', 'unverified-submitter', ArtworkStatus::Approved->value, 'in-use'))){
$status = ArtworkStatus::Approved->value; $status = ArtworkStatus::Approved->value;
$filterArtworkStatus = $status; $filterArtworkStatus = $status;
} }
if($queryEbookUrl !== null){ if($queryEbookUrl !== null){
$artworks = Db::Query('SELECT * from Artworks where EbookUrl = ? and Status = ?', [$queryEbookUrl, ArtworkStatus::Approved], 'Artwork'); $artworks = Db::Query('SELECT * from Artworks where EbookUrl = ? and Status = ? limit 1', [$queryEbookUrl, ArtworkStatus::Approved], 'Artwork');
} $totalArtworkCount = sizeof($artworks);
else{ }
$artworks = Library::FilterArtwork($query != '' ? $query : null, $filterArtworkStatus, $sort, $submitterUserId); else{
} $result = Library::FilterArtwork($query, $filterArtworkStatus, $sort, $submitterUserId, $page, $perPage);
$artworks = $result['artworks'];
$totalArtworkCount = $result['artworksCount'];
}
$pageTitle = 'Browse Artwork'; $pageTitle = 'Browse Artwork';
$pages = ceil(sizeof($artworks) / $perPage); if($page > 1){
$totalArtworkCount = sizeof($artworks);
$artworks = array_slice($artworks, ($page - 1) * $perPage, $perPage);
if($page > 1){
$pageTitle .= ', page ' . $page; $pageTitle .= ', page ' . $page;
}
$pageDescription = 'Page ' . $page . ' of artwork';
$queryStringParams = [];
if($query !== null && $query != ''){
$queryStringParams['query'] = $query;
}
if($status !== null){
$queryStringParams['status'] = $status;
}
if($sort !== null){
$queryStringParams['sort'] = $sort;
}
if($perPage !== ARTWORK_PER_PAGE){
$queryStringParams['per-page'] = $perPage;
}
$queryString = http_build_query($queryStringParams);
$pages = ceil($totalArtworkCount / $perPage);
if($pages > 0 && $page > $pages){
throw new Exceptions\PageOutOfBoundsException();
}
} }
catch(Exceptions\PageOutOfBoundsException){
$url = '/artworks?page=' . $pages;
if($queryString != ''){
$url .= '&' . $queryString;
}
$pageDescription = 'Page ' . $page . ' of artwork'; header('Location: ' . $url);
exit();
if($query != ''){
$queryString .= '&amp;query=' . urlencode($query);
} }
if($status !== null){
$queryString .= '&amp;status=' . urlencode($status);
}
if($sort !== null){
$queryString .= '&amp;sort=' . urlencode($sort);
}
if($perPage !== ARTWORK_PER_PAGE){
$queryString .= '&amp;per-page=' . urlencode((string)$perPage);
}
?><?= Template::Header(['title' => $pageTitle, 'artwork' => true, 'description' => $pageDescription]) ?> ?><?= Template::Header(['title' => $pageTitle, 'artwork' => true, 'description' => $pageDescription]) ?>
<main class="artworks"> <main class="artworks">
<section class="narrow"> <section class="narrow">
@ -153,13 +171,13 @@ if($perPage !== ARTWORK_PER_PAGE){
<? if($totalArtworkCount > 0){ ?> <? if($totalArtworkCount > 0){ ?>
<nav class="pagination"> <nav class="pagination">
<a<? if($page > 1){ ?> href="/artworks?page=<?= $page - 1 ?><? if($queryString != ''){ ?><?= $queryString ?><? } ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a> <a<? if($page > 1){ ?> href="/artworks?page=<?= $page - 1 ?><? if($queryString != ''){ ?><?= Formatter::EscapeXhtmlQueryString('&' . $queryString) ?><? } ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a>
<ol> <ol>
<? for($i = 1; $i < $pages + 1; $i++){ ?> <? for($i = 1; $i < $pages + 1; $i++){ ?>
<li<? if($page == $i){ ?> class="highlighted"<? } ?>><a href="/artworks?page=<?= $i ?><? if($queryString != ''){ ?><?= $queryString ?><? } ?>"><?= $i ?></a></li> <li<? if($page == $i){ ?> class="highlighted"<? } ?>><a href="/artworks?page=<?= $i ?><? if($queryString != ''){ ?><?= Formatter::EscapeXhtmlQueryString('&' . $queryString) ?><? } ?>"><?= $i ?></a></li>
<? } ?> <? } ?>
</ol> </ol>
<a<? if($page < ceil($totalArtworkCount / $perPage)){ ?> href="/artworks?page=<?= $page + 1 ?><? if($queryString != ''){ ?><?= $queryString ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a> <a<? if($page < ceil($totalArtworkCount / $perPage)){ ?> href="/artworks?page=<?= $page + 1 ?><? if($queryString != ''){ ?><?= Formatter::EscapeXhtmlQueryString('&' . $queryString) ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
</nav> </nav>
<? } ?> <? } ?>
</section> </section>