PHPを使った位置情報収集(ラブライバーはどこから来るの?)

標準

開発に至った経緯

9月29日、30日に「Aqours 2nd ラブライブ! HAPPY PARTY TRAIN TOUR 埼玉公園」(公式サイト)が開催されまして、1日目に私はライブ・ビューイングに参戦しました。(2日目は残念ながらTwitterを眺めるだけでしたが😥)(感想は後述)
そこで、ライブのために埼玉まで来た方はどこから来ているのかと言うのを簡易的に調査するために、Twitterを使った位置情報収集アプリを作ることにしました。

調査方式

  1. ある任意の座標(今回は西武ドームの 35.768653,139.419132 / 国土地理院の地図)をプログラムに与えると、その地点の半径1km以内で投稿されたツイートを集めます。
  2. そのツイート主を全員それぞれ200ツイートずつ遡ってツイートを取得します。
  3. その中でジオタグ(位置情報)があれば集めます。
  4. Excelで収集したデータセットを整理します。
  5. 最後にそれを地図に出力します。

といった感じで、本当に簡易的です。

コード

<!DOCTYPE html>
<html lang="ja">
<head>
    <title>Twitter trace</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="index.css" />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
    <?php
    ini_set('display_errors', 1);
    require_once(__DIR__ . "/twitteroauth/autoload.php");
    use Abraham\TwitterOAuth\TwitterOAuth;
    $consumer_key = "***************";
    $consumer_secret = "****************************************************";
    $access_token = "***************************************************";
    $access_token_secret = "*******************************************";

    $connection = new TwitterOAuth($consumer_key, $consumer_secret, $access_token, $access_token_secret);

    ////////////////////////////////////////
    $count_limit = (int)"100"; //0-100
    $get_users_status = (int)"200"; //0-200
    $default_lat = "35.768653";
    $default_long = "139.419132";
    ////////////////////////////////////////

    if(isset($_GET["lat"]) && isset($_GET["long"]) && $_GET["lat"] != "" && $_GET["long"] != "") {
        $set_lat = $_GET["lat"];
        $set_long = $_GET["long"];
    } else {
        $set_lat = $default_lat;
        $set_long = $default_long;
    }

    if(isset($_GET["max_id"]) && $_GET['max_id'] != "") {
        $max_id = $_GET["max_id"];
    } else {
        $max_id = "";
    }

    $geo_params = Array(
        "geocode" => "{$set_lat},{$set_long},1.0km",
        "count" => $count_limit,
        "result_type" => "mixed",
        "max_id" => $max_id,
        "include_entities" => false

    );

    $result_tweets = $connection->get("search/tweets", $geo_params);

    $result_tweets_array = json_decode(json_encode($result_tweets),true);

    $result_coordinate = Array();
    $oldest_id = "";
    $debug_info = "";

    $i = (int)"0";
    while (true) {
        $debug_info .= "{$i}\r\n";
        $status = $result_tweets_array["statuses"][$i];
        if($created_at = $status["created_at"] == "") {
            break;
        }
        $screen_name = $status["user"]["screen_name"];
        $lat = $status["geo"]["coordinates"][0];
        $long = $status["geo"]["coordinates"][1];

        if($screen_name != "mgn_tokorozawa") {
            if($lat != "" && $long != "") {
                array_push($result_coordinate,Array("lat" => $lat,"long" => $long));
            }
        }

        $users_info = Array(
            "screen_name" => $screen_name,
            "count" => $get_users_status,
            "exclude_replies" => true,
            "include_rts" => false,
            "trim_user" => true,
            "exclude_replies" => true
        );

        $users_data = $connection->get("statuses/user_timeline",$users_info);

        $users_data_array = json_decode(json_encode($users_data),true);

        $i2 = (int)"0";
        while(true) {
            $status_data = $users_data_array[$i2];
            if($status_data["geo"] != "" && $lat != $status_lat && $long != $status_long) {
                $status_lat = $status_data["geo"]["coordinates"][0];
                $status_long = $status_data["geo"]["coordinates"][1];
                $debug_info .= "status_lat:{$status_lat}\r\n";
                $debug_info .= "status_long:{$status_long}\r\n";
                $debug_info .= "http://maps.gsi.go.jp/#16/{$status_lat}/{$status_long}/&base=std\r\n";
                if ($screen_name != "mgn_tokorozawa") {
                    if($status_lat != "" && $status_long != "") {
                        array_push($result_coordinate,Array("lat" => $status_lat,"long" => $status_long));
                    }
                }
            }

            $i2++;
            if($i2 >= $get_users_status) {
                break;
            }
        }

        if ($status['id'] != "") {
            $oldest_id = $status['id'];
        }

        $debug_info .= "screen_name:{$screen_name}\r\n";
        $debug_info .= "id:{$status['id']}\r\n";
        $debug_info .= "text:{$status['text']}\r\n";
        $debug_info .= "lat:{$lat}\r\n";
        $debug_info .= "long:{$long}\r\n";
        $debug_info .= "http://maps.gsi.go.jp/#16/{$lat}/{$long}/&base=std\r\n";
        $debug_info .= "https://twitter.com/{$status['user']['screen_name']}/status/{$status['id']}\r\n=================\r\n";
        $i++;
        if($i >= $count_limit) {
            break;
        }
    }

    $api_limit_info = $connection->get("application/rate_limit_status");
    $api_limit_info_array = json_decode(json_encode($api_limit_info),true);
    ?>
    <div class="container" id="main">
        <?php
        echo "<h2>API LIMIT INFO</h2>";
        echo "<ul class='list-group'><li>/search/tweets:{$api_limit_info_array['resources']['search']['/search/tweets']['remaining']}/{$api_limit_info_array['resources']['search']['/search/tweets']['limit']}</li>";
        echo "<li>/statuses/user_timeline:{$api_limit_info_array['resources']['statuses']['/statuses/user_timeline']['remaining']}/{$api_limit_info_array['resources']['statuses']['/statuses/user_timeline']['limit']}</li></ul>";

        $result = (int)"0";
        $result_count = count($result_coordinate);
        $output = (string)"";
        while(true) {
            $output .= "{lat: {$result_coordinate[$result]['lat']},lng: {$result_coordinate[$result]['long']}},";
            $result++;
            if($result >= $result_count) {
                break;
            }
        }
        ?>

        <h2>Custom search</h2>
        <form method="get" action="#" class="form-horizontal">
            <div class="form-group">
                <label for="lat" class="control-label" >latitude</label>
                <input type="text" id="lat" name="lat" class="form-control input-lg" value="<?php echo $set_lat; ?>" />
            </div>
            <div class="form-group">
                <label for="long" class="control-label" >longitude</label>
                <input type="text" id="long" name="long" class="form-control input-lg" value="<?php echo $set_long; ?>" />
            </div>
            <div class="form-group">
                <label for="submit" class="control-label">search</label>
                <input type="submit" class="form-control input-lg" id="submit" value="Search" />
            </div>
        </form>

        <h2>Result</h2>
        <form action="#" class="form-horizontal">
            <div class="form-group">
                <label for="coordinates">Result coordinates dataset</label>
                <textarea rows="40" cols="100" class="form-control input-lg" name="coordinates"><?php echo $output; ?></textarea>
            </div>
            <div class="form-group">
                <label for="debug_info">Debug information</label>
                <textarea rows="40" cols="100" class="form-control input-lg" name="debug_info"><?php echo $debug_info; ?></textarea>
            </div>
        </form>

        <h2>Re-search</h2>
        <form action="#" method="get" class="form-horizontal">
            <div class="form-group">
            <label for="max_id" class="control-label">max_id</label>
            <input type="text" class="form-control input-lg" name="max_id" id="max_id" value="<?php echo $oldest_id; ?>" />
        </div>
        <div class="form-group">
            <label for="submit" class="control-label">search</label>
            <input type="submit" class="form-control input-lg" value="Search" />
        </div>
        </form>
        <p><a href="direct_input.php">input page</a></p>
    </div>
</body>
</html>

人に見せることを目的としていないコードなので本当に適当ですが、一応自分の中では使いやすいように色々とカスタムしてあります。このプログラムを順次走らせていくことで、ライブ期間中の全ツイートを調べることが出来ます。

データを整理する

上記のプログラムで収集した座標のデータセットは大量の重複を含んでいて、そのままのデータを地図にプロットすると閲覧するのにちょっと不便です。なので、重複データを排除するためにExcelで整理します。
適当に集めたJSON形式のデータをExcelに打ち込んでいきます。(改行挟んだりの整形はPHP使うと便利です。)

ワンクリックで重複削除できるんですね。便利!整理したデータを再びJSONにして、次はいよいよ地図にプロットしていきます。

地図にプロット

今回はGoogleが公開している Maps JavaScript API を使っていきたいと思います。APIの使い方のページに記載されている方法で自分用のAPIキーを取得します。そして、以下のようなページを作成して、いざプロットしていきます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <title>Twitter trace</title>
    <meta charset="UTF-8" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="Cache-Control" content="no-cache" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="index.css" />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
        <script>
        var map;
        var marker = [];
        var infoWindow = [];
        var markerData = [
            {lat:35.768575,lng:139.420501},
            {lat:35.768493,lng:139.420522},
            {lat:35.77059347,lng:139.4194273},
            {lat:35.7707,lng:139.4198},
            {lat:35.76880072,lng:139.4209541},
            {lat:35.76879838,lng:139.4209604},
            {lat:35.7112046,lng:139.4640989},
            {lat:35.77136271,lng:139.4202191},
            {lat:35.8353742,lng:139.6503192},
            {lat:35.83783604,lng:139.6495825},
            {lat:35.76880259,lng:139.4209549},
            {lat:35.76946619,lng:139.4382386}
        ];
        function initMap() {
            var mapLatLng = new google.maps.LatLng({lat: markerData[0]['lat'], lng: markerData[0]['lng']});
            map = new google.maps.Map(document.getElementById('result_map'), {
                center: mapLatLng,
                zoom: 15
            });
            for (var i = 0; i < markerData.length; i++) {
                markerLatLng = new google.maps.LatLng({lat: markerData[i]['lat'], lng: markerData[i]['lng']});
                marker[i] = new google.maps.Marker({
                    position: markerLatLng,
                    map: map
                });
            }
        }
        </script>

        <div id="result_map"></div>

        <script async defer
        src="https://maps.googleapis.com/maps/api/js?key=*************************************&callback=initMap">
        </script>
    </div>
</body>
</html>

これだけで好きなだけ地図に地点をプロットできるなんて凄いですね。

まとめ

今回は最終的に8,000件くらいのツイートを収集して2,405件のユニークなデータを取得しました。このデータに関しては、誰でも簡単に閲覧することができるように公開してありますので、参考までに見てみてください。
データ閲覧

日本国内でも北は北海道、南は鹿児島県(確認できた中で最南端)から、世界にまで目を向けると上海やシンガポールなどからも来ていることが分かりました。こんなにも津々浦々から人々を呼び寄せることができるなんてラブライブ凄いなぁ、と言った感じです。

(ライブの)感想

初披露の曲や衣装やダンスを拝めて本当に素晴らしかった!初めてライブを見たんですけど、楽しすぎる…!というかキャスト同士にイチャつきが尊すぎる!!!!!もっと見たかった、というか2日目も参戦したかった!
次はファンミーティングや3rdツアーなどが予定されていますので、こちらの方はぜひ現地で参戦したいですね。

WordPressの重大な脆弱性の検証

標準

2017年2月17日、Wordpressに重大なREST APIに関する脆弱が存在することが公表されました。

WordPress の脆弱性対策について – IPA

あまりにも重大すぎて、パッチが配布されるまで脆弱の存在の公表を遅らせるほどでした。この脆弱はREST APIの不具合を利用することで、ユーザー認証を必要とせずにWordpressに投稿されている記事を簡単に書き換えることが出来てしまいます。今回はこの脆弱をWindowsで実際にWordpressを仮想環境で構築して検証していきます。また、この脆弱を利用して他人のサイトを不正に書き換えることは国内法に抵触しますので、真似は自分の中だけで終わらせてください。

WindowsにDocker環境を整備する

簡単にWordpressの仮想環境を構築するために今回はDockerを使っていきます。(WordPressの仮想環境を作る方法は色々とありますので、ここらへんは個人の好みで。) また、この方法はWindows10 proでないと出来ない工程もありますので、ご注意ください。

まずはDockerのインストーラーを公式サイトからダウンロードします。適当にstable版などをご利用ください。

インストールが完了すると、Dockerが起動します。しっかりと起動するまでに少し時間がかかるので、気長にお待ち下さい。

しっかりと起動しましたら、DockerのServerが稼働していることをPowerShellで確認します。

> docker version
Client:
 Version:      17.06.0-ce
 API version:  1.30
 Go version:   go1.8.3
 Git commit:   02c1d87
 Built:        Fri Jun 23 21:30:30 2017
 OS/Arch:      windows/amd64

Server:
 Version:      17.06.0-ce
 API version:  1.30 (minimum version 1.12)
 Go version:   go1.8.3
 Git commit:   02c1d87
 Built:        Fri Jun 23 21:51:55 2017
 OS/Arch:      linux/amd64
 Experimental: true

Serverの部分が確認されずエラーが発生している場合は、Dockerが起動しきっていないかHyper-Vを有効にする必要があります。 コントロールパネル→プログラムと機能→Windowsの機能の有効下または無効化 からHyper-Vを有効にしてからもう一度試してみてください。

次にDockerの構成情報をまとめていきます。適当なディレクトリを作成し、その中に docker-compose.yml を作成し、以下のようにします。

version: '2'
services:
  db:
    image: mysql:5.7
    volumes:
      - db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: wordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress

  wordpress:
    depends_on:
      - db
    image: wordpress:4.7.0
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_PASSWORD: wordpress

volumes:
  db_data:

ファイルを作成し終えたらPowerShellのカレントディレクトリをdocker-compose.ymlが存在するディレクトリに変更して

> docker-compose pull
> docker-compose build
> docker-compose up -d

これでイメージをビルドして起動するまでがおしまいです。しっかりと起動しているか確認するために

> docker ps

でしっかりとwordpressがport:8000で起動しているかを見ます。

正常な起動を確認できましたら、ブラウザで http://127.0.0.1:8000/ にアクセスします。もし正常なレスポンスがなければまだWordpressが起動しきってないかもしれないので、1分くらい待ってみてください。
この辺りの作業はMastodonで何回もやったので慣れたもんですね。

WordPressの初期設定を一通り終えると以下のようにサイトが表示されます。

これでWordpressの仮想環境の構築はおしまいです。こんな簡単にWindowsで出来るとは。

早速攻撃してみる

今回はChromeアプリのAdvanced REST clientを使っていきます。指定したURLにリクエスト出来るのであれば利用するツールの種類は問いません。

一番最初の投稿の記事IDが1なので、以下のURLにtitleとcontentをPOSTしてみます。

http://127.0.0.1:8000/wp-json/wp/v2/posts/1?id=1

このように、普通のURLでは 401 – Unauthorized が返ってきて、記事を書き換えることなんて出来ません。しかし、

http://127.0.0.1:8000/wp-json/wp/v2/posts/1?id=1abc

上記のURLに対して同様にtitleとcontentをPOSTしてみます。すると、

なんと、処理が完了して 200 – OK が返ってきてしまいました。実際に投稿の方を見てみますと、

確かに、記事が書き換えられていることが確認できます。

原因は何か

この脆弱はPHPの厄介な仕様で起こってしまう脆弱です。詳しくは Content Injection Vulnerability in WordPress を見ていただければ全てが分かると思いますが、端折って説明しますと、

$request = array("id"=>"1abc");
$id = (int)$request['id'];
var_dump($id);

// int(1)

このようにして不正にIDがキャストされてしまい、しっかりとした判断ができなくなってしまうのです。困った!

まとめ

こうしてWordpressの脆弱を検証することが出来ました。自分の手で作業をしてみると、まさかこんなに簡単な手順でページが改ざん出来るとは… 私自身もWordpressやMastodonを運営していますので、こういったセキュリティホールなどに関する情報には常に敏感になってないといけないですね。

Mastodonをメンテナンス状態に切り替える方法

標準

Mastodonのインスタンスをメンテナンス状態に切り替えずに停止していたりすると、404(Not Found)だとか502(Bad Gateway)だとかが周りのインスタンスに返してしまうことになり、結果としてPuSH購読を切られてしまう事になります。そうすると一時的にでも(PuSHの更新が7日周期なので最長7日)連合タイムラインが寂しいことになったりと、インスタンス利用者にも迷惑をかけてしまう事になりかねません。

というわけで今後の為にもしっかりと503(Service Unavailable)を返すように設定しながら、メンテナンスが安心して出来るようにNGINXを設定しましょう。

NGINXの設定ファイル

NGINXの設定ファイル(*.conf)は基本的に /etc/nginx/conf.d に全部入っています。この中に拡張子.confのファイルを入れていくことで色々と設定できます。ココらへんの詳しい仕様は適当にググってみるとよく分かると思います。

私のインスタンスでは、公式ドキュメントにもある設定例を基本的に引用してNGINXの設定をしました。今回は、この公式ドキュメント通りの設定でNGINXを動かしていることを前提に説明していきます。

以下のようにMastodonの設定ファイルを書き換えます。

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

server {
  listen 80;
  listen [::]:80;
  server_name haruhi-mstdn.club; #ドメイン名
  # Useful for Let's Encrypt
  location /.well-known/acme-challenge/ { allow all; }
  location / { return 301 https://$host$request_uri; }
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name haruhi-mstdn.club; #ドメイン名

  ssl_protocols TLSv1.2;
  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  #必要なファイルが存在するパスを指定
  ssl_certificate     /etc/letsencrypt/live/haruhi-mstdn.club/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/haruhi-mstdn.club/privkey.pem;
  ssl_dhparam         /etc/ssl/certs/dhparam.pem;

  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 0;

  #パスを指定
  root /root/mastodon/public;

  gzip on;
  gzip_disable "msie6";
  gzip_vary on;
  gzip_proxied any;
  gzip_comp_level 6;
  gzip_buffers 16 8k;
  gzip_http_version 1.1;
  gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

  add_header Strict-Transport-Security "max-age=31536000";

  location / {
    try_files $uri @proxy;
  }

  location ~ ^/(assets|system/media_attachments/files|system/accounts/avatars) {
    add_header Cache-Control "public, max-age=31536000, immutable";
  }

  location @proxy {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Proxy "";
    proxy_pass_header Server;

    proxy_pass http://127.0.0.1:3000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_intercept_errors on;

    tcp_nodelay on;
  }

  location /api/v1/streaming {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Proxy "";

    proxy_pass http://localhost:4000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  error_page 500 501 502 504 /50x.html; #500,501,502,504はそのまま
  error_page 503 @maintenance; #503は新たに定義する
  location @maintenance {
    root /var/www/error_pages; #表示させたいhtmlファイルが存在するパスを指定
    rewrite ^(.*)$ /custom_503.html break; #表示させたいhtmlファイルにリダイレクト
    internal; #表面上はリダイレクトしたように見せない
  }

  #ここからメンテナンス状態するかどうかの判定
  set $maintenance false;

  #/var/tmp/503というファイルが存在すればメンテナンスモードに切り替え
  if (-e /var/tmp/503) {
    set $maintenance true;
  }

  #自分はいつも通りにアクセスできるように
  if ($remote_addr = "***.***.***.***") {
  set $maintenance false;
  }

  #メンテナンスモードになっているなら503を返す
  if ($maintenance = true) {
    return 503;
  }
  #ここまで判定
}

一部カスタムが必要な部分がありますが、基本的にはこの通りに書けばちゃんと動くと思います。書き換えた後には nginx -t を実行し、反映させる時は systemctl restart nginx をお忘れなきよう。

表示させるHTMLファイル

デフォルトの503ページでも機能を果たしていれば別に問題は無いのですが、やはりアクセスしてくれた利用者には「いつまで続くのか」「何をしているのか」を明示したいものです。というわけで、専用の503ページを作っていきます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>メンテナンス中</title>
    <meta charset="UTF-8" />
    <style>
      article#main {
      width:80%;
      margin:0 auto;
    }
    </style>
  </head>
  <body>
    <article id="main">
      <h1>メンテナンス中</h1>
      <h2>503 - Service Unavailable</h2>
      現在、Mastodonのアップデートに伴う臨時メンテナンスを行っております。ご不便をおかけしますが、ご理解とご協力の程をよろしくお願いいたします。
      <ul>
        <li>期間:XX時XX分~XX時XX分(予定)</li>
        <li>内容:Mastodon 1.x.x -> 1.x.xへのアップデート</li>
      </ul>
    </article>
  </body>
</html>

これを先程のNGINXで設定したディレクトリに入れます。ファイル自身やディレクトリに対してchmodを使ってパーミッションを変更することを絶対に忘れないで下さいね。そうしないと、せっかくファイルを作っても外部からアクセスすることが出来ません。

メンテナンスモードに切り替え

いよいよ実際にメンテナンスモードに切り替えていきます。今回の設定では /var/tmp/503 が存在するか否かで判定をしていますので、touch /var/tmp/503 を実行するだけで切り替えられます。超便利!逆にメンテナンスモードを解除する時は rm -f /var/tmp/503 を実行するだけです。


tor(外部IP)で確認してみると、しっかりと専用503ページが表示されていることが分かります。しかもURLもそのまま。

この切替さえ忘れなければPuSH購読を切られること無く安心してメンテナンスを行うことが出来るので、是非活用してみてください。

何か不具合などが有りましたらお気軽にコメント下さい。よろしくお願いします。

Mastodonをはじめよう

標準

1週間ほど前から日本国内でアツくなり始めたMastodon(マストドン)。このページでは始め方や仕組み、各プラットフォーム(iOS, Android, Windows, Linux, MacOS…)でのクライアントなどを紹介、説明していきます。

Mastodonとは?

Mastodonは他のSNSでは見られない「分散型」という仕組みを採用しています。個人でも企業でも誰でも簡単にそれぞれの拠点となる「インスタンス」を立ち上げることが出来て、それぞれのインスタンスがそれぞれのサービスとして機能します。それぞれが独立して機能していますので、それぞれのインスタンスが各管理者の意向で動きます。意向というのは例えば運営方針や、メンテナンスのタイミング、話題などです。

と言っても、各インスタンスが独立しているからと言ってそれぞれのインスタンス同士でコミュニケーションがとれないというわけではありません。多くのインスタンスは数百ものインスタンスと「連合」を組んでおり、その連合を組んでいるインスタンス同士であれば普段からフォローをしていなくても「トゥート」をお互いに見ることも可能ですし、連合を組んでいないインスタンス同士でも、「リモートフォロー」をすることで新たにそのインスタンス同士で連合を組むことが出来ます。

このようにオープンソースであることを最大限まで活用して運営されているMastodon。これからも目が離せない新サービスになりそうです。

インスタンスを選ぶ

次は自分が登録するインスタンスを選びましょう。単純にSNSとしてのMastodonを楽しみたいのであれば日本人の多いインスタンスの方がきっと楽しいでしょうし、専門の分野について話したいのであればそれに適したインスタンスを選ぶと良いでしょう。そんな感じにインスタンスを決めるためにインスタンス一覧がいくつか用意されています。ぜひ活用してください。

日本だけでも結構ジャンル別のインスタンスに別れてるんですね。私は「mstdn.jp」と「x0r.be」と「pawoo.net」の3つのインスタンスに登録しています。なお、現段階ではアカウント削除については対応していないので、くれぐれも注意してください。また、無名の海外のインスタンスに登録したりすると凄くアウェー感あふれることになるかもしれない点も注意が必要です。

クライアントを使おう

いざMastodonを頻繁に使おうとしても、いちいちブラウザからインスタンスにアクセスしているようでは通信容量も多くなりますし、なにより面倒です。そこで使うのが専用クライアント。これを使うことでより一層便利にMastodonを使うことが出来ますので、ぜひ利用してみては。

対応 クライアント名 備考
Android Tusky Androidでは見た感じ最もポピュラーなクライアント。
11t
TootyFruity リリース前なので不安定。
iOS Amaroq 現在はこれしかiOSはありません。
Windows Capella 開発途上なので私の環境では結構不安定でした。
Mastodon UWP app UWP版です。他に比べるとまだほとんど機能はありません。
Windows/Linux/Mac Web-based Desktop Client デスクトップで単独アプリとして楽しめます。結構安定しています。

ブラウザ拡張でより賢く

この章はChromeユーザー対象の文になってしまいますが、ご容赦ください。Chromeには拡張機能が用意されていて、それを使うことでMastodonをより便利に、賢く使うことが出来ます。

とりあえずTooterです。この拡張機能は普段Twitterを使っている方は簡単にTwitterとMastodonの両方に同じ内容を投稿することが出来ます。いちいち画面を切り替えて投稿していて、煩わしい思いをしている方には嬉しい機能です。公式ページ(twitter.comの一部)でのツイート、tweetdeckの両方で使うことが出来ます。



最後に

Mastodonの成長は1週間でmstdn.jpのユーザー数が7万人を超えるほど、めまぐるしい成長を遂げました。これからも暫くはその成長は衰えることは無いでしょう。初期の方はあまりの急成長さで新規会員の登録を中止したり、サーバーを急遽補強したり色々と大変でした。今では7万人を越すユーザー数を獲得している過去にも、途中で全ユーザーのデータが吹っ飛んで0からのスタートを2回していますが、これだけユーザー数が集まるってのは相当ですよね。それだけの魅力があるのだから、これから先もサービスも含めて更に成長していくことを心から願っています。

WordPressを軽量化する

標準

はじめに

ウェブページのロード時間が訪問者のその後の行動に大きく影響を与えることは広く知られていますし、日々のネットサーフィンを思い返してみてもそれは自明です。こちらのマーケティングなどの解析をテーマにしているブログでは

  • 47%の利用者が2秒以内のロード時間を望んでいる
  • 40%の利用者が3秒以上のロード時間では閲覧を諦める

と記述されている程です。

当サイトの実情

P3 (Plugin Performance Profiler)というロードに必要な時間などを計測する専用のプラグインで計測してみたところ、以下のような結果が出ました。


計測結果によるとロード完了までに3.42秒かかっています。これでは遅すぎます。せめて2秒台に収めたいですね。ということで対策を講じていきます。

具体的な対策

今回はWP Super Cacheというプラグインを利用していきます。画像の方の対策はあまりロード時間には一般的には影響を及ぼさなさそうなのでしません。

基本的には設定画面にあるものを推奨に持っていくだけで大丈夫です。これだけでも凄く効果があります。

効果はいかほど?

これらの設定を施した後に再びロード時間を計測してみます。

効果は絶大ですね!なんとロード時間は1.50秒まで短縮されました。これでロード時間の長さによる直帰率は大分下げられたはず・・・閲覧数が増えると良いな。

最後に

慣れていれば5分位で軽量化出来るので世の中はとても便利ですね。ロード時間の長さで悩んでいるサイト管理者の方はぜひ試してみてください。

HTML5.1勧告での変更点の紹介

標準

先月始めの11月1日、W3C(World Wide Web Consortium;ワールド・ワイド・ウェブ・コンソーシアム)からHTML5.1を勧告する文書が公開されました。今回はHTML5.1に搭載された新機能がとても便利なものが多かったと個人的に思ったので、まとめていきます。また、利用できるブラウザについてはCan I use…?で調べて、一般的なPC用のブラウザについての情報についてのみ紹介しています。(※編集当時の情報です。)

picture要素

img要素にpictureやsource属性が追加されました。これを使うことで、低解像度環境で画像を表示させたい時、例えばトリミングなどして見やすくした画像を表示させることが出来ます。実際にChromeで試してみました。

しっかりと分岐されているのが分かります。以下のようにHTMLを記述すると実装できます。

<picture>
   <source media="screen and (max-width: 980px)" srcset="small.png">
   <img src="large.png" alt="#">
</picture>

source要素のmedia属性で指定しているmax-widthはメディアクエリで解像度の指定ができるので便利です。メディアクエリと同様、source要素は複数追加することが出来ます。この機能はIE以外の全てのブラウザで利用できます。

sizes属性とsrcset属性

sizes属性とsrcset属性はimg要素に対して追加されたもので、組み合わせて使うことで閲覧環境によってブラウザが判断した上で、最適な画像を読み込んでくれるように切り替えることが出来ます。

<img src="large.png"
     sizes="50vw"
     srcset="small.png 320w, smaller.png 640w">

sizes属性ではビューポート(≒ブラウザの横幅)の何%で画像を表示させたいかを指定します。単位のvwは「Viewport Width」の略です。また、srcset属性では「画像のパス 表示させたい解像度(単位:w)」をコンマ繋ぎで記述します。利用例1では採寸基準がブラウザ幅の50%になるので、620pxの時に320wの画像、1280pxの時に640wの画像を表示するように出来ます。また、sizes属性にはメディアクエリを使うことが出来、以下のように更に細かい分岐が可能になります。

<img src="large.png"
     sizes="(max-width: 640px) 100vw, 50vw"
     srcset="smaller.png 320w, small.png 640w">

利用例2ではsizes属性にメディアクエリを加えました。これをすることで、640px以下の環境では採寸基準はブラウザ幅の100%、それ以上では50%になり、これによってスマホ版とデスクトップ版とでカラム数を異なるものにしたいという望みをより簡単に、より最適に実現させることが出来るようになりました。しかしながら、この属性はIEには対応していません。そんな時はpicturefillというライブラリを使うことで解決することが出来ます。

また、srcset属性には相対的な比率を「*.png 1x, *.png 2x」という感じで記述する方法がありますが、この記述を採用した場合はsizes属性を使用することは出来ません。今回は詳しい説明を省きます。

details要素とsummary要素

details要素とsummary要素が追加されたことで、難解な用語を簡単にHTMLだけを使って表示させることが出来るようになりました。

<details>
   <summary>詳細見出し</summary>
   <p>簡単にHTMLだけを使って詳しい説明ができます。</p>
</details>

記述法は至って簡単で、上記の通りです。この記述だけで以下のサンプルのように簡単に説明するための展開タブが実装できます。※IE、Edgeは利用できません。


 

詳細見出し

簡単にHTMLだけを使って詳しい説明ができます。

 


sortable属性

table要素にsortable属性を追加することが出来るようになりました。これによって、テーブルの中でもその列のものがソート出来るのか出来ないのかをブラウザー側に示すことが出来ますが、現段階ではどのブラウザでもソートするまでの機能を実装しているものはありません。しかし、将来的にはソート出来るブラウザが登場するものと見られ、これからの発展が期待されます。ここでは、実際のソースを掲載します。

<table sortable="sortable">
   <thead>
     <tr>
       <th sorted="1">名前</th>
       <th sorted="2">数学Ⅲ</th>
       <th>物理</th>
     </tr>
   </thead>
   <tbody>
     <tr>
       <td>Aくん</td>
       <td>73</td>
       <td>83</td>
     </tr>
     <tr>
       <td>Bくん</td>
       <td>52</td>
       <td>43</td>
     </tr>
   </tbody>
</table>

menu要素とmenuitem要素

これらを使うことで、JavaScriptなどを一切利用せずにHTMLのみでコンテクストメニュー(右クリックメニュー)を展開させることが出来ます。この要素は現在はFirefoxしか対応していませんが、順次Chromeなどのブラウザでも利用できるようになる予定です。また、対応していないブラウザが殆どの為、現在実装するとそのまま出てきてしまいますので、CSSなどで隠すようにしてください。

<p contextmenu="context-menu">
   ここを右クリック<!--// 反応する領域を定義 //-->
</p>

<menu type="context" id="context-menu">
   <menuitem type="checkbox" checked="true">チェック1</menuitem>
   <menuitem type="checkbox">チェック2</menuitem>
   <menuitem type="checkbox" disabled>無効化</menuitem>
   <menuitem type="command" onclick="alert('押されました。')">コマンド</menuitem>
   <menuitem type="radio" name="radio" checked="true">ラジオ1</menuitem>
   <menuitem type="radio" name="radio">ラジオ2</menuitem>
</menu>

サンプルを実装すると以下のようになります。Firefoxで見てみてください。


ここを右クリック


最後に/補足

ここまでHTML5.1での追加要素を説明してきましたが、一部に過ぎません。この他にもAPI関連やイベント関連などたくさんあります。また、追加ではなく変更されたものや、廃止されたものもあります。これからHTML5.2や5.3がリリースされるのではないかというのが世間一般の予測ですが、これから更にHTMLが便利になっていくことを考えるととても楽しみです。

PHPでTwitterのユーザー情報を取得する

標準

今回は、PHPでTwitterのユーザー情報を取得してみたいと思います。このユーザーの情報は内部ID(Twitterのシステム内部で定められている、一意のIDの事。このIDは変更することが出来ない。)や、スクリーンネーム、名前を始めとして、アカウント作成した日時などありとあらゆる情報を取得できます。こちらは非常に簡単に作成出来るので、早速作っていきます。

使用するライブラリ

今回も例のごとく abraham/twitteroauth を使っていきます。使い方の詳細は前章を御覧ください。

コード

今回はプロフィール情報としても特に重要だと考える一部の情報を呼び出し、列挙する方法について書いていきます。最終的にコードは以下のようになりました。

<?php
$connection=new TwitterOAuth($api_key, $api_secret, $access_token, $access_token_secret);
$user_data=$connection->get("users/show",["screen_name"=>$user_screen_name]);

echo "name: " . $user_data->name;
echo "screen name: @" . $user_data->screen_name;
echo "location: " . $user_data->location;
echo "description: " . $user_data->description;
echo "url: " . $user_data->entities->url->urls[0]->expanded_url;
if($user_data->protected) {echo "protected";} else {echo "unprotected";}
echo "followers: " . number_format($user_data->followers_count);
echo "follows: " . number_format($user_data->friends_count);
echo "listed: " . number_format($user_data->listed_count);
echo "created at: " . $user_data->created_at;
echo number_format($user_data->favourites_count) . " favs";
echo number_format($user_data->statuses_count) . " tweets";
echo "banner-image: " . $user_data->profile_banner_url;
echo "profile-image" . $user_data->profile_image_url;
if($user_data->following) {echo "have followed";} else {echo "haven't followed";}
if($user_data->follow_request_sent) {echo "haven't sent";} else {echo "have sent";}
?>

今回のコードでは2行目にTwitterに接続して、3行目にユーザー情報を取得しています。この時、ユーザーの情報がstdClassで返ってくるので、それを分解して表示させます。

今回このユーザー情報を取得するためにはusers/showからgetしていますが、この時引数として指定できるのは

  • user_id (必須): Twitterから与えられている一意のID
  • screen_name(必須): 見慣れた@に続くID
  • include_entities(任意): エンティティ(返ってくるデータにエンティティノードが存在する。詳しくは後述。)を含めるか

を指定できます。また、この場合user_idとscreen_nameの両方が必須となっていますが、実際にはどちらか一方を指定するのみで大丈夫です。

返ってくるデータ

実際にgetしてみた時のデータをvar_dump()で見てみるとこんな感じです。

object(stdClass)#3 (46) {
	["id"]=> int(2414640626)
	["id_str"]=> string(10) "2414640626"
	["name"]=> string(5) "yoooo"
	["screen_name"]=> string(9) "yoshi_pc_"
	["location"]=> string(12) "鹿屋基地"
	["profile_location"]=> NULL
	["description"]=> string(53) "長門主義(ハルヒ)/SCP財団職員/提督業"
	["url"]=> string(23) "https://t.co/MP3ew8X4mK"
	["entities"]=> object(stdClass)#8 (2) {
		["url"]=> object(stdClass)#9 (1) {
			["urls"]=> array(1) {
				[0]=> object(stdClass)#10 (4) {
					["url"]=> string(23) "https://t.co/MP3ew8X4mK"
					["expanded_url"]=> string(18) "http://yoshipc.net"
					["display_url"]=> string(11) "yoshipc.net"
					["indices"]=> array(2) {
						[0]=> int(0)
						[1]=> int(23)
					}
				}
			}
		}
		["description"]=> object(stdClass)#11 (1) {
			["urls"]=> array(0) { }
		}
	}
	["protected"]=> bool(false)
	["followers_count"]=> int(663)
	["friends_count"]=> int(812)
	["listed_count"]=> int(14)
	["created_at"]=> string(30) "Thu Mar 27 16:51:41 +0000 2014"
	["favourites_count"]=> int(42076)
	["utc_offset"]=> int(32400)
	["time_zone"]=> string(5) "Tokyo"
	["geo_enabled"]=> bool(true)
	["verified"]=> bool(false)
	["statuses_count"]=> int(99429)
	["lang"]=> string(2) "ja"
	["status"]=> object(stdClass)#12 (22) {
		["created_at"]=> string(30) "Tue Oct 18 10:54:24 +0000 2016"
		["id"]=> int(788332394625441792)
		["id_str"]=> string(18) "788332394625441792"
		["text"]=> string(21) "千葉県魔うまい"
		["truncated"]=> bool(false)
		["entities"]=> object(stdClass)#13 (4) {
			["hashtags"]=> array(0) { }
			["symbols"]=> array(0) { }
			["user_mentions"]=> array(0) { }
			["urls"]=> array(0) { }
		}
		["source"]=> string(63) "TheWorld Rev"
		["in_reply_to_status_id"]=> NULL
		["in_reply_to_status_id_str"]=> NULL
		["in_reply_to_user_id"]=> NULL
		["in_reply_to_user_id_str"]=> NULL
		["in_reply_to_screen_name"]=> NULL
		["geo"]=> NULL
		["coordinates"]=> NULL
		["place"]=> NULL
		["contributors"]=> NULL
		["is_quote_status"]=> bool(false)
		["retweet_count"]=> int(0)
		["favorite_count"]=> int(1)
		["favorited"]=> bool(false)
		["retweeted"]=> bool(false)
		["lang"]=> string(2) "ja"
	}
	["contributors_enabled"]=> bool(false)
	["is_translator"]=> bool(false)
	["is_translation_enabled"]=> bool(false)
	["profile_background_color"]=> string(6) "C0DEED"
	["profile_background_image_url"]=> string(78) "http://pbs.twimg.com/profile_background_images/602136959914418176/TWaUNkvB.jpg"
	["profile_background_image_url_https"]=> string(79) "https://pbs.twimg.com/profile_background_images/602136959914418176/TWaUNkvB.jpg"
	["profile_background_tile"]=> bool(true)
	["profile_image_url"]=> string(74) "http://pbs.twimg.com/profile_images/762332527734378496/AzzARkY3_normal.jpg"
	["profile_image_url_https"]=> string(75) "https://pbs.twimg.com/profile_images/762332527734378496/AzzARkY3_normal.jpg"
	["profile_banner_url"]=> string(59) "https://pbs.twimg.com/profile_banners/2414640626/1468929549"
	["profile_link_color"]=> string(6) "3B94D9"
	["profile_sidebar_border_color"]=> string(6) "000000"
	["profile_sidebar_fill_color"]=> string(6) "DDEEF6"
	["profile_text_color"]=> string(6) "333333"
	["profile_use_background_image"]=> bool(true)
	["has_extended_profile"]=> bool(true)
	["default_profile"]=> bool(false)
	["default_profile_image"]=> bool(false)
	["following"]=> bool(false)
	["follow_request_sent"]=> bool(false)
	["notifications"]=> bool(false)
	["translator_type"]=> string(7) "regular"
	["suspended"]=> bool(false)
	["needs_phone_verification"]=> bool(false)
}

これを使うだけでも十分なユーザープロフィール画面が作れそうですね。前述したエンティティを含めるか、と言うのは上記のデータの中で「entities」以下のハイライトされている部分を含めるかどうかということです。主にURLについてのデータなので、不要だと感じたのであれば拒否ることで(若干ですが)早い挙動を得ることが出来るでしょう。
実際に作ってみたサンプルがこちらです。

最後になりますが、実践としてクライアントを同時並行で制作しているので、お試しいただければ幸いです。
http://yoshipc.net/twitter/

では、次回をお楽しみに。

PHPでTwitterのタイムラインを取得する

標準

今回、色々な事情を鑑みてこのレンタルサーバーを利用してWebアプリという形態でTwitterクライアントを作成することにしました。Webクライアントの強みはどのプラットフォームでも動作可能など、強みがたくさんあるので、作る価値があるかもしれません。しかも、私の場合は例の「事情」もあるので尚更です。というわけでPHPを利用したTwitterのクライントを作っていくシリーズがこれより始まります。

利用するライブラリ

今回は abraham/twitteroauth というものを使っていきます。このライブラリはTwitterの一通りのAPIが実装されているので、私が目標とするクライアント作成にはもってこいです。

ダウンロードした後、autoload.phpを読み込ませることで利用できるようになります。(本来はcomposer.jsonを使って読み込ませるのが正規ルートです。)

$connection = new TwitterOAuth($api_key, $api_secret, $access_token, $access_token_secret);
$statuses = $connection->get("statuses/home_timeline", ["count" => 50]); //タイムラインを50ツイート取得
if(isset($statuses->errors)) {
  //エラー発生
  echo "some error occurred.";
  echo "error message: " . $statuses->errors[0]->message;
} else {
  //ツイートの取得に成功
  for ($i=0; $i < 49; $i++) {
    echo "profile image url: " . $statuses[$i]->user->profile_image_url;
    echo "user name: " . $statuses[$i]->user->name;
    echo "user's id: " . $statuses[$i]->user->screen_name;
    echo "tweet text: " . $statuses[$i]->text;
    echo "created at: " . $statuses[$i]->created_at;
    echo "via: " . $statuses[$i]->source;
    echo $statuses[$i]->retweet_count . " rt";
    echo $statuses[$i]->favorite_count . " fav";
    if($statuses[$i]->user->protected) {echo "非公開";}
    echo "-------------------------------"
    }
  }

1行目で取得済みの各キーでTwitterに接続するための前準備を行い、2行目でタイムラインを50ツイート分取得しています。この時、APIのレートリミットを超過したアクションを行うとerrors[]->messageにその内容が帰ってきます。(エラーコードなども取得できるので詳しくはvar_dumpなどを行ってください。)

8行目以降はツイート取得に成功した場合です。最小限で利用するような情報を引き抜いてみました。それぞれのツイートには、ツイートに関する情報が$statuses以下、投稿者に関する情報が$statuses->userにそれぞれ格納されています。

以下は、試しにたまたま流れてきたユーザーの$statuses(Array 配列だけではなく、stdClass オブジェクトでも構成されていることに注意。Arrayは$statuses[“hoge”]と指定するのに対して、オブジェクトは$statuses->hogeと指定。)をvar_dump()で出力してみた結果が以下です。

array(25) { 
	[0]=> object(stdClass)#3 (23) { 
		["created_at"]=> string(30) "Fri Oct 14 12:44:02 +0000 2016"
		["id"]=> int(786910433936420869)
		["id_str"]=> string(18) "786910433936420869"
		["text"]=> string(30) "コジィさん出るのかな"
		["truncated"]=> bool(false)
		["entities"]=> object(stdClass)#8 (4) {
			["hashtags"]=> array(0) { }
			["symbols"]=> array(0) { }
			["user_mentions"]=> array(0) { }
			["urls"]=> array(0) { }
		}
		["source"]=> string(81) "Twitter for iPad"
		["in_reply_to_status_id"]=> NULL
		["in_reply_to_status_id_str"]=> NULL
		["in_reply_to_user_id"]=> NULL
		["in_reply_to_user_id_str"]=> NULL
		["in_reply_to_screen_name"]=> NULL
		["user"]=> object(stdClass)#9 (41) {
			["id"]=> int(219588098)
			["id_str"]=> string(9) "219588098"
			["name"]=> string(9) "ふゆべ
			["screen_name"]=> string(11) "who_you_bee"
			["location"]=> string(0) ""
			["description"]=> string(64) "Tumblr:https://t.co/X4hF7e4Jvi Pixiv:https://t.co/8vCm4L63xm"
			["url"]=> string(23) "https://t.co/8vCm4L63xm"
			["entities"]=> object(stdClass)#10 (2) {
				["url"]=> object(stdClass)#11 (1) {
					["urls"]=> array(1) {
						[0]=> object(stdClass)#12 (4) {
							["url"]=> string(23) "https://t.co/8vCm4L63xm"
							["expanded_url"]=> string(27) "http://pixiv.me/savoiaxxs21"
							["display_url"]=> string(20) "pixiv.me/savoiaxxs21"
							["indices"]=> array(2) {
								[0]=> int(0)
								[1]=> int(23)
							}
						}
					}
				}
				["description"]=> object(stdClass)#13 (1) {
					["urls"]=> array(2) {
						[0]=> object(stdClass)#14 (4) {
							["url"]=> string(23) "https://t.co/X4hF7e4Jvi"
							["expanded_url"]=> string(26) "http://huyube21.tumblr.com"
							["display_url"]=> string(19) "huyube21.tumblr.com"
							["indices"]=> array(2) {
								[0]=> int(7)
								[1]=> int(30)
							}
						}
						[1]=> object(stdClass)#15 (4) {
							["url"]=> string(23) "https://t.co/8vCm4L63xm"
							["expanded_url"]=> string(27) "http://pixiv.me/savoiaxxs21"
							["display_url"]=> string(20) "pixiv.me/savoiaxxs21"
							["indices"]=> array(2) {
								[0]=> int(37)
								[1]=> int(60)
							}
						}
					}
				}
			}
			["protected"]=> bool(false)
			["followers_count"]=> int(8710)
			["friends_count"]=> int(668)
			["listed_count"]=> int(350)
			["created_at"]=> string(30) "Thu Nov 25 08:30:34 +0000 2010"
			["favourites_count"]=> int(15994)
			["utc_offset"]=> int(32400)
			["time_zone"]=> string(5) "Tokyo"
			["geo_enabled"]=> bool(false)
			["verified"]=> bool(false)
			["statuses_count"]=> int(96048)
			["lang"]=> string(2) "ja"
			["contributors_enabled"]=> bool(false)
			["is_translator"]=> bool(false)
			["is_translation_enabled"]=> bool(false)
			["profile_background_color"]=> string(6) "C0DEED"
			["profile_background_image_url"]=> string(79) "http://pbs.twimg.com/profile_background_images/488513744176037888/Jq1mlOeM.jpeg"
			["profile_background_image_url_https"]=> string(80) "https://pbs.twimg.com/profile_background_images/488513744176037888/Jq1mlOeM.jpeg"
			["profile_background_tile"]=> bool(true)
			["profile_image_url"]=> string(74) "http://pbs.twimg.com/profile_images/759076048420032512/nXPEc9QI_normal.jpg"
			["profile_image_url_https"]=> string(75) "https://pbs.twimg.com/profile_images/759076048420032512/nXPEc9QI_normal.jpg"
			["profile_banner_url"]=> string(58) "https://pbs.twimg.com/profile_banners/219588098/1471816404"
			["profile_link_color"]=> string(6) "ABB8C2"
			["profile_sidebar_border_color"]=> string(6) "FFFFFF"
			["profile_sidebar_fill_color"]=> string(6) "DDEEF6"
			["profile_text_color"]=> string(6) "333333"
			["profile_use_background_image"]=> bool(true)
			["has_extended_profile"]=> bool(false)
			["default_profile"]=> bool(false)
			["default_profile_image"]=> bool(false)
			["following"]=> bool(true)
			["follow_request_sent"]=> bool(false)
			["notifications"]=> bool(false)
		}
	["geo"]=> NULL
	["coordinates"]=> NULL
	["place"]=> NULL
	["contributors"]=> NULL
	["is_quote_status"]=> bool(false)
	["retweet_count"]=> int(0)
	["favorite_count"]=> int(0)
	["favorited"]=> bool(false)
	["retweeted"]=> bool(false)
	["lang"]=> string(2) "ja"
}

想像以上に情報が取得できていて、驚きです。これらの情報を使うことで、クライアントとしての道以外にも使い道が出来ると思うので、ぜひご活用ください。

また、実際にクライアントを作成している様子をこちらのサーバーで公開中なので、ぜひお試しいただければと。
http://yoshipc.net/twitter

SQL Serverの予約語に苦しめられる

標準

こんにちは。最近SQLサーバーをPHPでいじる機会が増えたんですが、その時いくら見直してもおかしいところが無い時、結局おかしかったのは(なぜか)予約語を使ってるのが原因だったり、色々と苦しめられました。
それを鑑みて一度予約語をまとめておきます。以下はOracle SQLとMicrosoft SQLのいずれかで予約語として指定されているワード一覧です。ここに記載されている予約語はいずれかで予約語として使われているので、どういった場面でも使用するのは望ましくないです。(スマホで閲覧する場合は、テーブルが横にもスクロールします。)

absolute dec long smallint
access decimal longbinary smallint
add declare longchar smallmoney
add default longtext some
admindb delete lower space
all delete match sql
all desc max sqlcode、sqlerror、sqlstate
alphanumeric disallow maxextents start
alter disconnect memo stdev
alter distinct min stdevp
alter distinctrow minus string
and domain minute substring
any double mlslabel successful
are drop mod sum
as else mode synonym
as eqv modify sysdate
asc exclusive money sysname
assertion exclusiveconnect month system_user
audit exec、execute national table
authorization exists nchar tableid
autoincrement extract nested_table_id temporary
avg false noaudit text
begin fetch nocompress then
between file nonclustered time
binary first not timestamp
bit float、float8 nowait timezone_hour
bit_length float4 ntext timezone_minute
boolean for null tinyint
both foreign number to
by from numeric top
byte general nvarchar trailing
cascade grant octet_length transaction
catalog group of transform
char guid offline translate
char、character having oleobject translation
char_length hour on trigger
character_length identified online trim
check identity open true
close ieeedouble option uid
cluster ieeesingle or union
clustered ignore or unique
coalesce image order uniqueidentifier
collate immediate pctfree unknown
collation imp prior update
column in privileges updateidentity
column_value increment public updateowner
comment index raw updatesecurity
commit indexcreatedb real upper
comp initial references usage
compress inner rename user
connect input resource using
connection insensitive restrict validate
constraint、constraints insert revoke value
container int、integer、integer4 right values
contains integer rollback var
convert integer1 row varbinary
count integer2 rowid varchar
counter intersect rownum varchar2
create interval rows varp
create into schema varying
currency is second view
current isolation select when
current_date join selectschema whenever
current_time key selectsecurity where
current_timestamp language session with
current_user last set work
cursor left share xor
database level short year
date like single yesno
datetime lock size zone
day logical、logical1 smalldatetime

以上238個の予約語でした。想像以上にあって困ったものです。これは適当にやってても引っかかりますわ・・・みなさんもお気をつけて。

WordPressをスパムから守る

標準

最近当サイトにもスパムが多く襲うようになりました。

スパム

上の画像は実際に当サイトに送られたスパムコメントの一例です。多い日にはこのようなメッセージが1日に4~5件届きます。

今回はこのようなただの迷惑なスパムコメントを徹底的に除去するために施した処置についてまとめていきます。

プラグイン導入

早速スパム対策に特化したプラグインを入れていきます。その名も「Akismet」です。評価は5点中4.8点と非常に高く、スパム対策系のプラグインでは老舗と言った感じです。

では早速インストールしてみます。設定画面を開くとこのような感じの画面になります。

akismet初回起動画面

このプラグインはakismetのアカウントを取得した上で、APIキーを取得する必要があります。

https://akismet.com/wordpress/

こちらのページからakismetのアカウントを作成します。wordpress.comのアカウントがあれば即刻ログインできます。アカウント作成、ログインなどが成功すると、プランの設定画面が出てきます。理由がなければ「Get Basic」を選択し、無料で始めるのがいいと思います。

無料のプランで始めても次の画面で再び違うプランに行くように誘導されるので注意します、具体的には支払画面が出てくるんですね。ここでは支払金額のレンジを0にすることで、無料で次に進めます。

支払画面でレンジを0に

最初は戸惑いますよね。0ドルにするとcontinueだけの画面になるので、そのままコンテニューします。

アクティベート画面

するとこのような画面が出てきます。内容に相違なければ、Activate this siteをクリックします。すると、自分のサイトにリダイレクトされて作業は終了です。もしリダイレクトされなければ、以下の画像のようにAPIキーを自分でコピーしてWordpess側で手動入力し、アクティベート終了です。

APIキー

目のアイコンをクリックすると、アスタリスクが解除されます。

以上でスパム対策の作業は完了になります。

効果は?

導入して1日経ったところでAkismetのダッシュボードを見ると以下のようになっていました。

akismetのダッシュボード

ちゃんとブロックされているのがわかります。ちなみにに設定での「厳密度」は「スパムを常にスパムフォルダーに振り分けレビューする。」に設定しています。これで十分すぎるくらいですね。

プラグインの導入でスパムに触れないwordpressライフを送りましょう。