s238Note.php
<?php

use Michelf\MarkdownExtra;

/**
 * ノート
 * @author fkit
 * @version 2014.12
 */
class s238Note
{
    const   FILE_CONFIG = '_contents.conf';                         //設定ファイル名
    const   KWD         = '!!';                                     //キーワードの囲み文字
    const   FILEMTIME   = s238Note::KWD.'filemtime'.s238Note::KWD;  //ファイル更新日付
    const   DEF_TITLE           = '- Untitled -';                   //デフォルト値 グループとノートのタイトル
    const   DEF_NOTE_SUMMARY    = '';                               //デフォルト値 ノートの概要
    const   DEF_NOTE_CREATE_DT  = '----/--/-- --:--';               //デフォルト値 ノートの作成日時
    const   DEF_NOTE_UPDATE_DT  = s238Note::FILEMTIME;              //デフォルト値 ノートの更新日時
    const   DEF_NOTE_REFERENCE  = '';                               //デフォルト値 ノートの参照元
    
    
    /**
     * @var string      $items      ノートの本文以外の項目 ...see:__construct()
     * @var string      $parser     ノートの本文のパーサ
     * @var array       $summary    ノート概要データ()
     * @var object      $cacheObj   Zend_Cache
     * @var array       $cacheInfo  array('summaryId'=>'xxx', 'noteId'=>'xxx', 'remove'=true/false)
     */
    private $items, $parser, $summary, $cacheObj, $cacheInfo;
    
    
    /**
     * コンストラクタ
     * @parrm   string      $inputDir       ノートを格納しているディレクトリ
     * @paran   array       $cacheConf      キャッシュパラメタ array('lifetime'=>キャッシュ保存期間(秒),'cache_dir'=>キャッシュ保存先ディレクトリ,'id'=>キャッシュID)
     * @paran   boolean     $cacheRemove    ノートの読み込み前にキャッシュを削除する
     */
    function __construct($inputDir, $cacheConf=array(), $cacheRemove=false)
    {
        //ノートの本文以外の項目のデフォルト値を設定
        $this->items['title']       = s238Note::DEF_TITLE;          //タイトル
        $this->items['summary']     = s238Note::DEF_NOTE_SUMMARY;   //概要
        $this->items['create_dt']   = s238Note::DEF_NOTE_CREATE_DT; //作成日時
        $this->items['update_dt']   = s238Note::DEF_NOTE_UPDATE_DT; //更新日時
        $this->items['reference']   = s238Note::DEF_NOTE_REFERENCE; //参照元
        
        //ノートの本文のパーサ
        if (strpos(implode('', get_included_files()), 'MarkdownExtra') > 0){
            $this->parser   = function ($val) {return MarkdownExtra::defaultTransform($val);};  //Markdown
        }else{
            $this->parser   = function ($val) {return nl2br($val);};                            //nl2br
        }
        
        //cache設定
        $cacheObj   = null;
        if (strpos(implode('', get_included_files()), 'Zend/Cache') > 0 && $cacheConf != array()){
            $this->cacheInfo['summaryId']  = "{$cacheConf['id']}__SUMMARY__";
            $this->cacheInfo['noteId']     = "{$cacheConf['id']}__NOTE__";
            $this->cacheInfo['remove']     = $cacheRemove;
            
            $this->cacheObj  = Zend_Cache::factory(
                'Core', 'File',
                array('lifetime' => $cacheConf['lifetime'], 'automatic_serialization'=>true),
                array('cache_dir'=> $cacheConf['cache_dir'])
            );
        }
        
        //全ノートの概要の読み込み
        if ($this->cacheObj == null){
            $this->summary  = $this->readAllSummary($inputDir);
        }else{
            if ($this->cacheInfo['remove']){    $this->cacheObj->remove($this->cacheInfo['summaryId']);}
            
            if (false === ($cacheData = $this->cacheObj->load($this->cacheInfo['summaryId']))){
                $cacheData  = $this->readAllSummary($inputDir);
                $this->cacheObj->save($cacheData, $this->cacheInfo['summaryId']);
            }
            
            $this->summary  = $cacheData;
        }
    }
    
    
    /**
     * 全〜ノートの概要を取得
     * @parrm   string  $gId    グループID
     * @parrm   string  $nId    ノートID
     * @return  array           概要情報
     */
    public function getSummary($gId=null, $nId=null)
    {
        if (null != $gId && null != $nId)   return $this->summary[$gId]['notes'][$nId]; //ノート概要
        if (null != $gId && null == $nId)   return $this->summary[$gId];                //グループ概要
        if (null == $gId && null == $nId)   return $this->summary;                      //全概要
        return null;
    }
    
    
    /**
     * ノートの詳細を取得
     * @param   string  $gId    グループID
     * @parrm   string  $nId    ノートID
     * @return  array           ノート情報
     */
    public function getNote($gId, $nId)
    {
        //概要
        $ret    = $this->summary[$gId]['notes'][$nId];
        
        //本文
        if ($this->cacheObj == null){
            $ret['content'] = $this->readNote($gId, $nId);
        }else{
            $id = "{$this->cacheInfo['noteId']}__{$gId}__{$nId}__";
            if ($this->cacheInfo['remove']){    $this->cacheObj->remove($id);}
            
            if (false === ($cacheData = $this->cacheObj->load($id))){
                $cacheData  = $this->readNote($gId, $nId);
                $this->cacheObj->save($cacheData, $id);
            }
            
            $ret['content'] = $cacheData;
        }
        
        return $ret;
    }
    
    
    /**
     * ページの存在確認
     * @parrm   string  $file404    404ページのファイル
     * @parrm   string  $gId        グループID
     * @parrm   string  $nId        ノートID
     */
    public function pageExists($file404, $gId=null, $nId=null)
    {
        if ((null != $gId && null == $nId && !isset($this->summary[$gId])) ||
            (null != $gId && null != $nId && !isset($this->summary[$gId]['notes'][$nId]))){
            header("HTTP/1.0 404 Not Found");
            require_once($file404);
            die();
        }
    }
    
    
    /**
     * キャッシュ状況の取得
     * @return  string	'on' or 'of-nc' or 'of-rm'
     */
    public function getCacheState()
    {
        if (null == $this->cacheObj)    return 'of-nc'; //キャッシュ環境がない
        if ($this->cacheInfo['remove']) return 'of-rm'; //キャッシュが削除された
        return 'on';
    }
    
    
    /**
     * キャッシュクリア判定
     * @parrm   string[]    $iplist キャッシュを無効にするIPアドレス ex) array('111.111.111.0/24', '222.222.222.222/0', ...)
     * @return  bool
     */
    public static function isCacheRemove($iplist=array())
    {
        if (isset($_GET['nocache']))    return true;            //urlパラメタ"?nocache"がある場合はキャッシュクリア
        return self::checkIp($_SERVER['REMOTE_ADDR'], $iplist); //ipアドレスが適合したらキャッシュクリア
    }
    
    
    // ------------------------- private ------------------------- //
    
    
    /**
     * ノートの概要の読み込み
     * @param   string  $inputDir  ディレクトリ
     * @return  array              全グループ・全ノートの概要情報
     */
    private function readAllSummary($inputDir)
    {
        clearstatcache();
        
        //グループディレクトリ毎にファイル名を取得する
        $dirFiles   = array();
        foreach (glob(rtrim($inputDir, '/'). '/*', GLOB_ONLYDIR) as $subDir) {
            if (substr(basename($subDir), 0, 1)=='_')   continue;   //'_'ではじまるディレクトリは非表示なのでスキップ
            foreach (glob($subDir. '/*') as $file) {                //空ディレクトリは以降のループに入らない
                if (!is_file($file))                    continue;   //ファイルのみ対象
                if (substr(basename($file), 0, 1)=='_') continue;   //'_'ではじまるファイルは非表示なのでスキップ
                
                $dirFiles[basename($subDir)][basename($file)]= $file;
            }
        }
        
        //ノートデータの取得
        foreach ($dirFiles as $dirKey => $files){
            //ノートグループ設定ファイルを処理
            if (file_exists("{$inputDir}/{$dirKey}/". s238Note::FILE_CONFIG)){
                //取得
                $configVal  = $this->getGroupConfig("{$inputDir}/{$dirKey}/". s238Note::FILE_CONFIG);
                //セット
                $dirFiles[$dirKey]  = $configVal;
            }
            
            //グループのデフォルト値設定...hint:項目を増やす場合はココと$this->getGroupConfig()に追加する
            if (!isset($dirFiles[$dirKey]['title']))    $dirFiles[$dirKey]['title'] = s238Note::DEF_TITLE;
            if (!isset($dirFiles[$dirKey]['summary']))  $dirFiles[$dirKey]['summary']= 'グループディレクトリ下の設定ファイル('. s238Note::FILE_CONFIG. ')の"summary"がありません。';
            
            //各ノートの処理
            foreach ($files as $fileKey => $file){
                if (s238Note::FILE_CONFIG == $fileKey)  continue;   //設定ファイルはスキップ
                unset($dirFiles[$dirKey][$fileKey]);                //"グループ/ファイル名"はもう必要ないので削除
                $dirFiles[$dirKey]['notes'][strtr($fileKey, '.', '_')]  = $this->readNoteSummary($file);
            }
        }
        
        //keywordを値に変換
        foreach ($dirFiles as $dirKey => $dirVals){
            foreach ($dirVals['notes'] as $noteKey => $noteVals){
                foreach ($noteVals as $noteItemKey => $noteItemVal){
                    $dirFiles[$dirKey]['notes'][$noteKey][$noteItemKey] = $this->keyword2Value($dirFiles[$dirKey]['notes'][$noteKey][$noteItemKey], $dirFiles, $dirKey, $noteKey);
                }
            }
        }
        
        return $dirFiles;
    }
    
    
    /**
     * グループ設定ファイルから値取得
     * @param   string $file    ファイル名(フルパス)
     * @return  array           array('title'=>'タイトル', 'summary'=>'概要')
     */
    private function getGroupConfig($file)
    {
        $ret    = array();
        
        foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $v){
            $line   = trim($v);
            if ('title'     == substr($line, 0, 5)) $ret['title']   = trim(substr($line, strpos($line, ':')+1));
            if ('summary'   == substr($line, 0, 7)) $ret['summary'] = trim(substr($line, strpos($line, ':')+1));
            //hint:項目を増やす場合はココと$this->readAllSummary()のデフォルト値設定に追加する
        }
        
        return $ret;
    }
    
    
    /**
     * ノートファイルから本文以外の情報を取得
     * @param   string  $file   ファイル名(フルパス)
     * @return  array           ノートの概要情報
     */
    private function readNoteSummary($file)
    {
        //概要項目をセット
        $ret        = $this->items; //デフォルト値
        $ret['file']= $file;        //ファイルのフルパス
        
        //ノートに値があったらデフォルト値を上書き
        foreach (file($file) as $v){
            $line   = trim($v);
            if ('' == $line)    continue;
            
            $key    = substr($line, 0, strpos($line, ':'));
            $val    = (string)substr($line, (strpos($line, ':')+1));    //値なしだとfalseになるのでstringでキャスト
            
            if ('note' == $key) break;  //本文以降はスキップ
            
            if ('' != $val && isset($ret[$key]))    $ret[$key]  = $val;
        }
        
        return $ret;
    }
    
    
    /**
     * ノート情報を取得
     * @param   string  $gId    グループID
     * @parrm   string  $nId    ノートID
     * @return  string          ノートの内容
     */
    private function readNote($gId, $nId)
    {
        $isNote = false;
        $note   = '';
        foreach (file($this->summary[$gId]['notes'][$nId]['file']) as $v){
            $line   = trim($v);
            if (substr_count($line, 'note:-') > 0)  $isNote = true;
            if (substr_count($line, '-:note') > 0)  $isNote = false;
            if (substr_count($line, 'hide:-') > 0)  $isNote = false;
            if (substr_count($line, '-:hide') > 0)  $isNote = true;
            if ($isNote){
                if (substr_count($line, 'note:-') > 0 || substr_count($line, '-:note') > 0) continue;
                if (substr_count($line, 'hide:-') > 0 || substr_count($line, '-:hide') > 0){
                    $note  .= "[非公開]\n\n";
                    continue;
                }
                $note   .= $v;
            }
        }
        
        return call_user_func(
            $this->parser,
            $this->keyword2Value(
                $note,
                $this->summary,
                $gId, $nId
            )
        );
    }
    
    
    /**
     * キーワードを値に変換する
     * @param   string  $str        変換前の値
     * @param   array   $allSummary 全概要の配列
     * @param   string  $gId        グループID
     * @parrm   string  $nId        ノートID
     * @return  string              変換後の値
     */
    private function keyword2Value($str, $allSummary, $gId, $nId)
    {
        $KWD    = s238Note::KWD;
        
        //単純なキーワード
        $strtrAry[s238Note::FILEMTIME]  = date('Y/m/d H:i', filemtime($allSummary[$gId]['notes'][$nId]['file']));       //ファイル日時
        
        //パラメタ付きキーワード:他のノートから値を取得
        if (0 < preg_match_all("/{$KWD}note:(.+?){$KWD}/", $str, $matches, PREG_SET_ORDER)){
            foreach ($matches as $match){
                list($groupKey, $pageKey, $noteItemKey) = explode('/', trim($match[1], '/'));
                if (isset($allSummary[$groupKey]['notes'][$pageKey][$noteItemKey])){
                    $strtrAry[$match[0]]    = $allSummary[$groupKey]['notes'][$pageKey][$noteItemKey];
                }
            }
        }
        
        //パラメタ付きキーワード:外部fileの読み込み
        if (0 < preg_match_all("/{$KWD}file:(.+?){$KWD}/", $str, $matches, PREG_SET_ORDER)){    
            foreach ($matches as $match){
                if (file_exists($match[1])){
                    $strtrAry[$match[0]]    = htmlspecialchars(file_get_contents($match[1]));
                }
            }
        }
        
        return strtr($str, $strtrAry);      //置換して戻す
    }
    
    
    /*
    * IPアドレスのチェック
    * @param    string      $remoteIp       リモートIP
    * @param    string[]    $allowIpAry     許可IPの配列 ex) array('111.111.111.111/32', '222.222.222.0/24'...)
    * @return   boolean
    */
    private static function checkIp($remoteIp, $allowIpAry)
    {
        foreach ($allowIpAry as $ipAndMask){
            list($allowIp, $mask) = explode("/", trim($ipAndMask));
            
            $remoteLong = ip2long($remoteIp) >> (32 - $mask);
            $allowLong  = ip2long($allowIp)  >> (32 - $mask);
            
            if ($remoteLong == $allowLong)  return true;
        }
        return false;
    }
}