Passed
Push — master ( 854cc1...b77bfa )
by MusikAnimal
07:15
created

DefaultController::getParamsForRegexQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 38
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 32
dl 0
loc 38
rs 9.408
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Controller;
6
7
use App\Model\Query;
8
use App\Repository\CloudElasticRepository;
9
use App\Repository\WikiDomainRepository;
10
use GuzzleHttp\Client;
11
use Psr\Cache\CacheItemPoolInterface;
12
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
13
use Symfony\Component\HttpFoundation\Request;
14
use Symfony\Component\HttpFoundation\Response;
15
use Symfony\Component\Routing\Annotation\Route;
16
17
/**
18
 * A DefaultController serves the main routes for the application and processing the submitted form.
19
 */
20
class DefaultController extends AbstractController
21
{
22
    /** @var Client */
23
    private $client;
24
25
    /** @var CacheItemPoolInterface */
26
    private $cache;
27
28
    /** @var bool Whether the results were pulled from cache. */
29
    private $fromCache = false;
30
31
    /** @var string Duration of cache for main results set, as accepted by DateInterval::createFromDateString() */
32
    private const CACHE_TIME = '10 minutes';
33
34
    /** @var string[]|null Map from wiki dbname to domain name */
35
    private $domainLookup;
36
37
    /**
38
     * DefaultController constructor.
39
     * @param CacheItemPoolInterface $cache
40
     */
41
    public function __construct(CacheItemPoolInterface $cache)
42
    {
43
        $this->client = new Client([
44
            'verify' => $_ENV['ELASTIC_INSECURE'] ? false : true,
45
        ]);
46
        $this->cache = $cache;
47
    }
48
49
    /**
50
     * Splash page, shown when user is logged out.
51
     * @Route("/splash")
52
     */
53
    public function splashAction(): Response
54
    {
55
        return $this->render('jumbotron.html.twig');
56
    }
57
58
    /**
59
     * The main route.
60
     * @Route("/", name="home")
61
     * @param Request $request
62
     * @return Response
63
     */
64
    public function indexAction(Request $request): Response
65
    {
66
        if (!$this->get('session')->get('logged_in_user')) {
67
            return $this->render('jumbotron.html.twig');
68
        }
69
        $query = $request->query->get('q');
70
        $regex = (bool)$request->query->get('regex');
71
        $ignoreCase = (bool)$request->query->get('ignorecase');
72
        [$namespaces, $namespaceIds] = $this->parseNamespaces($request);
73
        $purgeCache = (bool)$request->query->get('purge');
74
75
        $ret = [
76
            'q' => $query,
77
            'regex' => $regex,
78
            'max_results' => Query::MAX_RESULTS,
79
            'namespaces' => $namespaces,
80
            'ignore_case' => $ignoreCase,
81
        ];
82
83
        if ($query) {
84
            $ret = array_merge($ret, $this->getResults($query, $regex, $ignoreCase, $namespaceIds, $purgeCache));
85
            $ret['from_cache'] = $this->fromCache;
86
            return $this->render('default/result.html.twig', $ret);
87
        }
88
89
        return $this->render('default/index.html.twig', $ret);
90
    }
91
92
    /**
93
     * Get results based on given Request.
94
     * @param string $query
95
     * @param bool $regex
96
     * @param bool $ignoreCase
97
     * @param int[] $namespaceIds
98
     * @param bool $purgeCache
99
     * @return mixed[]
100
     */
101
    public function getResults(
102
        string $query,
103
        bool $regex,
104
        bool $ignoreCase,
105
        array $namespaceIds,
106
        bool $purgeCache = false
107
    ): array {
108
        $cacheItem = md5($query.$regex.$ignoreCase.implode('|', $namespaceIds));
109
110
        if (!$purgeCache && $this->cache->hasItem($cacheItem)) {
111
            $this->fromCache = true;
112
            return $this->cache->getItem($cacheItem)->get();
113
        }
114
115
        $query = new Query($query, $namespaceIds, $regex, $ignoreCase);
116
        $params = $query->getParams();
117
        $res = (new CloudElasticRepository($this->client, $params))->makeRequest();
118
        $data = [
119
            'query' => $query,
120
            'regex' => $regex,
121
            'ignore_case' => $ignoreCase,
122
            'total' => $res['hits']['total'],
123
            'hits' => $this->formatHits($res),
124
        ];
125
126
        $cacheItem = $this->cache->getItem($cacheItem)
127
            ->set($data)
128
            ->expiresAfter(\DateInterval::createFromDateString(self::CACHE_TIME));
129
        $this->cache->save($cacheItem);
130
        return $data;
131
    }
132
133
    /**
134
     * Build the data structure for each hit, giving the view what it needs.
135
     * @param mixed[] $data
136
     * @return mixed[]
137
     */
138
    private function formatHits(array $data): array
139
    {
140
        $hits = $data['hits']['hits'];
141
        $newData = [];
142
143
        foreach ($hits as $hit) {
144
            $result = $hit['_source'];
145
            $title = ($result['namespace_text'] ? $result['namespace_text'].':' : '').$result['title'];
146
            $domain = $this->getWikiDomainFromDbName($result['wiki']);
147
            $newData[] = [
148
                'wiki' => rtrim($domain, '.org'),
149
                'title' => $title,
150
                'url' => $this->getUrlForTitle($domain, $title),
151
                'source_text' => $this->highlightQuery(
152
                    $hit['highlight']['source_text.plain'][0] ?? ''
153
                ),
154
            ];
155
        }
156
157
        return $newData;
158
    }
159
160
    /**
161
     * Get the URL to the page with the given title on the given wiki.
162
     * @param string $domain
163
     * @param string $title
164
     * @return string
165
     */
166
    private function getUrlForTitle(string $domain, string $title): string
167
    {
168
        return 'https://'.$domain.'/wiki/'.$title;
169
    }
170
171
    /**
172
     * Query Siteinfo API to get the domain of the wiki with the given database name.
173
     * @param string $wiki
174
     * @return string
175
     */
176
    private function getWikiDomainFromDbName(string $wiki): string
177
    {
178
        if (null === $this->domainLookup) {
179
            $this->domainLookup = (new WikiDomainRepository($this->client, $this->cache))->load();
180
        }
181
        return $this->domainLookup[$wiki] ?? 'WIKINOTFOUND';
182
    }
183
184
    /**
185
     * Make the highlight text safe and wrap the search term in a span so that we can style it.
186
     * @param string $text
187
     * @return string
188
     */
189
    private function highlightQuery(string $text): string
190
    {
191
        $text = htmlspecialchars($text);
192
        return strtr($text, [
193
            Query::PRE_TAG => "<span class='highlight'>",
194
            Query::POST_TAG => "</span>",
195
        ]);
196
    }
197
198
    /**
199
     * Parse the namespaces parameter of the query string.
200
     * @param Request $request
201
     * @return mixed[] [normalized comma-separated list as a string, array of ids as ints]
202
     */
203
    private function parseNamespaces(Request $request): array
204
    {
205
        $param = $request->query->get('namespaces', '');
206
207
        if ('' === $param) {
208
            $ids = [];
209
        } else {
210
            $ids = array_map(
211
                'intval',
212
                explode(',', $param)
213
            );
214
        }
215
216
        return [
217
            implode(',', $ids),
218
            $ids,
219
        ];
220
    }
221
}
222