TwitterApiClient::enable_cache()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
namespace Integrations\Connectors\Twitter;
4
5
define('TWITTER_API_TIMEOUT', 5);  
6
define('TWITTER_API_USERAGENT', 'PHP/'.PHP_VERSION.'; http://github.com/timwhitlock/php-twitter-api');  
7
define('TWITTER_API_BASE', 'https://api.twitter.com/1.1');
8
define('TWITTER_OAUTH_REQUEST_TOKEN_URL', 'https://twitter.com/oauth/request_token');
9
define('TWITTER_OAUTH_AUTHORIZE_URL', 'https://twitter.com/oauth/authorize');
10
define('TWITTER_OAUTH_AUTHENTICATE_URL', 'https://twitter.com/oauth/authenticate');
11
define('TWITTER_OAUTH_ACCESS_TOKEN_URL', 'https://twitter.com/oauth/access_token');
12
 
13
/**
14
 * Client for the Twitter REST API 1.1
15
 */
16
class TwitterApiClient
17
{
18
    /**
19
     * Consumer key token for application
20
     *
21
     * @var TwitterOAuthToken
22
     */
23
    private $Consumer;
24
    /**
25
     * Authenticated access token
26
     *
27
     * @var TwitterOAuthToken
28
     */
29
    private $AccessToken;
30
    
31
    /**
32
     * Whether caching API GET requests
33
     *
34
     * @var int
35
     */
36
    private $cache_ttl = null;
37
    
38
    /**
39
     * Namespace/prefix for cache keys
40
     *
41
     * @var string
42
     */    
43
    private $cache_ns;     
44
    
45
    /**
46
     * Registry of last rate limit arrays by full api function call
47
     *
48
     * @var array
49
     */    
50
    private $last_rate = array();    
51
    
52
    /**
53
     * Last api function called, e.g. "direct_messages/sent"
54
     *
55
     * @var string
56
     */    
57
    private $last_call;     
58
    
59
    /**
60
     * @internal
61
     */
62
    public function __sleep()
63
    {
64
        return array('Consumer','AccessToken');
65
    }
66
    
67
    /**
68
     * Enable caching of subsequent API calls
69
     *
70
     * @return TwitterApiClient
71
     */
72
    public function enable_cache( $ttl = 0, $namespace = 'twitter_api_' )
73
    {
74
        if(function_exists('apc_store') ) {
75
            $this->cache_ttl = (int) $ttl;
76
            $this->cache_ns  = $namespace;
77
            return $this;
78
        }
79
        trigger_error('Cannot enable Twitter API cache without APC extension');
80
        return $this->disable_cache();
81
    }
82
    
83
    /**
84
     * Disable caching for susequent API calls
85
     *
86
     * @return TwitterApiClient
87
     */
88
    public function disable_cache()
89
    {
90
        $this->cache_ttl = null;
91
        $this->cache_ns  = null;
92
        return $this;
93
    }
94
    /**
95
     * Test whether the client has full authentication data.
96
     * Warning: does not validate credentials 
97
     *
98
     * @return bool
99
     */
100
    public function has_auth()
101
    {
102
        return $this->AccessToken instanceof TwitterOAuthToken && $this->AccessToken->secret;
103
    }    
104
    
105
    /**
106
     * Unset all logged in credentials - useful in error situations
107
     *
108
     * @return TwitterApiClient
109
     */
110
    public function deauthorize()
111
    {
112
        $this->AccessToken = null;
113
        return $this;
114
    }
115
    /**
116
     * Set currently logged in user's OAuth access token
117
     *
118
     * @param  string consumer api key
119
     * @param  string consumer secret
120
     * @param  string access token
121
     * @param  string access token secret
122
     * @return TwitterApiClient
123
     */
124
    public function set_oauth( $consumer_key, $consumer_secret, $access_key = '', $access_secret = '' )
125
    {
126
        $this->deauthorize();
127
        $this->Consumer = new TwitterOAuthToken($consumer_key, $consumer_secret);
128
        if($access_key && $access_secret ) {
129
            $this->AccessToken = new TwitterOAuthToken($access_key, $access_secret);
130
        }
131
        return $this;
132
    }
133
    
134
    /**
135
     * Set consumer oauth token by object
136
     *
137
     * @param  TwitterOAuthToken
138
     * @return TwitterApiClient
139
     */
140
    public function set_oauth_consumer( TwitterOAuthToken $token )
141
    {
142
        $this->Consumer = $token;
143
        return $this;
144
    }
145
    
146
    /**
147
     * Set access oauth token by object
148
     *
149
     * @param  TwitterOAuthToken
150
     * @return TwitterApiClient
151
     */
152
    public function set_oauth_access( TwitterOAuthToken $token )
153
    {
154
        $this->AccessToken = $token;
155
        return $this;
156
    }
157
    
158
    
159
    /**
160
     * Contact Twitter for a request token, which will be exchanged for an access token later.
161
     *
162
     * @param  string callback URL or "oob" for desktop apps (out of bounds)
163
     * @return TwitterOAuthToken Request token
164
     */
165
    public function get_oauth_request_token( $oauth_callback = 'oob' )
166
    {
167
        $params = $this->oauth_exchange(TWITTER_OAUTH_REQUEST_TOKEN_URL, compact('oauth_callback'));
168
        return new TwitterOAuthToken($params['oauth_token'], $params['oauth_token_secret']);
169
    }
170
    /**
171
     * Exchange request token for an access token after authentication/authorization by user
172
     *
173
     * @param  string verifier passed back from Twitter or copied out of browser window
174
     * @return TwitterOAuthToken Access token
175
     */
176
    public function get_oauth_access_token( $oauth_verifier )
177
    {
178
        $params = $this->oauth_exchange(TWITTER_OAUTH_ACCESS_TOKEN_URL, compact('oauth_verifier'));
179
        $token = new TwitterOAuthToken($params['oauth_token'], $params['oauth_token_secret']);
180
        $token->user = array (
181
            'id' => $params['user_id'],
182
            'screen_name' => $params['screen_name'],
183
        );
184
        return $token;
185
    }    
186
    
187
    
188
    
189
    /**
190
     * Basic sanitation of api request arguments
191
     *
192
     * @param  array original params passed by client code
193
     * @return array sanitized params that we'll serialize
194
     */    
195
    private function sanitize_args( array $_args )
196
    {
197
        // transform some arguments and ensure strings
198
        // no further validation is performed
199
        $args = array();
200
        foreach( $_args as $key => $val ){
201
            if(is_string($val) ) {
202
                $args[$key] = $val;
203
            }
204
            else if(true === $val ) {
205
                $args[$key] = 'true';
206
            }
207
            else if(false === $val || null === $val ) {
208
                 $args[$key] = 'false';
209
            }
210
            else if(! is_scalar($val) ) {
211
                throw new TwitterApiException('Invalid Twitter parameter ('.gettype($val).') '.$key, -1);
212
            }
213
            else {
214
                $args[$key] = (string) $val;
215
            }
216
        }
217
        return $args;
218
    }    
219
    
220
    
221
    
222
    /**
223
     * Call API method over HTTP and return serialized data
224
     *
225
     * @param  string API method, e.g. "users/show"
226
     * @param  array method arguments
227
     * @param  string http request method
228
     * @return array unserialized data returned from Twitter
229
     * @throws TwitterApiException
230
     */
231
    public function call( $path, array $args = array(), $http_method = 'GET' )
232
    {
233
        $args = $this->sanitize_args($args);
234
        // Fetch response from cache if possible / allowed / enabled
235
        if($http_method === 'GET' && isset($this->cache_ttl) ) {
236
            $cachekey = $this->cache_ns.$path.'_'.md5(serialize($args));
237
            if(preg_match('/^(\d+)-/', $this->AccessToken->key, $reg) ) {
238
                $cachekey .= '_'.$reg[1];
239
            }
240
            $data = apc_fetch($cachekey);
241
            if(is_array($data) ) {
242
                return $data;
243
            }
244
        }
245
        $http = $this->rest_request($path, $args, $http_method);
246
        // Deserialize response
247
        $status = $http['status'];
248
        $data = json_decode($http['body'], true);
249
        // unserializable array assumed to be serious error
250
        if(! is_array($data) ) {
251
            $err = array( 
252
                'message' => $http['error'], 
253
                'code' => -1 
254
            );
255
            TwitterApiException::chuck($err, $status);
256
        }
257
        // else could be well-formed error
258
        if(isset($data['errors']) ) {
259
            while( $err = array_shift($data['errors']) ){
260
                $err['message'] = $err['message'];
261
                if($data['errors'] ) {
262
                    $message = sprintf('Twitter error #%d', $err['code']).' "'.$err['message'].'"';
263
                    trigger_error($message, E_USER_WARNING);
264
                }
265
                else {
266
                    TwitterApiException::chuck($err, $status);
267
                }
268
            }
269
        }
270
        if(isset($cachekey) ) {
271
            apc_store($cachekey, $data, $this->cache_ttl);
272
        }
273
        return $data;
274
    }
275
    /**
276
     * Call API method over HTTP and return raw response data without caching
277
     *
278
     * @param  string API method, e.g. "users/show"
279
     * @param  array method arguments
280
     * @param  string http request method
281
     * @return array structure from http_request
282
     * @throws TwitterApiException
283
     */
284
    public function raw( $path, array $args = array(), $http_method = 'GET' )
285
    {
286
        $args = $this->sanitize_args($args);
287
        return $this->rest_request($path, $args, $http_method);
288
    }
289
    /**
290
     * Perform an OAuth request - these differ somewhat from regular API calls
291
     *
292
     * @internal
293
     */
294
    private function oauth_exchange( $endpoint, array $args )
295
    {
296
        // build a post request and authenticate via OAuth header
297
        $params = new TwitterOAuthParams($args);
298
        $params->set_consumer($this->Consumer);
299
        if($this->AccessToken ) {
300
            $params->set_token($this->AccessToken);
301
        }
302
        $params->sign_hmac('POST', $endpoint);
303
        $conf = array (
304
            'method' => 'POST',
305
            'headers' => array( 'Authorization' => $params->oauth_header() ),
306
        );
307
        $http = self::http_request($endpoint, $conf);
308
        $body = trim($http['body']);
309
        $stat = $http['status'];
310
        if(200 !== $stat ) {
311
            // Twitter might respond as XML, but with an HTML content type for some reason
312
            if(0 === strpos($body, '<?') ) {
313
                $xml = simplexml_load_string($body);
314
                $body = (string) $xml->error;
315
            }
316
            throw new TwitterApiException($body, -1, $stat);
317
        }
318
        parse_str($body, $params);
319
        if(! is_array($params) || ! isset($params['oauth_token']) || ! isset($params['oauth_token_secret']) ) {
320
            throw new TwitterApiException('Malformed response from Twitter', -1, $stat);
321
        }
322
        return $params;   
323
    }
324
    
325
    
326
    
327
    /**
328
     * Sign and execute REST API call
329
     *
330
     * @return array
331
     */
332
    private function rest_request( $path, array $args, $http_method )
333
    {
334
        // all calls must be authenticated in API 1.1
335
        if(! $this->has_auth() ) {
336
            throw new TwitterApiException('Twitter client not authenticated', 0, 401);
337
        }
338
        // prepare HTTP request config
339
        $conf = array (
340
            'method' => $http_method,
341
        );
342
        // build signed URL and request parameters
343
        $endpoint = TWITTER_API_BASE.'/'.$path.'.json';
344
        $params = new TwitterOAuthParams($args);
345
        $params->set_consumer($this->Consumer);
346
        $params->set_token($this->AccessToken);
347
        $params->sign_hmac($http_method, $endpoint);
348
        if('GET' === $http_method ) {
349
            $endpoint .= '?'.$params->serialize();
350
        }
351
        else {
352
            $conf['body'] = $params->serialize();
353
        }
354
        $http = self::http_request($endpoint, $conf);        
355
        // remember current rate limits for this endpoint
356
        $this->last_call = $path;
357
        if(isset($http['headers']['x-rate-limit-limit']) ) {
358
            $this->last_rate[$path] = array (
359
                'limit'     => (int) $http['headers']['x-rate-limit-limit'],
360
                'remaining' => (int) $http['headers']['x-rate-limit-remaining'],
361
                'reset'     => (int) $http['headers']['x-rate-limit-reset'],
362
            );
363
        }
364
        return $http;
365
    }    
366
    /**
367
     * Abstract HTTP call, currently just uses cURL extension
368
     *
369
     * @return array e.g. { body: '', error: '', status: 200, headers: {} }
370
     */
371
    public static function http_request( $endpoint, array $conf )
372
    {
373
        $conf += array(
374
            'body' => '',
375
            'method'  => 'GET',
376
            'headers' => array(),
377
        );
378
        $ch = curl_init();
379
        curl_setopt($ch, CURLOPT_URL, $endpoint);
380
        curl_setopt($ch, CURLOPT_TIMEOUT, TWITTER_API_TIMEOUT);
381
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, TWITTER_API_TIMEOUT);
382
        curl_setopt($ch, CURLOPT_USERAGENT, TWITTER_API_USERAGENT);
383
        curl_setopt($ch, CURLOPT_HEADER, true);
384
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
385
        
386
        switch ( $conf['method'] ) {
387
        case 'GET':
388
            break;
389
        case 'POST':
390
            curl_setopt($ch, CURLOPT_POST, true);
391
            curl_setopt($ch, CURLOPT_POSTFIELDS, $conf['body']);
392
            break;
393
        default:
394
            throw new TwitterApiException('Unsupported method '.$conf['method']);    
395
        }
396
        
397
        foreach( $conf['headers'] as $key => $val ){
398
            $headers[] = $key.': '.$val;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$headers was never initialized. Although not strictly required by PHP, it is generally a good practice to add $headers = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
399
        }
400
        if(isset($headers) ) {
401
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
402
        }
403
        
404
        // execute and parse response
405
        $response = curl_exec($ch);
406
        if (60 === curl_errno($ch) ) { // CURLE_SSL_CACERT
407
            curl_setopt($ch, CURLOPT_CAINFO, __DIR__.'/ca-chain-bundle.crt');
408
            $response = curl_exec($ch);
409
        }
410
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
411
        $headers = array();
412
        $body = '';
413
        if($response && $status ) {
414
            list( $header, $body ) = preg_split('/\r\n\r\n/', $response, 2); 
415
            if(preg_match_all('/^(Content[\w\-]+|X-Rate[^:]+):\s*(.+)/mi', $header, $r, PREG_SET_ORDER) ) {
416
                foreach( $r as $match ){
417
                    $headers[ strtolower($match[1]) ] = $match[2];
418
                }        
419
            }
420
            curl_close($ch);
421
        }
422
        else {
423
            $error = curl_error($ch) or 
424
            $error = 'No response from Twitter';
425
            is_resource($ch) and curl_close($ch);
426
            throw new TwitterApiException($error);
427
        }
428
        return array (
429
            'body'    => $body,
430
            'status'  => $status,
431
            'headers' => $headers,
432
        );
433
    }
434
    /**
435
     * Get current rate limit, if known. does not look it up
436
     */
437
    public function last_rate_limit_data( $func = '' )
438
    {
439
        $func or $func = $this->last_call;
440
        return isset($this->last_rate[$func]) ? $this->last_rate[$func] : array( 'limit' => 0 );
441
    }
442
    
443
    
444
    /**
445
     * Get rate limit allowance for last endpoint request
446
     */
447
    public function last_rate_limit_allowance( $func = '' )
448
    {
449
        $data = $this->last_rate_limit_data($func);
450
        return isset($data['limit']) ? $data['limit'] : null;
451
    }
452
    
453
    
454
    /**
455
     * Get number of requests remaining this period for last endpoint request
456
     */
457
    public function last_rate_limit_remaining( $func = '' )
458
    {
459
        $data = $this->last_rate_limit_data($func);
460
        return isset($data['remaining']) ? $data['remaining'] : null;
461
    }
462
    
463
    
464
    /**
465
     * Get rate limit reset time for last endpoint request
466
     */
467
    public function last_rate_limit_reset( $func = '' )
468
    {
469
        $data = $this->last_rate_limit_data($func);
470
        return isset($data['reset']) ? $data['reset'] : null;
471
    }
472
}
473
/**
474
 * Simple token class that holds key and secret
475
 *
476
 * @internal
477
 */
478
class TwitterOAuthToken
479
{
480
    public $key;
481
    public $secret;
482
    public $verifier;
483
    public $user;
484
    public function __construct( $key, $secret = '' )
485
    {
486
        if(! $key ) {
487
            throw new Exception('Invalid OAuth token - Key required even if secret is empty');
488
        }
489
        $this->key = $key;
490
        $this->secret = $secret;
491
        $this->verifier = '';
492
    }
493
    public function get_authorization_url()
494
    {
495
        return TWITTER_OAUTH_AUTHORIZE_URL.'?oauth_token='.rawurlencode($this->key);
496
    }
497
    public function get_authentication_url()
498
    {
499
        return TWITTER_OAUTH_AUTHENTICATE_URL.'?oauth_token='.rawurlencode($this->key);
500
    }
501
}
502
/**
503
 * Class for compiling, signing and serializing OAuth parameters
504
 *
505
 * @internal
506
 */
507
class TwitterOAuthParams
508
{
509
    
510
    private $args;
511
    private $consumer_secret;
512
    private $token_secret;
513
    
514
    private static function urlencode( $val )
515
    {
516
        return str_replace('%7E', '~', rawurlencode($val));
517
    }    
518
    
519
    public function __construct( array $args = array() )
520
    {
521
        $this->args = $args + array ( 
522
            'oauth_version' => '1.0',
523
        );
524
    }
525
    
526
    public function set_consumer( TwitterOAuthToken $Consumer )
527
    {
528
        $this->consumer_secret = $Consumer->secret;
529
        $this->args['oauth_consumer_key'] = $Consumer->key;
530
    }   
531
    
532
    public function set_token( TwitterOAuthToken $Token )
533
    {
534
        $this->token_secret = $Token->secret;
535
        $this->args['oauth_token'] = $Token->key;
536
    }   
537
    
538
    private function normalize()
539
    {
540
        $flags = SORT_STRING | SORT_ASC;
541
        ksort($this->args, $flags);
542
        foreach( $this->args as $k => $a ){
543
            if(is_array($a) ) {
544
                sort($this->args[$k], $flags);
545
            }
546
        }
547
        return $this->args;
548
    }
549
    
550
    public function serialize()
551
    {
552
        $str = http_build_query($this->args);
553
        // PHP_QUERY_RFC3986 requires PHP >= 5.4
554
        $str = str_replace(array('+','%7E'), array('%20','~'), $str);
555
        return $str;
556
    }
557
    public function sign_hmac( $http_method, $http_rsc )
558
    {
559
        $this->args['oauth_signature_method'] = 'HMAC-SHA1';
560
        $this->args['oauth_timestamp'] = sprintf('%u', time());
561
        $this->args['oauth_nonce'] = sprintf('%f', microtime(true));
562
        unset($this->args['oauth_signature']);
563
        $this->normalize();
564
        $str = $this->serialize();
565
        $str = strtoupper($http_method).'&'.self::urlencode($http_rsc).'&'.self::urlencode($str);
566
        $key = self::urlencode($this->consumer_secret).'&'.self::urlencode($this->token_secret);
567
        $this->args['oauth_signature'] = base64_encode(hash_hmac('sha1', $str, $key, true));
568
        return $this->args;
569
    }
570
    public function oauth_header()
571
    {
572
        $lines = array();
573
        foreach( $this->args as $key => $val ){
574
            $lines[] = self::urlencode($key).'="'.self::urlencode($val).'"';
575
        }
576
        return 'OAuth '.implode(",\n ", $lines);
577
    }
578
}
579
/**
580
 * HTTP status codes with some overridden for Twitter-related messages.
581
 * Note these do not replace error text from Twitter, they're for complete API failures.
582
 *
583
 * @param  int HTTP status code
584
 * @return string HTTP status text
585
 */
586
function _twitter_api_http_status_text( $s )
587
{
588
    static $codes = array (
589
        100 => 'Continue',
590
        101 => 'Switching Protocols',
591
        
592
        200 => 'OK',
593
        201 => 'Created',
594
        202 => 'Accepted',
595
        203 => 'Non-Authoritative Information',
596
        204 => 'No Content',
597
        205 => 'Reset Content',
598
        206 => 'Partial Content',
599
        
600
        300 => 'Multiple Choices',
601
        301 => 'Moved Permanently',
602
        302 => 'Found',
603
        303 => 'See Other',
604
        304 => 'Not Modified',
605
        305 => 'Use Proxy',
606
        307 => 'Temporary Redirect',
607
        
608
        400 => 'Bad Request',
609
        401 => 'Authorization Required',
610
        402 => 'Payment Required',
611
        403 => 'Forbidden',
612
        404 => 'Not Found',
613
        405 => 'Method Not Allowed',
614
        406 => 'Not Acceptable',
615
        407 => 'Proxy Authentication Required',
616
        408 => 'Request Time-out',
617
        409 => 'Conflict',
618
        410 => 'Gone',
619
        411 => 'Length Required',
620
        412 => 'Precondition Failed',
621
        413 => 'Request Entity Too Large',
622
        414 => 'Request-URI Too Large',
623
        415 => 'Unsupported Media Type',
624
        416 => 'Requested range not satisfiable',
625
        417 => 'Expectation Failed',
626
        //  ..
627
        429 => 'Twitter API rate limit exceeded',
628
        
629
        500 => 'Twitter server error',
630
        501 => 'Not Implemented',
631
        502 => 'Twitter is not responding',
632
        503 => 'Twitter is too busy to respond',
633
        504 => 'Gateway Time-out',
634
        505 => 'HTTP Version not supported',
635
    );
636
    return  isset($codes[$s]) ? $codes[$s] : sprintf('Status %u from Twitter', $s);
637
}
638
/**
639
 * Exception for throwing when Twitter responds with something unpleasant
640
 */
641
class TwitterApiException extends Exception
642
{
643
    /**
644
     * HTTP Status of error
645
     *
646
     * @var int
647
     */        
648
    protected $status = 0;        
649
        
650
    /**
651
     * Throw appropriate exception type according to HTTP status code
652
     *
653
     * @param array Twitter error data from their response 
654
     */
655
    public static function chuck( array $err, $status )
656
    {
657
        $code = isset($err['code']) ? (int) $err['code'] : -1;
658
        $mess = isset($err['message']) ? trim($err['message']) : '';
659
        static $classes = array (
660
            404 => 'TwitterApiNotFoundException',
661
            429 => 'TwitterApiRateLimitException',
662
        );
663
        $eclass = isset($classes[$status]) ? $classes[$status] : __CLASS__;
664
        throw new $eclass($mess, $code, $status);
665
    }
666
        
667
        
668
    /**
669
     * Construct TwitterApiException with addition of HTTP status code.
670
     *
671
     * @overload
672
     */        
673
    public function __construct( $message, $code = 0 )
674
    {
675
        if(2 < func_num_args() ) {
676
            $this->status = (int) func_get_arg(2);
677
        }
678
        if(! $message ) {
679
            $message = _twitter_api_http_status_text($this->status);
680
        }
681
        parent::__construct($message, $code);
682
    }
683
    
684
    
685
    /**
686
     * Get HTTP status of error
687
     *
688
     * @return int
689
     */
690
    public function getStatus()
691
    {
692
        return $this->status;
693
    }
694
    
695
}
696
/**
697
 * 404 
698
 */
699
class TwitterApiNotFoundException extends TwitterApiException
700
{
701
    
702
}
703
/**
704
 * 429 
705
 */
706
class TwitterApiRateLimitException extends TwitterApiException
707
{
708
    
709
}