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で外部に繋がる程度で。

関連記事: