├── .gitmodules ├── .htaccess ├── index.php ├── lib ├── config.php └── relmeauth.php └── readme.markdown /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/cassis"] 2 | path = lib/cassis 3 | url = https://github.com/tantek/cassis.git 4 | [submodule "lib/tmhOAuth"] 5 | path = lib/tmhOAuth 6 | url = https://github.com/themattharris/tmhOAuth.git 7 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteBase / 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteCond %{REQUEST_FILENAME} !-d 6 | RewriteRule ^(.*)$ /index.php [L,NC] 7 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | redirect(); 10 | } 11 | elseif ( isset($_REQUEST['oauth_verifier'] ) ) { 12 | $ok = $relmeauth->complete_oauth( $_REQUEST['oauth_verifier'] ); 13 | // error message on false! 14 | } 15 | else if (isset($_REQUEST['denied'] ) ) { 16 | // user cancelled login 17 | $relmeauth->error('Sign in cancelled.'); 18 | } 19 | else if ( isset($_POST['url']) ) { 20 | $user_url = strip_tags( stripslashes( $_POST['url'] ) ); 21 | 22 | $user_site = parse_url($user_url); 23 | if ($user_site['path']==='') { // fix-up domain only URLs with a path 24 | $user_url = $user_url . '/'; 25 | } 26 | 27 | $_SESSION['relmeauth']['url'] = $user_url; 28 | $_SESSION['relmeauth']['write'] = $_POST['write']; 29 | 30 | // discover relme on the url 31 | $relmeauth->main( $user_url, $_POST['write'] ); 32 | } 33 | else if ($relmeauth->is_loggedin()) { 34 | $relmeauth->create_from_session(); 35 | } 36 | 37 | function _e($content) { 38 | echo htmlentities($content); 39 | } 40 | 41 | ?> 42 | 43 | 44 | 45 | RelMeAuth prototype 46 | 47 | 78 | 79 | 80 | 81 |

RelMeAuth prototype

82 | is_loggedin()) { ?> 83 |

You are logged in as using 84 | . logout?

85 | 86 | 87 |

Congratulations. You've unlocked RelMeAuth level 2 and can now post updates.

88 | 89 |
90 | 91 | 92 | 93 |
94 | 95 | 96 |

Tweeting...

97 | tmhOAuth; 98 | $tmhOAuth->request('POST', $tmhOAuth->url('statuses/update'), array( 99 | 'status' => $_POST['post'] 100 | )); 101 | ?> 102 |

Twitter's API says:

103 | response['code'] == 200) { 104 | $tmhOAuth->pr(json_decode($tmhOAuth->response['response'])); 105 | } else { 106 | $tmhOAuth->pr(htmlentities($tmhOAuth->response['response'])); 107 | } 108 | } // /user posted 109 | } else { // no write access yet, so encourage user to upgrade 110 | ?> 111 |

Congratulations. You've unlocked RelMeAuth level 1.

112 | 113 | 114 |

Would you like to try posting? Allow RelMeAuth to update your Twitter.

115 |
116 | 117 | 118 | 119 |
120 | printError(); ?> 124 |

This is a working prototype of RelMeAuth.

125 |

This is purely a test user interface. If this had been an actual user interface, 126 | you wouldn't be wondering what the hell is going on, what is "my domain", 127 | who am I, and why do I exist.

128 |

This is only a test.

129 |

Enter your personal web address, click Sign In, and see what happens.

130 | 131 |
132 | 133 | 136 | 137 |

And that's it. Well that's it for the simple case. If you want to 138 | try an even more experimental feature, try checking the following 139 | checkbox before clicking the Sign in button.

140 | 141 |
142 | 143 |

This checkbox is a metaphorical big red button you're supposed to try ignoring, or dare to click. It's only here because the OAuth allow permission page of the presumed destination doesn't have it, which is really where it should be (so we don't have to presume the destination).

144 | 145 |

It is likely there are still errors and any issues should be reported on the 146 | GitHub Project Page. This code is written by 147 | @themattharris and @t. It 148 | uses a modified OAuth PHP library.

149 | 150 | 151 | 152 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /lib/config.php: -------------------------------------------------------------------------------- 1 | array( 5 | 'keys' => array( 6 | 'consumer_key' => 'YOUR_CONSUMER_KEY', // YOUR_CONSUMER_KEY 7 | 'consumer_secret' => 'YOUR_CONSUMER_SECRET', // YOUR_CONSUMER_SECRET 8 | ), 9 | 'urls' => array( 10 | 'request' => 'https://api.twitter.com/oauth/request_token', 11 | 'authenticate' => 'https://api.twitter.com/oauth/authenticate', // auto 12 | 'authorize' => 'https://api.twitter.com/oauth/authorize', // ask 13 | 'access' => 'https://api.twitter.com/oauth/access_token', 14 | 'verify' => 'https://api.twitter.com/1/account/verify_credentials.json', 15 | ), 16 | 'verify' => array( 17 | 'url' => 'url', 18 | 'name' => 'url' 19 | ), 20 | 'rtrimprofile' => '/', 21 | 'ltrimdomain' => 'www.' 22 | ), 23 | 'identi.ca' => array( 24 | 'keys' => array( 25 | 'consumer_key' => 'YOUR_CONSUMER_KEY', // YOUR_CONSUMER_KEY 26 | 'consumer_secret' => 'YOUR_CONSUMER_SECRET', // YOUR_CONSUMER_SECRET 27 | ), 28 | 'urls' => array( 29 | 'request' => 'https://api.identi.ca/oauth/request_token', // http://identi.ca/api/oauth/request_token 30 | 'authenticate' => 'https://api.identi.ca/oauth/authenticate', 31 | 'authorize' => 'https://api.identi.ca/oauth/authorize', // http://identi.ca/api/oauth/authorize 32 | 'access' => 'https://api.identi.ca/oauth/access_token', // http://identi.ca/api/oauth/access_token 33 | 'verify' => 'https://api.identi.ca/1/account/verify_credentials.json', 34 | ), 35 | 'verify' => array( 36 | 'url' => 'url', 37 | 'name' => 'url' 38 | ), 39 | 'rtrimprofile' => '/', 40 | 'ltrimdomain' => 'www.' 41 | ) 42 | ); 43 | 44 | ?> -------------------------------------------------------------------------------- /lib/relmeauth.php: -------------------------------------------------------------------------------- 1 | $v) { 8 | unset($process[$key][$k]); 9 | if (is_array($v)) { 10 | $process[$key][stripslashes($k)] = $v; 11 | $process[] = &$process[$key][stripslashes($k)]; 12 | } else { 13 | $process[$key][stripslashes($k)] = stripslashes($v); 14 | } 15 | } 16 | } 17 | unset($process); 18 | } 19 | 20 | ob_start(); require_once dirname(__FILE__) . '/cassis/cassis.js'; ob_end_clean(); 21 | require dirname(__FILE__) . '/tmhOAuth/tmhOAuth.php'; 22 | require dirname(__FILE__) . '/config.php'; 23 | 24 | class relmeauth { 25 | function __construct() { 26 | session_start(); 27 | $this->tmhOAuth = new tmhOAuth(array()); 28 | } 29 | 30 | function is_loggedin() { 31 | // TODO: should have a timestamp expiry in here. 32 | return (isset($_SESSION['relmeauth']['name'])); 33 | } 34 | 35 | function create_from_session() { 36 | global $providers; 37 | 38 | $config = $providers[$_SESSION['relmeauth']['provider']]; 39 | 40 | // create tmhOAuth from session info 41 | $this->tmhOAuth = new tmhOAuth(array( 42 | 'consumer_key' => $config['keys']['consumer_key'], 43 | 'consumer_secret' => $config['keys']['consumer_secret'], 44 | 'user_token' => $_SESSION['relmeauth']['access']['oauth_token'], 45 | 'user_secret' => $_SESSION['relmeauth']['access']['oauth_token_secret'] 46 | )); 47 | } 48 | 49 | function main($user_url, $askwrite) { 50 | // first try to authenticate directly with the URL given 51 | if ($this->is_provider($user_url)) { 52 | $_SESSION['relmeauth']['direct'] = true; 53 | if ($this->authenticate_url($user_url, $askwrite)) { 54 | return true; // bail once something claims to authenticate 55 | } 56 | unset($_SESSION['relmeauth']['direct']); 57 | } 58 | 59 | // get the rel-me URLs from the given site 60 | $source_rels = $this->discover($user_url); 61 | 62 | if ($source_rels==false || count($source_rels) == 0) { 63 | return false; // no rel-me links found, bail 64 | } 65 | 66 | // separate them into external and same domain 67 | $external_rels = array(); 68 | $local_rels = array(); 69 | $user_site = parse_url($user_url); 70 | 71 | foreach ($source_rels as $source_rel => $details) : 72 | $provider = parse_url($source_rel); 73 | if ($provider['host'] == $user_site['host']) { 74 | $local_rels[$source_rel] = $details; 75 | } else { 76 | $external_rels[$source_rel] = $details; 77 | } 78 | endforeach; // source_rels 79 | 80 | // see if any of the external rel-me URLs reciprocate - check rels in order 81 | // and then try authing it. needs to maintain more session state to resume. 82 | foreach ($external_rels as $external_rel => $details): 83 | // only bother to confirm rel-me etc. if we know how to auth the dest. 84 | if ($this->is_provider($external_rel) && 85 | $this->confirm_rel($user_url, $external_rel)) { 86 | // We could keep this as a URL we actually try to auth, for debugging 87 | if ($this->authenticate_url($external_rel, $askwrite)) { 88 | return true; // bail once something claims to authenticate 89 | } 90 | } 91 | endforeach; // external_rels 92 | 93 | $source_rels = array_merge($local_rels, $external_rels); 94 | $source2_tried = array(); 95 | 96 | // no external_rels, or none of them reciprocated or authed. try next level. 97 | foreach ($source_rels as $source_rel => $details) : 98 | // try rel-me-authing $source_rel, 99 | // and test its respective external $source2_urls 100 | // to match against $source_rel OR $user_url. 101 | 102 | $source_rel_confirmed = 103 | strpos($source_rel, $user_url)===0 || 104 | $this->confirm_rel($user_url, $source_rel); 105 | // if $source_rel is a confirmed rel-me itself, 106 | // then we'll allow for 2nd level to confirm to it 107 | 108 | // then check its external_rels 109 | $source2_rels = $this->discover($source_rel); 110 | if ($source2_rels!=false) { 111 | foreach ($source2_rels as $source2_rel => $details) : 112 | $provider = parse_url($source2_rel); 113 | if ($provider['host'] != $user_site['host'] && 114 | $this->is_provider($source2_rel)) 115 | { 116 | $source2_tried[$source2_rel] = $details; 117 | if ((!$source_rel_confirmed && 118 | $this->confirm_rel($user_url, $source2_rel)) || 119 | ($source_rel_confirmed && 120 | $this->confirms_rel($user_url, $source_rel, $source2_rel))) 121 | { 122 | // could keep this as a URL we actually try to auth, for debugging 123 | if ($source_rel_confirmed) { 124 | $_SESSION['relmeauth']['url2'] = $source_rel; 125 | } 126 | if ($this->authenticate_url($source2_rel, $askwrite)) { 127 | // this exits if it succeeds. next statement unnecessary. 128 | return true; // bail once something claims to authenticate 129 | } 130 | $_SESSION['relmeauth']['url2'] = ''; 131 | } 132 | } 133 | endforeach; // source_rels 134 | } 135 | 136 | // if successful, should have returned true, which can be returned 137 | endforeach; // source_rels 138 | 139 | /* 140 | //debugging 141 | $debugurls = $this->discover('http://twitter.com/kevinmarks/'); 142 | 143 | //end debugging 144 | */ 145 | 146 | // otherwise, no URLs worked. 147 | $source_rels = implode(', ', array_keys($source_rels)) . 148 | ($source2_tried && count($source2_tried)>=0 ? ', ' . 149 | implode(', ', array_keys($source2_tried)) : '') 150 | /* 151 | . 152 | ($debugurls && count($debugurls)>=0 ? '. debug: ' . 153 | implode(', ', array_keys($debugurls)) : '') 154 | */ 155 | ; 156 | 157 | $this->error('None of your providers are supported. Tried ' . $source_rels . '.'); 158 | 159 | return false; 160 | 161 | /* 162 | // old code that first confirmed all rel-me links, and then tried as a batch 163 | // see if any of the relmes match back - we check the rels in the order 164 | // they are listed in the HTML 165 | $confirmed_rels = $this->confirm_rels($user_url, $source_rels); 166 | if ($confirmed_rels != false) { 167 | return $this->authenticate($confirmed_rels); 168 | } else { 169 | // error message will have already been set 170 | return false; 171 | } 172 | */ 173 | } 174 | 175 | function request($keys, $method, $url, $params=array(), $useauth=true) { 176 | $this->tmhOAuth = new tmhOAuth(array()); 177 | 178 | $this->tmhOAuth->config['consumer_key'] = $keys['consumer_key']; 179 | $this->tmhOAuth->config['consumer_secret'] = $keys['consumer_secret']; 180 | $this->tmhOAuth->config['user_token'] = @$keys['user_token']; 181 | $this->tmhOAuth->config['user_secret'] = @$keys['user_secret']; 182 | $code = $this->tmhOAuth->request( 183 | $method, 184 | $url, 185 | $params, 186 | $useauth 187 | ); 188 | 189 | return ( $code == 200 ); 190 | } 191 | 192 | 193 | /** 194 | * check to see if we know how to OAuth a URL 195 | * 196 | * @return whether or not it's a provider we know how to deal with 197 | * @author Tantek Çelik 198 | */ 199 | function is_provider($confirmed_rel) { 200 | global $providers; 201 | 202 | $provider = parse_url($confirmed_rel); 203 | if (array_key_exists($provider['host'], $providers)) { 204 | return true; 205 | } 206 | if (strpos($provider['host'], 'www.')===0) { 207 | $provider['host'] = substr($provider['host'],4); 208 | if (array_key_exists($provider['host'], $providers) && 209 | $providers[$provider['host']]['ltrimdomain'] == 'www.') 210 | { 211 | return true; 212 | } 213 | } 214 | return false; 215 | } 216 | 217 | /** 218 | * Wrapper for the OAuth authentication process for a URL 219 | * 220 | * @return false if authentication failed 221 | * @author Matt Harris and Tantek Çelik 222 | */ 223 | function authenticate_url($confirmed_rel, $askwrite) { 224 | global $providers; 225 | 226 | if (!$this->is_provider($confirmed_rel)) 227 | return false; 228 | 229 | $provider = parse_url($confirmed_rel); 230 | $config = $providers[ $provider['host'] ]; 231 | $ok = $this->request( 232 | $config['keys'], 233 | 'GET', 234 | $config['urls']['request'], 235 | array( 236 | 'oauth_callback' => $this->here(), 237 | 'x_auth_access_type' => ($askwrite ? 'write' : 'read'), // http://dev.twitter.com/doc/post/oauth/request_token 238 | ) 239 | ); 240 | 241 | if ($ok) { 242 | // need these later 243 | $relpath = $provider['path']; 244 | $user = $this->tmhOAuth->extract_params($this->tmhOAuth->response['response']); 245 | 246 | $_SESSION['relmeauth']['provider'] = $provider['host']; 247 | $_SESSION['relmeauth']['secret'] = $user['oauth_token_secret']; 248 | $_SESSION['relmeauth']['token'] = $user['oauth_token']; 249 | $url = ($askwrite ? $config['urls']['authorize'] 250 | : $config['urls']['authenticate']) . '?' 251 | . "oauth_token={$user['oauth_token']}"; 252 | $this->redirect($url); 253 | return true; 254 | } else { 255 | $this->error("There was a problem communicating with {$provider['host']}. Error {$this->tmhOAuth->response['code']}. Please try later."); 256 | } 257 | 258 | return false; 259 | } 260 | 261 | /** 262 | * Wrapper for the OAuth authentication process 263 | * 264 | * @return false upon failure 265 | * @author Matt Harris and Tantek Çelik 266 | */ 267 | function authenticate($confirmed_rels) { 268 | global $providers; 269 | 270 | foreach ($confirmed_rels as $host => $details) : 271 | if (authenticate_url($host)) 272 | return true; 273 | endforeach; // confirmed_rels 274 | 275 | $this->error('None of your providers are supported. Tried ' . implode(', ', array_keys($confirmed_rels)) . '.'); 276 | return false; 277 | } 278 | 279 | function complete_oauth( $verifier ) { 280 | global $providers; 281 | 282 | if ( ! array_key_exists($_SESSION['relmeauth']['provider'], $providers) ) { 283 | $this->error('None of your providers are supported, or you might have cookies disabled. Make sure your browser preferences are set to accept cookies and try again.'); 284 | return false; 285 | } 286 | 287 | $config = $providers[$_SESSION['relmeauth']['provider']]; 288 | $ok = $this->request( 289 | array_merge( 290 | $config['keys'], 291 | array( 292 | 'user_token' => $_SESSION['relmeauth']['token'], 293 | 'user_secret' => $_SESSION['relmeauth']['secret'] 294 | ) 295 | ), 296 | 'GET', 297 | $config['urls']['access'], 298 | array( 299 | 'oauth_verifier' => $verifier 300 | ) 301 | ); 302 | unset($_SESSION['relmeauth']['token']); 303 | unset($_SESSION['relmeauth']['secret']); 304 | 305 | if ($ok) { 306 | // get the users token and secret 307 | $_SESSION['relmeauth']['access'] = $this->tmhOAuth->extract_params($this->tmhOAuth->response['response']); 308 | 309 | // FIXME: validate this is the user who requested. 310 | // At the moment if I use another users URL that rel=me to Twitter for example, it 311 | // will work for me - because all we do is go 'oh Twitter, sure, login there and you're good to go 312 | // the rel=me bit doesn't get confirmed it belongs to the user 313 | $this->verify( $config ); 314 | $this->redirect(); 315 | } 316 | $this->error("There was a problem authenticating with {$provider['host']}. Error {$this->tmhOAuth->response['code']}. Please try later."); 317 | return false; 318 | } 319 | 320 | function verify( &$config ) { 321 | global $providers; 322 | $config = $providers[$_SESSION['relmeauth']['provider']]; 323 | 324 | $ok = $this->request( 325 | array_merge( 326 | $config['keys'], 327 | array( 328 | 'user_token' => $_SESSION['relmeauth']['access']['oauth_token'], 329 | 'user_secret' => $_SESSION['relmeauth']['access']['oauth_token_secret'] 330 | ) 331 | ), 332 | 'GET', 333 | $config['urls']['verify'] 334 | ); 335 | 336 | $creds = json_decode($this->tmhOAuth->response['response'], true); 337 | 338 | $given = self::normalise_url($_SESSION['relmeauth']['url']); 339 | $found = self::normalise_url(self::expand_tco($creds[ $config['verify']['url'] ])); 340 | 341 | $_SESSION['relmeauth']['debug']['verify']['given'] = $given; 342 | $_SESSION['relmeauth']['debug']['verify']['found'] = $found; 343 | 344 | if ( $given != $found && 345 | array_key_exists('url2', $_SESSION['relmeauth'])) 346 | { 347 | $given = self::normalise_url($_SESSION['relmeauth']['url2']); 348 | } 349 | 350 | if ( $given == $found || 351 | ($this->is_provider($given) && $_SESSION['relmeauth']['direct'])) 352 | { 353 | $_SESSION['relmeauth']['name'] = $creds[ $config['verify']['name'] ]; 354 | return true; 355 | } else { 356 | // destroy everything 357 | $provider = $_SESSION['relmeauth']['provider']; 358 | // unset($_SESSION['relmeauth']); 359 | $this->error("That isn't you! If it really is you, try signing out of {$provider}. Entered $given (". @$_SESSION['relmeauth']['url2'] . "), found $found."); 360 | return false; 361 | } 362 | } 363 | 364 | function error($message) { 365 | if ( ! isset( $_SESSION['relmeauth']['error'] ) ) { 366 | $_SESSION['relmeauth']['error'] = $message; 367 | } else { 368 | $_SESSION['relmeauth']['error'] .= ' ' . $message; 369 | } 370 | } 371 | 372 | /** 373 | * Print the last error message if there is one. 374 | * 375 | * @return void 376 | * @author Matt Harris 377 | */ 378 | function printError() { 379 | if ( isset( $_SESSION['relmeauth']['error'] ) ) { 380 | echo '
' . 381 | $_SESSION['relmeauth']['error'] . '
'; 382 | unset($_SESSION['relmeauth']['error']); 383 | } 384 | } 385 | 386 | /** 387 | * Check one rel=me URLs obtained from the users URL and see 388 | * if it contains a rel=me which equals this user URL. 389 | * 390 | * @return true if URL rel-me reciprocation confirmed else false 391 | * @author Matt Harris and Tantek Çelik 392 | */ 393 | function confirm_rel($user_url, $source_rel) { 394 | $othermes = $this->discover($source_rel, false); 395 | $_SESSION['relmeauth']['debug']['source_rels'][$source_rel] = $othermes; 396 | if (is_array( $othermes)) { 397 | $othermes = array_map(array('relmeauth', 'normalise_url'), $othermes); 398 | $user_url = self::normalise_url($user_url); 399 | 400 | if (in_array($user_url, $othermes)) { 401 | $_SESSION['relmeauth']['debug']['matched'][] = $source_rel; 402 | return true; 403 | } 404 | } 405 | return false; 406 | } 407 | 408 | /** 409 | * Check one rel=me URLs obtained from the users URL and see 410 | * if it contains a rel=me which equals this user URL. 411 | * 412 | * @return true if URL rel-me reciprocation confirmed else false 413 | * @author Matt Harris and Tantek Çelik 414 | * Should really abstract confirms_rel() confirm_rel() and replace both 415 | */ 416 | function confirms_rel($user_url, $local_url, $source_rel) { 417 | $othermes = $this->discover( $source_rel, false ); 418 | $_SESSION['relmeauth']['debug']['source_rels'][$source_rel] = $othermes; 419 | if ( is_array( $othermes ) ) { 420 | $othermes = array_map(array('relmeauth', 'normalise_url'), $othermes); 421 | $user_url = self::normalise_url($user_url); 422 | $local_url = self::normalise_url($local_url); 423 | 424 | if (in_array($user_url, $othermes) || 425 | in_array($local_url, $othermes)) { 426 | $_SESSION['relmeauth']['debug']['matched'][] = $source_rel; 427 | return true; 428 | } 429 | } 430 | return false; 431 | } 432 | 433 | 434 | /** 435 | * Go through the rel=me URLs obtained from the users URL and see 436 | * if any of those sites contain a rel=me which equals this user URL. 437 | * 438 | * @return URLs that have confirmed rel-me links back to user_url or false 439 | * @author Matt Harris and Tantek Çelik 440 | */ 441 | function confirm_rels($user_url, $source_rels) { 442 | if (!is_array($source_rels)) { 443 | $this->error('No rels found.'); 444 | return false; 445 | } 446 | 447 | $confirmed_rels = array(); 448 | foreach ( $source_rels as $url => $text ) { 449 | if (confirm_rel($user_url, $url)) { 450 | $confirmed_rels[$url] = $text; 451 | } 452 | } 453 | if (count($confirmed_rels)>0) { 454 | return $confirmed_rels; 455 | } 456 | $this->error('No rels matched. Tried ' . implode(', ', array_keys($this->source_rels))); 457 | return false; 458 | } 459 | 460 | /** 461 | * Does the job of discovering rel="me" urls 462 | * 463 | * @return array of rel="me" urls for the given source URL 464 | * @author Matt Harris 465 | */ 466 | function discover($source_url, $titles=true) { 467 | global $providers; 468 | 469 | $this->tmhOAuth->request('GET', $source_url, array(), false); 470 | if ($this->tmhOAuth->response['code'] != 200) { 471 | $this->error('Was expecting a 200 and instead got a ' 472 | . $this->tmhOAuth->response['code']); 473 | error_log('got an unexpected response from ' . $source_url . ', ' 474 | . json_encode($this->tmhOAuth->response)); 475 | return false; 476 | } 477 | 478 | libxml_use_internal_errors(true); // silence HTML parser warnings 479 | $doc = new DOMDocument(); 480 | if ( ! $doc->loadHTML($this->tmhOAuth->response['response']) ) { 481 | error_log('could not parse '.$source_url); 482 | $this->error('Looks like I can\'t do anything with ' . $source_url); 483 | return false; 484 | } 485 | 486 | $xpath = new DOMXPath($doc); 487 | $relmes = $xpath->query(xphasrel('me')); 488 | $base = self::real_url( 489 | self::html_base_href($xpath), $source_url 490 | ); 491 | 492 | // get anything? 493 | if ( empty($relmes) ) { 494 | error_log('No rel-me tags found for ' . $source_url); 495 | return false; 496 | } 497 | 498 | // clean up the relmes 499 | $urls = array(); 500 | foreach ($relmes as $rel) { 501 | $title = (string) $rel->getAttribute('title'); 502 | $url = (string) $rel->getAttribute('href'); 503 | $url = self::real_url($base, $url); 504 | if (empty($url)) 505 | continue; 506 | $url = self::expand_tco($url); 507 | 508 | // trim extra trailing stuff from external profile URLs 509 | // workaround for providers failing to properly 301 to the shortest URL 510 | $provider = parse_url($url); 511 | if (array_key_exists($provider['host'], $providers)) 512 | { 513 | $config = $providers[ $provider['host']]; 514 | if (array_key_exists('rtrimprofile', $config)) { 515 | $url = rtrim($url,$config['rtrimprofile']); 516 | } 517 | } 518 | 519 | $title = empty($title) ? $url : $title; 520 | if ( $titles ) { 521 | $urls[ $url ] = $title; 522 | } else { 523 | $urls[] = $url; 524 | } 525 | } 526 | return $urls; 527 | } 528 | 529 | /** 530 | * Works out the base URL for the page for use when calculating relative and 531 | * absolute URLs. This function looks for the base element in the head of 532 | * the document and if found uses that as the html base href. 533 | * 534 | * @param string $simple_xml_element the SimpleXML representing the obtained HTML 535 | * @return the new base URL if found or empty string otherwise 536 | * @author Tantek Çelik 537 | */ 538 | function html_base_href($xpath) { 539 | if ( ! $xpath) 540 | return ''; 541 | 542 | $base_elements = $xpath->query('//head//base[@href]'); 543 | return ( $base_elements && ( $base_elements->length > 0 ) ) ? 544 | $base_elements->item(0)->getAttribute('href') : 545 | ''; 546 | } 547 | 548 | /** 549 | * Calculates the normalised URL for a given URL and base href. Absolute and 550 | * relative URLs are supported as well as full URIs. 551 | * 552 | * @param string $base the base href 553 | * @param string $url the URL to be normalised 554 | * @return void 555 | * @author Matt Harris and Tantek Çelik 556 | */ 557 | function real_url($base, $url) { 558 | // has a protcol, and therefore assumed domain 559 | if (preg_matches('/^[\w-]+:/', $url)) { 560 | /* 561 | $parsed = parse_url($url); 562 | if ($parsed['path']==='') { // fix-up domain only URLs with a path 563 | $url .= '/'; 564 | } 565 | */ 566 | return $url; 567 | } 568 | 569 | // absolute URL 570 | if ( $url[0] == '/' ) { 571 | $url_bits = parse_url($base); 572 | $host = $url_bits['scheme'] . '://' . $url_bits['host']; 573 | return $host . $url; 574 | } 575 | 576 | // inspect base, check we have the directory 577 | $path = substr($base, 0, strrpos($base, '/')) . '/'; 578 | // relative URL 579 | 580 | // explode the url with relatives in it 581 | $url = explode('/', $path.$url); 582 | 583 | // remove the domain as we can't go higher than that 584 | $base = $url[0].'//'.$url[2].'/'; 585 | array_splice($url, 0, 3); 586 | 587 | // process each folder 588 | // for every .. remove the previous non .. in the array 589 | $keys = array_keys($url, '..'); 590 | foreach( $keys as $idx => $dir ) { 591 | // work out the new offset for .. 592 | $offset = $dir - ($idx * 2 + 1); 593 | 594 | if ($offset < 0 && $url[0] == '..') { 595 | array_splice($url, 0, 1); 596 | } elseif ( $offset < 0 ) { 597 | // need to know where the new .. are 598 | return self::real_url($base, implode('/', $url)); 599 | } else { 600 | array_splice($url, $offset, 2); 601 | } 602 | } 603 | $url = implode('/', $url); 604 | $url = str_replace('./', '', $url); 605 | return $base . $url; 606 | } 607 | 608 | /** 609 | * try and convert the string to SimpleXML 610 | * 611 | * @param string $str the HTML 612 | * @return SimpleXMLElement or false on fail 613 | * @author Matt Harris 614 | */ 615 | // TODO this was replaced by DOMDocument::loadHTML; remove this function? 616 | function toXML($str) { 617 | $xml = false; 618 | 619 | try { 620 | $xml = @ new SimpleXMLElement($str); 621 | } catch (Exception $e) { 622 | if ( stripos('String could not be parsed as XML', $e->getMessage()) ) { 623 | return false; 624 | } 625 | } 626 | return $xml; 627 | } 628 | 629 | /** 630 | * Run tidy on the given string if it is installed. This function configures 631 | * tidy to support HTML5. 632 | * 633 | * @param string $html the html to run through tidy. 634 | * @return the tidied html or false if tidy is not installed. 635 | * @author Matt Harris 636 | */ 637 | // TODO this shouldn't be necessary anymore with DOMDocument::loadHTML, remove it? 638 | function tidy($html) { 639 | if ( class_exists('tidy') ) { 640 | $tidy = new tidy(); 641 | $config = array( 642 | 'bare' => TRUE, 643 | 'clean' => TRUE, 644 | 'indent' => TRUE, 645 | 'output-xml' => TRUE, // 'output-xhtml' => TRUE, 646 | // must be -xml to cleanup named entities that are ok in XHTML but not XML 647 | 'wrap' => 200, 648 | 'hide-comments' => TRUE, 649 | 'new-blocklevel-tags' => implode(' ', array( 650 | 'header', 'footer', 'article', 'section', 'aside', 'nav', 'figure', 651 | )), 652 | 'new-inline-tags' => implode(' ', array( 653 | 'mark', 'time', 'meter', 'progress', 654 | )), 655 | ); 656 | $tidy->parseString( $html, $config, 'utf8' ); 657 | $tidy->cleanRepair(); 658 | $html = str_ireplace( '','­', (string)$tidy ); 659 | unset($tidy); 660 | return $html; 661 | } else { 662 | $this->error('no tidy :('); 663 | // need some other way to clean here. html5lib? 664 | return $html; 665 | } 666 | return false; 667 | } 668 | 669 | /** 670 | * Twitter now shortens rel-me URLs and replaces them with their 671 | * t.co short links (even in the return value from 672 | * verify_credentials). For this specific case, issue a HEAD request 673 | * to find the real URL. 674 | * 675 | * @param string $url the original URL, possibly a t.co short-link 676 | * @return the expanded URL if found; otherwise the unmodified original URL 677 | * @author Kyle Mahan 678 | */ 679 | function expand_tco($url) { 680 | if (strpos($url, '/t.co/')) { 681 | $this->tmhOAuth->request('HEAD', $url, array(), false); 682 | if ($this->tmhOAuth->response['code'] == 301 683 | || $this->tmhOAuth->response['code'] == 302) { 684 | $redirect_url = $this->tmhOAuth->response['info']['redirect_url']; 685 | if ($redirect_url) { 686 | return $redirect_url; 687 | } 688 | } 689 | } 690 | return $url; 691 | } 692 | 693 | function redirect($url=false) { 694 | $url = ! $url ? $this->here() : $url; 695 | header( "Location: $url" ); 696 | die; 697 | } 698 | 699 | function here($withqs=false) { 700 | $url = sprintf('%s://%s%s', 701 | $_SERVER['SERVER_PORT'] == 80 ? 'http' : 'https', 702 | $_SERVER['SERVER_NAME'], 703 | $_SERVER['REQUEST_URI'] 704 | ); 705 | $parts = parse_url($url); 706 | $url = sprintf('%s://%s%s', 707 | $parts['scheme'], 708 | $parts['host'], 709 | $parts['path'] 710 | ); 711 | if ($withqs) { 712 | $url .= '?' . $url['query']; 713 | } 714 | return $url; 715 | } 716 | 717 | function normalise_url($url) { 718 | $parts = parse_url($url); 719 | if ( ! isset($parts['path'])) 720 | $url = $url . '/'; 721 | 722 | return strtolower($url); 723 | } 724 | } 725 | 726 | ?> 727 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | @RelMeAuth 2 | ======== 3 | 4 | A PHP implementation and extension of [relmeauth](http://github.com/tantek/relmeauth). 5 | See [@relmeauth on Twitter](http://twitter.com/relmeauth) 6 | 7 | SubModules 8 | ============ 9 | 10 | RelMeAuth relies on a couple of submodules which have been included as submodules of this 11 | git repository. To ensure you have the submodules installed remember to initialize them with: 12 | 13 | git submodule update --init --------------------------------------------------------------------------------