DefaultController   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 264
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 105
c 8
b 0
f 0
dl 0
loc 264
rs 10
wmc 24

11 Methods

Rating   Name   Duplication   Size   Complexity  
A splashAction() 0 3 1
A __construct() 0 6 2
A getFilenameForRequest() 0 4 1
A formatResponse() 0 36 5
A getResults() 0 31 3
A formatHits() 0 20 3
A parseNamespaces() 0 14 2
A getWikiDomainFromDbName() 0 6 2
A indexAction() 0 35 3
A highlightQuery() 0 6 1
A getUrlForTitle() 0 3 1
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\JsonResponse;
14
use Symfony\Component\HttpFoundation\Request;
15
use Symfony\Component\HttpFoundation\Response;
16
use Symfony\Component\Routing\Annotation\Route;
17
18
/**
19
 * A DefaultController serves the main routes for the application and processing the submitted form.
20
 */
21
class DefaultController extends AbstractController
22
{
23
    /** @var Client */
24
    private $client;
25
26
    /** @var CacheItemPoolInterface */
27
    private $cache;
28
29
    /** @var bool Whether the results were pulled from cache. */
30
    private $fromCache = false;
31
32
    /** @var string Duration of cache for main results set, as accepted by DateInterval::createFromDateString() */
33
    private const CACHE_TIME = '10 minutes';
34
35
    /** @var string[]|null Map from wiki dbname to domain name */
36
    private $domainLookup;
37
38
    /**
39
     * DefaultController constructor.
40
     * @param CacheItemPoolInterface $cache
41
     */
42
    public function __construct(CacheItemPoolInterface $cache)
43
    {
44
        $this->client = new Client([
45
            'verify' => $_ENV['ELASTIC_INSECURE'] ? false : true,
46
        ]);
47
        $this->cache = $cache;
48
    }
49
50
    /**
51
     * Splash page, shown when user is logged out.
52
     * @Route("/splash")
53
     */
54
    public function splashAction(): Response
55
    {
56
        return $this->render('jumbotron.html.twig');
57
    }
58
59
    /**
60
     * The main route.
61
     * @Route("/", name="home")
62
     * @param Request $request
63
     * @return Response
64
     */
65
    public function indexAction(Request $request): Response
66
    {
67
        if (!$this->get('session')->get('logged_in_user')) {
68
            return $this->render('jumbotron.html.twig');
69
        }
70
        $query = $request->query->get('q');
71
        $regex = (bool)$request->query->get('regex');
72
        $ignoreCase = (bool)$request->query->get('ignorecase');
73
        $namespaceIds = $this->parseNamespaces($request);
74
        $titlePattern = $request->query->get('title');
75
        $purgeCache = (bool)$request->query->get('purge');
76
77
        $ret = [
78
            'q' => $query,
79
            'regex' => $regex,
80
            'max_results' => Query::MAX_RESULTS,
81
            'namespaces' => $namespaceIds,
82
            'title' => $titlePattern,
83
            'ignore_case' => $ignoreCase,
84
        ];
85
86
        if ($query) {
87
            $ret = array_merge($ret, $this->getResults(
88
                $query,
89
                $regex,
90
                $ignoreCase,
91
                $namespaceIds,
92
                $titlePattern,
93
                $purgeCache
94
            ));
95
            $ret['from_cache'] = $this->fromCache;
96
            return $this->formatResponse($request, $query, $ret);
97
        }
98
99
        return $this->render('default/index.html.twig', $ret);
100
    }
101
102
    /**
103
     * Get the rendered template for the requested format.
104
     * @param Request $request
105
     * @param string $query Query string, used for filenames.
106
     * @param mixed[] $data Data that should be passed to the view.
107
     * @return Response
108
     */
109
    private function formatResponse(Request $request, string $query, array $data): Response
110
    {
111
        $format = $request->query->get('format', 'html');
112
        if ('' == $format) {
113
            // The default above doesn't work when the 'format' parameter is blank.
114
            $format = 'html';
115
        }
116
117
        $formatMap = [
118
            'html' => 'text/html',
119
            'wikitext' => 'text/plain',
120
            'csv' => 'text/csv',
121
            'tsv' => 'text/tab-separated-values',
122
            'json' => 'application/json',
123
        ];
124
125
        // Use HTML if unknown format requested.
126
        $format = isset($formatMap[$format]) ? $format : 'html';
127
128
        // Nothing more needed if requesting JSON.
129
        if ('json' === $format) {
130
            return new JsonResponse($data);
131
        }
132
133
        $response = new Response();
134
135
        $response->headers->set('Content-Type', $formatMap[$format]);
136
        if (in_array($format, ['csv', 'tsv'])) {
137
            $filename = $this->getFilenameForRequest($query);
138
            $response->headers->set(
139
                'Content-Disposition',
140
                "attachment; filename=\"{$filename}.$format\""
141
            );
142
        }
143
144
        return $this->render("default/result.$format.twig", $data, $response);
145
    }
146
147
    /**
148
     * Returns pretty filename from the given query, with problematic characters filtered out.
149
     * @param string $query
150
     * @return string
151
     */
152
    private function getFilenameForRequest(string $query): string
153
    {
154
        $filename = trim($query, '/');
155
        return trim(preg_replace('/[-\/\\:;*?|<>%#"]+/', '-', $filename));
156
    }
157
158
    /**
159
     * Get results based on given Request.
160
     * @param string $query
161
     * @param bool $regex
162
     * @param bool $ignoreCase
163
     * @param int[] $namespaceIds
164
     * @param string|null $titlePattern
165
     * @param bool $purgeCache
166
     * @return mixed[]
167
     */
168
    public function getResults(
169
        string $query,
170
        bool $regex,
171
        bool $ignoreCase,
172
        array $namespaceIds,
173
        ?string $titlePattern = null,
174
        bool $purgeCache = false
175
    ): array {
176
        $cacheItem = md5($query.$regex.$ignoreCase.$titlePattern.implode('|', $namespaceIds));
177
178
        if (!$purgeCache && $this->cache->hasItem($cacheItem)) {
179
            $this->fromCache = true;
180
            return $this->cache->getItem($cacheItem)->get();
181
        }
182
183
        $query = new Query($query, $namespaceIds, $regex, $ignoreCase, $titlePattern);
184
        $params = $query->getParams();
185
        $res = (new CloudElasticRepository($this->client, $params))->makeRequest();
186
        $data = [
187
            'regex' => $regex,
188
            'ignore_case' => $ignoreCase,
189
            'title' => $titlePattern,
190
            'total' => $res['hits']['total'],
191
            'hits' => $this->formatHits($res),
192
        ];
193
194
        $cacheItem = $this->cache->getItem($cacheItem)
195
            ->set($data)
196
            ->expiresAfter(\DateInterval::createFromDateString(self::CACHE_TIME));
197
        $this->cache->save($cacheItem);
198
        return $data;
199
    }
200
201
    /**
202
     * Build the data structure for each hit, giving the view what it needs.
203
     * @param mixed[] $data
204
     * @return mixed[]
205
     */
206
    private function formatHits(array $data): array
207
    {
208
        $hits = $data['hits']['hits'];
209
        $newData = [];
210
211
        foreach ($hits as $hit) {
212
            $result = $hit['_source'];
213
            $title = ($result['namespace_text'] ? $result['namespace_text'].':' : '').$result['title'];
214
            $domain = $this->getWikiDomainFromDbName($result['wiki']);
215
            $newData[] = [
216
                'wiki' => rtrim($domain, '.org'),
217
                'title' => $title,
218
                'url' => $this->getUrlForTitle($domain, $title),
219
                'source_text' => $this->highlightQuery(
220
                    $hit['highlight']['source_text.plain'][0] ?? ''
221
                ),
222
            ];
223
        }
224
225
        return $newData;
226
    }
227
228
    /**
229
     * Get the URL to the page with the given title on the given wiki.
230
     * @param string $domain
231
     * @param string $title
232
     * @return string
233
     */
234
    private function getUrlForTitle(string $domain, string $title): string
235
    {
236
        return 'https://'.$domain.'/wiki/'.$title;
237
    }
238
239
    /**
240
     * Query Siteinfo API to get the domain of the wiki with the given database name.
241
     * @param string $wiki
242
     * @return string
243
     */
244
    private function getWikiDomainFromDbName(string $wiki): string
245
    {
246
        if (null === $this->domainLookup) {
247
            $this->domainLookup = (new WikiDomainRepository($this->client, $this->cache))->load();
248
        }
249
        return $this->domainLookup[$wiki] ?? 'WIKINOTFOUND';
250
    }
251
252
    /**
253
     * Make the highlight text safe and wrap the search term in a span so that we can style it.
254
     * @param string $text
255
     * @return string
256
     */
257
    private function highlightQuery(string $text): string
258
    {
259
        $text = htmlspecialchars($text);
260
        return strtr($text, [
261
            Query::PRE_TAG => "<span class='highlight'>",
262
            Query::POST_TAG => "</span>",
263
        ]);
264
    }
265
266
    /**
267
     * Parse the namespaces parameter of the query string.
268
     * @param Request $request
269
     * @return int[] Namespace IDs.
270
     */
271
    private function parseNamespaces(Request $request): array
272
    {
273
        $param = $request->query->get('namespaces', '');
274
275
        if ('' === $param) {
276
            $ids = [];
277
        } else {
278
            $ids = array_map(
279
                'intval',
280
                explode(',', $param)
281
            );
282
        }
283
284
        return $ids;
285
    }
286
}
287