Issues (177)

controller/Controller.php (7 issues)

1
<?php
2
3
/**
4
 * Handles all the requests from the user and changes the view accordingly.
5
 */
6
class Controller
7
{
8
    /**
9
     * How long to store retrieved disk configuration for HTTP 304 header
10
     * from git information.
11
     */
12
    const GIT_MODIFIED_CONFIG_TTL = 600; // 10 minutes
13
14
    /**
15
     * The controller has to know the model to access the data stored there.
16
     * @var Model $model contains the Model object.
17
     */
18
    public $model;
19
20
    protected $negotiator;
21
22
    protected $languages;
23
24
    /**
25
     * Initializes the Model object.
26
     */
27
    public function __construct($model)
28
    {
29
        $this->model = $model;
30
        $this->negotiator = new \Negotiation\Negotiator();
31
        $domain = 'skosmos';
32
33
        // Specify the location of the translation tables
34
        bindtextdomain($domain, 'resource/translations');
35
        bind_textdomain_codeset($domain, 'UTF-8');
36
37
        // Choose domain for translations
38
        textdomain($domain);
39
40
        // Build arrays of language information, with 'locale' and 'name' keys
41
        $this->languages = array();
42
        foreach ($this->model->getConfig()->getLanguages() as $langcode => $locale) {
43
            $this->languages[$langcode] = array('locale' => $locale);
44
            $this->setLanguageProperties($langcode);
45
            $this->languages[$langcode]['name'] = gettext('in_this_language');
46
            $this->languages[$langcode]['lemma'] = Punic\Language::getName($langcode, $langcode);
47
        }
48
    }
49
50
    /**
51
     * Sets the locale language properties from the parameter (used by gettext and some Model classes).
52
     * @param string $lang language parameter eg. 'fi' for Finnish.
53
     */
54
    public function setLanguageProperties($lang)
55
    {
56
        if (array_key_exists($lang, $this->languages)) {
57
            $locale = $this->languages[$lang]['locale'];
58
            putenv("LANGUAGE=$locale");
59
            putenv("LC_ALL=$locale");
60
            setlocale(LC_ALL, $locale);
61
        } else {
62
            trigger_error("Unsupported language '$lang', not setting locale", E_USER_WARNING);
63
        }
64
    }
65
66
    /**
67
     * Negotiate a MIME type according to the proposed format, the list of valid
68
     * formats, and an optional proposed format.
69
     * As a side effect, set the HTTP Vary header if a choice was made based on
70
     * the Accept header.
71
     * @param array $choices possible MIME types as strings
72
     * @param string $accept HTTP Accept header value
73
     * @param string $format proposed format
74
     * @return string selected format, or null if negotiation failed
75
     */
76
    protected function negotiateFormat($choices, $accept, $format)
77
    {
78
        if ($format) {
79
            if (!in_array($format, $choices)) {
80
                return null;
81
            }
82
            return $format;
83
        }
84
85
        // if there was no proposed format, negotiate a suitable format
86
        header('Vary: Accept'); // inform caches that a decision was made based on Accept header
87
        $best = $this->negotiator->getBest($accept, $choices);
88
        return ($best !== null) ? $best->getValue() : null;
89
    }
90
91
    private function isSecure()
92
    {
93
        if ($protocol = filter_input(INPUT_SERVER, 'HTTP_X_FORWARDED_PROTO', FILTER_SANITIZE_FULL_SPECIAL_CHARS)) {
0 ignored issues
show
The constant FILTER_SANITIZE_FULL_SPECIAL_CHARS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
94
            return \in_array(strtolower($protocol), ['https', 'on', 'ssl', '1'], true);
95
        }
96
97
        return filter_input(INPUT_SERVER, 'HTTPS', FILTER_SANITIZE_FULL_SPECIAL_CHARS) !== null;
98
    }
99
100
    private function guessBaseHref()
101
    {
102
        $script_name = filter_input(INPUT_SERVER, 'SCRIPT_NAME', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
0 ignored issues
show
The constant FILTER_SANITIZE_FULL_SPECIAL_CHARS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
103
        $script_filename = filter_input(INPUT_SERVER, 'SCRIPT_FILENAME', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
104
        $script_filename = realpath($script_filename); // resolve any symlinks (see #274)
105
        $script_filename = str_replace("\\", "/", $script_filename); // fixing windows paths with \ (see #309)
106
        $base_dir = __DIR__; // Absolute path to your installation, ex: /var/www/mywebsite
107
        $base_dir = str_replace("\\", "/", $base_dir); // fixing windows paths with \ (see #309)
108
        $doc_root = preg_replace("!{$script_name}$!", '', $script_filename);
109
        $base_url = preg_replace("!^{$doc_root}!", '', $base_dir);
110
        $base_url = str_replace('/controller', '/', $base_url);
111
        $protocol = $this->isSecure() ? 'https' : 'http';
112
        $port = filter_input(INPUT_SERVER, 'SERVER_PORT', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
113
        $disp_port = ($port == 80 || $port == 443) ? '' : ":$port";
114
        $domain = filter_input(INPUT_SERVER, 'SERVER_NAME', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
115
        return "$protocol://{$domain}{$disp_port}{$base_url}";
116
    }
117
118
    public function getBaseHref()
119
    {
120
        return ($this->model->getConfig()->getBaseHref() !== null) ? $this->model->getConfig()->getBaseHref() : $this->guessBaseHref();
0 ignored issues
show
The condition $this->model->getConfig()->getBaseHref() !== null is always true.
Loading history...
121
    }
122
123
    /**
124
     * Creates Skosmos links from uris.
125
     * @param string $uri
126
     * @param Vocabulary $vocab
127
     * @param string $lang
128
     * @param string $type
129
     * @param string $clang content
130
     * @param string $term
131
     * @throws Exception if the vocabulary ID is not found in configuration
132
     * @return string containing the Skosmos link
133
     */
134
    public function linkUrlFilter($uri, $vocab, $lang, $type = 'page', $clang = null, $term = null) {
135
        // $vocab can either be null, a vocabulary id (string) or a Vocabulary object
136
        if ($vocab === null) {
137
            // target vocabulary is unknown, best bet is to link to the plain URI
138
            return $uri;
139
        } elseif (is_string($vocab)) {
0 ignored issues
show
The condition is_string($vocab) is always false.
Loading history...
140
            $vocid = $vocab;
141
            $vocab = $this->model->getVocabulary($vocid);
142
        } else {
143
            $vocid = $vocab->getId();
144
        }
145
146
        $params = array();
147
        if (isset($clang) && $clang !== $lang) {
148
            $params['clang'] = $clang;
149
        }
150
151
        if (isset($term)) {
152
            $params['q'] = $term;
153
        }
154
155
        // case 1: URI within vocabulary namespace: use only local name
156
        $localname = $vocab->getLocalName($uri);
157
        if ($localname !== $uri && $localname === urlencode($localname)) {
158
            // check that the prefix stripping worked, and there are no problematic chars in localname
159
            $paramstr = count($params) > 0 ? '?' . http_build_query($params) : '';
160
            if ($type && $type !== '' && $type !== 'vocab' && !($localname === '' && $type === 'page')) {
161
                return "$vocid/$lang/$type/$localname" . $paramstr;
162
            }
163
164
            return "$vocid/$lang/$localname" . $paramstr;
165
        }
166
167
        // case 2: URI outside vocabulary namespace, or has problematic chars
168
        // pass the full URI as parameter instead
169
        $params['uri'] = $uri;
170
        return "$vocid/$lang/$type/?" . http_build_query($params);
171
    }
172
173
    /**
174
     * Echos an error message when the request can't be fulfilled.
175
     * @param string $code
176
     * @param string $status
177
     * @param string $message
178
     */
179
    protected function returnError($code, $status, $message)
180
    {
181
        header("HTTP/1.0 $code $status");
182
        header("Content-type: text/plain; charset=utf-8");
183
        echo "$code $status : $message";
184
    }
185
186
    protected function notModified(Modifiable $modifiable = null)
187
    {
188
        $notModified = false;
189
        if ($modifiable !== null && $modifiable->isUseModifiedDate()) {
190
            $modifiedDate = $this->getModifiedDate($modifiable);
191
            $notModified = $this->sendNotModifiedHeader($modifiedDate);
192
        }
193
        return $notModified;
194
    }
195
196
    /**
197
     * Return the modified date.
198
     *
199
     * @param Modifiable $modifiable
200
     * @return DateTime|null
201
     */
202
    protected function getModifiedDate(Modifiable $modifiable = null)
203
    {
204
        $modified = null;
205
        $modifiedDate = $modifiable !== null ? $modifiable->getModifiedDate() : null;
206
        $gitModifiedDate = $this->getGitModifiedDate();
207
        $configModifiedDate = $this->getConfigModifiedDate();
208
209
        // max with an empty list raises an error and returns bool
210
        if ($modifiedDate || $gitModifiedDate || $configModifiedDate) {
0 ignored issues
show
$configModifiedDate is of type DateTime, thus it always evaluated to true.
Loading history...
211
            $modified = max($modifiedDate, $gitModifiedDate, $configModifiedDate);
212
        }
213
        return $modified;
214
    }
215
216
    /**
217
     * Return the datetime of the latest commit, or null if git is not available or if the command failed
218
     * to execute.
219
     *
220
     * @see https://stackoverflow.com/a/33986403
221
     * @return DateTime|null
222
     */
223
    protected function getGitModifiedDate()
224
    {
225
        $commitDate = null;
226
        $cache = $this->model->getConfig()->getCache();
227
        $cacheKey = "git:modified_date";
228
        $gitCommand = 'git log -1 --date=iso --pretty=format:%cd';
229
        if ($cache->isAvailable()) {
230
            $commitDate = $cache->fetch($cacheKey);
231
            if (!$commitDate) {
232
                $commitDate = $this->executeGitModifiedDateCommand($gitCommand);
233
                if ($commitDate) {
234
                    $cache->store($cacheKey, $commitDate, static::GIT_MODIFIED_CONFIG_TTL);
235
                }
236
            }
237
        } else {
238
            $commitDate = $this->executeGitModifiedDateCommand($gitCommand);
239
        }
240
        return $commitDate;
241
    }
242
243
    /**
244
     * Execute the git command and return a parsed date time, or null if the command failed.
245
     *
246
     * @param string $gitCommand git command line that returns a formatted date time
247
     * @return DateTime|null
248
     */
249
    protected function executeGitModifiedDateCommand($gitCommand)
250
    {
251
        $commitDate = null;
252
        $commandOutput = @exec($gitCommand);
253
        if ($commandOutput) {
254
            $commitDate = new \DateTime(trim($commandOutput));
255
            $commitDate->setTimezone(new \DateTimeZone('UTC'));
256
        }
257
        return $commitDate;
258
    }
259
260
    /**
261
     * Return the datetime of the modified time of the config file. This value is read in the GlobalConfig
262
     * for every request, so we simply access that value and if not null, we will return a datetime. Otherwise,
263
     * we return a null value.
264
     *
265
     * @see http://php.net/manual/en/function.filemtime.php
266
     * @return DateTime|null
267
     */
268
    protected function getConfigModifiedDate()
269
    {
270
        $dateTime = null;
271
        $configModifiedTime = $this->model->getConfig()->getConfigModifiedTime();
272
        if ($configModifiedTime !== null) {
0 ignored issues
show
The condition $configModifiedTime !== null is always true.
Loading history...
273
            $dateTime = (new DateTime())->setTimestamp($configModifiedTime);
274
        }
275
        return $dateTime;
276
    }
277
278
    /**
279
     * If the $modifiedDate is a valid DateTime, and if the $_SERVER variable contains the right info, and
280
     * if the $modifiedDate is not more recent than the latest value in $_SERVER, then this function sets the
281
     * HTTP 304 not modified and returns true..
282
     *
283
     * If the $modifiedDate is still valid, then it sets the Last-Modified header, to be used by the browser for
284
     * subsequent requests, and returns false.
285
     *
286
     * Otherwise, it returns false.
287
     *
288
     * @param DateTime|null $modifiedDate the last modified date to be compared against server's modified since information
289
     * @return bool whether it sent the HTTP 304 not modified headers or not (useful for sending the response without
290
     *              further actions)
291
     */
292
    protected function sendNotModifiedHeader($modifiedDate): bool
293
    {
294
        if ($modifiedDate) {
295
            $ifModifiedSince = $this->getIfModifiedSince();
296
            $this->sendHeader("Last-Modified: " . $modifiedDate->format('D, d M Y H:i:s \G\M\T'));
297
            if ($ifModifiedSince !== null && $ifModifiedSince >= $modifiedDate) {
298
                $this->sendHeader("HTTP/1.0 304 Not Modified");
299
                return true;
300
            }
301
        }
302
        return false;
303
    }
304
305
    /**
306
     * @return DateTime|null a DateTime object if the value exists in the $_SERVER variable, null otherwise
307
     */
308
    protected function getIfModifiedSince()
309
    {
310
        $ifModifiedSince = null;
311
        $ifModSinceHeader = filter_input(INPUT_SERVER, 'HTTP_IF_MODIFIED_SINCE', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
0 ignored issues
show
The constant FILTER_SANITIZE_FULL_SPECIAL_CHARS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
312
        if ($ifModSinceHeader) {
313
            // example value set by a browser: "Mon, 11 May 2020 10:46:57 GMT"
314
            $ifModifiedSince = new DateTime($ifModSinceHeader);
315
        }
316
        return $ifModifiedSince;
317
    }
318
319
    /**
320
     * Sends HTTP headers. Simply calls PHP built-in header function. But being
321
     * a function here, it can easily be tested/mocked.
322
     *
323
     * @param $header string header to be sent
324
     */
325
    protected function sendHeader($header)
326
    {
327
        header($header);
328
    }
329
}
330