-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathexception.inc
389 lines (347 loc) · 13.4 KB
/
exception.inc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
<?php
/**
* QQ API: Exception handling
*
* @copyright QQ Trend Ltd, 2012, 2013
* @author Darren Cook <[email protected]>
* @license MIT
*/
namespace QQAPI;
include_once "application.inc";
include_once "logger.inc";
/**
* Base class for our exceptions
*
* All the logic and vars are in this base class: the derived classes are (mostly) just descriptive.
*
* What it adds beyond the basic PHP exception class is:
* * Formatting as text, json, xml, etc.
* * An optional reference to a user object.
* * Logging
* * Backup logging (for when main log destination cannot be opened, etc.)
* * Extra info that can be inserted in messages.
* * Extra message just put in the logs.
* * Some design-for-testing support
*
* NOTE: if you want certain information from requests included, then
* use self::$details. This can be accessed directly, but the better way is to
* call Exception::addToDetails().
*
* @internal It has a number of static vars and functions, as it also handles reporting.
* I suppose we could move all those static vars to either a base class, or a helper
* utility class.
*/
class Exception extends \Exception{
/**
* This is used to translate the messages that can be given to the constructor,
* as well as the couple of internal messages.
*
* If null, the default, then no translations are used. This is fine for English.
* Otherwise set it to an array where the key is the English, and the value is
* the string in the target language.
*
* Note: it can also be used to override the default messages. E.g. for more
* verbose messages assign this:
* array(
* 'System error'=>'System error. Try again later.',
* 'Please authenticate'=>'Please authenticate. If you see this in a browser, then please click reload to authenticate.',
* ),
*
* The keys can be anything, so do not need to be in the base language. E.g. they
* can be error codes:
* array(
* '001'=>'You must give %1$d choices',
* '002'=>'That is an invalid value for email',
* );
* However this tends to make the point where the exception is thrown harder to read. So
* it is best saved for cases where the message is very long. (It is fine to mix real strings
* and error codes!)
*
* @see self::translate()
*
* @internal For simplicitly, documentation assumes the base language is
* English. The code does not require this, as the error-code example shows,
* though the hard-coded strings in this class are in English.
*/
static public $strings=null;
/**
* An optional set of key/value pairs that describe the request.
*
* For instance with a typical web access, we'll put keys in here for
* IP address, username, user agent.
*
* @internal you can make any guarantee on the order that entries will be
* output in. Use ksort(Exception::$details) if you want to be sure
* of the order (e.g. for unit test asserts).
*/
static public $details=array();
/**
* This is where normal errors are written. E.g. ErrorException would go here.
*/
static public $normalErrorLog="logs/normal_error.log";
/**
* This is where the more serious system errors are written.
*
* This is the log we'd be checking and emailing to an admin regularly.
*/
static public $systemErrorLog="logs/system_error.log";
/**
* Extra information about the problem, to only be shown to the system administrator
*
* If a string it is written to the logfile as-is; if not then it is run through print_r(),
* so that arrays and objects get converted to a string format.
*/
public $detailedMessage;
//---------------------------------
/**
*
* @param String $message This is the message that will be shown to the user,
* as well as written to the log.
* It can contain custom information (see below). Remember to use single quotes!
* To have it translated into user's language, assign self::strings to be an array
* based on their language preference.
* @param Array $params Parameters to replace inside $message
* (which should be a sprintf format string if using this array).
* They'll be inserted in order with sprintf's %N$d and %N$s format,
* where N is 1..N, corresponding to the 0..N-1 index in $params.
* (Actually we use vsprintf, not sprintf.)
* IMPORTANT: If using this, remember you must give the $message in single quotes. Or, if you use
* double quotes, you need to escape the dollar signs. I.e. either of these will work:
* throw new ErrorException('Hello %2$s, %1$s',array('People','World'));
* throw new ErrorException("Hello %2\$s, %1\$s",array('People','World'));
* ("Hello World, People" is output for both.)
* @param String $detailedMessage This is extra information, written just to the
* logfile, and not returned to the user.
* NOTE: the logfile will automatically get file, line number, the stack trace,
* as well as all the entries in self::$details. So $detailedMessage does not
* need to be used for any of those.
*/
public function __construct($message,$params=array(),$detailedMessage="") {
parent::__construct(@vsprintf(self::translate($message),$params));
$this->detailedMessage=$detailedMessage;
}
/**
* Call this when we want to output the message to the user (i.e. to tell them
* more exactly what went wrong).
*/
public function reportProblemLogAndExit(){
self::encodeAndOutputMessage($this->message);
$this->logMessage(self::$normalErrorLog);
exit;
}
/**
* @see reportProblemLogAndExit
*
* @internal This exists more for unit testing than anything else!
*/
public function reportProblemLogButDoNotExit(){
self::encodeAndOutputMessage($this->message);
$this->logMessage(self::$normalErrorLog);
}
/**
*/
public function reportSystemErrorLogAndExit(){
self::encodeAndOutputMessage('System error. Try again later.');
$this->logMessage(self::$systemErrorLog);
exit;
}
/**
*
* For escaping, we assume json_encode() escapes all special characters.
* For text format, we also do no escaping at all.
* For XML, we do escaping, and also send the XML version tag.
*
* Text format is the default when $format is unrecognized.
*
* @param String $message The message to show to the user. It should already
* be in the desired language, and have had any parameters already inserted.
*
* @todo Need to output a suitable header? Or will Router already have done that?
* ---> Perhaps we can detect if header already sent?
* ---> When used from commandline we don't want the header sent though...
* ====> It seems most of the time header() won't have been sent. There is a Router function
* to help, but it is current non-static and protected...
*/
protected static function encodeAndOutputMessage($message){
if(substr(Application::$format,0,4)=='json'){
echo json_encode(array('timestamp'=>gmdate("Y-m-d H:i:s").'Z','error'=>$message))."\n";
}
elseif(substr(Application::$format,0,3)=='xml'){
$msg=str_replace(array( "&","<", ">", "\"", "'"),
array("&","<", ">", """, "'"), $message); //Encode for XML
echo '<?xml version="1.0" encoding="UTF-8"?>'."\n";
echo '<response timestamp="'.gmdate("Y-m-d H:i:s").'Z">'.
'<error>'.$msg.'</error>'.
'</response>'."\n";
}
elseif(substr(Application::$format,0,4)=='html'){
if(!Application::$errorTemplateFile || !file_exists(Application::$errorTemplateFile)){
Logger::log(self::$systemErrorLog,"format is html, but templateFile (".Application::$errorTemplateFile.") not set.");
echo '<html><head><title>Error</title></head><body>'.htmlspecialchars($message).'</body></html>';
}
else{
include_once(Application::$errorTemplateFile);
}
}
else echo gmdate("Y-m-d H:i:s").'Z:'.$message."\n";
}
/**
* Use this for other exceptions, so we report and log them consistently.
*/
public static function reportOtherExceptionAndExit($e){
self::encodeAndOutputMessage(self::translate('System error.'));
$msg=date("Y-m-d H:i:s T");
$msg.=':'.(string)$e."\n"; //Let the exception describe itself
foreach(self::$details as $k=>$v)$msg.="$k=$v\n";
Logger::log(self::$systemErrorLog,$msg);
exit;
}
/**
* Appends full information on the exception to a logfile.
*
* The entry will take up multiple lines in the log file. Each entry starts with a
* timestamp, and finishes with "---" on a line by itself. However it is meant
* to be human-readable more than machine-readable.
*/
private function logMessage($fname,$maxTries=5,$sleepMicroseconds=200000){
$msg=date("Y-m-d H:i:s T").':'.$this->message."\n";
if($this->detailedMessage!=""){
if(is_string($this->detailedMessage))$msg.=$this->detailedMessage."\n";
else $msg.=print_r($this->detailedMessage,true);
}
foreach(self::$details as $k=>$v)$msg.="$k=$v\n";
$msg.="File=".$this->getFile()."; Line=".$this->getLine()."\n";
$msg.=$this->getTraceAsString()."\n";
Logger::log($fname,$msg,$maxTries,$sleepMicroseconds);
}
/**
* Convenience function for adding a bunch of entries to the $details array (and
* not complaining if they are not in $d). It can be called multiple times.
*
* Example usage:
* Exception::addToDetails(array('REMOTE_ADDR','HTTP_USER_AGENT',
* 'REQUEST_METHOD','REQUEST_TIME','PHP_AUTH_USER','HTTPS','REQUEST_URI'),$_SERVER);
*
* @param Mixed $keys An array of keys to take from $d and copy to self::$details.
* It quietly ignores keys that are not found in $d. (Values that exist but
* are blank strings are copied, however.)
* $keys can also be a single string, if there is only one key of interest.
* @param Mixed $d Can be array or object. E.g. $_SERVER is typical.
* If an object then it is simply cast to an array. (Only public vars will be available.)
*
*/
public static function addToDetails($keys,$d){
if(is_object($d))$d=(array)$d; //Allow $d to be an object
if(!is_array($keys))$keys=array($keys); //So it will work when $keys is a string
if(is_array($d)){
foreach($keys as $k){
if(array_key_exists($k,$d))self::$details[$k]=$d[$k];
}
}
}
/**
* Does translations of exception messages (before inserting parameters).
*
* @see self::$strings
*/
static protected function translate($s){
if(!self::$strings)return $s;
if(!array_key_exists($s,self::$strings))return $s;
return self::$strings[$s];
}
}
//=========================================
/**
* This is the main exception that is used for anything triggered by user input.
*
* So, this exception is thrown if the user requests a data feed that does not exist,
* or that they don't have permission for. (These mistakes are usually user error, but
* could also indicate some system problem, for instance a feed has been deleted by
* mistake, or a permission class has got removed from the user by mistake.)
*
* System admin should look at the log hourly or daily, just in case it indicates a
* more serious problem.
*/
class ErrorException extends Exception{}
//=========================================
/**
* This is for exceptions where it can only happen due to some system problem.
*
* Users are shown a "System Error, try again later" message, and system admin
* should be alerted by email (perhaps rotating and emailling a logfile once a minute
* just to avoid sending too much email).
*
* NOTE: PDOExceptions should also be treated the same way as SystemErrorException (if
* they can be triggered by user input then they should be caught and converted to
* APIErrorExceptions instead)
*
* NOTE: The message when creating the exception is only logged - it is never shown
* to the user.
*/
class SystemException extends Exception {}
/**
* Specially for when we want to trigger a basic auth login box
*
* @internal We only extend Exception so we can use encodeAndOutputMessage(). We
* don't use anything else at all. (Well, except Application::$format of course.)
*/
class MustAuthenticateException extends Exception {
/**
* This is the realm shown in the WWW-Authenticate header. In a browser it
* will appear in the dialog box that is shown to the user.
*
* Note: it will be encoded with addslashes(), but be aware of poor browser compatibility
* with exotic strings. It is best to stick to alphanumeric plus space.
*/
static public $realm='realm';
/** */
public function __construct(){
parent::__construct("");
}
/**
*/
public function requestBasicAuthAndExit(){
header('WWW-Authenticate: Basic realm="'.addslashes(self::$realm).'"');
header('HTTP/1.0 401 Unauthorized');
self::encodeAndOutputMessage(self::translate("Please authenticate."));
exit;
}
}
/**
*/
class RedirectException extends Exception {
/**
* The prefix when $url starts with "/" (or is blank). Typically this
* is the domain name of the site. E.g. "http://example.com/" but it
* might also be a sub-section of a site, e.g. "http://example.com/user/site/";
*
* NOTE: this must always be set if your code might throw this exception
* without full URLs. However if exceptions will always be thrown with full
* URLs starting with "http://" (or whatever) then there is no need to set this.
*/
static public $urlPrefix;
/**
* This is the fully qualified URL. Set by constructor.
*/
private $url;
/** */
public function __construct($url=''){
parent::__construct(""); //No error message to store
if($url=='')$url='/';
if($url{0}=='/'){
if(!self::$urlPrefix)throw new SystemException('RedirectException::urlPrefix not set, but given a url of'.$url);
if(substr(self::$urlPrefix,-1)=='/')$url = self::$urlPrefix . substr($url,1);
else $url = self::$urlPrefix;
}
$this->url = $url;
$this->message="URL:$url"; //This is for unit tests, more than anything else.
}
/**
* @todo Should look to see if headers already sent. If so, throw a SystemException instead.
*/
public function performRedirectAndExit(){
header('Location: '.$this->url);
exit;
}
}
?>