1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Shared functionality for token-based authentication of potentially dangerous URLs or query |
5
|
|
|
* string parameters |
6
|
|
|
* |
7
|
|
|
* @internal This class is designed specifically for use pre-startup and may change without warning |
8
|
|
|
*/ |
9
|
|
|
abstract class AbstractConfirmationToken { |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* The validated and checked token for this parameter |
13
|
|
|
* |
14
|
|
|
* @var string|null A string value, or null if either not provided or invalid |
15
|
|
|
*/ |
16
|
|
|
protected $token = null; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* What to use instead of BASE_URL. Must not contain protocol or host. |
20
|
|
|
* |
21
|
|
|
* @var string |
22
|
|
|
*/ |
23
|
|
|
public static $alternateBaseURL = null; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Given a list of token names, suppress all tokens that have not been validated, and |
27
|
|
|
* return the non-validated token with the highest priority |
28
|
|
|
* |
29
|
|
|
* @param array $keys List of token keys in ascending priority (low to high) |
30
|
|
|
* @return static The token container for the unvalidated $key given with the highest priority |
31
|
|
|
*/ |
32
|
|
|
public static function prepare_tokens($keys) { |
33
|
|
|
$target = null; |
34
|
|
|
foreach ($keys as $key) { |
35
|
|
|
$token = new static($key); |
|
|
|
|
36
|
|
|
// Validate this token |
37
|
|
|
if ($token->reloadRequired()) { |
38
|
|
|
$token->suppress(); |
39
|
|
|
$target = $token; |
40
|
|
|
} |
41
|
|
|
} |
42
|
|
|
return $target; |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Generate a local filesystem path to store a token |
47
|
|
|
* |
48
|
|
|
* @param $token |
49
|
|
|
* @return string |
50
|
|
|
*/ |
51
|
|
|
protected function pathForToken($token) { |
52
|
|
|
return TEMP_FOLDER . DIRECTORY_SEPARATOR . 'token_' . preg_replace('/[^a-z0-9]+/', '', $token); |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Generate a new random token and store it |
57
|
|
|
* |
58
|
|
|
* @return string Token name |
59
|
|
|
*/ |
60
|
|
|
protected function genToken() { |
61
|
|
|
// Generate a new random token (as random as possible) |
62
|
|
|
require_once(dirname(dirname(dirname(__FILE__))).'/security/RandomGenerator.php'); |
63
|
|
|
|
64
|
|
|
$rg = new RandomGenerator(); |
65
|
|
|
$token = $rg->randomToken('md5'); |
66
|
|
|
|
67
|
|
|
// Store a file in the session save path (safer than /tmp, as open_basedir might limit that) |
68
|
|
|
file_put_contents($this->pathForToken($token), $token); |
69
|
|
|
|
70
|
|
|
return $token; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Is the necessary token provided for this parameter? |
75
|
|
|
* A value must be provided for the token |
76
|
|
|
* |
77
|
|
|
* @return bool |
78
|
|
|
*/ |
79
|
|
|
public function tokenProvided() { |
80
|
|
|
return !empty($this->token); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Validate a token |
85
|
|
|
* |
86
|
|
|
* @param string $token |
87
|
|
|
* @return boolean True if the token is valid |
88
|
|
|
*/ |
89
|
|
|
protected function checkToken($token) { |
90
|
|
|
if (!$token) { |
91
|
|
|
return false; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
$file = $this->pathForToken($token); |
95
|
|
|
$content = null; |
96
|
|
|
|
97
|
|
|
if (file_exists($file)) { |
98
|
|
|
$content = file_get_contents($file); |
99
|
|
|
unlink($file); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
return $content === $token; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Get redirect url, excluding querystring |
107
|
|
|
* |
108
|
|
|
* @return string |
109
|
|
|
*/ |
110
|
|
|
protected function currentAbsoluteURL() { |
|
|
|
|
111
|
|
|
global $url; |
|
|
|
|
112
|
|
|
|
113
|
|
|
// Preserve BC - this has been moved from ParameterConfirmationToken |
114
|
|
|
require_once(dirname(__FILE__).'/ParameterConfirmationToken.php'); |
115
|
|
|
if (isset(ParameterConfirmationToken::$alternateBaseURL)) { |
116
|
|
|
self::$alternateBaseURL = ParameterConfirmationToken::$alternateBaseURL; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
// Are we http or https? Replicates Director::is_https() without its dependencies/ |
120
|
|
|
$proto = 'http'; |
121
|
|
|
// See https://en.wikipedia.org/wiki/List_of_HTTP_header_fields |
122
|
|
|
// See https://support.microsoft.com/?kbID=307347 |
123
|
|
|
$headerOverride = false; |
124
|
|
|
if(TRUSTED_PROXY) { |
125
|
|
|
$headers = (defined('SS_TRUSTED_PROXY_PROTOCOL_HEADER')) ? array(SS_TRUSTED_PROXY_PROTOCOL_HEADER) : null; |
126
|
|
|
if(!$headers) { |
127
|
|
|
// Backwards compatible defaults |
128
|
|
|
$headers = array('HTTP_X_FORWARDED_PROTO', 'HTTP_X_FORWARDED_PROTOCOL', 'HTTP_FRONT_END_HTTPS'); |
129
|
|
|
} |
130
|
|
|
foreach($headers as $header) { |
131
|
|
|
$headerCompareVal = ($header === 'HTTP_FRONT_END_HTTPS' ? 'on' : 'https'); |
132
|
|
|
if(!empty($_SERVER[$header]) && strtolower($_SERVER[$header]) == $headerCompareVal) { |
133
|
|
|
$headerOverride = true; |
134
|
|
|
break; |
135
|
|
|
} |
136
|
|
|
} |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
if($headerOverride) { |
140
|
|
|
$proto = 'https'; |
141
|
|
|
} else if((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) { |
142
|
|
|
$proto = 'https'; |
143
|
|
|
} else if(isset($_SERVER['SSL'])) { |
144
|
|
|
$proto = 'https'; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
$parts = array_filter(array( |
148
|
|
|
// What's our host |
149
|
|
|
$_SERVER['HTTP_HOST'], |
150
|
|
|
// SilverStripe base |
151
|
|
|
self::$alternateBaseURL !== null ? self::$alternateBaseURL : BASE_URL, |
152
|
|
|
// And URL including base script (eg: if it's index.php/page/url/) |
153
|
|
|
(defined('BASE_SCRIPT_URL') ? '/' . BASE_SCRIPT_URL : '') . $url, |
154
|
|
|
)); |
155
|
|
|
|
156
|
|
|
// Join together with protocol into our current absolute URL, avoiding duplicated "/" characters |
157
|
|
|
return "$proto://" . preg_replace('#/{2,}#', '/', implode('/', $parts)); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Forces a reload of the request with the token included |
162
|
|
|
*/ |
163
|
|
|
public function reloadWithToken() { |
164
|
|
|
require_once(dirname(dirname(__FILE__)).'/Convert.php'); |
165
|
|
|
$location = $this->redirectURL(); |
166
|
|
|
$locationJS = Convert::raw2js($location); |
167
|
|
|
$locationATT = Convert::raw2att($location); |
168
|
|
|
|
169
|
|
|
if (headers_sent()) { |
170
|
|
|
echo " |
171
|
|
|
<script>location.href='{$locationJS}';</script> |
172
|
|
|
<noscript><meta http-equiv='refresh' content='0; url={$locationATT}'></noscript> |
173
|
|
|
You are being redirected. If you are not redirected soon, <a href='{$locationATT}'>click here to continue</a> |
174
|
|
|
"; |
175
|
|
|
} else { |
176
|
|
|
header("location: {$location}", true, 302); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
die; |
|
|
|
|
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Is this parameter requested without a valid token? |
184
|
|
|
* |
185
|
|
|
* @return bool True if the parameter is given without a valid token |
186
|
|
|
*/ |
187
|
|
|
abstract public function reloadRequired(); |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Suppress the current parameter for the duration of this request |
191
|
|
|
*/ |
192
|
|
|
abstract public function suppress(); |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Determine the querystring parameters to include |
196
|
|
|
* |
197
|
|
|
* @param bool $includeToken Include the token value? |
198
|
|
|
* @return array List of querystring parameters, possibly including token parameter |
199
|
|
|
*/ |
200
|
|
|
abstract public function params($includeToken = true); |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* @return string |
204
|
|
|
*/ |
205
|
|
|
abstract public function getRedirectUrlBase(); |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* @return array |
209
|
|
|
*/ |
210
|
|
|
abstract public function getRedirectUrlParams(); |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Get redirection URL |
214
|
|
|
* |
215
|
|
|
* @return string |
216
|
|
|
*/ |
217
|
|
|
abstract protected function redirectURL(); |
218
|
|
|
} |
219
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.