2014年9月30日火曜日

備忘録 : Linuxのコマンド(Apacheの再起動)、phpからLinuxのコマンドを実行する方法

PHPの案件をやっていると、どうしても、サーバーの設定に悩まされ続けることになる。

覚悟を決めてサーバーに接続してややこしい設定に挑むことがある。
サーバーへの接続には、接続のためのソフトを使う。私の会社PCでは、PuTTYというソフトを使っているみたい。Telnetとかターミナルとか呼ばれるやつと同系統なのかな?
開発の会社で、サーバーに詳しい人が建てた会社のサーバーなら、上記のソフトで接続して設定をいじれるけれど、レンタルサーバーなんかでは多分出来ない。

メモしておきたいのは以下の一行(サーバーのApacheを再起動する命令)

apachectl restart

これもついでにメモっておこうかな。
ターミナルでテキストファイルを編集する時、「vi」というテキスト編集ソフトでファイルを開くんだけど、そのコマンド。

コマンドモードと挿入モードとの切り替え
  コマンドモード→挿入モード: a i o のどれか
  挿入モード→コマンドモード: Escキー
ファイルの保存、終了: :wq
保存せずに強制終了: :q!


また、Linuxのコマンドは、PHPスクリプトから実行できる。

exec("Linuxコマンドの文字列");

ただし、実行されるのがPHPの命令ではなくLinuxの命令なので、いくつか注意点がある。
■戻り値がきれいに取れない
■Linuxの命令の実行は別スレッドでの非同期処理になるらしい

つまり、 Linuxで巨大なファイルを複製するとして、そのコマンドをPHPから実行した直後に、PHPの処理でその複製されたファイルにアクセスしようとしても、そのファイルはまだ存在してない可能性があるということみたいだ(PHPはLinuxコマンドの実行完了を待ってから次の処理に行くわけではないらしい)。

2014年9月26日金曜日

【TortoiseSVN】DAV request failed: 411 Content length required. のエラー

現在やっている案件、ファイルのバージョン管理システムにサブバージョンを使っています。
 
サブヴァージョンを使ってのコミットが簡単そうなんですがうまくいきませんでした。
「TortoiseSVN」というクライアントを使っていました。

苦しんだエラーメッセージ:
Error: Commit failed (details follow):  
Error: DAV request failed: 411 Content length required. The server or an intermediate  
Error:  proxy does not accept chunked encoding. Try setting 'http-chunked-requests' to  
Error:  'auto' or 'no' in your client configuration. 
 
 
解決した時にやったこと:
ウィンドウズのエクスプローラーで、サブバージョンと紐付いたフォルダにおいて右クリック
→TortoiseSVN
→Settings
→Network
→Subversion server file : Edit
→「servers」というテキストファイルが開く
→「
http-chunked-requests = no
」という行を追加して保存。

サーバー関係の設定ファイルをいじるようなノリのことをやったらうまく行ったという話でした。

【PHP】wgetを使ってログイン後のページのHTMLを文字列として取得したい

PHPで次のようなことがやりたい。

■ブログの記事のように動的に生成されているページを静的HTMLファイルとして保存する。

■ダウンロードするのではなくサーバー上にHTMLファイルを複製する。

■該当のページは、システムにログインしている状態でアクセスした場合とログインしていない状態でアクセスした場合とで表示内容が異なるが、ログインした状態での表示内容を取得したい。

■サーバーの環境的にcURLの機能が使えない。

■複製するHTMLファイルは、リンクや画像へのパスなど、内容を一部書き換える。

---------------------------------------------

実装方針は以下のようになる。

■対象のURLに、ブラウザでアクセスするようなノリでプログラムからアクセスし、レンダリングされたHTMLを取得する。
 つまり、CMS(コンテンツマネジメントシステム=ワードプレスなどようなコンテンツ作成、管理システム / 今回はconcrete5)の動作を解析して、「このブロックはこのデータベーステーブルのこのコラムから値をとって」みたいな内部処理を再現するのではなく、システムの外側からアクセスした時の表示内容を取得する。

■「システムにログインする」というのをPHPプログラムで再現しないといけない。
 URLを読み込むだけならPHPの「file()」や「file_get_contents()」を使えばいいのだが、それらだとログインの挙動を再現できない。
 検索するとよく出てくるのはcURLを使っての実装だが(例: http://web-prog.com/php/curl-login-scraiping/ )、今回の環境ではこれは使えないということになった。
 そこで代わりに使うのは、Linuxのコマンドである「wget」をPHPの「exec()」メソッドで実行させるという方法である。

■wgetコマンドは対象のURLの内容を変数に取得するものではなくファイルに出力するものなので、出力されたファイルをさらに読み込みにいって変数に格納する。wgetで出力されたファイルは読み込んだ後は削除する。


---------------------------------------------

URLの内容を取得するのが難しかったのですが、それなりに上手くいったので、きちんと動いたソースコードの一部を転載用に書き換えて以下に載せます。
今回(仕事で取り組まねばならなかったシステム)は幸運にも、ログインフォームには動的に生成されるアクセストークンなどのめんどくさい値はなかったので、そこはすっきりいきました。

---------------------------------------------
サンプルプログラムソース
ファイルの保存文字コードはUTF8
---------------------------------------------
<?php

class ModelGetterContentFromUrl{
  //////////////////////////////////////////////
  /**
   * wgetを使い、システムへのログイン後のURLの内容を取得する
   *
   * @param  String 対象URL
   * @return String 取得したコンテンツ
   */
  public static function getContentFromUrlUsingWget($url){
    //引数チェック
    if(
      (isset($url) == false)or
      ($url == "")
    ){
      return;
    }//if

    /////////////////////////////////////
    //定数定義(サンプルプログラムなのでここに書いてますが)
    //本来ならコンフィグ用のファイルなどで行ってください

    //一時的な作業フォルダ、ファイル
    define("OUTPUT_DIR_WORK_TEMP" , $_SERVER["DOCUMENT_ROOT"]."/work_temp"); //作業用フォルダ。あらかじめ作成しておく必要がある。
    define("TEMP_FILE_COOKIE"     , OUTPUT_DIR_WORK_TEMP."/cookie.txt");     //クッキー保存用
    define("TEMP_FILE_HTML"       , OUTPUT_DIR_WORK_TEMP."/temp.html");      //wgetで作成されるHTMLファイル

    //ログインページURL
    define("URL_LOGIN_PAGE", "ログインページのURL");
    //ログイン処理を実行するスクリプトのURL(ログインページのフォームのアクションで指定されたURL)
    define("URL_LOGIN_PROCESS", "ログイン処理を実行するスクリプトのURL");

    //ログインID
    define("LOGIN_FORM_NAME_ID", "name"); //ログインフォームのID入力部分のname属性
    define("LOGIN_ID", "admin");          //ログインID
    //ログインパスワード
    define("LOGIN_FORM_NAME_PW", "password"); //ログインフォームのパスワード入力部分のname属性
    define("LOGIN_PW", "password");           //ログインパスワード


    /////////////////////////////////////
    //PHPプログラムをログインさせ、適切なクッキーファイルを作成し、
    //目当てのURLアクセス時にそのクッキーファイルを利用させる

    /////////////////////////////////////
    //処理の三段階の説明
    //1.ログインページにアクセスし、クッキーを取得・保存
    //2.form actionに値をPOSTし、ログイン完了ページへ
    //3.取得したいページへアクセスし、ソース取得

    //クッキー保存ファイルを作成
    touch(TEMP_FILE_COOKIE);

    /////////////////////////////////////
    //第1段階 クッキーを取得
    //該当URL : URL_LOGIN_PAGE
    $linux_command = ""
      ." wget "
      ." --save-cookies='".TEMP_FILE_COOKIE."' "  //クッキー保存指定
      ." --keep-session-cookies "                 //クッキーの内容を漏らさず保存しろという指定
      ." -p DirPath='".OUTPUT_DIR_WORK_TEMP."/' " //書き出しディレクトリ指定
      ." --delete-after=on "                      //ダウンロード後にファイル削除
      ." --no-cache "
      ." '".URL_LOGIN_PAGE."'"
    ;
    exec($linux_command);

    //※このログインページには、システムが動的に生成するアクセストークンなどの値が
    //  hiddenフィールドに存在しないものと想定している。
    //  もしシステムが動的に生成するアクセストークンなどのhiddenフィールドが
    //  ログインページにある場合、「--delete-after=on」を外し、書きだされたHTMLファイル
    //  の内容を解析し、アクセストークンのnameとvalueとを取得する必要がある。


    /////////////////////////////////////
    //第2段階 ログイン完了ページヘ
    //該当URL : URL_LOGIN_PROCESS
    $linux_command = ""
      ." wget "
      ." --load-cookies='".TEMP_FILE_COOKIE."' "  //クッキー読み込み指定
      ." --save-cookies='".TEMP_FILE_COOKIE."' "  //クッキー保存指定
      ." --keep-session-cookies "                 //クッキーの内容を漏らさず保存しろという指定
      ." -p DirPath='".OUTPUT_DIR_WORK_TEMP."/' " //書き出しディレクトリ指定
      ." --delete-after=on "                      //ダウンロード後にファイル削除
      ." --no-cache "
      ." --post-data '".LOGIN_FORM_NAME_ID."=".LOGIN_ID."&".LOGIN_FORM_NAME_PW."=".LOGIN_PW."' "
      ." '".URL_LOGIN_PROCESS."'"
    ;
    exec($linux_command);

    //※もしシステムが動的に生成するアクセストークンなどのhiddenフィールドが
    //  ログインページにある場合、「--post-data 'key=value&key=value'」の行に、
    //  「&アクセストークンのhiddenフィールドのname=アクセストークンの値」を追加


    /////////////////////////////////////
    //第3段階 取得したいページヘアクセスし、コンテンツを取得
    $linux_command = ""
      ." wget "
      ." --load-cookies='".TEMP_FILE_COOKIE."' " //クッキー読み込み指定
      ." --no-cache "                            //キャッシュさせずに常に最新のデータを取得する
      ." -O '".TEMP_FILE_HTML."' "               //書き出しディレクトリとファイル名との指定
      ." '".$url."' "                            //対象URL
    ;
    exec($linux_command);

    //出力したファイルからHTMLの内容を読み取る
    $html_string = file_get_contents(TEMP_FILE_HTML);

    //UTF8に文字コード変換
    mb_language("Japanese");
    $html_string = mb_convert_encoding($html_string, "UTF-8", "auto");

    //一時ファイルの削除
    unlink(TEMP_FILE_COOKIE);
    unlink(TEMP_FILE_HTML);

    //取得したHTMLを文字列に格納したものを戻す
    return $html_string;

  }//function
}//class

---------------------------------------------
/サンプルプログラムソース
---------------------------------------------

2014年9月16日火曜日

【Android】PDFをアプリ内で表示させたい PDFビューアーライブラリ

仕事で作成しているAndroidアプリにおいて、PDFをアプリ内で表示したいという要求があり、いい方法ないかなと調べていました。
Androidは標準ではPDF表示の機能がないし、いいライブラリもあんまり無いようなのですね。

調べた結果、次のブログで紹介されていた「android-pdfview」というのが、現状では組み込んだ自分のアプリ内では動作しています。
http://www.takemisousaku.com/?p=846#more-846

配布元(海外の個人のサイトみたい)
http://joanzapata.com/android-pdfview/

【導入】
例によって、エクリプスから、「ファイル>新規>その他>既存コードからのAndroidプロジェクト」で、ダウンロードして解凍した 「android-pdfview」を指定して、ワークスペースに読み込ませます。
パッケージエクスプローラでそのライブラリを使いたい作成中のアプリのパッケージ名で右クリック>プロパティ>Android>ライブラリ>追加で、「android-pdfview」を追加。

ところが、2014/09/16現在でこの作業をすると、「android-pdfview」がエラーを吐きまくっていて動きませんでした。
エラー内容を調べてみると どうも、解凍したファイルたちのフォルダ構成がパッケージ名と一致していないということが原因らしい。

すんなり解凍するとフォルダ構成は以下のようになってる:
android-pdfview\src\main\java\com\joanzapata\pdfview
android-pdfview\src\main\java\org\vudroid

ところが、各ファイルに書いてあるパッケージ名には、「\main\java」の記載はない。

そこで、「com」「org」のふたつのディレクトリを、「android-pdfview\src」にカット&ペーストした。
したら動くようになりました。

つまりこうです:
android-pdfview\src\com\joanzapata\pdfview
android-pdfview\src\org\vudroid


「android-pdfview」自体は、その他の特別なライブラリなんかは必要としていなかったように見えました。
 

2014年9月11日木曜日

【Android】HTTPSのWebAPIにAndroidで接続するとエラーになる

Androidで、「https://」ではじまるURLにHTTPリクエストを送って、データを取得したい。

HttpsURLConnectionのクラスを使うらしい。
HttpURLConnectionを使い、「http://」から始まるURLに接続すると、特に困難なくデータが取得できたのに、「https://」の接続先だとうまくいかない。
接続先URLにブラウザで直接アクセスすると、証明書がないからセキュリティ的に怪しいんだけどそれでも接続しちゃっていいの?という画面が表示されて、例外を認める操作をすると接続できるようになる。

Androidが吐くエラーは以下のとおりだった。
09-11 17:16:37.886: W/System.err(3801): javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

どうも、サーバーに信用できる証明書がインストールされてないからこの端末からの接続はしたくないんだ!じゃあね!ということらしい。

開発中のアプリの場合、サーバーも開発用のサーバーで証明書なんて無いことが普通なので、これでは困る。

対処法。

サーバーに信用できる証明書がインストールされて無くても無理やりつないでしまえ、という処理を実装するらしい。

参考ページへのリンクを貼っておきます。
http://symfoware.blog68.fc2.com/blog-entry-1165.html

http://blog.kinjouj.net/self-signed-cerificate-https-server-connection-from-java.html

検索の際のキーワード: X509TrustManager オレオレ Android

リンク先が消えた時のため、キモのところだけ、自分のアプリで動いたものをコピペしておきます

---
(AsyncTask継承クラス内の、doInBackground()内)


//////////////////////////////////////////////
//オレオレ証明書によるSSLサーバー接続でもエラーをスルーできるようにする
SSLContext sslcontext = null;

try {
    //証明書情報 全て空を返す
    //証明書情報 全て空を返す
    TrustManager[] tm = {
        new X509TrustManager() {
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }//function
            @Override
            public void checkClientTrusted(X509Certificate[] chain,
                    String authType) throws CertificateException {
            }//function
            @Override
            public void checkServerTrusted(X509Certificate[] chain,
                    String authType) throws CertificateException {
            }//function
        }//class
    };
    sslcontext = SSLContext.getInstance("SSL");
    sslcontext.init(null, tm, null);
    //ホスト名の検証ルール 何が来てもtrueを返す
    HttpsURLConnection.setDefaultHostnameVerifier(
        new HostnameVerifier(){
            @Override
            public boolean verify(String hostname,
                    SSLSession session) {
                return true;
            }//function
        }//class
    );
} catch (Exception e) {
    e.printStackTrace();
}//try

////////////////////////////////////////////////////////
//APIに接続
//接続URLを作成
String url_string = "ここにhttps://のURL";

////////////////////////////////////////////////////////
URL url = null;
try {
    url = new URL(url_string);
} catch (MalformedURLException e1) {
}

////////////////////////////////////////////////////////
//HTTPコネクションの作成
HttpsURLConnection connection = null;
try {
    connection = (HttpsURLConnection)url.openConnection();
} catch (IOException e) {
}//try

//オレオレ証明書によるSSLサーバー接続でもエラーをスルーできるようにする
connection.setSSLSocketFactory(sslcontext.getSocketFactory());

////////////////////////////////////////////////////////
//リクエストするパラメータの設定
try {
    connection.setRequestMethod("GET");
} catch (ProtocolException e) {
}//try

////////////////////////////////////////////////////////
//通信の開始
try {
    connection.connect();
} catch (IOException e) {
}//try

---