$time = microtime(true) - $time;
$this->debug(
sprintf(
'<= %s %s',
$response->getStatusCode(),
$response->getReasonPhrase()
),
['time' => $time]
);
if (!$response->isSuccess()) {
// Return a more detailed error message for a 400 error:
if ($response->getStatusCode() === 400) {
$json = json_decode($response->getBody(), true);
$msgParts = ['400', $response->getReasonPhrase()];
if ($msg = $json['error']['msg'] ?? '') {
$msgParts[] = $msg;
}
throw new RequestErrorException(
implode(' ', $msgParts),
400,
$response
);
}
throw HttpErrorException::createFromResponse($response);
}
return $response->getBody();
}
}
Arguments
"400 Bad Request undefined field: """
$exception = new \Exception('Unexpected exception.');
// Loop through all base URLs and try them in turn until one works.
$cacheKey = null;
foreach ((array)$this->url as $base) {
$client = ($this->clientFactory)($base . $urlSuffix);
$client->setMethod($method);
if (is_callable($callback)) {
$callback($client);
}
// Always create the cache key from the first server, and only after any
// callback has been called above.
if ($cacheable && $this->cache && null === $cacheKey) {
$cacheKey = $this->getCacheKey($client);
if ($result = $this->getCachedData($cacheKey)) {
return $result;
}
}
try {
$result = $this->send($client);
if ($cacheKey) {
$this->putCachedData($cacheKey, $result);
}
return $result;
} catch (\Exception $ex) {
if ($this->isRethrowableSolrException($ex)) {
throw $this->forceToBackendException($ex);
}
$exception = $ex;
}
}
// If we got this far, everything failed -- throw a BackendException with
// the most recent exception caught above set as the previous exception.
throw $this->forceToBackendException($exception);
}
/**
* Extract the Solr core from the connector's URL.
*
*/
public function query($handler, ParamBag $params, bool $cacheable = false)
{
$urlSuffix = '/' . $handler;
$paramString = implode('&', $params->request());
if (strlen($paramString) > self::MAX_GET_URL_LENGTH) {
$method = Request::METHOD_POST;
$callback = function ($client) use ($paramString) {
$client->setRawBody($paramString);
$client->setEncType(HttpClient::ENC_URLENCODED);
$client->setHeaders(['Content-Length' => strlen($paramString)]);
};
} else {
$method = Request::METHOD_GET;
$urlSuffix .= '?' . $paramString;
$callback = null;
}
$this->debug(sprintf('Query %s', $paramString));
return $this->trySolrUrls($method, $urlSuffix, $callback, $cacheable);
}
/**
* Call a method with provided options for the HTTP client
*
* @param array $options HTTP client options
* @param string $method Method to call
* @param array ...$args Method parameters
*
* @return mixed
*/
public function callWithHttpOptions(
array $options,
string $method,
...$args
) {
$reflectionMethod = new \ReflectionMethod($this, $method);
if (!$reflectionMethod->isPublic()) {
throw new InvalidArgumentException("Method '$method' is not public");
}
// If Solr was unable to fetch the record, just act like we have no similar records:
if (str_contains($e->getMessage(), 'Could not fetch document with id')) {
return '{}';
}
throw $e;
}
}
/**
* Execute a search.
*
* @param ParamBag $params Parameters
*
* @return string
*/
public function search(ParamBag $params)
{
$handler = $this->map->getHandler(__FUNCTION__);
$this->map->prepare(__FUNCTION__, $params);
return $this->query($handler, $params, true);
}
/**
* Extract terms from a SOLR index.
*
* @param ParamBag $params Parameters
*
* @return string
*/
public function terms(ParamBag $params)
{
$handler = $this->map->getHandler(__FUNCTION__);
$this->map->prepare(__FUNCTION__, $params);
return $this->query($handler, $params, true);
}
/**
* Write to the SOLR index.
*
* @param AbstractQuery $query Search query
* @param int $offset Search offset
* @param int $limit Search limit
* @param ParamBag $params Search backend parameters
*
* @return string
*/
public function rawJsonSearch(
AbstractQuery $query,
$offset,
$limit,
ParamBag $params = null
) {
$params = $params ?: new ParamBag();
$this->injectResponseWriter($params);
$params->set('rows', $limit);
$params->set('start', $offset);
$params->mergeWith($this->getQueryBuilder()->build($query, $params));
return $this->connector->search($params);
}
/**
* Returns some extra details about the search.
*
* @return array
*/
public function getExtraRequestDetails()
{
return [
'solrRequestUrl' => $this->connector->getLastUrl(),
];
}
/**
* Clears all accumulated extra request details
*
* @return void
*/
public function resetExtraRequestDetails()
/**
* Perform a search and return record collection.
*
* @param AbstractQuery $query Search query
* @param int $offset Search offset
* @param int $limit Search limit
* @param ParamBag $params Search backend parameters
*
* @return RecordCollectionInterface
*/
public function search(
AbstractQuery $query,
$offset,
$limit,
ParamBag $params = null
) {
if ($query instanceof WorkKeysQuery) {
return $this->workKeysSearch($query, $offset, $limit, $params);
}
$json = $this->rawJsonSearch($query, $offset, $limit, $params);
$collection = $this->createRecordCollection($json);
$this->injectSourceIdentifier($collection);
return $collection;
}
/**
* Perform a search and return a raw response.
*
* @param AbstractQuery $query Search query
* @param int $offset Search offset
* @param int $limit Search limit
* @param ParamBag $params Search backend parameters
*
* @return string
*/
public function rawJsonSearch(
AbstractQuery $query,
$offset,
$limit,
*
* @return CommandInterface Command instance for method chaining
*/
public function execute(BackendInterface $backend): CommandInterface
{
$this->validateBackend($backend);
if (
!($backend instanceof $this->interface)
|| !method_exists($this->interface, $this->method)
) {
throw new BackendException(
"$this->backendId does not support $this->method()"
);
}
$args = $this->getArguments();
if ($backend instanceof ExtraRequestDetailsInterface) {
$backend->resetExtraRequestDetails();
}
$this->finalizeExecution(
call_user_func([$backend, $this->method], ...$args)
);
if ($backend instanceof ExtraRequestDetailsInterface) {
$this->extraRequestDetails = $backend->getExtraRequestDetails();
}
return $this;
}
}
*
* @return CommandInterface Command instance for method chaining
*/
public function execute(BackendInterface $backend): CommandInterface
{
$this->validateBackend($backend);
if (
!($backend instanceof $this->interface)
|| !method_exists($this->interface, $this->method)
) {
throw new BackendException(
"$this->backendId does not support $this->method()"
);
}
$args = $this->getArguments();
if ($backend instanceof ExtraRequestDetailsInterface) {
$backend->resetExtraRequestDetails();
}
$this->finalizeExecution(
call_user_func([$backend, $this->method], ...$args)
);
if ($backend instanceof ExtraRequestDetailsInterface) {
$this->extraRequestDetails = $backend->getExtraRequestDetails();
}
return $this;
}
}
}
/**
* Invoke a command.
*
* @param CommandInterface $command Command
*
* @return CommandInterface
*/
public function invoke(CommandInterface $command)
{
// The backend instance is no longer added as an event parameter.
// All other legacy event parameters are accessible via the command object.
$args = ['command' => $command];
$backend = $this->resolve($command->getTargetIdentifier(), $args);
$this->triggerPre($this, $args);
try {
$command->execute($backend);
} catch (BackendException $e) {
$args['error'] = $e;
$this->triggerError($this, $args);
throw $e;
}
$this->triggerPost($this, $args);
return $command;
}
/**
* Resolve a backend.
*
* @param string $backendId Backend name
* @param array|ArrayAccess $args Service function arguments
*
* @return BackendInterface
*
* @throws Exception\RuntimeException Unable to resolve backend
*/
$params = $this->getParams()->getBackendParameters();
$searchService = $this->getSearchService();
$cursorMark = $this->getCursorMark();
if (null !== $cursorMark) {
$params->set('cursorMark', '' === $cursorMark ? '*' : $cursorMark);
// Override any default timeAllowed since it cannot be used with
// cursorMark
$params->set('timeAllowed', -1);
}
try {
$command = new SearchCommand(
$this->backendId,
$query,
$offset,
$limit,
$params
);
$collection = $searchService->invoke($command)->getResult();
} catch (\VuFindSearch\Backend\Exception\BackendException $e) {
// If the query caused a parser error, see if we can clean it up:
if (
$e->hasTag(ErrorListener::TAG_PARSER_ERROR)
&& $newQuery = $this->fixBadQuery($query)
) {
// We need to get a fresh set of $params, since the previous one was
// manipulated by the previous search() call.
$params = $this->getParams()->getBackendParameters();
$command = new SearchCommand(
$this->backendId,
$newQuery,
$offset,
$limit,
$params
);
$collection = $searchService->invoke($command)->getResult();
} else {
throw $e;
}
}
/**
* Actually execute the search.
*
* @return void
*/
public function performAndProcessSearch()
{
// Initialize variables to defaults (to ensure they don't stay null
// and cause unnecessary repeat processing):
// The value of -1 indicates that resultTotal is not available.
$this->resultTotal = -1;
$this->results = [];
$this->suggestions = [];
$this->errors = [];
// Run the search:
$this->startQueryTimer();
$this->performSearch();
$this->stopQueryTimer();
}
/**
* Returns the stored list of facets for the last search
*
* @param array $filter Array of field => on-screen description listing
* all of the desired facet fields; set to null to get all configured values.
*
* @return array Facets data arrays
*/
abstract public function getFacetList($filter = null);
/**
* Abstract support method for performAndProcessSearch -- perform a search based
* on the parameters passed to the object. This method is responsible for
* filling in all of the key class properties: results, resultTotal, etc.
*
* @return void
*/
{
return $this->getSpellingProcessor()->processSuggestions(
$this->getRawSuggestions(),
$this->spellingQuery,
$this->getParams()
);
}
/**
* Returns the stored list of facets for the last search
*
* @param array $filter Array of field => on-screen description listing
* all of the desired facet fields; set to null to get all configured values.
*
* @return array Facets data arrays
*/
public function getFacetList($filter = null)
{
if (null === $this->responseFacets) {
$this->performAndProcessSearch();
}
return $this->buildFacetList($this->responseFacets, $filter);
}
/**
* Get counts of facet values filtered out by the HideFacetValueListener,
* indexed by field name.
*
* @return array
*/
public function getFilteredFacetCounts(): array
{
if (null === $this->filteredFacetCounts) {
$this->performAndProcessSearch();
}
return $this->filteredFacetCounts;
}
/**
* Get complete facet counts for several index fields
// Clear existing filters for the selected field if necessary:
if ($removeFilter) {
$params->removeAllFilters($facetName);
}
}
// Don't waste time on spellcheck:
$params->getOptions()->spellcheckEnabled(false);
// Don't fetch any records:
$params->setLimit(0);
// Disable highlighting:
$params->getOptions()->disableHighlighting();
// Disable sort:
$params->setSort('', true);
// Do search
$result = $clone->getFacetList();
$filteredCounts = $clone->getFilteredFacetCounts();
// Reformat into a hash:
foreach ($result as $key => $value) {
// Detect next page and crop results if necessary
$more = false;
if (
isset($page) && count($value['list']) > 0
&& (count($value['list']) + ($filteredCounts[$key] ?? 0)) == $limit + 1
) {
$more = true;
array_pop($value['list']);
}
$result[$key] = ['more' => $more, 'data' => $value];
}
// Send back data:
return $result;
}
if ($sort === null || !in_array($sort, array_keys($facetSortOptions))) {
$sort = empty($facetSortOptions)
? 'count'
: current(array_keys($facetSortOptions));
}
$config = $this->getService(\VuFind\Config\PluginManager::class)
->get($options->getFacetsIni());
$limit = $config->Results_Settings->lightboxLimit ?? 50;
$limit = $this->params()->fromQuery('facetlimit', $limit);
if (!empty($contains)) {
$params->setFacetContains($contains);
$params->setFacetContainsIgnoreCase(true);
}
$facets = $results->getPartialFieldFacets(
[$facet],
false,
$limit,
$sort,
$page,
$this->params()->fromQuery('facetop', 'AND') == 'OR'
);
$list = $facets[$facet]['data']['list'] ?? [];
$facetLabel = $params->getFacetLabel($facet);
$viewParams = [
'contains' => $contains,
'data' => $list,
'exclude' => intval($this->params()->fromQuery('facetexclude', 0)),
'facet' => $facet,
'facetLabel' => $facetLabel,
'operator' => $this->params()->fromQuery('facetop', 'AND'),
'page' => $page,
'results' => $results,
'anotherPage' => $facets[$facet]['more'] ?? '',
'sort' => $sort,
'sortOptions' => $facetSortOptions,
'baseUriExtra' => $this->params()->fromQuery('baseUriExtra'),
'active' => $sort,
'key' => $sort,
'urlBase' => $urlBase,
*/
public function onDispatch(MvcEvent $e)
{
$routeMatch = $e->getRouteMatch();
if (! $routeMatch) {
/**
* @todo Determine requirements for when route match is missing.
* Potentially allow pulling directly from request metadata?
*/
throw new DomainException('Missing route matches; unsure how to retrieve action');
}
$action = $routeMatch->getParam('action', 'not-found');
$method = static::getMethodFromAction($action);
if (! method_exists($this, $method)) {
$method = 'notFoundAction';
}
$actionResponse = $this->$method();
$e->setResult($actionResponse);
return $actionResponse;
}
}
}
if ($this->sharedManager) {
foreach ($this->sharedManager->getListeners($this->identifiers, $name) as $priority => $listeners) {
$listOfListenersByPriority[$priority][] = $listeners;
}
}
// Sort by priority in reverse order
krsort($listOfListenersByPriority);
// Initial value of stop propagation flag should be false
$event->stopPropagation(false);
// Execute listeners
$responses = new ResponseCollection();
foreach ($listOfListenersByPriority as $listOfListeners) {
foreach ($listOfListeners as $listeners) {
foreach ($listeners as $listener) {
$response = $listener($event);
$responses->push($response);
// If the event was asked to stop propagating, do so
if ($event->propagationIsStopped()) {
$responses->setStopped(true);
return $responses;
}
// If the result causes our validation callback to return true,
// stop propagation
if (is_callable($callback) && $callback($response)) {
$responses->setStopped(true);
return $responses;
}
}
}
}
return $responses;
}
$event->setParams($argv);
}
return $this->triggerListeners($event, $callback);
}
/**
* @inheritDoc
*/
public function triggerEvent(EventInterface $event)
{
return $this->triggerListeners($event);
}
/**
* @inheritDoc
*/
public function triggerEventUntil(callable $callback, EventInterface $event)
{
return $this->triggerListeners($event, $callback);
}
/**
* @inheritDoc
*/
public function attach($eventName, callable $listener, $priority = 1)
{
if (! is_string($eventName)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a string for the event; received %s',
__METHOD__,
get_debug_type($eventName),
));
}
$this->events[$eventName][(int) $priority][0][] = $listener;
return $listener;
}
/**
* @events dispatch.pre, dispatch.post
* @param Request $request
* @param null|Response $response
* @return Response|mixed
*/
public function dispatch(Request $request, ?Response $response = null)
{
$this->request = $request;
if (! $response) {
$response = new HttpResponse();
}
$this->response = $response;
$e = $this->getEvent();
$e->setName(MvcEvent::EVENT_DISPATCH);
$e->setRequest($request);
$e->setResponse($response);
$e->setTarget($this);
$result = $this->getEventManager()->triggerEventUntil(static fn($test): bool => $test instanceof Response, $e);
if ($result->stopped()) {
return $result->last();
}
return $e->getResult();
}
/**
* Get request object
*
* @return Request
*/
public function getRequest()
{
if (! $this->request) {
$this->request = new HttpRequest();
}
return $this->request;
);
return $this->complete($return, $e);
} catch (Throwable $exception) {
$return = $this->marshalBadControllerEvent($controllerName, $e, $application, $exception);
return $this->complete($return, $e);
} catch (Exception $exception) { // @TODO clean up once PHP 7 requirement is enforced
$return = $this->marshalBadControllerEvent($controllerName, $e, $application, $exception);
return $this->complete($return, $e);
}
if ($controller instanceof InjectApplicationEventInterface) {
$controller->setEvent($e);
}
$request = $e->getRequest();
$response = $application->getResponse();
$caughtException = null;
try {
$return = $controller->dispatch($request, $response);
} catch (Throwable $ex) {
$caughtException = $ex;
} catch (Exception $ex) { // @TODO clean up once PHP 7 requirement is enforced
$caughtException = $ex;
}
if ($caughtException !== null) {
$e->setName(MvcEvent::EVENT_DISPATCH_ERROR);
$e->setError($application::ERROR_EXCEPTION);
$e->setController($controllerName);
$e->setControllerClass($controller::class);
$e->setParam('exception', $caughtException);
$return = $application->getEventManager()->triggerEvent($e)->last();
if (! $return) {
$return = $e->getResult();
}
}
return $this->complete($return, $e);
}
if ($this->sharedManager) {
foreach ($this->sharedManager->getListeners($this->identifiers, $name) as $priority => $listeners) {
$listOfListenersByPriority[$priority][] = $listeners;
}
}
// Sort by priority in reverse order
krsort($listOfListenersByPriority);
// Initial value of stop propagation flag should be false
$event->stopPropagation(false);
// Execute listeners
$responses = new ResponseCollection();
foreach ($listOfListenersByPriority as $listOfListeners) {
foreach ($listOfListeners as $listeners) {
foreach ($listeners as $listener) {
$response = $listener($event);
$responses->push($response);
// If the event was asked to stop propagating, do so
if ($event->propagationIsStopped()) {
$responses->setStopped(true);
return $responses;
}
// If the result causes our validation callback to return true,
// stop propagation
if (is_callable($callback) && $callback($response)) {
$responses->setStopped(true);
return $responses;
}
}
}
}
return $responses;
}
$event->setParams($argv);
}
return $this->triggerListeners($event, $callback);
}
/**
* @inheritDoc
*/
public function triggerEvent(EventInterface $event)
{
return $this->triggerListeners($event);
}
/**
* @inheritDoc
*/
public function triggerEventUntil(callable $callback, EventInterface $event)
{
return $this->triggerListeners($event, $callback);
}
/**
* @inheritDoc
*/
public function attach($eventName, callable $listener, $priority = 1)
{
if (! is_string($eventName)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a string for the event; received %s',
__METHOD__,
get_debug_type($eventName),
));
}
$this->events[$eventName][(int) $priority][0][] = $listener;
return $listener;
}
/**
$response = $result->last();
if ($response instanceof ResponseInterface) {
$event->setName(MvcEvent::EVENT_FINISH);
$event->setTarget($this);
$event->setResponse($response);
$event->stopPropagation(false); // Clear before triggering
$events->triggerEvent($event);
$this->response = $response;
return $this;
}
}
if ($event->getError()) {
return $this->completeRequest($event);
}
// Trigger dispatch event
$event->setName(MvcEvent::EVENT_DISPATCH);
$event->stopPropagation(false); // Clear before triggering
$result = $events->triggerEventUntil($shortCircuit, $event);
// Complete response
$response = $result->last();
if ($response instanceof ResponseInterface) {
$event->setName(MvcEvent::EVENT_FINISH);
$event->setTarget($this);
$event->setResponse($response);
$event->stopPropagation(false); // Clear before triggering
$events->triggerEvent($event);
$this->response = $response;
return $this;
}
$response = $this->response;
$event->setResponse($response);
return $this->completeRequest($event);
}
/**
* Complete the request
$vufindProfiler = getenv('VUFIND_PROFILER_XHPROF');
if (!empty($vufindProfiler)) {
include __DIR__ . '/../module/VuFind/functions/profiler.php';
enableVuFindProfiling($vufindProfiler);
}
// Run the application!
$app = include __DIR__ . '/../config/application.php';
if (PHP_SAPI === 'cli') {
return $app->getServiceManager()
->get(\VuFindConsole\ConsoleRunner::class)->run();
} else {
// Setup remote code coverage if enabled:
if (getenv('VUFIND_CODE_COVERAGE')) {
$modules = $app->getServiceManager()
->get(\Laminas\ModuleManager\ModuleManager::class)->getModules();
include __DIR__ . '/../module/VuFind/functions/codecoverage.php';
setupVuFindRemoteCodeCoverage($modules);
}
$app->run();
}