ラベル php の投稿を表示しています。 すべての投稿を表示
ラベル php の投稿を表示しています。 すべての投稿を表示

2011年7月19日火曜日

文字列の1文字目だけ大文字にする関数

最近、Perlにucfirstという関数があると知ったので、記録しておく。

Perlの場合
ucfirst
$ perl -le 'print ucfirst("hello")'
Hello
PHPの場合
Perlと同じく、ucfirst
$ php -r 'echo ucfirst("hello");'
Hello
Emacs Lispの場合
upcase-initials
※Perl, PHPとは仕様が少し異なる。1文字目が空白だと読み飛ばし、最初のアルファベットを大文字にする。
(upcase-initials "hello")
"Hello"
(upcase-initials " hello")
" Hello"
Common Lispの場合
string-upcase
(string-upcase "hello" :start 0 :end 1)
"Hello"

2011年2月6日日曜日

CGIの環境変数 SERVER_NAME と HTTP_HOST

ほかの誰かが作ったCGIを移行する仕事で、バグの原因になったので記録しておく。

症状

CGIで作成された、お問い合わせフォーム。このCGIが出力するHTMLは、head要素内のscript要素でJavaScriptファイルを参照し、ユーザーインターフェースを制御している。

www有りと無しの二通りのURLでアクセスが可能(http://www.example.com/form/ と http://example.com/form/)。しかし、www有りでアクセスした場合は、JavaScriptが正しく動作しない。

原因と対応

JavaScriptファイルのうち、動的に生成されるファイルにバグがあった。動的に生成される部分というのは XMLHTTPオブジェクトがアクセスするURLであり、環境変数SERVER_NAMEを使って生成されていた。次のように。

$url = "\/\/" . $ENV{'SERVER_NAME'} . $service_path . '?';
 $script =~ s/<url>/$url/g;  # $scriptの中身はJSのテンプレ。それを置換している。
 print "Pragma: no-cache\n";
 print "Cache-Control: no-cache\n";
 print "Content-type: text/plain; charset=UTF-8\n\n";
 print $script;

SERVER_NAMEの値はWebサーバーの設定に依存してしまうため、JavaScriptファイルを取得するときのリクエストが www.example.com なのに対して、XMLHTTPオブジェクトは example.com を参照してしまうという矛盾が生じた。これはクロスドメイン制限(cross-domain restrictions)に抵触、あるいは同一生成元ポリシー(same-origin policy)に反してしまう。よって、次のように修正すればOK。

$url = "\/\/" . $ENV{'HTTP_HOST'} . $service_path . '?';  # HTTP_HOST に変更
 $script =~ s/<url>/$url/g;
  (略)

SERVER_NAME と HTTP_HOST

CGIの環境変数のうち、SERVER_NAME と HTTP_HOST はよく似ていているのだが、意味が異なる。

これらの意味は、CGIに環境変数を渡しているApacheのドキュメントに書いてあるはずなのだが、いまいちみつからない。とりあえず、mod_rewriteの中に説明があったのでそこを引用しておく。

mod_rewrite - Apache HTTP Server
  • HTTP headers:
    • (略)
    • HTTP_HOST
    • (略)
  • server internals:
    • (略)
    • SERVER_NAME
    • (略)

These variables all correspond to the similarly named HTTP MIME-headers, C variables of the Apache server or struct tm fields of the Unix system. Most are documented elsewhere in the Manual or in the CGI specification.

SERVER_NAME and SERVER_PORT depend on the values of UseCanonicalName and UseCanonicalPhysicalPort respectively.

要するに、HTTP_HOSTのほうは"HTTP headers"、つまりブラウザからのリクエストヘッダに含まれるHOSTを表し、SERVER_NAMEのほうは"server interanals"、つまりApacheサーバー内の設定ファイル等に記述されているSERVER_NAMEを表すということ。したがって、HTTP_HOSTを扱うときはセキュリティ的に注意を要する、なんて話題も避けられない(変な文字列が送られてくる可能性があるから、内容をチェックしたりエスケープしたりする)。

このあたりの話は、Apacheから環境を引き継ぐプロセス全般に通用する話だから、PerlだけでなくPHPやRuby, Javaサーブレットなどを作るときにも頭に入っているとよいかもしれない。あと、mod_rewrite の条件を書く時にも。

2010年9月1日水曜日

CakePHP paginateのカスタマイズ

URLをカスタマイズ

とりあえず、"Paginator"が生成するURLを文字列置換する方式。これを共通部品にするため、app/views/elements/ の中に"element"を作成する。

app/views/elements/pagination.ctp

<?php
$prev_link = str_replace('page:', '',
                         $paginator->prev('前のページ', null, null, array('class' => 'disabled')) );
$prev_link = preg_replace('/\/1"/', '"', $prev_link);

$numbers_link = str_replace('page:', '', $paginator->numbers());
$numbers_link = preg_replace('/\/1"/', '"', $numbers_link);

$next_link = str_replace('page:', '',
                         $paginator->next('次のページ', null, null, array('class' => 'disabled')));

echo $prev_link;
echo $numbers_link;
echo $next_link;
?>

このelementを呼び出すためのコード(Viewに記述)。

  <?php echo $this->element('pagination'); ?>

Routesの変更

paginateのURLに対応するため、Routing情報を追加。

app/config/routes.php

/* pagination */
  Router::connect('/:controller/:action/:page',
                  array('action' => 'index', 'page' => null),
                  array('action' => 'index', 'page' => '[0-9]+'));

ここでconnect()の引数は、

  • URLは controller名/action名/page名 の形式になっている
  • controller名に対応するControllerが呼び出される(デフォルトのRouting)
  • action名に対応するActionではなく、強制的にindexを呼び出す(デフォルトのRoutingを変更)
  • page名はnull可(オプショナルな「カスタムRoute要素」)
  • action名は"index"という文字列にマッチしないといけない
  • page名は半角数字列にマッチしないといけない

というような意味。

2010年8月31日火曜日

PHPアップロードファイルサイズ

XOOPSの仕事。

アップロードできるファイルサイズの上限値を変えたいという話。

XOOPSの管理画面で変更するほか、PHPのオプションも変えないといけません。

.htaccess

php_value upload_max_filesize 10M
php_value post_max_size 10M

マニュアルでオプションの説明に"PHP_INI_ALL"、"PHP_INI_USER"、"PHP_INI_PERDIR"のいずれかが書いてあれば .htaccessで変更可能。"PHP_INI_SYSTEM"だと不可能(php.ini, httpd.confレベル)。

前提として、Apacheのhttpd.confで "AllowOverride Options" が有効になっていること。

2010年7月29日木曜日

"はなまるメールフォーム"の拡張

メールフォームが必要な場合に使っているライブラリ。PHP4で動く(5でも動くかは知らない)

チェックボックスのような複数選択項目を処理する場合は、下記のようなコードを追加すればOK。


hnmailform.php:

function initData($posts){//入力データを初期化する
...
  $keys = array_keys($posts);
  foreach ($keys as $k){
    if(is_array($posts[$k])){
      $posts[$k] = implode(", ", $posts[$k]);
    }
  }
...

html:

htmlのコード:

<input type="checkbox" id="item1" name="チェック項目[]" value="あ" checked="checked" /><label for="inqtype1">あ</label>
<input type="checkbox" id="item2" name="チェック項目[]" value="い" checked="checked" /><label for="inqtype2">い</label>
<input type="checkbox" id="item3" name="チェック項目[]" value="う" /><label for="inqtype3">う</label>

SSLとIEの組み合わせでファイルダウンロードができない問題

よくある問題。

XOOPSの掲示板モジュールでも発生したから、キャッシュを有効にするコードをいれた。

xoops_trust_path/modules/attachfile/include/attach_functions.php:
        header( "Cache-Control: public" );
        header( "Pragma: cache" );

2010年7月20日火曜日

CakePHPでCSVをデータソースにする

参考


なお、例示されてるコードにはPHP4で動かない箇所がある。
  • "private"修飾子
  • foreach文での参照記法("&$l")

DBに接続できないというWarningが出たので"app/config/datagbase.php"を一部コメントアウトした。今回のシステムではDBを使わないので問題ないはず。
/*
 var $default = array(
  'driver' =>'mysql',
  'persistent' => false,
  'host' => 'localhost',
  'login' => 'user',
  'password' => 'password',
  'database' => 'database_name',
  'prefix' => '',
 );

 var $test = array(
  'driver' => 'mysql',
  'persistent' => false,
  'host' => 'localhost',
  'login' => 'user',
  'password' => 'password',
  'database' => 'test_database_name',
  'prefix' => '',
 );
    */

2010年6月24日木曜日

PHP5のグローバル変数

グローバル変数は、globalキーワードを付けて宣言することになったらしい。
※以前のようにブロック内でglobal宣言しただけでは、参照できない。
茶々ラボ - Chacha Lab.: PHPでグローバル変数

しかも、global宣言と同時に初期化できないのが面倒。
※2つ文を書く必要がある。

global $category_tbl;
$category_tbl = array(
    '広告[  ]+総合広告' => array(1),
);
...
function(){
    global $category_tbl;
    print_r($category_tbl);
}

PHPでTSV処理

エクセルで出力されたTSVを想定。

CSVのほうが知名度はあるが、TSVが好ましい(区切りには制御文字のほうが適している、それこそ制御文字の本分でしょう、という気がする)。

エクセルでデータを作る人、つまり非プログラマー、非SEの人は普通TSVのことを知らないが、そこは事前に説明してTSVで出力するようお願いしておく。

TSVに関する大まかな仕様

  • 改行を含む列は"で始まり"で終わる
  • タブ、二重引用符を含んでもいい
  • SJIS(PHPプログラム内ではUTF8に変換して処理する)

TSVファイルを読み込んで連想配列の配列に変換(PHP)

/*
 * TSVファイルを読み込んで連想配列の配列に変換
 * 引数:処理結果(参照渡し)、TSVファイルのパス
 */
function parseTSV(&$records, $path){
    $fp = fopen($path, 'r');
    if($fp===FALSE){
        return FALSE;
    }

    $records = array();
    $errors = array();
    //$cnt = 0; // これはデバッグ専用のリミット(後で削除)
    //    while(!feof($fp) && $cnt < 10){
    while(!feof($fp)){

        $line = fgets($fp);
        $line = mb_convert_encoding($line, 'UTF-8', 'SJIS-win');
        $line = str_replace(array("\r\n","\r"), "\n", $line);
        $len = mb_strlen($line, 'UTF-8');

        $state = 'START';
        $flg = 0;
        $buf = '';
        $fields = array();
        //        $cnt++;

        for($i=0; $i<$len; $i++){
            $char = mb_substr($line, $i, 1, 'UTF-8');
            if($state=='START'){
                if($char === '"' && $flg == 0) {
                    $flg = 1;
                    $state = 'MID';
                } elseif($char === "\n" && $flg == 0) {
                    break;
                } elseif($char === "\t") {
                    $state = 'END';
                } else {
                    $buf .= $char;
                    $state = 'MID';
                }
            } elseif($state == 'MID') {
                if($char === "\t") {
                    if($flg){
                        $buflen = mb_strlen($buf, 'UTF-8');
                        if($buflen && mb_substr($buf, $buflen - 1, 1, 'UTF-8') == '"') {
                            $state = 'END';
                        } else {
                            $char = ' ';
                        }
                    } else {
                        $state = 'END';
                    }
                } elseif($char === "\n"){
                    if($flg){
                        $buf .= $char;
                        // データが次の行に続いている場合
                        $line = fgets($fp);
                        $line = mb_convert_encoding($line, 'UTF-8', 'SJIS-win');
                        $line = str_replace(array("\r\n","\r"), "\n", $line);
                        $len = mb_strlen($line, 'UTF-8');
                        $i = -1; // ここは強引
                    } else {
                        $state = 'END';
                    }
                } else {
                    $buf .= $char;
                }
            }
            if($state == 'END') {
                if($flg){
                    $buf = mb_substr($buf, 0, mb_strlen($buf, 'UTF-8') - 1, 'UTF-8');
                }
                $buf = mb_ereg_replace('""', '"', $buf); // 連続したダブルクォートは1つに

                // 連想配列に追加して、初期化
                $fields[] = $buf;
                $state = 'START';
                $flg = 0;
                $buf = '';
            }
        }
        // 1レコード読み込み終了
        if(count($fields) > 0) {
            $records[] = $fields;
        }
    }
    fclose($fp);

    return TRUE;
}

2010年6月7日月曜日

PHPのレアな関数

* error_get_last()
(PHP 5 >= 5.2.0)
error_get_last — 最後に発生したエラーを取得する。

* simplexml_load_file()
(PHP 5)
simplexml_load_file — XMLファイルをパースし、オブジェクトに代入する

* PHPバージョン5以上だとini_set()、.htaccessでallow_url_fopenを有効にできない
管理者にphp.iniを変更してもらわなければ file_get_contents() や simplexml_load_file() が使えない。
↓こんな感じのエラーが発生(failed to open steream

Array ( [type] => 2 [message] => file_get_contents(http://twitter.com/statuses/user_timeline/138673716.rss) [function.file-get-contents]: failed to open stream: no suitable wrapper could be found [file] => /virtual/www/2012/lib/twjson.php [line] => 12 )

2010年6月3日木曜日

htmlタグを除いてマルチバイト150文字にtruncateする関数

htmlタグを除き、さらにマルチバイト文字150文字を取り出す。


/********************************************************************************************
myTruncate() : 
- 用途 : truncate string considering HTML tag.
- 引数 : html string, maxlength, url
- 戻値 : truncated string
********************************************************************************************/
function myTruncate($html, $maxLength, $url) {
    $printedLength = 0;
    $position = 0;
    $tags = array();
    $printstr = '';

    mb_internal_encoding("UTF-8");

    // while ($printedLength < $maxLength && preg_match('{</?([a-z]+)[^>]*>|&#?[a-zA-Z0-9]+;}', $html, $match, PREG_OFFSET_CAPTURE, $position))
    // ここの preg_match を等価なマルチバイト処理に変更すればうまくいく(たぶん)
    while ($printedLength < $maxLength && $this->mb_preg_match('{</?([^>]+)>|&#?[a-zA-Z0-9]+;}', $html, $match, PREG_OFFSET_CAPTURE, $position))
        {
            list($tag, $tagPosition) = $match[0];

            // Print text leading up to the tag.
            $str = mb_substr($html, $position, $tagPosition - $position);
            if ($printedLength + mb_strlen($str) > $maxLength)
                {
                    //print(mb_substr($str, 0, $maxLength - $printedLength));
                    $printstr .= mb_substr($str, 0, $maxLength - $printedLength);
                    $printedLength = $maxLength;
                    break;
                }

            //print($str);
            $printstr .= $str;
            $printedLength += mb_strlen($str);

            if ($tag[0] == '&')
                {
                    // Handle the entity.
                    //print($tag);
                    $printstr .= $tag;
                    $printedLength++;
                }
            else
                {
                    // Handle the tag.
                    $tagName = $match[1][0];
                    $tagName = mb_ereg_replace(' .*', '', $tagName);
                    if ($tag[1] == '/')
                        {
                            // This is a closing tag.

                            $openingTag = array_pop($tags);
                            if($openingTag != $tagName) die;
                            assert($openingTag == $tagName); // check that tags are properly nested.

                            //print($tag);
                            $printstr .= $tag;
                        }
                    else if ($tag[mb_strlen($tag) - 2] == '/')
                        {
                            // Self-closing tag.
                            //print($tag);
                            $printstr .= $tag;
                        }
                    else
                        {
                            // Opening tag.
                            //print($tag);
                            $printstr .= $tag;
                            $tags[] = $tagName;
                        }
                }

            // Continue after the tag.
            $position = $tagPosition + mb_strlen($tag);
        }

    // Print any remaining text.
    if ($printedLength < $maxLength && $position < mb_strlen($html))
        //print(mb_substr($html, $position, $maxLength - $printedLength));
        $printstr .= mb_substr($html, $position, $maxLength - $printedLength);

    // Close any open tags.
    while (!empty($tags)) //printf('</%s>', array_pop($tags));
        $printstr .= sprintf('</%s>', array_pop($tags));

    if(mb_strlen($html) > mb_strlen($printstr)){
        $readmore = '<p class="nav"><a href="' . $url . '" title="続きを読む">続きを読む</a></p>';
    } else {
        $readmore = '';
    }

    return '<p>' . $printstr . '</p>' . $readmore;
}

2010年6月2日水曜日

Smarty反復構文での制御変数参照方法

for文のような、制御変数を明示的に利用する反復構文がない。

1. sectionを使う

{section name=cnt start=0 loop=10}
{$smarty.section.cnt.index}
{/section}

2. foreachのプロパティを使う

{foreach from=emps item=emp name=emploop}
{$smarty.foreach.emploop.index}
{/section}

どちらの場合も、breakとcontinueに相当するものはない。

2010年5月31日月曜日

Smartyのassignと反復に関して

assign

valueに2つ変数を使用する例。ダブルクォートの内側にバッククォートを入れる。

{assign var=myvar value="`$var1`の中央部にある`$var2`で`$var3`を見つけました"}

参考:http://xfield.info/modules/tautech/content0107.html

反復のためのforはない。代わりは"section"

{section name=cnt start=0 loop=10}
{$smarty.section.cnt.index}
{/section}

※startを設定することで任意の値から開始できる(デフォルト値は0)。