Cisco 7961G電話機でCardDAVの連絡帳を利用する

この記事の題名は7961電話機となっているけどCiscoのIP電話ならたぶんそのまま利用できる筈。Cisco以外のIP電話機でもウェブサーバからXMLで電話帳を取る仕組みがある機種ならXMLを少し変えるだけで同じように利用できると思う。

Cisco 7961G電話機のサービスメニュー設定の記事で、電話帳としてXMLテキストファイルを書くなら32エントリーまでなので(少なすぎて使い物にならないので)普通はCGIなどでというようなことを書いたけど、せっかくなのでCGIの電話帳サンプルをブログ記事用に作ってみた。
巨大にならないよう必要最低限の「なんとか動くよね」レベルなのでこの記事のサンプルを流用してどうこうするよりは、ちょろっと見るだけ見て後は自分で1から作った方がまともなのが出来るんじゃないかな。

連絡帳のデータとしてCardDAVを利用することにしたのは個人・グループレベルであれば大掛かりな連絡帳データベースを作るほどじゃないし、既に利用しているCardDAVがあればそれをそのまま利用できる方が簡単じゃないかなと思ったから。
企業レベルであったり個人でも連絡先が膨大にあるということであればデータベースやLDAPで管理してそこからデータをひっぱるようにすりゃ良いけど個人ブログでやることじゃなさそうだから触れない。

Ciscoの電話機には一応文字検索用の機能が付いていて電話機のダイヤルボタンを携帯電話の文字打ちの様に押して文字を入力すれば検索できるんだけど、Ciscoということもあって英数字だけ。日本語が入力できないので日本語検索ができない。そこで最も単純に50音の行別(あ行・か行・さ行・・・)に分けてリスト表示して選んで貰うという方式にした。

おおまかな仕組みは、CardDAVサーバから連絡帳データをまるっと貰ってきてそれをひらがな50音の行別に分けてリスト表示。宛先・かけ先を選択したら電話番号を表示。
スクリプトは基本的にはURLの引数による出し分け。

URLの例
  • http://HOGE/directory.php ひらがな50音の行一覧リスト
  • http://HOGE/directory.php?search=xx 指定した行別リスト
  • http://HOGE/directory.php?search=xx&page=nn 指定した行リストに複数ページある場合
  • http://HOGE/directory.php?search=xx&order=nn 指定した行リストから特定の相手を選んだ場合(ページの有無関係なし)

CardDAVサーバからデータを取る部分は自作する気にならなかったのでChristian Putzke氏のCardDAV PHPを利用させて貰う。ファイル1つをGitHubから貰ってくるだけ。変更不要。

CardDAVというかvCardでは多くの種類のエレメントがあるがそれらを網羅することはすっぱり諦めて以下のエレメントだけを扱うことにする。

  • FN: フルネーム表示用
  • X-PHONETIC-LAST-NAME: ラストネーム(姓)のよみがな
  • TEL;TYPE=home: 家の電話番号
  • TEL;TYPE=cell: 携帯の電話番号
  • TEL;TYPE=work: 仕事用の電話番号
  • TEL;TYPE=other: その他の電話番号

例えばクルマ用電話番号のTEL;TYPE=carとか秘書用電話番号のTEL;TYPE=x-assistantとかはもちろん扱わない。(その他たくさん)
CardDAVの登録・編集アプリによっては家用の電話番号をTEL;TYPE=HOME,VOICEのようにおまけ付きで登録するのがあるが、この記事のスクリプトではTEL;TYPE=homeまでを(大文字小文字関係なく)見て家用と判断し、カンマから後ろは無視。あと、1人が複数の携帯番号を持っていて TEL;TYPE=cell:xxxxxxxx が複数ある場合、CardDAVで取得したデータの中で先に出現した番号は後に出現した番号に上書きされるので表示されない。(注意)

ラストネームの最初の文字で50音の行別振り分けを行うのでラストネームを登録していなかったりアルファベットで登録していると振り分けが「英数他」に入る筈。

行別の宛先名が10を超えるとリスト表示の前にページリストが表示される。メニュー表示のアイテム数の限度が不明だけどディレクトリリストと同じく32件が限度であれば行別で登録可能な総数は行別でそれぞれ320件となる。「か行」「さ行」は多いけど「ま行」「や行」は少ないなど偏りがあると「件数の多い○行は320件では足りない」ということがあるかも。もっと増やしたければ1ページ10件を32件に変更すれば1024件まで扱えるということになる。(あくまでも電話機が扱えるメニューリストの最大アイテム数が32であるとするならばだけど)

directory.php (ファイル名は任意)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
<?php
include_once ('./carddav.php');
//--------------------------------------------------------------
//CardDAVサーバ接続用設定
$davurl = 'https://dav.example.com/remote.php/dav/addressbooks/users/foobar/contacts/'; //CardDAVのURL
$david = 'foobar'; //CardDAVのアカウント
$davpw = 'secretpassword'; //CardDAVのパスワード
//--------------------------------------------------------------
$url = 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];

header('Content-Type: text/xml');  //共通 レスポンスヘッダ XMLコンテンツとする

if (empty ($_GET['search'])) {
        $top = new xmlMenuGen("CiscoIPPhoneMenu", "MenuItem", "Name", "URL");
        $top->addHead('電話帳','選択して下さい');
        $top->addItem('英数他', $url . '?search=an');
        $top->addItem('あ行',   $url . '?search=aa');
        $top->addItem('か行',   $url . '?search=ka');
        $top->addItem('さ行',   $url . '?search=sa');
        $top->addItem('た行',   $url . '?search=ta');
        $top->addItem('な行',   $url . '?search=na');
        $top->addItem('は行',   $url . '?search=ha');
        $top->addItem('ま行',   $url . '?search=ma');
        $top->addItem('や行',   $url . '?search=ya');
        $top->addItem('ら行',   $url . '?search=ra');
        $top->addItem('わ行',   $url . '?search=wa');
        $top->output();
        exit;
}

//CardDAVサーバ接続・認証
$carddav = new carddav_backend($davurl);
$carddav->set_auth($david, $davpw);

if (! $carddav->check_connection()){
        //DAVサーバに接続できなかったら
        echo phoneTextXML('Error','CardDAVサーバに接続できませんでした');
        exit;
}

//DAVサーバに接続できたらデータ取得
$dataRaw = $carddav->get();
$dataArr = simplexml_load_string($dataRaw);
//var_dump($dataArr); //取得したデータ確認用

//取得したデータを配列に入れる
$test = array();
$fn = array();
$ln = array();
$phm = array();
$pcl = array();
$pwk = array();
$pot = array();
$i = 0; //CardDAVレコード番号として
foreach($dataArr->element as $elem){
        foreach($elem->vcard as $vcard){
                $arvcard = explode("\n",$vcard);
                $fn[$i] = '';
                $ln[$i] = '';
                $phm[$i] = '';
                $pcl[$i] = '';
                $pwk[$i] = '';
                $pot[$i] = '';
                foreach($arvcard as $line){
                        if(preg_match('/^FN:/i', $line)){
                                $fn[$i] = preg_replace('/^FN:/i', '', $line);
                                $fn[$i] = str_replace(array("\r\n", "\r", "\n"), '', $fn[$i]);
                        }
                        if (preg_match('/^X-PHONETIC-LAST-NAME:/i', $line)){
                                $ln[$i]  = preg_replace('/^X-PHONETIC-LAST-NAME:/i', '', $line);
                        }
                        if(preg_match('/^TEL;TYPE=home/i', $line)){
                                $phm[$i] = preg_replace('/[^0-9*#]/', '', $line);
                        }
                        if(preg_match('/^TEL;TYPE=cell/i', $line)){
                                $pcl[$i] = preg_replace('/[^0-9*#]/', '', $line);
                        }
                        if(preg_match('/^TEL;TYPE=work/i', $line)){
                                $pwk[$i] = preg_replace('/[^0-9*#]/', '', $line);
                        }
                        if(preg_match('/^TEL;TYPE=other/i', $line)){
                                $pot[$i] = preg_replace('/[^0-9*#]/', '', $line);
                        }
                }
        $i++;
        }
}

//50音x行別リスト
$idx = $_GET['search'];

$kana_idx = array(
"aa" => "[ア-オあ-おア-オ]",
"ka" => "[カ-コか-こが-ごカ-コガ-ゴ]",
"sa" => "[サ-ソさ-そざ-ぞサ-ソザ-ゾ]",
"ta" => "[タ-トた-とだ-どタ-トダ-ド]",
"na" => "[ナ-ノな-のナ-ノ]",
"ha" => "[ハ-ホは-ほば-ぼぱ-ぽハ-ホバ-ボパ-ポ]",
"ma" => "[マ-モま-もマ-モ]",
"ya" => "[ヤ-ヨや-よヤ-ヨ]",
"ra" => "[ラ-ロら-ろラ-ロ]",
"wa" => "[ワ-ンわ-んワ-ン]",
"an" => "[a-zA-Z0-9]"
);

$kana_gyo = array(
"aa" => "あ行", "ka" => "か行", "sa" => "さ行", "ta" => "た行", "na" => "な行",
"ha" => "は行", "ma" => "ま行", "ya" => "や行", "ra" => "ら行", "wa" => "わ行",
"an" => "英数他"
);

//行別リスト全体取得
$lifn = array();
$liphm = array();
$lipcl = array();
$lipwk = array();
$lipot = array();
$k = 0; //行別レコード番号として
for ($j = 0; $j < $i; $j++) {
        foreach ($kana_idx as $kidx=>$ptn) {
                if (preg_match("/^" . $ptn . "/u", $ln[$j])) {
                        if ($idx == $kidx){
                                $lifn[$k] = $fn[$j];
                                $liphm[$k] = $phm[$j];
                                $lipcl[$k] = $pcl[$j];
                                $lipwk[$k] = $pwk[$j];
                                $lipot[$k] = $pot[$j];
                                $k++;
                        }
                }
        }
}

//ページ数
$maxPage = ceil(($k - 1) / 10);
$amrPage = $k % 10;

//かけ先番号表示
if ((! empty ($_GET['order']) and (! empty ( $_GET['search'])))) {
        $order =  $_GET['order'] -1;
        $dial =new xmlMenuGen("CiscoIPPhoneDirectory", "DirectoryEntry", "Name", "Telephone");
        $dial->addHead($lifn[$order], '選択して下さい');
        if ($liphm[$order]){
                $dial->addItem('家', $liphm[$order]);
        }
        if ($lipcl[$order]){
                $dial->addItem('携帯', $lipcl[$order]);
        }
        if ($lipwk[$order]){
                $dial->addItem('仕事', $lipwk[$order]);
        }
        if ($lipot[$order]){
                $dial->addItem('その他', $lipot[$order]);
        }
        $dial->output();
        exit;
}

//ページ表示
if ((empty ($_GET['order']) and (! empty ( $_GET['search'])))) {
        //ページ指定無し
        if (empty ( $_GET['page'])){
                if ($k == 0) {
                        echo phoneTextXML($kana_gyo[$idx], '登録がありません');
                        exit;
                } elseif ($maxPage > 1){
                    $pmenu = new xmlMenuGen("CiscoIPPhoneMenu", "MenuItem", "Name", "URL");
                    $pmenu->addHead($kana_gyo[$idx], '選択して下さい');
                        for ($p = 1; $p <= $maxPage; $p++) {
                                $purl = $url . '&page=' . "$p";
                                $pmenu->addItem("ページ $p", $purl);
                        }
                        $pmenu->output();
                        exit;
                }
        }

        //行別リスト表示
        if (empty ($_GET['page'])){
                $page = 1;
        } else {
                $page = $_GET['page'];
        }
        //1ページ10アイテム表示の計算
        $lst = $page * 10 - 10;
        if (($k - $lst) > 10){
                $len = $lst + 10;
        } else {
                $len = $lst + $amrPage;
        }
        $pmenu = new xmlMenuGen("CiscoIPPhoneMenu", "MenuItem", "Name", "URL");
        $pmenu->addHead($kana_gyo[$idx], '選択して下さい');
        for ($l = $lst; $l < $len; $l++) {
                $m = $l + 1;
                $url = preg_replace('/page=[0-9]/', '', $url);
                $ourl = $url .'&order=' . "$m";
                $pmenu->addItem($lifn[$l], $ourl);
        }
        $pmenu->output();
        exit;
}

//メッセージ用
function phoneTextXML($title, $body){
        $dom = new DomDocument('1.0', 'utf-8');
        $dom->formatOutput = true;
        $out = $dom->appendChild($dom->createElement('CiscoIPPhoneText'));
         $out->appendChild($dom->createElement('Title', $title));
         $out->appendChild($dom->createElement('Text', $body));
        return $dom->saveXML();
}

//メニュー/番号表示用クラス
class xmlMenuGen{
        public $xmlmenu;
        public $menuItem = array();

        function __construct($h1, $h2, $itmA, $itmB){
                $this->h1 = $h1;
                $this->h2 = $h2;
                $this->itmA = $itmA;
                $this->itmB = $itmB;
                $this->xmlmenu = new DOMDocument('1.0', 'UTF-8');
                $this->xmlmenu->formatOutput = true;
                $this->menuItem = $this->xmlmenu->appendChild( $this->xmlmenu->createElement($this->h1));
        }

        function addHead($title, $prompt){
                $this->menuItem->appendChild($this->xmlmenu->createElement('Title', $title));
                $this->menuItem->appendChild($this->xmlmenu->createElement('Prompt', $prompt));
        }

        function addItem($name, $url){
                $item = $this->menuItem->appendChild( $this->xmlmenu->createElement($this->h2));
                $item->appendChild($this->xmlmenu->createElement($this->itmA, $name));
                $item->appendChild($this->xmlmenu->createElement($this->itmB, $url));
        }

        function output(){
                echo $this->xmlmenu->saveXML();
        }
}
?>

ファイルは1つだけ。carddav.phpと同じディレクトリに置くことを想定している。

トップメニュー以外は表示の度にCardDAVサーバからデータを取得するので動作はトロい。それはないだろということなら例えばトップメニューを開いたときにCardDAVサーバからデータを取得し、テキストファイルとして保存して、後はそのテキストファイルを利用するというようなのもあると思うけど、閉じたネットワークに置くとしてもあまり良くないかなと思っている。

あと、少なくともこのスクリプトを置くウェブサーバはインターネットからはアクセスできないところにしてね。インターネット側に公開してURLがバレたら何の認証もなく連絡先がXMLで丸見えになる。
なるべく電話用のネットワークは完全に閉じてるのが望ましいかと。許してもPBXだけがSIPで外部に繋がる程度で。

関連記事:

MySQL 5.7を入れる

「がとらぼ」の動いているサーバ(というか「がとらぼ」は規模が小さいのでおまけで相乗り)が、電源となっていたUPSの故障で、突然電源を絶たれた。普通は大したダメージにならないんだけど、これが余程タイミングが悪かったのかDBサーバのデータが手酷く壊れたようで、復旧を試みてもまったく歯が立たず。
MySQLをクリーンインストールしてバックアップのデータを流し込むことにした。
大量のデータを抱えるデータベースを迂闊に触るとロクなことにならないということもあってこのホストは構築時に入れたMySQL 5.5のままでいたが、せっかくクリーンインストールするならということでMySQL 5.7を入れることに。

別ホストではMySQL 5.6から5.7に更新したことがあるが、こちらはMySQL本体更新後にmy.cnfをちょろっと修正してmysql_upgradeを実行しただけでわりとサクッといった。(MySQLが起動しなくなったのでログ見ながらmy.cnfを修正したというのはあったけど)

今回はMySQLのクリーンインストールなので更新より簡単かなと思ったが勝手が違って意外と苦労したので備忘録。

インストール

FreeBSDのportsでインストールした。
# cd /usr/ports/databases/mysql57-server
# make install clean
構築時のオプションとしては普通?は初期値かINNOBASE(コンパイル静的)を追加で[x]付ける程度かしら。
これでmysql57-serverとmysql57-clientがインストールされる。

FreeBSDのportsではMySQL5.6以降? my.cnfの置き場所が /usr/local/etc/mysql 下に変更になっているようなのでrc.confの指定を変更する。

/etc/rc.conf
1
2
3
mysql_enable="YES"
mysql_optfile="/usr/local/etc/mysql/my.cnf"
#mysql_args=" --skip-grant-tables"    #MySQL5.7の最初の起動時だけ有効にする(後述)

/usr/local/etc/my.cnf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[client]
#中略
default-character-set           = utf8mb4

[mysqld]
#中略
character-set-server            = utf8mb4
#collation-server               = utf8mb4_bin
collation-server                = utf8mb4_general_ci
log-error                       = /var/log/mysqld-error.log
#中略

[mysqldump]
#中略
default-character-set           = utf8mb4

最初から用意されているmy.cnfの雛形にデフォルトの文字コードの指定を追加しておく。(エラーログも)
他はそのままで良いかと思う。(と、書いておきながら、いろいろ手を入れたけど)

MySQLサーバ起動

# service mysql-server start
または
# /usr/local/etc/rc.d/mysql-server start

ここは全く変わらず。

MySQLのCLIログイン

MySQL 5.7以前はMySQLのCLIの初回ログインは何も考えずにできた。
MySQL 5.7ではおせっかいなことに初期状態の管理者アカウントrootに勝手にユニークなパスワードが付けられるらしい。つまり(普通には)パスワードを何とかして知らないとログインできないということになる。
MySQLサーバを起動すると自動的root用のパスワードが付けられて、それがログ(テキストログ?)に記録されているのでそれを見ると判るということであるが、設定したmy.cnfの都合で存在しないので不明。それでは何もできないので上のmy.cnfの例のように--skip-grant-tablesをオプション指定するか--skip-grant-tablesを付けて手動でmysqldを実行する。これで以前のmysqlのCLI同様rootをパスワード無しで使える。

% mysql mysql
root@localhost [mysql]> ALTER USER 'root'@'localhost' IDENTIFIED BY 'MySecretPassword';
Query OK, 0 rows affected (0.00 sec)

root@localhost [mysql]> CREATE DATABASE new_db;
Query OK, 0 rows affected (0.00 sec)

root@localhost [mysql]> CREATE USER 'newuser'@'localhost' IDENTIFIED WITH mysql_native_password BY 'NewUserPassword';
Query OK, 0 rows affected (0.01 sec)

root@localhost [mysql]> GRANT ALL PRIVILEGES ON new_db.* TO 'newuser'@'localhost';
Query OK, 0 rows affected (0.01 sec)

root@localhost [mysql]> DROP DATABASE test;
Query OK, 0 rows affected (0.00 sec)

root@localhost [mysql]> exit
Bye
%

mysqlコマンドでMySQLのmysqlデータベースに接続する。
rootパスワードの設定にはMySQL 5.7.6以降はALTER USERを使うらしい。
新しいデータベース new_dbを作成。これは以前から変わらない。
新しいユーザー newuserを作成。
先に作成したnew_dbの全権をnewuserに与える。
最初から存在するtestデータベースを削除する。
MySQLのCLIを終了。

rootユーザーのパスワードを設定したら先に設定した /etc/rc.confの --skip-grant-tables行を削除してMySQLサーバーを再起動する。

ユーザーの作成は以前の様にGRANTでデータベースへの権限付けと同時に行うことはできなくなったらしい。CREATE USERでユーザーを作成するのとGRANT hogeで権限付けを分けて行う。ここは以前が変だったので常識的になったといえるかも。

または/usr/local/bin/mysql_secure_installationを実行してメッセージに従って入力することでrootのパスワードを設定できる他幾つか機能があるらしいが使いたくない。でも、今後はこれが標準手順になるのかしら?

これでMySQL 5.7で最初に躓くところは終わり?

関連記事:

Up