├── .gitignore ├── include ├── .htaccess ├── utility.php ├── twitteroauth.php ├── OAuth.php └── simple_html_dom.php ├── oauth ├── .htaccess └── index.html ├── .htaccess ├── README.md ├── config-example.php ├── filters ├── 1_1__statuses__NUMBER__activity__summary_json.php ├── oauth__access_token.php ├── _default.php ├── 1_1__statuses__update_with_media_json.php └── 1_1__timeline__home_json.php ├── index.php ├── index.html ├── getapi.php ├── rewrite_rules.md ├── image_proxy.php ├── style.css ├── oauth_proxy.php ├── oauth.php └── twip.php /.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | oauth/ 3 | debug 4 | log 5 | -------------------------------------------------------------------------------- /include/.htaccess: -------------------------------------------------------------------------------- 1 | Order deny,allow 2 | Deny from all 3 | -------------------------------------------------------------------------------- /oauth/.htaccess: -------------------------------------------------------------------------------- 1 | Order deny,allow 2 | Deny from all 3 | -------------------------------------------------------------------------------- /oauth/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | #RewriteBase /twip 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteCond %{REQUEST_FILENAME} !-d 6 | RewriteRule . index.php [L] 7 | 8 | DirectoryIndex index.html 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # twip, a twitter API proxy in PHP 2 | 3 | ## Prerequisite 4 | 5 | - PHP >= 5.3.0 with curl support. 6 | - Proper HTTP server rewrite rules. Apache/nginx/lighttpd is supported 7 | for now. Please contribute more rules if you happened to run twip on 8 | an unsupported HTTP server. 9 | -------------------------------------------------------------------------------- /config-example.php: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /filters/1_1__statuses__NUMBER__activity__summary_json.php: -------------------------------------------------------------------------------- 1 | filters[$filterName] = function($args) { 6 | // TODO: should we try to parse some official API and get the correct response? 7 | return '{"retweeters_count":"0","retweeters":[],"repliers_count":"0","repliers":[],"favoriters":[],"favoriters_count":"0"}'; 8 | }; 9 | -------------------------------------------------------------------------------- /filters/oauth__access_token.php: -------------------------------------------------------------------------------- 1 | filters[$filterName] = function($args) { 6 | return sprintf( 7 | "oauth_token=%s&oauth_token_secret=%s&user_id=%s&screen_name=%s&x_auth_expires=0\n", 8 | $args['self']->access_token['oauth_token'], 9 | $args['self']->access_token['oauth_token_secret'], 10 | $args['self']->access_token['user_id'], 11 | $args['self']->access_token['screen_name'] 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /filters/_default.php: -------------------------------------------------------------------------------- 1 | filters[$filterName] = function($args) { 6 | if (substr($args['path'], 0, 4) != '1.1/') { 7 | $url = sprintf("https://api.twitter.com/1.1/%s", $args['path']); 8 | } else { 9 | $url = sprintf("https://api.twitter.com/%s", $args['path']); 10 | } 11 | if ($args['method'] === 'POST') { 12 | return $args['self']->connection->post($url, $args['params']); 13 | } else { 14 | return $args['self']->connection_get->get($url, $args['params']); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /include/utility.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /filters/1_1__statuses__update_with_media_json.php: -------------------------------------------------------------------------------- 1 | filters[$filterName] = function($args) { 6 | $url = sprintf("https://api.twitter.com/%s", $args['path']); 7 | $headers = OAuthUtil::get_headers(); 8 | // Check actually media uplaod 9 | if(strpos(@$headers['Content-Type'], 'multipart/form-data') === FALSE 10 | or count($_FILES) == 0 or !isset($_FILES['media'])) { 11 | header('HTTP/1.0 400 Bad Request'); 12 | return; 13 | } 14 | 15 | $auth_headers = $args['self']->connection->getOAuthRequest( 16 | $url, $args['method'], null)->to_header(); 17 | $forwarded_headers = array( 18 | "Host: api.twitter.com", 19 | $auth_headers, 20 | "Expect:"); 21 | $parameters = preg_replace('/^@/', "\0@", $_POST); 22 | 23 | $media = $_FILES['media']; 24 | $fn = is_array($media['tmp_name']) ? $media['tmp_name'][0] : $media['tmp_name']; 25 | $parameters["media[]"] = '@' . $fn; 26 | 27 | $ch = curl_init($url); 28 | curl_setopt($ch, CURLOPT_HTTPHEADER, $forwarded_headers); 29 | curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($args['self'],'headerfunction')); 30 | curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters); 31 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 32 | $ret = curl_exec($ch); 33 | return $ret; 34 | }; 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Twip 4 - Configuration 6 | 7 | 8 | 9 | 10 |

Twip4

11 |

Twitter API Proxy, redefined.

12 |
13 |

Twip 使用说明

14 |

什么是 Twip?

15 |

Twip 是一个运行在 LAMP 主机上的 PHP 程序,用于绕过GFW的限制在各种Twitter客户端里访问Twitter

16 |

什么是 T 模式?

17 |

T模式 ( Transparent ) 会透明转发客户端的HTTP请求,能完整保留客户端的Source信息。

18 |

什么是 O 模式?

19 |

O模式 ( Override ) 会使用自定义OAuth Consumer Key/Secret重新对客户端HTTP请求进行签名,无法保留客户端Source信息。

20 |

开始使用 Twip

21 |

22 | 使用 T 模式 23 | 使用 O 模式 24 |

25 |

FAQ 及反馈

26 |

27 | 更多安装说明和问题解决,请访问项目 Wiki:
https://github.com/twip/twip/wiki 28 |

29 |

30 | 反馈 BUG 请在此提交:
31 | https://github.com/twip/twip/issues 32 |

33 |
34 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /getapi.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | Twip 4 - Configuration 9 | 10 | 11 | 12 | 13 |

Twip4

14 |

Twitter API Proxy, redefined.

15 |
16 | 17 |

你的 API Proxy 地址

18 | 19 |

20 | 21 |

22 | 23 | 24 |

你的 Image Proxy 地址

25 | 26 |

27 | 28 |

29 | 30 | 31 |

32 | 友情提醒:请不要随意泄漏你的 API 地址。Twip 默认会保护你的 API 地址不被搜索引擎爬取。 33 |

34 | 35 | 38 |

39 | 注意: O 模式下每次提交认证都会生成新的随机 API 地址! 40 |

41 | 44 | 45 |

46 | 返回首页 47 |

48 | 49 |
50 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /filters/1_1__timeline__home_json.php: -------------------------------------------------------------------------------- 1 | filters[$filterName] = function($args) { 6 | $url = "https://api.twitter.com/1.1/statuses/home_timeline.json"; 7 | if ($args['method'] === 'POST') { 8 | $ret = $args['self']->connection->post($url, $args['params']); 9 | } else { 10 | $ret = $args['self']->connection_get->get($url, $args['params']); 11 | } 12 | $raw_obj = json_decode($ret); 13 | $ret = array( 14 | "twitter_objects" => array( 15 | "users" => array(), 16 | "tweets" => array(), 17 | "event_summaries" => array(), 18 | ), 19 | "response" => array( 20 | "timeline" => array(), 21 | ), 22 | ); 23 | $tweets = &$ret['twitter_objects']['tweets']; 24 | $users = &$ret['twitter_objects']['users']; 25 | $timeline = &$ret['response']['timeline']; 26 | foreach($raw_obj as $tweet) { 27 | $user = $tweet->user; 28 | $users[$user->id_str] = $user; 29 | unset($tweet->user); 30 | $tweet->user = array( 31 | "id" => $user->id, 32 | "id_str" => $user->id_str, 33 | ); 34 | $tweets[strval($tweet->id)] = $tweet; 35 | $timeline[] = array( 36 | 'tweet' => array( 37 | 'id' => strval($tweet->id), 38 | ), 39 | 'entity_id' => array( 40 | 'type' => 'tweet', 41 | 'ids' => array( 42 | strval($tweet->id), 43 | ), 44 | ), 45 | ); 46 | } 47 | return json_encode($ret); 48 | }; 49 | -------------------------------------------------------------------------------- /rewrite_rules.md: -------------------------------------------------------------------------------- 1 | # Rewrite Rules 2 | 3 | 4 | ## Apache 5 | 6 | Just set `AllowOverride` in you Host, Apache will follow `.htaccess` rules provided in twip. 7 | 8 | ## Nginx(with php-fpm) 9 | 10 | Example: 11 | 12 | ```nginx 13 | server { 14 | listen 443 ssl spdy; 15 | server_name m.example.net; 16 | ssl on; 17 | ssl_certificate /path/to/cert.crt; 18 | ssl_certificate_key /path/to/privkey.pem; 19 | ssl_prefer_server_ciphers on; 20 | client_max_body_size 8m; 21 | gzip on; 22 | index index.php; 23 | 24 | root /srv/http/twitter; 25 | location /twip/oauth { deny all; } 26 | location /twip/ { try_files $uri /twip/index.php; } 27 | location ~ \.php$ { 28 | try_files $uri =404; 29 | include fastcgi_params; 30 | fastcgi_pass unix:/var/run/php-fpm/php-cgi.socket; 31 | } 32 | } 33 | ``` 34 | 35 | Note: 36 | 37 | * In the example, twip source is located at: `/srv/http/twitter/twip`, and twip is working at `https://m.example.net/twip`. 38 | * If you want to change to some other directory, change `root` / `location /twip/oauth` / `location /twip/` 39 | * Remember to protect `/twip/oauth/` from leaking info when you're adjusting rules. 40 | 41 | 42 | ## lightTPD 43 | 44 | (Sorry I'm not familar with lightTPD, can't provide a working example here, text below was provided by someone other. If you want to help other lightTPD users, fire an issue at github with a full example.) 45 | 46 | 47 | For lightTPD users, please use the following rules: 48 | 49 | ``` 50 | url.rewrite-if-not-file += ( "^/(.*)$" => "/index.php/$1" ) 51 | ``` 52 | 53 | _Provided by kk198_ 54 | 55 | 56 | Just a reminder, please specify the index-file to "index.php" 57 | 58 | ``` 59 | ^/twip/(.*)$ /twip/index.php 60 | ``` 61 | 62 | instead of 63 | 64 | ``` 65 | ^/(.*)$. /index.php 66 | 67 | index-file.names = ( "index.php", "index.html", 68 | "index.htm", "default.htm" ) 69 | ``` 70 | -------------------------------------------------------------------------------- /image_proxy.php: -------------------------------------------------------------------------------- 1 | empty($_POST['message']) ? '' : $_POST['message'], 9 | 'media' => "@$image", 10 | ); 11 | $signingurl = 'https://api.twitter.com/1/account/verify_credentials.json'; 12 | $consumer = new OAuthConsumer($oauth_key, $oauth_secret); 13 | $token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); 14 | $sha1_method = new OAuthSignatureMethod_HMAC_SHA1(); 15 | $request = OAuthRequest::from_consumer_and_token($consumer, $token, 'GET', $signingurl, array()); 16 | $request->sign_request($sha1_method, $consumer, $token); 17 | // header 18 | $header = $request->to_header("http://api.twitter.com/"); 19 | 20 | /**** request method ****/ 21 | $url = 'http://img.ly/api/2/upload.json'; 22 | $ch = curl_init($url); 23 | curl_setopt($ch, CURLOPT_POST, true); 24 | curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata); 25 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('X-Auth-Service-Provider: '.$signingurl,'X-Verify-Credentials-'.$header)); 26 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 27 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); 28 | curl_setopt($ch, CURLOPT_TIMEOUT, 60); 29 | 30 | $response = curl_exec($ch); 31 | $response_info=curl_getinfo($ch); 32 | curl_close($ch); 33 | 34 | if ($response_info['http_code'] == 200) { 35 | if(preg_match('/^Twitter\/[^ ]+ CFNetwork\/[^ ]+ Darwin\/[^ ]+$/',$_SERVER['HTTP_USER_AGENT'])){ 36 | $data = json_decode($response); 37 | return empty($data) ? '' : ''.$data->{'url'}.''; 38 | }else{ 39 | header('Content-Type: application/json'); 40 | return $response; 41 | } 42 | } else { 43 | return 'error '.$response_info['http_code']; 44 | } 45 | } 46 | 47 | ?> -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Styles for Twip 4 3 | Author: disinfeqt 4 | Website: www.zdxia.com 5 | E-mail: disinfeqt@gmail.com 6 | */ 7 | *{margin:0;padding:0;font-size:1em;border:0;outline:none;list-style:none;text-decoration:none;} 8 | .clear{clear:both;display:block;overflow:hidden;visibility:hidden;width:0;height:0;} 9 | .clearfix:after{clear:both;content:' ';display:block;font-size:0;line-height:0;visibility:hidden;width:0;height:0;} 10 | * html .clearfix,:first-child+html .clearfix{zoom:1;} 11 | body{background:#5B686A;background:rgba(0,24,26,0.7);color:#143C3F;font:13px/1.8 "Helvetica Neue",Helvetica,Arial,sans-serif;} 12 | a:link,a:visited{color:#043034;border-bottom:1px dotted #334648;} 13 | a:hover,a:focus{border-bottom:1px solid #334648;color:#334648;} 14 | a:active,input[type=submit]:active{position:relative;bottom:-1px;} 15 | p{margin:0 0 10px;} 16 | small{font-size:12px;} 17 | div{margin:0 auto;width:100%;text-align:left;background:#eee;width:340px;padding:5px 20px 20px;} 18 | div#footer{margin-top:10px;background:none;text-align:center;padding:0;color:#DDD;font-size:10px;font-weight:100;letter-spacing:1px;text-shadow:0 1px 0 #314446;text-transform:uppercase;} 19 | div#footer a{color:#ddd;border:none;} 20 | div#footer a:hover{color:#fff;} 21 | h1,h2{width:100%;text-align:center;color:#fff;} 22 | h1{font-size:40px;text-transform:lowercase;margin-top:20px;line-height:1.2;} 23 | h1 sup{font-size:20px;} 24 | h1 a{color:#fff!important;border:none!important;} 25 | h1 a:hover{color:#eee!important;} 26 | h2{margin-bottom:20px;} 27 | h3{border-left:5px solid #B4BABB;font-size:20px;line-height:1;padding-left:10px;margin:15px 0;text-transform:uppercase;font-weight:100;} 28 | h4{font-size:14px;font-weight:700;} 29 | a.button{background:#143C3F;border:medium none;color:#FFF;display:block;float:left;padding:4px 5px;text-align:center;width:150px;} 30 | a.last{margin-left:20px;} 31 | input[type=text],input[type=password]{border:1px solid #B4BABB;font-family:Helvetica;font-size:13px;padding:5px;width:328px;} 32 | input.half {width:156px;} 33 | input[type=text]:focus,input[type=password]:focus{border:1px solid #143C3F;} 34 | input[type=submit]{background:none repeat scroll 0 0 #143C3F;border:1px solid #143C3F;color:#FFF;font-family:Helvetica;font-size:13px;padding:5px;cursor:pointer;} 35 | label{display:block} 36 | div ul{border-bottom:2px solid #143C3F;margin-bottom:10px;} 37 | ul li a{display:block;float:left;background:#B4BABB;color:#fff!important;border:none!important;padding:2px 10px;text-align:center;width:150px;} 38 | ul li a.active{background:#143C3F;color:#fff;} -------------------------------------------------------------------------------- /oauth_proxy.php: -------------------------------------------------------------------------------- 1 | 抓 11 | oauth_token -> 抓 12 | session[username_or_email] -> twitterAccount 13 | session[password] -> twitterPassword 14 | */ 15 | /* After this page, we should validate the returning page. 16 | Statud 403 -> No longer valid / wrong password. 17 | Status 200 -> 18 | if (contain_Allow) { 19 | post_allow; 20 | get_oauth_strings; 21 | post_oauth_strings_to_oauth.php; 22 | } else { 23 | get_oauth_strings; 24 | ... 25 | } 26 | */ 27 | $page_auth = file_get_html($oAuthEntryPage); 28 | if($page_auth === FALSE){ 29 | echo "Cannot load http resource using file_get_contents"; 30 | exit(); 31 | } 32 | $oauth_token = $page_auth->find('input[name=oauth_token]', 0)->attr['value']; 33 | $authenticity_token = $page_auth->find('input[name=authenticity_token]', 0)->attr['value']; 34 | $login_fields = Array( 35 | 'oauth_token' => urlencode($oauth_token), 36 | 'authenticity_token' => urlencode($authenticity_token), 37 | 'session[username_or_email]' => urlencode($twitterAccount), 38 | 'session[password]' => urlencode($twitterPassword) 39 | ); 40 | foreach($login_fields as $key=>$value) { 41 | $login_string .= $key.'='.$value.'&'; 42 | } 43 | $ckfile = tempnam ("/tmp", "CURLCOOKIE"); 44 | $ch = curl_init(); 45 | curl_setopt($ch, CURLOPT_URL, 'https://api.twitter.com/oauth/authorize'); 46 | curl_setopt($ch, CURLOPT_COOKIEJAR, $ckfile); 47 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 48 | curl_setopt($ch, CURLOPT_POST, count($login_fields)); 49 | curl_setopt($ch, CURLOPT_POSTFIELDS, $login_string); 50 | $login_result = curl_exec($ch); 51 | curl_close($ch); 52 | $login_obj = str_get_html($login_result); 53 | $login_error = $login_obj->find('div[class=error notice] p', 0)->innertext; 54 | if(strlen($login_error) > 8) { 55 | /* This is a workaround coz oauth_errors can be " " */ 56 | echo "There must be something wrong with your user account and password combination.
"; 57 | echo "Twitter said: $login_error\n"; 58 | die(-1); 59 | } 60 | $code = $login_obj->find('code', 0)->innertext; 61 | return $code; 62 | } 63 | -------------------------------------------------------------------------------- /oauth.php: -------------------------------------------------------------------------------- 1 | getRequestToken(BASE_URL.'oauth.php'); 24 | 25 | /* Save request token to session */ 26 | $_SESSION['oauth_token'] = $request_token['oauth_token']; 27 | $_SESSION['oauth_token_secret'] = $request_token['oauth_token_secret']; 28 | 29 | if ($connection->http_code != 200) { 30 | http_error($connection->http_code); 31 | } 32 | 33 | /* Build authorize URL */ 34 | $url = $connection->getAuthorizeURL($_SESSION['oauth_token'],FALSE); 35 | header('HTTP/1.1 302 Found'); 36 | header('Status: 302 Found'); 37 | header('Location: ' . $url); 38 | } 39 | elseif ($_GET['type'] == 2) { 40 | function oob($oauth_key, $oauth_secret, $suff = '') { 41 | $connection = new TwitterOAuth($oauth_key, $oauth_secret); 42 | $request_token = $connection->getRequestToken('oob'); 43 | 44 | /* Save request token to session */ 45 | $_SESSION['oauth_token' . $suff] = $request_token['oauth_token']; 46 | $_SESSION['oauth_token_secret' . $suff] = $request_token['oauth_token_secret']; 47 | 48 | if ($connection->http_code != 200) { 49 | http_error($connection->http_code); 50 | } 51 | 52 | $url = $connection->getAuthorizeURL($request_token['oauth_token'], FALSE); 53 | $oauth_verifier = oauth_proxy($url, $_POST['username'], $_POST['password']); 54 | return "&oauth_token$suff=" . $request_token['oauth_token'] . "&oauth_verifier$suff=" . $oauth_verifier; 55 | } 56 | 57 | $url = oob(OAUTH_KEY, OAUTH_SECRET); 58 | $url .= oob(OAUTH_KEY_GET, OAUTH_SECRET_GET, '_get'); 59 | 60 | header('HTTP/1.1 302 Found'); 61 | header('Status: 302 Found'); 62 | header('Location: ' . BASE_URL . 'oauth.php?' . $url); 63 | } 64 | exit(); 65 | } 66 | if(isset($_GET['oauth_token']) && isset($_GET['oauth_verifier'])){ 67 | $connection = new TwitterOAuth(OAUTH_KEY, OAUTH_SECRET, $_SESSION['oauth_token'], $_SESSION['oauth_token_secret']); 68 | $access_token = $connection->getAccessToken($_GET['oauth_verifier']); 69 | if(isset($_GET['oauth_token_get']) && isset($_GET['oauth_verifier_get'])) { 70 | // XXX: really need to be refactored with previous part 71 | $connection = new TwitterOAuth(OAUTH_KEY_GET, OAUTH_SECRET_GET, $_SESSION['oauth_token_get'], $_SESSION['oauth_token_secret_get']); 72 | $access_token_get = $connection->getAccessToken($_GET['oauth_verifier_get']); 73 | $access_token['oauth_token_get'] = $access_token_get['oauth_token']; 74 | $access_token['oauth_token_secret_get'] = $access_token_get['oauth_token_secret']; 75 | } 76 | if($connection->http_code == 200){ 77 | $old_tokens = glob('oauth/*.'.$access_token['screen_name']); 78 | if(!empty($old_tokens)){ 79 | foreach($old_tokens as $file){ 80 | unlink($file); 81 | } 82 | } 83 | if($_SESSION['url_suffix']==''){ 84 | for ($i=0; $i<6; $i++) { 85 | $d=rand(1,30)%2; 86 | $suffix_string .= $d ? chr(rand(65,90)) : chr(rand(48,57)); 87 | } 88 | } 89 | else{ 90 | $suffix_string = $_SESSION['url_suffix']; 91 | } 92 | if(file_put_contents('oauth/'.$suffix_string.'.'.$access_token['screen_name'],serialize($access_token)) === FALSE){ 93 | echo 'Error failed to write access_token file.Please check if you have write permission to oauth/ directory'."\n"; 94 | exit(); 95 | } 96 | $url = BASE_URL.'o/'.$suffix_string; 97 | header('HTTP/1.1 302 Found'); 98 | header('Status: 302 Found'); 99 | header('Location: getapi.php?api='.$url); 100 | } 101 | else { 102 | echo 'Error '.$connection->http_code."\n"; 103 | print_r($connection); 104 | } 105 | exit(); 106 | } 107 | ?> 108 | 109 | 110 | 111 | 112 | Twip 4 - Configuration 113 | 114 | 115 | 116 | 117 |

Twip4

118 |

Twitter API Proxy, redefined.

119 | 122 |
123 | 124 |

Twip 配置

125 | 126 |
127 | 128 | 132 | 133 |
134 | 135 |

136 | 137 | 138 | 139 |

140 | 141 | 142 | 143 |
144 |
145 | 148 | 149 | 150 | 154 |
155 | 156 |

Twip 配置

157 | 158 |
159 | 160 | 164 | 165 |
166 | 167 |

168 | 169 | 170 | 171 |

172 | 173 |

174 | 175 | 176 |

177 | 178 |

179 | 180 | 181 |

182 | 183 | 184 | 185 |
186 | 187 |
188 | 191 | 192 | 193 | 196 | -------------------------------------------------------------------------------- /include/twitteroauth.php: -------------------------------------------------------------------------------- 1 | http_status; } 54 | function lastAPICall() { return $this->last_api_call; } 55 | 56 | /** 57 | * construct TwitterOAuth object 58 | */ 59 | function __construct($consumer_key, $consumer_secret, $oauth_token = NULL, $oauth_token_secret = NULL) { 60 | $this->sha1_method = new OAuthSignatureMethod_HMAC_SHA1(); 61 | $this->consumer = new OAuthConsumer($consumer_key, $consumer_secret); 62 | if (!empty($oauth_token) && !empty($oauth_token_secret)) { 63 | $this->token = new OAuthConsumer($oauth_token, $oauth_token_secret); 64 | } else { 65 | $this->token = NULL; 66 | } 67 | } 68 | 69 | 70 | /** 71 | * Get a request_token from Twitter 72 | * 73 | * @returns a key/value array containing oauth_token and oauth_token_secret 74 | */ 75 | function getRequestToken($oauth_callback) { 76 | $parameters = array(); 77 | $parameters['oauth_callback'] = $oauth_callback; 78 | $request = $this->oAuthRequest($this->requestTokenURL(), 'GET', $parameters); 79 | $token = OAuthUtil::parse_parameters($request); 80 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); 81 | return $token; 82 | } 83 | 84 | /** 85 | * Get the authorize URL 86 | * 87 | * @returns a string 88 | */ 89 | function getAuthorizeURL($token, $sign_in_with_twitter = TRUE) { 90 | if (is_array($token)) { 91 | $token = $token['oauth_token']; 92 | } 93 | if (empty($sign_in_with_twitter)) { 94 | return $this->authorizeURL() . "?oauth_token={$token}"; 95 | } else { 96 | return $this->authenticateURL() . "?oauth_token={$token}"; 97 | } 98 | } 99 | 100 | /** 101 | * Exchange request token and secret for an access token and 102 | * secret, to sign API calls. 103 | * 104 | * @returns array("oauth_token" => "the-access-token", 105 | * "oauth_token_secret" => "the-access-secret", 106 | * "user_id" => "9436992", 107 | * "screen_name" => "abraham") 108 | */ 109 | function getAccessToken($oauth_verifier) { 110 | $parameters = array(); 111 | $parameters['oauth_verifier'] = $oauth_verifier; 112 | $request = $this->oAuthRequest($this->accessTokenURL(), 'GET', $parameters); 113 | $token = OAuthUtil::parse_parameters($request); 114 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); 115 | return $token; 116 | } 117 | 118 | /** 119 | * One time exchange of username and password for access token and secret. 120 | * 121 | * @returns array("oauth_token" => "the-access-token", 122 | * "oauth_token_secret" => "the-access-secret", 123 | * "user_id" => "9436992", 124 | * "screen_name" => "abraham", 125 | * "x_auth_expires" => "0") 126 | */ 127 | function getXAuthToken($username, $password) { 128 | $parameters = array(); 129 | $parameters['x_auth_username'] = $username; 130 | $parameters['x_auth_password'] = $password; 131 | $parameters['x_auth_mode'] = 'client_auth'; 132 | $request = $this->oAuthRequest($this->accessTokenURL(), 'POST', $parameters); 133 | $token = OAuthUtil::parse_parameters($request); 134 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); 135 | return $token; 136 | } 137 | 138 | /** 139 | * GET wrapper for oAuthRequest. 140 | */ 141 | function get($url, $parameters = array()) { 142 | $response = $this->oAuthRequest($url, 'GET', $parameters); 143 | if ($this->format === 'json' && $this->decode_json) { 144 | return json_decode($response); 145 | } 146 | return $response; 147 | } 148 | 149 | /** 150 | * POST wrapper for oAuthRequest. 151 | */ 152 | function post($url, $parameters = array()) { 153 | $response = $this->oAuthRequest($url, 'POST', $parameters); 154 | if ($this->format === 'json' && $this->decode_json) { 155 | return json_decode($response); 156 | } 157 | return $response; 158 | } 159 | 160 | /** 161 | * DELETE wrapper for oAuthReqeust. 162 | */ 163 | function delete($url, $parameters = array()) { 164 | $response = $this->oAuthRequest($url, 'DELETE', $parameters); 165 | if ($this->format === 'json' && $this->decode_json) { 166 | return json_decode($response); 167 | } 168 | return $response; 169 | } 170 | 171 | /** 172 | * Format and sign an OAuth / API request 173 | */ 174 | function oAuthRequest($url, $method, $parameters) { 175 | if (strrpos($url, 'https://') !== 0 && strrpos($url, 'http://') !== 0) { 176 | $url = "{$this->host}{$url}.{$this->format}"; 177 | } 178 | $request = OAuthRequest::from_consumer_and_token($this->consumer, $this->token, $method, $url, $parameters); 179 | $request->sign_request($this->sha1_method, $this->consumer, $this->token); 180 | switch ($method) { 181 | case 'GET': 182 | return $this->http($request->to_url(), 'GET'); 183 | default: 184 | return $this->http($request->get_normalized_http_url(), $method, $request->to_postdata()); 185 | } 186 | } 187 | 188 | 189 | /** 190 | * Format and sign an OAuth / API request (But Not perform it) 191 | */ 192 | function getOAuthRequest($url, $method, $parameters) { 193 | if (strrpos($url, 'https://') !== 0 && strrpos($url, 'http://') !== 0) { 194 | $url = "{$this->host}{$url}.{$this->format}"; 195 | } 196 | $request = OAuthRequest::from_consumer_and_token($this->consumer, $this->token, $method, $url, $parameters); 197 | $request->sign_request($this->sha1_method, $this->consumer, $this->token); 198 | return $request; 199 | } 200 | 201 | /** 202 | * Make an HTTP request 203 | * 204 | * @return API results 205 | */ 206 | function http($url, $method, $postfields = NULL) { 207 | $this->http_info = array(); 208 | $ci = curl_init(); 209 | /* Curl settings */ 210 | curl_setopt($ci, CURLOPT_USERAGENT, $this->useragent); 211 | curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, $this->connecttimeout); 212 | curl_setopt($ci, CURLOPT_TIMEOUT, $this->timeout); 213 | curl_setopt($ci, CURLOPT_RETURNTRANSFER, TRUE); 214 | curl_setopt($ci, CURLOPT_HTTPHEADER, array('Expect:')); 215 | curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, $this->ssl_verifypeer); 216 | curl_setopt($ci, CURLOPT_HEADERFUNCTION, array($this, 'getHeader')); 217 | curl_setopt($ci, CURLOPT_HEADER, FALSE); 218 | 219 | switch ($method) { 220 | case 'POST': 221 | curl_setopt($ci, CURLOPT_POST, TRUE); 222 | if (!empty($postfields)) { 223 | curl_setopt($ci, CURLOPT_POSTFIELDS, $postfields); 224 | } 225 | break; 226 | case 'DELETE': 227 | curl_setopt($ci, CURLOPT_CUSTOMREQUEST, 'DELETE'); 228 | if (!empty($postfields)) { 229 | $url = "{$url}?{$postfields}"; 230 | } 231 | } 232 | 233 | curl_setopt($ci, CURLOPT_URL, $url); 234 | $response = curl_exec($ci); 235 | $this->http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE); 236 | $this->http_info = array_merge($this->http_info, curl_getinfo($ci)); 237 | $this->url = $url; 238 | curl_close ($ci); 239 | return $response; 240 | } 241 | 242 | /** 243 | * Get the header info to store. 244 | */ 245 | function getHeader($ch, $header) { 246 | if (strpos($header, 'Content-Length:') === FALSE) { 247 | header($header); 248 | } 249 | $i = strpos($header, ':'); 250 | if (!empty($i)) { 251 | $key = str_replace('-', '_', strtolower(substr($header, 0, $i))); 252 | $value = trim(substr($header, $i + 2)); 253 | $this->http_header[$key] = $value; 254 | } 255 | return strlen($header); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /twip.php: -------------------------------------------------------------------------------- 1 | entities)){ 15 | return; 16 | } 17 | 18 | $shift=0; 19 | mb_internal_encoding('UTF-8'); 20 | 21 | if(isset($status->entities->urls)){ 22 | foreach($status->entities->urls as &$url){ 23 | if($url->expanded_url){ 24 | $url->indices[0] += $shift; 25 | $url->indices[1] += $shift; 26 | $status->text = mb_substr($status->text, 0, $url->indices[0]) . $url->expanded_url . mb_substr($status->text, $url->indices[1]); 27 | $url->indices[1] = $url->indices[0] + mb_strlen($url->expanded_url); 28 | $diff = mb_strlen($url->expanded_url) - mb_strlen($url->url); 29 | $shift += $diff; 30 | $url->url = $url->expanded_url; 31 | } 32 | } 33 | } 34 | 35 | if(!isset($status->entities->media)){ 36 | return; 37 | } 38 | foreach($status->entities->media as &$media){ 39 | $media->indices[0] += $shift; 40 | $media->indices[1] += $shift; 41 | $status->text = mb_substr($status->text, 0, $media->indices[0]) . $media->media_url_https . mb_substr($status->text, $media->indices[1]); 42 | $media->indices[1] = $media->indices[0] + mb_strlen($media->media_url_https); 43 | $diff = mb_strlen($media->media_url_https) - mb_strlen($media->url); 44 | $shift += $diff; 45 | $media->url = $media->media_url_https; 46 | } 47 | } 48 | 49 | public function json_x86_decode($in){ 50 | $in = preg_replace('/id":(\d+)/', 'id":"\1"', $in); 51 | return json_decode($in); 52 | } 53 | public function json_x86_encode($in){ 54 | $in = json_encode($in); 55 | return preg_replace('/id":"(\d+)"/', 'id":\1', $in); 56 | } 57 | 58 | public function parse_entities($status){ 59 | if($this->o_mode_parse_entities){ 60 | $j = is_string($status) ? $this->json_x86_decode($status) : $status; 61 | if(is_array($j)){ 62 | foreach($j as &$s){ 63 | $s = $this->parse_entities($s); 64 | } 65 | } 66 | else { 67 | $this->replace_tco_json($j); 68 | if(isset($j->status)){ 69 | $this->replace_tco_json($j->status); 70 | } 71 | if(isset($j->retweeted_status)){ 72 | $this->replace_tco_json($j->retweeted_status); 73 | } 74 | if(isset($j->status->retweeted_status)){ 75 | $this->replace_tco_json($j->status->retweeted_status); 76 | } 77 | } 78 | return is_string($status) ? $this->json_x86_encode($j) : $j; 79 | } 80 | return $status; 81 | } 82 | 83 | 84 | 85 | function __construct($options = null){ 86 | $this->parse_variables($options); 87 | 88 | # Import all filters 89 | foreach(glob('filters/*.php') as $f) { 90 | include_once($f); 91 | } 92 | unset($f); 93 | 94 | ob_start(); 95 | $compressed = $this->compress && Extension_Loaded('zlib') && ob_start("ob_gzhandler"); 96 | 97 | if($this->mode=='t'){ 98 | $this->transparent_mode(); 99 | } 100 | else if($this->mode=='o'){ 101 | $this->override_mode(); 102 | } 103 | else if($this->mode=='i'){ 104 | $this->override_mode(true); 105 | } 106 | else{ 107 | header('HTTP/1.0 400 Bad Request'); 108 | exit(); 109 | } 110 | 111 | $str = ob_get_contents(); 112 | if ($compressed) ob_end_flush(); 113 | header('Content-Length: '.ob_get_length()); 114 | ob_flush(); 115 | 116 | if($this->debug){ 117 | print_r($this); 118 | print_r($_SERVER); 119 | file_put_contents('debug',ob_get_contents().$str); 120 | ob_clean(); 121 | } 122 | if($this->dolog){ 123 | file_put_contents('log',$this->method.' '.$this->request_uri."\n",FILE_APPEND); 124 | } 125 | } 126 | 127 | private function parse_variables($options){ 128 | //parse options 129 | $this->parent_api = isset($options['parent_api']) ? $options['parent_api'] : self::PARENT_API; 130 | $this->api_version = isset($options['api_version']) ? $options['api_version'] : self::API_VERSION; 131 | $this->debug = isset($options['debug']) ? !!$options['debug'] : FALSE; 132 | $this->dolog = isset($options['dolog']) ? !!$options['dolog'] : FALSE; 133 | $this->compress = isset($options['compress']) ? !!$options['compress'] : FALSE; 134 | $this->oauth_key = $options['oauth_key']; 135 | $this->oauth_secret = $options['oauth_secret']; 136 | $this->oauth_key_get = $options['oauth_key_get']; 137 | $this->oauth_secret_get = $options['oauth_secret_get']; 138 | $this->o_mode_parse_entities = isset($options['o_mode_parse_entities']) ? !!$options['o_mode_parse_entities'] : FALSE; 139 | 140 | if(substr($this->parent_api, -1) !== '/') $this->parent_api .= '/'; 141 | 142 | $this->base_url = isset($options['base_url']) ? trim($options['base_url'],'/').'/' : self::BASE_URL; 143 | if(preg_match('/^https?:\/\//i',$this->base_url) == 0){ 144 | $this->base_url = 'http://'.$this->base_url; 145 | } 146 | 147 | //parse $_SERVER 148 | $this->method = $_SERVER['REQUEST_METHOD']; 149 | 150 | 151 | $this->parse_request_uri(); 152 | } 153 | 154 | private function override_mode($imageproxy = FALSE){ 155 | $tokenfile = glob('oauth/'.$this->password.'.*'); 156 | if(!empty($tokenfile)){ 157 | $access_token = @file_get_contents($tokenfile[0]); 158 | } 159 | if(empty($access_token)){ 160 | header('HTTP/1.1 401 Unauthorized'); 161 | header('WWW-Authenticate: Basic realm="Twip4 Override Mode"'); 162 | echo 'You are not allowed to use this API proxy'; 163 | exit(); 164 | } 165 | $access_token = unserialize($access_token); 166 | $this->access_token = $access_token; 167 | $this->has_get_token = isset($access_token['oauth_token_get']); 168 | 169 | if($imageproxy){ 170 | if($this->method=='POST'){ 171 | echo imageUpload($this->oauth_key, $this->oauth_secret, $this->access_token); 172 | }else{ 173 | echo 'The image proxy needs POST method.'; 174 | } 175 | return; 176 | } 177 | 178 | if($this->request_uri == null){ 179 | echo 'click HERE to get your API url'; 180 | return; 181 | } 182 | $this->parameters = $this->get_parameters(); 183 | foreach(array('pc', 'earned') as $param) { 184 | unset($this->parameters[$param]); 185 | } 186 | $this->connection = new TwitterOAuth($this->oauth_key, $this->oauth_secret, $this->access_token['oauth_token'], $this->access_token['oauth_token_secret']); 187 | $this->connection_get = $this->has_get_token ? new TwitterOAuth($this->oauth_key_get, $this->oauth_secret_get, $this->access_token['oauth_token_get'], $this->access_token['oauth_token_secret_get']) : $this->connection; 188 | 189 | $filterName = Twip::encode_uri($this->forwarded_request_uri); 190 | if (!array_key_exists($filterName, $this->filters)) { 191 | $filterName = '_default'; 192 | } 193 | $parts = parse_url($this->forwarded_request_uri); 194 | $raw_response = $this->filters[$filterName](array( 195 | 'path' => $parts['path'], 196 | 'method' => $this->method, 197 | 'params' => $this->parameters, 198 | 'self' => $this, 199 | )); 200 | echo $this->parse_entities($raw_response); 201 | return; 202 | } 203 | 204 | private function transparent_mode(){ 205 | $this->uri_fixer(); 206 | $ch = curl_init($this->request_uri); 207 | $this->request_headers = OAuthUtil::get_headers(); 208 | 209 | // Don't parse POST arguments as array if emulating a browser submit 210 | if(isset($this->request_headers['Content-Type']) && 211 | strpos($this->request_headers['Content-Type'], 'application/x-www-form-urlencoded') !== FALSE){ 212 | $this->parameters = $this->get_parameters(false); 213 | }else{ 214 | $this->parameters = $this->get_parameters(true); 215 | } 216 | 217 | // Process Upload image (currently only first file will proxy to Twitter) 218 | if(strpos($this->request_uri,'statuses/update_with_media') !== FALSE && 219 | strpos(@$this->request_headers['Content-Type'], 'multipart/form-data') !== FALSE) { 220 | 221 | $this->parameters = preg_replace('/^@/', "\0@", $_POST); 222 | if(count($_FILES) > 0 && isset($_FILES['media'])) { 223 | $media = $_FILES['media']; 224 | $fn = is_array($media['tmp_name']) ? $media['tmp_name'][0] : $media['tmp_name']; 225 | $this->parameters["media[]"] = '@' . $fn; 226 | unset($this->request_headers['Content-Type']); 227 | } 228 | } 229 | 230 | $forwarded_headers = array( 231 | 'User-Agent', 232 | 'Authorization', 233 | 'Content-Type', 234 | 'X-Forwarded-For', 235 | 'Expect', 236 | ); 237 | foreach($forwarded_headers as $header){ 238 | if(isset($this->request_headers[$header])){ 239 | $this->forwarded_headers[] = $header.': '.$this->request_headers[$header]; 240 | } 241 | } 242 | if(!isset($this->forwarded_headers['Expect'])) $this->forwarded_headers[] = 'Expect:'; 243 | curl_setopt($ch,CURLOPT_HTTPHEADER,$this->forwarded_headers); 244 | curl_setopt($ch,CURLOPT_HEADERFUNCTION,array($this,'headerfunction')); 245 | if($this->method != 'GET'){ 246 | curl_setopt($ch,CURLOPT_CUSTOMREQUEST,$this->method); 247 | curl_setopt($ch,CURLOPT_POSTFIELDS,$this->parameters); 248 | } 249 | curl_setopt($ch,CURLOPT_RETURNTRANSFER,TRUE); 250 | $ret = curl_exec($ch); 251 | //fixme:redirect request back to twip,this is nasty and insecure... 252 | if(strpos($this->request_uri,'oauth/authorize?oauth_token=')!==FALSE){ 253 | $ret = str_replace('
','

Warning!This page is proxied by twip and therefore you may leak your password to API proxy owner!

',$ret); 255 | } 256 | echo $ret; 257 | } 258 | 259 | private function uri_fixer(){ 260 | // $api is the API request without version number 261 | list($version, $api) = $this->extract_uri_version($this->request_uri); 262 | 263 | // If user specified version, use that version. Else use default version 264 | $version = ($version == "") ? $this->api_version : $version; 265 | 266 | $this->request_headers['Host'] = 'api.twitter.com'; 267 | 268 | if($version === "1") { 269 | header("HTTP/1.0 410 Gone"); 270 | die(); 271 | } 272 | 273 | $replacement = array( 274 | 'pc=true' => 'pc=false', //change pc=true to pc=false 275 | '&earned=true' => '', //remove "&earned=true" 276 | ); 277 | 278 | $api = str_replace(array_keys($replacement), array_values($replacement), $api); 279 | 280 | if( strpos($api,'oauth/') === 0 ) { 281 | // These API requests don't needs version string 282 | $this->request_uri = sprintf("%s%s", $this->parent_api, $api); 283 | }else{ 284 | $this->request_uri = sprintf("%s%s/%s", $this->parent_api, $version, $api); 285 | } 286 | } 287 | 288 | public function extract_uri_version($uri){ 289 | $re = '/^(([0-9.]+)\/)?(.*)/'; 290 | 291 | preg_match($re, $uri, $matches); 292 | 293 | $version = $matches[2]; 294 | $api = $matches[3]; 295 | return array($version, $api); 296 | } 297 | 298 | private function parse_request_uri(){ 299 | // old value 300 | //$full_request_uri = substr($_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'],strlen(preg_replace('/^https?:\/\//i','',$this->base_url))); 301 | $full_request_uri = substr( 302 | $_SERVER['REQUEST_URI'], 303 | strlen(dirname($_SERVER['SCRIPT_NAME']))); 304 | 305 | $prefix = substr($full_request_uri, 0, 3); 306 | switch($prefix) { 307 | case '/o/': 308 | // full_request_uri: /o/PASSWORD/1.1/xxx/xxx.json 309 | $this->mode = 'o'; 310 | list($this->password, $forwarded_request_uri) = explode('/', substr($full_request_uri, 3), 2); 311 | $this->forwarded_request_uri = $this->request_uri = $forwarded_request_uri; 312 | break; 313 | case '/i/': 314 | $this->mode = 'i'; 315 | // full_request_uri: /i/????? 316 | // does this mode need this parsing anyway? @yegle 317 | list($this->password, $forwarded_request_uri) = explode('/', substr($full_request_uri, 3), 2); 318 | $this->forwarded_request_uri = $this->request_uri = $forwarded_request_uri; 319 | break; 320 | case '/t/': 321 | // full_request_uri: /t/1.1/xxx/xxx.json 322 | $this->mode = 't'; 323 | $this->request_uri = substr($full_request_uri, 3); 324 | break; 325 | default: 326 | $this->mode = 'UNKNOWN'; 327 | break; 328 | } 329 | } 330 | 331 | private function headerfunction($ch,$str){ 332 | if(strpos($str,'Content-Length:')!==FALSE){ 333 | header($str); 334 | } 335 | $this->response_headers[] = $str; 336 | return strlen($str); 337 | } 338 | 339 | private function get_parameters($returnArray = TRUE){ 340 | if($returnArray) { 341 | return $_REQUEST; 342 | } 343 | else { 344 | return http_build_query($_REQUEST); 345 | } 346 | } 347 | 348 | public static function encode_uri($raw_uri) { 349 | $parts = parse_url($raw_uri); 350 | $path = $parts['path']; 351 | $replacements = array( 352 | '/' => '__', 353 | '.' => '_', 354 | ); 355 | $regex_replacements = array( 356 | '/\d{2,}/' => 'NUMBER', 357 | ); 358 | 359 | $path = str_replace( 360 | array_keys($replacements), 361 | array_values($replacements), 362 | $path 363 | ); 364 | $path = preg_replace( 365 | array_keys($regex_replacements), 366 | array_values($regex_replacements), 367 | $path 368 | ); 369 | return $path; 370 | } 371 | } 372 | ?> 373 | -------------------------------------------------------------------------------- /include/OAuth.php: -------------------------------------------------------------------------------- 1 | key = $key; 18 | $this->secret = $secret; 19 | $this->callback_url = $callback_url; 20 | } 21 | 22 | function __toString() { 23 | return "OAuthConsumer[key=$this->key,secret=$this->secret]"; 24 | } 25 | } 26 | 27 | class OAuthToken { 28 | // access tokens and request tokens 29 | public $key; 30 | public $secret; 31 | 32 | /** 33 | * key = the token 34 | * secret = the token secret 35 | */ 36 | function __construct($key, $secret) { 37 | $this->key = $key; 38 | $this->secret = $secret; 39 | } 40 | 41 | /** 42 | * generates the basic string serialization of a token that a server 43 | * would respond to request_token and access_token calls with 44 | */ 45 | function to_string() { 46 | return "oauth_token=" . 47 | OAuthUtil::urlencode_rfc3986($this->key) . 48 | "&oauth_token_secret=" . 49 | OAuthUtil::urlencode_rfc3986($this->secret); 50 | } 51 | 52 | function __toString() { 53 | return $this->to_string(); 54 | } 55 | } 56 | 57 | /** 58 | * A class for implementing a Signature Method 59 | * See section 9 ("Signing Requests") in the spec 60 | */ 61 | abstract class OAuthSignatureMethod { 62 | /** 63 | * Needs to return the name of the Signature Method (ie HMAC-SHA1) 64 | * @return string 65 | */ 66 | abstract public function get_name(); 67 | 68 | /** 69 | * Build up the signature 70 | * NOTE: The output of this function MUST NOT be urlencoded. 71 | * the encoding is handled in OAuthRequest when the final 72 | * request is serialized 73 | * @param OAuthRequest $request 74 | * @param OAuthConsumer $consumer 75 | * @param OAuthToken $token 76 | * @return string 77 | */ 78 | abstract public function build_signature($request, $consumer, $token); 79 | 80 | /** 81 | * Verifies that a given signature is correct 82 | * @param OAuthRequest $request 83 | * @param OAuthConsumer $consumer 84 | * @param OAuthToken $token 85 | * @param string $signature 86 | * @return bool 87 | */ 88 | public function check_signature($request, $consumer, $token, $signature) { 89 | $built = $this->build_signature($request, $consumer, $token); 90 | return $built == $signature; 91 | } 92 | } 93 | 94 | /** 95 | * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104] 96 | * where the Signature Base String is the text and the key is the concatenated values (each first 97 | * encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&' 98 | * character (ASCII code 38) even if empty. 99 | * - Chapter 9.2 ("HMAC-SHA1") 100 | */ 101 | class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod { 102 | function get_name() { 103 | return "HMAC-SHA1"; 104 | } 105 | 106 | public function build_signature($request, $consumer, $token) { 107 | $base_string = $request->get_signature_base_string(); 108 | $request->base_string = $base_string; 109 | 110 | $key_parts = array( 111 | $consumer->secret, 112 | ($token) ? $token->secret : "" 113 | ); 114 | 115 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); 116 | $key = implode('&', $key_parts); 117 | 118 | return base64_encode(hash_hmac('sha1', $base_string, $key, true)); 119 | } 120 | } 121 | 122 | /** 123 | * The PLAINTEXT method does not provide any security protection and SHOULD only be used 124 | * over a secure channel such as HTTPS. It does not use the Signature Base String. 125 | * - Chapter 9.4 ("PLAINTEXT") 126 | */ 127 | class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod { 128 | public function get_name() { 129 | return "PLAINTEXT"; 130 | } 131 | 132 | /** 133 | * oauth_signature is set to the concatenated encoded values of the Consumer Secret and 134 | * Token Secret, separated by a '&' character (ASCII code 38), even if either secret is 135 | * empty. The result MUST be encoded again. 136 | * - Chapter 9.4.1 ("Generating Signatures") 137 | * 138 | * Please note that the second encoding MUST NOT happen in the SignatureMethod, as 139 | * OAuthRequest handles this! 140 | */ 141 | public function build_signature($request, $consumer, $token) { 142 | $key_parts = array( 143 | $consumer->secret, 144 | ($token) ? $token->secret : "" 145 | ); 146 | 147 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); 148 | $key = implode('&', $key_parts); 149 | $request->base_string = $key; 150 | 151 | return $key; 152 | } 153 | } 154 | 155 | /** 156 | * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in 157 | * [RFC3447] section 8.2 (more simply known as PKCS#1), using SHA-1 as the hash function for 158 | * EMSA-PKCS1-v1_5. It is assumed that the Consumer has provided its RSA public key in a 159 | * verified way to the Service Provider, in a manner which is beyond the scope of this 160 | * specification. 161 | * - Chapter 9.3 ("RSA-SHA1") 162 | */ 163 | abstract class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod { 164 | public function get_name() { 165 | return "RSA-SHA1"; 166 | } 167 | 168 | // Up to the SP to implement this lookup of keys. Possible ideas are: 169 | // (1) do a lookup in a table of trusted certs keyed off of consumer 170 | // (2) fetch via http using a url provided by the requester 171 | // (3) some sort of specific discovery code based on request 172 | // 173 | // Either way should return a string representation of the certificate 174 | protected abstract function fetch_public_cert(&$request); 175 | 176 | // Up to the SP to implement this lookup of keys. Possible ideas are: 177 | // (1) do a lookup in a table of trusted certs keyed off of consumer 178 | // 179 | // Either way should return a string representation of the certificate 180 | protected abstract function fetch_private_cert(&$request); 181 | 182 | public function build_signature($request, $consumer, $token) { 183 | $base_string = $request->get_signature_base_string(); 184 | $request->base_string = $base_string; 185 | 186 | // Fetch the private key cert based on the request 187 | $cert = $this->fetch_private_cert($request); 188 | 189 | // Pull the private key ID from the certificate 190 | $privatekeyid = openssl_get_privatekey($cert); 191 | 192 | // Sign using the key 193 | $ok = openssl_sign($base_string, $signature, $privatekeyid); 194 | 195 | // Release the key resource 196 | openssl_free_key($privatekeyid); 197 | 198 | return base64_encode($signature); 199 | } 200 | 201 | public function check_signature($request, $consumer, $token, $signature) { 202 | $decoded_sig = base64_decode($signature); 203 | 204 | $base_string = $request->get_signature_base_string(); 205 | 206 | // Fetch the public key cert based on the request 207 | $cert = $this->fetch_public_cert($request); 208 | 209 | // Pull the public key ID from the certificate 210 | $publickeyid = openssl_get_publickey($cert); 211 | 212 | // Check the computed signature against the one passed in the query 213 | $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); 214 | 215 | // Release the key resource 216 | openssl_free_key($publickeyid); 217 | 218 | return $ok == 1; 219 | } 220 | } 221 | 222 | class OAuthRequest { 223 | private $parameters; 224 | private $http_method; 225 | private $http_url; 226 | // for debug purposes 227 | public $base_string; 228 | public static $version = '1.0'; 229 | public static $POST_INPUT = 'php://input'; 230 | 231 | function __construct($http_method, $http_url, $parameters=NULL) { 232 | @$parameters or $parameters = array(); 233 | $parameters = array_merge( OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters); 234 | $this->parameters = $parameters; 235 | $this->http_method = $http_method; 236 | $this->http_url = $http_url; 237 | } 238 | 239 | 240 | /** 241 | * attempt to build up a request from what was passed to the server 242 | */ 243 | public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) { 244 | $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") 245 | ? 'http' 246 | : 'https'; 247 | @$http_url or $http_url = $scheme . 248 | '://' . $_SERVER['HTTP_HOST'] . 249 | ':' . 250 | $_SERVER['SERVER_PORT'] . 251 | $_SERVER['REQUEST_URI']; 252 | @$http_method or $http_method = $_SERVER['REQUEST_METHOD']; 253 | 254 | // We weren't handed any parameters, so let's find the ones relevant to 255 | // this request. 256 | // If you run XML-RPC or similar you should use this to provide your own 257 | // parsed parameter-list 258 | if (!$parameters) { 259 | // Find request headers 260 | $request_headers = OAuthUtil::get_headers(); 261 | 262 | // Parse the query-string to find GET parameters 263 | $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']); 264 | 265 | // It's a POST request of the proper content-type, so parse POST 266 | // parameters and add those overriding any duplicates from GET 267 | if ($http_method == "POST" 268 | && @strstr($request_headers["Content-Type"], 269 | "application/x-www-form-urlencoded") 270 | ) { 271 | $post_data = OAuthUtil::parse_parameters( 272 | file_get_contents(self::$POST_INPUT) 273 | ); 274 | $parameters = array_merge($parameters, $post_data); 275 | } 276 | 277 | // We have a Authorization-header with OAuth data. Parse the header 278 | // and add those overriding any duplicates from GET or POST 279 | if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") { 280 | $header_parameters = OAuthUtil::split_header( 281 | $request_headers['Authorization'] 282 | ); 283 | $parameters = array_merge($parameters, $header_parameters); 284 | } 285 | 286 | } 287 | 288 | return new OAuthRequest($http_method, $http_url, $parameters); 289 | } 290 | 291 | /** 292 | * pretty much a helper function to set up the request 293 | */ 294 | public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) { 295 | @$parameters or $parameters = array(); 296 | $defaults = array("oauth_version" => OAuthRequest::$version, 297 | "oauth_nonce" => OAuthRequest::generate_nonce(), 298 | "oauth_timestamp" => OAuthRequest::generate_timestamp(), 299 | "oauth_consumer_key" => $consumer->key); 300 | if ($token) 301 | $defaults['oauth_token'] = $token->key; 302 | 303 | $parameters = array_merge($parameters, $defaults); 304 | 305 | return new OAuthRequest($http_method, $http_url, $parameters); 306 | } 307 | 308 | public function set_parameter($name, $value, $allow_duplicates = true) { 309 | if ($allow_duplicates && isset($this->parameters[$name])) { 310 | // We have already added parameter(s) with this name, so add to the list 311 | if (is_scalar($this->parameters[$name])) { 312 | // This is the first duplicate, so transform scalar (string) 313 | // into an array so we can add the duplicates 314 | $this->parameters[$name] = array($this->parameters[$name]); 315 | } 316 | 317 | $this->parameters[$name][] = $value; 318 | } else { 319 | $this->parameters[$name] = $value; 320 | } 321 | } 322 | 323 | public function get_parameter($name) { 324 | return isset($this->parameters[$name]) ? $this->parameters[$name] : null; 325 | } 326 | 327 | public function get_parameters() { 328 | return $this->parameters; 329 | } 330 | 331 | public function unset_parameter($name) { 332 | unset($this->parameters[$name]); 333 | } 334 | 335 | /** 336 | * The request parameters, sorted and concatenated into a normalized string. 337 | * @return string 338 | */ 339 | public function get_signable_parameters() { 340 | // Grab all parameters 341 | $params = $this->parameters; 342 | 343 | // Remove oauth_signature if present 344 | // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") 345 | if (isset($params['oauth_signature'])) { 346 | unset($params['oauth_signature']); 347 | } 348 | 349 | return OAuthUtil::build_http_query($params); 350 | } 351 | 352 | /** 353 | * Returns the base string of this request 354 | * 355 | * The base string defined as the method, the url 356 | * and the parameters (normalized), each urlencoded 357 | * and the concated with &. 358 | */ 359 | public function get_signature_base_string() { 360 | $parts = array( 361 | $this->get_normalized_http_method(), 362 | $this->get_normalized_http_url(), 363 | $this->get_signable_parameters() 364 | ); 365 | 366 | $parts = OAuthUtil::urlencode_rfc3986($parts); 367 | 368 | return implode('&', $parts); 369 | } 370 | 371 | /** 372 | * just uppercases the http method 373 | */ 374 | public function get_normalized_http_method() { 375 | return strtoupper($this->http_method); 376 | } 377 | 378 | /** 379 | * parses the url and rebuilds it to be 380 | * scheme://host/path 381 | */ 382 | public function get_normalized_http_url() { 383 | $parts = parse_url($this->http_url); 384 | 385 | $port = @$parts['port']; 386 | $scheme = $parts['scheme']; 387 | $host = $parts['host']; 388 | $path = @$parts['path']; 389 | 390 | $port or $port = ($scheme == 'https') ? '443' : '80'; 391 | 392 | if (($scheme == 'https' && $port != '443') 393 | || ($scheme == 'http' && $port != '80')) { 394 | $host = "$host:$port"; 395 | } 396 | return "$scheme://$host$path"; 397 | } 398 | 399 | /** 400 | * builds a url usable for a GET request 401 | */ 402 | public function to_url() { 403 | $post_data = $this->to_postdata(); 404 | $out = $this->get_normalized_http_url(); 405 | if ($post_data) { 406 | $out .= '?'.$post_data; 407 | } 408 | return $out; 409 | } 410 | 411 | /** 412 | * builds the data one would send in a POST request 413 | */ 414 | public function to_postdata() { 415 | return OAuthUtil::build_http_query($this->parameters); 416 | } 417 | 418 | /** 419 | * builds the Authorization: header 420 | */ 421 | public function to_header($realm=null) { 422 | $first = true; 423 | if($realm) { 424 | $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"'; 425 | $first = false; 426 | } else 427 | $out = 'Authorization: OAuth'; 428 | 429 | $total = array(); 430 | foreach ($this->parameters as $k => $v) { 431 | if (substr($k, 0, 5) != "oauth") continue; 432 | if (is_array($v)) { 433 | throw new OAuthException('Arrays not supported in headers'); 434 | } 435 | $out .= ($first) ? ' ' : ','; 436 | $out .= OAuthUtil::urlencode_rfc3986($k) . 437 | '="' . 438 | OAuthUtil::urlencode_rfc3986($v) . 439 | '"'; 440 | $first = false; 441 | } 442 | return $out; 443 | } 444 | 445 | public function __toString() { 446 | return $this->to_url(); 447 | } 448 | 449 | 450 | public function sign_request($signature_method, $consumer, $token) { 451 | $this->set_parameter( 452 | "oauth_signature_method", 453 | $signature_method->get_name(), 454 | false 455 | ); 456 | $signature = $this->build_signature($signature_method, $consumer, $token); 457 | $this->set_parameter("oauth_signature", $signature, false); 458 | } 459 | 460 | public function build_signature($signature_method, $consumer, $token) { 461 | $signature = $signature_method->build_signature($this, $consumer, $token); 462 | return $signature; 463 | } 464 | 465 | /** 466 | * util function: current timestamp 467 | */ 468 | private static function generate_timestamp() { 469 | return time(); 470 | } 471 | 472 | /** 473 | * util function: current nonce 474 | */ 475 | private static function generate_nonce() { 476 | $mt = microtime(); 477 | $rand = mt_rand(); 478 | 479 | return md5($mt . $rand); // md5s look nicer than numbers 480 | } 481 | } 482 | 483 | class OAuthServer { 484 | protected $timestamp_threshold = 300; // in seconds, five minutes 485 | protected $version = '1.0'; // hi blaine 486 | protected $signature_methods = array(); 487 | 488 | protected $data_store; 489 | 490 | function __construct($data_store) { 491 | $this->data_store = $data_store; 492 | } 493 | 494 | public function add_signature_method($signature_method) { 495 | $this->signature_methods[$signature_method->get_name()] = 496 | $signature_method; 497 | } 498 | 499 | // high level functions 500 | 501 | /** 502 | * process a request_token request 503 | * returns the request token on success 504 | */ 505 | public function fetch_request_token(&$request) { 506 | $this->get_version($request); 507 | 508 | $consumer = $this->get_consumer($request); 509 | 510 | // no token required for the initial token request 511 | $token = NULL; 512 | 513 | $this->check_signature($request, $consumer, $token); 514 | 515 | // Rev A change 516 | $callback = $request->get_parameter('oauth_callback'); 517 | $new_token = $this->data_store->new_request_token($consumer, $callback); 518 | 519 | return $new_token; 520 | } 521 | 522 | /** 523 | * process an access_token request 524 | * returns the access token on success 525 | */ 526 | public function fetch_access_token(&$request) { 527 | $this->get_version($request); 528 | 529 | $consumer = $this->get_consumer($request); 530 | 531 | // requires authorized request token 532 | $token = $this->get_token($request, $consumer, "request"); 533 | 534 | $this->check_signature($request, $consumer, $token); 535 | 536 | // Rev A change 537 | $verifier = $request->get_parameter('oauth_verifier'); 538 | $new_token = $this->data_store->new_access_token($token, $consumer, $verifier); 539 | 540 | return $new_token; 541 | } 542 | 543 | /** 544 | * verify an api call, checks all the parameters 545 | */ 546 | public function verify_request(&$request) { 547 | $this->get_version($request); 548 | $consumer = $this->get_consumer($request); 549 | $token = $this->get_token($request, $consumer, "access"); 550 | $this->check_signature($request, $consumer, $token); 551 | return array($consumer, $token); 552 | } 553 | 554 | // Internals from here 555 | /** 556 | * version 1 557 | */ 558 | private function get_version(&$request) { 559 | $version = $request->get_parameter("oauth_version"); 560 | if (!$version) { 561 | // Service Providers MUST assume the protocol version to be 1.0 if this parameter is not present. 562 | // Chapter 7.0 ("Accessing Protected Ressources") 563 | $version = '1.0'; 564 | } 565 | if ($version !== $this->version) { 566 | throw new OAuthException("OAuth version '$version' not supported"); 567 | } 568 | return $version; 569 | } 570 | 571 | /** 572 | * figure out the signature with some defaults 573 | */ 574 | private function get_signature_method(&$request) { 575 | $signature_method = 576 | @$request->get_parameter("oauth_signature_method"); 577 | 578 | if (!$signature_method) { 579 | // According to chapter 7 ("Accessing Protected Ressources") the signature-method 580 | // parameter is required, and we can't just fallback to PLAINTEXT 581 | throw new OAuthException('No signature method parameter. This parameter is required'); 582 | } 583 | 584 | if (!in_array($signature_method, 585 | array_keys($this->signature_methods))) { 586 | throw new OAuthException( 587 | "Signature method '$signature_method' not supported " . 588 | "try one of the following: " . 589 | implode(", ", array_keys($this->signature_methods)) 590 | ); 591 | } 592 | return $this->signature_methods[$signature_method]; 593 | } 594 | 595 | /** 596 | * try to find the consumer for the provided request's consumer key 597 | */ 598 | private function get_consumer(&$request) { 599 | $consumer_key = @$request->get_parameter("oauth_consumer_key"); 600 | if (!$consumer_key) { 601 | throw new OAuthException("Invalid consumer key"); 602 | } 603 | 604 | $consumer = $this->data_store->lookup_consumer($consumer_key); 605 | if (!$consumer) { 606 | throw new OAuthException("Invalid consumer"); 607 | } 608 | 609 | return $consumer; 610 | } 611 | 612 | /** 613 | * try to find the token for the provided request's token key 614 | */ 615 | private function get_token(&$request, $consumer, $token_type="access") { 616 | $token_field = @$request->get_parameter('oauth_token'); 617 | $token = $this->data_store->lookup_token( 618 | $consumer, $token_type, $token_field 619 | ); 620 | if (!$token) { 621 | throw new OAuthException("Invalid $token_type token: $token_field"); 622 | } 623 | return $token; 624 | } 625 | 626 | /** 627 | * all-in-one function to check the signature on a request 628 | * should guess the signature method appropriately 629 | */ 630 | private function check_signature(&$request, $consumer, $token) { 631 | // this should probably be in a different method 632 | $timestamp = @$request->get_parameter('oauth_timestamp'); 633 | $nonce = @$request->get_parameter('oauth_nonce'); 634 | 635 | $this->check_timestamp($timestamp); 636 | $this->check_nonce($consumer, $token, $nonce, $timestamp); 637 | 638 | $signature_method = $this->get_signature_method($request); 639 | 640 | $signature = $request->get_parameter('oauth_signature'); 641 | $valid_sig = $signature_method->check_signature( 642 | $request, 643 | $consumer, 644 | $token, 645 | $signature 646 | ); 647 | 648 | if (!$valid_sig) { 649 | throw new OAuthException("Invalid signature"); 650 | } 651 | } 652 | 653 | /** 654 | * check that the timestamp is new enough 655 | */ 656 | private function check_timestamp($timestamp) { 657 | if( ! $timestamp ) 658 | throw new OAuthException( 659 | 'Missing timestamp parameter. The parameter is required' 660 | ); 661 | 662 | // verify that timestamp is recentish 663 | $now = time(); 664 | if (abs($now - $timestamp) > $this->timestamp_threshold) { 665 | throw new OAuthException( 666 | "Expired timestamp, yours $timestamp, ours $now" 667 | ); 668 | } 669 | } 670 | 671 | /** 672 | * check that the nonce is not repeated 673 | */ 674 | private function check_nonce($consumer, $token, $nonce, $timestamp) { 675 | if( ! $nonce ) 676 | throw new OAuthException( 677 | 'Missing nonce parameter. The parameter is required' 678 | ); 679 | 680 | // verify that the nonce is uniqueish 681 | $found = $this->data_store->lookup_nonce( 682 | $consumer, 683 | $token, 684 | $nonce, 685 | $timestamp 686 | ); 687 | if ($found) { 688 | throw new OAuthException("Nonce already used: $nonce"); 689 | } 690 | } 691 | 692 | } 693 | 694 | class OAuthDataStore { 695 | function lookup_consumer($consumer_key) { 696 | // implement me 697 | } 698 | 699 | function lookup_token($consumer, $token_type, $token) { 700 | // implement me 701 | } 702 | 703 | function lookup_nonce($consumer, $token, $nonce, $timestamp) { 704 | // implement me 705 | } 706 | 707 | function new_request_token($consumer, $callback = null) { 708 | // return a new token attached to this consumer 709 | } 710 | 711 | function new_access_token($token, $consumer, $verifier = null) { 712 | // return a new access token attached to this consumer 713 | // for the user associated with this token if the request token 714 | // is authorized 715 | // should also invalidate the request token 716 | } 717 | 718 | } 719 | 720 | class OAuthUtil { 721 | public static function urlencode_rfc3986($input) { 722 | if (is_array($input)) { 723 | return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input); 724 | } else if (is_scalar($input)) { 725 | return str_replace( 726 | '+', 727 | ' ', 728 | str_replace('%7E', '~', rawurlencode($input)) 729 | ); 730 | } else { 731 | return ''; 732 | } 733 | } 734 | 735 | 736 | // This decode function isn't taking into consideration the above 737 | // modifications to the encoding process. However, this method doesn't 738 | // seem to be used anywhere so leaving it as is. 739 | public static function urldecode_rfc3986($string) { 740 | return urldecode($string); 741 | } 742 | 743 | // Utility function for turning the Authorization: header into 744 | // parameters, has to do some unescaping 745 | // Can filter out any non-oauth parameters if needed (default behaviour) 746 | public static function split_header($header, $only_allow_oauth_parameters = true) { 747 | $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/'; 748 | $offset = 0; 749 | $params = array(); 750 | while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) { 751 | $match = $matches[0]; 752 | $header_name = $matches[2][0]; 753 | $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0]; 754 | if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) { 755 | $params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content); 756 | } 757 | $offset = $match[1] + strlen($match[0]); 758 | } 759 | 760 | if (isset($params['realm'])) { 761 | unset($params['realm']); 762 | } 763 | 764 | return $params; 765 | } 766 | 767 | // helper to try to sort out headers for people who aren't running apache 768 | public static function get_headers() { 769 | if (function_exists('apache_request_headers')) { 770 | // we need this to get the actual Authorization: header 771 | // because apache tends to tell us it doesn't exist 772 | $headers = apache_request_headers(); 773 | 774 | // sanitize the output of apache_request_headers because 775 | // we always want the keys to be Cased-Like-This and arh() 776 | // returns the headers in the same case as they are in the 777 | // request 778 | $out = array(); 779 | foreach( $headers AS $key => $value ) { 780 | $key = str_replace( 781 | " ", 782 | "-", 783 | ucwords(strtolower(str_replace("-", " ", $key))) 784 | ); 785 | $out[$key] = $value; 786 | } 787 | } else { 788 | // otherwise we don't have apache and are just going to have to hope 789 | // that $_SERVER actually contains what we need 790 | $out = array(); 791 | if( isset($_SERVER['CONTENT_TYPE']) ) 792 | $out['Content-Type'] = $_SERVER['CONTENT_TYPE']; 793 | if( isset($_ENV['CONTENT_TYPE']) ) 794 | $out['Content-Type'] = $_ENV['CONTENT_TYPE']; 795 | 796 | foreach ($_SERVER as $key => $value) { 797 | if (substr($key, 0, 5) == "HTTP_") { 798 | // this is chaos, basically it is just there to capitalize the first 799 | // letter of every word that is not an initial HTTP and strip HTTP 800 | // code from przemek 801 | $key = str_replace( 802 | " ", 803 | "-", 804 | ucwords(strtolower(str_replace("_", " ", substr($key, 5)))) 805 | ); 806 | $out[$key] = $value; 807 | } 808 | } 809 | } 810 | return $out; 811 | } 812 | 813 | // This function takes a input like a=b&a=c&d=e and returns the parsed 814 | // parameters like this 815 | // array('a' => array('b','c'), 'd' => 'e') 816 | public static function parse_parameters( $input ) { 817 | if (!isset($input) || !$input) return array(); 818 | 819 | $pairs = explode('&', $input); 820 | 821 | $parsed_parameters = array(); 822 | foreach ($pairs as $pair) { 823 | $split = explode('=', $pair, 2); 824 | $parameter = OAuthUtil::urldecode_rfc3986($split[0]); 825 | $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : ''; 826 | 827 | if (isset($parsed_parameters[$parameter])) { 828 | // We have already recieved parameter(s) with this name, so add to the list 829 | // of parameters with this name 830 | 831 | if (is_scalar($parsed_parameters[$parameter])) { 832 | // This is the first duplicate, so transform scalar (string) into an array 833 | // so we can add the duplicates 834 | $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]); 835 | } 836 | 837 | $parsed_parameters[$parameter][] = $value; 838 | } else { 839 | $parsed_parameters[$parameter] = $value; 840 | } 841 | } 842 | return $parsed_parameters; 843 | } 844 | 845 | public static function build_http_query($params) { 846 | if (!$params) return ''; 847 | 848 | // Urlencode both keys and values 849 | $keys = OAuthUtil::urlencode_rfc3986(array_keys($params)); 850 | $values = OAuthUtil::urlencode_rfc3986(array_values($params)); 851 | $params = array_combine($keys, $values); 852 | 853 | // Parameters are sorted by name, using lexicographical byte value ordering. 854 | // Ref: Spec: 9.1.1 (1) 855 | uksort($params, 'strcmp'); 856 | 857 | $pairs = array(); 858 | foreach ($params as $parameter => $value) { 859 | if (is_array($value)) { 860 | // If two or more parameters share the same name, they are sorted by their value 861 | // Ref: Spec: 9.1.1 (1) 862 | natsort($value); 863 | foreach ($value as $duplicate_value) { 864 | $pairs[] = $parameter . '=' . $duplicate_value; 865 | } 866 | } else { 867 | $pairs[] = $parameter . '=' . $value; 868 | } 869 | } 870 | // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) 871 | // Each name-value pair is separated by an '&' character (ASCII code 38) 872 | return implode('&', $pairs); 873 | } 874 | } 875 | -------------------------------------------------------------------------------- /include/simple_html_dom.php: -------------------------------------------------------------------------------- 1 | 6 | Acknowledge: Jose Solorzano (https://sourceforge.net/projects/php-html/) 7 | Contributions by: 8 | Yousuke Kumakura (Attribute filters) 9 | Vadim Voituk (Negative indexes supports of "find" method) 10 | Antcs (Constructor with automatically load contents either text or file/url) 11 | Licensed under The MIT License 12 | Redistributions of files must retain the above copyright notice. 13 | *******************************************************************************/ 14 | 15 | define('HDOM_TYPE_ELEMENT', 1); 16 | define('HDOM_TYPE_COMMENT', 2); 17 | define('HDOM_TYPE_TEXT', 3); 18 | define('HDOM_TYPE_ENDTAG', 4); 19 | define('HDOM_TYPE_ROOT', 5); 20 | define('HDOM_TYPE_UNKNOWN', 6); 21 | define('HDOM_QUOTE_DOUBLE', 0); 22 | define('HDOM_QUOTE_SINGLE', 1); 23 | define('HDOM_QUOTE_NO', 3); 24 | define('HDOM_INFO_BEGIN', 0); 25 | define('HDOM_INFO_END', 1); 26 | define('HDOM_INFO_QUOTE', 2); 27 | define('HDOM_INFO_SPACE', 3); 28 | define('HDOM_INFO_TEXT', 4); 29 | define('HDOM_INFO_INNER', 5); 30 | define('HDOM_INFO_OUTER', 6); 31 | define('HDOM_INFO_ENDSPACE',7); 32 | 33 | // helper functions 34 | // ----------------------------------------------------------------------------- 35 | // get html dom form file 36 | function file_get_html() { 37 | $dom = new simple_html_dom; 38 | $args = func_get_args(); 39 | $dom->load(call_user_func_array('file_get_contents', $args), true); 40 | return $dom; 41 | } 42 | 43 | // get html dom form string 44 | function str_get_html($str, $lowercase=true) { 45 | $dom = new simple_html_dom; 46 | $dom->load($str, $lowercase); 47 | return $dom; 48 | } 49 | 50 | // dump html dom tree 51 | function dump_html_tree($node, $show_attr=true, $deep=0) { 52 | $lead = str_repeat(' ', $deep); 53 | echo $lead.$node->tag; 54 | if ($show_attr && count($node->attr)>0) { 55 | echo '('; 56 | foreach($node->attr as $k=>$v) 57 | echo "[$k]=>\"".$node->$k.'", '; 58 | echo ')'; 59 | } 60 | echo "\n"; 61 | 62 | foreach($node->nodes as $c) 63 | dump_html_tree($c, $show_attr, $deep+1); 64 | } 65 | 66 | // get dom form file (deprecated) 67 | function file_get_dom() { 68 | $dom = new simple_html_dom; 69 | $args = func_get_args(); 70 | $dom->load(call_user_func_array('file_get_contents', $args), true); 71 | return $dom; 72 | } 73 | 74 | // get dom form string (deprecated) 75 | function str_get_dom($str, $lowercase=true) { 76 | $dom = new simple_html_dom; 77 | $dom->load($str, $lowercase); 78 | return $dom; 79 | } 80 | 81 | // simple html dom node 82 | // ----------------------------------------------------------------------------- 83 | class simple_html_dom_node { 84 | public $nodetype = HDOM_TYPE_TEXT; 85 | public $tag = 'text'; 86 | public $attr = array(); 87 | public $children = array(); 88 | public $nodes = array(); 89 | public $parent = null; 90 | public $_ = array(); 91 | private $dom = null; 92 | 93 | function __construct($dom) { 94 | $this->dom = $dom; 95 | $dom->nodes[] = $this; 96 | } 97 | 98 | function __destruct() { 99 | $this->clear(); 100 | } 101 | 102 | function __toString() { 103 | return $this->outertext(); 104 | } 105 | 106 | // clean up memory due to php5 circular references memory leak... 107 | function clear() { 108 | $this->dom = null; 109 | $this->nodes = null; 110 | $this->parent = null; 111 | $this->children = null; 112 | } 113 | 114 | // dump node's tree 115 | function dump($show_attr=true) { 116 | dump_html_tree($this, $show_attr); 117 | } 118 | 119 | // returns the parent of node 120 | function parent() { 121 | return $this->parent; 122 | } 123 | 124 | // returns children of node 125 | function children($idx=-1) { 126 | if ($idx===-1) return $this->children; 127 | if (isset($this->children[$idx])) return $this->children[$idx]; 128 | return null; 129 | } 130 | 131 | // returns the first child of node 132 | function first_child() { 133 | if (count($this->children)>0) return $this->children[0]; 134 | return null; 135 | } 136 | 137 | // returns the last child of node 138 | function last_child() { 139 | if (($count=count($this->children))>0) return $this->children[$count-1]; 140 | return null; 141 | } 142 | 143 | // returns the next sibling of node 144 | function next_sibling() { 145 | if ($this->parent===null) return null; 146 | $idx = 0; 147 | $count = count($this->parent->children); 148 | while ($idx<$count && $this!==$this->parent->children[$idx]) 149 | ++$idx; 150 | if (++$idx>=$count) return null; 151 | return $this->parent->children[$idx]; 152 | } 153 | 154 | // returns the previous sibling of node 155 | function prev_sibling() { 156 | if ($this->parent===null) return null; 157 | $idx = 0; 158 | $count = count($this->parent->children); 159 | while ($idx<$count && $this!==$this->parent->children[$idx]) 160 | ++$idx; 161 | if (--$idx<0) return null; 162 | return $this->parent->children[$idx]; 163 | } 164 | 165 | // get dom node's inner html 166 | function innertext() { 167 | if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER]; 168 | if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); 169 | 170 | $ret = ''; 171 | foreach($this->nodes as $n) 172 | $ret .= $n->outertext(); 173 | return $ret; 174 | } 175 | 176 | // get dom node's outer text (with tag) 177 | function outertext() { 178 | if ($this->tag==='root') return $this->innertext(); 179 | 180 | // trigger callback 181 | if ($this->dom->callback!==null) 182 | call_user_func_array($this->dom->callback, array($this)); 183 | 184 | if (isset($this->_[HDOM_INFO_OUTER])) return $this->_[HDOM_INFO_OUTER]; 185 | if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); 186 | 187 | // render begin tag 188 | $ret = $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]->makeup(); 189 | 190 | // render inner text 191 | if (isset($this->_[HDOM_INFO_INNER])) 192 | $ret .= $this->_[HDOM_INFO_INNER]; 193 | else { 194 | foreach($this->nodes as $n) 195 | $ret .= $n->outertext(); 196 | } 197 | 198 | // render end tag 199 | if(isset($this->_[HDOM_INFO_END]) && $this->_[HDOM_INFO_END]!=0) 200 | $ret .= 'tag.'>'; 201 | return $ret; 202 | } 203 | 204 | // get dom node's plain text 205 | function text() { 206 | if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER]; 207 | switch ($this->nodetype) { 208 | case HDOM_TYPE_TEXT: return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); 209 | case HDOM_TYPE_COMMENT: return ''; 210 | case HDOM_TYPE_UNKNOWN: return ''; 211 | } 212 | if (strcasecmp($this->tag, 'script')===0) return ''; 213 | if (strcasecmp($this->tag, 'style')===0) return ''; 214 | 215 | $ret = ''; 216 | foreach($this->nodes as $n) 217 | $ret .= $n->text(); 218 | return $ret; 219 | } 220 | 221 | function xmltext() { 222 | $ret = $this->innertext(); 223 | $ret = str_ireplace('', '', $ret); 225 | return $ret; 226 | } 227 | 228 | // build node's text with tag 229 | function makeup() { 230 | // text, comment, unknown 231 | if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); 232 | 233 | $ret = '<'.$this->tag; 234 | $i = -1; 235 | 236 | foreach($this->attr as $key=>$val) { 237 | ++$i; 238 | 239 | // skip removed attribute 240 | if ($val===null || $val===false) 241 | continue; 242 | 243 | $ret .= $this->_[HDOM_INFO_SPACE][$i][0]; 244 | //no value attr: nowrap, checked selected... 245 | if ($val===true) 246 | $ret .= $key; 247 | else { 248 | switch($this->_[HDOM_INFO_QUOTE][$i]) { 249 | case HDOM_QUOTE_DOUBLE: $quote = '"'; break; 250 | case HDOM_QUOTE_SINGLE: $quote = '\''; break; 251 | default: $quote = ''; 252 | } 253 | $ret .= $key.$this->_[HDOM_INFO_SPACE][$i][1].'='.$this->_[HDOM_INFO_SPACE][$i][2].$quote.$val.$quote; 254 | } 255 | } 256 | $ret = $this->dom->restore_noise($ret); 257 | return $ret . $this->_[HDOM_INFO_ENDSPACE] . '>'; 258 | } 259 | 260 | // find elements by css selector 261 | function find($selector, $idx=null) { 262 | $selectors = $this->parse_selector($selector); 263 | if (($count=count($selectors))===0) return array(); 264 | $found_keys = array(); 265 | 266 | // find each selector 267 | for ($c=0; $c<$count; ++$c) { 268 | if (($levle=count($selectors[0]))===0) return array(); 269 | if (!isset($this->_[HDOM_INFO_BEGIN])) return array(); 270 | 271 | $head = array($this->_[HDOM_INFO_BEGIN]=>1); 272 | 273 | // handle descendant selectors, no recursive! 274 | for ($l=0; $l<$levle; ++$l) { 275 | $ret = array(); 276 | foreach($head as $k=>$v) { 277 | $n = ($k===-1) ? $this->dom->root : $this->dom->nodes[$k]; 278 | $n->seek($selectors[$c][$l], $ret); 279 | } 280 | $head = $ret; 281 | } 282 | 283 | foreach($head as $k=>$v) { 284 | if (!isset($found_keys[$k])) 285 | $found_keys[$k] = 1; 286 | } 287 | } 288 | 289 | // sort keys 290 | ksort($found_keys); 291 | 292 | $found = array(); 293 | foreach($found_keys as $k=>$v) 294 | $found[] = $this->dom->nodes[$k]; 295 | 296 | // return nth-element or array 297 | if (is_null($idx)) return $found; 298 | else if ($idx<0) $idx = count($found) + $idx; 299 | return (isset($found[$idx])) ? $found[$idx] : null; 300 | } 301 | 302 | // seek for given conditions 303 | protected function seek($selector, &$ret) { 304 | list($tag, $key, $val, $exp, $no_key) = $selector; 305 | 306 | // xpath index 307 | if ($tag && $key && is_numeric($key)) { 308 | $count = 0; 309 | foreach ($this->children as $c) { 310 | if ($tag==='*' || $tag===$c->tag) { 311 | if (++$count==$key) { 312 | $ret[$c->_[HDOM_INFO_BEGIN]] = 1; 313 | return; 314 | } 315 | } 316 | } 317 | return; 318 | } 319 | 320 | $end = (!empty($this->_[HDOM_INFO_END])) ? $this->_[HDOM_INFO_END] : 0; 321 | if ($end==0) { 322 | $parent = $this->parent; 323 | while (!isset($parent->_[HDOM_INFO_END]) && $parent!==null) { 324 | $end -= 1; 325 | $parent = $parent->parent; 326 | } 327 | $end += $parent->_[HDOM_INFO_END]; 328 | } 329 | 330 | for($i=$this->_[HDOM_INFO_BEGIN]+1; $i<$end; ++$i) { 331 | $node = $this->dom->nodes[$i]; 332 | $pass = true; 333 | 334 | if ($tag==='*' && !$key) { 335 | if (in_array($node, $this->children, true)) 336 | $ret[$i] = 1; 337 | continue; 338 | } 339 | 340 | // compare tag 341 | if ($tag && $tag!=$node->tag && $tag!=='*') {$pass=false;} 342 | // compare key 343 | if ($pass && $key) { 344 | if ($no_key) { 345 | if (isset($node->attr[$key])) $pass=false; 346 | } 347 | else if (!isset($node->attr[$key])) $pass=false; 348 | } 349 | // compare value 350 | if ($pass && $key && $val && $val!=='*') { 351 | $check = $this->match($exp, $val, $node->attr[$key]); 352 | // handle multiple class 353 | if (!$check && strcasecmp($key, 'class')===0) { 354 | foreach(explode(' ',$node->attr[$key]) as $k) { 355 | $check = $this->match($exp, $val, $k); 356 | if ($check) break; 357 | } 358 | } 359 | if (!$check) $pass = false; 360 | } 361 | if ($pass) $ret[$i] = 1; 362 | unset($node); 363 | } 364 | } 365 | 366 | protected function match($exp, $pattern, $value) { 367 | switch ($exp) { 368 | case '=': 369 | return ($value===$pattern); 370 | case '!=': 371 | return ($value!==$pattern); 372 | case '^=': 373 | return preg_match("/^".preg_quote($pattern,'/')."/", $value); 374 | case '$=': 375 | return preg_match("/".preg_quote($pattern,'/')."$/", $value); 376 | case '*=': 377 | if ($pattern[0]=='/') 378 | return preg_match($pattern, $value); 379 | return preg_match("/".$pattern."/i", $value); 380 | } 381 | return false; 382 | } 383 | 384 | protected function parse_selector($selector_string) { 385 | // pattern of CSS selectors, modified from mootools 386 | $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is"; 387 | preg_match_all($pattern, trim($selector_string).' ', $matches, PREG_SET_ORDER); 388 | $selectors = array(); 389 | $result = array(); 390 | //print_r($matches); 391 | 392 | foreach ($matches as $m) { 393 | $m[0] = trim($m[0]); 394 | if ($m[0]==='' || $m[0]==='/' || $m[0]==='//') continue; 395 | // for borwser grnreated xpath 396 | if ($m[1]==='tbody') continue; 397 | 398 | list($tag, $key, $val, $exp, $no_key) = array($m[1], null, null, '=', false); 399 | if(!empty($m[2])) {$key='id'; $val=$m[2];} 400 | if(!empty($m[3])) {$key='class'; $val=$m[3];} 401 | if(!empty($m[4])) {$key=$m[4];} 402 | if(!empty($m[5])) {$exp=$m[5];} 403 | if(!empty($m[6])) {$val=$m[6];} 404 | 405 | // convert to lowercase 406 | if ($this->dom->lowercase) {$tag=strtolower($tag); $key=strtolower($key);} 407 | //elements that do NOT have the specified attribute 408 | if (isset($key[0]) && $key[0]==='!') {$key=substr($key, 1); $no_key=true;} 409 | 410 | $result[] = array($tag, $key, $val, $exp, $no_key); 411 | if (trim($m[7])===',') { 412 | $selectors[] = $result; 413 | $result = array(); 414 | } 415 | } 416 | if (count($result)>0) 417 | $selectors[] = $result; 418 | return $selectors; 419 | } 420 | 421 | function __get($name) { 422 | if (isset($this->attr[$name])) return $this->attr[$name]; 423 | switch($name) { 424 | case 'outertext': return $this->outertext(); 425 | case 'innertext': return $this->innertext(); 426 | case 'plaintext': return $this->text(); 427 | case 'xmltext': return $this->xmltext(); 428 | default: return array_key_exists($name, $this->attr); 429 | } 430 | } 431 | 432 | function __set($name, $value) { 433 | switch($name) { 434 | case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value; 435 | case 'innertext': 436 | if (isset($this->_[HDOM_INFO_TEXT])) return $this->_[HDOM_INFO_TEXT] = $value; 437 | return $this->_[HDOM_INFO_INNER] = $value; 438 | } 439 | if (!isset($this->attr[$name])) { 440 | $this->_[HDOM_INFO_SPACE][] = array(' ', '', ''); 441 | $this->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE; 442 | } 443 | $this->attr[$name] = $value; 444 | } 445 | 446 | function __isset($name) { 447 | switch($name) { 448 | case 'outertext': return true; 449 | case 'innertext': return true; 450 | case 'plaintext': return true; 451 | } 452 | //no value attr: nowrap, checked selected... 453 | return (array_key_exists($name, $this->attr)) ? true : isset($this->attr[$name]); 454 | } 455 | 456 | function __unset($name) { 457 | if (isset($this->attr[$name])) 458 | unset($this->attr[$name]); 459 | } 460 | 461 | // camel naming conventions 462 | function getAllAttributes() {return $this->attr;} 463 | function getAttribute($name) {return $this->__get($name);} 464 | function setAttribute($name, $value) {$this->__set($name, $value);} 465 | function hasAttribute($name) {return $this->__isset($name);} 466 | function removeAttribute($name) {$this->__set($name, null);} 467 | function getElementById($id) {return $this->find("#$id", 0);} 468 | function getElementsById($id, $idx=null) {return $this->find("#$id", $idx);} 469 | function getElementByTagName($name) {return $this->find($name, 0);} 470 | function getElementsByTagName($name, $idx=null) {return $this->find($name, $idx);} 471 | function parentNode() {return $this->parent();} 472 | function childNodes($idx=-1) {return $this->children($idx);} 473 | function firstChild() {return $this->first_child();} 474 | function lastChild() {return $this->last_child();} 475 | function nextSibling() {return $this->next_sibling();} 476 | function previousSibling() {return $this->prev_sibling();} 477 | } 478 | 479 | // simple html dom parser 480 | // ----------------------------------------------------------------------------- 481 | class simple_html_dom { 482 | public $root = null; 483 | public $nodes = array(); 484 | public $callback = null; 485 | public $lowercase = false; 486 | protected $pos; 487 | protected $doc; 488 | protected $char; 489 | protected $size; 490 | protected $cursor; 491 | protected $parent; 492 | protected $noise = array(); 493 | protected $token_blank = " \t\r\n"; 494 | protected $token_equal = ' =/>'; 495 | protected $token_slash = " />\r\n\t"; 496 | protected $token_attr = ' >'; 497 | // use isset instead of in_array, performance boost about 30%... 498 | protected $self_closing_tags = array('img'=>1, 'br'=>1, 'input'=>1, 'meta'=>1, 'link'=>1, 'hr'=>1, 'base'=>1, 'embed'=>1, 'spacer'=>1); 499 | protected $block_tags = array('root'=>1, 'body'=>1, 'form'=>1, 'div'=>1, 'span'=>1, 'table'=>1); 500 | protected $optional_closing_tags = array( 501 | 'tr'=>array('tr'=>1, 'td'=>1, 'th'=>1), 502 | 'th'=>array('th'=>1), 503 | 'td'=>array('td'=>1), 504 | 'li'=>array('li'=>1), 505 | 'dt'=>array('dt'=>1, 'dd'=>1), 506 | 'dd'=>array('dd'=>1, 'dt'=>1), 507 | 'dl'=>array('dd'=>1, 'dt'=>1), 508 | 'p'=>array('p'=>1), 509 | 'nobr'=>array('nobr'=>1), 510 | ); 511 | 512 | function __construct($str=null) { 513 | if ($str) { 514 | if (preg_match("/^http:\/\//i",$str) || is_file($str)) 515 | $this->load_file($str); 516 | else 517 | $this->load($str); 518 | } 519 | } 520 | 521 | function __destruct() { 522 | $this->clear(); 523 | } 524 | 525 | // load html from string 526 | function load($str, $lowercase=true) { 527 | // prepare 528 | $this->prepare($str, $lowercase); 529 | // strip out comments 530 | $this->remove_noise("''is"); 531 | // strip out cdata 532 | $this->remove_noise("''is", true); 533 | // strip out