Completed
Push — master ( ba9152...abd588 )
by Henri
03:52
created

WebController::listStyle()   B

Complexity

Conditions 8
Paths 4

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 7.7777
c 0
b 0
f 0
cc 8
eloc 9
nc 4
nop 0
1
<?php
2
3
/**
4
 * Importing the dependencies.
5
 */
6
use \Punic\Language;
7
8
/**
9
 * WebController is an extension of the Controller that handles all
10
 * the requests originating from the view of the website.
11
 */
12
class WebController extends Controller
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
13
{
14
    /**
15
     * Provides access to the templating engine.
16
     * @property object $twig the twig templating engine.
17
     */
18
    public $twig;
19
20
    /**
21
     * Constructor for the WebController.
22
     * @param Model $model
23
     */
24
    public function __construct($model)
25
    {
26
        parent::__construct($model);
27
28
        // initialize Twig templates
29
        $tmpDir = $model->getConfig()->getTemplateCache();
30
31
        // check if the cache pointed by config.inc exists, if not we create it.
32
        if (!file_exists($tmpDir)) {
33
            mkdir($tmpDir);
34
        }
35
36
        // specify where to look for templates and cache
37
        $loader = new Twig_Loader_Filesystem('view');
38
        // initialize Twig environment
39
        $this->twig = new Twig_Environment($loader, array('cache' => $tmpDir,
40
            'auto_reload' => true, 'debug' => true));
41
        $this->twig->addExtension(new Twig_Extensions_Extension_I18n());
42
        //ENABLES DUMP() method for easy and fun debugging!
43
        $this->twig->addExtension(new Twig_Extension_Debug());
44
        // used for setting the base href for the relative urls
45
        $this->twig->addGlobal("BaseHref", $this->getBaseHref());
46
        // setting the service name string from the config.inc
47
        $this->twig->addGlobal("ServiceName", $this->model->getConfig()->getServiceName());
48
        // setting the service logo location from the config.inc
49
        if ($this->model->getConfig()->getServiceLogo() !== null) {
50
            $this->twig->addGlobal("ServiceLogo", $this->model->getConfig()->getServiceLogo());
51
        }
52
53
        // setting the service custom css file from the config.inc
54
        if ($this->model->getConfig()->getCustomCss() !== null) {
55
            $this->twig->addGlobal("ServiceCustomCss", $this->model->getConfig()->getCustomCss());
56
        }
57
        // used for displaying the ui language selection as a dropdown
58
        if ($this->model->getConfig()->getUiLanguageDropdown() !== null) {
59
            $this->twig->addGlobal("LanguageDropdown", $this->model->getConfig()->getUiLanguageDropdown());
60
        }
61
62
        // setting the list of properties to be displayed in the search results
63
        $this->twig->addGlobal("PreferredProperties", array('skos:prefLabel', 'skos:narrower', 'skos:broader', 'skosmos:memberOf', 'skos:altLabel', 'skos:related'));
64
65
        // register a Twig filter for generating URLs for vocabulary resources (concepts and groups)
66
        $controller = $this; // for use by anonymous function below
67
        $urlFilter = new Twig_SimpleFilter('link_url', function ($uri, $vocab, $lang, $type = 'page', $clang = null, $term = null) use ($controller) {
68
            // $vocab can either be null, a vocabulary id (string) or a Vocabulary object
69
            if ($vocab === null) {
70
                // target vocabulary is unknown, best bet is to link to the plain URI
71
                return $uri;
72
            } elseif (is_string($vocab)) {
73
                $vocid = $vocab;
74
                $vocab = $controller->model->getVocabulary($vocid);
75
            } else {
76
                $vocid = $vocab->getId();
77
            }
78
79
            $params = array();
80
            if (isset($clang) && $clang !== $lang) {
81
                $params['clang'] = $clang;
82
            }
83
84
            if (isset($term)) {
85
                $params['q'] = $term;
86
            }
87
88
            // case 1: URI within vocabulary namespace: use only local name
89
            $localname = $vocab->getLocalName($uri);
90
            if ($localname !== $uri && $localname === urlencode($localname)) {
91
                // check that the prefix stripping worked, and there are no problematic chars in localname
92
                $paramstr = sizeof($params) > 0 ? '?' . http_build_query($params) : '';
93
                if ($type && $type !== '' && $type !== 'vocab' && !($localname === '' && $type === 'page')) {
94
                    return "$vocid/$lang/$type/$localname" . $paramstr;
95
                }
96
97
                return "$vocid/$lang/$localname" . $paramstr;
98
            }
99
100
            // case 2: URI outside vocabulary namespace, or has problematic chars
101
            // pass the full URI as parameter instead
102
            $params['uri'] = $uri;
103
            return "$vocid/$lang/$type/?" . http_build_query($params);
104
        });
105
        $this->twig->addFilter($urlFilter);
106
107
        // register a Twig filter for generating strings from language codes with CLDR
108
        $langFilter = new Twig_SimpleFilter('lang_name', function ($langcode, $lang) {
109
            return Language::getName($langcode, $lang);
110
        });
111
        $this->twig->addFilter($langFilter);
112
113
    }
114
115
    /**
116
     * Guess the language of the user. Return a language string that is one
117
     * of the supported languages defined in the $LANGUAGES setting, e.g. "fi".
118
     * @param string $vocid identifier for the vocabulary eg. 'yso'.
119
     * @return string returns the language choice as a numeric string value
120
     */
121
    public function guessLanguage($vocid = null)
122
    {
123
        // 1. select language based on SKOSMOS_LANGUAGE cookie
124
        if (filter_input(INPUT_COOKIE, 'SKOSMOS_LANGUAGE', FILTER_SANITIZE_STRING)) {
125
            return filter_input(INPUT_COOKIE, 'SKOSMOS_LANGUAGE', FILTER_SANITIZE_STRING);
126
        }
127
128
        // 2. if vocabulary given, select based on the default language of the vocabulary
129
        if ($vocid !== null && $vocid !== '') {
130
            try {
131
                $vocab = $this->model->getVocabulary($vocid);
132
                return $vocab->getConfig()->getDefaultLanguage();
133
            } catch (Exception $e) {
134
                // vocabulary id not found, move on to the next selection method
135
            }
136
        }
137
138
        // 3. select language based on Accept-Language header
139
        header('Vary: Accept-Language'); // inform caches that a decision was made based on Accept header
140
        $this->negotiator = new \Negotiation\LanguageNegotiator();
141
        $langcodes = array_keys($this->languages);
142
        $acceptLanguage = filter_input(INPUT_SERVER, 'HTTP_ACCEPT_LANGUAGE', FILTER_SANITIZE_STRING) ? filter_input(INPUT_SERVER, 'HTTP_ACCEPT_LANGUAGE', FILTER_SANITIZE_STRING) : '';
143
        $bestLang = $this->negotiator->getBest($acceptLanguage, $langcodes);
144
        if (isset($bestLang) && in_array($bestLang, $langcodes)) {
145
            return $bestLang->getValue();
146
        }
147
148
        // show default site or prompt for language
149
        return $langcodes[0];
150
    }
151
152
    /**
153
     * Determines a css class that controls width and positioning of the vocabulary list element. 
154
     * The layout is wider if the left/right box templates have not been provided.
155
     * @return string css class for the container eg. 'voclist-wide' or 'voclist-right'
156
     */
157
    private function listStyle() {
158
        $left = file_exists('view/left.inc');
159
        $right = file_exists('view/right.inc');
160
        $ret = 'voclist';
161
        if (!$left && !$right) {
162
            $ret .= '-wide';
163
        } else if (!($left && $right) && ($right || $left)) {
164
            $ret .= ($right) ? '-left' : '-right';
165
        }
166
        return $ret;
167
    }
168
169
    /**
170
     * Loads and renders the view containing all the vocabularies.
171
     * @param Request $request
172
     */
173
    public function invokeVocabularies($request)
174
    {
175
        // set language parameters for gettext
176
        $this->setLanguageProperties($request->getLang());
177
        // load template
178
        $template = $this->twig->loadTemplate('light.twig');
179
        // set template variables
180
        $categoryLabel = $this->model->getClassificationLabel($request->getLang());
181
        $sortedVocabs = $this->model->getVocabularyList(false, true);
182
        $langList = $this->model->getLanguages($request->getLang());
183
        $listStyle = $this->listStyle(); 
184
185
        // render template
186
        echo $template->render(
187
            array(
188
                'sorted_vocabs' => $sortedVocabs,
189
                'category_label' => $categoryLabel,
190
                'languages' => $this->languages,
191
                'lang_list' => $langList,
192
                'request' => $request,
193
                'list_style' => $listStyle
194
            ));
195
    }
196
197
    /**
198
     * Invokes the concept page of a single concept in a specific vocabulary.
199
     */
200
    public function invokeVocabularyConcept($request)
201
    {
202
        $lang = $request->getLang();
203
        $this->setLanguageProperties($lang);
204
        $vocab = $request->getVocab();
205
206
        $langcodes = $vocab->getConfig()->getShowLangCodes();
207
        $uri = $vocab->getConceptURI($request->getUri()); // make sure it's a full URI
208
209
        $results = $vocab->getConceptInfo($uri, $request->getContentLang());
210
        if (!$results) {
211
            $this->invokeGenericErrorPage($request);
212
            return;
213
        }
214
        $template = (in_array('skos:Concept', $results[0]->getType())) ? $this->twig->loadTemplate('concept-info.twig') : $this->twig->loadTemplate('group-contents.twig');
215
        
216
        $crumbs = $vocab->getBreadCrumbs($request->getContentLang(), $uri);
217
        echo $template->render(array(
218
            'search_results' => $results,
219
            'vocab' => $vocab,
220
            'languages' => $this->languages,
221
            'explicit_langcodes' => $langcodes,
222
            'bread_crumbs' => $crumbs['breadcrumbs'],
223
            'combined' => $crumbs['combined'],
224
            'request' => $request)
225
        );
226
    }
227
228
    /**
229
     * Invokes the feedback page with information of the users current vocabulary.
230
     */
231
    public function invokeFeedbackForm($request)
232
    {
233
        $template = $this->twig->loadTemplate('feedback.twig');
234
        $this->setLanguageProperties($request->getLang());
235
        $vocabList = $this->model->getVocabularyList(false);
236
        $vocab = $request->getVocab();
237
238
        $feedbackSent = false;
239
        $feedbackMsg = null;
240
        if ($request->getQueryParamPOST('message')) {
241
            $feedbackSent = true;
242
            $feedbackMsg = $request->getQueryParamPOST('message');
243
        }
244
        $feedbackName = $request->getQueryParamPOST('name');
245
        $feedbackEmail = $request->getQueryParamPOST('email');
246
        $feedbackVocab = $request->getQueryParamPOST('vocab');
247
        $feedbackVocabEmail = ($vocab !== null) ? $vocab->getConfig()->getFeedbackRecipient() : null;
248
249
        // if the hidden field has been set a value we have found a spam bot
250
        // and we do not actually send the message.
251
        if ($feedbackSent && $request->getQueryParamPOST('trap') === '') {
252
            $this->sendFeedback($request, $feedbackMsg, $feedbackName, $feedbackEmail, $feedbackVocab, $feedbackVocabEmail);
253
        }
254
255
        echo $template->render(
256
            array(
257
                'languages' => $this->languages,
258
                'vocab' => $vocab,
259
                'vocabList' => $vocabList,
260
                'feedback_sent' => $feedbackSent,
261
                'request' => $request,
262
            ));
263
    }
264
265
    private function createFeedbackHeaders($fromName, $fromEmail, $toMail)
266
    {
267
        $headers = "MIME-Version: 1.0″ . '\r\n";
268
        $headers .= "Content-type: text/html; charset=UTF-8" . "\r\n";
269
        if ($toMail) {
270
            $headers .= "Cc: " . $this->model->getConfig()->getFeedbackAddress() . "\r\n";
271
        }
272
273
        $headers .= "From: $fromName <$fromEmail>" . "\r\n" . 'X-Mailer: PHP/' . phpversion();
274
        return $headers;
275
    }
276
277
    /**
278
     * Sends the user entered message through the php's mailer.
279
     * @param string $message only required parameter is the actual message.
280
     * @param string $fromName senders own name.
281
     * @param string $fromEmail senders email adress.
282
     * @param string $fromVocab which vocabulary is the feedback related to.
283
     */
284
    public function sendFeedback($request, $message, $fromName = null, $fromEmail = null, $fromVocab = null, $toMail = null)
285
    {
286
        $toAddress = ($toMail) ? $toMail : $this->model->getConfig()->getFeedbackAddress();
287
        if ($fromVocab && $fromVocab !== '') {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fromVocab of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
288
            $message = 'Feedback from vocab: ' . strtoupper($fromVocab) . "<br />" . $message;
289
        }
290
291
        $subject = SERVICE_NAME . " feedback";
292
        $headers = $this->createFeedbackHeaders($fromName, $fromEmail, $toMail);
293
        $envelopeSender = FEEDBACK_ENVELOPE_SENDER;
294
        $params = empty($envelopeSender) ? '' : "-f $envelopeSender";
295
296
        // adding some information about the user for debugging purposes.
297
        $message = $message . "<br /><br /> Debugging information:"
298
            . "<br />Timestamp: " . date(DATE_RFC2822)
299
            . "<br />User agent: " . $request->getServerConstant('HTTP_USER_AGENT')
300
            . "<br />IP address: " . $request->getServerConstant('REMOTE_ADDR')
301
            . "<br />Referer: " . $request->getServerConstant('HTTP_REFERER');
302
303
        try {
304
            mail($toAddress, $subject, $message, $headers, $params);
305
        } catch (Exception $e) {
306
            header("HTTP/1.0 404 Not Found");
307
            $template = $this->twig->loadTemplate('error-page.twig');
308
            if ($this->model->getConfig()->getLogCaughtExceptions()) {
309
                error_log('Caught exception: ' . $e->getMessage());
310
            }
311
312
            echo $template->render(
313
                array(
314
                    'languages' => $this->languages,
315
                ));
316
317
            return;
318
        }
319
    }
320
321
    /**
322
     * Invokes the about page for the Skosmos service.
323
     */
324 View Code Duplication
    public function invokeAboutPage($request)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
325
    {
326
        $template = $this->twig->loadTemplate('about.twig');
327
        $this->setLanguageProperties($request->getLang());
328
        $url = $request->getServerConstant('HTTP_HOST');
329
        $version = $this->model->getVersion();
330
331
        echo $template->render(
332
            array(
333
                'languages' => $this->languages,
334
                'version' => $version,
335
                'server_instance' => $url,
336
                'request' => $request,
337
            ));
338
    }
339
340
    /**
341
     * Invokes the search for concepts in all the availible ontologies.
342
     */
343
    public function invokeGlobalSearch($request)
344
    {
345
        $lang = $request->getLang();
346
        $template = $this->twig->loadTemplate('vocab-search-listing.twig');
347
        $this->setLanguageProperties($lang);
348
349
        $parameters = new ConceptSearchParameters($request, $this->model->getConfig());
350
351
        $vocabs = $request->getQueryParam('vocabs'); # optional
352
        // convert to vocids array to support multi-vocabulary search
353
        $vocids = ($vocabs !== null && $vocabs !== '') ? explode(' ', $vocabs) : null;
354
        $vocabObjects = array();
355
        if ($vocids) {
356
            foreach($vocids as $vocid) {
357
                $vocabObjects[] = $this->model->getVocabulary($vocid);
358
            }
359
        }
360
        $parameters->setVocabularies($vocabObjects);
361
362
        try {
363
            $countAndResults = $this->model->searchConceptsAndInfo($parameters);
364
        } catch (Exception $e) {
365
            header("HTTP/1.0 404 Not Found");
366
            if ($this->model->getConfig()->getLogCaughtExceptions()) {
367
                error_log('Caught exception: ' . $e->getMessage());
368
            }
369
            $this->invokeGenericErrorPage($request, $e->getMessage());
370
            return;
371
        }
372
        $counts = $countAndResults['count'];
373
        $searchResults = $countAndResults['results'];
374
        $vocabList = $this->model->getVocabularyList();
375
        $sortedVocabs = $this->model->getVocabularyList(false, true);
376
        $langList = $this->model->getLanguages($lang);
377
378
        echo $template->render(
379
            array(
380
                'search_count' => $counts,
381
                'languages' => $this->languages,
382
                'search_results' => $searchResults,
383
                'rest' => $parameters->getOffset()>0,
384
                'global_search' => true,
385
                'term' => $request->getQueryParam('q'),
386
                'lang_list' => $langList,
387
                'vocabs' => str_replace(' ', '+', $vocabs),
388
                'vocab_list' => $vocabList,
389
                'sorted_vocabs' => $sortedVocabs,
390
                'request' => $request,
391
                'parameters' => $parameters
392
            ));
393
    }
394
395
    /**
396
     * Invokes the search for a single vocabs concepts.
397
     */
398
    public function invokeVocabularySearch($request)
399
    {
400
        $template = $this->twig->loadTemplate('vocab-search-listing.twig');
401
        $this->setLanguageProperties($request->getLang());
402
        $vocab = $request->getVocab();
403
        try {
404
            $vocabTypes = $this->model->getTypes($request->getVocabid(), $request->getLang());
405
        } catch (Exception $e) {
406
            header("HTTP/1.0 404 Not Found");
407
            if ($this->model->getConfig()->getLogCaughtExceptions()) {
408
                error_log('Caught exception: ' . $e->getMessage());
409
            }
410
411
            echo $template->render(
412
                array(
413
                    'languages' => $this->languages,
414
                ));
415
416
            return;
417
        }
418
419
        $langcodes = $vocab->getConfig()->getShowLangCodes();
420
        $parameters = new ConceptSearchParameters($request, $this->model->getConfig());
421
422
        try {
423
            $countAndResults = $this->model->searchConceptsAndInfo($parameters);
424
            $counts = $countAndResults['count'];
425
            $searchResults = $countAndResults['results'];
426
        } catch (Exception $e) {
427
            header("HTTP/1.0 404 Not Found");
428
            if ($this->model->getConfig()->getLogCaughtExceptions()) {
429
                error_log('Caught exception: ' . $e->getMessage());
430
            }
431
432
            echo $template->render(
433
                array(
434
                    'languages' => $this->languages,
435
                    'vocab' => $vocab,
436
                    'term' => $request->getQueryParam('q'),
437
                ));
438
            return;
439
        }
440
        echo $template->render(
441
            array(
442
                'languages' => $this->languages,
443
                'vocab' => $vocab,
444
                'search_results' => $searchResults,
445
                'search_count' => $counts,
446
                'rest' => $parameters->getOffset()>0,
447
                'limit_parent' => $parameters->getParentLimit(),
448
                'limit_type' =>  $request->getQueryParam('type') ? explode('+', $request->getQueryParam('type')) : null,
449
                'limit_group' => $parameters->getGroupLimit(),
450
                'limit_scheme' =>  $request->getQueryParam('scheme') ? explode('+', $request->getQueryParam('scheme')) : null,
451
                'group_index' => $vocab->listConceptGroups($request->getContentLang()),
452
                'parameters' => $parameters,
453
                'term' => $request->getQueryParam('q'),
454
                'types' => $vocabTypes,
455
                'explicit_langcodes' => $langcodes,
456
                'request' => $request,
457
            ));
458
    }
459
460
    /**
461
     * Invokes the alphabetical listing for a specific vocabulary.
462
     */
463
    public function invokeAlphabeticalIndex($request)
464
    {
465
        $lang = $request->getLang();
466
        $this->setLanguageProperties($lang);
467
        $template = $this->twig->loadTemplate('alphabetical-index.twig');
468
        $vocab = $request->getVocab();
469
470
        $offset = ($request->getQueryParam('offset') && is_numeric($request->getQueryParam('offset')) && $request->getQueryParam('offset') >= 0) ? $request->getQueryParam('offset') : 0;
471
        if ($request->getQueryParam('limit')) {
472
            $count = $request->getQueryParam('limit');
473
        } else {
474
            $count = ($offset > 0) ? null : 250;
475
        }
476
477
        $contentLang = $request->getContentLang();
478
479
        $allAtOnce = $vocab->getConfig()->getAlphabeticalFull();
480
        if (!$allAtOnce) {
481
            $searchResults = $vocab->searchConceptsAlphabetical($request->getLetter(), $count, $offset, $contentLang);
482
            $letters = $vocab->getAlphabet($contentLang);
483
        } else {
484
            $searchResults = $vocab->searchConceptsAlphabetical('*', null, null, $contentLang);
485
            $letters = null;
486
        }
487
488
        $request->setContentLang($contentLang);
489
490
        echo $template->render(
491
            array(
492
                'languages' => $this->languages,
493
                'vocab' => $vocab,
494
                'alpha_results' => $searchResults,
495
                'letters' => $letters,
496
                'all_letters' => $allAtOnce,
497
                'request' => $request,
498
            ));
499
    }
500
501
    /**
502
     * Invokes the vocabulary group index page template.
503
     * @param boolean $stats set to true to get vocabulary statistics visible.
504
     */
505 View Code Duplication
    public function invokeGroupIndex($request, $stats = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
506
    {
507
        $lang = $request->getLang();
508
        $this->setLanguageProperties($lang);
509
        $template = $this->twig->loadTemplate('group-index.twig');
510
        $vocab = $request->getVocab();
511
512
        echo $template->render(
513
            array(
514
                'languages' => $this->languages,
515
                'stats' => $stats,
516
                'vocab' => $vocab,
517
                'request' => $request,
518
            ));
519
    }
520
521
    /**
522
     * Loads and renders the view containing a specific vocabulary.
523
     */
524
    public function invokeVocabularyHome($request)
525
    {
526
        $lang = $request->getLang();
527
        // set language parameters for gettext
528
        $this->setLanguageProperties($lang);
529
        $vocab = $request->getVocab();
530
531
        $defaultView = $vocab->getConfig()->getDefaultSidebarView();
532
        // load template
533
        if ($defaultView === 'groups') {
534
            $this->invokeGroupIndex($request, true);
535
            return;
536
        }
537
538
        $template = $this->twig->loadTemplate('vocab.twig');
539
540
        echo $template->render(
541
            array(
542
                'languages' => $this->languages,
543
                'vocab' => $vocab,
544
                'search_letter' => 'A',
545
                'active_tab' => $defaultView,
546
                'request' => $request,
547
            ));
548
    }
549
550
    /**
551
     * Invokes a very generic errorpage.
552
     */
553
    public function invokeGenericErrorPage($request, $message = null)
554
    {
555
        $this->setLanguageProperties($request->getLang());
556
        header("HTTP/1.0 404 Not Found");
557
        $template = $this->twig->loadTemplate('error-page.twig');
558
        echo $template->render(
559
            array(
560
                'languages' => $this->languages,
561
                'request' => $request,
562
                'message' => $message,
563
                'requested_page' => filter_input(INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_STRING),
564
            ));
565
    }
566
567
    /**
568
     * Loads and renders the view containing a list of recent changes in the vocabulary.
569
     * @param Request $request
570
     */
571
    public function invokeChangeList($request, $prop='dc:created')
572
    {
573
        // set language parameters for gettext
574
        $this->setLanguageProperties($request->getLang());
575
        $vocab = $request->getVocab();
576
        $offset = ($request->getQueryParam('offset') && is_numeric($request->getQueryParam('offset')) && $request->getQueryParam('offset') >= 0) ? $request->getQueryParam('offset') : 0;
577
        $changeList = $vocab->getChangeList($prop, $request->getContentLang(), $request->getLang(), $offset);
578
        // load template
579
        $template = $this->twig->loadTemplate('changes.twig');
580
581
        // render template
582
        echo $template->render(
583
            array(
584
                'vocab' => $vocab,
585
                'languages' => $this->languages,
586
                'request' => $request,
587
                'changeList' => $changeList)
588
            );
589
    }
590
591
}
592