1: <?php
2:
3: namespace Codebird;
4:
5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15:
16: 17: 18:
19: $constants = explode(' ', 'OBJECT ARRAY JSON');
20: foreach ($constants as $i => $id) {
21: $id = 'CODEBIRD_RETURNFORMAT_' . $id;
22: defined($id) or define($id, $i);
23: }
24: $constants = [
25: 'CURLE_SSL_CERTPROBLEM' => 58,
26: 'CURLE_SSL_CACERT' => 60,
27: 'CURLE_SSL_CACERT_BADFILE' => 77,
28: 'CURLE_SSL_CRL_BADFILE' => 82,
29: 'CURLE_SSL_ISSUER_ERROR' => 83
30: ];
31: foreach ($constants as $id => $i) {
32: defined($id) or define($id, $i);
33: }
34: unset($constants);
35: unset($i);
36: unset($id);
37:
38: 39: 40: 41: 42: 43:
44: class Codebird
45: {
46: 47: 48:
49: private static $_instance = null;
50:
51: 52: 53:
54: protected static $_oauth_consumer_key = null;
55:
56: 57: 58:
59: protected static $_oauth_consumer_secret = null;
60:
61: 62: 63:
64: protected static $_oauth_bearer_token = null;
65:
66: 67: 68:
69: protected static $_endpoint = 'https://api.twitter.com/1.1/';
70:
71: 72: 73:
74: protected static $_endpoint_media = 'https://upload.twitter.com/1.1/';
75:
76: 77: 78:
79: protected static $_endpoints_streaming = [
80: 'public' => 'https://stream.twitter.com/1.1/',
81: 'user' => 'https://userstream.twitter.com/1.1/',
82: 'site' => 'https://sitestream.twitter.com/1.1/'
83: ];
84:
85: 86: 87:
88: protected static $_endpoint_ton = 'https://ton.twitter.com/1.1/';
89:
90: 91: 92:
93: protected static $_endpoint_ads = 'https://ads-api.twitter.com/0/';
94:
95: 96: 97:
98: protected static $_endpoint_ads_sandbox = 'https://ads-api-sandbox.twitter.com/0/';
99:
100: 101: 102:
103: protected static $_endpoint_oauth = 'https://api.twitter.com/';
104:
105: 106: 107:
108: protected static $_possible_files = [
109:
110: 'statuses/update_with_media' => ['media[]'],
111: 'media/upload' => ['media'],
112:
113: 'account/update_profile_background_image' => ['image'],
114: 'account/update_profile_image' => ['image'],
115: 'account/update_profile_banner' => ['banner']
116: ];
117:
118: 119: 120:
121: protected $_oauth_token = null;
122:
123: 124: 125:
126: protected $_oauth_token_secret = null;
127:
128: 129: 130:
131: protected $_return_format = CODEBIRD_RETURNFORMAT_OBJECT;
132:
133: 134: 135:
136: protected $_supported_media_files = [
137: IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_BMP,
138: IMAGETYPE_GIF
139: ];
140:
141: 142: 143:
144: protected $_streaming_callback = null;
145:
146: 147: 148:
149: protected $_version = '3.0.0-dev';
150:
151: 152: 153:
154: protected $_use_curl = true;
155:
156: 157: 158:
159: protected $_timeout = 10000;
160:
161: 162: 163:
164: protected $_connectionTimeout = 3000;
165:
166: 167: 168:
169: protected $_remoteDownloadTimeout = 5000;
170:
171: 172: 173:
174: protected $_proxy = [];
175:
176: 177: 178: 179: 180:
181: public function __construct()
182: {
183:
184: $this->setUseCurl(function_exists('curl_init'));
185: }
186:
187: 188: 189: 190: 191: 192:
193: public static function getInstance()
194: {
195: if (self::$_instance === null) {
196: self::$_instance = new self;
197: }
198: return self::$_instance;
199: }
200:
201: 202: 203: 204: 205: 206: 207: 208:
209: public static function setConsumerKey($key, $secret)
210: {
211: self::$_oauth_consumer_key = $key;
212: self::$_oauth_consumer_secret = $secret;
213: }
214:
215: 216: 217: 218: 219: 220: 221:
222: public static function setBearerToken($token)
223: {
224: self::$_oauth_bearer_token = $token;
225: }
226:
227: 228: 229: 230: 231:
232: public function getVersion()
233: {
234: return $this->_version;
235: }
236:
237: 238: 239: 240: 241: 242: 243: 244:
245: public function setToken($token, $secret)
246: {
247: $this->_oauth_token = $token;
248: $this->_oauth_token_secret = $secret;
249: }
250:
251: 252: 253: 254: 255:
256: public function logout()
257: {
258: $this->_oauth_token =
259: $this->_oauth_token_secret = null;
260:
261: return true;
262: }
263:
264: 265: 266: 267: 268: 269: 270:
271: public function setUseCurl($use_curl)
272: {
273: if ($use_curl && ! function_exists('curl_init')) {
274: throw new \Exception('To use cURL, the PHP curl extension must be available.');
275: }
276:
277: $this->_use_curl = (bool) $use_curl;
278: }
279:
280: 281: 282: 283: 284: 285: 286:
287: public function setTimeout($timeout)
288: {
289: $this->_timeout = (int) $timeout;
290: }
291:
292: 293: 294: 295: 296: 297: 298:
299: public function setConnectionTimeout($timeout)
300: {
301: $this->_connectionTimeout = (int) $timeout;
302: }
303:
304: 305: 306: 307: 308: 309: 310:
311: public function setRemoteDownloadTimeout($timeout)
312: {
313: $this->_remoteDownloadTimeout = (int) $timeout;
314: }
315:
316: 317: 318: 319: 320: 321: 322: 323: 324:
325: public function setReturnFormat($return_format)
326: {
327: $this->_return_format = $return_format;
328: }
329:
330: 331: 332: 333: 334: 335: 336: 337:
338: public function setProxy($host, $port)
339: {
340: $this->_proxy['host'] = $host;
341: $this->_proxy['port'] = $port;
342: }
343:
344: 345: 346: 347: 348: 349: 350:
351: public function setProxyAuthentication($authentication)
352: {
353: $this->_proxy['authentication'] = $authentication;
354: }
355:
356: 357: 358: 359: 360: 361: 362:
363: public function setStreamingCallback($callback)
364: {
365: if (!is_callable($callback)) {
366: throw new \Exception('This is not a proper callback.');
367: }
368: $this->_streaming_callback = $callback;
369: }
370:
371: 372: 373: 374: 375: 376:
377: public function getApiMethods()
378: {
379: static $httpmethods = [
380: 'GET' => [
381: 'account/settings',
382: 'account/verify_credentials',
383: 'ads/accounts',
384: 'ads/accounts/:account_id',
385: 'ads/accounts/:account_id/app_event_provider_configurations',
386: 'ads/accounts/:account_id/app_event_provider_configurations/:id',
387: 'ads/accounts/:account_id/app_event_tags',
388: 'ads/accounts/:account_id/app_event_tags/:id',
389: 'ads/accounts/:account_id/app_lists',
390: 'ads/accounts/:account_id/authenticated_user_access',
391: 'ads/accounts/:account_id/campaigns',
392: 'ads/accounts/:account_id/campaigns/:campaign_id',
393: 'ads/accounts/:account_id/cards/app_download',
394: 'ads/accounts/:account_id/cards/app_download/:card_id',
395: 'ads/accounts/:account_id/cards/image_app_download',
396: 'ads/accounts/:account_id/cards/image_app_download/:card_id',
397: 'ads/accounts/:account_id/cards/image_conversation',
398: 'ads/accounts/:account_id/cards/image_conversation/:card_id',
399: 'ads/accounts/:account_id/cards/lead_gen',
400: 'ads/accounts/:account_id/cards/lead_gen/:card_id',
401: 'ads/accounts/:account_id/cards/video_app_download',
402: 'ads/accounts/:account_id/cards/video_app_download/:id',
403: 'ads/accounts/:account_id/cards/video_conversation',
404: 'ads/accounts/:account_id/cards/video_conversation/:card_id',
405: 'ads/accounts/:account_id/cards/website',
406: 'ads/accounts/:account_id/cards/website/:card_id',
407: 'ads/accounts/:account_id/features',
408: 'ads/accounts/:account_id/funding_instruments',
409: 'ads/accounts/:account_id/funding_instruments/:id',
410: 'ads/accounts/:account_id/line_items',
411: 'ads/accounts/:account_id/line_items/:line_item_id',
412: 'ads/accounts/:account_id/promotable_users',
413: 'ads/accounts/:account_id/promoted_accounts',
414: 'ads/accounts/:account_id/promoted_tweets',
415: 'ads/accounts/:account_id/reach_estimate',
416: 'ads/accounts/:account_id/scoped_timeline',
417: 'ads/accounts/:account_id/tailored_audience_changes',
418: 'ads/accounts/:account_id/tailored_audience_changes/:id',
419: 'ads/accounts/:account_id/tailored_audiences',
420: 'ads/accounts/:account_id/tailored_audiences/:id',
421: 'ads/accounts/:account_id/targeting_criteria',
422: 'ads/accounts/:account_id/targeting_criteria/:id',
423: 'ads/accounts/:account_id/targeting_suggestions',
424: 'ads/accounts/:account_id/tweet/preview',
425: 'ads/accounts/:account_id/tweet/preview/:tweet_id',
426: 'ads/accounts/:account_id/videos',
427: 'ads/accounts/:account_id/videos/:id',
428: 'ads/accounts/:account_id/web_event_tags',
429: 'ads/accounts/:account_id/web_event_tags/:web_event_tag_id',
430: 'ads/bidding_rules',
431: 'ads/iab_categories',
432: 'ads/insights/accounts/:account_id',
433: 'ads/insights/accounts/:account_id/available_audiences',
434: 'ads/line_items/placements',
435: 'ads/sandbox/accounts',
436: 'ads/sandbox/accounts/:account_id',
437: 'ads/sandbox/accounts/:account_id/app_event_provider_configurations',
438: 'ads/sandbox/accounts/:account_id/app_event_provider_configurations/:id',
439: 'ads/sandbox/accounts/:account_id/app_event_tags',
440: 'ads/sandbox/accounts/:account_id/app_event_tags/:id',
441: 'ads/sandbox/accounts/:account_id/app_lists',
442: 'ads/sandbox/accounts/:account_id/authenticated_user_access',
443: 'ads/sandbox/accounts/:account_id/campaigns',
444: 'ads/sandbox/accounts/:account_id/campaigns/:campaign_id',
445: 'ads/sandbox/accounts/:account_id/cards/app_download',
446: 'ads/sandbox/accounts/:account_id/cards/app_download/:card_id',
447: 'ads/sandbox/accounts/:account_id/cards/image_app_download',
448: 'ads/sandbox/accounts/:account_id/cards/image_app_download/:card_id',
449: 'ads/sandbox/accounts/:account_id/cards/image_conversation',
450: 'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id',
451: 'ads/sandbox/accounts/:account_id/cards/lead_gen',
452: 'ads/sandbox/accounts/:account_id/cards/lead_gen/:card_id',
453: 'ads/sandbox/accounts/:account_id/cards/video_app_download',
454: 'ads/sandbox/accounts/:account_id/cards/video_app_download/:id',
455: 'ads/sandbox/accounts/:account_id/cards/video_conversation',
456: 'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id',
457: 'ads/sandbox/accounts/:account_id/cards/website',
458: 'ads/sandbox/accounts/:account_id/cards/website/:card_id',
459: 'ads/sandbox/accounts/:account_id/features',
460: 'ads/sandbox/accounts/:account_id/funding_instruments',
461: 'ads/sandbox/accounts/:account_id/funding_instruments/:id',
462: 'ads/sandbox/accounts/:account_id/line_items',
463: 'ads/sandbox/accounts/:account_id/line_items/:line_item_id',
464: 'ads/sandbox/accounts/:account_id/promotable_users',
465: 'ads/sandbox/accounts/:account_id/promoted_accounts',
466: 'ads/sandbox/accounts/:account_id/promoted_tweets',
467: 'ads/sandbox/accounts/:account_id/reach_estimate',
468: 'ads/sandbox/accounts/:account_id/scoped_timeline',
469: 'ads/sandbox/accounts/:account_id/tailored_audience_changes',
470: 'ads/sandbox/accounts/:account_id/tailored_audience_changes/:id',
471: 'ads/sandbox/accounts/:account_id/tailored_audiences',
472: 'ads/sandbox/accounts/:account_id/tailored_audiences/:id',
473: 'ads/sandbox/accounts/:account_id/targeting_criteria',
474: 'ads/sandbox/accounts/:account_id/targeting_criteria/:id',
475: 'ads/sandbox/accounts/:account_id/targeting_suggestions',
476: 'ads/sandbox/accounts/:account_id/tweet/preview',
477: 'ads/sandbox/accounts/:account_id/tweet/preview/:tweet_id',
478: 'ads/sandbox/accounts/:account_id/videos',
479: 'ads/sandbox/accounts/:account_id/videos/:id',
480: 'ads/sandbox/accounts/:account_id/web_event_tags',
481: 'ads/sandbox/accounts/:account_id/web_event_tags/:web_event_tag_id',
482: 'ads/sandbox/bidding_rules',
483: 'ads/sandbox/iab_categories',
484: 'ads/sandbox/insights/accounts/:account_id',
485: 'ads/sandbox/insights/accounts/:account_id/available_audiences',
486: 'ads/sandbox/line_items/placements',
487: 'ads/sandbox/stats/accounts/:account_id',
488: 'ads/sandbox/stats/accounts/:account_id/campaigns',
489: 'ads/sandbox/stats/accounts/:account_id/campaigns/:id',
490: 'ads/sandbox/stats/accounts/:account_id/funding_instruments',
491: 'ads/sandbox/stats/accounts/:account_id/funding_instruments/:id',
492: 'ads/sandbox/stats/accounts/:account_id/line_items',
493: 'ads/sandbox/stats/accounts/:account_id/line_items/:id',
494: 'ads/sandbox/stats/accounts/:account_id/promoted_accounts',
495: 'ads/sandbox/stats/accounts/:account_id/promoted_accounts/:id',
496: 'ads/sandbox/stats/accounts/:account_id/promoted_tweets',
497: 'ads/sandbox/stats/accounts/:account_id/promoted_tweets/:id',
498: 'ads/sandbox/stats/accounts/:account_id/reach/campaigns',
499: 'ads/sandbox/targeting_criteria/app_store_categories',
500: 'ads/sandbox/targeting_criteria/behavior_taxonomies',
501: 'ads/sandbox/targeting_criteria/behaviors',
502: 'ads/sandbox/targeting_criteria/devices',
503: 'ads/sandbox/targeting_criteria/events',
504: 'ads/sandbox/targeting_criteria/interests',
505: 'ads/sandbox/targeting_criteria/languages',
506: 'ads/sandbox/targeting_criteria/locations',
507: 'ads/sandbox/targeting_criteria/network_operators',
508: 'ads/sandbox/targeting_criteria/platform_versions',
509: 'ads/sandbox/targeting_criteria/platforms',
510: 'ads/sandbox/targeting_criteria/tv_channels',
511: 'ads/sandbox/targeting_criteria/tv_genres',
512: 'ads/sandbox/targeting_criteria/tv_markets',
513: 'ads/sandbox/targeting_criteria/tv_shows',
514: 'ads/stats/accounts/:account_id',
515: 'ads/stats/accounts/:account_id/campaigns',
516: 'ads/stats/accounts/:account_id/campaigns/:id',
517: 'ads/stats/accounts/:account_id/funding_instruments',
518: 'ads/stats/accounts/:account_id/funding_instruments/:id',
519: 'ads/stats/accounts/:account_id/line_items',
520: 'ads/stats/accounts/:account_id/line_items/:id',
521: 'ads/stats/accounts/:account_id/promoted_accounts',
522: 'ads/stats/accounts/:account_id/promoted_accounts/:id',
523: 'ads/stats/accounts/:account_id/promoted_tweets',
524: 'ads/stats/accounts/:account_id/promoted_tweets/:id',
525: 'ads/stats/accounts/:account_id/reach/campaigns',
526: 'ads/targeting_criteria/app_store_categories',
527: 'ads/targeting_criteria/behavior_taxonomies',
528: 'ads/targeting_criteria/behaviors',
529: 'ads/targeting_criteria/devices',
530: 'ads/targeting_criteria/events',
531: 'ads/targeting_criteria/interests',
532: 'ads/targeting_criteria/languages',
533: 'ads/targeting_criteria/locations',
534: 'ads/targeting_criteria/network_operators',
535: 'ads/targeting_criteria/platform_versions',
536: 'ads/targeting_criteria/platforms',
537: 'ads/targeting_criteria/tv_channels',
538: 'ads/targeting_criteria/tv_genres',
539: 'ads/targeting_criteria/tv_markets',
540: 'ads/targeting_criteria/tv_shows',
541: 'application/rate_limit_status',
542: 'blocks/ids',
543: 'blocks/list',
544: 'collections/entries',
545: 'collections/list',
546: 'collections/show',
547: 'direct_messages',
548: 'direct_messages/sent',
549: 'direct_messages/show',
550: 'favorites/list',
551: 'followers/ids',
552: 'followers/list',
553: 'friends/ids',
554: 'friends/list',
555: 'friendships/incoming',
556: 'friendships/lookup',
557: 'friendships/lookup',
558: 'friendships/no_retweets/ids',
559: 'friendships/outgoing',
560: 'friendships/show',
561: 'geo/id/:place_id',
562: 'geo/reverse_geocode',
563: 'geo/search',
564: 'geo/similar_places',
565: 'help/configuration',
566: 'help/languages',
567: 'help/privacy',
568: 'help/tos',
569: 'lists/list',
570: 'lists/members',
571: 'lists/members/show',
572: 'lists/memberships',
573: 'lists/ownerships',
574: 'lists/show',
575: 'lists/statuses',
576: 'lists/subscribers',
577: 'lists/subscribers/show',
578: 'lists/subscriptions',
579: 'mutes/users/ids',
580: 'mutes/users/list',
581: 'oauth/authenticate',
582: 'oauth/authorize',
583: 'saved_searches/list',
584: 'saved_searches/show/:id',
585: 'search/tweets',
586: 'site',
587: 'statuses/firehose',
588: 'statuses/home_timeline',
589: 'statuses/mentions_timeline',
590: 'statuses/oembed',
591: 'statuses/retweeters/ids',
592: 'statuses/retweets/:id',
593: 'statuses/retweets_of_me',
594: 'statuses/sample',
595: 'statuses/show/:id',
596: 'statuses/user_timeline',
597: 'trends/available',
598: 'trends/closest',
599: 'trends/place',
600: 'user',
601: 'users/contributees',
602: 'users/contributors',
603: 'users/profile_banner',
604: 'users/search',
605: 'users/show',
606: 'users/suggestions',
607: 'users/suggestions/:slug',
608: 'users/suggestions/:slug/members'
609: ],
610: 'POST' => [
611: 'account/remove_profile_banner',
612: 'account/settings',
613: 'account/update_delivery_device',
614: 'account/update_profile',
615: 'account/update_profile_background_image',
616: 'account/update_profile_banner',
617: 'account/update_profile_colors',
618: 'account/update_profile_image',
619: 'ads/accounts/:account_id/app_lists',
620: 'ads/accounts/:account_id/campaigns',
621: 'ads/accounts/:account_id/cards/app_download',
622: 'ads/accounts/:account_id/cards/image_app_download',
623: 'ads/accounts/:account_id/cards/image_conversation',
624: 'ads/accounts/:account_id/cards/lead_gen',
625: 'ads/accounts/:account_id/cards/video_app_download',
626: 'ads/accounts/:account_id/cards/video_conversation',
627: 'ads/accounts/:account_id/cards/website',
628: 'ads/accounts/:account_id/line_items',
629: 'ads/accounts/:account_id/promoted_accounts',
630: 'ads/accounts/:account_id/promoted_tweets',
631: 'ads/accounts/:account_id/tailored_audience_changes',
632: 'ads/accounts/:account_id/tailored_audiences',
633: 'ads/accounts/:account_id/targeting_criteria',
634: 'ads/accounts/:account_id/tweet',
635: 'ads/accounts/:account_id/videos',
636: 'ads/accounts/:account_id/web_event_tags',
637: 'ads/batch/accounts/:account_id/campaigns',
638: 'ads/batch/accounts/:account_id/line_items',
639: 'ads/sandbox/accounts/:account_id/app_lists',
640: 'ads/sandbox/accounts/:account_id/campaigns',
641: 'ads/sandbox/accounts/:account_id/cards/app_download',
642: 'ads/sandbox/accounts/:account_id/cards/image_app_download',
643: 'ads/sandbox/accounts/:account_id/cards/image_conversation',
644: 'ads/sandbox/accounts/:account_id/cards/lead_gen',
645: 'ads/sandbox/accounts/:account_id/cards/video_app_download',
646: 'ads/sandbox/accounts/:account_id/cards/video_conversation',
647: 'ads/sandbox/accounts/:account_id/cards/website',
648: 'ads/sandbox/accounts/:account_id/line_items',
649: 'ads/sandbox/accounts/:account_id/promoted_accounts',
650: 'ads/sandbox/accounts/:account_id/promoted_tweets',
651: 'ads/sandbox/accounts/:account_id/tailored_audience_changes',
652: 'ads/sandbox/accounts/:account_id/tailored_audiences',
653: 'ads/sandbox/accounts/:account_id/targeting_criteria',
654: 'ads/sandbox/accounts/:account_id/tweet',
655: 'ads/sandbox/accounts/:account_id/videos',
656: 'ads/sandbox/accounts/:account_id/web_event_tags',
657: 'ads/sandbox/batch/accounts/:account_id/campaigns',
658: 'ads/sandbox/batch/accounts/:account_id/line_items',
659: 'blocks/create',
660: 'blocks/destroy',
661: 'collections/create',
662: 'collections/destroy',
663: 'collections/entries/add',
664: 'collections/entries/curate',
665: 'collections/entries/move',
666: 'collections/entries/remove',
667: 'collections/update',
668: 'direct_messages/destroy',
669: 'direct_messages/new',
670: 'favorites/create',
671: 'favorites/destroy',
672: 'friendships/create',
673: 'friendships/destroy',
674: 'friendships/update',
675: 'lists/create',
676: 'lists/destroy',
677: 'lists/members/create',
678: 'lists/members/create_all',
679: 'lists/members/destroy',
680: 'lists/members/destroy_all',
681: 'lists/subscribers/create',
682: 'lists/subscribers/destroy',
683: 'lists/update',
684: 'media/upload',
685: 'mutes/users/create',
686: 'mutes/users/destroy',
687: 'oauth/access_token',
688: 'oauth/request_token',
689: 'oauth2/invalidate_token',
690: 'oauth2/token',
691: 'saved_searches/create',
692: 'saved_searches/destroy/:id',
693: 'statuses/destroy/:id',
694: 'statuses/filter',
695: 'statuses/lookup',
696: 'statuses/retweet/:id',
697: 'statuses/update',
698: 'statuses/update_with_media',
699: 'ton/bucket/:bucket',
700: 'ton/bucket/:bucket?resumable=true',
701: 'users/lookup',
702: 'users/report_spam'
703: ],
704: 'PUT' => [
705: 'ads/accounts/:account_id/campaigns/:campaign_id',
706: 'ads/accounts/:account_id/cards/app_download/:card_id',
707: 'ads/accounts/:account_id/cards/image_app_download/:card_id',
708: 'ads/accounts/:account_id/cards/image_conversation/:card_id',
709: 'ads/accounts/:account_id/cards/lead_gen/:card_id',
710: 'ads/accounts/:account_id/cards/video_app_download/:id',
711: 'ads/accounts/:account_id/cards/video_conversation/:card_id',
712: 'ads/accounts/:account_id/cards/website/:card_id',
713: 'ads/accounts/:account_id/line_items/:line_item_id',
714: 'ads/accounts/:account_id/promoted_tweets/:id',
715: 'ads/accounts/:account_id/tailored_audiences/global_opt_out',
716: 'ads/accounts/:account_id/targeting_criteria',
717: 'ads/accounts/:account_id/videos/:id',
718: 'ads/accounts/:account_id/web_event_tags/:web_event_tag_id',
719: 'ads/sandbox/accounts/:account_id/campaigns/:campaign_id',
720: 'ads/sandbox/accounts/:account_id/cards/app_download/:card_id',
721: 'ads/sandbox/accounts/:account_id/cards/image_app_download/:card_id',
722: 'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id',
723: 'ads/sandbox/accounts/:account_id/cards/lead_gen/:card_id',
724: 'ads/sandbox/accounts/:account_id/cards/video_app_download/:id',
725: 'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id',
726: 'ads/sandbox/accounts/:account_id/cards/website/:card_id',
727: 'ads/sandbox/accounts/:account_id/line_items/:line_item_id',
728: 'ads/sandbox/accounts/:account_id/promoted_tweets/:id',
729: 'ads/sandbox/accounts/:account_id/tailored_audiences/global_opt_out',
730: 'ads/sandbox/accounts/:account_id/targeting_criteria',
731: 'ads/sandbox/accounts/:account_id/videos/:id',
732: 'ads/sandbox/accounts/:account_id/web_event_tags/:web_event_tag_id',
733: 'ton/bucket/:bucket/:file?resumable=true&resumeId=:resumeId'
734: ],
735: 'DELETE' => [
736: 'ads/accounts/:account_id/campaigns/:campaign_id',
737: 'ads/accounts/:account_id/cards/app_download/:card_id',
738: 'ads/accounts/:account_id/cards/image_app_download/:card_id',
739: 'ads/accounts/:account_id/cards/image_conversation/:card_id',
740: 'ads/accounts/:account_id/cards/lead_gen/:card_id',
741: 'ads/accounts/:account_id/cards/video_app_download/:id',
742: 'ads/accounts/:account_id/cards/video_conversation/:card_id',
743: 'ads/accounts/:account_id/cards/website/:card_id',
744: 'ads/accounts/:account_id/line_items/:line_item_id',
745: 'ads/accounts/:account_id/promoted_tweets/:id',
746: 'ads/accounts/:account_id/tailored_audiences/:id',
747: 'ads/accounts/:account_id/targeting_criteria/:id',
748: 'ads/accounts/:account_id/videos/:id',
749: 'ads/accounts/:account_id/web_event_tags/:web_event_tag_id',
750: 'ads/sandbox/accounts/:account_id/campaigns/:campaign_id',
751: 'ads/sandbox/accounts/:account_id/cards/app_download/:card_id',
752: 'ads/sandbox/accounts/:account_id/cards/image_app_download/:card_id',
753: 'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id',
754: 'ads/sandbox/accounts/:account_id/cards/lead_gen/:card_id',
755: 'ads/sandbox/accounts/:account_id/cards/video_app_download/:id',
756: 'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id',
757: 'ads/sandbox/accounts/:account_id/cards/website/:card_id',
758: 'ads/sandbox/accounts/:account_id/line_items/:line_item_id',
759: 'ads/sandbox/accounts/:account_id/promoted_tweets/:id',
760: 'ads/sandbox/accounts/:account_id/tailored_audiences/:id',
761: 'ads/sandbox/accounts/:account_id/targeting_criteria/:id',
762: 'ads/sandbox/accounts/:account_id/videos/:id',
763: 'ads/sandbox/accounts/:account_id/web_event_tags/:web_event_tag_id'
764: ]
765: ];
766: return $httpmethods;
767: }
768:
769: 770: 771: 772: 773: 774: 775: 776:
777:
778: public function __call($fn, $params)
779: {
780:
781: $apiparams = $this->_parseApiParams($params);
782:
783:
784: $apiparams = $this->_stringifyNullBoolParams($apiparams);
785:
786: $app_only_auth = false;
787: if (count($params) > 1) {
788:
789: $app_only_auth = !! $params[1];
790: }
791:
792:
793:
794: if ($fn === 'oauth_requestToken') {
795: $this->setToken(null, null);
796: }
797:
798:
799: list($method, $method_template) = $this->_mapFnToApiMethod($fn, $apiparams);
800:
801: $httpmethod = $this->_detectMethod($method_template, $apiparams);
802: $multipart = $this->_detectMultipart($method_template);
803:
804: return $this->_callApi(
805: $httpmethod,
806: $method,
807: $method_template,
808: $apiparams,
809: $multipart,
810: $app_only_auth
811: );
812: }
813:
814:
815: 816: 817:
818:
819: 820: 821: 822: 823: 824: 825:
826: protected function _parseApiParams($params)
827: {
828: $apiparams = [];
829: if (count($params) === 0) {
830: return $apiparams;
831: }
832:
833: if (is_array($params[0])) {
834:
835: $apiparams = $params[0];
836: return $apiparams;
837: }
838:
839:
840: parse_str($params[0], $apiparams);
841: if (! is_array($apiparams)) {
842: $apiparams = [];
843: }
844:
845: return $apiparams;
846: }
847:
848: 849: 850: 851: 852: 853: 854:
855: protected function _stringifyNullBoolParams($apiparams)
856: {
857: foreach ($apiparams as $key => $value) {
858: if (! is_scalar($value)) {
859:
860: continue;
861: }
862: if (is_null($value)) {
863: $apiparams[$key] = 'null';
864: } elseif (is_bool($value)) {
865: $apiparams[$key] = $value ? 'true' : 'false';
866: }
867: }
868:
869: return $apiparams;
870: }
871:
872: 873: 874: 875: 876: 877: 878: 879:
880: protected function _mapFnToApiMethod($fn, &$apiparams)
881: {
882:
883: $method = $this->_mapFnInsertSlashes($fn);
884:
885:
886: $method = $this->_mapFnRestoreParamUnderscores($method);
887:
888:
889: $method_template = $method;
890: $match = [];
891: if (preg_match_all('/[A-Z_]{2,}/', $method, $match)) {
892: foreach ($match[0] as $param) {
893: $param_l = strtolower($param);
894: if ($param_l === 'resumeid') {
895: $param_l = 'resumeId';
896: }
897: $method_template = str_replace($param, ':' . $param_l, $method_template);
898: if (! isset($apiparams[$param_l])) {
899: for ($i = 0; $i < 26; $i++) {
900: $method_template = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method_template);
901: }
902: throw new \Exception(
903: 'To call the templated method "' . $method_template
904: . '", specify the parameter value for "' . $param_l . '".'
905: );
906: }
907: $method = str_replace($param, $apiparams[$param_l], $method);
908: unset($apiparams[$param_l]);
909: }
910: }
911:
912: if (substr($method, 0, 4) !== 'ton/') {
913:
914: for ($i = 0; $i < 26; $i++) {
915: $method = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method);
916: $method_template = str_replace(chr(65 + $i), '_' . chr(97 + $i), $method_template);
917: }
918: }
919:
920: return [$method, $method_template];
921: }
922:
923: 924: 925: 926: 927: 928: 929:
930: protected function _mapFnInsertSlashes($fn)
931: {
932: $path = explode('_', $fn);
933: $method = implode('/', $path);
934:
935: return $method;
936: }
937:
938: 939: 940: 941: 942: 943: 944:
945: protected function _mapFnRestoreParamUnderscores($method)
946: {
947: $url_parameters_with_underscore = [
948: 'screen_name', 'place_id',
949: 'account_id', 'campaign_id', 'card_id', 'line_item_id',
950: 'tweet_id', 'web_event_tag_id'
951: ];
952: foreach ($url_parameters_with_underscore as $param) {
953: $param = strtoupper($param);
954: $replacement_was = str_replace('_', '/', $param);
955: $method = str_replace($replacement_was, $param, $method);
956: }
957:
958: return $method;
959: }
960:
961:
962: 963: 964:
965:
966: 967: 968: 969: 970: 971: 972: 973: 974:
975: public function oauth_authenticate($force_login = NULL, $screen_name = NULL, $type = 'authenticate')
976: {
977: if (! in_array($type, ['authenticate', 'authorize'])) {
978: throw new \Exception('To get the ' . $type . ' URL, use the correct third parameter, or omit it.');
979: }
980: if ($this->_oauth_token === null) {
981: throw new \Exception('To get the ' . $type . ' URL, the OAuth token must be set.');
982: }
983: $url = self::$_endpoint_oauth . 'oauth/' . $type . '?oauth_token=' . $this->_url($this->_oauth_token);
984: if ($force_login) {
985: $url .= "&force_login=1";
986: }
987: if ($screen_name) {
988: $url .= "&screen_name=" . $screen_name;
989: }
990: return $url;
991: }
992:
993: 994: 995: 996: 997: 998: 999:
1000: public function oauth_authorize($force_login = NULL, $screen_name = NULL)
1001: {
1002: return $this->oauth_authenticate($force_login, $screen_name, 'authorize');
1003: }
1004:
1005: 1006: 1007: 1008: 1009:
1010:
1011: public function oauth2_token()
1012: {
1013: if ($this->_use_curl) {
1014: return $this->_oauth2TokenCurl();
1015: }
1016: return $this->_oauth2TokenNoCurl();
1017: }
1018:
1019: 1020: 1021: 1022: 1023:
1024: protected function getCurlInitialization($url)
1025: {
1026: $ch = curl_init($url);
1027:
1028: curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
1029: curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
1030: curl_setopt($ch, CURLOPT_HEADER, 1);
1031: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
1032: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
1033: curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/cacert.pem');
1034: curl_setopt(
1035: $ch, CURLOPT_USERAGENT,
1036: 'codebird-php/' . $this->getVersion() . ' +https://github.com/jublonet/codebird-php'
1037: );
1038:
1039: if ($this->hasProxy()) {
1040: curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
1041: curl_setopt($ch, CURLOPT_PROXY, $this->getProxyHost());
1042: curl_setopt($ch, CURLOPT_PROXYPORT, $this->getProxyPort());
1043:
1044: if ($this->hasProxyAuthentication()) {
1045: curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
1046: curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->getProxyAuthentication());
1047: }
1048: }
1049:
1050: return $ch;
1051: }
1052:
1053: 1054: 1055: 1056: 1057: 1058: 1059: 1060: 1061:
1062: protected function getNoCurlInitialization($url, $contextOptions, $hostname = '')
1063: {
1064: $httpOptions = [];
1065:
1066: $httpOptions['header'] = [
1067: 'User-Agent: codebird-php/' . $this->getVersion() . ' +https://github.com/jublonet/codebird-php'
1068: ];
1069:
1070: $httpOptions['ssl'] = [
1071: 'verify_peer' => true,
1072: 'cafile' => __DIR__ . '/cacert.pem',
1073: 'verify_depth' => 5,
1074: 'peer_name' => $hostname
1075: ];
1076:
1077: if ($this->hasProxy()) {
1078: $httpOptions['request_fulluri'] = true;
1079: $httpOptions['proxy'] = $this->getProxyHost() . ':' . $this->getProxyPort();
1080:
1081: if ($this->hasProxyAuthentication()) {
1082: $httpOptions['header'][] =
1083: 'Proxy-Authorization: Basic ' . base64_encode($this->getProxyAuthentication());
1084: }
1085: }
1086:
1087:
1088: $options = array_merge_recursive(
1089: $contextOptions,
1090: ['http' => $httpOptions]
1091: );
1092:
1093:
1094: $options['http']['header'] = implode("\r\n", $options['http']['header']);
1095:
1096:
1097: $content = @file_get_contents($url, false, stream_context_create($options));
1098:
1099: $headers = [];
1100:
1101: if (isset($http_response_header)) {
1102: $headers = $http_response_header;
1103: }
1104:
1105: return [
1106: $content,
1107: $headers
1108: ];
1109: }
1110:
1111: protected function hasProxy()
1112: {
1113: if ($this->getProxyHost() === null) {
1114: return false;
1115: }
1116:
1117: if ($this->getProxyPort() === null) {
1118: return false;
1119: }
1120:
1121: return true;
1122: }
1123:
1124: protected function hasProxyAuthentication()
1125: {
1126: if ($this->getProxyAuthentication() === null) {
1127: return false;
1128: }
1129:
1130: return true;
1131: }
1132:
1133: 1134: 1135: 1136: 1137:
1138: protected function getProxyHost()
1139: {
1140: return $this->getProxyData('host');
1141: }
1142:
1143: 1144: 1145: 1146: 1147:
1148: protected function getProxyPort()
1149: {
1150: return $this->getProxyData('port');
1151: }
1152:
1153: 1154: 1155: 1156: 1157:
1158: protected function getProxyAuthentication()
1159: {
1160: return $this->getProxyData('authentication');
1161: }
1162:
1163: 1164: 1165:
1166: private function getProxyData($name)
1167: {
1168: if (empty($this->_proxy[$name])) {
1169: return null;
1170: }
1171:
1172: return $this->_proxy[$name];
1173: }
1174:
1175: 1176: 1177: 1178: 1179:
1180:
1181: protected function _oauth2TokenCurl()
1182: {
1183: if (self::$_oauth_consumer_key === null) {
1184: throw new \Exception('To obtain a bearer token, the consumer key must be set.');
1185: }
1186: $post_fields = [
1187: 'grant_type' => 'client_credentials'
1188: ];
1189: $url = self::$_endpoint_oauth . 'oauth2/token';
1190: $ch = $this->getCurlInitialization($url);
1191: curl_setopt($ch, CURLOPT_POST, 1);
1192: curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
1193:
1194: curl_setopt($ch, CURLOPT_USERPWD, self::$_oauth_consumer_key . ':' . self::$_oauth_consumer_secret);
1195: curl_setopt($ch, CURLOPT_HTTPHEADER, [
1196: 'Expect:'
1197: ]);
1198: $result = curl_exec($ch);
1199:
1200:
1201: if ($result === false) {
1202: throw new \Exception('Request error for bearer token: ' . curl_error($ch));
1203: }
1204:
1205:
1206: $validation_result = curl_errno($ch);
1207: $this->_validateSslCertificate($validation_result);
1208:
1209: $httpstatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
1210: $reply = $this->_parseBearerReply($result, $httpstatus);
1211: return $reply;
1212: }
1213:
1214: 1215: 1216: 1217: 1218:
1219:
1220: protected function _oauth2TokenNoCurl()
1221: {
1222: if (self::$_oauth_consumer_key == null) {
1223: throw new \Exception('To obtain a bearer token, the consumer key must be set.');
1224: }
1225:
1226: $url = self::$_endpoint_oauth . 'oauth2/token';
1227: $hostname = parse_url($url, PHP_URL_HOST);
1228:
1229: if ($hostname === false) {
1230: throw new \Exception('Incorrect API endpoint host.');
1231: }
1232:
1233: $contextOptions = [
1234: 'http' => [
1235: 'method' => 'POST',
1236: 'protocol_version' => '1.1',
1237: 'header' => "Accept: */*\r\n"
1238: . 'Authorization: Basic '
1239: . base64_encode(
1240: self::$_oauth_consumer_key
1241: . ':'
1242: . self::$_oauth_consumer_secret
1243: ),
1244: 'timeout' => $this->_timeout / 1000,
1245: 'content' => 'grant_type=client_credentials',
1246: 'ignore_errors' => true
1247: ]
1248: ];
1249: list($reply, $headers) = $this->getNoCurlInitialization($url, $contextOptions, $hostname);
1250: $result = '';
1251: foreach ($headers as $header) {
1252: $result .= $header . "\r\n";
1253: }
1254: $result .= "\r\n" . $reply;
1255:
1256:
1257: $httpstatus = '500';
1258: $match = [];
1259: if (!empty($headers[0]) && preg_match('/HTTP\/\d\.\d (\d{3})/', $headers[0], $match)) {
1260: $httpstatus = $match[1];
1261: }
1262:
1263: $reply = $this->_parseBearerReply($result, $httpstatus);
1264: return $reply;
1265: }
1266:
1267:
1268: 1269: 1270:
1271:
1272: 1273: 1274: 1275: 1276: 1277: 1278: 1279:
1280: protected function _parseBearerReply($result, $httpstatus)
1281: {
1282: list($headers, $reply) = $this->_parseApiHeaders($result);
1283: $reply = $this->_parseApiReply($reply);
1284: $rate = $this->_getRateLimitInfo($headers);
1285: switch ($this->_return_format) {
1286: case CODEBIRD_RETURNFORMAT_ARRAY:
1287: $reply['httpstatus'] = $httpstatus;
1288: $reply['rate'] = $rate;
1289: if ($httpstatus === 200) {
1290: self::setBearerToken($reply['access_token']);
1291: }
1292: break;
1293: case CODEBIRD_RETURNFORMAT_JSON:
1294: if ($httpstatus === 200) {
1295: $parsed = json_decode($reply, false, 512, JSON_BIGINT_AS_STRING);
1296: self::setBearerToken($parsed->access_token);
1297: }
1298: break;
1299: case CODEBIRD_RETURNFORMAT_OBJECT:
1300: $reply->httpstatus = $httpstatus;
1301: $reply->rate = $rate;
1302: if ($httpstatus === 200) {
1303: self::setBearerToken($reply->access_token);
1304: }
1305: break;
1306: }
1307: return $reply;
1308: }
1309:
1310: 1311: 1312: 1313: 1314: 1315: 1316:
1317: protected function _getRateLimitInfo($headers)
1318: {
1319: if (! isset($headers['x-rate-limit-limit'])) {
1320: return null;
1321: }
1322: return [
1323: 'limit' => $headers['x-rate-limit-limit'],
1324: 'remaining' => $headers['x-rate-limit-remaining'],
1325: 'reset' => $headers['x-rate-limit-reset']
1326: ];
1327: }
1328:
1329: 1330: 1331: 1332: 1333: 1334: 1335:
1336: protected function _validateSslCertificate($validation_result)
1337: {
1338: if (in_array(
1339: $validation_result,
1340: [
1341: CURLE_SSL_CERTPROBLEM,
1342: CURLE_SSL_CACERT,
1343: CURLE_SSL_CACERT_BADFILE,
1344: CURLE_SSL_CRL_BADFILE,
1345: CURLE_SSL_ISSUER_ERROR
1346: ]
1347: )
1348: ) {
1349: throw new \Exception(
1350: 'Error ' . $validation_result
1351: . ' while validating the Twitter API certificate.'
1352: );
1353: }
1354: }
1355:
1356: 1357: 1358:
1359:
1360: 1361: 1362: 1363: 1364: 1365: 1366:
1367: protected function _url($data)
1368: {
1369: if (is_array($data)) {
1370: return array_map([
1371: $this,
1372: '_url'
1373: ], $data);
1374: } elseif (is_scalar($data)) {
1375: return str_replace([
1376: '+',
1377: '!',
1378: '*',
1379: "'",
1380: '(',
1381: ')'
1382: ], [
1383: ' ',
1384: '%21',
1385: '%2A',
1386: '%27',
1387: '%28',
1388: '%29'
1389: ], rawurlencode($data));
1390: } else {
1391: return '';
1392: }
1393: }
1394:
1395: 1396: 1397: 1398: 1399: 1400: 1401:
1402: protected function _sha1($data)
1403: {
1404: if (self::$_oauth_consumer_secret === null) {
1405: throw new \Exception('To generate a hash, the consumer secret must be set.');
1406: }
1407: if (!function_exists('hash_hmac')) {
1408: throw new \Exception('To generate a hash, the PHP hash extension must be available.');
1409: }
1410: return base64_encode(hash_hmac(
1411: 'sha1',
1412: $data,
1413: self::$_oauth_consumer_secret
1414: . '&'
1415: . ($this->_oauth_token_secret !== null
1416: ? $this->_oauth_token_secret
1417: : ''
1418: ),
1419: true
1420: ));
1421: }
1422:
1423: 1424: 1425: 1426: 1427: 1428: 1429:
1430: protected function _nonce($length = 8)
1431: {
1432: if ($length < 1) {
1433: throw new \Exception('Invalid nonce length.');
1434: }
1435: return substr(md5(microtime(true)), 0, $length);
1436: }
1437:
1438: 1439: 1440: 1441: 1442: 1443: 1444: 1445: 1446:
1447: protected function _getSignature($httpmethod, $method, $base_params)
1448: {
1449:
1450: $base_string = '';
1451: foreach ($base_params as $key => $value) {
1452: $base_string .= $key . '=' . $value . '&';
1453: }
1454:
1455:
1456: $base_string = substr($base_string, 0, -1);
1457:
1458:
1459: return $this->_sha1(
1460: $httpmethod . '&' .
1461: $this->_url($method) . '&' .
1462: $this->_url($base_string)
1463: );
1464: }
1465:
1466: 1467: 1468: 1469: 1470: 1471: 1472: 1473: 1474: 1475:
1476: protected function _sign($httpmethod, $method, $params = [], $append_to_get = false)
1477: {
1478: if (self::$_oauth_consumer_key === null) {
1479: throw new \Exception('To generate a signature, the consumer key must be set.');
1480: }
1481: $sign_base_params = array_map(
1482: [$this, '_url'],
1483: [
1484: 'oauth_consumer_key' => self::$_oauth_consumer_key,
1485: 'oauth_version' => '1.0',
1486: 'oauth_timestamp' => time(),
1487: 'oauth_nonce' => $this->_nonce(),
1488: 'oauth_signature_method' => 'HMAC-SHA1'
1489: ]
1490: );
1491: if ($this->_oauth_token !== null) {
1492: $sign_base_params['oauth_token'] = $this->_url($this->_oauth_token);
1493: }
1494: $oauth_params = $sign_base_params;
1495:
1496:
1497: $sign_base_params = array_merge(
1498: $sign_base_params,
1499: array_map([$this, '_url'], $params)
1500: );
1501: ksort($sign_base_params);
1502:
1503: $signature = $this->_getSignature($httpmethod, $method, $sign_base_params);
1504:
1505: $params = $append_to_get ? $sign_base_params : $oauth_params;
1506: $params['oauth_signature'] = $signature;
1507:
1508: ksort($params);
1509: if ($append_to_get) {
1510: $authorization = '';
1511: foreach ($params as $key => $value) {
1512: $authorization .= $key . '="' . $this->_url($value) . '", ';
1513: }
1514: return substr($authorization, 0, -1);
1515: }
1516: $authorization = 'OAuth ';
1517: foreach ($params as $key => $value) {
1518: $authorization .= $key . "=\"" . $this->_url($value) . "\", ";
1519: }
1520: return substr($authorization, 0, -2);
1521: }
1522:
1523: 1524: 1525: 1526: 1527: 1528: 1529: 1530:
1531: protected function _detectMethod($method, &$params)
1532: {
1533: if (isset($params['httpmethod'])) {
1534: $httpmethod = $params['httpmethod'];
1535: unset($params['httpmethod']);
1536: return $httpmethod;
1537: }
1538: $apimethods = $this->getApiMethods();
1539:
1540:
1541: switch ($method) {
1542: case 'ads/accounts/:account_id/campaigns':
1543: case 'ads/sandbox/accounts/:account_id/campaigns':
1544: if (isset($params['funding_instrument_id'])) {
1545: return 'POST';
1546: }
1547: break;
1548: case 'ads/accounts/:account_id/line_items':
1549: case 'ads/sandbox/accounts/:account_id/line_items':
1550: if (isset($params['campaign_id'])) {
1551: return 'POST';
1552: }
1553: break;
1554: case 'ads/accounts/:account_id/targeting_criteria':
1555: case 'ads/sandbox/accounts/:account_id/targeting_criteria':
1556: if (isset($params['targeting_value'])) {
1557: return 'POST';
1558: }
1559: break;
1560: case 'ads/accounts/:account_id/app_lists':
1561: case 'ads/accounts/:account_id/campaigns':
1562: case 'ads/accounts/:account_id/cards/app_download':
1563: case 'ads/accounts/:account_id/cards/image_app_download':
1564: case 'ads/accounts/:account_id/cards/image_conversion':
1565: case 'ads/accounts/:account_id/cards/lead_gen':
1566: case 'ads/accounts/:account_id/cards/video_app_download':
1567: case 'ads/accounts/:account_id/cards/video_conversation':
1568: case 'ads/accounts/:account_id/cards/website':
1569: case 'ads/accounts/:account_id/tailored_audiences':
1570: case 'ads/accounts/:account_id/web_event_tags':
1571: case 'ads/sandbox/accounts/:account_id/app_lists':
1572: case 'ads/sandbox/accounts/:account_id/campaigns':
1573: case 'ads/sandbox/accounts/:account_id/cards/app_download':
1574: case 'ads/sandbox/accounts/:account_id/cards/image_app_download':
1575: case 'ads/sandbox/accounts/:account_id/cards/image_conversion':
1576: case 'ads/sandbox/accounts/:account_id/cards/lead_gen':
1577: case 'ads/sandbox/accounts/:account_id/cards/video_app_download':
1578: case 'ads/sandbox/accounts/:account_id/cards/video_conversation':
1579: case 'ads/sandbox/accounts/:account_id/cards/website':
1580: case 'ads/sandbox/accounts/:account_id/tailored_audiences':
1581: case 'ads/sandbox/accounts/:account_id/web_event_tags':
1582: if (isset($params['name'])) {
1583: return 'POST';
1584: }
1585: break;
1586: case 'ads/accounts/:account_id/promoted_accounts':
1587: case 'ads/sandbox/accounts/:account_id/promoted_accounts':
1588: if (isset($params['user_id'])) {
1589: return 'POST';
1590: }
1591: break;
1592: case 'ads/accounts/:account_id/promoted_tweets':
1593: case 'ads/sandbox/accounts/:account_id/promoted_tweets':
1594: if (isset($params['tweet_ids'])) {
1595: return 'POST';
1596: }
1597: break;
1598: case 'ads/accounts/:account_id/videos':
1599: case 'ads/sandbox/accounts/:account_id/videos':
1600: if (isset($params['video_media_id'])) {
1601: return 'POST';
1602: }
1603: break;
1604: case 'ads/accounts/:account_id/tailored_audience_changes':
1605: case 'ads/sandbox/accounts/:account_id/tailored_audience_changes':
1606: if (isset($params['tailored_audience_id'])) {
1607: return 'POST';
1608: }
1609: break;
1610: case 'ads/accounts/:account_id/cards/image_conversation/:card_id':
1611: case 'ads/accounts/:account_id/cards/video_conversation/:card_id':
1612: case 'ads/accounts/:account_id/cards/website/:card_id':
1613: case 'ads/sandbox/accounts/:account_id/cards/image_conversation/:card_id':
1614: case 'ads/sandbox/accounts/:account_id/cards/video_conversation/:card_id':
1615: case 'ads/sandbox/accounts/:account_id/cards/website/:card_id':
1616: if (isset($params['name'])) {
1617: return 'PUT';
1618: }
1619: break;
1620: default:
1621:
1622: if (count($params) > 0) {
1623: if (isset($apimethods['POST'][$method])) {
1624: return 'POST';
1625: }
1626: if (isset($apimethods['PUT'][$method])) {
1627: return 'PUT';
1628: }
1629: }
1630: }
1631:
1632: foreach ($apimethods as $httpmethod => $methods) {
1633: if (in_array($method, $methods)) {
1634: return $httpmethod;
1635: }
1636: }
1637: throw new \Exception('Can\'t find HTTP method to use for "' . $method . '".');
1638: }
1639:
1640: 1641: 1642: 1643: 1644: 1645: 1646:
1647: protected function _detectMultipart($method)
1648: {
1649: $multiparts = [
1650:
1651: 'statuses/update_with_media',
1652: 'media/upload',
1653:
1654:
1655:
1656:
1657:
1658:
1659: ];
1660: return in_array($method, $multiparts);
1661: }
1662:
1663: 1664: 1665: 1666: 1667: 1668: 1669: 1670: 1671:
1672: protected function _getMultipartRequestFromParams($method_template, $border, $params)
1673: {
1674: $request = '';
1675: foreach ($params as $key => $value) {
1676:
1677: if (is_array($value)) {
1678: throw new \Exception('Using URL-encoded parameters is not supported for uploading media.');
1679: }
1680: $request .=
1681: '--' . $border . "\r\n"
1682: . 'Content-Disposition: form-data; name="' . $key . '"';
1683:
1684:
1685: $data = $this->_checkForFiles($method_template, $key, $value);
1686: if ($data !== false) {
1687: $value = $data;
1688: }
1689:
1690: $request .= "\r\n\r\n" . $value . "\r\n";
1691: }
1692:
1693: return $request;
1694: }
1695:
1696: 1697: 1698: 1699: 1700: 1701: 1702: 1703: 1704:
1705: protected function _checkForFiles($method_template, $key, $value) {
1706: if (!in_array($key, self::$_possible_files[$method_template])) {
1707: return false;
1708: }
1709: if (
1710: @file_exists($value)
1711: && @is_readable($value)
1712: ) {
1713:
1714: $data = @getimagesize($value);
1715: if ((is_array($data) && in_array($data[2], $this->_supported_media_files))
1716: || imagecreatefromwebp($data)
1717: ) {
1718:
1719: $data = @file_get_contents($value);
1720: if ($data !== false && strlen($data) !== 0) {
1721: return $data;
1722: }
1723: }
1724: } elseif (
1725: filter_var($value, FILTER_VALIDATE_URL)
1726: && preg_match('/^https?:\/\//', $value)
1727: ) {
1728: $data = $this->_fetchRemoteFile($value);
1729: if ($data !== false) {
1730: return $data;
1731: }
1732: }
1733: return false;
1734: }
1735:
1736:
1737: 1738: 1739: 1740: 1741: 1742: 1743: 1744: 1745:
1746: protected function _buildMultipart($method, $params)
1747: {
1748:
1749: if (! $this->_detectMultipart($method)) {
1750: return;
1751: }
1752:
1753:
1754:
1755: if (! in_array($method, array_keys(self::$_possible_files))) {
1756: return;
1757: }
1758:
1759: $multipart_border = '--------------------' . $this->_nonce();
1760: $multipart_request =
1761: $this->_getMultipartRequestFromParams($method, $multipart_border, $params)
1762: . '--' . $multipart_border . '--';
1763:
1764: return $multipart_request;
1765: }
1766:
1767: 1768: 1769: 1770: 1771: 1772: 1773:
1774: protected function _buildBinaryBody($input)
1775: {
1776: if (
1777: @file_exists($input)
1778: && @is_readable($input)
1779: ) {
1780:
1781: $data = @file_get_contents($input);
1782: if ($data !== false && strlen($data) !== 0) {
1783: return $data;
1784: }
1785: } elseif (
1786: filter_var($input, FILTER_VALIDATE_URL)
1787: && preg_match('/^https?:\/\//', $input)
1788: ) {
1789: $data = $this->_fetchRemoteFile($input);
1790: if ($data !== false) {
1791: return $data;
1792: }
1793: }
1794: return $input;
1795: }
1796:
1797: 1798: 1799: 1800: 1801: 1802: 1803:
1804: protected function _fetchRemoteFile($url)
1805: {
1806:
1807: if ($this->_use_curl) {
1808: $ch = $this->getCurlInitialization($url);
1809: curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
1810: curl_setopt($ch, CURLOPT_HEADER, 0);
1811:
1812: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
1813: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
1814:
1815: curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->_remoteDownloadTimeout);
1816: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->_remoteDownloadTimeout / 2);
1817:
1818: curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
1819:
1820: curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate,sdch');
1821: $result = curl_exec($ch);
1822: if ($result !== false) {
1823: return $result;
1824: }
1825: return false;
1826: }
1827:
1828: $contextOptions = [
1829: 'http' => [
1830: 'method' => 'GET',
1831: 'protocol_version' => '1.1',
1832: 'timeout' => $this->_remoteDownloadTimeout
1833: ],
1834: 'ssl' => [
1835: 'verify_peer' => false
1836: ]
1837: ];
1838: list($result) = $this->getNoCurlInitialization($url, $contextOptions);
1839: if ($result !== false) {
1840: return $result;
1841: }
1842: return false;
1843: }
1844:
1845: 1846: 1847: 1848: 1849: 1850: 1851:
1852: protected function _detectMedia($method) {
1853: $medias = [
1854: 'media/upload'
1855: ];
1856: return in_array($method, $medias);
1857: }
1858:
1859: 1860: 1861: 1862: 1863: 1864: 1865:
1866: protected function _detectJsonBody($method) {
1867: $json_bodies = [
1868: 'collections/entries/curate'
1869: ];
1870: return in_array($method, $json_bodies);
1871: }
1872:
1873: 1874: 1875: 1876: 1877: 1878: 1879:
1880: protected function _detectBinaryBody($method_template) {
1881: $binary = [
1882: 'ton/bucket/:bucket',
1883: 'ton/bucket/:bucket?resumable=true',
1884: 'ton/bucket/:bucket/:file?resumable=true&resumeId=:resumeId'
1885: ];
1886: return in_array($method_template, $binary);
1887: }
1888:
1889: 1890: 1891: 1892: 1893: 1894: 1895:
1896: protected function _detectStreaming($method) {
1897: $streamings = [
1898: 'public' => [
1899: 'statuses/sample',
1900: 'statuses/filter',
1901: 'statuses/firehose'
1902: ],
1903: 'user' => ['user'],
1904: 'site' => ['site']
1905: ];
1906: foreach ($streamings as $key => $values) {
1907: if (in_array($method, $values)) {
1908: return $key;
1909: }
1910: }
1911:
1912: return false;
1913: }
1914:
1915: 1916: 1917: 1918: 1919: 1920: 1921: 1922:
1923: protected function _getEndpoint($method, $method_template)
1924: {
1925: if (substr($method_template, 0, 5) === 'oauth') {
1926: $url = self::$_endpoint_oauth . $method;
1927: } elseif ($this->_detectMedia($method_template)) {
1928: $url = self::$_endpoint_media . $method . '.json';
1929: } elseif ($variant = $this->_detectStreaming($method_template)) {
1930: $url = self::$_endpoints_streaming[$variant] . $method . '.json';
1931: } elseif ($this->_detectBinaryBody($method_template)) {
1932: $url = self::$_endpoint_ton . $method;
1933: } elseif (substr($method_template, 0, 12) === 'ads/sandbox/') {
1934: $url = self::$_endpoint_ads_sandbox . substr($method, 12);
1935: } elseif (substr($method_template, 0, 4) === 'ads/') {
1936: $url = self::$_endpoint_ads . substr($method, 4);
1937: } else {
1938: $url = self::$_endpoint . $method . '.json';
1939: }
1940: return $url;
1941: }
1942:
1943: 1944: 1945: 1946: 1947: 1948: 1949: 1950: 1951: 1952: 1953: 1954:
1955:
1956: protected function _callApi($httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false)
1957: {
1958: if (! $app_only_auth
1959: && $this->_oauth_token === null
1960: && substr($method, 0, 5) !== 'oauth'
1961: ) {
1962: throw new \Exception('To call this API, the OAuth access token must be set.');
1963: }
1964:
1965: if ($this->_detectStreaming($method) !== false) {
1966: return $this->_callApiStreaming($httpmethod, $method, $method_template, $params, $app_only_auth);
1967: }
1968:
1969: if ($this->_use_curl) {
1970: return $this->_callApiCurl($httpmethod, $method, $method_template, $params, $multipart, $app_only_auth);
1971: }
1972: return $this->_callApiNoCurl($httpmethod, $method, $method_template, $params, $multipart, $app_only_auth);
1973: }
1974:
1975: 1976: 1977: 1978: 1979: 1980: 1981: 1982: 1983: 1984: 1985: 1986:
1987:
1988: protected function _callApiCurl(
1989: $httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false
1990: )
1991: {
1992: list ($authorization, $url, $params, $request_headers)
1993: = $this->_callApiPreparations(
1994: $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
1995: );
1996:
1997: $ch = $this->getCurlInitialization($url);
1998: $request_headers[] = 'Authorization: ' . $authorization;
1999: $request_headers[] = 'Expect:';
2000:
2001: if ($httpmethod !== 'GET') {
2002: curl_setopt($ch, CURLOPT_POST, 1);
2003: curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
2004: if (in_array($httpmethod, ['POST', 'PUT', 'DELETE'])) {
2005: curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $httpmethod);
2006: }
2007: }
2008:
2009: curl_setopt($ch, CURLOPT_HTTPHEADER, $request_headers);
2010:
2011: if (isset($this->_timeout)) {
2012: curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->_timeout);
2013: }
2014:
2015: if (isset($this->_connectionTimeout)) {
2016: curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->_connectionTimeout);
2017: }
2018:
2019: $result = curl_exec($ch);
2020:
2021:
2022: if ($result === false) {
2023: throw new \Exception('Request error for API call: ' . curl_error($ch));
2024: }
2025:
2026:
2027: $validation_result = curl_errno($ch);
2028: $this->_validateSslCertificate($validation_result);
2029:
2030: $httpstatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
2031: list($headers, $reply) = $this->_parseApiHeaders($result);
2032:
2033: $reply = $this->_parseApiReplyPrefillHeaders($headers, $reply);
2034: $reply = $this->_parseApiReply($reply);
2035: $rate = $this->_getRateLimitInfo($headers);
2036:
2037: switch ($this->_return_format) {
2038: case CODEBIRD_RETURNFORMAT_ARRAY:
2039: $reply['httpstatus'] = $httpstatus;
2040: $reply['rate'] = $rate;
2041: break;
2042: case CODEBIRD_RETURNFORMAT_OBJECT:
2043: $reply->httpstatus = $httpstatus;
2044: $reply->rate = $rate;
2045: break;
2046: }
2047: return $reply;
2048: }
2049:
2050: 2051: 2052: 2053: 2054: 2055: 2056: 2057: 2058: 2059: 2060: 2061:
2062:
2063: protected function _callApiNoCurl(
2064: $httpmethod, $method, $method_template, $params = [], $multipart = false, $app_only_auth = false
2065: )
2066: {
2067: list ($authorization, $url, $params, $request_headers)
2068: = $this->_callApiPreparations(
2069: $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
2070: );
2071:
2072: $hostname = parse_url($url, PHP_URL_HOST);
2073: if ($hostname === false) {
2074: throw new \Exception('Incorrect API endpoint host.');
2075: }
2076:
2077: $request_headers[] = 'Authorization: ' . $authorization;
2078: $request_headers[] = 'Accept: */*';
2079: $request_headers[] = 'Connection: Close';
2080: if ($httpmethod !== 'GET' && ! $multipart) {
2081: $request_headers[] = 'Content-Type: application/x-www-form-urlencoded';
2082: }
2083:
2084: $contextOptions = [
2085: 'http' => [
2086: 'method' => $httpmethod,
2087: 'protocol_version' => '1.1',
2088: 'header' => implode("\r\n", $request_headers),
2089: 'timeout' => $this->_timeout / 1000,
2090: 'content' => in_array($httpmethod, ['POST', 'PUT']) ? $params : null,
2091: 'ignore_errors' => true
2092: ]
2093: ];
2094:
2095: list($reply, $headers) = $this->getNoCurlInitialization($url, $contextOptions, $hostname);
2096: $result = '';
2097: foreach ($headers as $header) {
2098: $result .= $header . "\r\n";
2099: }
2100: $result .= "\r\n" . $reply;
2101:
2102:
2103: $httpstatus = '500';
2104: $match = [];
2105: if (!empty($headers[0]) && preg_match('/HTTP\/\d\.\d (\d{3})/', $headers[0], $match)) {
2106: $httpstatus = $match[1];
2107: }
2108:
2109: list($headers, $reply) = $this->_parseApiHeaders($result);
2110:
2111: $reply = $this->_parseApiReplyPrefillHeaders($headers, $reply);
2112: $reply = $this->_parseApiReply($reply);
2113: $rate = $this->_getRateLimitInfo($headers);
2114: switch ($this->_return_format) {
2115: case CODEBIRD_RETURNFORMAT_ARRAY:
2116: $reply['httpstatus'] = $httpstatus;
2117: $reply['rate'] = $rate;
2118: break;
2119: case CODEBIRD_RETURNFORMAT_OBJECT:
2120: $reply->httpstatus = $httpstatus;
2121: $reply->rate = $rate;
2122: break;
2123: }
2124: return $reply;
2125: }
2126:
2127: 2128: 2129: 2130: 2131: 2132: 2133: 2134: 2135: 2136:
2137: protected function _callApiPreparationsGet(
2138: $httpmethod, $url, $params, $app_only_auth
2139: ) {
2140: return [
2141: $app_only_auth ? null : $this->_sign($httpmethod, $url, $params),
2142: json_encode($params) === '[]' ? $url : $url . '?' . http_build_query($params)
2143: ];
2144: }
2145:
2146: 2147: 2148: 2149: 2150: 2151: 2152: 2153: 2154: 2155: 2156: 2157: 2158:
2159: protected function _callApiPreparationsPost(
2160: $httpmethod, $url, $method, $method_template, $params, $multipart, $app_only_auth
2161: ) {
2162: $authorization = null;
2163: $request_headers = [];
2164: if ($multipart) {
2165: if (! $app_only_auth) {
2166: $authorization = $this->_sign($httpmethod, $url, []);
2167: }
2168: $params = $this->_buildMultipart($method, $params);
2169: $first_newline = strpos($params, "\r\n");
2170: $multipart_boundary = substr($params, 2, $first_newline - 2);
2171: $request_headers[] = 'Content-Type: multipart/form-data; boundary='
2172: . $multipart_boundary;
2173: } elseif ($this->_detectJsonBody($method)) {
2174: $authorization = $this->_sign($httpmethod, $url, []);
2175: $params = json_encode($params);
2176: $request_headers[] = 'Content-Type: application/json';
2177: } elseif ($this->_detectBinaryBody($method_template)) {
2178:
2179: foreach ([
2180: 'Content-Type', 'X-TON-Content-Type',
2181: 'X-TON-Content-Length', 'Content-Range'
2182: ] as $key) {
2183: if (isset($params[$key])) {
2184: $request_headers[] = $key . ': ' . $params[$key];
2185: unset($params[$key]);
2186: }
2187: }
2188: $sign_params = [];
2189: parse_str(parse_url($method, PHP_URL_QUERY), $sign_params);
2190: if ($sign_params === null) {
2191: $sign_params = [];
2192: }
2193: $authorization = $this->_sign($httpmethod, $url, $sign_params);
2194: if (isset($params['media'])) {
2195: $params = $this->_buildBinaryBody($params['media']);
2196: } else {
2197:
2198: $params = [];
2199: }
2200: } else {
2201:
2202: foreach ($params as $key => $value) {
2203: $data = $this->_checkForFiles($method_template, $key, $value);
2204: if ($data !== false) {
2205: $params[$key] = base64_encode($data);
2206: }
2207: }
2208: if (! $app_only_auth) {
2209: $authorization = $this->_sign($httpmethod, $url, $params);
2210: }
2211: $params = http_build_query($params);
2212: }
2213: return [$authorization, $params, $request_headers];
2214: }
2215:
2216: 2217: 2218: 2219: 2220:
2221: protected function _getBearerAuthorization()
2222: {
2223: if (self::$_oauth_consumer_key === null
2224: && self::$_oauth_bearer_token === null
2225: ) {
2226: throw new \Exception('To make an app-only auth API request, consumer key or bearer token must be set.');
2227: }
2228:
2229: if (self::$_oauth_bearer_token === null) {
2230: $this->oauth2_token();
2231: }
2232: return 'Bearer ' . self::$_oauth_bearer_token;
2233: }
2234:
2235: 2236: 2237: 2238: 2239: 2240: 2241: 2242: 2243: 2244: 2245: 2246:
2247: protected function _callApiPreparations(
2248: $httpmethod, $method, $method_template, $params, $multipart, $app_only_auth
2249: )
2250: {
2251: $url = $this->_getEndpoint($method, $method_template);
2252: $request_headers = [];
2253: if ($httpmethod === 'GET') {
2254:
2255: list ($authorization, $url) =
2256: $this->_callApiPreparationsGet($httpmethod, $url, $params, $app_only_auth);
2257: } else {
2258:
2259: list ($authorization, $params, $request_headers) =
2260: $this->_callApiPreparationsPost($httpmethod, $url, $method, $method_template, $params, $multipart, $app_only_auth);
2261: }
2262: if ($app_only_auth) {
2263: $authorization = $this->_getBearerAuthorization();
2264: }
2265:
2266: return [
2267: $authorization, $url, $params, $request_headers
2268: ];
2269: }
2270:
2271: 2272: 2273: 2274: 2275: 2276: 2277: 2278: 2279: 2280: 2281:
2282:
2283: protected function _callApiStreaming(
2284: $httpmethod, $method, $method_template, $params = [], $app_only_auth = false
2285: )
2286: {
2287: if ($this->_streaming_callback === null) {
2288: throw new \Exception('Set streaming callback before consuming a stream.');
2289: }
2290:
2291: $params['delimited'] = 'length';
2292:
2293: list ($authorization, $url, $params, $request_headers)
2294: = $this->_callApiPreparations(
2295: $httpmethod, $method, $method_template, $params, false, $app_only_auth
2296: );
2297:
2298: $hostname = parse_url($url, PHP_URL_HOST);
2299: $path = parse_url($url, PHP_URL_PATH);
2300: $query = parse_url($url, PHP_URL_QUERY);
2301: if ($hostname === false) {
2302: throw new \Exception('Incorrect API endpoint host.');
2303: }
2304:
2305: $request_headers[] = 'Authorization: ' . $authorization;
2306: $request_headers[] = 'Accept: */*';
2307: if ($httpmethod !== 'GET') {
2308: $request_headers[] = 'Content-Type: application/x-www-form-urlencoded';
2309: $request_headers[] = 'Content-Length: ' . strlen($params);
2310: }
2311:
2312: $errno = 0;
2313: $errstr = '';
2314: $ch = stream_socket_client(
2315: 'ssl://' . $hostname . ':443',
2316: $errno, $errstr,
2317: $this->_connectionTimeout,
2318: STREAM_CLIENT_CONNECT
2319: );
2320:
2321:
2322: $request = $httpmethod . ' '
2323: . $path . ($query ? '?' . $query : '') . " HTTP/1.1\r\n"
2324: . 'Host: ' . $hostname . "\r\n"
2325: . implode("\r\n", $request_headers)
2326: . "\r\n\r\n";
2327: if ($httpmethod !== 'GET') {
2328: $request .= $params;
2329: }
2330: fputs($ch, $request);
2331: stream_set_blocking($ch, 0);
2332: stream_set_timeout($ch, 0);
2333:
2334:
2335: do {
2336: $result = stream_get_line($ch, 1048576, "\r\n\r\n");
2337: } while(!$result);
2338: $headers = explode("\r\n", $result);
2339:
2340:
2341: $httpstatus = '500';
2342: $match = [];
2343: if (!empty($headers[0]) && preg_match('/HTTP\/\d\.\d (\d{3})/', $headers[0], $match)) {
2344: $httpstatus = $match[1];
2345: }
2346:
2347: list($headers,) = $this->_parseApiHeaders($result);
2348: $rate = $this->_getRateLimitInfo($headers);
2349:
2350: if ($httpstatus !== '200') {
2351: $reply = [
2352: 'httpstatus' => $httpstatus,
2353: 'rate' => $rate
2354: ];
2355: switch ($this->_return_format) {
2356: case CODEBIRD_RETURNFORMAT_ARRAY:
2357: return $reply;
2358: case CODEBIRD_RETURNFORMAT_OBJECT:
2359: return (object) $reply;
2360: case CODEBIRD_RETURNFORMAT_JSON:
2361: return json_encode($reply);
2362: }
2363: }
2364:
2365: $signal_function = function_exists('pcntl_signal_dispatch');
2366: $data = '';
2367: $last_message = time();
2368: $message_length = 0;
2369:
2370: while (!feof($ch)) {
2371:
2372: if ($signal_function) {
2373: pcntl_signal_dispatch();
2374: }
2375: $cha = [$ch];
2376: $write = $except = null;
2377: if (false === ($num_changed_streams = stream_select($cha, $write, $except, 0, 200000))) {
2378: break;
2379: } elseif ($num_changed_streams === 0) {
2380: if (time() - $last_message >= 1) {
2381:
2382: $cancel_stream = $this->_deliverStreamingMessage(null);
2383: if ($cancel_stream) {
2384: break;
2385: }
2386: $last_message = time();
2387: }
2388: continue;
2389: }
2390: $chunk_length = fgets($ch, 10);
2391: if ($chunk_length === '' || !$chunk_length = hexdec($chunk_length)) {
2392: continue;
2393: }
2394:
2395: $chunk = '';
2396: do {
2397: $chunk .= fread($ch, $chunk_length);
2398: $chunk_length -= strlen($chunk);
2399: } while($chunk_length > 0);
2400:
2401: if(0 === $message_length) {
2402: $message_length = (int) strstr($chunk, "\r\n", true);
2403: if ($message_length) {
2404: $chunk = substr($chunk, strpos($chunk, "\r\n") + 2);
2405: } else {
2406: continue;
2407: }
2408:
2409: $data = $chunk;
2410: } else {
2411: $data .= $chunk;
2412: }
2413:
2414: if (strlen($data) < $message_length) {
2415: continue;
2416: }
2417:
2418: $reply = $this->_parseApiReply($data);
2419: switch ($this->_return_format) {
2420: case CODEBIRD_RETURNFORMAT_ARRAY:
2421: $reply['httpstatus'] = $httpstatus;
2422: $reply['rate'] = $rate;
2423: break;
2424: case CODEBIRD_RETURNFORMAT_OBJECT:
2425: $reply->httpstatus = $httpstatus;
2426: $reply->rate = $rate;
2427: break;
2428: }
2429:
2430: $cancel_stream = $this->_deliverStreamingMessage($reply);
2431: if ($cancel_stream) {
2432: break;
2433: }
2434:
2435: $data = '';
2436: $message_length = 0;
2437: $last_message = time();
2438: }
2439:
2440: return;
2441: }
2442:
2443: 2444: 2445: 2446: 2447: 2448: 2449:
2450: protected function _deliverStreamingMessage($message)
2451: {
2452: return call_user_func($this->_streaming_callback, $message);
2453: }
2454:
2455: 2456: 2457: 2458: 2459: 2460: 2461:
2462: protected function _parseApiHeaders($reply) {
2463:
2464: $headers = [];
2465: $reply = explode("\r\n\r\n", $reply, 4);
2466:
2467:
2468: $proxy_tester = strtolower(substr($reply[0], 0, 35));
2469: if ($proxy_tester === 'http/1.0 200 connection established'
2470: || $proxy_tester === 'http/1.1 200 connection established'
2471: ) {
2472: array_shift($reply);
2473: } elseif (count($reply) > 2) {
2474: $headers = array_shift($reply);
2475: $reply = [
2476: $headers,
2477: implode("\r\n", $reply)
2478: ];
2479: }
2480:
2481: $headers_array = explode("\r\n", $reply[0]);
2482: foreach ($headers_array as $header) {
2483: $header_array = explode(': ', $header, 2);
2484: $key = $header_array[0];
2485: $value = '';
2486: if (count($header_array) > 1) {
2487: $value = $header_array[1];
2488: }
2489: $headers[$key] = $value;
2490: }
2491:
2492: if (count($reply) > 1) {
2493: $reply = $reply[1];
2494: } else {
2495: $reply = '';
2496: }
2497:
2498: return [$headers, $reply];
2499: }
2500:
2501: 2502: 2503: 2504: 2505: 2506: 2507: 2508:
2509: protected function _parseApiReplyPrefillHeaders($headers, $reply)
2510: {
2511: if ($reply === '' && (isset($headers['Location']))) {
2512: $reply = [
2513: 'Location' => $headers['Location']
2514: ];
2515: if (isset($headers['X-TON-Min-Chunk-Size'])) {
2516: $reply['X-TON-Min-Chunk-Size'] = $headers['X-TON-Min-Chunk-Size'];
2517: }
2518: if (isset($headers['X-TON-Max-Chunk-Size'])) {
2519: $reply['X-TON-Max-Chunk-Size'] = $headers['X-TON-Max-Chunk-Size'];
2520: }
2521: if (isset($headers['Range'])) {
2522: $reply['Range'] = $headers['Range'];
2523: }
2524: $reply = json_encode($reply);
2525: }
2526: return $reply;
2527: }
2528:
2529: 2530: 2531: 2532: 2533: 2534: 2535:
2536: protected function _parseApiReply($reply)
2537: {
2538: $need_array = $this->_return_format === CODEBIRD_RETURNFORMAT_ARRAY;
2539: if ($reply === '[]') {
2540: switch ($this->_return_format) {
2541: case CODEBIRD_RETURNFORMAT_ARRAY:
2542: return [];
2543: case CODEBIRD_RETURNFORMAT_JSON:
2544: return '{}';
2545: case CODEBIRD_RETURNFORMAT_OBJECT:
2546: return new \stdClass;
2547: }
2548: }
2549: if (! $parsed = json_decode($reply, $need_array, 512, JSON_BIGINT_AS_STRING)) {
2550: if ($reply) {
2551: if (stripos($reply, '<' . '?xml version="1.0" encoding="UTF-8"?' . '>') === 0) {
2552:
2553:
2554:
2555: preg_match('/<request>(.*)<\/request>/', $reply, $request);
2556: preg_match('/<error>(.*)<\/error>/', $reply, $error);
2557: $parsed['request'] = htmlspecialchars_decode($request[1]);
2558: $parsed['error'] = htmlspecialchars_decode($error[1]);
2559: } else {
2560:
2561: $reply = explode('&', $reply);
2562: foreach ($reply as $element) {
2563: if (stristr($element, '=')) {
2564: list($key, $value) = explode('=', $element, 2);
2565: $parsed[$key] = $value;
2566: } else {
2567: $parsed['message'] = $element;
2568: }
2569: }
2570: }
2571: }
2572: $reply = json_encode($parsed);
2573: }
2574: switch ($this->_return_format) {
2575: case CODEBIRD_RETURNFORMAT_ARRAY:
2576: return $parsed;
2577: case CODEBIRD_RETURNFORMAT_JSON:
2578: return $reply;
2579: case CODEBIRD_RETURNFORMAT_OBJECT:
2580: return (object) $parsed;
2581: }
2582: return $parsed;
2583: }
2584: }
2585: