- OS
- Windows 7
- サーバー言語
- PHP
- 言語バージョン
- ローカル 5.38
リモート 5.6 - 実行環境
- Xampp 1.7.7
- ローカルサーバー
- Apache 2.2.21
リアルタイム・チャットシステム 公開ソースコード
開発/実行環境
ディレクトリ・ファイル構成
online-chat/ (http://wiz-code.digick.jp/dev/html5/stream/online-chat/room.php)
│
│ room.php
│ send-chat-data.php
│ chat-data-stream.php
│ functions.php
│ Data_Stream.php
│ tree.txt
│
├─js
│ online-chat.js
│
└─tmp
library/
│
├─ PEAR Cache_Lite
│
├─ jQuery 2.2.2
├─ Underscore.js 1.8.3
└─ Bootstrap 3.3.1
Cache_Liteによるデータ管理
- Cache_Lite
- PHP PEARライブラリ
| ファイル名 | グループ名 | オプションの詳細 | 内容 |
|---|---|---|---|
| chat_data | online_chat |
// ファイルロックをかける fileLocking: true // 読み込み中に競合がないか検出する readControl: true // 書き込み中に競合がないか検出する writeControl: true // ファイル名を暗号化する fileNameProtection: true |
JSON id: チャットID name: チャット送信者 message: メッセージ内容 create_time: 送信時間(ミリ秒) |
ソースコード
room.php
<?php
date_default_timezone_set('UTC');
/* スクリプトの実行時間 */
set_time_limit(60);
/* セッション・クッキーはブラウザ終了で破棄。HTTPSであれば第4引数はtrueを指定する */
session_set_cookie_params(0, '/', '', false, true);
/* キャッシュを無効化とブラウザバック対応 */
session_cache_expire(0);
session_cache_limiter('private_no_expire');
require_once('Cache/Lite.php');
require_once('functions.php');
define('APP_NAME', 'online_chat');
header('Content-type: text/html; charset=utf-8');
session_start();
/* ページの初回訪問者への処理 */
if (!isset($_SESSION['client_id'])) {
$_SESSION['client_id'] = random_str();
}
/* ノンスを作成 */
$nonce = session_id();
/* Cache_Liteの初期化時に指定するオプション。チャットデータの保存期間は24時間とする */
$options = get_cache_lite_options(array('lifeTime' => 86400));
try {
$cache = new Cache_Lite($options);
$chat_data = $cache->get('chat_data', APP_NAME);
/* チャットデータが存在しなければ新規作成してファイルに保存する */
if ($chat_data === false || !is_array($chat_data)) {
$chat_data = array();
$cache->save($chat_data, 'chat_data', APP_NAME);
}
/* 読み込みエラー時に備えて直前のデータをキャッシュする */
$_SESSION['chat_data_cache'] = $chat_data;
/* クライアントに最後に送ったチャットデータのIDを格納 */
$_SESSION['last_chat_id'] = null;
session_write_close();
} catch (Exception $e) {
format_error_log($e->getMessage, __FILE__, __LINE__);
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>チャットルームのテストページ - Server-Sent EventsとAjax・PHPのリアルタイム・チャットシステム | ウィザード・コード - WIZARD-CODE</title>
<link href="/lib/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<h1 class="h2">チャットルームのテストページ</h1>
<p><strong>Server-Sent Events</strong>/<strong>Ajax</strong>/<strong>PHP</strong>を組み合わせたリアルタイム・チャットシステムです。SSEが実装されていない<strong>Internet Explorer</strong>では利用できません。リアルタイムとありますが、ユーザー間で同期していないので実際には擬似リアルタイムです。</p>
<p>セキュリティについて、Ajax通信はトークンで<strong>CSRF</strong>対策を行います。入力文字のバリデーションはクライアント側で行い、制御文字や特殊文字は入力不可として送信しません。サーバー側の対策は出力時のエスケープ処理のみです。</p>
<p>Google ChromeのデベロッパーツールやFirebugなどでストリーミングの中身が確認できます。「ネットワーク」のタブを開くとchat-data-stream.phpがコネクションを張り続けているのがわかると思います。書き込まれた内容はキャッシュファイルに24時間保存され、その後に自動的に削除されます。</p>
<p>ソースコードを<a href="http://wiz-code.digick.jp/dev/html5/stream/online-chat/online-chat.html">こちらのページ</a>で公開しています。ライセンスフリーです。</p>
<form id="chat-form">
<div id="name-group" class="form-group">
<label for="chat-name" class="control-label">名前を入れてください</label>
<input type="text" class="form-control" id="chat-name" name="chat-name" placeholder="3文字以上20文字まで入力できます(空欄でもOK)。">
</div>
<div id="message-group" class="form-group">
<label for="chat-message" class="control-label">メッセージをどうぞ</label>
<input type="text" class="form-control" id="chat-message" name ="chat-message" placeholder="最大60文字まで入力できます。" autocomplete="off">
</div>
<input type="hidden" id="nonce" name="nonce" value="<?=h($nonce)?>">
<button id="submit-button" type="submit" class="btn btn-info">チャットを送信</button>
</form>
<table class="table">
<thead>
<tr>
<th class="col-xs-2 col-sm-2 col-md-2 col-lg-2">名前</th>
<th class="col-xs-8 col-sm-8 col-md-8 col-lg-8">メッセージ</th>
<th class="col-xs-2 col-sm-2 col-md-2 col-lg-2 text-center">送信時間</th>
</tr>
</thead>
<tbody id="chat-list"></tbody>
</table>
</div>
<script src="/lib/jquery/jquery-2.2.2.js"></script>
<script src="/lib/bootstrap/3.3.1/js/bootstrap.min.js"></script>
<script src="/lib/underscore/1.8.3/underscore.js"></script>
<script src="/js/online-chat.js"></script>
</body>
</html>
send-chat-data.php
date_default_timezone_set('UTC');
set_time_limit(60);
session_set_cookie_params(0, '/', '', false, true);
require_once('Cache/Lite.php');
require_once('functions.php');
define('APP_NAME', 'online_chat');
define('MAX_CHAT_LIMIT', 100);
/* レスポンスはテキストデータとして返す */
header('Content-Type: text/plain; charset=utf-8');
header('X-Content-Type-Options: nosniff');
session_start();
/* クライアントに渡す変数(真偽値) */
$response = false;
/* Ajax通信でなければ終了 */
if (@$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
$message = 'invalid access';
format_error_log($message, __FILE__, __LINE__);
echo $response;
exit;
}
/* アクセスが room.php 経由でない場合終了 */
if (!isset($_SESSION['client_id'])) {
$message = 'client id not found';
format_error_log($message, __FILE__, __LINE__);
echo $response;
exit;
}
/* POSTデータを読み込む */
$raw_post_data = @file_get_contents('php://input');
parse_str($raw_post_data, $params);
$chat_name = $params['chat-name'];
$chat_message = $params['chat-message'];
$nonce = $params['nonce'];
/* ノンスが異なっていたら終了 */
if (session_id() !== $nonce) {
$message = 'invalid nonce';
format_error_log($message, __FILE__, __LINE__);
echo $response;
exit;
}
/* チャットデータの保存期間は24時間とする */
$options = get_cache_lite_options(array('lifeTime' => 86400));
try {
/* Cache_Liteライブラリを初期化 */
$cache = new Cache_Lite($options);
/* チャットデータを読み込む */
$chat_data = $cache->get('chat_data', APP_NAME);
if ($chat_data === false || !is_array($chat_data)) {
/* データが壊れている可能性があるので直前のデータで上書き */
$cache->save($_SESSION['chat_data_cache'], 'chat_data', APP_NAME);
$message = 'chat data not found';
format_error_log($message, __FILE__, __LINE__);
echo $response;
exit;
}
/* ランダムな文字列を生成してチャットIDにする */
$chat_id = random_str();
$new_chat = array(
'id' => $chat_id,
'name' => $chat_name,
'message' => $chat_message,
'create_time' => floor(microtime(true) * 1000),
);
array_unshift($chat_data, $new_chat);
/* データ数が最大数を超えたら古い順に削除 */
$chat_data = array_slice($chat_data, 0, MAX_CHAT_LIMIT);
/* セッション変数に直前のチャットデータを格納 */
$_SESSION['chat_data_cache'] = $chat_data;
/* チャットデータを保存 */
$result = $cache->save($chat_data, 'chat_data', APP_NAME);
if ($result === false || is_object($result)) {
$message = 'failed to save';
format_error_log($message, __FILE__, __LINE__);
echo $response;
exit;
}
$response = true;
echo $response;
} catch (Exception $e) {
format_error_log($e->getMessage, __FILE__, __LINE__);
echo $response;
}
chat-data-stream.php
date_default_timezone_set('UTC');
set_time_limit(60);
session_set_cookie_params(0, '/', '', false, true);
mb_http_output('pass');
require_once('Cache/Lite.php');
require_once('Data_Stream.php');
require_once('functions.php');
/* UPDATE_FREQUENCY: 何秒ごとにデータストリームを送るか */
define('UPDATE_FREQUENCY', 1);
/* ストリーミングのタイムアウト時間を指定 */
define('TIMEOUT', 30);
define('APP_NAME', 'online_chat');
/* ページロード時、一括して読み込むチャットデータの数 */
define('INIT_CHAT_OUTPUT', 30);
/* セッションはストリーミング中の排他ロックを防ぐため明示的に終了させる */
session_start();
/* アクセスが room.php 経由でない場合終了 */
if (!isset($_SESSION['client_id'])) {
$message = 'client id not found';
abort_stream($message);
exit;
}
/* Last-Event-IDをチェックする */
$last_event_id = @$_SERVER['HTTP_LAST_EVENT_ID'];
$last_event_id = is_null($last_event_id) ? 0 : intval($last_event_id) + 1;
/* 最後に送ったチャットデータのIDをセッション変数から取り出す */
$last_chat_id = $_SESSION['last_chat_id'];
session_write_close();
/* Cache_Liteの初期化時に指定するオプション。チャットデータの保存期間は24時間とする */
$options = get_cache_lite_options(array('lifeTime' => 86400));
try {
$cache = new Cache_Lite($options);
$timeout = TIMEOUT * 1000;
$frequency = UPDATE_FREQUENCY * 1000 * 1000;
/* データストリーミング用のクラスを初期化 */
$ds = new Data_Stream;
$ds->start();
/* イベントIDは再接続時に前回からの連番にする */
if ($last_event_id > 0) {
$ds->setId($last_event_id);
}
/* タイムアウト時間までループ処理 */
while ($ds->getElapsedTime() < $timeout) {
/* チャットデータを読み込む */
$chat_data = $cache->get('chat_data', APP_NAME);
if ($chat_data === false || !is_array($chat_data)) {
$message = 'chat data not found';
format_error_log($message, __FILE__, __LINE__);
/* データが壊れている可能性があるので直前のデータで上書きする */
@session_start();
$cache->save($_SESSION['chat_data_cache'], 'chat_data', APP_NAME);
session_write_close();
$ds->flush('', 'chat-data');
usleep($frequency);
continue;
}
/* 初回だけ一括してチャットデータを送る */
if (is_null($last_chat_id)) {
$sliced_data = array_slice($chat_data, 0, INIT_CHAT_OUTPUT);
for ($i = 0, $l = count($sliced_data); $i < $l; $i++) {
$data = $sliced_data[$i];
if ($i === 0) {
$last_chat_id = $data['id'];
}
/* 文字列データはエスケープする */
$data = escapeStreamData($data);
$sliced_data[$i] = $data;
}
/* 初回の送信は配列データを送る */
if (!empty($sliced_data)) {
$json = json_safe_encode($sliced_data);
$ds->storeData($json);
}
/* 以降は新規のチャットだけを送る */
} else {
$chat_ids = array_column($chat_data, 'id');
$last_chat_index = array_search($last_chat_id, $chat_ids, true);
if ($last_chat_index > 0) {
$sliced_data = array_slice($chat_data, 0, $last_chat_index);
for ($i = 0, $l = count($sliced_data); $i < $l; $i++) {
$data = $sliced_data[$i];
if ($i === 0) {
$last_chat_id = $data['id'];
}
/* 文字列データはエスケープする */
$data = escapeStreamData($data);
$json = json_safe_encode($data);
/* Data_Stream::storeData()の第2引数にtrueを渡すと配列に要素をunshiftで入れる。作成日時の古い順にスタックするため */
$ds->storeData($json, true);
}
}
}
/* チャットデータをフラッシュして一定時間スリープ */
$ds->output('chat-data');
usleep($frequency);
}
$ds->end();
/* セッションを再開して変数の値を更新する */
@session_start();
$_SESSION['chat_data_cache'] = $chat_data;
$_SESSION['last_chat_id'] = $last_chat_id;
session_write_close();
} catch (Exception $e) {
format_error_log($e->getMessage(), __FILE__, __LINE__);
abort_stream();
exit;
}
/* クライアントにストリーミングの中断を指示する */
function abort_stream($error_log = false) {
if ($error_log !== false) {
format_error_log($error_log, __FILE__, __LINE__);
}
$ds = new Data_Stream;
$ds->start();
$ds->flush('failed to stream', 'abort-chat-data-stream');
$ds->end();
}
functions.php
/* PHP 5.5.0未満のバージョンではarray_column()がサポートされておらず、下記のコードが必要 */
/**
* This file is part of the array_column library
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @copyright Copyright (c) 2013 Ben Ramsey <http://benramsey.com>
* @license http://opensource.org/licenses/MIT MIT
*/
if (!function_exists('array_column')) {
/**
* Returns the values from a single column of the input array, identified by
* the $columnKey.
*
* Optionally, you may provide an $indexKey to index the values in the returned
* array by the values from the $indexKey column in the input array.
*
* @param array $input A multi-dimensional array (record set) from which to pull
* a column of values.
* @param mixed $columnKey The column of values to return. This value may be the
* integer key of the column you wish to retrieve, or it
* may be the string key name for an associative array.
* @param mixed $indexKey (Optional.) The column to use as the index/keys for
* the returned array. This value may be the integer key
* of the column, or it may be the string key name.
* @return array
*/
function array_column($input = null, $columnKey = null, $indexKey = null)
{
// Using func_get_args() in order to check for proper number of
// parameters and trigger errors exactly as the built-in array_column()
// does in PHP 5.5.
$argc = func_num_args();
$params = func_get_args();
if ($argc < 2) {
trigger_error("array_column() expects at least 2 parameters, {$argc} given", E_USER_WARNING);
return null;
}
if (!is_array($params[0])) {
trigger_error('array_column() expects parameter 1 to be array, ' . gettype($params[0]) . ' given', E_USER_WARNING);
return null;
}
if (!is_int($params[1])
&& !is_float($params[1])
&& !is_string($params[1])
&& $params[1] !== null
&& !(is_object($params[1]) && method_exists($params[1], '__toString'))
) {
trigger_error('array_column(): The column key should be either a string or an integer', E_USER_WARNING);
return false;
}
if (isset($params[2])
&& !is_int($params[2])
&& !is_float($params[2])
&& !is_string($params[2])
&& !(is_object($params[2]) && method_exists($params[2], '__toString'))
) {
trigger_error('array_column(): The index key should be either a string or an integer', E_USER_WARNING);
return false;
}
$paramsInput = $params[0];
$paramsColumnKey = ($params[1] !== null) ? (string) $params[1] : null;
$paramsIndexKey = null;
if (isset($params[2])) {
if (is_float($params[2]) || is_int($params[2])) {
$paramsIndexKey = (int) $params[2];
} else {
$paramsIndexKey = (string) $params[2];
}
}
$resultArray = array();
foreach ($paramsInput as $row) {
$key = $value = null;
$keySet = $valueSet = false;
if ($paramsIndexKey !== null && array_key_exists($paramsIndexKey, $row)) {
$keySet = true;
$key = (string) $row[$paramsIndexKey];
}
if ($paramsColumnKey === null) {
$valueSet = true;
$value = $row;
} elseif (is_array($row) && array_key_exists($paramsColumnKey, $row)) {
$valueSet = true;
$value = $row[$paramsColumnKey];
}
if ($valueSet) {
if ($keySet) {
$resultArray[$key] = $value;
} else {
$resultArray[] = $value;
}
}
}
return $resultArray;
}
}
function get_cache_lite_options($options = null) {
$default_options = array(
'cacheDir' => './tmp/',
'caching' => true,
'fileLocking' => true,
'readControl' => true,
'writeControl' => true,
'fileNameProtection' => true,
'automaticSerialization' => true,
'automaticCleaningFactor' => 200,
'lifeTime' => 3600,
'hashedDirectoryLevel' => 1,
);
if (is_null($options)) {
return $default_options;
}
foreach ($default_options as $key => $value) {
if (!array_key_exists($key, $options)) {
$options[$key] = $value;
}
}
return $options;
}
function escapeStreamData($data) {
foreach ($data as $key => $value) {
if (is_string($value)) {
$data[$key] = h($value);
}
}
return $data;
}
function json_safe_encode($val) {
return json_encode($val, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
}
function h($str) {
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
function random_str($length = 32) {
return strtr(substr(base64_encode(openssl_random_pseudo_bytes($length)), 0, $length), '/+', '_-');
}
function format_error_log($message, $file = null, $line = null) {
$message = is_string($message) ? $message : strval($message);
$file = !is_null($file) ? basename($file) . ' ' : '';
$line = !is_null($line) ? "line {$line} " : '';
$message = $file . $line . $message;
$date = new DateTime();
$date->setTimezone(new DateTimeZone('Asia/Tokyo'));
$message = $message . ' [' . $date->format(DATE_ISO8601) . ']' . PHP_EOL;
/* ログファイルの場所をフルパスで指定 */
error_log($message, '3', '/home/*****/log/debug.log');
}
Data_Stream.php
class Data_Stream
{
private $id = 1;
private $options = array(
'defaultEvent' => 'message',
'retry' => 0,
);
private $started = false;
private $data;
private $elapsedTime;
private $startTime;
public function __construct($options = NULL)
{
if (!is_null($options)) {
$this->setOptions($options);
}
if (headers_sent() === true) {
header_remove('Content-type');
header_remove('Cache-Control');
}
header('Content-type: text/event-stream; charset=utf-8');
header('Cache-Control: no-cache');
}
public function setOption($options)
{
foreach ($options as $key => $value) {
if (isset($value)) {
$this->options[$key] = $value;
}
}
}
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = is_int($id) ? $id : intval($id);
return $this->id;
}
public function start()
{
ob_end_clean();
ob_start();
$this->data = array();
$this->elapsedTime = 0;
$this->startTime = microtime(true) * 1000;
$this->started = true;
}
public function end()
{
ob_end_clean();
$this->started = false;
}
public function getElapsedTime()
{
$result = 0;
if ($this->started === true) {
$result = $this->elapsedTime = (microtime(true) * 1000) - $this->startTime;
return $result;
}
return $result;
}
public function storeData($data, $reverse = false)
{
if (is_string($data)) {
if ($reverse === false) {
array_push($this->data, $data);
} else {
array_unshift($this->data, $data);
}
return true;
}
return false;
}
public function output($event = null, $id = null)
{
$event = !is_null($event) ? $event : $this->options['defaultEvent'];
$id = !is_null($id) ? $id : $this->id;
$data_list = "";
if (!empty($this->data)) {
for ($i = 0, $l = count($this->data); $i < $l; $i++) {
$value = $this->data[$i];
$data_list .= "id: {$id}" . PHP_EOL . "event: {$event}" . PHP_EOL . "data: {$value}" . PHP_EOL . PHP_EOL;
$id = ++$this->id;
}
}
if ($this->started === true && is_string($data_list)) {
/* ヒアドキュメントの終わりにある二つの改行はデータの終了を示す */
$message = <<< EOM
: keep alive
retry: {$this->options['retry']}
{$data_list}
EOM;
echo $message;
@ob_flush();
@flush();
$this->data = array();
}
}
public function flush($data, $event = null, $id = null)
{
$event = !is_null($event) ? $event : $this->options['defaultEvent'];
if (is_null($id)) {
$id = $this->id;
$this->id++;
}
if ($this->started === true && is_string($data)) {
if (!empty($data)) {
$data = "data: {$data}";
} else {
$data = '';
}
/* ヒアドキュメントの終わりにある二つの改行はデータの終了を示す */
$message = <<< EOM
: keep alive
id: {$id}
event: {$event}
retry: {$this->options['retry']}
{$data}
EOM;
echo $message;
@ob_flush();
@flush();
}
}
}
online-chat.js
/* このファイルはコメントを削除したり圧縮しないでください */
;(function () {
var heredocReg, validReg, fragment, MIN_NAME_LIMIT, MAX_NAME_LIMIT, MAX_MESSAGE_LIMIT, DATE_UPDATE_INTERVAL;
heredocReg = /^function\s+\([^)]*\)\s*\{\s*|\s*\/\*\s*|\s*\*\/\s*\}$/g;
validReg = /^[\x20\x21\x23-\x25\x28-\x3B\x3D\x3F-\x7E\u2010-\u2027\u3000-\u3036\u303F\u3041-\u3094\u3099-\u309E\u30A1-\u30F6\u30FB-\u30FE\u4E00-\u9FA5\uFF01-\uFF5E\uFF61-\uFF9F\uFFE0-\uFFE6]+$/;
fragment = {};
/* チャット表示要素のフラグメント */
fragment.chat = (function () {
/*
<tr>
<td class="chat-from"><%= name %></td>
<td class="chat-body"><%= message %></td>
<td class="chat-time"><%= time %></td>
</tr>
*/
}).toString().replace(heredocReg, '');
MIN_NAME_LIMIT = 3;
MAX_NAME_LIMIT = 20;
MAX_MESSAGE_LIMIT = 60;
DATE_UPDATE_INTERVAL = 20;
$(function () {
var chatList, chatForm, chatName, chatMessage, submitButton, chatDataStream, chatData, dateUpdateInterval;
chatForm = $('#chat-form');
chatName = $('#chat-name');
chatMessage = $('#chat-message');
chatList = $('#chat-list');
submitButton = $('#submit-button');
chatData = [];
chatForm.on('submit', function (e) {
e.preventDefault();
});
chatName.on('keypress', function (e) {
/* テキストフォームのエンターキー押し下げによるイベントを抑止 */
if (e.key === 'Enter') {
e.preventDefault();
}
});
submitButton.on('click', sendChatMessage);
/* 新規のチャットメッセージを確認するストリーム */
chatDataStream = new EventSource('/dev/html5/stream/online-chat/chat-data-stream.php');
/* サーバーからのストリームデータを受信 */
chatDataStream.addEventListener('chat-data', function (e) {
var data, time, element;
data = JSON.parse(e.data);
/* ページロード時はチャットデータが配列にまとめられて送られてくる */
if (_.isArray(data)) {
_.each(data, function (d) {
element = insertChatData(d);
highlightElement(element);
});
chatData = chatData.concat(data.reverse());
/* 以降は一件ずつ送られてくる */
} else {
element = insertChatData(data, 'prependTo');
highlightElement(element);
chatData.push(data);
}
});
chatDataStream.addEventListener('error', function (e) {
console.info('チャットデータのストリーミングを再開します');
}, false);
chatDataStream.addEventListener('abort-chat-data-stream', function (e) {
console.warn('チャットデータのストリーミングを中断しました');
chatDataStream.close();
}, false);
$(window).on('unload', function (e) {
chatDataStream.close();
}, false);
/* チャットの作成時間を一定時間ごとに更新 */
dateUpdateInterval = DATE_UPDATE_INTERVAL * 1000;
window.setInterval(dateUpdater, dateUpdateInterval);
function sendChatMessage(e) {
var name, message, label, messageGroup;
e.preventDefault();
name = chatName.val();
message = chatMessage.val();
messageGroup = chatForm.children('#message-group');
label = messageGroup.children('label');
/* ユーザー名のバリデーション */
if (_.isEmpty(name) || name.length < MIN_NAME_LIMIT || name.length > MAX_NAME_LIMIT || !validReg.test(name)) {
name = 'NO NAME';
}
/* メッセージ内容のバリデーション */
if (_.isEmpty(message)) {
if (!messageGroup.hasClass('has-error')) {
messageGroup.addClass('has-error');
}
label.text('メッセージを入力してください');
chatMessage.focus();
return;
}
if (message.length > MAX_MESSAGE_LIMIT) {
if (!messageGroup.hasClass('has-error')) {
messageGroup.addClass('has-error');
}
label.text('メッセージの入力文字数は60文字までです');
chatMessage.focus();
return;
}
if (!validReg.test(message)) {
if (!messageGroup.hasClass('has-error')) {
messageGroup.addClass('has-error');
}
label.text('特殊文字(& < > " \'など)は使用できません');
chatMessage.focus();
return;
}
if (messageGroup.hasClass('has-error')) {
messageGroup.removeClass('has-error');
label.text('チャットツール');
}
/* 名前欄が空欄のとき代替名を入れる */
chatName.val(name);
$(this).prop('disabled', true).blur();
/* メッセージを送信 */
$.post(
'/dev/html5/stream/online-chat/send-chat-data.php',
chatForm.serialize(),
function (data) {
submitButton.prop('disabled', false);
if (_.isEmpty(data)) {
if (!messageGroup.hasClass('has-error')) {
messageGroup.addClass('has-error');
}
label.text('メッセージの送信に失敗しました');
chatMessage.focus();
return;
}
chatMessage.val('');
},
'text'
);
}
function dateUpdater() {
var chatRows;
chatRows = chatList.children('tr');
_.each(chatData, function (data) {
var time;
time = formatUpdateTime(data.create_time);
chatRows.filter('[id="' + data.id + '"]').
find('.chat-time').text(time);
});
}
function highlightElement(element) {
var color, duration;
color = 50;
duration = 700;
$(element).css({backgroundColor: 'hsl(60,100%,' + color + '%)'}).
animate({backgroundColor: 'transparent'}, {
duration: duration,
progress: function (animation, progress) {
$(this).css({backgroundColor: 'hsl(60,100%,' + Math.floor(progress * (100 - color) + color) + '%)'});
}
}
);
}
function insertChatData(data, method) {
var time, element, temp, rendered;
method = !_.isUndefined(method) ? method : 'appendTo';
time = formatUpdateTime(Math.floor(data.create_time));
temp = _.template(fragment.chat);
rendered = temp({
name: data.name,
message: data.message,
time: time,
});
element = $(rendered).attr('id', data.id)[method](chatList);
return element;
}
function formatUpdateTime(startTime) {
var elapsedTime, remainder, years, months, days, hours, minutes, seconds;
elapsedTime = _.now() - startTime;
elapsedTime = Math.max(elapsedTime, 0);
if (Math.floor(elapsedTime / 1000) === 0) {
return 'たった今';
}
years = Math.floor(elapsedTime / 3.1536e+10);
if (years > 0) {
elapsedTime = years + '年前';
} else {
remainder = elapsedTime % 3.1536e+10;
months = Math.floor(remainder / 2.592e+9);
if (months > 0) {
elapsedTime = months + 'ヶ月前';
} else {
remainder = elapsedTime % 2.592e+9;
days = Math.floor(remainder / 8.64e+7);
if (days > 0) {
elapsedTime = days + '日前';
} else {
remainder = elapsedTime % 8.64e+7;
hours = Math.floor(remainder / 3.6e+6);
if (hours > 0) {
elapsedTime = hours + '時間前';
} else {
remainder = elapsedTime % 3.6e+6;
minutes = Math.floor(remainder / 60000);
if (minutes > 0) {
elapsedTime = minutes + '分前';
} else {
seconds = Math.floor(elapsedTime / 1000);
elapsedTime = seconds + '秒前';
}
}
}
}
}
return elapsedTime;
}
});
}());