Passed
Push — main ( 18feea...e6d7db )
by
unknown
03:22
created

Controller   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 249
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 38
eloc 91
c 0
b 0
f 0
dl 0
loc 249
rs 9.36

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 2
A isSecure() 0 7 2
A getBaseHref() 0 3 2
A negotiateFormat() 0 13 4
A guessBaseHref() 0 16 4
A sendHeader() 0 3 1
A getModifiedDate() 0 12 5
A notModified() 0 8 3
A returnError() 0 5 1
A getGitModifiedDate() 0 18 4
A executeGitModifiedDateCommand() 0 9 2
A sendNotModifiedHeader() 0 11 4
A getIfModifiedSince() 0 9 2
A getConfigModifiedDate() 0 8 2
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
    public 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';
0 ignored issues
show
Unused Code introduced by
The assignment to $domain is dead and can be removed.
Loading history...
32
33
        // Build arrays of language information, with 'locale' and 'name' keys
34
        $this->languages = array();
35
        foreach ($this->model->getConfig()->getLanguages() as $langcode => $locale) {
36
            $this->languages[$langcode] = array('locale' => $locale);
37
            $this->model->setLocale($langcode);
38
            $this->languages[$langcode]['name'] = $this->model->getText('in_this_language');
39
            $this->languages[$langcode]['lemma'] = Punic\Language::getName($langcode, $langcode);
40
        }
41
    }
42
43
    /**
44
     * Negotiate a MIME type according to the proposed format, the list of valid
45
     * formats, and an optional proposed format.
46
     * As a side effect, set the HTTP Vary header if a choice was made based on
47
     * the Accept header.
48
     * @param array $choices possible MIME types as strings
49
     * @param string $accept HTTP Accept header value
50
     * @param string $format proposed format
51
     * @return string selected format, or null if negotiation failed
52
     */
53
    protected function negotiateFormat($choices, $accept, $format)
54
    {
55
        if ($format) {
56
            if (!in_array($format, $choices)) {
57
                return null;
58
            }
59
            return $format;
60
        }
61
62
        // if there was no proposed format, negotiate a suitable format
63
        header('Vary: Accept'); // inform caches that a decision was made based on Accept header
64
        $best = $this->negotiator->getBest($accept, $choices);
65
        return ($best !== null) ? $best->getValue() : null;
66
    }
67
68
    private function isSecure()
69
    {
70
        if ($protocol = filter_input(INPUT_SERVER, 'HTTP_X_FORWARDED_PROTO', FILTER_SANITIZE_FULL_SPECIAL_CHARS)) {
0 ignored issues
show
Bug introduced by
The constant FILTER_SANITIZE_FULL_SPECIAL_CHARS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
71
            return \in_array(strtolower($protocol), ['https', 'on', 'ssl', '1'], true);
72
        }
73
74
        return filter_input(INPUT_SERVER, 'HTTPS', FILTER_SANITIZE_FULL_SPECIAL_CHARS) !== null;
75
    }
76
77
    private function guessBaseHref()
78
    {
79
        $script_name = filter_input(INPUT_SERVER, 'SCRIPT_NAME', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
0 ignored issues
show
Bug introduced by
The constant FILTER_SANITIZE_FULL_SPECIAL_CHARS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
80
        $script_filename = filter_input(INPUT_SERVER, 'SCRIPT_FILENAME', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
81
        $script_filename = realpath($script_filename); // resolve any symlinks (see #274)
82
        $script_filename = str_replace("\\", "/", $script_filename); // fixing windows paths with \ (see #309)
83
        $base_dir = __DIR__; // Absolute path to your installation, ex: /var/www/mywebsite
84
        $base_dir = str_replace("\\", "/", $base_dir); // fixing windows paths with \ (see #309)
85
        $doc_root = preg_replace("!{$script_name}$!", '', $script_filename);
86
        $base_url = preg_replace("!^{$doc_root}!", '', $base_dir);
87
        $base_url = str_replace('/src/controller', '/', $base_url);
88
        $protocol = $this->isSecure() ? 'https' : 'http';
89
        $port = filter_input(INPUT_SERVER, 'SERVER_PORT', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
90
        $disp_port = ($port == 80 || $port == 443) ? '' : ":$port";
91
        $domain = filter_input(INPUT_SERVER, 'SERVER_NAME', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
92
        return "$protocol://{$domain}{$disp_port}{$base_url}";
93
    }
94
95
    public function getBaseHref()
96
    {
97
        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...
98
    }
99
100
    /**
101
     * Echos an error message when the request can't be fulfilled.
102
     * @param string $code
103
     * @param string $status
104
     * @param string $message
105
     */
106
    protected function returnError($code, $status, $message)
107
    {
108
        header("HTTP/1.0 $code $status");
109
        header("Content-type: text/plain; charset=utf-8");
110
        echo "$code $status : $message";
111
    }
112
113
    protected function notModified(Modifiable $modifiable = null)
114
    {
115
        $notModified = false;
116
        if ($modifiable !== null && $modifiable->isUseModifiedDate()) {
117
            $modifiedDate = $this->getModifiedDate($modifiable);
118
            $notModified = $this->sendNotModifiedHeader($modifiedDate);
119
        }
120
        return $notModified;
121
    }
122
123
    /**
124
     * Return the modified date.
125
     *
126
     * @param Modifiable $modifiable
127
     * @return DateTime|null
128
     */
129
    protected function getModifiedDate(Modifiable $modifiable = null)
130
    {
131
        $modified = null;
132
        $modifiedDate = $modifiable !== null ? $modifiable->getModifiedDate() : null;
133
        $gitModifiedDate = $this->getGitModifiedDate();
134
        $configModifiedDate = $this->getConfigModifiedDate();
135
136
        // max with an empty list raises an error and returns bool
137
        if ($modifiedDate || $gitModifiedDate || $configModifiedDate) {
0 ignored issues
show
introduced by
$configModifiedDate is of type DateTime, thus it always evaluated to true.
Loading history...
138
            $modified = max($modifiedDate, $gitModifiedDate, $configModifiedDate);
139
        }
140
        return $modified;
141
    }
142
143
    /**
144
     * Return the datetime of the latest commit, or null if git is not available or if the command failed
145
     * to execute.
146
     *
147
     * @see https://stackoverflow.com/a/33986403
148
     * @return DateTime|null
149
     */
150
    protected function getGitModifiedDate()
151
    {
152
        $commitDate = null;
153
        $cache = $this->model->getConfig()->getCache();
154
        $cacheKey = "git:modified_date";
155
        $gitCommand = 'git log -1 --date=iso --pretty=format:%cd';
156
        if ($cache->isAvailable()) {
157
            $commitDate = $cache->fetch($cacheKey);
158
            if (!$commitDate) {
159
                $commitDate = $this->executeGitModifiedDateCommand($gitCommand);
160
                if ($commitDate) {
161
                    $cache->store($cacheKey, $commitDate, static::GIT_MODIFIED_CONFIG_TTL);
162
                }
163
            }
164
        } else {
165
            $commitDate = $this->executeGitModifiedDateCommand($gitCommand);
166
        }
167
        return $commitDate;
168
    }
169
170
    /**
171
     * Execute the git command and return a parsed date time, or null if the command failed.
172
     *
173
     * @param string $gitCommand git command line that returns a formatted date time
174
     * @return DateTime|null
175
     */
176
    protected function executeGitModifiedDateCommand($gitCommand)
177
    {
178
        $commitDate = null;
179
        $commandOutput = @exec($gitCommand);
180
        if ($commandOutput) {
181
            $commitDate = new \DateTime(trim($commandOutput));
182
            $commitDate->setTimezone(new \DateTimeZone('UTC'));
183
        }
184
        return $commitDate;
185
    }
186
187
    /**
188
     * Return the datetime of the modified time of the config file. This value is read in the GlobalConfig
189
     * for every request, so we simply access that value and if not null, we will return a datetime. Otherwise,
190
     * we return a null value.
191
     *
192
     * @see http://php.net/manual/en/function.filemtime.php
193
     * @return DateTime|null
194
     */
195
    protected function getConfigModifiedDate()
196
    {
197
        $dateTime = null;
198
        $configModifiedTime = $this->model->getConfig()->getConfigModifiedTime();
199
        if ($configModifiedTime !== null) {
0 ignored issues
show
introduced by
The condition $configModifiedTime !== null is always true.
Loading history...
200
            $dateTime = (new DateTime())->setTimestamp($configModifiedTime);
201
        }
202
        return $dateTime;
203
    }
204
205
    /**
206
     * If the $modifiedDate is a valid DateTime, and if the $_SERVER variable contains the right info, and
207
     * if the $modifiedDate is not more recent than the latest value in $_SERVER, then this function sets the
208
     * HTTP 304 not modified and returns true..
209
     *
210
     * If the $modifiedDate is still valid, then it sets the Last-Modified header, to be used by the browser for
211
     * subsequent requests, and returns false.
212
     *
213
     * Otherwise, it returns false.
214
     *
215
     * @param DateTime|null $modifiedDate the last modified date to be compared against server's modified since information
216
     * @return bool whether it sent the HTTP 304 not modified headers or not (useful for sending the response without
217
     *              further actions)
218
     */
219
    protected function sendNotModifiedHeader($modifiedDate): bool
220
    {
221
        if ($modifiedDate) {
222
            $ifModifiedSince = $this->getIfModifiedSince();
223
            $this->sendHeader("Last-Modified: " . $modifiedDate->format('D, d M Y H:i:s \G\M\T'));
224
            if ($ifModifiedSince !== null && $ifModifiedSince >= $modifiedDate) {
225
                $this->sendHeader("HTTP/1.0 304 Not Modified");
226
                return true;
227
            }
228
        }
229
        return false;
230
    }
231
232
    /**
233
     * @return DateTime|null a DateTime object if the value exists in the $_SERVER variable, null otherwise
234
     */
235
    protected function getIfModifiedSince()
236
    {
237
        $ifModifiedSince = null;
238
        $ifModSinceHeader = filter_input(INPUT_SERVER, 'HTTP_IF_MODIFIED_SINCE', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
0 ignored issues
show
Bug introduced by
The constant FILTER_SANITIZE_FULL_SPECIAL_CHARS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
239
        if ($ifModSinceHeader) {
240
            // example value set by a browser: "Mon, 11 May 2020 10:46:57 GMT"
241
            $ifModifiedSince = new DateTime($ifModSinceHeader);
242
        }
243
        return $ifModifiedSince;
244
    }
245
246
    /**
247
     * Sends HTTP headers. Simply calls PHP built-in header function. But being
248
     * a function here, it can easily be tested/mocked.
249
     *
250
     * @param $header string header to be sent
251
     */
252
    protected function sendHeader($header)
253
    {
254
        header($header);
255
    }
256
}
257