Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @since 17 Oct 2017 */ namespace IPS\core\api; /* To prevent PHP errors (extending class does not exist) revealing path */ use DateInterval; use IPS\Api\Controller; use IPS\Api\Response; use IPS\Content\Search\ApiResponse; use IPS\Content\Search\ContentFilter; use IPS\Content\Search\Query; use IPS\core\modules\front\search\search as FrontModuleSearch; use IPS\DateTime; use IPS\Member; use IPS\Request; use IPS\Settings; use OutOfRangeException; use function defined; use function in_array; if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * @brief Search API */ class search extends Controller { /** * GET /core/search * Perform a search and get a list of results * * @apiparam int page Page number * @apiparam int perPage Number of results per page - defaults to 25 * @apiparam string q String to search for * @apiparam string tags Comma-separated list of tags to search for * @apiparam string type Content type class to restrict searches to * @apiparam int item Restrict searches to comments or reviews made to the specified item * @apiparam string nodes Comma-separated list of node IDs to restrict searches to * @apiparam int search_min_comments Minimum number of comments search results must have for content types that support comments * @apiparam int search_min_replies Minimum number of comments search results must have for content types that require the first comment (e.g. topics) * @apiparam int search_min_reviews Minimum number of reviews search results must have * @apiparam int search_min_views Minimum number of views search results must have (note, not supported by elasticsearch) * @apiparam string author Restrict searches to results posted by this member (name) * @apiparam string club Comma-separated list of club IDs to restrict searches to * @apiparam string start_before Date period (from current time) that search results should start before * @apiparam string start_after Date period (from current time) that search results should start after * @apiparam string updated_before Date period (from current time) that search results should last be updated before * @apiparam string updated_after Date period (from current time) that search results should last be updated after * @apiparam string sortby Sort by method (newest or relevancy) * @apiparam string eitherTermsOrTags Whether to search both tags and search term ("and") or either ("or") * @apiparam string search_and_or Whether to perform an "and" search or an "or" search for all search terms * @apiparam string search_in Specify "titles" to search in titles only, otherwise titles and content are both searched * @apiparam int search_as Member ID to perform the search as (guest permissions will be used when this parameter is omitted) * @apiparam bool doNotTrack If doNotTrack is passed with a value of 1, the search will not be tracked for statistical purposes * @apireturn array * @apiresponse int page api_int_page * @apiresponse int perPage api_int_perpage * @apiresponse int totalResults api_int_totalresults * @apiresponse int totalPages api_int_totalpages * @apiresponse [\IPS\Content\Search\Result] results api_results_thispage * @note For requests using an OAuth Access Token for a particular member, only content the authorized user can view will be included and the "search_as" parameter will be ignored. * @return ApiResponse */ public function GETindex(): ApiResponse { $memberPermissions = $this->member; if( !$this->member AND isset( Request::i()->search_as ) ) { $memberPermissions = Member::load( Request::i()->search_as ); if( !$memberPermissions->member_id ) { $memberPermissions = NULL; } } /* Get valid content types */ $contentTypes = FrontModuleSearch::contentTypes( $memberPermissions ?: TRUE ); /* Initialize search */ $query = Query::init( $memberPermissions ?: NULL ); /* Set content type */ if ( isset( Request::i()->type ) and array_key_exists( Request::i()->type, $contentTypes ) ) { if ( isset( Request::i()->item ) ) { $class = $contentTypes[ Request::i()->type ]; try { $item = $class::loadAndCheckPerms( Request::i()->item ); $query->filterByContent( array( ContentFilter::init( $class )->onlyInItems( array( Request::i()->item ) ) ) ); } catch ( OutOfRangeException $e ) { } } else { $filter = ContentFilter::init( $contentTypes[ Request::i()->type ] ); if ( isset( Request::i()->nodes ) ) { $filter->onlyInContainers( explode( ',', Request::i()->nodes ) ); } if ( isset( Request::i()->search_min_comments ) ) { $filter->minimumComments( Request::i()->search_min_comments ); } if ( isset( Request::i()->search_min_replies ) ) { $filter->minimumComments( Request::i()->search_min_replies + 1 ); } if ( isset( Request::i()->search_min_reviews ) ) { $filter->minimumReviews( Request::i()->search_min_reviews ); } if ( isset( Request::i()->search_min_views ) ) { $filter->minimumViews( Request::i()->search_min_views ); } $query->filterByContent( array( $filter ) ); } } /* Filter by author */ if ( isset( Request::i()->author ) ) { $author = Member::load( Request::i()->author, 'name' ); if ( $author->member_id ) { $query->filterByAuthor( $author ); } } /* Filter by club */ if ( isset( Request::i()->club ) AND Settings::i()->clubs ) { $query->filterByClub( explode( ',', Request::i()->club ) ); } /* Set time cutoffs */ foreach ( array( 'start' => 'filterByCreateDate', 'updated' => 'filterByLastUpdatedDate' ) as $k => $method ) { $beforeKey = "{$k}_before"; $afterKey = "{$k}_after"; if ( isset( Request::i()->$beforeKey ) or isset( Request::i()->$afterKey ) ) { foreach ( array( 'before', 'after' ) as $l ) { $$l = NULL; $key = "{$l}Key"; if ( isset( Request::i()->$$key ) AND Request::i()->$$key != 'any' ) { switch ( Request::i()->$$key ) { case 'day': $$l = DateTime::create()->sub( new DateInterval( 'P1D' ) ); break; case 'week': $$l = DateTime::create()->sub( new DateInterval( 'P1W' ) ); break; case 'month': $$l = DateTime::create()->sub( new DateInterval( 'P1M' ) ); break; case 'six_months': $$l = DateTime::create()->sub( new DateInterval( 'P6M' ) ); break; case 'year': $$l = DateTime::create()->sub( new DateInterval( 'P1Y' ) ); break; default: $$l = DateTime::ts( (int)Request::i()->$$key ); break; } } } $query->$method( $after, $before ); } } /* Set Order */ if ( ! isset( Request::i()->sortby ) ) { Request::i()->sortby = $query->getDefaultSortMethod(); } switch( Request::i()->sortby ) { case 'newest': $query->setOrder( Query::ORDER_NEWEST_CREATED ); break; case 'relevancy': $query->setOrder( Query::ORDER_RELEVANCY ); break; } $flags = ( isset( Request::i()->eitherTermsOrTags ) and Request::i()->eitherTermsOrTags === 'and' ) ? Query::TERM_AND_TAGS : Query::TERM_OR_TAGS; $operator = NULL; if ( isset( Request::i()->search_and_or ) and in_array( Request::i()->search_and_or, array( Query::OPERATOR_OR, Query::OPERATOR_AND ) ) ) { $operator = Request::i()->search_and_or; } if ( isset( Request::i()->search_in ) and Request::i()->search_in === 'titles' ) { $flags = $flags | Query::TERM_TITLES_ONLY; } /* Return */ return new ApiResponse( 200, array( $query, $flags, isset( Request::i()->q ) ? ( Request::i()->q ) : NULL, isset( Request::i()->tags ) ? explode( ',', Request::i()->tags ) : NULL, $operator ), isset( Request::i()->page ) ? Request::i()->page : 1, '', 0, $this->member, isset( Request::i()->perPage ) ? Request::i()->perPage : NULL ); } /** * GET /core/search/contenttypes * Get list of content types that can be searched * * @clientapiparam int search_as Member ID to perform the search as (by default, search results are based on guest permissions) * @apireturn array * @apiresponse array contenttypes Content types that can be used in /search requests in the 'type' parameter * @return Response */ public function GETitem(): Response { $memberPermissions = $this->member; if( !$this->member AND isset( Request::i()->search_as ) ) { $memberPermissions = Member::load( Request::i()->search_as ); if( !$memberPermissions->member_id ) { $memberPermissions = NULL; } } return new Response( 200, array( 'contenttypes' => array_keys( FrontModuleSearch::contentTypes( $memberPermissions ?: TRUE ) ) ) ); } }