<NME> Frontend.php
<BEF> <?php

namespace Bolt\Controller;

use Bolt\Asset\File\JavaScript;
use Bolt\Asset\File\Stylesheet;
use Bolt\Asset\Snippet\Snippet;
use Bolt\Asset\Target;
use Bolt\Helpers\Input;
use Bolt\Response\TemplateResponse;
use Bolt\Storage\Entity\Content;
use Bolt\Storage\Entity\Relations;
use Bolt\Storage\Entity\Taxonomy;
use Bolt\Storage\EntityManager;
use Bolt\Storage\Mapping\ContentType;
use Bolt\Storage\Query\QueryResultset;
use Bolt\Storage\Repository\TaxonomyRepository;
use Bolt\Translation\Translator as Trans;
use Silex\ControllerCollection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;

/**
 * Standard Frontend actions.
 *
 * This file acts as a grouping for the default front-end controllers.
 *
 * For overriding the default behavior here, please reference
 * https://docs.bolt.cm/templating/templates-routes#routing or the routing.yml
 * file in your configuration.
 */
class Frontend extends ConfigurableBase
{
    protected function getConfigurationRoutes()
    {
        return $this->app['config']->get('routing', []);
    }

    protected function addRoutes(ControllerCollection $c)
    {
        $c->value(Zone::KEY, Zone::FRONTEND);
        parent::addRoutes($c);
    }

    /**
     * The default before filter for the controllers in this file.
     *
     * Refer to the routing.yml config file for overridding.
     *
     * @param Request $request The Symfony Request
     *
     * @return null|TemplateResponse|RedirectResponse
     */
    public function before(Request $request)
    {
        // Start the 'stopwatch' for the profiler.
        $this->app['stopwatch']->start('bolt.frontend.before');

        // If there are no users in the users table, or the table doesn't exist.
        // Repair the DB, and let's add a new user.
        if (!$this->hasUsers()) {
            $this->flashes()->info(Trans::__('general.phrase.users-none-create-first'));

            return $this->redirectToRoute('userfirst');
        }

        // If we are in maintenance mode and current user is not logged in, show maintenance notice.
        if ($this->getOption('general/maintenance_mode') && !$this->isAllowed('maintenance-mode')) {
            $twig = $this->app['twig'];
            $template = $this->templateChooser()->maintenance();

            $html = $twig->resolveTemplate($template)->render([]);
            $response = new TemplateResponse($template, [], $html, Response::HTTP_SERVICE_UNAVAILABLE);

            return $response;
        }

        // Stop the 'stopwatch' for the profiler.
        $this->app['stopwatch']->stop('bolt.frontend.before');

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function after(Request $request, Response $response)
    {
        if ($this->session()->isStarted()) {
            $response->setPrivate();
        } else {
            $sharedMaxAge = $this->getOption('general/caching/duration', 10) * 60;
            $response
                ->setPublic()
                ->setSharedMaxAge($sharedMaxAge)
            ;
        }
    }

    /**
     * Controller for the "Homepage" route. Usually the front page of the website.
     *
     * @param Request $request
     *
     * @return TemplateResponse
     */
    public function homepage(Request $request)
    {
        $homepage = $this->getOption('theme/homepage') ?: $this->getOption('general/homepage');
        $listingParameters = $this->getListingParameters($homepage, true);
        $content = $this->getContent($homepage, $listingParameters);

        $template = $this->templateChooser()->homepage($content);
        $globals = [];

        if (is_array($content) && count($content) > 0) {
            $first = current($content);
            $globals[$first->contenttype['slug']] = $content;
            $globals['records'] = $content;
        } elseif (is_object($content)) {
            $globals['record'] = $content;
            $globals[$content->contenttype['singular_slug']] = $content;
            $globals['records'] = [$content->id => $content];
        }

        return $this->render($template, [], $globals);
    }

    /**
     * Controller for a single record page, like '/page/about/' or '/entry/lorum'.
     *
     * @param Request $request         The request
     * @param string  $contenttypeslug The content type slug
     * @param string  $slug            The content slug
     *
     * @return TemplateResponse
     */
    public function record(Request $request, $contenttypeslug, $slug = '')
    {
        $contenttype = $this->getContentType($contenttypeslug);

        // If the ContentType is 'viewless', don't show the record page.
        if (isset($contenttype['viewless']) && $contenttype['viewless'] === true) {
            $this->abort(Response::HTTP_NOT_FOUND, "Page $contenttypeslug/$slug not found.");

            return null;
        }

        // Perhaps we don't have a slug. Let's see if we can pick up the 'id', instead.
        if (empty($slug)) {
            $slug = $request->get('id');
        }

        $slug = $this->app['slugify']->slugify($slug);

        // First, try to get it by slug.
        $content = $this->getContent($contenttype['slug'], ['slug' => $slug, 'returnsingle' => true, 'log_not_found' => !is_numeric($slug)]);

        if (is_numeric($slug) && !$content) {
            // And otherwise try getting it by ID
            $content = $this->getContent($contenttype['slug'], ['id' => $slug, 'returnsingle' => true]);
        }

        // No content, no page!
        if (!$content) {
            $this->abort(Response::HTTP_NOT_FOUND, "Page $contenttypeslug/$slug not found.");

            return null;
        }

        // Then, select which template to use, based on our 'cascading templates rules'
        $template = $this->templateChooser()->record($content);

        // Setting the editlink
        $this->app['editlink'] = $this->generateUrl('editcontent', ['contenttypeslug' => $contenttype['slug'], 'id' => $content->id]);
        $this->app['edittitle'] = $content->getTitle();

        // Make sure we can also access it as {{ page.title }} for pages, etc. We set these in the global scope,
        // So that they're also available in menu's and templates rendered by extensions.
        $globals = [
            'record'                      => $content,
            $contenttype['singular_slug'] => $content,
        ];

        return $this->render($template, [], $globals);
    }

    /**
     * The controller for previewing a content from posted data.
     *
     * @param Request $request         The Symfony Request
     * @param string  $contenttypeslug The content type slug
     *
     * @throws \Exception
     *
     * @return TemplateResponse
     */
    public function preview(Request $request, $contenttypeslug)
    {
        if (!$request->isMethod('POST')) {
            throw new MethodNotAllowedHttpException(['POST'], 'This route only accepts POST requests.');
        }

        // Only accept requests with a valid token
        $tokenValue = $request->request->get('content_edit', ['_token' => null])['_token'];
        if (!$this->isAllowed('dashboard') || !$this->isCsrfTokenValid($tokenValue, 'content_edit')) {
            return new Response('Not allowed or invalid CSRF token', Response::HTTP_FORBIDDEN);
        }

        $contenttype = $this->getContentType($contenttypeslug);
        $formValues = $request->request->all();

        $storage = $this->storage();
        if ($storage instanceof EntityManager) {
            /** @var EntityManager $storage */
            /** @var Content $content */
            $content = $storage->create($contenttypeslug, $formValues);

            /** @var Collection\Relations $related */
            $related = $storage->createCollection(Relations::class);
            $related->setFromPost($formValues, $content);
            $content->setRelation($related);

            /** @var Collection\Taxonomy $taxonomies */
            $taxonomies = $storage->createCollection(Taxonomy::class);
            $taxonomies->setFromPost($formValues, $content);
            $content->setTaxonomy($taxonomies);

        } else {
            $content = $storage->getContentObject($contenttypeslug, [], false);
            $content->setFromPost($formValues, $contenttype);
        }

        $this->fixBlockFieldsForPreview($content);
        $this->fixHTMLFieldsForPreview($content);

        $liveEditor = $request->get('_live-editor-preview');
        if (!empty($liveEditor)) {
            $jsFile = (new JavaScript('js/ckeditor/ckeditor.js', 'bolt'))
                ->setPriority(1)
                ->setLate(false);
            $cssFile = (new Stylesheet('css/liveeditor.css', 'bolt'))
                ->setPriority(5)
                ->setLate(false);
            $snippet = (new Snippet())
                ->setCallback('<script>window.boltIsEditing = true;</script>')
                ->setLocation(Target::BEFORE_HEAD_JS);

            $this->app['asset.queue.snippet']->add($snippet);
            $this->app['asset.queue.file']->add($jsFile);
            $this->app['asset.queue.file']->add($cssFile);
        }

        // Then, select which template to use, based on our 'cascading templates rules'
        $template = $this->templateChooser()->record($content);

        // Make sure we can also access it as {{ page.title }} for pages, etc. We set these in the global scope,
        // So that they're also available in menu's and templates rendered by extensions.
        $globals = [
            'record'                      => $content,
            $contenttype['singular_slug'] => $content,
        ];

        return $this->render($template, [], $globals);
    }

    /**
     * Helper function to prefill `block` fields with an additional `block` field per item.
     *
     * This is needed to make previewing content with block fields fully working.
     */
    private function fixBlockFieldsForPreview($record)
    {
        foreach ($record->contenttype['fields'] as $fieldSlug => $fieldSettings) {
            if ($fieldSettings['type'] === 'block') {
                $fieldValue = $this->getBlockFieldValue($record, $fieldSlug);
                if ($fieldValue) {
                    $newArray = [];
                    foreach ($fieldValue as $item) {
                        if (!empty($item)) {
                            foreach ($item as $k => &$v) {
                                $v['block'] = $k;
                            }
                            $newArray[] = $v;
                        }
                    }
                    $this->setFieldValue($record, $fieldSlug, $newArray);
                }
            }
        }

        return $record;
    }

    /**
     * Helper function to make sure HTML-like fields are Twig_Markup, and image fields are
     * an image. Like they are in a "real" page.
     *
     * This is needed to make previewing content with block fields fully working.
     * See: https://github.com/bolt/bolt/issues/7753
     */
    private function fixHTMLFieldsForPreview($record)
    {
        foreach ($record->contenttype['fields'] as $fieldSlug => $fieldSettings) {
            if (in_array($fieldSettings['type'], ['html', 'text', 'textarea', 'markdown'])) {
                $fieldValue = new \Twig_Markup($this->getFieldValue($record, $fieldSlug), "UTF-8");

                $this->setFieldValue($record, $fieldSlug, $fieldValue);
            }
            if ($fieldSettings['type'] === 'image') {
                $fieldValue = $this->getFieldValue($record, $fieldSlug);

                if (is_array($fieldValue) && isset($fieldValue['file'])) {
                    $this->setFieldValue($record, $fieldSlug, $fieldValue['file']);
                }
            }
        }

        return $record;
    }

    private function getBlockFieldValue($record, $field)
    {
        if ($record instanceof Content) {
            $value = $record->get($field);
            if (is_array($value)) {
                return $value;
            }
        } elseif (isset($record->values[$field]) && is_array($record->values[$field])) {
            return $record->values[$field];
        }

        return null;
    }

    private function getFieldValue($record, $field)
    {
        if ($record instanceof Content) {
            return $record->get($field);
        } elseif (isset($record->values[$field]) && is_array($record->values[$field])) {
            return $record->values[$field];
        }

        return null;
    }

    private function setFieldValue($record, $field, $value)
    {
        if ($record instanceof Content) {
            $record->set($field, $value);
        } else {
            $record->values[$field] = $value;
        }
    }

    /**
     * The listing page controller.
     *
     * @param Request $request         The Symfony Request
     * @param string  $contenttypeslug The content type slug
     *
     * @return TemplateResponse
     */
    public function listing(Request $request, $contenttypeslug)
    {
        $listingParameters = $this->getListingParameters($contenttypeslug);
        $content = $this->getContent($contenttypeslug, $listingParameters);
        $contenttype = $this->getContentType($contenttypeslug);

        $template = $this->templateChooser()->listing($contenttype);

        // Make sure we can also access it as {{ pages }} for pages, etc. We set these in the global scope,
        // So that they're also available in menu's and templates rendered by extensions.
        $globals = [
            'records'        => $content,
            $contenttypeslug => $content,
            'contenttype'    => $contenttype['name'],
        ];

        return $this->render($template, [], $globals);
    }

    /**
     * The taxonomy listing page controller.
     *
     * @param Request $request      The Symfony Request
     * @param string  $taxonomytype The taxonomy type slug
     * @param string  $slug         The taxonomy slug
     *
     * @throws \Bolt\Exception\InvalidRepositoryException
     *
     * @return TemplateResponse|false
     */
    public function taxonomy(Request $request, $taxonomytype, $slug)
    {
        $taxonomy = $this->app['config']->get('taxonomy/' . $taxonomytype);
        // No taxonomytype, no possible content.
        if (empty($taxonomy)) {
            return false;
        }
        $taxonomyslug = $taxonomy['slug'];

        // First, get some content
        $context = $taxonomy['singular_slug'] . '_' . $slug;
        $page = $this->app['pager']->getCurrentPage($context);
        // Theme value takes precedence over default config @see https://github.com/bolt/bolt/issues/3951
        $amount = $this->getOption('theme/listing_records', false) ?: $this->getOption('general/listing_records');

        // Handle case where listing records has been override for specific taxonomy
        if (array_key_exists('listing_records', $taxonomy) && is_int($taxonomy['listing_records'])) {
            $amount = $taxonomy['listing_records'];
        }

        $order = $this->getOption('theme/listing_sort', false) ?: $this->getOption('general/listing_sort');
        $isLegacy = $this->getOption('general/compatibility/setcontent_legacy', true);
        if ($isLegacy) {
            $content = $this->storage()->getContentByTaxonomy($taxonomytype, $slug, ['limit' => $amount, 'order' => $order, 'page' => $page]);
        } else {
            $page = $this->app['pager']->getCurrentPage('taxonomy');
            $appCt = array_keys($this->app['query.search_config']->getSearchableTypes());
            /** @var TaxonomyRepository $repo */
            $repo = $this->app['storage']->getRepository(Taxonomy::class);
            $query = $repo->queryContentByTaxonomy($appCt, [$taxonomytype => $slug])
                ->setFirstResult(($page - 1) * $amount)
                ->setMaxResults($amount)
            ;

            $results = $repo->getContentByTaxonomy($query);
            $set = new QueryResultset();
            foreach ($results->getCollection() as $record) {
                $set->add([$record]);
            }
            $content = $this->app['twig.records.view']->createView($set);
        }

        if (!$this->isTaxonomyValid($content, $slug, $taxonomy)) {
            $this->abort(Response::HTTP_NOT_FOUND, "No slug '$slug' in taxonomy '$taxonomyslug'");
        }

        $template = $this->templateChooser()->taxonomy($taxonomyslug);

        // Get a display value for slug. This should be moved from 'slug' context key to 'name' in v4.0.
        $name = $slug;
        if ($taxonomy['behaves_like'] !== 'tags' && isset($taxonomy['options'][$slug])) {
            $name = $taxonomy['options'][$slug];
        }

        $globals = [
            'records'      => $content,
            'slug'         => $name,
            'taxonomy'     => $this->getOption('taxonomy/' . $taxonomyslug),
            'taxonomytype' => $taxonomyslug,
        ];

        return $this->render($template, [], $globals);
    }

    /**
     * Check if the taxonomy is valid.
     *
     * @see https://github.com/bolt/bolt/pull/2310
     *
     * @param array|false $content
     * @param string      $slug
     * @param array       $taxonomy
     *
     * @return bool
     */
    protected function isTaxonomyValid($content, $slug, array $taxonomy)
    {
        if ($taxonomy['behaves_like'] === 'tags' && !$content) {
            return false;
        }

        $isNotTag = in_array($taxonomy['behaves_like'], ['categories', 'grouping']);
        $options = isset($taxonomy['options']) ? array_keys($taxonomy['options']) : [];
        $isTax = in_array($slug, $options);
        if ($isNotTag && !$isTax) {
            return false;
        }

        return true;
    }

    /**
     * The search result page controller.
     *
     * @param Request $request      The Symfony Request
     * @param array   $contenttypes The content type slug(s) you want to search for
     *
     * @return TemplateResponse
     */
    public function search(Request $request, array $contenttypes = null)
    {
        $q = '';
        $context = __FUNCTION__;

        if ($request->query->has('q')) {
            $q = $request->query->get('q');
        } elseif ($request->query->has($context)) {
            $q = $request->query->get($context);
        }
        $q = Input::cleanPostedData($q, false);

        $page = $this->app['pager']->getCurrentPage($context);

        // Theme value takes precedence over default config @see https://github.com/bolt/bolt/issues/3951
        $pageSize = $this->getOption('theme/search_results_records', false);
        if ($pageSize === false && !$pageSize = $this->getOption('general/search_results_records', false)) {
            $pageSize = $this->getOption('theme/listing_records', false) ?: $this->getOption('general/listing_records', 10);
        }

        $offset = ($page - 1) * $pageSize;
        $limit = $pageSize;

        // set-up filters from URL
        $filters = [];
        foreach ($request->query->all() as $key => $value) {
            if (strpos($key, '_') > 0) {
                list($contenttypeslug, $field) = explode('_', $key, 2);
                if (isset($filters[$contenttypeslug])) {
                    $filters[$contenttypeslug][$field] = $value;
                } else {
                    $contenttype = $this->getContentType($contenttypeslug);
                    if (is_array($contenttype)) {
                        $filters[$contenttypeslug] = [
                            $field => $value,
                        ];
                    }
                }
            }
        }
        if (count($filters) == 0) {
            $filters = null;
        }

        $isLegacy = $this->getOption('general/compatibility/setcontent_legacy', true);
        if ($isLegacy) {
            $result = $this->storage()->searchContent($q, $contenttypes, $filters, $limit, $offset);

            /** @var \Bolt\Pager\PagerManager $manager */
            $manager = $this->app['pager'];
            $manager
                ->createPager($context)
                ->setCount($result['no_of_results'])
                ->setTotalpages(ceil($result['no_of_results'] / $pageSize))
                ->setCurrent($page)
                ->setShowingFrom($offset + 1)
                ->setShowingTo($offset + ($result ? count($result['results']) : 0));
            ;

            $manager->setLink($this->generateUrl('search', ['q' => $q]) . '&page_search=');
        } else {
            $appCt = array_keys($this->app['query.search_config']->getSearchableTypes());
            $textQuery = '(' . join(',', $appCt) . ')/search';
            $params = [
                'filter' => $q,
                'page'   => $page,
                'limit'  => $pageSize,
            ];
            $searchResult = $this->getContent($textQuery, $params);

            $result = [
                'results' => $searchResult->getSortedResults(),
                'query'   => [
                    'sanitized_q' => strip_tags($q),
                ],
            ];
        }

        $globals = [
            'records'      => $result['results'],
            $context       => $result['query']['sanitized_q'],
            'searchresult' => $result,
        ];

        $template = $this->templateChooser()->search();

        return $this->render($template, [], $globals);
    }

    /**
     * Renders the specified template from the current theme in response to a request without
     * loading any content.
     *
     * @param string $template The template name
     *
     * @return TemplateResponse
     */
    public function template($template)
    {
        // Add the template extension if it is missing
        if (!preg_match('/\\.twig$/i', $template)) {
            $template .= '.twig';
        }

        return $this->render($template);
    }

    /**
     * Returns an array of the parameters used in getContent for listing pages.
     *
     * @param string $contentTypeSlug The content type slug
     * @param bool   $allowViewless   Allow viewless contenttype
     *
     * @return array Parameters to use in getContent
     */
    private function getListingParameters($contentTypeSlug, $allowViewless = false)
    {
        $contentType = $this->getContentType(current(explode('/', $contentTypeSlug)));

        // If there is no ContentType, don't get parameters for it
        if ($contentType === false) {
            return [];
        }

        // If the ContentType is 'viewless', don't show the listing / record page.
        if ($contentType['viewless'] && !$allowViewless) {
            $this->abort(Response::HTTP_NOT_FOUND, 'Page ' . $contentType['slug'] . ' not found.');
        }

        // Build the pager
        $page = $this->app['pager']->getCurrentPage($contentType['slug']);
        $order = isset($contentType['listing_sort']) ? $contentType['listing_sort'] : $this->getListingOrder($contentType);

        // CT value takes precedence over theme & config.yml
        if (!empty($contentType['listing_records'])) {
            $amount = $contentType['listing_records'];
        } else {
            $amount = $this->getOption('theme/listing_records') ?: $this->getOption('general/listing_records');
        }

        return ['limit' => $amount, 'order' => $order, 'page' => $page, 'paging' => true];
    }

    /**
     * Return the listing order.
     *
     * If the ContentType's sort is false (default in Config::parseContentType),
     * either:
     *  - we let `getContent()` sort by itself
     *  - we explicitly set it to sort on the general/listing_sort setting
     *
     * @param ContentType|array $contentType
     *
     * @return null|string
     */
    private function getListingOrder($contentType)
    {
        // An empty default isn't set in config yet, arrays got to hate them.
        if (isset($contentType['taxonomy'])) {
            $taxonomies = $this->getOption('taxonomy');
            foreach ($contentType['taxonomy'] as $taxonomyName) {
                if ($taxonomies[$taxonomyName]['has_sortorder']) {
                    // Let getContent() handle it
                    return null;
                }
            }
        }

        return $this->getOption('theme/listing_sort') ?: $this->getOption('general/listing_sort');
    }
}

<MSG> Fix bolt/bolt#7902 PHP 7.4 exception on empty search

PHP 7.4 throws an exception when someone tries to access an array item
on a variable of boolean type. This commit adds a check that the array
is populated before attempting to access an array member.

<DFF> @@ -541,12 +541,14 @@ class Frontend extends ConfigurableBase
         if ($isLegacy) {
             $result = $this->storage()->searchContent($q, $contenttypes, $filters, $limit, $offset);
 
+            $resultcount = isset($result['no_of_results']) ? $result['no_of_results'] : 0;
+
             /** @var \Bolt\Pager\PagerManager $manager */
             $manager = $this->app['pager'];
             $manager
                 ->createPager($context)
-                ->setCount($result['no_of_results'])
-                ->setTotalpages(ceil($result['no_of_results'] / $pageSize))
+                ->setCount($resultcount)
+                ->setTotalpages(ceil($resultcount / $pageSize))
                 ->setCurrent($page)
                 ->setShowingFrom($offset + 1)
                 ->setShowingTo($offset + ($result ? count($result['results']) : 0));
@@ -572,8 +574,8 @@ class Frontend extends ConfigurableBase
         }
 
         $globals = [
-            'records'      => $result['results'],
-            $context       => $result['query']['sanitized_q'],
+            'records'      => isset($result['results']) ? $result['results'] : false,
+            $context       => isset($result['query']['sanitized_q']) ? $result['query']['sanitized_q'] : false,
             'searchresult' => $result,
         ];
 

{'commit': "diff --git a/src/Controller/Frontend.php b/src/Controller/Frontend.php\nindex d969f6501..d7811c4f4 100644\n--- a/src/Controller/Frontend.php\n+++ b/src/Controller/Frontend.php\n@@ -541,12 +541,14 @@ class Frontend extends ConfigurableBase\n         if ($isLegacy) {\n             $result = $this->storage()->searchContent($q, $contenttypes, $filters, $limit, $offset);\n \n+            $resultcount = isset($result['no_of_results']) ? $result['no_of_results'] : 0;\n+\n             /** @var \\Bolt\\Pager\\PagerManager $manager */\n             $manager = $this->app['pager'];\n             $manager\n                 ->createPager($context)\n-                ->setCount($result['no_of_results'])\n-                ->setTotalpages(ceil($result['no_of_results'] / $pageSize))\n+                ->setCount($resultcount)\n+                ->setTotalpages(ceil($resultcount / $pageSize))\n                 ->setCurrent($page)\n                 ->setShowingFrom($offset + 1)\n                 ->setShowingTo($offset + ($result ? count($result['results']) : 0));\n@@ -572,8 +574,8 @@ class Frontend extends ConfigurableBase\n         }\n \n         $globals = [\n-            'records'      => $result['results'],\n-            $context       => $result['query']['sanitized_q'],\n+            'records'      => isset($result['results']) ? $result['results'] : false,\n+            $context       => isset($result['query']['sanitized_q']) ? $result['query']['sanitized_q'] : false,\n             'searchresult' => $result,\n         ];\n \n", 'commit_subject': 'Fix bolt/bolt#7902 PHP 7.4 exception on empty search', 'repo_name': 'bolt/bolt', 'committer': {'name': 'Stephen Pewter', 'email': '433cbdfa86bea1d51b2340339a74f992bcbffbed@users.noreply.github.com', 'time_sec': '1626269173', 'tz_offset': '120', 'date': {'seconds': '1626269173'}}, 'commit_tree': '0197d164fade21c869a6d6c69395ff2d74c9dba4', 'commit_message': 'Fix bolt/bolt#7902 PHP 7.4 exception on empty search\n\nPHP 7.4 throws an exception when someone tries to access an array item\non a variable of boolean type. This commit adds a check that the array\nis populated before attempting to access an array member.\n', 'language_name': 'JavaScript', 'license': 'mit', 'hunks': "@@ -541,12 +541,14 @@ class Frontend extends ConfigurableBase\n         if ($isLegacy) {\n             $result = $this->storage()->searchContent($q, $contenttypes, $filters, $limit, $offset);\n \n+            $resultcount = isset($result['no_of_results']) ? $result['no_of_results'] : 0;\n+\n             /** @var \\Bolt\\Pager\\PagerManager $manager */\n             $manager = $this->app['pager'];\n             $manager\n                 ->createPager($context)\n-                ->setCount($result['no_of_results'])\n-                ->setTotalpages(ceil($result['no_of_results'] / $pageSize))\n+                ->setCount($resultcount)\n+                ->setTotalpages(ceil($resultcount / $pageSize))\n                 ->setCurrent($page)\n                 ->setShowingFrom($offset + 1)\n                 ->setShowingTo($offset + ($result ? count($result['results']) : 0));\n@@ -572,8 +574,8 @@ class Frontend extends ConfigurableBase\n         }\n \n         $globals = [\n-            'records'      => $result['results'],\n-            $context       => $result['query']['sanitized_q'],\n+            'records'      => isset($result['results']) ? $result['results'] : false,\n+            $context       => isset($result['query']['sanitized_q']) ? $result['query']['sanitized_q'] : false,\n             'searchresult' => $result,\n         ];\n \n", 'addition_count': 6, 'deletion_count': 4, 'src_file': 'a/src/Controller/Frontend.php', 'tgt_file': 'b/src/Controller/Frontend.php', 'file_extension': '.php', 'before_file': '<?php\n\nnamespace Bolt\\Controller;\n\nuse Bolt\\Asset\\File\\JavaScript;\nuse Bolt\\Asset\\File\\Stylesheet;\nuse Bolt\\Asset\\Snippet\\Snippet;\nuse Bolt\\Asset\\Target;\nuse Bolt\\Helpers\\Input;\nuse Bolt\\Response\\TemplateResponse;\nuse Bolt\\Storage\\Entity\\Content;\nuse Bolt\\Storage\\Entity\\Relations;\nuse Bolt\\Storage\\Entity\\Taxonomy;\nuse Bolt\\Storage\\EntityManager;\nuse Bolt\\Storage\\Mapping\\ContentType;\nuse Bolt\\Storage\\Query\\QueryResultset;\nuse Bolt\\Storage\\Repository\\TaxonomyRepository;\nuse Bolt\\Translation\\Translator as Trans;\nuse Silex\\ControllerCollection;\nuse Symfony\\Component\\HttpFoundation\\JsonResponse;\nuse Symfony\\Component\\HttpFoundation\\RedirectResponse;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\HttpKernel\\Exception\\MethodNotAllowedHttpException;\n\n/**\n * Standard Frontend actions.\n *\n * This file acts as a grouping for the default front-end controllers.\n *\n * For overriding the default behavior here, please reference\n * https://docs.bolt.cm/templating/templates-routes#routing or the routing.yml\n * file in your configuration.\n */\nclass Frontend extends ConfigurableBase\n{\n    protected function getConfigurationRoutes()\n    {\n        return $this->app[\'config\']->get(\'routing\', []);\n    }\n\n    protected function addRoutes(ControllerCollection $c)\n    {\n        $c->value(Zone::KEY, Zone::FRONTEND);\n        parent::addRoutes($c);\n    }\n\n    /**\n     * The default before filter for the controllers in this file.\n     *\n     * Refer to the routing.yml config file for overridding.\n     *\n     * @param Request $request The Symfony Request\n     *\n     * @return null|TemplateResponse|RedirectResponse\n     */\n    public function before(Request $request)\n    {\n        // Start the \'stopwatch\' for the profiler.\n        $this->app[\'stopwatch\']->start(\'bolt.frontend.before\');\n\n        // If there are no users in the users table, or the table doesn\'t exist.\n        // Repair the DB, and let\'s add a new user.\n        if (!$this->hasUsers()) {\n            $this->flashes()->info(Trans::__(\'general.phrase.users-none-create-first\'));\n\n            return $this->redirectToRoute(\'userfirst\');\n        }\n\n        // If we are in maintenance mode and current user is not logged in, show maintenance notice.\n        if ($this->getOption(\'general/maintenance_mode\') && !$this->isAllowed(\'maintenance-mode\')) {\n            $twig = $this->app[\'twig\'];\n            $template = $this->templateChooser()->maintenance();\n\n            $html = $twig->resolveTemplate($template)->render([]);\n            $response = new TemplateResponse($template, [], $html, Response::HTTP_SERVICE_UNAVAILABLE);\n\n            return $response;\n        }\n\n        // Stop the \'stopwatch\' for the profiler.\n        $this->app[\'stopwatch\']->stop(\'bolt.frontend.before\');\n\n        return null;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function after(Request $request, Response $response)\n    {\n        if ($this->session()->isStarted()) {\n            $response->setPrivate();\n        } else {\n            $sharedMaxAge = $this->getOption(\'general/caching/duration\', 10) * 60;\n            $response\n                ->setPublic()\n                ->setSharedMaxAge($sharedMaxAge)\n            ;\n        }\n    }\n\n    /**\n     * Controller for the "Homepage" route. Usually the front page of the website.\n     *\n     * @param Request $request\n     *\n     * @return TemplateResponse\n     */\n    public function homepage(Request $request)\n    {\n        $homepage = $this->getOption(\'theme/homepage\') ?: $this->getOption(\'general/homepage\');\n        $listingParameters = $this->getListingParameters($homepage, true);\n        $content = $this->getContent($homepage, $listingParameters);\n\n        $template = $this->templateChooser()->homepage($content);\n        $globals = [];\n\n        if (is_array($content) && count($content) > 0) {\n            $first = current($content);\n            $globals[$first->contenttype[\'slug\']] = $content;\n            $globals[\'records\'] = $content;\n        } elseif (is_object($content)) {\n            $globals[\'record\'] = $content;\n            $globals[$content->contenttype[\'singular_slug\']] = $content;\n            $globals[\'records\'] = [$content->id => $content];\n        }\n\n        return $this->render($template, [], $globals);\n    }\n\n    /**\n     * Controller for a single record page, like \'/page/about/\' or \'/entry/lorum\'.\n     *\n     * @param Request $request         The request\n     * @param string  $contenttypeslug The content type slug\n     * @param string  $slug            The content slug\n     *\n     * @return TemplateResponse\n     */\n    public function record(Request $request, $contenttypeslug, $slug = \'\')\n    {\n        $contenttype = $this->getContentType($contenttypeslug);\n\n        // If the ContentType is \'viewless\', don\'t show the record page.\n        if (isset($contenttype[\'viewless\']) && $contenttype[\'viewless\'] === true) {\n            $this->abort(Response::HTTP_NOT_FOUND, "Page $contenttypeslug/$slug not found.");\n\n            return null;\n        }\n\n        // Perhaps we don\'t have a slug. Let\'s see if we can pick up the \'id\', instead.\n        if (empty($slug)) {\n            $slug = $request->get(\'id\');\n        }\n\n        $slug = $this->app[\'slugify\']->slugify($slug);\n\n        // First, try to get it by slug.\n        $content = $this->getContent($contenttype[\'slug\'], [\'slug\' => $slug, \'returnsingle\' => true, \'log_not_found\' => !is_numeric($slug)]);\n\n        if (is_numeric($slug) && !$content) {\n            // And otherwise try getting it by ID\n            $content = $this->getContent($contenttype[\'slug\'], [\'id\' => $slug, \'returnsingle\' => true]);\n        }\n\n        // No content, no page!\n        if (!$content) {\n            $this->abort(Response::HTTP_NOT_FOUND, "Page $contenttypeslug/$slug not found.");\n\n            return null;\n        }\n\n        // Then, select which template to use, based on our \'cascading templates rules\'\n        $template = $this->templateChooser()->record($content);\n\n        // Setting the editlink\n        $this->app[\'editlink\'] = $this->generateUrl(\'editcontent\', [\'contenttypeslug\' => $contenttype[\'slug\'], \'id\' => $content->id]);\n        $this->app[\'edittitle\'] = $content->getTitle();\n\n        // Make sure we can also access it as {{ page.title }} for pages, etc. We set these in the global scope,\n        // So that they\'re also available in menu\'s and templates rendered by extensions.\n        $globals = [\n            \'record\'                      => $content,\n            $contenttype[\'singular_slug\'] => $content,\n        ];\n\n        return $this->render($template, [], $globals);\n    }\n\n    /**\n     * The controller for previewing a content from posted data.\n     *\n     * @param Request $request         The Symfony Request\n     * @param string  $contenttypeslug The content type slug\n     *\n     * @throws \\Exception\n     *\n     * @return TemplateResponse\n     */\n    public function preview(Request $request, $contenttypeslug)\n    {\n        if (!$request->isMethod(\'POST\')) {\n            throw new MethodNotAllowedHttpException([\'POST\'], \'This route only accepts POST requests.\');\n        }\n\n        // Only accept requests with a valid token\n        $tokenValue = $request->request->get(\'content_edit\', [\'_token\' => null])[\'_token\'];\n        if (!$this->isAllowed(\'dashboard\') || !$this->isCsrfTokenValid($tokenValue, \'content_edit\')) {\n            return new Response(\'Not allowed or invalid CSRF token\', Response::HTTP_FORBIDDEN);\n        }\n\n        $contenttype = $this->getContentType($contenttypeslug);\n        $formValues = $request->request->all();\n\n        $storage = $this->storage();\n        if ($storage instanceof EntityManager) {\n            /** @var EntityManager $storage */\n            /** @var Content $content */\n            $content = $storage->create($contenttypeslug, $formValues);\n\n            /** @var Collection\\Relations $related */\n            $related = $storage->createCollection(Relations::class);\n            $related->setFromPost($formValues, $content);\n            $content->setRelation($related);\n\n            /** @var Collection\\Taxonomy $taxonomies */\n            $taxonomies = $storage->createCollection(Taxonomy::class);\n            $taxonomies->setFromPost($formValues, $content);\n            $content->setTaxonomy($taxonomies);\n\n        } else {\n            $content = $storage->getContentObject($contenttypeslug, [], false);\n            $content->setFromPost($formValues, $contenttype);\n        }\n\n        $this->fixBlockFieldsForPreview($content);\n        $this->fixHTMLFieldsForPreview($content);\n\n        $liveEditor = $request->get(\'_live-editor-preview\');\n        if (!empty($liveEditor)) {\n            $jsFile = (new JavaScript(\'js/ckeditor/ckeditor.js\', \'bolt\'))\n                ->setPriority(1)\n                ->setLate(false);\n            $cssFile = (new Stylesheet(\'css/liveeditor.css\', \'bolt\'))\n                ->setPriority(5)\n                ->setLate(false);\n            $snippet = (new Snippet())\n                ->setCallback(\'<script>window.boltIsEditing = true;</script>\')\n                ->setLocation(Target::BEFORE_HEAD_JS);\n\n            $this->app[\'asset.queue.snippet\']->add($snippet);\n            $this->app[\'asset.queue.file\']->add($jsFile);\n            $this->app[\'asset.queue.file\']->add($cssFile);\n        }\n\n        // Then, select which template to use, based on our \'cascading templates rules\'\n        $template = $this->templateChooser()->record($content);\n\n        // Make sure we can also access it as {{ page.title }} for pages, etc. We set these in the global scope,\n        // So that they\'re also available in menu\'s and templates rendered by extensions.\n        $globals = [\n            \'record\'                      => $content,\n            $contenttype[\'singular_slug\'] => $content,\n        ];\n\n        return $this->render($template, [], $globals);\n    }\n\n    /**\n     * Helper function to prefill `block` fields with an additional `block` field per item.\n     *\n     * This is needed to make previewing content with block fields fully working.\n     */\n    private function fixBlockFieldsForPreview($record)\n    {\n        foreach ($record->contenttype[\'fields\'] as $fieldSlug => $fieldSettings) {\n            if ($fieldSettings[\'type\'] === \'block\') {\n                $fieldValue = $this->getBlockFieldValue($record, $fieldSlug);\n                if ($fieldValue) {\n                    $newArray = [];\n                    foreach ($fieldValue as $item) {\n                        if (!empty($item)) {\n                            foreach ($item as $k => &$v) {\n                                $v[\'block\'] = $k;\n                            }\n                            $newArray[] = $v;\n                        }\n                    }\n                    $this->setFieldValue($record, $fieldSlug, $newArray);\n                }\n            }\n        }\n\n        return $record;\n    }\n\n    /**\n     * Helper function to make sure HTML-like fields are Twig_Markup, and image fields are\n     * an image. Like they are in a "real" page.\n     *\n     * This is needed to make previewing content with block fields fully working.\n     * See: https://github.com/bolt/bolt/issues/7753\n     */\n    private function fixHTMLFieldsForPreview($record)\n    {\n        foreach ($record->contenttype[\'fields\'] as $fieldSlug => $fieldSettings) {\n            if (in_array($fieldSettings[\'type\'], [\'html\', \'text\', \'textarea\', \'markdown\'])) {\n                $fieldValue = new \\Twig_Markup($this->getFieldValue($record, $fieldSlug), "UTF-8");\n\n                $this->setFieldValue($record, $fieldSlug, $fieldValue);\n            }\n            if ($fieldSettings[\'type\'] === \'image\') {\n                $fieldValue = $this->getFieldValue($record, $fieldSlug);\n\n                if (is_array($fieldValue) && isset($fieldValue[\'file\'])) {\n                    $this->setFieldValue($record, $fieldSlug, $fieldValue[\'file\']);\n                }\n            }\n        }\n\n        return $record;\n    }\n\n    private function getBlockFieldValue($record, $field)\n    {\n        if ($record instanceof Content) {\n            $value = $record->get($field);\n            if (is_array($value)) {\n                return $value;\n            }\n        } elseif (isset($record->values[$field]) && is_array($record->values[$field])) {\n            return $record->values[$field];\n        }\n\n        return null;\n    }\n\n    private function getFieldValue($record, $field)\n    {\n        if ($record instanceof Content) {\n            return $record->get($field);\n        } elseif (isset($record->values[$field]) && is_array($record->values[$field])) {\n            return $record->values[$field];\n        }\n\n        return null;\n    }\n\n    private function setFieldValue($record, $field, $value)\n    {\n        if ($record instanceof Content) {\n            $record->set($field, $value);\n        } else {\n            $record->values[$field] = $value;\n        }\n    }\n\n    /**\n     * The listing page controller.\n     *\n     * @param Request $request         The Symfony Request\n     * @param string  $contenttypeslug The content type slug\n     *\n     * @return TemplateResponse\n     */\n    public function listing(Request $request, $contenttypeslug)\n    {\n        $listingParameters = $this->getListingParameters($contenttypeslug);\n        $content = $this->getContent($contenttypeslug, $listingParameters);\n        $contenttype = $this->getContentType($contenttypeslug);\n\n        $template = $this->templateChooser()->listing($contenttype);\n\n        // Make sure we can also access it as {{ pages }} for pages, etc. We set these in the global scope,\n        // So that they\'re also available in menu\'s and templates rendered by extensions.\n        $globals = [\n            \'records\'        => $content,\n            $contenttypeslug => $content,\n            \'contenttype\'    => $contenttype[\'name\'],\n        ];\n\n        return $this->render($template, [], $globals);\n    }\n\n    /**\n     * The taxonomy listing page controller.\n     *\n     * @param Request $request      The Symfony Request\n     * @param string  $taxonomytype The taxonomy type slug\n     * @param string  $slug         The taxonomy slug\n     *\n     * @throws \\Bolt\\Exception\\InvalidRepositoryException\n     *\n     * @return TemplateResponse|false\n     */\n    public function taxonomy(Request $request, $taxonomytype, $slug)\n    {\n        $taxonomy = $this->app[\'config\']->get(\'taxonomy/\' . $taxonomytype);\n        // No taxonomytype, no possible content.\n        if (empty($taxonomy)) {\n            return false;\n        }\n        $taxonomyslug = $taxonomy[\'slug\'];\n\n        // First, get some content\n        $context = $taxonomy[\'singular_slug\'] . \'_\' . $slug;\n        $page = $this->app[\'pager\']->getCurrentPage($context);\n        // Theme value takes precedence over default config @see https://github.com/bolt/bolt/issues/3951\n        $amount = $this->getOption(\'theme/listing_records\', false) ?: $this->getOption(\'general/listing_records\');\n\n        // Handle case where listing records has been override for specific taxonomy\n        if (array_key_exists(\'listing_records\', $taxonomy) && is_int($taxonomy[\'listing_records\'])) {\n            $amount = $taxonomy[\'listing_records\'];\n        }\n\n        $order = $this->getOption(\'theme/listing_sort\', false) ?: $this->getOption(\'general/listing_sort\');\n        $isLegacy = $this->getOption(\'general/compatibility/setcontent_legacy\', true);\n        if ($isLegacy) {\n            $content = $this->storage()->getContentByTaxonomy($taxonomytype, $slug, [\'limit\' => $amount, \'order\' => $order, \'page\' => $page]);\n        } else {\n            $page = $this->app[\'pager\']->getCurrentPage(\'taxonomy\');\n            $appCt = array_keys($this->app[\'query.search_config\']->getSearchableTypes());\n            /** @var TaxonomyRepository $repo */\n            $repo = $this->app[\'storage\']->getRepository(Taxonomy::class);\n            $query = $repo->queryContentByTaxonomy($appCt, [$taxonomytype => $slug])\n                ->setFirstResult(($page - 1) * $amount)\n                ->setMaxResults($amount)\n            ;\n\n            $results = $repo->getContentByTaxonomy($query);\n            $set = new QueryResultset();\n            foreach ($results->getCollection() as $record) {\n                $set->add([$record]);\n            }\n            $content = $this->app[\'twig.records.view\']->createView($set);\n        }\n\n        if (!$this->isTaxonomyValid($content, $slug, $taxonomy)) {\n            $this->abort(Response::HTTP_NOT_FOUND, "No slug \'$slug\' in taxonomy \'$taxonomyslug\'");\n        }\n\n        $template = $this->templateChooser()->taxonomy($taxonomyslug);\n\n        // Get a display value for slug. This should be moved from \'slug\' context key to \'name\' in v4.0.\n        $name = $slug;\n        if ($taxonomy[\'behaves_like\'] !== \'tags\' && isset($taxonomy[\'options\'][$slug])) {\n            $name = $taxonomy[\'options\'][$slug];\n        }\n\n        $globals = [\n            \'records\'      => $content,\n            \'slug\'         => $name,\n            \'taxonomy\'     => $this->getOption(\'taxonomy/\' . $taxonomyslug),\n            \'taxonomytype\' => $taxonomyslug,\n        ];\n\n        return $this->render($template, [], $globals);\n    }\n\n    /**\n     * Check if the taxonomy is valid.\n     *\n     * @see https://github.com/bolt/bolt/pull/2310\n     *\n     * @param array|false $content\n     * @param string      $slug\n     * @param array       $taxonomy\n     *\n     * @return bool\n     */\n    protected function isTaxonomyValid($content, $slug, array $taxonomy)\n    {\n        if ($taxonomy[\'behaves_like\'] === \'tags\' && !$content) {\n            return false;\n        }\n\n        $isNotTag = in_array($taxonomy[\'behaves_like\'], [\'categories\', \'grouping\']);\n        $options = isset($taxonomy[\'options\']) ? array_keys($taxonomy[\'options\']) : [];\n        $isTax = in_array($slug, $options);\n        if ($isNotTag && !$isTax) {\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * The search result page controller.\n     *\n     * @param Request $request      The Symfony Request\n     * @param array   $contenttypes The content type slug(s) you want to search for\n     *\n     * @return TemplateResponse\n     */\n    public function search(Request $request, array $contenttypes = null)\n    {\n        $q = \'\';\n        $context = __FUNCTION__;\n\n        if ($request->query->has(\'q\')) {\n            $q = $request->query->get(\'q\');\n        } elseif ($request->query->has($context)) {\n            $q = $request->query->get($context);\n        }\n        $q = Input::cleanPostedData($q, false);\n\n        $page = $this->app[\'pager\']->getCurrentPage($context);\n\n        // Theme value takes precedence over default config @see https://github.com/bolt/bolt/issues/3951\n        $pageSize = $this->getOption(\'theme/search_results_records\', false);\n        if ($pageSize === false && !$pageSize = $this->getOption(\'general/search_results_records\', false)) {\n            $pageSize = $this->getOption(\'theme/listing_records\', false) ?: $this->getOption(\'general/listing_records\', 10);\n        }\n\n        $offset = ($page - 1) * $pageSize;\n        $limit = $pageSize;\n\n        // set-up filters from URL\n        $filters = [];\n        foreach ($request->query->all() as $key => $value) {\n            if (strpos($key, \'_\') > 0) {\n                list($contenttypeslug, $field) = explode(\'_\', $key, 2);\n                if (isset($filters[$contenttypeslug])) {\n                    $filters[$contenttypeslug][$field] = $value;\n                } else {\n                    $contenttype = $this->getContentType($contenttypeslug);\n                    if (is_array($contenttype)) {\n                        $filters[$contenttypeslug] = [\n                            $field => $value,\n                        ];\n                    }\n                }\n            }\n        }\n        if (count($filters) == 0) {\n            $filters = null;\n        }\n\n        $isLegacy = $this->getOption(\'general/compatibility/setcontent_legacy\', true);\n        if ($isLegacy) {\n            $result = $this->storage()->searchContent($q, $contenttypes, $filters, $limit, $offset);\n\n            /** @var \\Bolt\\Pager\\PagerManager $manager */\n            $manager = $this->app[\'pager\'];\n            $manager\n                ->createPager($context)\n                ->setCount($result[\'no_of_results\'])\n                ->setTotalpages(ceil($result[\'no_of_results\'] / $pageSize))\n                ->setCurrent($page)\n                ->setShowingFrom($offset + 1)\n                ->setShowingTo($offset + ($result ? count($result[\'results\']) : 0));\n            ;\n\n            $manager->setLink($this->generateUrl(\'search\', [\'q\' => $q]) . \'&page_search=\');\n        } else {\n            $appCt = array_keys($this->app[\'query.search_config\']->getSearchableTypes());\n            $textQuery = \'(\' . join(\',\', $appCt) . \')/search\';\n            $params = [\n                \'filter\' => $q,\n                \'page\'   => $page,\n                \'limit\'  => $pageSize,\n            ];\n            $searchResult = $this->getContent($textQuery, $params);\n\n            $result = [\n                \'results\' => $searchResult->getSortedResults(),\n                \'query\'   => [\n                    \'sanitized_q\' => strip_tags($q),\n                ],\n            ];\n        }\n\n        $globals = [\n            \'records\'      => $result[\'results\'],\n            $context       => $result[\'query\'][\'sanitized_q\'],\n            \'searchresult\' => $result,\n        ];\n\n        $template = $this->templateChooser()->search();\n\n        return $this->render($template, [], $globals);\n    }\n\n    /**\n     * Renders the specified template from the current theme in response to a request without\n     * loading any content.\n     *\n     * @param string $template The template name\n     *\n     * @return TemplateResponse\n     */\n    public function template($template)\n    {\n        // Add the template extension if it is missing\n        if (!preg_match(\'/\\\\.twig$/i\', $template)) {\n            $template .= \'.twig\';\n        }\n\n        return $this->render($template);\n    }\n\n    /**\n     * Returns an array of the parameters used in getContent for listing pages.\n     *\n     * @param string $contentTypeSlug The content type slug\n     * @param bool   $allowViewless   Allow viewless contenttype\n     *\n     * @return array Parameters to use in getContent\n     */\n    private function getListingParameters($contentTypeSlug, $allowViewless = false)\n    {\n        $contentType = $this->getContentType(current(explode(\'/\', $contentTypeSlug)));\n\n        // If there is no ContentType, don\'t get parameters for it\n        if ($contentType === false) {\n            return [];\n        }\n\n        // If the ContentType is \'viewless\', don\'t show the listing / record page.\n        if ($contentType[\'viewless\'] && !$allowViewless) {\n            $this->abort(Response::HTTP_NOT_FOUND, \'Page \' . $contentType[\'slug\'] . \' not found.\');\n        }\n\n        // Build the pager\n        $page = $this->app[\'pager\']->getCurrentPage($contentType[\'slug\']);\n        $order = isset($contentType[\'listing_sort\']) ? $contentType[\'listing_sort\'] : $this->getListingOrder($contentType);\n\n        // CT value takes precedence over theme & config.yml\n        if (!empty($contentType[\'listing_records\'])) {\n            $amount = $contentType[\'listing_records\'];\n        } else {\n            $amount = $this->getOption(\'theme/listing_records\') ?: $this->getOption(\'general/listing_records\');\n        }\n\n        return [\'limit\' => $amount, \'order\' => $order, \'page\' => $page, \'paging\' => true];\n    }\n\n    /**\n     * Return the listing order.\n     *\n     * If the ContentType\'s sort is false (default in Config::parseContentType),\n     * either:\n     *  - we let `getContent()` sort by itself\n     *  - we explicitly set it to sort on the general/listing_sort setting\n     *\n     * @param ContentType|array $contentType\n     *\n     * @return null|string\n     */\n    private function getListingOrder($contentType)\n    {\n        // An empty default isn\'t set in config yet, arrays got to hate them.\n        if (isset($contentType[\'taxonomy\'])) {\n            $taxonomies = $this->getOption(\'taxonomy\');\n            foreach ($contentType[\'taxonomy\'] as $taxonomyName) {\n                if ($taxonomies[$taxonomyName][\'has_sortorder\']) {\n                    // Let getContent() handle it\n                    return null;\n                }\n            }\n        }\n\n        return $this->getOption(\'theme/listing_sort\') ?: $this->getOption(\'general/listing_sort\');\n    }\n}\n', 'message': 'Fix bolt/bolt#7902 PHP 7.4 exception on empty search\n\nPHP 7.4 throws an exception when someone tries to access an array item\non a variable of boolean type. This commit adds a check that the array\nis populated before attempting to access an array member.\n', 'author_name': 'Stephen Pewter', 'lang': 'php'}