diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php index 41de7069..b5de07c9 100644 --- a/src/Controllers/PostController.php +++ b/src/Controllers/PostController.php @@ -423,7 +423,6 @@ class PostController PrivilegesHelper::confirmWithException(Privilege::ViewPost); PrivilegesHelper::confirmWithException(Privilege::ViewPost, PostSafety::toString($post->safety)); - PostSearchService::enableTokenLimit(false); try { $this->context->transport->lastSearchQuery = InputHelper::get('last-search-query'); @@ -439,7 +438,6 @@ class PostController PostSearchService::getPostIdsAround( $this->context->transport->lastSearchQuery, $id); } - PostSearchService::enableTokenLimit(true); $favorite = $this->context->user->hasFavorited($post); $score = $this->context->user->getScore($post); diff --git a/src/Controllers/TagController.php b/src/Controllers/TagController.php index 737f64b6..efd014a1 100644 --- a/src/Controllers/TagController.php +++ b/src/Controllers/TagController.php @@ -12,8 +12,8 @@ class TagController public function listAction($filter = null, $page = 1) { $this->context->viewName = 'tag-list-wrapper'; - PrivilegesHelper::confirmWithException(Privilege::ListTags); + $suppliedFilter = $filter ?: InputHelper::get('filter') ?: 'order:alpha,asc'; $page = max(1, intval($page)); $tagsPerPage = intval($this->config->browsing->tagsPerPage); @@ -22,6 +22,7 @@ class TagController $tagCount = TagSearchService::getEntityCount($suppliedFilter); $pageCount = ceil($tagCount / $tagsPerPage); $page = min($pageCount, $page); + $this->context->filter = $suppliedFilter; $this->context->transport->tags = $tags; diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php index 2c6ca8d5..450c5074 100644 --- a/src/Controllers/UserController.php +++ b/src/Controllers/UserController.php @@ -100,35 +100,32 @@ class UserController /** * @route /users * @route /users/{page} - * @route /users/{sortStyle} - * @route /users/{sortStyle}/{page} - * @validate sortStyle alpha|alpha,asc|alpha,desc|date,asc|date,desc|pending + * @route /users/{filter} + * @route /users/{filter}/{page} + * @validate filter [a-zA-Z\32:,_-]+ * @validate page [0-9]+ */ - public function listAction($sortStyle, $page) + public function listAction($filter, $page) { - if ($sortStyle == '' or $sortStyle == 'alpha') - $sortStyle = 'alpha,asc'; - if ($sortStyle == 'date') - $sortStyle = 'date,asc'; - - $page = intval($page); - $usersPerPage = intval($this->config->browsing->usersPerPage); PrivilegesHelper::confirmWithException(Privilege::ListUsers); - $page = max(1, $page); - $users = UserSearchService::getEntities($sortStyle, $usersPerPage, $page); - $userCount = UserSearchService::getEntityCount($sortStyle); - $pageCount = ceil($userCount / $usersPerPage); + $suppliedFilter = $filter ?: InputHelper::get('filter') ?: 'order:alpha,asc'; + $page = max(1, intval($page)); + $usersPerPage = intval($this->config->browsing->usersPerPage); - $this->context->sortStyle = $sortStyle; + $users = UserSearchService::getEntities($suppliedFilter, $usersPerPage, $page); + $userCount = UserSearchService::getEntityCount($suppliedFilter); + $pageCount = ceil($userCount / $usersPerPage); + $page = min($pageCount, $page); + + $this->context->filter = $suppliedFilter; + $this->context->transport->users = $users; $this->context->transport->paginator = new StdClass; $this->context->transport->paginator->page = $page; $this->context->transport->paginator->pageCount = $pageCount; $this->context->transport->paginator->entityCount = $userCount; $this->context->transport->paginator->entities = $users; $this->context->transport->paginator->params = func_get_args(); - $this->context->transport->users = $users; } diff --git a/src/Models/SearchParsers/AbstractSearchParser.php b/src/Models/SearchParsers/AbstractSearchParser.php new file mode 100644 index 00000000..6506e514 --- /dev/null +++ b/src/Models/SearchParsers/AbstractSearchParser.php @@ -0,0 +1,99 @@ +statement = $statement; + + $tokens = preg_split('/\s+/', $filterString); + $tokens = array_filter($tokens); + $tokens = array_unique($tokens); + $this->processSetup($tokens); + + foreach ($tokens as $token) + { + $neg = false; + if ($token{0} == '-') + { + $token = substr($token, 1); + $neg = true; + } + + if (strpos($token, ':') !== false) + { + list ($key, $value) = explode(':', $token, 2); + $key = strtolower($key); + + if ($key == 'order') + { + $this->internalProcessOrderToken($value, $neg); + } + else + { + if (!$this->processComplexToken($key, $value, $neg)) + throw new SimpleException('Invalid search token: ' . $key); + } + } + else + { + if (!$this->processSimpleToken($token, $neg)) + throw new SimpleException('Invalid search token: ' . $token); + } + } + $this->processTeardown(); + } + + protected function processSetup(&$tokens) + { + } + + protected function processTeardown() + { + } + + protected function internalProcessOrderToken($orderToken, $neg) + { + $arr = preg_split('/[;,]/', $orderToken); + if (count($arr) == 1) + $arr []= 'asc'; + + if (count($arr) != 2) + throw new SimpleException('Invalid search order token: ' . $orderToken); + + $orderByString = strtolower(array_shift($arr)); + $orderDirString = strtolower(array_shift($arr)); + if ($orderDirString == 'asc') + $orderDir = SqlSelectStatement::ORDER_ASC; + elseif ($orderDirString == 'desc') + $orderDir = SqlSelectStatement::ORDER_DESC; + else + throw new SimpleException('Invalid search order direction: ' . $searchOrderDir); + + if ($neg) + { + $orderDir = $orderDir == SqlSelectStatement::ORDER_ASC + ? SqlSelectStatement::ORDER_DESC + : SqlSelectStatement::ORDER_ASC; + } + + if (!$this->processOrderToken($orderByString, $orderDir)) + throw new SimpleException('Invalid search order type: ' . $orderbyString); + } + + protected function processComplexToken($key, $value, $neg) + { + return false; + } + + protected function processSimpleToken($value, $neg) + { + return false; + } + + protected function processOrderToken($orderToken, $orderDir) + { + return false; + } +} diff --git a/src/Models/SearchParsers/CommentSearchParser.php b/src/Models/SearchParsers/CommentSearchParser.php new file mode 100644 index 00000000..7eb5892b --- /dev/null +++ b/src/Models/SearchParsers/CommentSearchParser.php @@ -0,0 +1,16 @@ +statement->addInnerJoin('post', new SqlEqualsOperator('post_id', 'post.id')); + + $allowedSafety = PrivilegesHelper::getAllowedSafety(); + $this->statement->setCriterion(new SqlConjunction()); + $this->statement->getCriterion()->add(SqlInOperator::fromArray('post.safety', SqlBinding::fromArray($allowedSafety))); + if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden')) + $this->statement->getCriterion()->add(new SqlNegationOperator(new SqlStringExpression('hidden'))); + + $this->statement->addOrderBy('comment.id', SqlSelectStatement::ORDER_DESC); + } +} diff --git a/src/Models/SearchParsers/PostSearchParser.php b/src/Models/SearchParsers/PostSearchParser.php new file mode 100644 index 00000000..f0936d7f --- /dev/null +++ b/src/Models/SearchParsers/PostSearchParser.php @@ -0,0 +1,258 @@ +tags = []; + $this->statement->setCriterion(new SqlConjunction()); + + $allowedSafety = PrivilegesHelper::getAllowedSafety(); + $this->statement->getCriterion()->add(SqlInOperator::fromArray('safety', SqlBinding::fromArray($allowedSafety))); + + if (\Chibi\Registry::getContext()->user->hasEnabledHidingDislikedPosts() and !in_array('special:disliked', array_map('strtolower', $tokens))) + $this->processComplexToken('special', 'disliked', true); + + if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden') or !in_array('special:hidden', array_map('strtolower', $tokens))) + $this->processComplexToken('special', 'hidden', true); + + if (count($tokens) > $config->browsing->maxSearchTokens) + throw new SimpleException('Too many search tokens (maximum: ' . $config->browsing->maxSearchTokens . ')'); + } + + protected function processTeardown() + { + foreach ($this->tags as $item) + { + list ($tagName, $neg) = $item; + $tag = TagModel::findByName($tagName); + $innerStmt = new SqlSelectStatement(); + $innerStmt->setTable('post_tag'); + $innerStmt->setCriterion((new SqlConjunction) + ->add(new SqlEqualsOperator('post_id', 'post.id')) + ->add(new SqlEqualsOperator('post_tag.tag_id', new SqlBinding($tag->id)))); + $operator = new SqlExistsOperator($innerStmt); + if ($neg) + $operator = new SqlNegationOperator($operator); + $this->statement->getCriterion()->add($operator); + } + + $this->statement->addOrderBy('id', + empty($this->statement->getOrderBy()) + ? SqlSelectStatement::ORDER_DESC + : $this->statement->getOrderBy()[0][1]); + } + + protected function processSimpleToken($value, $neg) + { + $this->tags []= [$value, $neg]; + return true; + } + + protected static function getCriterionForComplexToken($key, $value) + { + if (in_array($key, ['id'])) + { + $ids = preg_split('/[;,]/', $value); + $ids = array_map('intval', $ids); + return SqlInOperator::fromArray('id', SqlBinding::fromArray($ids)); + } + + elseif (in_array($key, ['fav', 'favs'])) + { + $user = UserModel::findByNameOrEmail($value); + $innerStmt = (new SqlSelectStatement) + ->setTable('favoritee') + ->setCriterion((new SqlConjunction) + ->add(new SqlEqualsOperator('post_id', 'post.id')) + ->add(new SqlEqualsOperator('favoritee.user_id', new SqlBinding($user->id)))); + return new SqlExistsOperator($innerStmt); + } + + elseif (in_array($key, ['comment', 'commenter'])) + { + $user = UserModel::findByNameOrEmail($value); + $innerStmt = (new SqlSelectStatement) + ->setTable('comment') + ->setCriterion((new SqlConjunction) + ->add(new SqlEqualsOperator('post_id', 'post.id')) + ->add(new SqlEqualsOperator('commenter_id', new SqlBinding($user->id)))); + return new SqlExistsOperator($innerStmt); + } + + elseif (in_array($key, ['submit', 'upload', 'uploader', 'uploaded'])) + { + $user = UserModel::findByNameOrEmail($value); + return new SqlEqualsOperator('uploader_id', new SqlBinding($user->id)); + } + + elseif (in_array($key, ['idmin'])) + return new SqlEqualsOrGreaterOperator('id', new SqlBinding(intval($value))); + + elseif (in_array($key, ['idmax'])) + return new SqlEqualsOrLesserOperator('id', new SqlBinding(intval($value))); + + elseif (in_array($key, ['scoremin'])) + return new SqlEqualsOrGreaterOperator('score', new SqlBinding(intval($value))); + + elseif (in_array($key, ['scoremax'])) + return new SqlEqualsOrLesserOperator('score', new SqlBinding(intval($value))); + + elseif (in_array($key, ['tagmin'])) + return new SqlEqualsOrGreaterOperator('tag_count', new SqlBinding(intval($value))); + + elseif (in_array($key, ['tagmax'])) + return new SqlEqualsOrLesserOperator('tag_count', new SqlBinding(intval($value))); + + elseif (in_array($key, ['favmin'])) + return new SqlEqualsOrGreaterOperator('fav_count', new SqlBinding(intval($value))); + + elseif (in_array($key, ['favmax'])) + return new SqlEqualsOrLesserOperator('fav_count', new SqlBinding(intval($value))); + + elseif (in_array($key, ['commentmin'])) + return new SqlEqualsOrGreaterOperator('comment_count', new SqlBinding(intval($value))); + + elseif (in_array($key, ['commentmax'])) + return new SqlEqualsOrLesserOperator('comment_count', new SqlBinding(intval($value))); + + elseif (in_array($key, ['datemin', 'date'])) + { + list ($dateMin, $dateMax) = self::parseDate($value); + return new SqlEqualsOrGreaterOperator('upload_date', new SqlBinding($dateMin)); + } + + elseif (in_array($key, ['datemax', 'date'])) + { + list ($dateMin, $dateMax) = self::parseDate($value); + return new SqlEqualsOrLesserOperator('upload_date', new SqlBinding($dateMax)); + } + + elseif ($key == 'special') + { + $context = \Chibi\Registry::getContext(); + $value = strtolower($value); + if (in_array($value, ['liked', 'likes'])) + { + $innerStmt = new SqlSelectStatement(); + $innerStmt->setTable('post_score'); + $innerStmt->setCriterion((new SqlConjunction) + ->add(new SqlGreaterOperator('score', '0')) + ->add(new SqlEqualsOperator('post_id', 'post.id')) + ->add(new SqlEqualsOperator('user_id', new SqlBinding($context->user->id)))); + return new SqlExistsOperator($innerStmt); + } + + elseif (in_array($value, ['disliked', 'dislikes'])) + { + $innerStmt = new SqlSelectStatement(); + $innerStmt->setTable('post_score'); + $innerStmt->setCriterion((new SqlConjunction) + ->add(new SqlLesserOperator('score', '0')) + ->add(new SqlEqualsOperator('post_id', 'post.id')) + ->add(new SqlEqualsOperator('user_id', new SqlBinding($context->user->id)))); + return new SqlExistsOperator($innerStmt); + } + + elseif ($value == 'hidden') + return new SqlStringExpression('hidden'); + + else + throw new SimpleException('Invalid special token: ' . $value); + } + + elseif ($key == 'type') + { + $value = strtolower($value); + if ($value == 'swf') + $type = PostType::Flash; + elseif ($value == 'img') + $type = PostType::Image; + elseif ($value == 'yt' or $value == 'youtube') + $type = PostType::Youtube; + else + throw new SimpleException('Invalid post type: ' . $value); + + return new SqlEqualsOperator('type', new SqlBinding($type)); + } + + return null; + } + + protected function processComplexToken($key, $value, $neg) + { + $criterion = self::getCriterionForComplexToken($key, $value); + if (!$criterion) + return false; + + if ($neg) + $criterion = new SqlNegationOperator($criterion); + + $this->statement->getCriterion()->add($criterion); + return true; + } + + protected function processOrderToken($orderByString, $orderDir) + { + $randomReset = true; + + if (in_array($orderByString, ['id'])) + $orderColumn = 'id'; + + elseif (in_array($orderByString, ['date'])) + $orderColumn = 'upload_date'; + + elseif (in_array($orderByString, ['comment', 'comments', 'commentcount', 'comment_count'])) + $orderColumn = 'comment_count'; + + elseif (in_array($orderByString, ['fav', 'favs', 'favcount', 'fav_count'])) + $orderColumn = 'fav_count'; + + elseif (in_array($orderByString, ['score'])) + $orderColumn = 'score'; + + elseif (in_array($orderByString, ['tag', 'tags', 'tagcount', 'tag_count'])) + $orderColumn = 'tag_count'; + + elseif ($orderByString == 'random') + { + //seeding works like this: if you visit anything + //that triggers order other than random, the seed + //is going to reset. however, it stays the same as + //long as you keep visiting pages with order:random + //specified. + $randomReset = false; + if (!isset($_SESSION['browsing-seed'])) + $_SESSION['browsing-seed'] = mt_rand(); + $seed = $_SESSION['browsing-seed']; + $orderColumn = 'SUBSTR(id * ' . $seed .', LENGTH(id) + 2)'; + } + + else + return false; + + if ($randomReset and isset($_SESSION['browsing-seed'])) + unset($_SESSION['browsing-seed']); + + $this->statement->setOrderBy($orderColumn, $orderDir); + return true; + } + + protected static function parseDate($value) + { + list ($year, $month, $day) = explode('-', $value . '-0-0'); + $yearMin = $yearMax = intval($year); + $monthMin = $monthMax = intval($month); + $monthMin = $monthMin ?: 1; + $monthMax = $monthMax ?: 12; + $dayMin = $dayMax = intval($day); + $dayMin = $dayMin ?: 1; + $dayMax = $dayMax ?: intval(date('t', mktime(0, 0, 0, $monthMax, 1, $year))); + $timeMin = mktime(0, 0, 0, $monthMin, $dayMin, $yearMin); + $timeMax = mktime(0, 0, -1, $monthMax, $dayMax+1, $yearMax); + return [$timeMin, $timeMax]; + } +} diff --git a/src/Models/SearchParsers/TagSearchParser.php b/src/Models/SearchParsers/TagSearchParser.php new file mode 100644 index 00000000..6b5098e9 --- /dev/null +++ b/src/Models/SearchParsers/TagSearchParser.php @@ -0,0 +1,37 @@ +statement + ->addInnerJoin('post_tag', new SqlEqualsOperator('tag.id', 'post_tag.tag_id')) + ->addInnerJoin('post', new SqlEqualsOperator('post.id', 'post_tag.post_id')) + ->setCriterion((new SqlConjunction)->add(SqlInOperator::fromArray('safety', SqlBinding::fromArray($allowedSafety)))) + ->groupBy('tag.id'); + } + + protected function processSimpleToken($value, $neg) + { + if ($neg) + return false; + + if (strlen($value) >= 3) + $value = '%' . $value; + $value .= '%'; + + $this->statement->getCriterion()->add(new SqlNoCaseOperator(new SqlLikeOperator('tag.name', new SqlBinding($value)))); + return true; + } + + protected function processOrderToken($orderByString, $orderDir) + { + if ($orderByString == 'popularity') + $this->statement->setOrderBy('post_count', $orderDir); + elseif ($orderByString == 'alpha') + $this->statement->setOrderBy('tag.name', $orderDir); + else + return false; + return true; + } +} diff --git a/src/Models/SearchParsers/UserSearchParser.php b/src/Models/SearchParsers/UserSearchParser.php new file mode 100644 index 00000000..cc5bc08d --- /dev/null +++ b/src/Models/SearchParsers/UserSearchParser.php @@ -0,0 +1,30 @@ +statement->setCriterion((new SqlDisjunction) + ->add(new SqlIsNullOperator('staff_confirmed')) + ->add(new SqlEqualsOperator('staff_confirmed', '0'))); + return true; + } + return false; + } + + protected function processOrderToken($orderByString, $orderDir) + { + if ($orderByString == 'alpha') + $this->statement->setOrderBy(new SqlNoCaseOperator('name'), $orderDir); + elseif ($orderByString == 'date') + $this->statement->setOrderBy('join_date', $orderDir); + else + return false; + + return true; + } +} diff --git a/src/Models/SearchServices/AbstractSearchService.php b/src/Models/SearchServices/AbstractSearchService.php index af674c28..37f2ef4c 100644 --- a/src/Models/SearchServices/AbstractSearchService.php +++ b/src/Models/SearchServices/AbstractSearchService.php @@ -8,9 +8,21 @@ abstract class AbstractSearchService return $modelClassName; } - protected static function decorate(SqlSelectStatement $stmt, $searchQuery) + protected static function getParserClassName() + { + $searchServiceClassName = get_called_class(); + $parserClassName = str_replace('SearchService', 'SearchParser', $searchServiceClassName); + return $parserClassName; + } + + protected static function decorateParser(SqlSelectStatement $stmt, $searchQuery) + { + $parserClassName = self::getParserClassName(); + (new $parserClassName)->decorate($stmt, $searchQuery); + } + + protected static function decorateCustom(SqlSelectStatement $stmt) { - throw new NotImplementedException(); } protected static function decoratePager(SqlSelectStatement $stmt, $perPage, $page) @@ -29,11 +41,12 @@ abstract class AbstractSearchService $stmt = new SqlSelectStatement(); $stmt->setColumn($table . '.*'); - static::decorate($stmt, $searchQuery); + $stmt->setTable($table); + static::decorateParser($stmt, $searchQuery); + static::decorateCustom($stmt); static::decoratePager($stmt, $perPage, $page); - $rows = Database::fetchAll($stmt); - return $rows; + return Database::fetchAll($stmt); } public static function getEntities($searchQuery, $perPage = null, $page = 1) @@ -49,7 +62,9 @@ abstract class AbstractSearchService $table = $modelClassName::getTableName(); $innerStmt = new SqlSelectStatement(); - static::decorate($innerStmt, $searchQuery); + $innerStmt->setTable($table); + static::decorateParser($innerStmt, $searchQuery); + static::decorateCustom($innerStmt); $stmt = new SqlSelectStatement(); $stmt->setColumn(new SqlAliasOperator(new SqlCountOperator('1'), 'count')); diff --git a/src/Models/SearchServices/CommentSearchService.php b/src/Models/SearchServices/CommentSearchService.php index 45f290ec..5d5e18c5 100644 --- a/src/Models/SearchServices/CommentSearchService.php +++ b/src/Models/SearchServices/CommentSearchService.php @@ -1,17 +1,4 @@ setTable('comment'); - $stmt->addInnerJoin('post', new SqlEqualsOperator('post_id', 'post.id')); - - $allowedSafety = PrivilegesHelper::getAllowedSafety(); - $stmt->setCriterion(new SqlConjunction()); - $stmt->getCriterion()->add(SqlInOperator::fromArray('post.safety', SqlBinding::fromArray($allowedSafety))); - if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden')) - $stmt->getCriterion()->add(new SqlNegationOperator(new SqlStringExpression('hidden'))); - - $stmt->addOrderBy('comment.id', SqlSelectStatement::ORDER_DESC); - } } diff --git a/src/Models/SearchServices/PostSearchService.php b/src/Models/SearchServices/PostSearchService.php index b3008d1c..f85f13f6 100644 --- a/src/Models/SearchServices/PostSearchService.php +++ b/src/Models/SearchServices/PostSearchService.php @@ -1,8 +1,6 @@ setColumn('id'); - self::decorate($innerStmt, $searchQuery); + $innerStmt->setTable('post'); + self::decorateParser($innerStmt, $searchQuery); $stmt = new SqlInsertStatement(); $stmt->setTable('post_search'); $stmt->setSource(['post_id'], $innerStmt); @@ -45,372 +44,4 @@ class PostSearchService extends AbstractSearchService return [$prevPostId, $nextPostId]; }); } - - public static function enableTokenLimit($enable) - { - self::$enableTokenLimit = $enable; - } - - protected static function decorateNegation(SqlExpression $criterion, $negative) - { - return !$negative - ? $criterion - : new SqlNegationOperator($criterion); - } - - protected static function filterUserSafety(SqlSelectStatement $stmt) - { - $allowedSafety = PrivilegesHelper::getAllowedSafety(); - $stmt->getCriterion()->add(SqlInOperator::fromArray('safety', SqlBinding::fromArray($allowedSafety))); - } - - protected static function filterTag(SqlSelectStatement $stmt, $val, $neg) - { - $tag = TagModel::findByName($val); - $innerStmt = new SqlSelectStatement(); - $innerStmt->setTable('post_tag'); - $innerStmt->setCriterion((new SqlConjunction) - ->add(new SqlEqualsOperator('post_id', 'post.id')) - ->add(new SqlEqualsOperator('post_tag.tag_id', new SqlBinding($tag->id)))); - $stmt->getCriterion()->add(self::decorateNegation(new SqlExistsOperator($innerStmt), $neg)); - } - - protected static function filterTokenId($val) - { - $ids = preg_split('/[;,]/', $val); - $ids = array_map('intval', $ids); - return SqlInOperator::fromArray('id', $ids); - } - - protected static function filterTokenIdMin($val) - { - return new SqlEqualsOrGreaterOperator('id', new SqlBinding(intval($val))); - } - - protected static function filterTokenIdMax($val) - { - return new SqlEqualsOrLesserOperator('id', new SqlBinding(intval($val))); - } - - protected static function filterTokenScoreMin($val) - { - return new SqlEqualsOrGreaterOperator('score', new SqlBinding(intval($val))); - } - - protected static function filterTokenScoreMax($val) - { - return new SqlEqualsOrLesserOperator('score', new SqlBinding(intval($val))); - } - - protected static function filterTokenTagMin($val) - { - return new SqlEqualsOrGreaterOperator('tag_count', new SqlBinding(intval($val))); - } - - protected static function filterTokenTagMax($val) - { - return new SqlEqualsOrLesserOperator('tag_count', new SqlBinding(intval($val))); - } - - protected static function filterTokenFavMin($val) - { - return new SqlEqualsOrGreaterOperator('fav_count', new SqlBinding(intval($val))); - } - - protected static function filterTokenFavMax($val) - { - return new SqlEqualsOrLesserOperator('fav_count', new SqlBinding(intval($val))); - } - - protected static function filterTokenCommentMin($val) - { - return new SqlEqualsOrGreaterOperator('comment_count', new SqlBinding(intval($val))); - } - - protected static function filterTokenCommentMax($val) - { - return new SqlEqualsOrLesserOperator('comment_count', new SqlBinding(intval($val))); - } - - protected static function filterTokenSpecial($val) - { - $context = \Chibi\Registry::getContext(); - - switch ($val) - { - case 'liked': - case 'likes': - $innerStmt = new SqlSelectStatement(); - $innerStmt->setTable('post_score'); - $innerStmt->setCriterion((new SqlConjunction) - ->add(new SqlGreaterOperator('score', '0')) - ->add(new SqlEqualsOperator('post_id', 'post.id')) - ->add(new SqlEqualsOperator('user_id', new SqlBinding($context->user->id)))); - return new SqlExistsOperator($innerStmt); - - case 'disliked': - case 'dislikes': - $innerStmt = new SqlSelectStatement(); - $innerStmt->setTable('post_score'); - $innerStmt->setCriterion((new SqlConjunction) - ->add(new SqlLesserOperator('score', '0')) - ->add(new SqlEqualsOperator('post_id', 'post.id')) - ->add(new SqlEqualsOperator('user_id', new SqlBinding($context->user->id)))); - return new SqlExistsOperator($innerStmt); - - case 'hidden': - return new SqlStringExpression('hidden'); - - default: - throw new SimpleException('Unknown special "' . $val . '"'); - } - } - - protected static function filterTokenType($val) - { - switch ($val) - { - case 'swf': - $type = PostType::Flash; - break; - case 'img': - $type = PostType::Image; - break; - case 'yt': - case 'youtube': - $type = PostType::Youtube; - break; - default: - throw new SimpleException('Unknown type "' . $val . '"'); - } - return new SqlEqualsOperator('type', new SqlBinding($type)); - } - - protected static function __filterTokenDateParser($val) - { - list ($year, $month, $day) = explode('-', $val . '-0-0'); - $yearMin = $yearMax = intval($year); - $monthMin = $monthMax = intval($month); - $monthMin = $monthMin ?: 1; - $monthMax = $monthMax ?: 12; - $dayMin = $dayMax = intval($day); - $dayMin = $dayMin ?: 1; - $dayMax = $dayMax ?: intval(date('t', mktime(0, 0, 0, $monthMax, 1, $year))); - $timeMin = mktime(0, 0, 0, $monthMin, $dayMin, $yearMin); - $timeMax = mktime(0, 0, -1, $monthMax, $dayMax+1, $yearMax); - return [$timeMin, $timeMax]; - } - - protected static function filterTokenDate($val) - { - list ($timeMin, $timeMax) = self::__filterTokenDateParser($val); - return (new SqlConjunction) - ->add(new SqlEqualsOrGreaterOperator('upload_date', new SqlBinding($timeMin))) - ->add(new SqlEqualsOrLesserOperator('upload_date', new SqlBinding($timeMax))); - } - - protected static function filterTokenDateMin($val) - { - list ($timeMin, $timeMax) = self::__filterTokenDateParser($val); - return new SqlEqualsOrGreaterOperator('upload_date', new SqlBinding($timeMin)); - } - - protected static function filterTokenDateMax($val) - { - list ($timeMin, $timeMax) = self::__filterTokenDateParser($val); - return new SqlEqualsOrLesserOperator('upload_date', new SqlBinding($timeMax)); - } - - protected static function filterTokenFav($val) - { - $user = UserModel::findByNameOrEmail($val); - $innerStmt = (new SqlSelectStatement) - ->setTable('favoritee') - ->setCriterion((new SqlConjunction) - ->add(new SqlEqualsOperator('post_id', 'post.id')) - ->add(new SqlEqualsOperator('favoritee.user_id', new SqlBinding($user->id)))); - return new SqlExistsOperator($innerStmt); - } - - protected static function filterTokenFavs($val) - { - return self::filterTokenFav($val); - } - - protected static function filterTokenComment($val) - { - $user = UserModel::findByNameOrEmail($val); - $innerStmt = (new SqlSelectStatement) - ->setTable('comment') - ->setCriterion((new SqlConjunction) - ->add(new SqlEqualsOperator('post_id', 'post.id')) - ->add(new SqlEqualsOperator('commenter_id', new SqlBinding($user->id)))); - return new SqlExistsOperator($innerStmt); - } - - protected static function filterTokenCommenter($val) - { - return self::filterTokenComment($searchContext, $stmt, $val); - } - - protected static function filterTokenSubmit($val) - { - $user = UserModel::findByNameOrEmail($val); - return new SqlEqualsOperator('uploader_id', new SqlBinding($user->id)); - } - - protected static function filterTokenUploader($val) - { - return self::filterTokenSubmit($val); - } - - protected static function filterTokenUpload($val) - { - return self::filterTokenSubmit($val); - } - - protected static function filterTokenUploaded($val) - { - return self::filterTokenSubmit($val); - } - - - - protected static function changeOrder($stmt, $val, $neg = true) - { - $randomReset = true; - - $orderDir = SqlSelectStatement::ORDER_DESC; - if (substr($val, -4) == 'desc') - { - $orderDir = SqlSelectStatement::ORDER_DESC; - $val = rtrim(substr($val, 0, -4), ','); - } - elseif (substr($val, -3) == 'asc') - { - $orderDir = SqlSelectStatement::ORDER_ASC; - $val = rtrim(substr($val, 0, -3), ','); - } - if ($neg) - { - $orderDir = $orderDir == SqlSelectStatement::ORDER_DESC - ? SqlSelectStatement::ORDER_ASC - : SqlSelectStatement::ORDER_DESC; - } - - switch ($val) - { - case 'id': - $orderColumn = 'id'; - break; - case 'date': - $orderColumn = 'upload_date'; - break; - case 'comment': - case 'comments': - case 'commentcount': - case 'comment_count': - $orderColumn = 'comment_count'; - break; - case 'fav': - case 'favs': - case 'favcount': - case 'fav_count': - $orderColumn = 'fav_count'; - break; - case 'score': - $orderColumn = 'score'; - break; - case 'tag': - case 'tags': - case 'tagcount': - case 'tag_count': - $orderColumn = 'tag_count'; - break; - case 'random': - //seeding works like this: if you visit anything - //that triggers order other than random, the seed - //is going to reset. however, it stays the same as - //long as you keep visiting pages with order:random - //specified. - $randomReset = false; - if (!isset($_SESSION['browsing-seed'])) - $_SESSION['browsing-seed'] = mt_rand(); - $seed = $_SESSION['browsing-seed']; - $orderColumn = 'SUBSTR(id * ' . $seed .', LENGTH(id) + 2)'; - break; - default: - throw new SimpleException('Unknown key "' . $val . '"'); - } - - if ($randomReset and isset($_SESSION['browsing-seed'])) - unset($_SESSION['browsing-seed']); - - $stmt->setOrderBy($orderColumn, $orderDir); - } - - - - public static function decorate(SqlSelectStatement $stmt, $searchQuery) - { - $config = \Chibi\Registry::getConfig(); - - $stmt->setTable('post'); - $stmt->setCriterion(new SqlConjunction()); - - self::filterUserSafety($stmt); - - /* query tokens */ - $tokens = array_filter(array_unique(preg_split('/\s+/', strtolower($searchQuery)))); - if (self::$enableTokenLimit and count($tokens) > $config->browsing->maxSearchTokens) - throw new SimpleException('Too many search tokens (maximum: ' . $config->browsing->maxSearchTokens . ')'); - - if (\Chibi\Registry::getContext()->user->hasEnabledHidingDislikedPosts() and !in_array('special:disliked', $tokens)) - $tokens []= '-special:disliked'; - if (!PrivilegesHelper::confirm(Privilege::ListPosts, 'hidden') or !in_array('special:hidden', $tokens)) - $tokens []= '-special:hidden'; - - $searchContext = new StdClass; - $searchContext->orderColumn = 'id'; - $searchContext->orderDir = 1; - - foreach ($tokens as $token) - { - $neg = false; - if ($token{0} == '-') - { - $neg = true; - $token = substr($token, 1); - } - - if (strpos($token, ':') !== false) - { - list ($key, $val) = explode(':', $token); - $key = strtolower($key); - if ($key == 'order') - { - self::changeOrder($stmt, $val, $neg); - } - else - { - $methodName = 'filterToken' . TextHelper::kebabCaseToCamelCase($key); - if (!method_exists(__CLASS__, $methodName)) - throw new SimpleException('Unknown search token "' . $key . '"'); - - $criterion = self::$methodName($val); - $criterion = self::decorateNegation($criterion, $neg); - $stmt->getCriterion()->add($criterion); - } - } - else - { - self::filterTag($stmt, $token, $neg); - } - } - - $stmt->addOrderBy('id', - empty($stmt->getOrderBy()) - ? SqlSelectStatement::ORDER_DESC - : $stmt->getOrderBy()[0][1]); - } } diff --git a/src/Models/SearchServices/TagSearchService.php b/src/Models/SearchServices/TagSearchService.php index 4d60caf4..0f79971a 100644 --- a/src/Models/SearchServices/TagSearchService.php +++ b/src/Models/SearchServices/TagSearchService.php @@ -1,74 +1,8 @@ addColumn('COUNT(post_tag.post_id) AS post_count') - ->setTable('tag') - ->addInnerJoin('post_tag', new SqlEqualsOperator('tag.id', 'post_tag.tag_id')) - ->addInnerJoin('post', new SqlEqualsOperator('post.id', 'post_tag.post_id')); - $stmt->setCriterion((new SqlConjunction)->add(SqlInOperator::fromArray('safety', SqlBinding::fromArray($allowedSafety)))); - - $orderToken = null; - - if ($searchQuery !== null) - { - $tokens = preg_split('/\s+/', $searchQuery); - foreach ($tokens as $token) - { - if (strpos($token, ':') !== false) - { - list ($key, $value) = explode(':', $token); - - if ($key == 'order') - $orderToken = $value; - else - throw new SimpleException('Unknown key: ' . $key); - } - else - { - if (strlen($token) >= 3) - $token = '%' . $token; - $token .= '%'; - $stmt->getCriterion()->add(new SqlNoCaseOperator(new SqlLikeOperator('tag.name', new SqlBinding($token)))); - } - } - } - - $stmt->groupBy('tag.id'); - if ($orderToken) - self::order($stmt,$orderToken); - } - - private static function order(SqlSelectStatement $stmt, $value) - { - if (strpos($value, ',') !== false) - { - list ($orderColumn, $orderDir) = explode(',', $value); - } - else - { - $orderColumn = $value; - $orderDir = 'asc'; - } - - switch ($orderColumn) - { - case 'popularity': - $stmt->setOrderBy('post_count', - $orderDir == 'asc' - ? SqlSelectStatement::ORDER_ASC - : SqlSelectStatement::ORDER_DESC); - break; - - case 'alpha': - $stmt->setOrderBy(new SqlNoCaseOperator('tag.name'), - $orderDir == 'asc' - ? SqlSelectStatement::ORDER_ASC - : SqlSelectStatement::ORDER_DESC); - break; - } + $stmt->addColumn(new SqlAliasOperator(new SqlCountOperator('post_tag.post_id'), 'post_count')); } } diff --git a/src/Models/SearchServices/UserSearchService.php b/src/Models/SearchServices/UserSearchService.php index 0acb22db..627a5325 100644 --- a/src/Models/SearchServices/UserSearchService.php +++ b/src/Models/SearchServices/UserSearchService.php @@ -1,32 +1,4 @@ setTable('user'); - - $sortStyle = $searchQuery; - switch ($sortStyle) - { - case 'alpha,asc': - $stmt->setOrderBy(new SqlNoCaseOperator('name'), SqlSelectStatement::ORDER_ASC); - break; - case 'alpha,desc': - $stmt->setOrderBy(new SqlNoCaseOperator('name'), SqlSelectStatement::ORDER_DESC); - break; - case 'date,asc': - $stmt->setOrderBy('join_date', SqlSelectStatement::ORDER_ASC); - break; - case 'date,desc': - $stmt->setOrderBy('join_date', SqlSelectStatement::ORDER_DESC); - break; - case 'pending': - $stmt->setCriterion((new SqlDisjunction) - ->add(new SqlIsNullOperator('staff_confirmed')) - ->add(new SqlEqualsOperator('staff_confirmed', '0'))); - break; - default: - throw new SimpleException('Unknown sort style "' . $sortStyle . '"'); - } - } } diff --git a/src/Views/tag-list.phtml b/src/Views/tag-list.phtml index 8b8808c1..4c6bc113 100644 --- a/src/Views/tag-list.phtml +++ b/src/Views/tag-list.phtml @@ -1,7 +1,7 @@