mirror of
https://github.com/standardebooks/web.git
synced 2025-07-12 01:22:23 -04:00
Limit artwork results at the DB level
This commit is contained in:
parent
d7275074b8
commit
a9dcdcde94
4 changed files with 190 additions and 111 deletions
5
lib/Exceptions/PageOutOfBoundsException.php
Normal file
5
lib/Exceptions/PageOutOfBoundsException.php
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class PageOutOfBoundsException extends AppException{
|
||||||
|
}
|
|
@ -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('&', '&', 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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
110
lib/Library.php
110
lib/Library.php
|
@ -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,39 +220,85 @@ 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 ?? '')));
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Join the tokens with '|' to search on any token, but add word boundaries to force the full token to match
|
|
||||||
$tokenizedQuery = '\b(' . implode('|', $tokenArray) . ')\b';
|
|
||||||
|
|
||||||
$params[] = $tokenizedQuery; // art.Name
|
|
||||||
$params[] = $tokenizedQuery; // art.EbookUrl
|
|
||||||
$params[] = $tokenizedQuery; // a.Name
|
|
||||||
$params[] = $tokenizedQuery; // aan.Name
|
|
||||||
$params[] = $tokenizedQuery; // t.Name
|
|
||||||
|
|
||||||
// We use replace() below because if there's multiple contributors separated by an underscore,
|
// 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.
|
// the underscore won't count as word boundary and we won't get a match.
|
||||||
// See https://github.com/standardebooks/web/pull/325
|
// See https://github.com/standardebooks/web/pull/325
|
||||||
|
$limit = $perPage;
|
||||||
|
$offset = (($page - 1) * $perPage);
|
||||||
|
|
||||||
$artworks = Db::Query('
|
if($query == ''){
|
||||||
SELECT art.*
|
$artworksCount = Db::QueryInt('
|
||||||
from Artworks art
|
SELECT count(*)
|
||||||
inner join Artists a using (ArtistId)
|
from Artworks art
|
||||||
left join ArtistAlternateNames aan using (ArtistId)
|
where ' . $statusCondition, $params);
|
||||||
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
|
|
||||||
order by ' . $orderBy, $params, 'Artwork');
|
|
||||||
|
|
||||||
return $artworks;
|
$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.
|
||||||
|
$tokenArray = preg_split('/\b\s+/', $query, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
|
||||||
|
// Join the tokens with '|' to search on any token, but add word boundaries to force the full token to match
|
||||||
|
$tokenizedQuery = '\b(' . implode('|', $tokenArray) . ')\b';
|
||||||
|
|
||||||
|
$params[] = $tokenizedQuery; // art.Name
|
||||||
|
$params[] = $tokenizedQuery; // art.EbookUrl
|
||||||
|
$params[] = $tokenizedQuery; // a.Name
|
||||||
|
$params[] = $tokenizedQuery; // aan.Name
|
||||||
|
$params[] = $tokenizedQuery; // t.Name
|
||||||
|
|
||||||
|
$artworksCount = Db::QueryInt('
|
||||||
|
SELECT
|
||||||
|
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('
|
||||||
|
SELECT art.*
|
||||||
|
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
|
||||||
|
order by ' . $orderBy . '
|
||||||
|
limit ?
|
||||||
|
offset ?', $params, 'Artwork');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['artworks' => $artworks, 'artworksCount' => $artworksCount];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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{
|
||||||
$page = 1;
|
if($page <= 0){
|
||||||
}
|
$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($status == 'all' || $status === null){
|
||||||
|
$filterArtworkStatus = 'all-submitter';
|
||||||
|
}
|
||||||
|
if($status == 'unverified'){
|
||||||
|
$filterArtworkStatus = 'unverified-submitter';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$isReviewerView && !$isSubmitterView && !in_array($status, array('all', ArtworkStatus::Approved->value, 'in-use'))){
|
||||||
|
$status = ArtworkStatus::Approved->value;
|
||||||
|
$filterArtworkStatus = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
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'))){
|
||||||
|
$status = ArtworkStatus::Approved->value;
|
||||||
|
$filterArtworkStatus = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
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'))){
|
||||||
|
$status = ArtworkStatus::Approved->value;
|
||||||
|
$filterArtworkStatus = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($queryEbookUrl !== null){
|
||||||
|
$artworks = Db::Query('SELECT * from Artworks where EbookUrl = ? and Status = ? limit 1', [$queryEbookUrl, ArtworkStatus::Approved], 'Artwork');
|
||||||
|
$totalArtworkCount = sizeof($artworks);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$result = Library::FilterArtwork($query, $filterArtworkStatus, $sort, $submitterUserId, $page, $perPage);
|
||||||
|
$artworks = $result['artworks'];
|
||||||
|
$totalArtworkCount = $result['artworksCount'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = 'Browse Artwork';
|
||||||
|
if($page > 1){
|
||||||
|
$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){
|
||||||
if($isSubmitterView){
|
$url = '/artworks?page=' . $pages;
|
||||||
if($status == 'all' || $status === null){
|
if($queryString != ''){
|
||||||
$filterArtworkStatus = 'all-submitter';
|
$url .= '&' . $queryString;
|
||||||
}
|
|
||||||
if($status == 'unverified'){
|
|
||||||
$filterArtworkStatus = 'unverified-submitter';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header('Location: ' . $url);
|
||||||
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!$isReviewerView && !$isSubmitterView && !in_array($status, array('all', ArtworkStatus::Approved->value, 'in-use'))){
|
|
||||||
$status = ArtworkStatus::Approved->value;
|
|
||||||
$filterArtworkStatus = $status;
|
|
||||||
}
|
|
||||||
|
|
||||||
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'))){
|
|
||||||
$status = ArtworkStatus::Approved->value;
|
|
||||||
$filterArtworkStatus = $status;
|
|
||||||
}
|
|
||||||
|
|
||||||
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'))){
|
|
||||||
$status = ArtworkStatus::Approved->value;
|
|
||||||
$filterArtworkStatus = $status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if($queryEbookUrl !== null){
|
|
||||||
$artworks = Db::Query('SELECT * from Artworks where EbookUrl = ? and Status = ?', [$queryEbookUrl, ArtworkStatus::Approved], 'Artwork');
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
$artworks = Library::FilterArtwork($query != '' ? $query : null, $filterArtworkStatus, $sort, $submitterUserId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pageTitle = 'Browse Artwork';
|
|
||||||
$pages = ceil(sizeof($artworks) / $perPage);
|
|
||||||
$totalArtworkCount = sizeof($artworks);
|
|
||||||
$artworks = array_slice($artworks, ($page - 1) * $perPage, $perPage);
|
|
||||||
|
|
||||||
if($page > 1){
|
|
||||||
$pageTitle .= ', page ' . $page;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pageDescription = 'Page ' . $page . ' of artwork';
|
|
||||||
|
|
||||||
if($query != ''){
|
|
||||||
$queryString .= '&query=' . urlencode($query);
|
|
||||||
}
|
|
||||||
|
|
||||||
if($status !== null){
|
|
||||||
$queryString .= '&status=' . urlencode($status);
|
|
||||||
}
|
|
||||||
|
|
||||||
if($sort !== null){
|
|
||||||
$queryString .= '&sort=' . urlencode($sort);
|
|
||||||
}
|
|
||||||
|
|
||||||
if($perPage !== ARTWORK_PER_PAGE){
|
|
||||||
$queryString .= '&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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue