PHPとメモ帳で掲示板をつくってみた話


#はじめに

ファイルは[こちら](https://github.com/Yomogenium/WordTile_Lite/tree/work)からダウンロードできます。御自由にお使いください。


#ファイル構造と各部の解説

wordtile lite
├─css ……文字通りcssファイルを入れるとこ。ありていに言えば見た目を装飾するものなので、なくても動作するし実際いま入れてない
├─log
│  └─log.txt ……投稿された情報が記録されるファイル。書き込みも削除もすべてプログラムで行うので触る必要なし。
├─index.php ……トップページ。現状、list.phpに移動する為だけの存在。
├─edit.php ……投稿された記録の修正画面のページ。
├─list.php ……投稿された記録を一覧表示するページ。新規追加用のフォームもここに同梱されている。
├─result.php ……後述。
├─header.php ……ヘッダーパーツ。サイトタイトルとページ上部のレイアウトを担う部品で、全ページで使われている。
├─footer.php ……フッターパーツ。ページ下部のレイアウトを担う部品で、全ページで使われている。
├─regist.php ……投稿の新規追加、編集、削除の３種類の処理を行うプログラムファイル。処理が終わるとresult.phpを表示する。
└─system.php ……サイトの設定や利用する関数などをまとめてあるファイル。

※このうち、regist.php、system.phpは各種処理を行うシステムファイル、header.php、footer.phpはパーツファイルであることには注意。
つまり、regist.php、system.php、header.php、footer.phpはウェブサイトでいう「ページ」**ではない**のだ。




##記事の保存

##テキストファイル（≒記事データ）の読み込みと表示

###応用：記事のソート

###応用：記事の検索


##記事の登録

###応用：バリデーション

###応用：BOMについて

##記事の削除

##記事の編集

###応用：ひとつのページで処理を完結させる

```php:test.php


    <form action="" name="admin_options" method="post">

        <div class="textbox">
            <span>メッセージ変更(改行は反映されません。)</span>
            <textarea name="sekoucontent" cols="80" rows="5"><?php echo $text_validated; ?></textarea>
        </div>

        <div class="textbox">
            <span>メッセージの流れる速度</span>
            <input type="" value="" />
        </div>


        <input type="submit" name="change" value="変更">
    </form>

```



#参考文献

https://qiita.com/tadsan/items/0955b3de7dc58490ddaf
https://qiita.com/tadsan/items/bbc23ee596d55159f044






#index.php

```php:index.php
<?php

require_once(dirname(__FILE__).'/system.php');

MakeHeader(); ?>
<div id="index_page">
	<div class="main">

		<a href="./list.php">一覧表示ページへ。。。</a>

	</div>
</div>
<?php MakeFooter(); ?>
```

#list.php

```php:list.php
<?php

require_once(dirname(__FILE__).'/system.php');


// クッキーから取り出し
$post_hall_cookie = $_COOKIE['posthall'];
$post_name_cookie = $_COOKIE['postname'];


// ログデータ取り出し
$log_list = ReadLog();

// 取り出したログデータを降順にソート
foreach($log_list as $val){
    $timestamplist[] = $val[2];
}
array_multisort($timestamplist, SORT_DESC, $log_list);


MakeHeader(); ?>


<div id="list_page">
	<div class="main">

		<article class="list_wrap">
			<?php
			foreach($log_list as $one_log){
				
				$post_id = $one_log[0];
				$post_time_first = $one_log[2];
				$post_time_last = $one_log[3];
				$post_name = $one_log[4];
				$post_content = $one_log[5];

				if($post_time_last === $post_time_first){
					$date_result = StampToDate($post_time_first);
				}
				else{
					$date_result = StampToDate($post_time_first)." (編集済み)";
				}

				?>

			<section class="list_cell">
				<h3><?php echo $post_name; ?></h3>
				<p class="content"><?php echo OpenText($post_content, 'html'); ?></p>
				<p class="post_time"><?php echo OpenText($date_result, 'html'); ?></p>
				<div class="actions">
					<a href="./edit.php?id=<?php echo $post_id ?>">編集する</a>
					<a href="./regist.php?type=delete&id=<?php echo $post_id ?>">削除する</a>
				</div>
			</section>

				<?php
			} ?>
		</article>

		<div id="form">
		    <h2>form</h2>
	        <form action="./regist.php?type=regist" method="post">
	        	<div class="textbox">
	        		<input type="text" name="name" size="30" value="<?php echo $post_name_cookie; ?>" placeholder="お名前">
	        	</div>
	        	<div class="textbox">
	        		<textarea name="content" cols="80" rows="5" placeholder="すきなコメントをいれてね"></textarea>
	        	</div>
	        	<div class="submit">
	                <input type="submit" value="書き込む">
	            </div>
	        </form>
		</div>

	</div>
</div>



<?php MakeFooter(); ?>
```

#edit.php

```php:edit.php
<?php

require_once(dirname(__FILE__).'/system.php');


// GETからIDを取得
if($_GET['id'] == ""){
	header("Location: index.php");
	exit;
}
else{
	$target_id = $_GET['id'];
}

// ログデータ取り出し
$log_list = ReadLog();


// 編集対象となる投稿情報の取得
$switch = 0;
foreach($log_list as $line){
	if($line[0] == $target_id){

		$post_time_first = $line[2];
		$post_time_last = $line[3];
		$post_name = $line[4];
		$post_content = $line[5];

		$post_name = OpenText($post_name, 'textarea');
		$post_content = OpenText($post_content, 'textarea');

		$switch++;
	}
}
if($switch === 0){
	$resultmessage = urlencode("対象となる投稿が存在しません。");
	header("Location: result.php?message=".$resultmessage);
	exit;
}



MakeHeader(); ?>

<div id="edit_page">
	<div class="main">

		<ul>
			<li>
				<span>post</span>
				<span><?php echo StampToDate($post_time_first); ?></span>
			</li>
			<li>
				<span>last edit</span>
				<span><?php echo StampToDate($post_time_last); ?></span>
			</li>
		</ul>

		<div id="form">
		    <h2>form</h2>
	        <form action="./regist.php?type=edit&id=<?php echo $target_id; ?>" method="post">

	        	<div class="textbox">
	        		<input type="text" name="name" size="30" value="<?php echo $post_name; ?>" placeholder="お名前">
	        	</div>
	        	<div class="textbox">
	        		<textarea name="content" cols="80" rows="5" placeholder="コメントをいれてね"><?php echo $post_content; ?></textarea>
	        	</div>
	        	<div class="submit">
	                <input type="submit" value="上書きする">
	            </div>
	        </form>
		</div>

	</div>
</div>




<?php MakeFooter(); ?>
```

#result.php

```php:result.php
<?php

// 動作ファイル認識
require_once(dirname(__FILE__).'/system.php');

if($_GET["message"] == ""){
	header("Location: index.php");
	exit;
}
else{
	$message_acquired = $_GET["message"];
}

MakeHeader(); ?>
<div id="edit_page">
	<div class="main">


	<p><?php echo $message_acquired; ?></p>
	<ul>
	  <li><a href="index.php">一覧へ戻る</a></li>
	</ul>

	
	</div>
</div>
<?php MakeFooter(); ?>
```

#header.php

```php:header.php
<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
	<title>PHP WORDTILE</title>
	<meta http-equiv="x-ua-compatible" content="ie=edge">
	<meta name="description" content="さいとのせつめい。"/>
	<meta name="keywords" content="検索のときに使うキーワード"/>
	<!-- 電話番号等の自動認識を阻害 -->
	<meta name="format-detection" content="telephone=no">
	<!-- jQueryライブラリの読み込み -->
	<!-- <script src="//code.jquery.com/jquery-2.2.4.min.js"></script> -->
	<!-- ▼共通css -->
	<link rel="stylesheet" href="./css/style.css">
	<!-- ▼共通js -->
</head>
<body>

<div id="header">
	<h1>PHP WORDTILE</h1>
</div>
```
#footer.php

```php:footer.php
<div id="footer">
</div>
</body>
</html>
```
#regist.php

```php:regist.php
<?php

require_once(dirname(__FILE__).'/system.php');


// アクションの種類の判定
if($_GET['type'] == "regist"){
    RegistAction($_POST, $_SERVER);
}
else if($_GET['type'] == "delete" && $_GET['id'] != ""){
    $target_id = $_GET['id'];
    DeleteAction($target_id);

}
else if($_GET['type'] == "edit" && $_GET['id'] != ""){
    $target_id = $_GET['id'];
    EditAction($_POST, $_SERVER, $target_id);

}
else{
    header("Location: index.php");
    exit;

}

// regist(新規投稿)
function RegistAction($data, $server){

    // postされた値のセキュリティチェック
    if($server['REQUEST_METHOD'] !== "POST"){
        $resultmessage = urlencode("不正なリクエストではありませんか？");
        header("Location: result.php?message=".$resultmessage);
        exit;
    }

    if($data['content'] == ""){
        $resultmessage = urlencode("本文が空欄のままです。");
        header("Location: result.php?message=".$resultmessage);
        exit;
    }
    else{
        // タイムスタンプの取得
        $timestamp = time();

        // IPアドレスの取得
        $ipaddr = $server['REMOTE_ADDR'];


        // POSTの値は変数に格納しておくと、ハッキング攻撃を受けても中身を不正に変えられなくなる。
        if($data['name'] == ""){
            $post_name = "名無しさん。";
        }
        else{
            $post_name = $data['name'];
        }
        $post_content = $data['content'];
        

        $post_content = CloseText($post_content);
        $post_name = CloseText($post_name);

        // ログファイルの有無をチェック。ないなら生成する
        if (!file_exists(setinits("root").'/log/log.txt')){
            if($filepass = fopen(setinits("root").'/log/log.txt', 'w')){
                fclose($filepass);
            }
            else{
                $resultmessage = urlencode("ファイルを開ける際に何らかの問題が発生しました。");
                header("Location: result.php?message=".$resultmessage);
                exit;
            }
        }


        // ファイル読み込み
        $logfiles_array = file(setinits("root").'/log/log.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

        $cntall = count($logfiles_array);
        $cnt = 1;
        foreach($logfiles_array as $logfile_line){

            // 一行になった文字列を分割
            $log_parts = explode("<>", $logfile_line);
            list($log_id, $log_addr, $log_first_timestamp, $log_timestamp, $log_name, $log_content) = $log_parts;

            // 既存の投稿からidだけを抜き出して一覧をつくる。すぐ下のid生成で利用。
            $idlist[] = $log_id;

            if($cnt == $cntall){

                // 連続投稿を制限する関数
                $log_span = $timestamp - $log_timestamp;
                if($log_span < 60 && $ipaddr == $log_addr && $post_name == $log_name && $post_content == $log_content){
                    $resultmessage = urlencode("同じ端末から同じ内容の投稿が投稿されたばかりです。1分ほどお待ちください");
                    header("Location: result.php?message=".$resultmessage);
                    exit;
                }

            }
            $cnt++;
        }



        // idを生成しつつ、重複チェック。このidは編集、削除するときに利用する。
        if( $idlist == null ){
            $new_id = uniqid( rand(7,7) );
        }
        else{
            $new_id = uniqid( rand(7,7) );
            while( array_search($new_id, $idlist) !== false ){
                $new_id = uniqid( rand(7,7) );
            }
        }

        // この $first_timestamp は、編集するときに使います。
        $first_timestamp = $timestamp;

        // 文生成
        $datatext = $new_id."<>".$ipaddr."<>".$first_timestamp."<>".$timestamp."<>".$post_name."<>".$post_content."\n";

        // 投稿処理
        if($filepass = fopen(setinits("root").'/log/log.txt', 'a+')){
            fwrite($filepass, $datatext);
            fclose($filepass);
        }
        else{
            $resultmessage = urlencode("ファイルを開ける際に何らかの問題が発生しました。");
            header("Location: result.php?message=".$resultmessage);
            exit;
        }

        // 投稿した内容をクッキーに格納
        setcookie('postname', $post_name, time()+900);

        // 処理が終わったらresultページに移動。これには、二重処理を防ぐ意味合いがある。
        $resultmessage = urlencode("メッセージを投稿しました。");
        header("Location: result.php?message=".$resultmessage);
        exit;
    }
}


// delete(削除)
function DeleteAction($id){
    // ログファイルのチェック
    if(file_exists(setinits("root").'/log/log.txt')){
        if($filehandle = fopen(setinits("root").'/log/log.txt', 'r+')){

            // 削除対象の読み込み処理

            $switch = 'off';
            while( ($logfile_line = fgets($filehandle)) !== false ){
                
                // ここでは、消去したい行のスタート位置座標＆消去したい行より後のすべての行を取得。
                // 消去したい行の次以降の行が欲しいので、このifは消去したい行かどうかをチェックするifよりも前に書いてある
                if($switch === 'on'){
                    // 後で使うのでまとめておく。
                    $rewritestr[] = $logfile_line;
                }
                else{
                    $log_parts = explode("<>", $logfile_line);
                    if($log_parts[0] == $id){

                        // 今取得している行の長さ（バイト数）
                        $target_strlen = strlen($logfile_line);

                        // 今取得している行の終了地点座標（バイト数）
                        $target_finish = ftell($filehandle);

                        // 上書き（≒消去）したい行のスタート位置座標。
                        // 現在読み込んでいる位置から、$target_strlenを引くと求められる。
                        $rewritestart = $target_finish - $target_strlen;

                        $switch = 'on';
                    }
                }
            }
            if($switch === 'off'){
                $resultmessage = urlencode("対象となる投稿が存在しません。");
                header("Location: result.php?message=".$resultmessage);
                exit;
            }

            // 削除処理

            // ポインタを消去したい行の先頭に移動させる
            fseek($filehandle, $rewritestart, SEEK_SET);

            // ファイル内のデータ（≒文字）のうち、$rewritestart以降の文字を削除。つまり、ここの処理で削除したい行とそれ以降の行が削除される。
            ftruncate($filehandle, $rewritestart);

            // 復元処理

            // 先ほどのftruncateで消したくない行まで消してしまっているので、あらかじめ$rewritestrに保存しておいたそれらの行を再度書いていく。
            if(count($rewritestr) !== 0){
                foreach($rewritestr as $onecolomn){
                    fwrite($filehandle, $onecolomn);
                }
            }
            fclose($filehandle);
        }
        else{
            $resultmessage = urlencode("ファイルを開ける際に何らかの問題が発生しました。");
            header("Location: result.php?message=".$resultmessage);
            exit;
        }
    }
    else{
        $resultmessage = urlencode("ログファイルが存在しません。");
        header("Location: result.php?message=".$resultmessage);
        exit;
    }

    // resultページに移動。
    $resultmessage = urlencode("投稿は正常に削除されました。");
    header("Location: result.php?message=".$resultmessage);
    exit;
}

// edit(編集)
function EditAction($data, $server, $id){

    // postされた値のセキュリティチェック
    if($server['REQUEST_METHOD'] !== "POST"){
        $resultmessage = urlencode("不正なリクエストではありませんか？");
        header("Location: result.php?message=".$resultmessage);
        exit;
    }

    if($data['content'] == ""){
        $resultmessage = urlencode("本文が空欄のままです。");
        header("Location: result.php?message=".$resultmessage);
        exit;
    }
    else{
        // タイムスタンプの取得
        $current_timestamp = time();

        // POSTの値は変数に格納しておくと、ハッキング攻撃を受けても中身を不正に変えられなくなる。
        if($data['name'] == ""){
            $post_name = "名無しさん。";
        }
        else{
            $post_name = $data['name'];
        }
        $post_content = $data['content'];

        $post_content = CloseText($post_content);
        $post_name = CloseText($post_name);

    }

    if(file_exists(setinits("root").'/log/log.txt')){
        if($filehandle = fopen(setinits("root").'/log/log.txt', 'r+')){


            // 削除対象の読み込み処理

            $switch = 'off';
            while( ($logfile_line = fgets($filehandle)) !== false ){
                $log_parts = explode("<>", $logfile_line);

                // 消去したい行より後の記述のみすべて取得。消去したい行で$switchオンになる＝つまり、その次の行以降が欲しいので、このifは消去したい行かどうかをチェックするifよりも前に書いてある
                if($switch === 'on'){
                    // 消去したい行より後にある記事はまとめておく。
                    $rewritestr[] = $logfile_line;
                }

                // 消去したい行のポインタ位置と投稿情報を取得する
                if($log_parts[0] == $id){

                    // 今取得している行の長さ（バイト数）
                    $edit_strlen = strlen($logfile_line);

                    // 今取得している行の終了地点座標（バイト数）
                    $edit_finish = ftell($filehandle);

                    // 上書き（≒消去）したい行のスタート位置座標。
                    // 現在読み込んでいる位置から、$edit_strlenを引くと求められる。
                    $rewritestart = $edit_finish - $edit_strlen;

                    // 上書き前の投稿情報取得
                    $log_id = $log_parts[0];
                    $log_addr = $log_parts[1];
                    $log_first_timestamp = $log_parts[2];

                    // $log_parts[3]は上書きするので使いません

                    $log_name = $log_parts[4];
                    $log_hall = $log_parts[5];
                    $log_content = $log_parts[6];

                    $switch = 'on';
                }
            }
            if($switch === 'off'){
                $resultmessage = urlencode("対象となる投稿が存在しません。");
                header("Location: result.php?message=".$resultmessage);
                exit;
            }

            // 削除処理

            // ポインタを消去したい行の先頭に移動させる
            fseek($filehandle, $rewritestart, SEEK_SET);

            // ファイル内のデータ（≒文字）のうち、$rewritestart以降の文字を削除。つまり、ここの処理で削除したい行とそれ以降の行が削除される。
            ftruncate($filehandle, $rewritestart);

            // 上書き処理

            // 書き換えたい投稿のスタート地点までを消しているので、ここに変更後の投稿を書き込む
            fwrite($filehandle, $log_id."<>".$log_addr."<>".$log_first_timestamp."<>".$current_timestamp."<>".$post_name."<>".$post_hall."<>".$post_content."\n");
                
            // 先ほどのftruncateで消したくない行まで消してしまっているので、あらかじめ$rewritestrに保存しておいたそれらの行を再度書いていく。
            if(count($rewritestr) !== 0){
                foreach($rewritestr as $onecolomn){
                    fwrite($filehandle, $onecolomn);
                }
            }
            fclose($filehandle);
        }
        else{
            $resultmessage = urlencode("ファイルを開ける際に何らかの問題が発生しました。");
            header("Location: result.php?message=".$resultmessage);
            exit;
        }
    }
    else{
        $resultmessage = urlencode("ログファイルが存在しません。");
        header("Location: result.php?message=".$resultmessage);
        exit;
    }

    // resultページに移動。これには、二重処理を防ぐ意味合いがある。
    $resultmessage = urlencode("投稿は正常に変更されました。");
    header("Location: result.php?message=".$resultmessage);
    exit;
}



?>
```

#system.php

```php:system.php
<?php

// 読み込み時の動作～～～～～～～～～～

// 基本情報を格納しておく関数
function setinits($target){
// ＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝

	// 基本ファイルパス
	$tmpl['root'] = dirname(__FILE__);

	// 基本URLパス
	$tmpl['site_root'] = (empty($_SERVER["HTTPS"]) ? "http://" : "https://") . $_SERVER["HTTP_HOST"];

// ＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝

	// 設定値を返す
	return $tmpl[$target];
}

// 関数～～～～～～～～～～

// ヘッダーを生成する関数
function MakeHeader(){
	if(file_exists(setinits("root").'/header.php')){
		include_once setinits("root").'/header.php';
	}
	else{
		print '<!DOCTYPE html>';
		print '<html lang="ja">';
		print '<head>';
		print '<meta charset="utf-8">';
		print '<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">';
		print '<title>'.setinits("site_title").'</title>';
		print '</head>';
		print '<body>';
		print '<!-- header.phpの読み込みに失敗しました -->';
	}
}

// フッターを生成する関数
function MakeFooter(){
	if(file_exists(setinits("root").'/footer.php')){
		include_once setinits("root").'/footer.php';
	}
	else{
		print '<!-- footerパーツの読み込みに失敗しました -->';
		print '</body></html>';
	}
}


// ログファイルのチェックと内容の取得
function ReadLog(){
	if( file_exists(setinits("root").'/log/log.txt') ){
		// 存在するなら、ファイルの中身を読み込む
		$logfiles_array = file(setinits("root").'/log/log.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

		foreach($logfiles_array as $logfile_line){
			$log_parts = explode("<>", $logfile_line);
			$logdata[] = $log_parts;
		}
		return $logdata;
	}
	else{
		// 存在しなかった場合、falseを返す
		return false;
	}
}


// BOM付きファイルからBOMをとり、ファイルを掃除する
function TrimBOM($target_txt){
    $result = preg_replace('/^\xEF\xBB\xBF/', '', $target_txt);
    return $result;
}


// 文字のバリデーションを行う関数
function OpenText($text, $mode){
	if($mode == 'html'){
		$text = htmlspecialchars($text);
		$text = str_replace("_", "<br>", $text);
		return $text;
	}
	else if($mode == 'textarea'){
		$text = htmlspecialchars($text);
		$text = str_replace("_", "\n", $text);
		return $text;
	}
}

// バリデーションした文字列を表示用に元に戻す関数
function CloseText($text){
	$text = preg_replace('/[@$<>_,\-]+/u', '', $text);
	$result = preg_replace("/\r\n|\r|\n/", "_", $text);
	return $result;
}

// タイムスタンプを日付表示にする関数
function StampToDate($timestamp){
	$week_name = array("日", "月", "火", "水", "木", "金", "土");
	$result = date( "Y/m/d", $timestamp ).' ('.$week_name[date("w", $timestamp)].') '.date( "H:i", $timestamp );
	return $result;
}

?>
```