<?
/*
Пример реализации OpenID dumb mode consumer-а на PHP
Используйте как хотите. Цитировать публично лучше со ссылкой.

(c) Давид Мзареулян, 2005
david@hiero.ru
http://www.livejournal.com/users/david_m/

По поводу данного кода можно также писать сюда: http://www.livejournal.com/users/david_m/760821.html


OpenID: http://www.openid.net/

v. 1.06

2008.11.19
1.06 -- Исправлен баг с определением https-ности текущего урла

2005.07.13
1.05 -- Добавлена обработка нового варианта ответа на check_authentication -- поле is_valid
        Понимается и старый вариант (lifetime), поскольку его успели реализовать многие серверы (включая ЖЖ)

2005.07.10
1.04 -- Исправлен баг с потерей сессионной информации в immediate-режиме
        Изменён алгоритм определения URL данного скрипта

2005.07.08
1.03 -- Область поиска LINK-ов ограничена началом страницы (до тега <body...)

2005.07.07
1.02 -- Добавлена обработка HTML-entities в атрибутах тега LINK (спасибо Владимиру Паланту за замечание)

2005.07.06
1.01 -- Поскольку публика восприняла этот _пример_ как боевой вариант, все HTTP-запросы (числом два) теперь делаются через CURL.
        Это позволяет работать с HTTPS, отрабатывать редиректы и т.д. Если у вас нет CURL-а -- замените его чем-нибудь по вкусу,
        хоть fsockopen. Да, в качестве боевого этот код использовать вполне можно (в нём нет упрощений, влияющих на безопасность,
        не считая работы в dumb mode), но всё равно лучше сначала пройтись по нему и понять, что именно он делает. Это не сложно.
        Также добавлена обработка openid.error, о котором в спецификации упомянуто ну очень мелким почерком.

2005.07.04
1.0  -- Начальный вариант.


*/

if(isset($_GET["view-source"])) { highlight_file(__FILE__); exit; }

// определяем URL данной страницы
// он будет использоваться в openid.trust_root и openid.return_to
$isHttps = (boolean)preg_match('/https/i'$_SERVER["SERVER_PROTOCOL"]);
$thisUrl $isHttps "https://" "http://";
$thisUrl .= $_SERVER["SERVER_NAME"];
$thisUrl .= ($_SERVER["SERVER_PORT"] == ($isHttps 443 80)) ? "" ":".$_SERVER["SERVER_PORT"];
preg_match('/^[^?]+/'$_SERVER["REQUEST_URI"], $m);
$thisUrl .= $m[0];
define("THIS_URL"$thisUrl);

/**** РАЗНЫЕ ФУНКЦИИ **************/

function error($message) {
    
$_SESSION["OpenID"]["status"] = $message;
    
header("Location: ".THIS_URL);
    exit;
}
// собирает из хэша строку типа key=val&...
function makeQueryString($params) {
    
$query "";
    foreach(
$params as $k => $v$query .= "&".urlencode($k)."=".urlencode($v);
    return 
substr($query1);
}
// присобачивает к $url строку типа key=val&..., собранную из хэша $params
function appendParams($url$params) {
    
$query makeQueryString($params);
    if(
strpos($url"?") !== false) return $url."&".$query;
    return 
$url."?".$query;
}

// функция, обратная к htmlspecialchars
function unhtmlspecialchars($arg)
{
    if(
is_string($arg)) 
        return 
preg_replace_callback('/&(amp|lt|gt|quot|apos|#(\d+)|#[xX]([a-fA-F\d]+));/'__FUNCTION__$arg);

    
$entities = array("amp" => "&""lt" => "<""gt" => ">""quot" => "\"""apos" => "'");
    if(isset(
$entities[$arg[1]])) return $entities[$arg[1]];
    
// обрабатываем только символы с кодами меньше 256
    
if($arg[2]) $code = (int)$arg[2] & 0xFF;
    if(
$arg[3]) $code hexdec($arg[3]) & 0xFF;
    if(
$code) return chr($code);
    return 
"?";
}

/************************************/

// для удобства заводим сессию
session_start();


// форму, из которой приходит этот POST см. в конце скрипта
if($_POST) {
    
    
$openidUrl trim($_POST["openid_url"]);
    
$checkMode = (int)$_POST["mode"] ? "checkid_setup" "checkid_immediate";

    
// первым делом нужно сходить по подсунутому нам урлу и вытащить адрес сервиса (и, если нужно, delegate-урл)

    
if(!$openidUrlerror("пустой url");
    
// если нужно, дописываем протокол...
    
if(!preg_match('{^[a-z]+://}i'$openidUrl)) $openidUrl "http://".$openidUrl;
    
// ...и финальный слэш
    
if(!preg_match('{^[a-z]+://.*?/}i'$openidUrl)) $openidUrl $openidUrl."/";
    
// разрешаем только HTTP и HTTPS
    
if(!preg_match('{^https?://}i'$openidUrl)) error("некорректный url");
    
// получаем страницу по этому адресу
    // естественно, если нет CURL-а, то можно использовать любой другой механизм
    
$ch curl_init();   
    
curl_setopt($chCURLOPT_URL$openidUrl);
    
curl_setopt($chCURLOPT_HEADERfalse);
    
curl_setopt($chCURLOPT_RETURNTRANSFERtrue);
    
$body curl_exec($ch);
    
$error curl_error($ch);
    
curl_close($ch);

    if(
$error or !$bodyerror("не удалось получить страницу ".htmlspecialchars($openidUrl)." (".htmlspecialchars($error).")");

    
// нас интересуют два LINK-элемента: openid.server и openid.delegate
    // действуем тупо и по-простому
    
$serviceUrl "";
    
$delegateId "";

    
$body preg_replace('/<body\b.*/i'''$body);

    if(
preg_match('/<link\b[^>]+?\brel=([\'"])openid\.server\1.*?>/i'$body$m)) {
        
$link $m[0];
        if(
preg_match('/\bhref=([\'"])(.*?)\1/i'$link$m)) $serviceUrl unhtmlspecialchars($m[2]);
    }
    if(!
$serviceUrlerror("openid.server не найден");
    if(!
preg_match('{^https?://}i'$serviceUrl)) error("некорректный url openid.server");

    if(
preg_match('/<link\b[^>]+?\brel=([\'"])openid\.delegate\1.*?>/i'$body$m)) {
        
$link $m[0];
        if(
preg_match('/\bhref=([\'"])(.*?)\1/i'$link$m)) $delegateId unhtmlspecialchars($m[2]);
    }
    if(!
$delegateId$delegateId $openidUrl;
    
    
// правильность openid.delegate не проверяем -- она на совести автора страницы

    
$_SESSION["OpenID"] = array(
        
"serviceUrl"    => $serviceUrl,
        
"openidUrl"     => $openidUrl,
    );

        
/*
        Лирическое отступление по поводу openid.delegate
            
            В спецификации об этом написано немного туманно, а на самом деле всё просто. Предположим, у вас есть урл,
            которым вы хотите  подписываться (MyURL),  но на сайте этого урла нет OpenID-сервера.  Но зато у вас есть
            авторизация  где-нибудь ещё,  например, в ЖЖ (где OpenID-сервер есть).  Тогда на странице по адресу MyURL
            можно написать:

                <link rel="openid.server"   href="http://www.livejournal.com/openid/server.bml" />
                <link rel="openid.delegate" href="http://www.livejournal.com/users/YOUR_USERNAME/" />

            Что это означает?  Это означает,  что OpenID-consumer реально должен  спрашивать  openid.server про  урл
            openid.delegate. А полученный результат должен трактоваться как бы для MyURL. Т.е. если юзер авторизован
            на openid.delegate, то он считается авторизованным и на MyURL.

            В теории  это позволяет не привязываться к OpenID-серверу и менять его при необходимости, сохраняя MyURL
            постоянным.

        */


    // получили адрес сервиса
    // подготовим запрос...

    
$params = array(
        
"openid.mode"       => $checkMode,
        
"openid.identity"   => $delegateId,
        
"openid.return_to"  => THIS_URL."?return",
        
"openid.trust_root" => THIS_URL
    
);

    
// всё, перекидываем клиента на $serviceUrl с нужными параметрами
    // ждать его обратно будем по адресу THIS_URL."?return"

    
header("Location: ".appendParams($serviceUrl$params));
    exit;
}


if(isset(
$_GET["return"]) and $_SESSION["OpenID"]) {

    
// мы вернулись с опенид-сервиса
    // посмотрим, с каким результатом...

    // кстати, GET-параметры имеют вид openid.something, но PHP их переименовывает в openid_something
    // так что не удивляйтесь, что параметры называются не совсем так, как в спецификации

    
if($_GET["openid_mode"] == "cancel") {

        
// юзер сам запретил сообщать нам информацию о себе
        // имеет право
        
$_SESSION["OpenID"]["status"] = "С нами не захотели разговаривать:(";

    } elseif(
$_GET["openid_mode"] == "error") {

        
// произошла какая-то ошибка
        // вряд ли в этом наша вина, но тем не менее...
        
$_SESSION["OpenID"]["status"] = "Произошла внезапная ошибка: ".htmlspecialchars($_GET["openid_error"]);

    } elseif(
$_GET["openid_mode"] == "id_res") {
        
        
// какой-то результат всё-таки получен

        
if(isset($_GET["openid_user_setup_url"])) {
            
// что-то пошло не так
            // либо у нас таки нет прав на этот урл
            // либо (это может случиться только в режиме checkid_immediate)
            // у сервиса нет разрешения выдавать нам информацию автоматически, 
            // и он даёт URL, по которому следует послать юзера, чтобы тот сам нам всё разрешил
            // (в режиме checkid_setup юзер сам попадает на этот урл)

            
$setupUrl $_GET["openid_user_setup_url"];
            
// добавим параметр openid.post_grant=return, чтобы нас автоматически вернули обратно после разрешения
            
$setupUrl appendParams($setupUrl, array("openid.post_grant" => "return"));
            
$_SESSION["OpenID"]["status"] = "Автоматическая идентификация не удалась. Вам нужно проследовать по адресу <a href=\"".htmlspecialchars($setupUrl)."\">".htmlspecialchars(substr($setupUrl050))."...</a> и определиться с тем, что мы можем, а что нет.";
            
$_SESSION["OpenID"]["dontclear"] = true;
        } else {

            
// кажется, всё в порядке
            // но для полной уверенности надо проверить подпись
            // т.к. мы глупые, то поручим это самому сервису
            
$params = array(
                
"openid.mode"           => "check_authentication",
                
"openid.assoc_handle"   => $_GET["openid_assoc_handle"],
                
"openid.sig"            => $_GET["openid_sig"],
                
"openid.signed"         => $_GET["openid_signed"],
            );
            foreach (
$_GET as $k => $v) {
                if (
strpos($k'openid_') !== 0) continue;
                
$k str_replace('openid_''openid.'$k);
                if (!isset(
$params[$k])) $params[$k] = $v;
            }

            
// надо послать POST-запрос с этими параметрами
            // естественно, если нет CURL-а, то можно использовать любой другой механизм
            
$ch curl_init();   
            
curl_setopt($chCURLOPT_URL$_SESSION["OpenID"]["serviceUrl"]);
            
curl_setopt($chCURLOPT_POSTtrue);
            
curl_setopt($chCURLOPT_HEADERfalse);
            
curl_setopt($chCURLOPT_RETURNTRANSFERtrue);
            
curl_setopt($chCURLOPT_POSTFIELDSmakeQueryString($params));
            
$result curl_exec($ch);
            
curl_close($ch);

            
// ответ придёт в виде строк вида key:val\n
            // разбираем его...
            
$vals = array();
            foreach(
preg_split('/\n/'$result, -1PREG_SPLIT_NO_EMPTY) as $pair) {
                list(
$k$v) = explode(":"$pair2);
                
$vals[$k] = $v;
            }

            
// нас интересует только один параметр ответа — lifetime
            
if(
                isset(
$vals["lifetime"]) and (int)$vals["lifetime"] > or
                isset(
$vals["is_valid"]) and $vals["is_valid"] == "true"
            
) {
                
$_SESSION["OpenID"]["status"] = "Поздравляем, Вы и в самом деле <b>".htmlspecialchars($_SESSION["OpenID"]["openidUrl"])."</b>!";
            } else {
                
$_SESSION["OpenID"]["status"] = "Либо цифровая подпись протухла, либо <b>".htmlspecialchars($_SESSION["OpenID"]["openidUrl"])."</b> — это не Ваш урл.";
            }
        }
    }

    
header("Location: ".THIS_URL);
    exit;
}



?>
<html>
<head>
    <title>OpenID Consumer example</title>
</head>
<body>
    <blockquote>
    <?if($_SESSION["OpenID"]["status"]){?>
    <p style="color: #900"><?=$_SESSION["OpenID"]["status"]?></p>
    <?
        
if(!$_SESSION["OpenID"]["dontclear"]) $_SESSION["OpenID"] = null; else $_SESSION["OpenID"]["dontclear"] = false;
    }
?>
    <form method="POST" action="<?=THIS_URL?>">
        Какой URL будем проверять?
        <br />
        <input type="text" name="openid_url" size="60">
        <input type="submit" value="Check It!">
        <br />
        Метод проверки:
        <br />
        <select name="mode">
            <option value="1">checkid_setup (человекопонятный)</option>
            <option value="0">checkid_immediate (автоматопонятный)</option>
        </select>
    </form>
    </blockquote>
    <a href="<?=THIS_URL?>?view-source">view source</a>
</body>
</html>