解説コードを読んでみよう
このサンプルでわかること
- ajax でフォームの内容を PHP に送り、結果だけを画面に差し込む方法
- ランダムな文字列を作り、データベースに保存する方法
header('Location: ...')で別のページへ転送(リダイレクト)する方法- QRコードをブラウザ側で作る方法(qrcode.js)
使っているファイル
- index.php入力画面(URL を ajax で create.php に送る)
- create.php短縮コードを作ってデータベースに保存し、結果の HTML を返す
- go.php短縮URLが開かれたとき、元のURLへ転送する
- tool.phpランダム文字列を作る関数・ページタイトルを取得する関数
- db.phpデータベースに接続する関数
- js/qrcode.min.jsQRコードを作るライブラリ(qrcode.js)
0. データベースのテーブル
短縮コード(uri)と元のURL(url)の組み合わせを保存するテーブルです。ページのタイトルや作成日時、作成した人の IP アドレスも一緒に記録します。
CREATE TABLE sumidai_short_url (
uri VARCHAR(10) NOT NULL, -- 短縮コード(例: Ab3x)
domain VARCHAR(255) NOT NULL, -- 短縮URLのドメイン
url TEXT NOT NULL, -- 元のURL
title VARCHAR(255) DEFAULT '', -- 元のページのタイトル
time DATETIME NOT NULL, -- 作成日時
ip VARCHAR(45) DEFAULT '', -- 作成した人の IP アドレス
PRIMARY KEY (uri)
);
1. 入力画面から ajax で送る
「短縮する」ボタンが押されたら、$.ajax() で入力された URL を create.php に送ります。ページ全体を読み直さず、返ってきた HTML を #short の中に差し込むだけなので、画面がちらつきません。
短縮URLができていたら、その URL から QR コードを作ります。new QRCode(場所, 設定) と書くだけで、画像が作られます。
<script>
$(function () {
$('#make').on('click', function () {
var url = $('input[name="url"]').val();
$('#qrcode').empty();
$.ajax({
url: 'create.php', // 同じフォルダの create.php に送る
type: 'POST',
data: { url: url },
timeout: 10000,
dataType: 'html'
})
.done(function (data) {
$('#short').html(data); // 返ってきた HTML をそのまま表示
var link = $('#link').val();
if (link) { // 短縮URLができていたら QR コードを作る
new QRCode(document.getElementById('qrcode'), { text: link, width: 160, height: 160 });
$('input[name="url"]').val('');
}
})
.fail(function () {
$('#short').html('<div class="alert alert-danger">通信に失敗しました。</div>');
});
});
// コピーボタン(あとから追加された要素なので document に対して設定する)
$(document).on('click', '#copy', function () {
var text = $('#link').val();
var done = function () { $('#complete').text('コピーしました'); };
var oldWay = function () { // 古いブラウザ用
$('#link').trigger('select');
document.execCommand('copy');
done();
};
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(done, oldWay);
} else {
oldWay();
}
});
});
</script>
「コピーする」ボタンは、ajax であとから追加された要素です。$('#copy').on('click', ...) と書いても、ページを開いた時点ではボタンが存在しないので反応しません。$(document).on('click', '#copy', ...) のように書くと、あとから追加された要素にも効きます。
2. 受け取った URL を確認する(create.php)
送られてきた文字が本当に URL か確認します。filter_var($url, FILTER_VALIDATE_URL) は PHP に用意されている URL のチェック機能です。さらに http:// か https:// で始まるものだけを受け付けます(javascript: などの危ない URL を防ぐため)。
// 入力されたURL(保存は「そのまま」、画面に出すときにエスケープする)
$url = trim((string)($_POST['url'] ?? ''));
// http:// か https:// で始まるURLだけ受け付ける
if (!preg_match('#^https?://#i', $url) || filter_var($url, FILTER_VALIDATE_URL) === false || strlen($url) > 2000) {
echo '<div class="alert alert-danger" role="alert">入力された文字がURLの形式ではありません。</div>';
exit;
}
3. 4文字のコードを作って保存する
makeRandStr(4) で英数字4文字を作り、同じコードがすでにあれば作り直します(do 〜 while は「まず1回やって、条件が合う間くり返す」書き方です)。決まったら、データベースに INSERT します。
$title = getPageTitle($url);
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
// 4文字のコードを作り、使われていたら作り直す
do {
$uri = makeRandStr(4);
} while (confirmation($uri, $db));
$stmt = $db->prepare("INSERT INTO sumidai_short_url (uri, domain, url, title, time, ip)
VALUES (:uri, :domain, :url, :title, now(), :ip)");
$stmt->execute([':uri' => $uri, ':domain' => $host, ':url' => $url, ':title' => $title, ':ip' => $ip]);
// 公開デモなので、古いものから削除して件数を一定に保つ
demo_prune_table($db, 'sumidai_short_url', 'time', DEMO_MAX_URLS);
// 短縮URL(このサイトの go.php?c=コード)
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$short = $scheme . '://' . $host . url('php/short-url/go.php') . '?c=' . $uri;
// ランダムな英数字を作る makeRandStr(文字数)
function makeRandStr($length) {
$str = array_merge(range('a', 'z'), range('0', '9'), range('A', 'Z'));
$r_str = '';
for ($i = 0; $i < $length; $i++) {
$r_str .= $str[random_int(0, count($str) - 1)]; // 推測されにくい乱数を使う
}
return $r_str;
}
random_int() は、暗号にも使える「予想されにくい」乱数を作る関数です。短縮URLのコードが予想できると、他人の作ったURLを順番に開かれてしまうため、rand() より random_int() が向いています。
4. 元のページのタイトルを取得する
cURL という機能で元のURLのページを読み込み、<title> の中身を正規表現で取り出します。
// 外部から取りに行ってよいURLか確認する
// (サーバーの内側=社内ネットワークなどにアクセスさせないための対策)
function isPublicUrl($url) {
$host = parse_url($url, PHP_URL_HOST);
if (!is_string($host) || $host === '') {
return false;
}
$ip = gethostbyname($host);
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
}
// ページのタイトルを取得する getPageTitle(URL)
function getPageTitle($url){
if (!isPublicUrl($url)) {
return '';
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 結果を画面に出さず、文字列で受け取る
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); // 転送先は追わない
curl_setopt($ch, CURLOPT_TIMEOUT, 5); // 5秒で諦める
$html = curl_exec($ch);
curl_close($ch);
if (!is_string($html) || $html === '') { // 取得に失敗したとき(false)は空文字
return '';
}
$html = mb_convert_encoding($html, 'UTF-8', 'ASCII,JIS,UTF-8,CP51932,SJIS-win');
return preg_match('@<title>([^<]++)</title>@i', $html, $m) ? trim($m[1]) : '';
}
「ユーザーが入力した URL にサーバーがアクセスする」機能は、悪用されると、外からは見えないサーバー内部のネットワークを調べられてしまう危険があります(SSRF という攻撃)。isPublicUrl() で、行き先がインターネット上の公開アドレスかどうかを確認してからアクセスしています。
5. 短縮URLから元のURLへ転送する(go.php)
短縮URL go.php?c=Ab3x が開かれたら、$_GET['c'] でコードを受け取り、データベースから元のURLを探します。見つかったら header('Location: 元のURL') で転送します。見つからなければ「404(ページがありません)」を返します。
<?php
// 短縮URLを開いたときに、元のURLへ転送する 例: go.php?c=Ab3x
require_once 'db.php';
$code = (string)($_GET['c'] ?? '');
$url = false;
if (preg_match('/^[A-Za-z0-9]{4}$/', $code)) { // コードは英数字4文字だけ
$db = db_connect();
$stmt = $db->prepare("SELECT url FROM sumidai_short_url WHERE uri = :uri");
$stmt->bindValue(':uri', $code);
$stmt->execute();
$url = $stmt->fetchColumn();
}
// 見つかって、http/https のURLなら転送する
if (is_string($url) && preg_match('#^https?://#i', $url)) {
header('Location: ' . $url, true, 302);
exit;
}
// 見つからなかったとき
http_response_code(404);
header() は、HTML を1文字でも出力する前に呼ぶ必要があります。先に何か表示してしまうと「Cannot modify header information」というエラーになり、転送されません。
PHP 8.3 で書き直したポイント
- 旧版は、短縮に「real.ml」などの無料ドメインを使っていましたが、そのサービスが終了して使えなくなりました。このサイト自身の
go.phpで転送する方式に変えました。 - 旧版は URL を
htmlspecialchars()してから保存していたため、&を含む URL が壊れていました。保存は元のまま行い、画面に表示するときにだけエスケープ(h())するのが正しい順番です。 - 重複したコードを作り直す処理が、結果を使っていなかった(バグ)ため、
do 〜 whileで確実に作り直すようにしました。 - コピー機能を、非推奨になった
document.execCommand('copy')からnavigator.clipboardに変更しました(古いブラウザ用に旧方式も残しています)。
コードの文字数を 4 から 6 に変えてみましょう(makeRandStr(6) と go.php の {4})。作れる組み合わせが一気に増えます。