php オリジナルフレームワーク Lightning Tone モバイルセッションライブラリ

 PHP3から10年近くphpを使い続け、その間にコツコツ作っていたオリジナルのphpのオリジナルフレームワーク『Lightning Tone』。
 ずっと整理がつかずそのままになっていて公開する機会がなかったのですが、何かの参考になるかもしれませんのでその一部を公開します。
 まずはこのフレームワークから、セッションライブラリを抜粋して紹介します。
 このセッションライブラリの特徴は、携帯端末IDベースのセッションを実現しているところです。

Ruby on Rails携帯サイト開発技法

Ruby on Rails携帯サイト開発技法

これはRuby on Rails携帯サイト開発技法で紹介されている方法ですね。尚、使用にあたっては携帯キャリアのIPアドレス帯域のアクセス制限前提です。携帯サイトの場合、携帯端末IDを使った簡単ログイン機能は必須ですし、下手に簡単ログイン+PHP標準のセッション(trans_sid)を使うと、他人のセッションを乗っ取られる可能性が高いので、携帯キャリアのIPアドレス帯域のアクセス制限前提ですが、携帯端末IDベースのセッションの方が安全性は高いのかなと個人的には思っています。

携帯端末IDベースのセッションをphpで作ろうとするとphp標準のセッション関数をカスタマイズすることでは実現ができない為、セッションの仕組みそのものをphpで一から作るしかありません。このライブラリではphpの標準セッション関数のパラメータをそのまま利用しつつ、携帯端末IDに対応したphp互換のセッション関数を自前で実装しています。

特徴

  • 携帯端末IDベースのセッションに対応
  • 既存の標準phpセッションのパラメータを利用
  • 指定された複数ドメイン間のセッションの受け渡しに対応
  • セッションの保存先データの圧縮・暗号化対応
  • セッションの保存先名の暗号化

使い方

  • 呼び出し方
<?php
$ss = new ss();
  • セッションに値を入れる
<?php
$ss->count=1;
//php標準のセッション変数でも可能
$_SESSION['count']=1;
  • セッションのパラメータ設定
<?php
// _(アンダーバー)をprefixに付けるとセッションのパラメータを設定できる)
$ss->_name='sess_id';
//ini_setでphp標準のセッションパラメータ変更もできる。
ini_set('session.name','sess_id');

免責事項

  • 公開するにあたって何個か独自定義の関数がない為このままでは動きません。参考程度にして頂ければと思います。
  • 携帯端末の判定に$ENV['IS_MOBILE']を利用しています。docomo,au,softbankが入ります。事前にキャリアのIPアドレス帯をチェックして入れておきます。
<?php

class ss{
  const serialize_php      = 0; // phpのserialize関数
  const serialize_session  = 1; // phpのセッション互換のserialize関数(デコードが不完全)
  const session_id_custom  = 0; // 超圧縮・高レベル乱数系・サムチェック付き 
  const session_id_php     = 1; // phpのセッション互換のセッション関数(サムチェックなし)

  var $_prefix    = 'sess_';
  var $__id       = '';
  var $_check     = array("ua");
  var $_type;     // 利用するセッションIDの種類 get post cookie SN guid subno vuid
  var $_serialize  = self::serialize_php;    
  var $_session_id = self::session_id_custom;
  var $_fname_enc  =  true;
  // セッションの圧縮オプション
  #var $_compress   = "gzcompress"; //デバックがめんどいから一時的に外す
  var $_guid_domain = true;  // ドメイン毎にguidデータを分ける
  var $_is_lock     = true;  // ロックを使う
  var $_fp;                  // ファイルポインタ
  var $_is_through;          // デストラクタでセッションの保存をスルーする為のフラグ
                             // 主にリダイレクトで使用する。
  var $_is_setcookie;        // cookieを送るかどうか
  var $__name     = "sid";   // PHPSSIDは長すぎる
  var $_is_debug  = true;    // デバックモード

  function __construct($id = null,$name = null,$save_path = null){
    if(isset($id       )){ $this->_id        = $id;        }
    if(isset($name     )){ $this->_name      = $name;      } 
    if(isset($save_path)){ $this->_save_path = $save_path; } 
    // 環境変数自動読み込み
    $this->_trust_domains = $GLOBALS['ss']["trust_domains"];
    if(!$this->_save_path){
      $this->_save_path = HOME."/tmp";
    }
    // 永続的cookie 
    $this->_cookie_lifetime = strtotime("2037/01/01");

    ini_set("url_rewriter.tags",
    "a=href,area=href,frame=src,input=src,form=fakeentry,fieldset=");

    // ドメイン設定
    
    // レンタルサーバーのサブドメイン利用の場合
    if(eregi("sakura.ne.jp$",$_SERVER["HTTP_HOST"])){
      $this->_cookie_domain = $_SERVER["HTTP_HOST"];
    }else{
      $dp = explode('.',$_SERVER["HTTP_HOST"]);
      while(count($dp)>2){
        array_shift($dp);
      }
      $this->_cookie_domain = implode('.',$dp);
    }
    // 大手プロバイダのスペースを使わない場合は問題なし
    $this->_cookie_path = '/';

    //$sessPath   = ini_get('session.save_path'); 
    //$sessCookie = ini_get('session.cookie_path'); 
    //$sessName   = ini_get('session.name');

    if($this->__name){$this->_name = $this->__name;}
    
    if($_ENV["IS_MOBILE"]){
      $init_method ='_init_'.$_ENV["IS_MOBILE"];
      if(method_exists($this,$init_method)){
        call_user_func(array($this,$init_method));
      }
    // PC
    }else{
      //  session_start()
      // trans_sidはoff
      $this->_is_set_cookie = true;
    }    

    if($this->__id){
      // 端末IDを常に取得できる場合
      $this->_prefix ='';
    }else{
      if(count($_COOKIE)>0){
        // クッキーが使える場合
        if($_COOKIE[$this->_name]){
          $this->__id = $_COOKIE[$this->_name];
          $this->_type = "cookie";
          if($_GET[$this->_name]){
            $this->is_through = true;
            $this->del_trans_sid_redirect();
          }
        }
      }else{
        if($_POST[$this->_name] or
            $_GET[$this->_name]){
          if(    $_POST[$this->_name]){
            $this->_type = "post";
            $this->__id = $_POST[$this->_name];
          }elseif($_GET[$this->_name]){
            $this->_type = "get";
            $this->__id = $_GET[ $this->_name];
          }
        }else{
          if($this->_is_set_cookie){
            $this->setcookie();
          }
          $this->is_through = true;
          $this->add_trans_sid_redirect();
        }
      }
    }
    $this->_read();
    $_ENV["rewrite_vars"][session_name()] = session_id();
    if(count($_ENV["rewrite_vars"])>0){
      ob_start(array($this,"url_rewrite"));
    }

    // PHP4だったらデストラクタをエミュレート
    if(version_compare(PHP_VERSION, '5.0.0','<')){
      register_shutdown_function(array(&$this, '__destruct'));
    }
  }

  function __get($name){
    switch($name){
      case '_name':{
        return session_name();
      }
      case '_id':{
        $this->__id =
        session_id()?
        session_id():(
         $this->__id?
         $this->__id:
         $this->create_session_id()
        );
        session_id($this->__id);
        return $this->__id;
      }
      case '_save_path':{
        return session_save_path()?
               session_save_path():
               $this->_save_path;
      }
      case '_gc_probability':  // 1
      case '_gc_divisor':      // 100
      case '_gc_maxlifetime':  // 1440
      case '_cookie_lifetime': 
      case '_cookie_path':
      case '_cookie_domain': 
      case '_cookie_secure': 
      case '_cookie_httponly':{ 
        return ini_get('session.'.ltrim($name,'_'));
      }
      default:{
        return $_SESSION[$name];
      }
    }
  }

  function __set($name,$value){
    //http://www.php.net/manual/ja/function.ini-get-all.php
    //ini_get_all("saession");
    switch($name){
      case '_name'     :{session_name($value);break;}
      case '_id'       :{session_id(  $value);break;}
      case '_save_path':{if(session_save_path()){
                            session_save_path($value);
                         }else{
                            $this->_save_path=$value;
                        }break;}
      case '_gc_probability': // 1
      case '_gc_divisor':     // 100
      case '_gc_maxlifetime': // 1440
      case '_cookie_lifetime': 
      case '_cookie_path':
      case '_cookie_domain': 
      case '_cookie_secure': 
      case '_cookie_httponly':{ 
        return ini_set('session.'.ltrim($name,'_'),$value);
      }
      default:{
        return $_SESSION[$name] = $value;
      }
    }
  }

  function __call($name,$args){
    
  }

  function __destruct(){
    if(!$this->_is_through){
      $this->_write();
    }
  }

  function _init_docomo(){
    $_ENV["rewrite_vars"]["guid"] = "on";
    //output_add_rewrite_var("guid","on");
    //非SSLの場合
    if($_GET["guid"] == "on"){
      if ( $_SERVER["HTTP_X_DCMGUID"]){
        $this->__id = $_SERVER["HTTP_X_DCMGUID"];
        $this->_type = "guid";
      }elseif(!$_GET[$this->_name]){
        $this->is_through = true;
        $this->add_trans_sid_redirect();
      }
    }else{
      $this->is_through = true;
      self::add_guid_on_redirect();
    }
    // guid
    // ser icc 
    // docomo_uid 
  }

  function _init_au(){
    if ( $_SERVER["HTTP_X_UP_SUBNO"]   ){
      $this->__id = $_SERVER["HTTP_X_UP_SUBNO"];
      $this->_type = "subno";
    }else{
      $this->_is_set_cookie = true;
    }
  }

  function _init_softbank(){
    // 非SSL
    if ( $_SERVER["HTTP_X_JPHONE_UID"]){
      $this->__id = $_SERVER["HTTP_X_JPHONE_UID"];
      $this->_type = "juid";
    }elseif(preg_match('|/(SN[0-9A-Za-z]{15})|',
      $_SERVER["HTTP_USER_AGENT"],$match)){
      $this->__id = $match[1];
      $this->_type = "SN";
    }else{
      $this->_is_set_cookie = true;
    }
  }
  
  function setcookie(){
    setcookie(
      $this->_name,
      $this->_id,
      $this->_cookie_lifetime,
      $this->_cookie_path,
      $this->_cookie_domain,
      $this->_cookie_secure,
      $this->_cookie_httponly
    );
  }

  static function add_guid_on_redirect(){
    self::change_queries_redirect(array("guid"=>"on"));
  }

  function add_trans_sid_redirect(){
    self::change_queries_redirect(array($this->_name=>$this->_id));
  }

  function del_trans_sid_redirect(){
    self::change_queries_redirect(array($this->_name));
  }

  static function change_queries_redirect($change_queries){
    // リダイレクト先のURLからクエリーを抽出
    $rurl = $_SERVER["REQUEST_URI"];
    //$query = parse_url($rurl,PHP_URL_QUERY);
    $urls = parse_url($rurl);
    $query = $urls["query"];
    parse_str($query,$queries);

    // リダイレクトURLからクエリーを取り除く    
    $urls = explode('?',$rurl);
    $rurl = array_shift($urls);
    // 成果トラッキング用にセッションIDを引き渡す

    // 渡された配列が『数字配列』だったら『要素の削除』
    if(is_numeric(current(array_keys($change_queries)))){
      foreach($change_queries as $key){
        unset($queries[$key]);
      }
    }else{
    // 渡された配列が『文字配列』だったら『要素の上書き』
      if ( is_array($change_queries) ){
        $queries = array_merge($queries,$change_queries);
      }
    }

    if(count($queries)>0){
      $query = "?".http_build_query($queries);
      if ( $_ENV["IS_MOBILE"] == "softbank" ){
        self::softbank3g_query_escape($query);
      }
    }else{
      $query='';
    }

    $loc   = 'http://'.$_SERVER["HTTP_HOST"].$rurl.$query;
    //echo $loc;print_r($change_queries);exit;
    header("Location: ".$loc);
    exit;
  }

  // 公式CPに予約されているクエリーをURLエンコードして
  // 回避する関数
  // see ... http://labs.unoh.net/2006/10/softbank.html
  // see ... http://www.geeklog.jp/forum/viewtopic.php?showtopic=9027

  static function softbank3g_query_escape(&$query){
    $softbank3g_reserved_queries = array(
      'pid','sid','uid','lid','gid','rpid',
      'rsid','nl','cl','ol','pl','jsky(*)',
      'prc','cnt','reg','vsekey','vsernk',
    );

    $tmp_regex = '('.implode('|',array_map('preg_quote',$softbank3g_reserved_queries)).')';
    //$regex = '/('.$tmp_regex.'=/e';
    $regex = array(
      '/^()'    .$tmp_regex.'([=&])/e', // 行頭
      '/([\?&])'.$tmp_regex.'([=&])/e', // 中間
      '/([\?&])'.$tmp_regex.   '()$/e', // 行末
      '/^()'    .$tmp_regex.   '()$/e', // 行頭行末(単一)
    );
    $query = preg_replace($regex,"'\\1'.allurlencode('\\2').'\\3'",$query);
  }

  static function unserialize($str){
    if($str=='') return $str;
    // 正規表現で頑張るにはちとツライ
    // 補正かけて頑張っているが、まだ漏れがあるかも
    // 本当は字句解析すべきだが...
    $array = array();

    //正規表現で行頭};が引っかかるのでそれのfix
    $fix_chr='';
    if(in_array($str[0],array('}',';'))){
      $fix_chr = $str[0];
      $str = substr($str,-(strlen($str)-1));
    }
    $a = preg_split("/([^|};][^|;]*)\|/", $str, -1,
        PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
    $a[0]=$fix_chr.$a[0];
    for($i = 0; $i < count($a); $i = $i+2) {
       // 配列のキーの行頭が{;だった時の対策
       // unserializeが、行末のゴミを無視することを利用して
       // 差分からfix文字列を取得する
       if(serialize(unserialize($a[$i+1]))!=$a[$i+1]){
         // 文字列の差分を取得
         $fix_chr = str_replace(serialize(unserialize($a[$i+1])),'',$a[$i+1]);
         $a[$i+2]=$fix_chr.$a[$i+2];
       }
       $array[$a[$i]] = unserialize($a[$i+1]);
    }
    return($array);
  }

  static function serialize($array){
    foreach($array as $key => $value){
      $str[]= $key.'|'. serialize($value);
    }
    return implode('',$str);
  }


  // ハッシュ取得 
  function get_file_hash($str){
    return anagram(rtrim(base64_url_encode(sha1($str,true)),','));
  }

  function _read($key=null){
    $key   = $key  ?$key  :($this->_prefix.$this->_id);
    if($this->_fname_enc) $key = $this->get_file_hash($key); 
    $path = $this->_save_path.'/'.$key;
    if(is_readable($path)){
      //get_object_varsは使えない
      //$_SESSION = unserialize(file_get_contents($path));
      if($this->_is_lock){
        $this->_fp = fopen($path,"r+");
        // 書き込みバッファを0にセット
        stream_set_write_buffer($this->_fp,0);
        // ファイルをロック
        flock($this->_fp,LOCK_EX);
        $data='';
        while(!feof($this->_fp)){
          $data .= fgets($this->_fp,4096);
        }
      }else{
        $data = file_get_contents($path);
      }
      if($this->_compress){
        $data = gzuncompress($data);
      }
      switch($this->_serialize){
        case self::serialize_php:{
          $_SESSION = unserialize($data);
          break;
        }
        case self::serialize_session:{
          $_SESSION = self::unserialize($data);
          break;
        }
      }
      
      if(is_array($this->_check) and
            count($this->_check)>0){
        foreach($this->_check as $check){
          switch($check){
            case "ua":{
              if($_SESSION["HTTP_USER_AGENT"]!=
                  $_SERVER["HTTP_USER_AGENT"]){
                $_SESSION = array();
                $this->__id = null;
              }
              break;
            }
          }
        }
      }
    }elseif($this->_is_lock){
      $this->_fp = fopen($path,"w");
      // 書き込みバッファを0にセット
      stream_set_write_buffer($this->_fp,0);
      return false;
    }else{
      return false;
    }
  }

  function _write($key=null,$value=null){
    // $_SESSIONをマージしておく
    $value = $value?$value:$_SESSION;
    $value["HTTP_USER_AGENT"] = $_SERVER["HTTP_USER_AGENT"];
    $value["REMOTE_ADDR"]     = $_SERVER["REMOTE_ADDR"];
    if($_SERVER["HTTP_X_DCMGUID"]   )
         $value["HTTP_X_DCMGUID"]   =$_SERVER["HTTP_X_DCMGUID"];
    if($_SERVER["HTTP_X_JPHONE_UID"])
         $value["HTTP_X_JPHONE_UID"]=$_SERVER["HTTP_X_JPHONE_UID"];
    
    $key   = $key  ?$key  :($this->_prefix.$this->_id);
    if($this->_fname_enc) $key = $this->get_file_hash($key); 
    $path  = $this->_save_path.'/'.$key;
    switch($this->_serialize){
      case self::serialize_php:{
        $str = serialize($value);
        break;
      }
      case self::serialize_session:{
        $str = self::serialize($value);
        break;
      }
    }
    if($this->_compress){
      $str = gzcompress($str,9);
    }
    if($this->_is_lock){
      // ファイルポインタを先頭に移動
      rewind($this->_fp);
      fwrite($this->_fp,$str);
      // ファイルをアンロック
      flock($this->_fp,LOCK_UN);
      fclose($this->_fp);
    }else{
      file_put_contents($path,$str);
    }
    $this->_gc_trigger();
  }
 

  // すべてのセッション変数を開放する
  //function unset(){
  //  $_SESION = array();
  //  session_unset();
  //  session_destroy();
  //}

  // すべてのセッションを破棄する
  function destroy(){
    $_SESSION = array();
    session_destroy();
  }

  function create_session_id(){
    switch($this->_session_id){
      case self::session_id_custom:{
        // mt_srand(microtime()*100000); 必要なくなったらしい
        $raw = sha1(uniqid(mt_rand(),true),true);
        $sum = chr(crc32($raw)<<24);
        $raw = $raw.$sum;
        $session_id = anagram(rtrim(base64_url_encode($raw),','));
        return $session_id;
      }
      case self::session_id_php:{
        // PHP互換
        return md5(uniqid((isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''), true));
      }
    }
  }

  static function check_session_id($session_id){
    $decode = (base64_url_decode(deanagram($session_id)));
    $raw = substr($decode,0,(strlen($decode)-1));
    $sum = substr($decode,-1,1);
    if(chr(crc32($raw)<<24)==$sum ){
      return true;
    }else{
      return false;
    }
  }

  function _gc_trigger(){
    //echo (rand(1,$this->_gc_divisor)."/".$this->_gc_probability)."<br>";
    if(rand(1,$this->_gc_divisor) <= $this->_gc_probability){
      $this->_dc();
    }
  }

  // ガベージコレクション
  function _gc(){
    clearstatcache(); // ファイル情報をフラッシュ 
    // 基本的にセッション系のみ除去
    foreach( glob($this->_save_path."/".$this->_prefix."*") as $f){
      $elapsed = (int)(time()-filemtime($f));
      if($elapsed > $this->_gc_maxlifetime){
        if(is_writeable($f)){
          unlink($f);
        }
      }
    }
  }

  /* バッファ関数 */

  function check_add_vars($urls){
    //cookieありのときと無しの時で動作が異なる
    //cookieはクロスドメインでないので
    //SSL 切り替え時やトラストドメインの場合だけ
    // trans_sidの場合は、上記の場合と
    // 相対アドレス、自ホストの場合
    // Cookie = on の時
    // (1) 信頼ドメイン
    // (2) SSL切り替え時
    // trans_sid empty($_COOKIE[session_name()])
    // (3) 相対アドレス
    // (4) 自ホスト
    // 絶対パス

    extract($urls);
    extract($_SERVER);
    if( (in_array($scheme,array("http","https")) and
       // 同一ドメインでSSL ⇔ 非SSL 切り替え時
        ( $HTTP_HOST == $host and (
          ($HTTPS == "on" and $scheme == "http") or
          ($HTTPS != "on" and $scheme == "https")
         // 信頼ドメインだった場合
         // 文末matchだから実際はregex使ったりとちと面倒
         ) or in_array($host,$this->_trust_domains)
           or (empty($_COOKIE[session_name()]) and
              $HTTP_HOST == $host
         )
        )
      )or(empty($host) and empty($_COOKIE[session_name()]))
    ){
      return true;
    }
    return false; 
  }

  function get_pram_from_matches($m){
    if     ($m[2]){
     $url = $m[2];$q='';
    }elseif($m[3]){
     $url = $m[3];$q='"';
    }elseif($m[4]){
     $url = $m[4];$q="'";
    }
    return array($url,$q);
  }

  function link_rewrite($matches){
    
    // formの場合だけ、コンテンツと閉じタグ取得
    $close_tag_etc = $matches[6];
    if($close_tag_etc){
      $html = new html($matches[0]);
      $form = ($html->tag("form"));
    }

    list($url,$q) = $this->get_pram_from_matches($matches);
    $urls = url::parse($url);
    if($this->check_add_vars($urls)){
      $add_entry='';
      foreach($_ENV["rewrite_vars"] as $name => $value){
        // 携帯でmethodがpostの場合はactionに付加
        if( empty($close_tag_etc) or
           (strtolower($form["method"]) == "post" and $_ENV["IS_MOBILE"])){
          $urls["queries"][$name] = $value;
        // それ以外の場合は通常通りhiddenを追加
        }else{
          $add_entry.='<input type="hidden" name="'.$name
                    . '" value="'.$value.'">';
        }
      }
      if(empty($add_entry)){
          $url = url::build($urls);
      }
    }

    $tag = $matches[1].$q.$url.$q.$matches[5].$add_entry.$close_tag_etc;
    return $tag;
  }


  function url_rewrite($buf){
    //url_rewriter.tags
    //a=href,area=href,frame=src,input=src,form=fakeentry,fieldset=
    //とりあえずfieldsetは放置意味分からん
    $tag = ini_get("url_rewriter.tags");
    $tmps = explode(",",$tag);
    foreach($tmps as $tmp){
      list($k,$v) = explode('=',$tmp);
      $tags[$k] = $v;
    }
    foreach($tags as $tag => $elm){
      //$tag = 'a'; $elm = 'href';
      if($elm and $elm!="fakeentry"){
        $regex = "%(<".$tag.".*?".$elm."\s*=\s*)"
               . "(?:([^\s\"'>]+)|\"([^\"]+)\"|'([^']+)')"
               . "((?:[^'\">]|\"[^\"]*\"|'[^']*')*>)%is";
        //preg_match($regex,$buf,$matches); print_r($matches); //regexテスト
        $buf   =  preg_replace_callback($regex,array($this,'link_rewrite'),$buf);
      }elseif($elm=="fakeentry"){
        $tag = "form";
        $elm = "action";
        //form hidden
        $regex = "%(<".$tag.".*?".$elm."\s*=\s*)"
               . "(?:([^\s\"'>]+)|\"([^\"]+)\"|'([^']+)')"
               . "((?:[^'\">]|\"[^\"]*\"|'[^']*')*>)(.*?</".$tag.">)%is";
        //preg_match($regex,$buf,$matches); print_r($matches); //regexテスト
          //$buf   =  preg_replace_callback($regex,array($this,'link_rewrite'),$buf);
        $buf   =  preg_replace_callback($regex,array($this,'link_rewrite'),$buf);
      }
    }
    //echo $buf."\n<br>";
    return $buf;
  }

}