Headers   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 296
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 71
dl 0
loc 296
rs 9.84
c 0
b 0
f 0
wmc 32

14 Methods

Rating   Name   Duplication   Size   Complexity  
A httpCode() 0 5 1
A headerSpecial() 0 5 1
A standardizeHeaderName() 0 7 1
A header() 0 8 1
B redirect() 0 30 7
A removeHeader() 0 17 2
A __construct() 0 3 1
A send() 0 4 1
A setDownloadFileNameHeader() 0 16 3
A contentType() 0 10 2
A setAttachmentFileParams() 0 17 3
A sendHeaders() 0 20 6
A charset() 0 5 1
A instance() 0 8 2
1
<?php
2
3
/**
4
 *
5
 * @package   ElkArte Forum
6
 * @copyright ElkArte Forum contributors
7
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
8
 *
9
 * @version 2.0 dev
10
 *
11
 */
12
13
namespace ElkArte\Http;
14
15
use ElkArte\Helper\HttpReq;
16
17
/**
18
 * Class Headers
19
 *
20
 * Handles HTTP headers for the application.
21
 */
22
class Headers
23
{
24
	/** @var string Default content type */
25
	protected $contentType = 'text/html';
26
27
	/** @var string Default character set */
28
	protected $charset = 'UTF-8';
29
30
	/** @var int Default HTTP return code */
31
	protected $httpCode = 200;
32
33
	/** @var array Holds any normal headers collected */
34
	protected $headers = [];
35
36
	/** @var array Holds any special (raw) headers collected */
37
	protected $specialHeaders = [];
38
39
	/** @var HttpReq|null */
40
	protected $req;
41
42
	/** @var Headers Sole private \ElkArte\Headers instance */
43
	private static $instance;
44
45
	/**
46
	 * Headers constructor.
47
	 */
48
	public function __construct()
49
	{
50
		$this->req = HttpReq::instance();
51
	}
52
53
	/**
54
	 * Sets a redirect location header
55
	 *
56
	 * What it does:
57
	 *
58
	 * - Adds in scripturl if needed
59
	 * - Calls call_integration_hook integrate_redirect before headers are sent
60
	 *
61
	 * @event integrate_redirect called before headers are sent
62
	 * @param string $setLocation = '' The URL to redirect to
63
	 * @param int $httpCode defaults to 200
64
	 */
65
	public function redirect($setLocation = '', $httpCode = null)
66
	{
67
		global $scripturl;
68
69
		// Convert relative URL to site url
70
		if (preg_match('~^(ftp|http)[s]?://~', $setLocation) === 0)
71
		{
72
			$setLocation = $scripturl . ($setLocation !== '' ? '?' . $setLocation : '');
73
		}
74
75
		// Put the session ID in.
76
		if (empty($_COOKIE) && defined('SID') && !empty(SID))
77
		{
78
			$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
79
		}
80
		// Keep that debug in their for template debugging!
81
		elseif (isset($this->req->debug))
0 ignored issues
show
Bug Best Practice introduced by
The property debug does not exist on ElkArte\Helper\HttpReq. Since you implemented __get, consider adding a @property annotation.
Loading history...
82
		{
83
			$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
84
		}
85
86
		// Maybe integrations want to change where we are heading?
87
		call_integration_hook('integrate_redirect', [&$setLocation]);
88
89
		// Set the location header and code
90
		$this
91
			->header('Location', $setLocation)
92
			->httpCode = $httpCode ?? 302;
93
94
		return $this;
95
	}
96
97
	/**
98
	 * Run maintenance function and then send the all collected headers
99
	 */
100
	public function send()
101
	{
102
		handleMaintenance();
103
		$this->sendHeaders();
104
	}
105
106
	/**
107
	 * Normally used for a header that starts with the string "HTTP/" (case is not significant),
108
	 * which will be used to figure out the HTTP status code to send.  You could stuff in any
109
	 * complete header you wanted as the value is used directly as header($value)
110
	 *
111
	 * @param $value
112
	 * @return $this
113
	 */
114
	public function headerSpecial($value)
115
	{
116
		$this->specialHeaders[] = $value;
117
118
		return $this;
119
	}
120
121
	/**
122
	 * Adds headers to the header array for eventual output to browser
123
	 *
124
	 * @param string $name Name of the header
125
	 * @param string|null $value Value for the header
126
	 *
127
	 * @return $this
128
	 */
129
	public function header($name, $value = null)
130
	{
131
		$name = $this->standardizeHeaderName($name);
132
133
		// Add new or overwrite
134
		$this->headers[$name] = $value;
135
136
		return $this;
137
	}
138
139
	/**
140
	 * Converts / Fixes header names to a standard format, so we have consistent search replace etc.
141
	 *
142
	 * @param string $name
143
	 * @return string
144
	 */
145
	protected function standardizeHeaderName($name)
146
	{
147
		// Combine spaces and Convert dashes "clear    Site-Data" => "clear Site Data"
148
		$name = preg_replace('~\s+~', ' ', str_replace('-', ' ', trim($name)));
149
150
		// Now ucword the header and add back the dash => Clear-Site-Data
151
		return str_replace(' ', '-', ucwords($name));
152
	}
153
154
	/**
155
	 * Set the http header code, like 404, 200, 301, etc.
156
	 * Only output if content type is not empty
157
	 *
158
	 * @param int $httpCode
159
	 * @return $this
160
	 */
161
	public function httpCode($httpCode)
162
	{
163
		$this->httpCode = (int) $httpCode;
164
165
		return $this;
166
	}
167
168
	/**
169
	 * Sets the context type based on if this is an image or not.  Calls
170
	 * setDownloadFileNameHeader to set the proper content disposition.
171
	 *
172
	 * @param string $mime_type
173
	 * @param string $fileName
174
	 * @param string $disposition 'attachment' or 'inline';
175
	 * @return $this
176
	 */
177
	public function setAttachmentFileParams($mime_type, $fileName, $disposition = 'attachment')
178
	{
179
		// If an image, set the content type to the image/type defined in the mime_type
180
		if (!empty($mime_type) && strpos($mime_type, 'image/') === 0)
181
		{
182
			$this->contentType($mime_type, '');
183
		}
184
		// Otherwise, arbitrary binary data
185
		else
186
		{
187
			$this->contentType('application/octet-stream', '');
188
		}
189
190
		// Set the content disposition and name
191
		$this->setDownloadFileNameHeader($fileName, $disposition);
192
193
		return $this;
194
	}
195
196
	/**
197
	 * Set the proper filename header accounting for UTF-8 characters in the name
198
	 *
199
	 * @param string $fileName That would be the name
200
	 * @param string $disposition 'inline' or 'attachment'
201
	 */
202
	private function setDownloadFileNameHeader($fileName, $disposition = false)
203
	{
204
		$type = ($disposition ? 'inline' : 'attachment');
205
206
		$fileName = str_replace('"', '', $fileName);
207
208
		// Send as UTF-8 if the name requires that
209
		$altName = '';
210
		if (preg_match('~[\x80-\xFF]~', $fileName))
211
		{
212
			$altName = "; filename*=UTF-8''" . rawurlencode($fileName);
213
		}
214
215
		$this->header('Content-Disposition', $type . '; filename="' . $fileName . '"' . $altName);
216
217
		return $this;
218
	}
219
220
	/**
221
	 * Sets the content type and character set.  Replaces an existing one if called multiple times
222
	 * so the last call to this method will be what is output.
223
	 *
224
	 * @param string|null $contentType
225
	 * @param string|null $charset
226
	 * @return $this
227
	 */
228
	public function contentType($contentType, $charset = null)
229
	{
230
		$this->contentType = $contentType;
231
232
		if ($charset !== null)
233
		{
234
			$this->charset($charset);
235
		}
236
237
		return $this;
238
	}
239
240
	/**
241
	 * Sets the character set in use, defaults to utf-8
242
	 *
243
	 * @param string $charset
244
	 * @return $this
245
	 */
246
	public function charset($charset)
247
	{
248
		$this->charset = $charset;
249
250
		return $this;
251
	}
252
253
	/**
254
	 * Removes a single header if set or all headers if we need to restart
255
	 * the process, such as during an error or other.
256
	 *
257
	 * @param string $name
258
	 * @return $this
259
	 */
260
	public function removeHeader($name)
261
	{
262
		// Full reset like nothing had been sent
263
		if ($name === 'all')
264
		{
265
			$this->headers = [];
266
			$this->specialHeaders = [];
267
			$this->contentType = '';
268
			$this->charset = 'UTF-8';
269
			$this->httpCode = 200;
270
		}
271
272
		// Or remove a specific header
273
		$name = $this->standardizeHeaderName($name);
274
		unset($this->headers[$name]);
275
276
		return $this;
277
	}
278
279
	/**
280
	 * Send the collection of headers using standard php header() function
281
	 */
282
	public function sendHeaders()
283
	{
284
		if (headers_sent())
285
		{
286
			return;
287
		}
288
289
		foreach ($this->headers as $header => $value)
290
		{
291
			header("$header: $value", true);
292
		}
293
294
		foreach ($this->specialHeaders as $header)
295
		{
296
			header($header, true);
297
		}
298
299
		if ($this->contentType)
300
		{
301
			header('Content-Type: ' . $this->contentType . ($this->charset ? '; charset=' . $this->charset : ''), true, $this->httpCode);
302
		}
303
	}
304
305
	/**
306
	 * Retrieve the sole instance of this class.
307
	 *
308
	 * @return Headers
309
	 */
310
	public static function instance()
311
	{
312
		if (self::$instance === null)
313
		{
314
			self::$instance = new Headers();
315
		}
316
317
		return self::$instance;
318
	}
319
}
320