Passed
Pull Request — master (#1337)
by
unknown
03:36
created

Controller::getBaseHref()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
		// Check if we have an authentication provider configured
50
		// If so; check if the configured provider is actually a class we can use
51
		if ( ( $authProvider = $this->model->getConfig()->getAuthenticationProvider() ) && class_exists( $authProvider ) ) {
52
			$this->invokeAuthenticationLayer( new $authProvider($this) );
53
		}
54
    }
55
56
	/**
57
	 * Invokes the authentication layer
58
	 *
59
	 * @param BaseAuthInterface $provider
60
	 * @return void
61
	 */
62
	private function invokeAuthenticationLayer( BaseAuthInterface $authProvider ) {
63
64
		// Validate the authentication provider's configuration parameters
65
		if ( $authProvider->validate() ) {
66
67
			// We don't have a valid user session; so sign in the user
68
			if ( !$authProvider->isSignedIn() ) {
69
				$authProvider->signIn();
70
71
			// We have a valid session for this user; see if it wants to do anything..
72
			} else if ( isset ( $_GET[ 'auth_do' ] ) ) {
73
				$action = $_GET[ 'auth_do' ];
74
75
				// Signout
76
				if ( $action === 'signout' ) {
77
					$authProvider->signOut();
78
79
				// Retrieve user info
80
				} else if ( $action === 'info' ) {
81
					echo json_encode( $authProvider->getUserAttributes() );
82
					$this->sendHeader('Content-Type: application/json' );
83
					exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
84
				}
85
			}
86
		}
87
	}
88
89
    /**
90
     * Sets the locale language properties from the parameter (used by gettext and some Model classes).
91
     * @param string $lang language parameter eg. 'fi' for Finnish.
92
     */
93
    public function setLanguageProperties($lang)
94
    {
95
        if (array_key_exists($lang, $this->languages)) {
96
            $locale = $this->languages[$lang]['locale'];
97
            putenv("LANGUAGE=$locale");
98
            putenv("LC_ALL=$locale");
99
            setlocale(LC_ALL, $locale);
100
        } else {
101
            trigger_error("Unsupported language '$lang', not setting locale", E_USER_WARNING);
102
        }
103
    }
104
105
    /**
106
     * Negotiate a MIME type according to the proposed format, the list of valid
107
     * formats, and an optional proposed format.
108
     * As a side effect, set the HTTP Vary header if a choice was made based on
109
     * the Accept header.
110
     * @param array $choices possible MIME types as strings
111
     * @param string $accept HTTP Accept header value
112
     * @param string $format proposed format
113
     * @return string selected format, or null if negotiation failed
114
     */
115
    protected function negotiateFormat($choices, $accept, $format)
116
    {
117
        if ($format) {
118
            if (!in_array($format, $choices)) {
119
                return null;
120
            }
121
            return $format;
122
        }
123
124
        // if there was no proposed format, negotiate a suitable format
125
        header('Vary: Accept'); // inform caches that a decision was made based on Accept header
126
        $best = $this->negotiator->getBest($accept, $choices);
127
        return ($best !== null) ? $best->getValue() : null;
128
    }
129
130
    private function isSecure()
131
    {
132
        if ($protocol = filter_input(INPUT_SERVER, 'HTTP_X_FORWARDED_PROTO', FILTER_SANITIZE_STRING)) {
133
            return \in_array(strtolower($protocol), ['https', 'on', 'ssl', '1'], true);
134
        }
135
136
        return filter_input(INPUT_SERVER, 'HTTPS', FILTER_SANITIZE_STRING) !== null;
137
    }
138
139
    private function guessBaseHref()
140
    {
141
        $script_name = filter_input(INPUT_SERVER, 'SCRIPT_NAME', FILTER_SANITIZE_STRING);
142
        $script_filename = filter_input(INPUT_SERVER, 'SCRIPT_FILENAME', FILTER_SANITIZE_STRING);
143
        $script_filename = realpath($script_filename); // resolve any symlinks (see #274)
144
        $script_filename = str_replace("\\", "/", $script_filename); // fixing windows paths with \ (see #309)
145
        $base_dir = __DIR__; // Absolute path to your installation, ex: /var/www/mywebsite
146
        $base_dir = str_replace("\\", "/", $base_dir); // fixing windows paths with \ (see #309)
147
        $doc_root = preg_replace("!{$script_name}$!", '', $script_filename);
148
        $base_url = preg_replace("!^{$doc_root}!", '', $base_dir);
149
        $base_url = str_replace('/controller', '/', $base_url);
150
        $protocol = $this->isSecure() ? 'https' : 'http';
151
        $port = filter_input(INPUT_SERVER, 'SERVER_PORT', FILTER_SANITIZE_STRING);
152
        $disp_port = ($port == 80 || $port == 443) ? '' : ":$port";
153
        $domain = filter_input(INPUT_SERVER, 'SERVER_NAME', FILTER_SANITIZE_STRING);
154
        return "$protocol://{$domain}{$disp_port}{$base_url}";
155
    }
156
157
    public function getBaseHref()
158
    {
159
        return ($this->model->getConfig()->getBaseHref() !== null) ? $this->model->getConfig()->getBaseHref() : $this->guessBaseHref();
0 ignored issues
show
introduced by
The condition $this->model->getConfig()->getBaseHref() !== null is always true.
Loading history...
160
    }
161
162
    /**
163
     * Creates Skosmos links from uris.
164
     * @param string $uri
165
     * @param Vocabulary $vocab
166
     * @param string $lang
167
     * @param string $type
168
     * @param string $clang content
169
     * @param string $term
170
     * @throws Exception if the vocabulary ID is not found in configuration
171
     * @return string containing the Skosmos link
172
     */
173
    public function linkUrlFilter($uri, $vocab, $lang, $type = 'page', $clang = null, $term = null) {
174
        // $vocab can either be null, a vocabulary id (string) or a Vocabulary object
175
        if ($vocab === null) {
176
            // target vocabulary is unknown, best bet is to link to the plain URI
177
            return $uri;
178
        } elseif (is_string($vocab)) {
0 ignored issues
show
introduced by
The condition is_string($vocab) is always false.
Loading history...
179
            $vocid = $vocab;
180
            $vocab = $this->model->getVocabulary($vocid);
181
        } else {
182
            $vocid = $vocab->getId();
183
        }
184
185
        $params = array();
186
        if (isset($clang) && $clang !== $lang) {
187
            $params['clang'] = $clang;
188
        }
189
190
        if (isset($term)) {
191
            $params['q'] = $term;
192
        }
193
194
        // case 1: URI within vocabulary namespace: use only local name
195
        $localname = $vocab->getLocalName($uri);
196
        if ($localname !== $uri && $localname === urlencode($localname)) {
197
            // check that the prefix stripping worked, and there are no problematic chars in localname
198
            $paramstr = count($params) > 0 ? '?' . http_build_query($params) : '';
199
            if ($type && $type !== '' && $type !== 'vocab' && !($localname === '' && $type === 'page')) {
200
                return "$vocid/$lang/$type/$localname" . $paramstr;
201
            }
202
203
            return "$vocid/$lang/$localname" . $paramstr;
204
        }
205
206
        // case 2: URI outside vocabulary namespace, or has problematic chars
207
        // pass the full URI as parameter instead
208
        $params['uri'] = $uri;
209
        return "$vocid/$lang/$type/?" . http_build_query($params);
210
    }
211
212
    /**
213
     * Echos an error message when the request can't be fulfilled.
214
     * @param string $code
215
     * @param string $status
216
     * @param string $message
217
     */
218
    protected function returnError($code, $status, $message)
219
    {
220
        header("HTTP/1.0 $code $status");
221
        header("Content-type: text/plain; charset=utf-8");
222
        echo "$code $status : $message";
223
    }
224
225
    protected function notModified(Modifiable $modifiable = null)
226
    {
227
        $notModified = false;
228
        if ($modifiable !== null && $modifiable->isUseModifiedDate()) {
229
            $modifiedDate = $this->getModifiedDate($modifiable);
230
            $notModified = $this->sendNotModifiedHeader($modifiedDate);
231
        }
232
        return $notModified;
233
    }
234
235
    /**
236
     * Return the modified date.
237
     *
238
     * @param Modifiable $modifiable
239
     * @return DateTime|null
240
     */
241
    protected function getModifiedDate(Modifiable $modifiable = null)
242
    {
243
        $modified = null;
244
        $modifiedDate = $modifiable !== null ? $modifiable->getModifiedDate() : null;
245
        $gitModifiedDate = $this->getGitModifiedDate();
246
        $configModifiedDate = $this->getConfigModifiedDate();
247
248
        // max with an empty list raises an error and returns bool
249
        if ($modifiedDate || $gitModifiedDate || $configModifiedDate) {
0 ignored issues
show
introduced by
$configModifiedDate is of type DateTime, thus it always evaluated to true.
Loading history...
250
            $modified = max($modifiedDate, $gitModifiedDate, $configModifiedDate);
251
        }
252
        return $modified;
253
    }
254
255
    /**
256
     * Return the datetime of the latest commit, or null if git is not available or if the command failed
257
     * to execute.
258
     *
259
     * @see https://stackoverflow.com/a/33986403
260
     * @return DateTime|null
261
     */
262
    protected function getGitModifiedDate()
263
    {
264
        $commitDate = null;
265
        $cache = $this->model->getConfig()->getCache();
266
        $cacheKey = "git:modified_date";
267
        $gitCommand = 'git log -1 --date=iso --pretty=format:%cd';
268
        if ($cache->isAvailable()) {
269
            $commitDate = $cache->fetch($cacheKey);
270
            if (!$commitDate) {
271
                $commitDate = $this->executeGitModifiedDateCommand($gitCommand);
272
                if ($commitDate) {
273
                    $cache->store($cacheKey, $commitDate, static::GIT_MODIFIED_CONFIG_TTL);
274
                }
275
            }
276
        } else {
277
            $commitDate = $this->executeGitModifiedDateCommand($gitCommand);
278
        }
279
        return $commitDate;
280
    }
281
282
    /**
283
     * Execute the git command and return a parsed date time, or null if the command failed.
284
     *
285
     * @param string $gitCommand git command line that returns a formatted date time
286
     * @return DateTime|null
287
     */
288
    protected function executeGitModifiedDateCommand($gitCommand)
289
    {
290
        $commitDate = null;
291
        $commandOutput = @exec($gitCommand);
292
        if ($commandOutput) {
293
            $commitDate = new \DateTime(trim($commandOutput));
294
            $commitDate->setTimezone(new \DateTimeZone('UTC'));
295
        }
296
        return $commitDate;
297
    }
298
299
    /**
300
     * Return the datetime of the modified time of the config file. This value is read in the GlobalConfig
301
     * for every request, so we simply access that value and if not null, we will return a datetime. Otherwise,
302
     * we return a null value.
303
     *
304
     * @see http://php.net/manual/en/function.filemtime.php
305
     * @return DateTime|null
306
     */
307
    protected function getConfigModifiedDate()
308
    {
309
        $dateTime = null;
310
        $configModifiedTime = $this->model->getConfig()->getConfigModifiedTime();
311
        if ($configModifiedTime !== null) {
0 ignored issues
show
introduced by
The condition $configModifiedTime !== null is always true.
Loading history...
312
            $dateTime = (new DateTime())->setTimestamp($configModifiedTime);
313
        }
314
        return $dateTime;
315
    }
316
317
    /**
318
     * If the $modifiedDate is a valid DateTime, and if the $_SERVER variable contains the right info, and
319
     * if the $modifiedDate is not more recent than the latest value in $_SERVER, then this function sets the
320
     * HTTP 304 not modified and returns true..
321
     *
322
     * If the $modifiedDate is still valid, then it sets the Last-Modified header, to be used by the browser for
323
     * subsequent requests, and returns false.
324
     *
325
     * Otherwise, it returns false.
326
     *
327
     * @param DateTime|null $modifiedDate the last modified date to be compared against server's modified since information
328
     * @return bool whether it sent the HTTP 304 not modified headers or not (useful for sending the response without
329
     *              further actions)
330
     */
331
    protected function sendNotModifiedHeader($modifiedDate): bool
332
    {
333
        if ($modifiedDate) {
334
            $ifModifiedSince = $this->getIfModifiedSince();
335
            $this->sendHeader("Last-Modified: " . $modifiedDate->format('D, d M Y H:i:s \G\M\T'));
336
            if ($ifModifiedSince !== null && $ifModifiedSince >= $modifiedDate) {
337
                $this->sendHeader("HTTP/1.0 304 Not Modified");
338
                return true;
339
            }
340
        }
341
        return false;
342
    }
343
344
    /**
345
     * @return DateTime|null a DateTime object if the value exists in the $_SERVER variable, null otherwise
346
     */
347
    protected function getIfModifiedSince()
348
    {
349
        $ifModifiedSince = null;
350
        $ifModSinceHeader = filter_input(INPUT_SERVER, 'HTTP_IF_MODIFIED_SINCE', FILTER_SANITIZE_STRING);
351
        if ($ifModSinceHeader) {
352
            // example value set by a browser: "Mon, 11 May 2020 10:46:57 GMT"
353
            $ifModifiedSince = new DateTime($ifModSinceHeader);
354
        }
355
        return $ifModifiedSince;
356
    }
357
358
    /**
359
     * Sends HTTP headers. Simply calls PHP built-in header function. But being
360
     * a function here, it can easily be tested/mocked.
361
     *
362
     * @param $header string header to be sent
363
     */
364
    protected function sendHeader($header)
365
    {
366
        header($header);
367
    }
368
}
369