Hatena::ブログ(Diary)

ザリガニが見ていた...。 このページをアンテナに追加 RSSフィード

2015-03-30

OSX 10.10 YosemiteでWeb共有を有効にする

OSX 10.8以降、システム環境設定からWeb共有のGUI設定がなくなってしまった。しかし、なくなったのはGUI設定だけで、apache2の設定ファイルを修正すれば、以前同様Web共有が使える。そこで、簡単にWeb共有を開始できるように、以前の日記でAppleScriptにまとめておいた。

ところが、時は流れてOSX 10.10 Yosemiteでは、上記AppleScriptではWeb共有が使えなくなっていた...。

apacheのバージョン

  • 調べてみると、OSX 10.10 Yosemiteから、apacheのバージョンが2.4に変更されていた。(以前はバージョン2.2だった)
    • OSX 10.9 Mavericksまで
$ apachectl -v
Server version: Apache/2.2.26 (Unix)
Server built:   Dec 10 2013 22:09:38
    • OSX 10.10 Yosemiteから
$ apachectl -v
Server version: Apache/2.4.9 (Unix)
Server built:   Sep  9 2014 14:48:20
  • apacheの設定ファイルをバージョン2.4に対応したものに修正することで、YosemiteでもWeb共有を使えるようになるのだ。

Web共有を有効にする手順

Webサーバーを起動
  • Webサーバーを起動するだけで、Web共有は始まる。但し...
    • /Library/WebServer/Documents/をルートとするWeb共有のみ有効。
    • ユーザーごとの~/SitesをルートとするWeb共有はまだ無効。
$ sudo apachectl start

  • http://localhost
    • /Library/WebServer/Documents/index.html.enの内容が表示されている。

f:id:zariganitosh:20150330124336p:image:w450

/etc/apache2/httpd.confを修正
  • ユーザーごとのWeb共有を有効にするため、以下のコメントマークを外して、有効にしておいた。
    • userdir_moduleをロードすることを指定。
    • /etc/apache2/extra/httpd-userdir.confの設定も取り込むことを指定。

  LoadModule authz_host_module libexec/apache2/mod_authz_host.so

  LoadModule authz_core_module libexec/apache2/mod_authz_core.so

 -#LoadModule userdir_module libexec/apache2/mod_userdir.so
 +LoadModule userdir_module libexec/apache2/mod_userdir.so
  
  # User home directories
 -#Include /private/etc/apache2/extra/httpd-userdir.conf
 +Include /private/etc/apache2/extra/httpd-userdir.conf

      • −赤い行 = 修正前
      • +緑の行 = 修正後
      •  黒い行 = デフォルトの設定状態

  • ついでにPHPも有効にするには、以下のコメントマークも外して、有効にしておく。
    • php5_moduleをロードすることを指定。

 -#LoadModule php5_module libexec/apache2/libphp5.so
 +LoadModule php5_module libexec/apache2/libphp5.so

/etc/apache2/extra/httpd-userdir.confを修正
  • ユーザーごとのWeb共有を有効にするため、以下のコメントマークを外して、有効にしておいた。
    • 上記/etc/apache2/httpd.confで取り込みを指定された設定ファイルである。
    • ユーザごとの設定ファイル/etc/apache2/users/*.confの設定も取り込むことを指定。

  # Settings for user home directories
  #
  # Required module: mod_authz_core, mod_authz_host, mod_userdir
 
  #
  # UserDir: The name of the directory that is appended onto a user's home
  # directory if a ~user request is received.  Note that you must also set
  # the default access control for these directories, as in the example below.
  #
  UserDir Sites
 
  #
  # Control access to UserDir directories.  The following is an example
  # for a site where these directories are restricted to read-only.
  #
 -#Include /private/etc/apache2/users/*.conf
 +Include /private/etc/apache2/users/*.conf
  <IfModule bonjour_module>
         RegisterUserSite customized-users
  </IfModule>

      • −赤い行 = 修正前
      • +緑の行 = 修正後
      •  黒い行 = デフォルトの設定状態
~/Sites フォルダを追加
  • ユーザーごとのWeb共有の起点となる ~/Sites を追加した。(以前と同じ)
    • 上記/etc/apache2/extra/httpd-userdir.confの"UserDir Sites"という設定によって、指定されたフォルダである。
$ mkdir -p ~/Sites
~/Sites/index.htmlも追加
  • ユーザーごとのWebページの動作確認用として、「Now Web Sharing!」というテキストファイルも保存しておいた。(以前と同じ)
$ echo Now Web Sharing! > ~/Sites/index.html
/etc/apache2/users/USER_NAME.confを追加
  • ユーザーごとのWeb共有の設定である。
  • 例えば、ログインユーザー名がzariであれば、/etc/apache2/users/zari.confというパスになる。
    • 上記/etc/apache2/extra/httpd-userdir.confの"Include /private/etc/apache2/users/*.conf"という先ほど有効にした設定によって、Includeされる。
$ cat <<EOS > /etc/apache2/users/zari.conf
<Directory \"$HOME/Sites/\">
    Options Indexes MultiViews
    AllowOverride None
    Require all granted
</Directory>
EOS

# apache 2.2
Order allow,deny
Allow from all
# apache 2.4
Require all granted
Webサーバーを再起動
  • 以上の設定を反映させるため、Webサーバーを再起動する。
$ sudo apachectl restart

f:id:zariganitosh:20150330124337p:image:w450

AppleScriptにまとめる

  • 以上の仕組みをAppleScriptにまとめておくのだ。

 activate
 set res to display dialog web_sharing_msg() buttons {"キャンセル", "OFF", "ON"} cancel button 1 default button web_sharing_btn() with title "Web共有コントローラー"
 
 if res's button returned = "ON" then
   apachectl_init()
   apachectl_restart()
 end if
 
 if res's button returned = "OFF" then
   apachectl_stop()
 end if
 
 
 
 
 on apachectl_init()
   activate
   do shell script "mkdir -p ~/Sites"
   --do shell script "chmod +x ~/Sites" --ホーム直下ではデフォルト権限なので不要
   do shell script "[ -f ~/Sites/index.html ] || echo Now Web Sharing! > ~/Sites/index.html"
   
   if apache_version() starts with "2.2" then
     do shell script "f=/etc/apache2/users/`basename $HOME`.conf
 cat <<EOS > $f~
 <Directory \"$HOME/Sites/\">
     Options Indexes MultiViews
     AllowOverride None
     Order allow,deny
     Allow from all
 </Directory>
 EOS
 diff -q $f~ $f || mv $f~ $f
 rm -f $f~
 " with administrator privileges
   end if
   
   if apache_version() starts with "2.4" then
     do shell script "f=/etc/apache2/users/`basename $HOME`.conf
 cat <<EOS > $f~
 <Directory \"$HOME/Sites/\">
     Options Indexes MultiViews
     AllowOverride None
     Require all granted
 </Directory>
 EOS
 diff -q $f~ $f || mv $f~ $f
 rm -f $f~
 " with administrator privileges
   end if
   
   do shell script "f=/etc/apache2/httpd.conf
 cat $f |
 sed -E 's|#(LoadModule[ \\t]+authz_core_module[ \\t]+libexec/apache2/mod_authz_core.so)|\\1|g' |
 sed -E 's|#(LoadModule[ \\t]+authz_host_module[ \\t]+libexec/apache2/mod_authz_host.so)|\\1|g' |
 sed -E 's|#(LoadModule[ \\t]+userdir_module[ \\t]+libexec/apache2/mod_userdir.so)|\\1|g' |
 sed -E 's|#(Include[ \\t]+/private/etc/apache2/extra/httpd-userdir.conf)|\\1|g' |
 sed -E 's|#(LoadModule[ \\t]+php5_module[ \\t]+libexec/apache2/libphp5.so)|\\1|g' | # PHP enabled
 cat > $f~
 diff -q $f~ $f || mv $f~ $f
 rm -f $f~
 " with administrator privileges
   
   do shell script "f=/etc/apache2/extra/httpd-userdir.conf
 cat $f |
 sed -E 's|#(Include /private/etc/apache2/users/\\*.conf)|\\1|g' > $f~
 diff -q $f~ $f || mv $f~ $f
 rm -f $f~
 " with administrator privileges
 end apachectl_init
 
 on apachectl_restart()
   activate
   do shell script "apachectl restart" with administrator privileges
 end apachectl_restart
 
 on apachectl_stop()
   activate
   do shell script "apachectl stop" with administrator privileges
 end apachectl_stop
 
 on web_sharing_state()
   try
     do shell script "curl -s http://localhost/"
     true
   on error
     false
   end try
 end web_sharing_state
 
 on web_sharing_btn()
   if web_sharing_state() then
     3
   else
     2
   end if
 end web_sharing_btn
 
 on web_sharing_msg()
   if web_sharing_state() then
     "Web共有の状態  : ON
 " & access_url_msg() & "
 " & php_enabled_msg()
   else
     "Web共有の状態  : OFF"
   end if
 end web_sharing_msg
 
 on http_code(url_text)
   do shell script "curl -LI " & url_text & " -o /dev/null -w %{http_code}"
 end http_code
 
 on user_level_root_state()
   try
     http_code("http://localhost/~" & (system info)'s short user name) = "200"
   on error
     false
   end try
 end user_level_root_state
 
 on access_url_msg()
   if user_level_root_state() then
     "
   http://" & first_ip_address() & "/
   http://" & first_ip_address() & "/~" & (system info)'s short user name & "/
 "
   else
     "
   http://" & first_ip_address() & "/
 "
   end if
 end access_url_msg
 
 on php_enabled()
   try
     set status_code to do shell script "grep -E '^LoadModule[ \\t]+php5_module[ \\t]+libexec/apache2/libphp5.so$' /etc/apache2/httpd.conf"
     true
   on error
     false
   end try
 end php_enabled
 
 on php_enabled_msg()
   if php_enabled() then
     "  <?php ... ?> 有効"
   else
     "  <?php ... ?> 無効"
   end if
 end php_enabled_msg
 
 on apache_version()
   do shell script "apachectl -v | awk '/version/{print $3}' | sed -E 's/[^0-9.]+//'"
 end apache_version
 
 --優先順位の高いIPアドレスを取得
 on first_ip_address()
   do shell script "ipconfig getifaddr `netstat -rn -f inet | awk '/^default/{print $6;exit}'`"
 end first_ip_address


  • 上記スクリプトを実行すると、以下のように表示され、Web共有のON・OFFと状態の確認ができるのだ。

f:id:zariganitosh:20150330124339p:image:w450

f:id:zariganitosh:20150330124338p:image:w450

参考ページ

以下のページがたいへん参考になりました。感謝です!

2014-08-07

ログイン情報を自動入力するブックマークレットの作り方

Webページの入力情報を読み取る

f:id:zariganitosh:20140723100104p:image:w450


  • 上記ページで以下のAppleScriptを実行すると...

 tell application "Safari"
   activate
   do JavaScript "(function(d, s, r, url_key, inputs, options, i){
 url_key=d.location.protocol.replace(/[^A-Za-z]/g,'_')+'__'+d.location.hostname.replace(/[^A-Za-z]/g,'_')+d.location.pathname.replace(/\\W/g,'_');
 inputs=d.getElementsByTagName('input');
 options=d.getElementsByTagName('option');
 for(i=0;i<inputs.length;i++){
   if(inputs[i].type!='hidden'&&inputs[i].type!='image'&&inputs[i].type!='button'){
     s.push(eval('('+'{type: \"'+inputs[i].type+'\", id: \"'+inputs[i].id+'\", name: \"'+inputs[i].name+'\", value: \"'+inputs[i].value+'\", checked: '+inputs[i].checked+', index: \"'+i+'\"}'+')'));
   }
 }
 for(i=0;i<options.length;i++){
   if(options[i].selected){
     s.push(eval('('+'{type: \"option\", name: \"'+options[i].name+'\", value: \"'+options[i].value+'\", selected: '+options[i].selected+', index: \"'+i+'\"}'+')'));
   }
 }
 r[url_key]=s;
 return r;
 })(document,[],{})" in document 1
 end tell


  • URLをキーとした、以下のレコードが返ってくる。

{
  https___healthcare_mb_softbank_jp_pc_assets_setup_web_explanation_input_php:{
    {checked:false, |id|:"telno3", value:"00012345678", type:"text", |name|:"", |index|:"0"}, 
    {checked:false, |id|:"passwd", value:"1234", type:"password", |name|:"passwd", |index|:"1"}, 
    {|index|:"3", value:"4", type:"option", |name|:"undefined", selected:true}
  }
}

  • URLをキーにして、以下3項目のフォームの情報を配列として保持している。
    • 体組成計の電話番号(テキストフィールド)
    • ユーザー番号(プルダウンリスト)
    • 暗証番号(パスワードフィールド)

自動入力するJavaScript

  • 上記AppleScriptレコードは、以下のようなJSONに変換できる。
{
  https___healthcare_mb_softbank_jp_pc_assets_setup_web_explanation_input_php:[
    {checked:false, id:"telno3", value:"00012345678", type:"text", name:"", index:"0"}, 
    {checked:false, id:"passwd", value:"1234", type:"password", name:"passwd", index:"1"}, 
    {index:"3", value:"4", type:"option", name:"undefined", selected:true}
  ]
}

  • そして、上記JSONを利用して、自動入力するJavaScriptコードを考えてみた。
(function(){


var json={
  https___healthcare_mb_softbank_jp_pc_assets_setup_web_explanation_input_php:[
    {checked:false, id:"telno3", value:"00012345678", type:"text", name:"", index:"0"}, 
    {checked:false, id:"passwd", value:"1234", type:"password", name:"passwd", index:"1"}, 
    {index:"3", value:"4", type:"option", name:"undefined", selected:true}
  ]
};
var url_key=document.location.protocol.replace(/[^A-Za-z]/g,'_')+'__'+document.location.hostname.replace(/[^A-Za-z]/g,'_')+document.location.pathname.replace(/\W/g,'_');
var j=json[url_key];

for(var i=0;i<j.length;i++){
  if(j[i].type=='text'||j[i].type=='password'){
    if(document.getElementsByName(j[i].name).length > 0){
      document.getElementsByName(j[i].name)[0].value=j[i].value
    }else{
      document.getElementById(j[i].id).value=j[i].value
    }
  }else if(j[i].type=='radio'||j[i].type=='checkbox'){
    document.getElementsByTagName('input')[j[i].index].checked=j[i].checked
  }else if(j[i].type=='option'){
    if(document.getElementsByTagName('option')[j[i].index].value==j[i].value){
      document.getElementsByTagName('option')[j[i].index].selected=j[i].selected
    }
  }
}
alert('auto_login works!');


})()

javascript:(function(){for(var b={https___healthcare_mb_softbank_jp_pc_assets_setup_web_explanation_input_php:[{checked:!1,id:"telno3",value:"00012345678",type:"text",name:"",index:"0"},{checked:!1,id:"passwd",value:"1234",type:"password",name:"passwd",index:"1"},{index:"3",value:"4",type:"option",name:"undefined",selected:!0}]}[document.location.protocol.replace(/[^A-Za-z]/g,"_")+"__"+document.location.hostname.replace(/[^A-Za-z]/g,"_")+document.location.pathname.replace(/\W/g,"_")],a=0;a<b.length;a++)"text"==b[a].type||"password"==b[a].type?0<document.getElementsByName(b[a].name).length?document.getElementsByName(b[a].name)[0].value=b[a].value:document.getElementById(b[a].id).value=b[a].value:"radio"==b[a].type||"checkbox"==b[a].type?document.getElementsByTagName("input")[b[a].index].checked=b[a].checked:"option"==b[a].type&&document.getElementsByTagName("option")[b[a].index].value==b[a].value&&(document.getElementsByTagName("option")[b[a].index].selected=b[a].selected);alert("auto_login works!")})();

f:id:zariganitosh:20140723100104p:image:w450


見事!元どおりに入力された!

安全の問題

  • 以上が、ブックマークレットを利用して自動入力する基本のコードである。
  • AppleScriptで入力情報を収集して、上記のようなJavaScriptコードを自動生成するようにすれば、iPhoneiPadでも自動入力が可能になる。
    • Safariのブックマークは、iCloud経由で勝手に同期されるので便利。
  • Safariが自動入力してくれないページでも、このブックマークレットなら自動入力してくれる。(可能性大)
  • さらに、プルダウンリストやチェックボックス・ラジオボタンの状態も自動入力できるのだ。

しかし、このブックマークレットをこのまま使うのは危険である!

  • 見てのとおり、コードの中にパスワードなどの重要な情報がそのまま保存されている。
  • もしも、このブックマークレットが何らかの原因で外部に漏洩すると、不正ログインが簡単に実行できてしまう。
  • このページはスマート体組成計のページなので大した実害はないが、(体重や脂肪の計測値が見られるだけ)
  • 仮に金融機関のページのログイン情報だと、痛い目に遭う可能性大。

重要な情報は暗号化しておかなければならないのだ!

  • 暗号化を実現するコマンドとJavaScriptライブラリを探してみた。

opensslコマンドの環境

  • OSX 10.9.4に標準インストールされているopensslコマンドのバージョンは、0.9.8y。
$ openssl version
OpenSSL 0.9.8y 5 Feb 2013
  • 巷を騒がせたopensslのバグ"Heartbleed"は、openssl 1.0.1〜1.0.1fに含まれる。
  • よって、OSX標準インストールされるopnessl 0.9.8yには、バグは存在しない。
  • 仮に openssl 1.0.1〜1.0.1fを利用している場合は、1.0.1gへアップデートする。

opensslコマンドで暗号化する

  • 例:文字列"hello world!!"を"1234"というパスワードで暗号化するしてみる。
    • 暗号化によって、見えない文字コードに変換されてしまうとターミナル出力で確認できない。
    • -a = -base64オプション。ここではbase64変換で確実に見える文字にして出力してみた。
$ echo 'hello world!!' | openssl enc -e -a -salt -aes-256-cbc -pass pass:1234
U2FsdGVkX1/ViT/1pTYv4dZsujl2feiy8Wuq72NlWCI=

enc共通鍵による暗号化
-eエンコードする(暗号化する)
-dデコードする(復号化する)
-a(-base64)base64エンコード・デコードを有効にする
-salt鍵生成ルーチンでソルトを利用する。特に理由(0.9.5以前との互換性を維持する等)がない限り、-saltは必ず指定しておくべき。
-aes-256-cbc特に理由(暗号化・復号化の処理スピードの問題など)がない限り、鍵長が最も長い-aes-256-cbcを選択しておいた方が良さそう。

  • 今度は、暗号化された”U2FsdGVkX1/ViT/1pTYv4dZsujl2feiy8Wuq72NlWCI=”を復元してみる。
$ echo U2FsdGVkX1/ViT/1pTYv4dZsujl2feiy8Wuq72NlWCI= | openssl enc -d -a -salt -aes-256-cbc -pass pass:1234
hello world!!

元の文字列"hello world!!"が復元できた!

JavaScriptで復号化する

暗号文をブックマークレットに含める場合、それを復号するためのJavaScriptが必要になる。当初、二つのAES JavaScriptライブラリを見つけた。

JavaScript Implementation of AES Advanced Encryption Standard in Counter Mode | Movable Type Scripts
  • このライブラリは、AES-CTR(CounTeR:カウンタ)モードの暗号化・復号化を行う。

  • サンプル:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>aes-256-ctrサンプル</title>
</head>

<body>
<p>原文 : hello world!!</p>
<p id="encrypted_text">0</p>
<p id="decrypted_text">0</p>
</body>


<script src="http://www.movable-type.co.uk/scripts/js/crypto/aes.js">/* AES JavaScript implementation */</script>
<script src="http://www.movable-type.co.uk/scripts/js/crypto/aes-ctr.js">/* AES Counter Mode implementation */</script>

<script type="text/javascript">
    var encrypted = Aes.Ctr.encrypt("hello world!!", "1234", 256);
    document.getElementById('encrypted_text').innerText = "暗号化: " + encrypted;
    
    var decrypted = Aes.Ctr.decrypt(encrypted, "1234", 256);
    document.getElementById('decrypted_text').innerText = "復号化: " + decrypted;
</script>
</html>

  • 上記コードをファイル名"aes_ctr.html"で保存して、開いてみると...
原文 : hello world!!

暗号化: xwHwoweU3FMNRuMVxVY1bVG3qJNP

復号化: hello world!!
opensslコマンドとの互換性
  • OSX 10.9.4に標準インストールされるOpenSSL 0.9.8yは、aes-ctrに対応していない。
  • 一方、HomebrewでインストールしたOpenSSL 1.0.1hでは、aes-ctrに対応している。
  • しかし、同じaes-ctrでありながら、このJavaScriptライブラリとopensslコマンド相互の暗号化・復号化ができない。
    • 例:opensslコマンドで暗号化したものを、このJavaScriptライブラリで複合化できなかった...。(なぜだろう?Saltの扱いの違い?)
  • たとえ互換性がなくても、このJavaScriptライブラリで暗号化すれば問題なく復号化できるので、躊躇する必要はない。
crypto-js - JavaScript implementations of standard and secure cryptographic algorithms - Google Project Hosting
  • このライブラリは、AES-256-CBC(Cipher Block Chaining:暗号ブロック連鎖)モードの暗号化・復号化を行う。
    • 読み込むライブラリ(ファイル)によって、AES-256-CBC以外にも様々な暗号化に対応しているようだ。
  • サンプル:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>aes-256-cbcサンプル</title>
</head>

<body>
<p>原文      : hello world!!</p>
<p id="encrypted_text">0</p>
<p id="decrypted_text_bin">0</p>
<p id="decrypted_text_utf8">0</p>
</body>

<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js"></script>
<script type="text/javascript">
    var encrypted = CryptoJS.AES.encrypt("hello world!!", "1234");
    document.getElementById('encrypted_text').innerText = "暗号化     : " + encrypted;
    
    var decrypted_bin = CryptoJS.AES.decrypt(encrypted, "1234");
    document.getElementById('decrypted_text_bin').innerText = "復号 文字コード: " + decrypted_bin;
    
    var decrypted_utf8 = decrypted_bin.toString(CryptoJS.enc.Utf8);
    document.getElementById('decrypted_text_utf8').innerText = "復号 UTF8 : " + decrypted_utf8;
</script>
</html>

  • 上記コードをファイル名"aes_cbc.html"で保存して、開いてみると...
原文      : hello world!!

暗号化     : U2FsdGVkX1/jmFp4MNDcqnb8zmD/3GB+xSEWuEs+qnI=

復号 文字コード: 68656c6c6f20776f726c642121

復号 UTF8 : hello world!!
opensslコマンドとの互換性
  • OSX 10.9.4に標準インストールされるOpenSSL 0.9.8yと、完全な互換性がある。
  • 試しに、上記で暗号化したものをopensslコマンドで復号化してみると...
$ echo U2FsdGVkX1/jmFp4MNDcqnb8zmD/3GB+xSEWuEs+qnI= | openssl enc -d -a -salt -aes-256-cbc -pass pass:1234
hello world!!

  • その逆も然り...
$ echo 'hello world!!' | openssl enc -e -a -salt -aes-256-cbc -pass pass:1234
U2FsdGVkX1/ZwwQ3vwuYHqYvPDhfAKria033cuKWHEs=
原文      : hello world!!

暗号化     : U2FsdGVkX1/ZwwQ3vwuYHqYvPDhfAKria033cuKWHEs=

復号 文字コード: 68656c6c6f20776f726c6421210a

復号 UTF8 : hello world!!

どちらも、ちゃんと復号化できた!

ログイン情報を暗号化する

  • 以上の暗号化ツールを活用して、ログイン情報を暗号化してみる。
    • opensslコマンドでログイン情報を暗号化してブックマークレットコードに組み込み、
    • crypto-jsJavaScriptライブラリでブックマークレット実行時に復号するのだ。

  • 余分な改行を削除したログイン情報のテキスト。
'{https___healthcare_mb_softbank_jp_pc_assets_setup_web_explanation_input_php:[{checked:false, id:"telno3", value:"00012345678", type:"text", name:"", index:"0"}, {checked:false, id:"passwd", value:"1234", type:"password", name:"passwd", index:"1"}, {index:"3", value:"4", type:"option", name:"undefined", selected:true}]}'

  • 上記テキストをaes-256-cbcで暗号化してみる。
    • パスワード=MSTER_PASSで暗号化して、base64エンコードしてみた。
$ echo -n '{https___healthcare_mb_softbank_jp_pc_assets_setup_web_explanation_input_php:[{checked:false, id:"telno3", value:"00012345678", type:"text", name:"", index:"0"}, {checked:false, id:"passwd", value:"1234", type:"password", name:"passwd", index:"1"}, {index:"3", value:"4", type:"option", name:"undefined", selected:true}]}' | openssl enc -e -salt -aes-256-cbc -pass pass:MSTER_PASS | base64
U2FsdGVkX1/uT2sjZl3uT43ciqzmXYZpB8mwYxMpyGk0EN/Gz0hzixJ4+Js2mU68qOWohJmpPGgr5g1+eaxL8aeLh+ooMgxXYLZozYUhWaJ99+0+tKV9GDarpbOMrfA6zE8hvCy62XChqZZINsPOA7G3KuFs54+BKNXFqKbsyA2f0MciDxQcmZgeTC8ci+rS1WZHyTMK5EPh6GhPK/QpoejG1JdXpcGdV94pN7R8flpT4iyMdGdbCfSeL7xjnmZs3BMvc5S50VZEtIdsD9BCeixQFf7GCZvuTJH4zxQHoPZMLc1T85ReBliiya9PvHcD6VfxTk4iuvodqIAC3J/pF5IP5fEhR/24XOZ/5gcEa92u8HulCp0LgfAeUxrgABo0a+ELvpysYBKQmwuTNU9ssFh9QA841RXvLh/JUnQAE8YTksS27PF50nhZap6C1OC5rOIRvIdrTgSgImjQ6TxhiA==

  • 暗号化されたログイン情報を利用したブックマークレットに修正してみた。
    • JavaScriptライブラリをロードする時間を考慮して、setTimeoutで0.5秒待機してから実行している。
    • ネットワーク環境やブラウザの性能によって、より多くの待機時間が必要になるかもしれない。
(function(){

//JavaScriptライブラリをロード
var e=document.createElement('script');
e.src='//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js';
document.body.appendChild(e);

setTimeout(function(){
//暗号化ログイン情報を復号する
var passwd_raw=prompt('auto_login Password','');
var json_text_cipher='U2FsdGVkX1/uT2sjZl3uT43ciqzmXYZpB8mwYxMpyGk0EN/Gz0hzixJ4+Js2mU68qOWohJmpPGgr5g1+eaxL8aeLh+ooMgxXYLZozYUhWaJ99+0+tKV9GDarpbOMrfA6zE8hvCy62XChqZZINsPOA7G3KuFs54+BKNXFqKbsyA2f0MciDxQcmZgeTC8ci+rS1WZHyTMK5EPh6GhPK/QpoejG1JdXpcGdV94pN7R8flpT4iyMdGdbCfSeL7xjnmZs3BMvc5S50VZEtIdsD9BCeixQFf7GCZvuTJH4zxQHoPZMLc1T85ReBliiya9PvHcD6VfxTk4iuvodqIAC3J/pF5IP5fEhR/24XOZ/5gcEa92u8HulCp0LgfAeUxrgABo0a+ELvpysYBKQmwuTNU9ssFh9QA841RXvLh/JUnQAE8YTksS27PF50nhZap6C1OC5rOIRvIdrTgSgImjQ6TxhiA==';
var json_text=CryptoJS.AES.decrypt(json_text_cipher, passwd_raw).toString(CryptoJS.enc.Utf8);
var json=eval('('+json_text+')');

//URLに対応したログイン情報を取り出す
var url_key=document.location.protocol.replace(/[^A-Za-z]/g,'_')+'__'+document.location.hostname.replace(/[^A-Za-z]/g,'_')+document.location.pathname.replace(/\W/g,'_');
var j=json[url_key];

//自動入力する
for(var i=0;i<j.length;i++){
  if(j[i].type=='text'||j[i].type=='password'){
    if(document.getElementsByName(j[i].name).length > 0){
      document.getElementsByName(j[i].name)[0].value=j[i].value
    }else{
      document.getElementById(j[i].id).value=j[i].value
    }
  }else if(j[i].type=='radio'||j[i].type=='checkbox'){
    document.getElementsByTagName('input')[j[i].index].checked=j[i].checked
  }else if(j[i].type=='option'){
    if(document.getElementsByTagName('option')[j[i].index].value==j[i].value){
      document.getElementsByTagName('option')[j[i].index].selected=j[i].selected
    }
  }
}
alert('auto_login works!');
}, 500);//setTimeout


})()
  • ブックマークレット中に含まれるのは暗号化されたログイン情報である。
  • ログイン情報を復号するためのマスターパスワードの入力が、毎回必要になるが、
  • たとえ漏洩したとしても、パスワードを知らない限り、不正利用はできないのだ。

これでログイン情報は暗号化され、安全になった!

マスターパスワードの照合

  • これでブックマークレットは安全になったが、一つ問題が発生した。
  • マスターパスワードが正しければ正常に自動入力されるのだけど、
  • マスターパスワードが間違っていると、何のリアクションもない。
  • 仮にパスワードが違っていた場合は「パスワードが違います」くらいの出力をしたいものだ。
  • 照合するためには、正しいマスターパスワードをどこかに保存しておく必要がある。
  • ところが、どこかに保存すると、それが漏洩したときの危険性が気になる。
  • できることなら、マスターパスワードはどこにも保存せずに、照合するのがベスト。
  • では、正しいマスターパスワードを知らずして、正誤判定する方法なんてあるのか?

ハッシュ値を使うと、それができる!

ハッシュ値の特性

  • 原文からハッシュ値を素早く求めることはできるが、
    • ハッシュ値から原文を推測することは相当困難。(現状は不可能)
  • 原文が同じ内容なら、必ず同一のハッシュ値になる。
    • 1文字でも違っていると、その値は大きく変化する。

  • 上記の特性を利用するとこで、パスワードの照合を安全に行うことができる。
    1. パスワードは原文を保存せず、そのハッシュ値を保存しておく。
    2. ユーザーがパスワード入力したら、そのハッシュ値を計算する。
    3. 上記1.と2.のハッシュ値の一致を確認すれば、入力されたパスワードが正しいと判定できる。

  • 仮にハッシュ値が漏洩しても、パスワード原文は推測できないので安全なのだ。
    • 一方、パスワード原文を保存していた場合は、それが漏洩した時に大問題になる。

opensslコマンドでsha256ハッシュ値を取得する

  • sha256を利用してマスターパスワードの照合を行うことにした。
    • sha1からsha256への移行が推奨されているため、sha256を利用した。
$ echo -n 1234 | openssl dgst -sha256
03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4

JavaScriptでsha256ハッシュ値を取得する

sha256ハッシュを計算するJavaScriptライブラリでも試してみた。

‎user1.matsumoto.ne.jp/~goma/js/sha1.js
  • サンプルコード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>SHA-256サンプル</title>
</head>

<body>
<p>原文      : 1234</p>
<p id="encrypted_text">0</p>
</body>

<script src="http://user1.matsumoto.ne.jp/~goma/js/sha256.js"></script>
<script type="text/javascript">
    var encrypted = sha256.hex("1234");
    document.getElementById('encrypted_text').innerText = "sha256     : " + encrypted;
</script>
</html>
  • 上記サンプルコードをSafariで開いてみると...
原文      : 1234

sha256     : 03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4
crypto-js - JavaScript implementations of standard and secure cryptographic algorithms - Google Project Hosting
  • サンプルコード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>sha256サンプル</title>
</head>

<body>
<p>原文      : 1234</p>
<p id="encrypted_text">0</p>
</body>

<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha256.js"></script>
<script type="text/javascript">
    var encrypted = CryptoJS.SHA256("1234");
    document.getElementById('encrypted_text').innerText = "sha256     : " + encrypted;
</script>
</html>
  • 上記サンプルコードをSafariで開いてみると...
原文      : 1234

sha256     : 03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4

opensslコマンド・Javascriptライブラリ、いずれのハッシュ値も同一の値だ!

sha256でマスターパスワードを照合

  • 以上のハッシュ関数を活用して、マスターパスワードを照合してみる。
    • opensslコマンドとcrypto-jsJavaScriptライブラリを使ってみた。

  • まずはMASTER_PASSのsha256ハッシュ値を求めた。
$ echo -n MASTER_PASS | openssl dgst -sha256
3b70f81b4a16b6546ea8114e43be495ce154c3a1e92056a59d7af3db6a9914fd

  • 上記ハッシュ値を利用して、パスワードを照合するコードを追加してみた。
(function(){

//JavaScriptライブラリをロード
(function(e){
  e=document.createElement('script');
  e.src='//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js';
  document.body.appendChild(e);
  e=document.createElement('script');
  e.src='//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha256.js';
  document.body.appendChild(e);
})()

setTimeout(function(){
//マスターパスワードを照合する
var passwd_sha256='3b70f81b4a16b6546ea8114e43be495ce154c3a1e92056a59d7af3db6a9914fd';
var passwd_raw=prompt('auto_login Password','');
if(CryptoJS.SHA256(passwd_raw) != passwd_sha256){
  if(passwd_raw != ''){alert('Password is NG.');}return;
}

//暗号化ログイン情報を復号する
var json_text_cipher='U2FsdGVkX1/uT2sjZl3uT43ciqzmXYZpB8mwYxMpyGk0EN/Gz0hzixJ4+Js2mU68qOWohJmpPGgr5g1+eaxL8aeLh+ooMgxXYLZozYUhWaJ99+0+tKV9GDarpbOMrfA6zE8hvCy62XChqZZINsPOA7G3KuFs54+BKNXFqKbsyA2f0MciDxQcmZgeTC8ci+rS1WZHyTMK5EPh6GhPK/QpoejG1JdXpcGdV94pN7R8flpT4iyMdGdbCfSeL7xjnmZs3BMvc5S50VZEtIdsD9BCeixQFf7GCZvuTJH4zxQHoPZMLc1T85ReBliiya9PvHcD6VfxTk4iuvodqIAC3J/pF5IP5fEhR/24XOZ/5gcEa92u8HulCp0LgfAeUxrgABo0a+ELvpysYBKQmwuTNU9ssFh9QA841RXvLh/JUnQAE8YTksS27PF50nhZap6C1OC5rOIRvIdrTgSgImjQ6TxhiA==';
var json_text=CryptoJS.AES.decrypt(json_text_cipher, passwd_raw).toString(CryptoJS.enc.Utf8);
var json=eval('('+json_text+')');

//URLに対応したログイン情報を取り出す
var url_key=document.location.protocol.replace(/[^A-Za-z]/g,'_')+'__'+document.location.hostname.replace(/[^A-Za-z]/g,'_')+document.location.pathname.replace(/\W/g,'_');
var j=json[url_key];
if(j==null){
  alert('No auto_login info.');return;
}

//自動入力する
for(var i=0;i<j.length;i++){
  if(j[i].type=='text'||j[i].type=='password'){
    if(document.getElementsByName(j[i].name).length > 0){
      document.getElementsByName(j[i].name)[0].value=j[i].value
    }else{
      document.getElementById(j[i].id).value=j[i].value
    }
  }else if(j[i].type=='radio'||j[i].type=='checkbox'){
    document.getElementsByTagName('input')[j[i].index].checked=j[i].checked
  }else if(j[i].type=='option'){
    if(document.getElementsByTagName('option')[j[i].index].value==j[i].value){
      document.getElementsByTagName('option')[j[i].index].selected=j[i].selected
    }
  }
}
alert('auto_login works!');
}, 500);//setTimeout


})()

これでマスターパスワードの照合もできるようになった!

  • 状況に応じて、以下のメッセージを出力するのだ。
    • マスターパスワードが違っていたら、'Password is NG.'
    • ログイン情報が見つからなかったら、'No auto_login info.'
    • 正常に自動入力できた場合は、'auto_login works!'

auto_loginプロジェクト

以上の仕組みを利用して、auto_loginプロジェクトはログイン情報を保全している。

      • 果たして、ちゃんと安全を確保できているのだろうか?

関連ページ


onloadすればsetTimeoutが不要

  • JavaScriptライブラリをロードする時間を考慮して、setTimeoutで大雑把に0.5秒待機する、という運を天に任せる的なコードは、常々どうにかしたいと思っていた。
  • 0.5秒も待つ必要ないかもしれないし、あるいはもっと待つ必要があるかもしれない。
  • 理想は、JavaScriptライブラリのロードが完了したタイミングで、本体のブックマークレットを実行するべき。

ロードが完了したタイミング=onloadイベントハンドラを使えばいいのだ!

  • scriptタグを追加する時に、onload属性を追加して、ブックマークレット本体の関数を呼び出すように変更してみた。
(function(f,e){
//JavaScriptライブラリをロード
e=document.createElement('script');
e.src='//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js';
document.body.appendChild(e);
e=document.createElement('script');
e.src='//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha256.js';
e.onload=function(){f()};
document.body.appendChild(e);

})(function(){
//マスターパスワードを照合する
var passwd_sha256='3b70f81b4a16b6546ea8114e43be495ce154c3a1e92056a59d7af3db6a9914fd';
var passwd_raw=prompt('auto_login Password','');
if(CryptoJS.SHA256(passwd_raw) != passwd_sha256){
  if(passwd_raw != ''){alert('Password is NG.');}return;
}

//暗号化ログイン情報を復号する
var json_text_cipher='U2FsdGVkX1/uT2sjZl3uT43ciqzmXYZpB8mwYxMpyGk0EN/Gz0hzixJ4+Js2mU68qOWohJmpPGgr5g1+eaxL8aeLh+ooMgxXYLZozYUhWaJ99+0+tKV9GDarpbOMrfA6zE8hvCy62XChqZZINsPOA7G3KuFs54+BKNXFqKbsyA2f0MciDxQcmZgeTC8ci+rS1WZHyTMK5EPh6GhPK/QpoejG1JdXpcGdV94pN7R8flpT4iyMdGdbCfSeL7xjnmZs3BMvc5S50VZEtIdsD9BCeixQFf7GCZvuTJH4zxQHoPZMLc1T85ReBliiya9PvHcD6VfxTk4iuvodqIAC3J/pF5IP5fEhR/24XOZ/5gcEa92u8HulCp0LgfAeUxrgABo0a+ELvpysYBKQmwuTNU9ssFh9QA841RXvLh/JUnQAE8YTksS27PF50nhZap6C1OC5rOIRvIdrTgSgImjQ6TxhiA==';
var json_text=CryptoJS.AES.decrypt(json_text_cipher, passwd_raw).toString(CryptoJS.enc.Utf8);
var json=eval('('+json_text+')');

//URLに対応したログイン情報を取り出す
var url_key=document.location.protocol.replace(/[^A-Za-z]/g,'_')+'__'+document.location.hostname.replace(/[^A-Za-z]/g,'_')+document.location.pathname.replace(/\W/g,'_');
var j=json[url_key];
if(typeof j==='undefined'){
  alert('No auto_login info.');return;
}

//自動入力する
for(var i=0;i<j.length;i++){
  if(j[i].type=='text'||j[i].type=='password'){
    if(document.getElementsByName(j[i].name).length > 0){
      document.getElementsByName(j[i].name)[0].value=j[i].value
    }else{
      document.getElementById(j[i].id).value=j[i].value
    }
  }else if(j[i].type=='radio'||j[i].type=='checkbox'){
    document.getElementsByTagName('input')[j[i].index].checked=j[i].checked
  }else if(j[i].type=='option'){
    if(document.getElementsByTagName('option')[j[i].index].value==j[i].value){
      document.getElementsByTagName('option')[j[i].index].selected=j[i].selected
    }
  }
}
alert('auto_login works!');
})

2014-06-10

購読者数とブックマーク数のログを記録する

Google Readerも、Fastladderも、とっくの昔に終了しているのに、自分のMacBookの中ではRSS_counter.appが、その購読者数のログを綿々と記録し続けていた。

f:id:zariganitosh:20140609171031p:image:w450

      • Fastladder:2012/6/1終了
      • Google Reader:2013/7/1終了

いかん、いかん、何とかせねばならない...。

何を記録すべきか?

  • Google ReaderとFastladder無き今、RSS_counter.appにはlivedoor Readerの購読者数しか、カウントする能力がない。
  • ところが、livedoor Readerの購読者数は、今年になってからほとんど変化していない。
    • Google Readerが終了した頃こそ若干の増加はあったが、現在は数人の増減がある程度で、ほとんど変化なしの状態。
  • いったい、Google Readerの購読者はどこへ行ってしまったのか?
  • 調べてみると、Feedlyにたくさんの購読者を発見した!

というわけで...

  • livedoor Readerに加えて、Feedlyの購読者数もカウントすることにした。
  • また、ついでにはてなブックマーク数も記録して、日々の精進に役立てる。

Feedlyの購読者数を取得する


  • 上記URLが意味するところは、以下のような感じ。
APIのパスhttp://cloud.feedly.com/v3/feeds/
feed/購読者数を調べたいブログのURL         feed/http://d.hatena.ne.jp/zariganitosh/rss
feed/購読者数を調べたいブログのURL(エスケープ)  feed%2Fhttp%3A%2F%2Fd.hatena.ne.jp%2Fzariganitosh%2Frss

  • そして、上記URLは以下のようなJSONを返した。
{"id":"feed/http://d.hatena.ne.jp/zariganitosh/rss","feedId":"feed/http://d.hatena.ne.jp/zariganitosh/rss","title":"ザリガニが見ていた...。","velocity":1.2,"subscribers":1542,"language":"ja","topics":["iPhone Blog 39","Mac","APPLE","Subscriptions","iPhone"],"website":"http://d.hatena.ne.jp/zariganitosh/","visualUrl":"http://www.st-hatena.com/users/za/zariganitosh/user_p.gif?","description":"ザリガニが見ていた...。"}
ワンライナー作り

以上の仕組みから、URLを与えたらその購読者数を返すワンライナーを考える。

$ URL=http://d.hatena.ne.jp/zariganitosh/
$ echo feed/${URL}rss|nkf -wMQ|tr = %
feed%2Fhttp%3A%2F%2Fd%2Ehatena%2Ene%2Ejp%2Fzariganitosh%2Frss

$ echo feed/${URL}rss|nkf -wMQ|tr = %|xargs -I{} curl --silent http://cloud.feedly.com/v3/feeds/{}
{"id":"feed/http://d.hatena.ne.jp/zariganitosh/rss","feedId":"feed/http://d.hatena.ne.jp/zariganitosh/rss","title":"ザリガニが見ていた...。","velocity":1.2,"subscribers":1542,"language":"ja","topics":["iPhone Blog 39","Mac","APPLE","Subscriptions","iPhone"],"website":"http://d.hatena.ne.jp/zariganitosh/","visualUrl":"http://www.st-hatena.com/users/za/zariganitosh/user_p.gif?","description":"ザリガニが見ていた...。"}

  • 手順3:上記JSONから"subscribers"の値を読み取る。
    • カンマ「,」を行区切り、コロン「:」をフィールド区切りとみなしてawkで処理した。
$ echo feed/${URL}rss|nkf -wMQ|tr = %|xargs -I{} curl --silent http://cloud.feedly.com/v3/feeds/{}|awk -F: -v RS=, '/subscribers/{print $2}'
1542

はてなブックマーク合計数を取得する

  • はてなブックマークの合計数もシンプルなURLで取得しようとしたのだけど、
  • XML-RPCのことはよく知らないのだが、上記解説ページのサンプルコードをそのまま使うと、ブックマーク合計数が取得できた。

被ブックマーク合計数取得API

...中略...

サンプルプログラム

...中略...

#!/usr/local/bin/perl
use strict;
use warnings;
use XMLRPC::Lite;

my $url = shift or die "usage $0 <url>";
our $EndPoint = 'http://b.hatena.ne.jp/xmlrpc';

my $count = XMLRPC::Lite
    ->proxy($EndPoint)
    ->call('bookmark.getTotalCount', $url)
    ->result;

print $count, "\n";
はてなブックマーク件数取得API - Hatena Developer Center
  • 上記スクリプトをget_all_count.plとして保存して、以下のコマンドで取得できた。
$ perl ~/Desktop/get_all_count.pl http://d.hatena.ne.jp/zariganitosh/
19348
ワンライナー作り
  • 必要最小のモジュールXMLRPC::Liteだけ組み込み、変数の代入は使わず、XMLRPC::Liteの引数に直接指定してみた。
$ URL=http://d.hatena.ne.jp/zariganitosh/
$ perl -mXMLRPC::Lite -e 'print XMLRPC::Lite->proxy("http://b.hatena.ne.jp/xmlrpc")->call("bookmark.getTotalCount", $ARGV[0])->result' $URL
19348

AppleScriptに仕上げる


 property rss_url : "" --"http://d.hatena.ne.jp/XXXX/" --購読者数を確認したいURL(最後のrssパスは不要)
 
 on run
   init()
 end run
 
 on idle
   main()
   return 3600
 end idle
 
 on reopen
   main()
   tell application "Console" to activate
 end reopen
 
 
 
 
 on init()
   if rss_url = "" then save_rss_url()
   touch_rss_log()
 end init
 
 on main()
   set rss_livedoor to livedoor_reader_count()
   set rss_feedly to feedly_feeds_count()
   set rss_total to rss_livedoor + rss_feedly
   set hatena_b to hatena_bookmark_total_count()
   set rss_log to datetime() & "購読者=" & rss_total & " (livedoor=" & rss_livedoor & ", feedly=" & rss_feedly & ")    はてなB=" & hatena_b
   if rss_loglast_log() then
     save_log(rss_log)
   end if
   open_unique_log(log_path())
 end main
 
 
 
 
 on livedoor_reader_count()
   do shell script "curl --silent " & quoted form of ("http://reader.livedoor.com/about/" & rss_url) & "|grep subscriber_count|sed -e 's/[^0-9]//g'|tr \"\\n\" \"+\"|sed -e 's/+$//'|bc"
 end livedoor_reader_count
 
 on feedly_feeds_count()
   --URI encode|nkf -wMQ|tr = %
   --URI decode|tr % =|nkf -WwmQ
   do shell script "echo feed/" & rss_url & "rss|/usr/local/bin/nkf -wMQ|tr = %|xargs -I{} curl --silent http://cloud.feedly.com/v3/feeds/{}|awk -F: -v RS=, '/subscribers/{print $2}'"
 end feedly_feeds_count
 
 on hatena_bookmark_total_count()
   do shell script "perl -mXMLRPC::Lite -e 'print XMLRPC::Lite->proxy(\"http://b.hatena.ne.jp/xmlrpc\")->call(\"bookmark.getTotalCount\", $ARGV[0])->result' " & rss_url
 end hatena_bookmark_total_count
 
 on save_rss_url()
   activate
   display dialog "照会するページのURLを入力してください。" default answer "" with title my name
   set rss_url to result's text returned
 end save_rss_url
 
 on datetime()
   do shell script "date '+%Y-%m-%d '"
 end datetime
 
 on save_log(info_text)
   do shell script "echo " & quoted form of (info_text) & " >> " & quoted form of log_path()
   
   --1000行を超えたら、ログローテーションする。(xxxx.log > xxxx.log.1xxxx.logには最新の100ログだけ残る)
   do shell script "f=" & log_path() & "; if [ `cat \"$f\"|wc -l` -ge 1000 ]; then mv \"$f.1\" \"$f.2\"; mv \"$f\" \"$f.1\"; cat \"$f.1\" | tail -n 100 >\"$f\"; fi;"
 end save_log
 
 on last_log()
   do shell script "tail -n 1 " & quoted form of log_path()
 end last_log
 
 on log_path()
   (path to library folder from user domain)'s POSIX path & "Logs/" & log_name(rss_url)
 end log_path
 
 on log_name(str)
   do shell script "echo " & quoted form of str & "|tr ':/' '--'"
   my name & "-" & result & ".log"
 end log_name
 
 on open_rss_log()
   do shell script "open " & quoted form of log_path()
 end open_rss_log
 
 on touch_rss_log()
   do shell script "touch " & quoted form of log_path()
 end touch_rss_log
 
 on open_unique_log(unix_path)
   tell application "Console" to launch
   delay 0.1
   set fname to do shell script "basename " & unix_path --ファイルパスからファイル名を取得する
   tell application "System Events"
     tell process "Console"
       if fname is not in windows's name then
         do shell script "open -ga Console " & unix_path --コンソール.appでファイルパスを開く
       end if
     end tell
   end tell
 end open_unique_log


  • 上記コードをAppleScriptエディタで開いて、以下の形式で保存する。
名前:        RSS_counter.app
ファイルフォーマット:アプリケーション
オプション:     「ハンドラの実行後に終了しない」チェックあり

使い方

  • GUIスクリプティングを使っているので、アプリケーションにコンピュータの制御を許可する必要がある。
    • システム環境設定 >> セキュリティとプライバシー >> アクセシビリティ >> プライバシーから、RSS_counter.appにコンピュータの制御を許可しておく。
  • RSS_counter.appを起動しておくと、1時間ごとに、購読者数・はてなブックマーク数をログに保存する。
    • ~/Library/Logs/RSS_counter-http---d.hatena.ne.jp-XXXX-.log
  • Dockアイコンをクリックすると、その時点の購読者数・はてなブックマーク数をチェックして、表示する。
  • 前回とのログに変化がなければ、記録しない。

f:id:zariganitosh:20140610140727p:image:w450


購読者数&ブックマーク数をチェックして、日々精進するのだ。

2014-06-06

auto_loginスクリプトのSafari7対応

あらゆるWebページで自動入力を可能にするauto_loginスクリプト(自作)が...

たぶんSafari7以降、使えなくなっていた。激しく不便!な状態が続いていた。自作なので自分で直すしかない...。

原因

  • Safariがサポートするjavascriptにおいて、window.locationあるいはdocument.locationが返す値が変化していた。

 tell application "Safari" to do JavaScript "window.location" in document 1

Safari5まで
  • window.locationのプロパティがすべて、レコードとして返る。

 {origin:"http://d.hatena.ne.jp", protocol:"http:", hash:"", pathname:"/zariganitosh/20140523/minimum_bash_keybind", hostname:"d.hatena.ne.jp", |port|:"", href:"http://d.hatena.ne.jp/zariganitosh/20140523/minimum_bash_keybind", |host|:"d.hatena.ne.jp", search:""}

Safari7から
  • 何も返らない...。(なぜこうなった?)

 missing value

対策

_login_base.scptを以下のように修正した。

  • Before(このままではmissing valueになってしまうので...)

 on url_info()
   tell application "Safari" to do JavaScript "document.location" in document 1
 end url_info

  • After(コード中で利用するoriginとpathnameをキーに持つレコードを作って返す)

 on url_info()
   tell application "Safari"
     {origin:(do JavaScript "document.location.origin" in document 1) ¬
       , pathname:(do JavaScript "document.location.pathname" in document 1)}
   end tell
 end url_info

参考:locationのプロパティ
location.href
http://d.hatena.ne.jp:80/zariganitosh/searchdiary?word=ruby&.submit=%B8%A1%BA%F7&type=detail#c
location.origin
http://d.hatena.ne.jp:80
location.host
d.hatena.ne.jp:80
location.protocol location.hostname location.portlocation.pathnamelocation.searchlocation.hash
http://d.hatena.ne.jp:80/zariganitosh/searchdiary?word=ruby&.submit=%B8%A1%BA%F7&type=detail#c

GitHub

  • GitHubにアップロードして管理することにした。
  • hubコマンドを使うと、苦労最小で公開できる。
# GitHubに新規リポジトリを作成
(master)$ hub create
Updating origin
created repository: zarigani/auto_login

# 最初のアップロード
(master)$ git push -u origin master
Warning: Permanently added the RSA host key for IP address 'xxx.xxx.xxx.xxx' to the list of known hosts.
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (15/15), 113.44 KiB | 0 bytes/s, done.
Total 15 (delta 1), reused 0 (delta 0)
To git@github.com:zarigani/auto_login.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

  • コンパイル済み(バイナリファイル)のAppleScriptもちゃんとdiffできるように設定しておいた。
# AppleScriptのdiff属性を追加
(master)$ echo '*.scpt diff=applescript' >> .gitattributes
(master)$ git add .
(master)$ git commit
[master b17bd49] AppleScriptのdiff属性を追加
 1 file changed, 1 insertion(+)
 create mode 100644 .gitattributes

# AppleScriptをdiffする設定を追加
(master)$ git config diff.applescript.textconv osadecompile
(master)$ cat .git/config
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
	ignorecase = true
	precomposeunicode = true
[diff "applescript"]
	textconv = osadecompile

# 2回目以降のアップロード
(master)$ git push

インストールと使い方

準備
  • ダウンロードして解凍したauto_loginフォルダを、スクリプトフォルダ(~/Library/Scripts/auto_login)に移動する。
  • AppleScriptエディタ >> 環境設定... >> 「メニューバーにスクリプトメニューを表示」を有効にしておくと便利。

f:id:zariganitosh:20140606102332p:image:w450

入力情報の保存と復元

f:id:zariganitosh:20140606101719p:image:w450


  • スクリプトメニューからauto_login/save_login(ログイン情報取得)を実行してみる。
    • これで、入力されたお客さま番号が保存される。

f:id:zariganitosh:20140606143323p:image:w428

f:id:zariganitosh:20140606143353p:image:w428

f:id:zariganitosh:20140606143322p:image:w428


  • スクリプトメニューからauto_login/auto_login(自動ログイン)を実行してみる。
    • きっと、先ほどのお客さま番号が復元されるはず。

f:id:zariganitosh:20140606143533p:image:w428

ブックマークレットに変換
  • さらに、auto_login/ブックマークレット作成.scptを実行してみる。
  • すると、クリップボードに自動入力するJavaScriptがコピーされる。

f:id:zariganitosh:20140606141649p:image:w428


  • と同時にSafariが開き、そのJavaScriptを埋め込んだauto_loginのリンクが表示される。

f:id:zariganitosh:20140606141735p:image:w450

  • auto_loginのリンクをブックマークバーにドラッグ&ドロップすれば、ブックマークレット*1として保存される。
  • Safariのブックマークは、iCloud経由で、その他のMac、iPhoneiPadなどの端末で同期される。

  • 今度は、iPhoneのSafariでゆうちょダイレクトのログインページを開いてみる。
  • iPhoneのブックマークには、auto_loginブックマークレットが、きっと見えているはず。
  • auto_loginブックマークレットを実行すれば、iPhoneでもお客さま番号が復元される!

f:id:zariganitosh:20140606144747p:image:h450

f:id:zariganitosh:20140606144745p:image:h450

あらゆるフォームが保存対象
  • 保存される情報は、テキストボックスだけではない。
  • チェックボックス・ラジオボタン・セレクトメニューなど、あらゆるフォームの情報が保存されるのだ。
  • よって、ログインページに限らず、通販ページの送付先情報などを保存しておくことも可能である。
  • 会員登録できない通販サイトで定期的に購入する場合など、素早く買い物できて便利かもしれない。
安全性
  • 保存する情報には、opensslでマスターパスワードをキーに暗号化している。
  • マスターパスワードは、sha1ハッシュ化して保持している。
  • マスターパスワードを知らなければ、暗号化された情報は解読できないはず。

バージョン系統影響を受けるバージョン修正バージョン
1.0.1系1.0.1 から 1.0.1f までのリリース1.0.1g
1.0.2-beta系1.0.2-beta から 1.0.2-beta1 までのリリース1.0.2-beta2 (リリース予定)
      • OSX 10.9.3にデフォルトインストールされるopensslには、HeartBleed脆弱性は存在しない。(古いけど)
$ openssl version
OpenSSL 0.9.8y 5 Feb 2013

Safariで再び快適に自動入力できるようになった!

*1:ブックマークに埋め込まれたJavaScript

2014-04-06

同じウィンドウをいくつも開かないコンソール.appが欲しい

Mavericksになって、コンソール.appの挙動が若干変わった。例えば、すでにA.logウィンドウが存在する状態でもう一度A.logを(Finderから)開こうとした時、MountainLionまでは現在のA.logのウィンドウがアクティブになる。一方、Mavericksでは同じA.logのウィンドウがもう一つ開かれてしまう...。A.logを開く度に、同じA.logのウィンドウがどんどん増えてしまうのだ。

些細な違いなのだけど、同じウィンドウが無駄にいくつも開くのは好きになれない。できることならMountainLionまでの行儀の良い仕様に戻したい。でも、それは出来ないみたい。よって、開く時に同じ名前のウィンドウが存在するかどうか確認してから開くようにしてみた。

 on open_unique_log(unix_path)
   tell application "Console" to launch
   
   set file_name to do shell script "basename " & unix_path --ファイルパスからファイル名を取得する
   tell application "System Events"
     tell process "Console"
       if file_name is not in windows's name then
         do shell script "open -ga Console " & unix_path --コンソール.appでファイルパスを開く
       end if
     end tell
   end tell
 end open_unique_log
 
 open_unique_log("~/Library/Logs/RSS_counter-http---d.hatena.ne.jp-zariganitosh-.log")


出来た、出来たと喜んで何度か実行しているうちに、タイミングによっては同じウィンドウが二つ開いてしまうことに気付いた。正確な理由は謎である。でもおそらく、コンソール.app起動直後はAppleScriptがそのGUIを認識するまでに若干のタイムラグがあるのかもしれない。Delay 0.1を追加して0.1秒待機することで、同じウィンドウが二つ開くことがなくなった。

 on open_unique_log(unix_path)
   tell application "Console" to launch
   delay 0.1
   
   set file_name to do shell script "basename " & unix_path --ファイルパスからファイル名を取得する
   tell application "System Events"
     tell process "Console"
       if file_name is not in windows's name then
         do shell script "open -ga Console " & unix_path --コンソール.appでファイルパスを開く
       end if
     end tell
   end tell
 end open_unique_log
 
 open_unique_log("~/Library/Logs/RSS_counter-http---d.hatena.ne.jp-zariganitosh-.log")


たった0.1秒、されどこの0.1秒が大きな仕事をする。GUIスクリプティングを使う場合、この0.1秒に気付かないと目的は達成されず、欲求は満たされないのだ。そうゆうことがよくある。

      • ちなみに、SSD環境のMacBookは0.1秒でOKかもしれないが、HDD環境ではさらに長い待機時間が必要かもしれない。

2013-12-25

OSX10.9におけるGUIスクリプティングとセキュリティとの戦い

AppleScriptにはGUIスクリプティングという最後の切り札がある。GUIスクリプティングは、OSが認識するGUIオブジェクトを直接指定して操作する方法である。AppleScriptに対応していないアプリケーションであっても、GUIスクリプティングを使えば、ある程度自由に操作できる。かなり便利で、よく使っていた。

GUIオブジェクトとは、画面に表示されるウィンドウとか、ボタンとか、テキスト等の、OSが管理しているあらゆる表示物である。そのようなものを自由に操作できるようになると、場合によっては悪意のある操作を実行することも可能になる。そのためセキュリティ上の危険性を考慮してか、Appleは長らく、たった1つの制約を付けていた。

GUIスクリプティングを実行するには、システム環境設定 >> アクセシビリティ >>「補助装置にアクセスできるようにする」をオン(チェックあり)にしておく必要があった。(OSX 10.8まで)

OSデフォルトはオフなのだけど、自分は当然、常にオンである。オフにすることなんて考えられない。例えれば、今時のWebブラウザで「JavaScriptを有効にする」が常にオンであり続けるのと同じ感覚である。GUIスクリプティングが使えなくなると、多くの場面で不便が生じる。だから、チェックを入れるひと手間はあるが、チェックさえ入れてしまえば常にGUIスクリプティングが実行可能になるOSX10.8までの状況は、自分にとっては素晴らしい環境であった。

一方、OSX10.9では、GUIスクリプティングに対するセキュリティ管理の方法がまったく別物になってしまった。かつての「補助装置にアクセスできるようにする」設定は消え去り、システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティとして生まれ変わった。新たな設定方法では、GUIスクリプティングを許可するアプリケーションを個別に登録する必要がある。

アプリケーションごとに許可する・しないを設定する必要があるので、確かに安全性は向上するのだけど、必要なアプリケーションが許可されていない初期状態では、GUIスクリプティングが思いどおりに実行されず、不便な思いをすること必至。また、最初はその仕組みもよく分からず、イライラを募らせていた。何をどう設定すれば、以前の利便性を取り戻せるのか、GUIスクリプティングを制限するセキュリティとの戦いの記録を残しておこうと思う。

必要なアプリケーションを許可する

AppleScript エディタ.appを許可する
  • AppleScript エディタで新規スクリプトを作成して、以下のようなコードを書き込んだ。

 tell application "System Events"
   tell process "AppleScript Editor"
     set frontmost to true
     click menu bar 1's menu bar item "ファイル"
     click menu bar 1's menu bar item "ファイル"'s menu 1's menu item "最近使った項目を開く"
   end tell
 end tell


  • そして実行してみると、以下のようなエラーが表示される。

f:id:zariganitosh:20131224170411p:image:w450


  • OKボタンを押して、システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティ を確認してみると...

f:id:zariganitosh:20131224170653p:image:w450

  • その項目に「AppleScript エディタ.app」が追加されている。

  • ロックを解除して、GUIスクリプティングを許可してみる。

f:id:zariganitosh:20131224171308p:image:w450

  • その後もう一度AppleScript エディタから実行してみると、

今度はちゃんと実行できた!(ファイル >> 最近使った項目を開く メニュー操作が実行された)

  • AppleScript エディタ.appに対して、GUIスクリプティングの実行を許可したからである。
  • 今後、AppleScript エディタ.appでは、あらゆるGUIスクリプティングが実行可能になる。
SystemUIServer.appを許可する
  • 次に、上記スクリプトを「gui_scripting.scpt」として保存してみた。
  • スクリプトメニューから実行してみる。

f:id:zariganitosh:20131225081542p:image:w450

  • ところが実行しても、何も起こらない...。

  • すかさず、システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティ を確認してみると、SystemUIServer.appが追加されている。

f:id:zariganitosh:20131225081750p:image:w450

  • チェックありにして、GUIスクリプティングを許可しておく。
  • その後、もう一度スクリプトメニューから実行してみると...

今度はちゃんと実行できた!(ファイル >> 最近使った項目を開く メニュー操作が実行された)

  • つまり、スクリプトメニューからAppleScriptを実行する場合は、SystemUIServer.appを許可しておく必要があるのだ。
  • 同じgui_scripting.scptをAppleScript エディタで開いて実行する場合は、AppleScript エディタ.appを許可しておく。

このように、AppleScriptを実行する環境をそれぞれ個別に許可しておく必要があるのだ。

これが、OSX10.9からのGUIスクリプティングのセキュリティ管理の仕組みの基本である。

Quicksilver.appを許可する
  • となると、gui_scripting.scptをQuicksilverから起動してみると、やはり何も起こらない...。
  • すかさず、システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティ を確認してみると、Quicksilver.appが追加されている。

f:id:zariganitosh:20131225084618p:image:w450

  • チェックありにして、GUIスクリプティングを許可しておく。
  • その後、もう一度Quicksilverから実行してみると...

今度はちゃんと実行できた!(ファイル >> 最近使った項目を開く メニュー操作が実行された)

ターミナル.appを許可する
  • ターミナルを開いて、osascriptコマンドを使って実行してみた。
  • 「補助アクセスは許可されません。 (-1719)」というエラー発生。
$ osascript /Users/zari/Library/Scripts/gui_scripting.scpt
/Users/zari/Library/Scripts/gui_scripting.scpt: execution error: System Events got an error: osascript には補助アクセスは許可されません。 (-1719)
  • すかさず、システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティ を確認してみると、ターミナル.appが追加されている。

f:id:zariganitosh:20131225085248p:image:w450

  • チェックありにして、GUIスクリプティングを許可しておく。
  • その後、もう一度スクリプトメニューから実行してみると...

今度はちゃんと実行できた!(ファイル >> 最近使った項目を開く メニュー操作が実行された)

caffeinateコマンドを許可する
  • 自作のtimerコマンドを使って、定期的な繰り返し処理を実行している。
  • timerコマンドは、launchdとpmset scheduleを利用している。
  • そして、launchdから実行する時は、処理中に無用なスリープをしないように、caffainateコマンドでラップして実行している。
    • 例えば「caffeinate osascript $HOME/Library/Scripts/gui_scripting.scpt」を実行している。
  • そのような場合、caffeinateコマンドにもGUIスクリプティングを許可しておく必要があるようだ。
BetterTouchTool.appを許可する
  • 以上のように試行錯誤を繰り返していると、いつの間にかBetterTouchTool.appも追加されていた。

f:id:zariganitosh:20131225093449p:image:w450

  • BetterTouchTool.appは、マルチタッチジェスチャーを高度にカスタマイズできる、必携のアプリである。
  • BetterTouchTool.appも、その内部でGUIスクリプティングを利用しているらしく、許可しておく必要があるようだ。

アプレットとの戦い

アプレットを許可する
  • アプレットとは、ファイルフォーマット=アプリケーションとして保存したAppleScriptのことである。
    • 一般的なアプリケーションと同じように振る舞う。
  • 例えば、gui_scripting.scptをファイルフォーマット=アプリケーションに変更して、保存してみる。(gui_scripting.app)
  • すると、AppleScript エディタから実行する時は、以前と変わらず実行できるが、
  • gui_scripting.appのアイコンをダブルクリックして実行すると、エラーが表示された。
    • スクリプトメニューから実行しても、エラーが表示された。
    • Quicksilverから実行しても、エラーが表示された。

f:id:zariganitosh:20131225102537p:image:w300←OKを押した。

f:id:zariganitosh:20131225102535p:image:w300←システム環境設定を開くを押した。

  • システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティ を確認してみると、gui_scripting.appが追加されている。

f:id:zariganitosh:20131225103008p:image:w450

  • チェックありにして、GUIスクリプティングを許可しておく。
  • その後、もう一度アイコンをダブルクリックしてみると...

今度はちゃんと実行できた!(ファイル >> 最近使った項目を開く メニュー操作が実行された)

  • アプレットは、アプレットのファイル名ごとに個別に許可しておく必要がある。
    • 唯一の例外は、AppleScript エディタから実行する場合である。
    • アプリケーションではなく、スクリプトとして実行することになるので、
      • アプレットの許可は不要。
      • AppleScript エディタの許可が必要。
アプレットを修正した場合
  • では、先ほどのアプレットを修正してみる。(実験なので、コメントを追加しただけの意味のない修正)

 --何らかの修正
 tell application "System Events"
   tell process "AppleScript Editor"
     set frontmost to true
     click menu bar 1's menu bar item "ファイル"
     click menu bar 1's menu bar item "ファイル"'s menu 1's menu item "最近使った項目を開く"
   end tell
 end tell


  • その後実行すると、再び「補助アクセスは許可されません」のエラーメッセージが表示されてしまう...。

f:id:zariganitosh:20131225102537p:image:w300


  • 一旦、システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティ >> gui_scripting.appのチェックを外し、
  • 再び、チェックを入りにすることで、gui_scripting.appのGUIスクリプティングが正常に実行できるようになった。
  • どうやらアプレットが修正された場合、その都度、許可設定をやり直す必要があるようだ。

OSX10.9では、アプレットやアプリケーションの変化まで監視しているのだ!

  • この仕組みによって安全性は高まるのかもしれないが、頻繁に修正しながらアプレットの完成度を高めようとすると、激しく不便を感じる。

プロパティとの戦い

  • 変化を監視しているならば、プロパティを持つアプレットの扱いはどうなるのだろうか?という疑問が湧いてくる。
  • AppleScriptでは、処理中に変化したプロパティの値を、実行終了後も永続的に保持する。
  • 永続的に保持するためには、通常、ファイルとして保存しておく必要がある。
  • それではプロパティは一体どこに保存されているのか?
  • 実は、アプレットやスクリプトのファイル本体に含めて保存されているのだ。

プロパティが変化すると、アプレットやスクリプトのファイル変更日も更新されるのだ!

  • かつて、この荒技(自己修正)とも言える仕組みを知った時、驚愕した...。

つまり、プロパティが変化するとは、アプレットが修正されるのと同じことなのである。

  • となると、プロパティが常に変化するアプレットでは、GUIスクリプティングを実行できないかもしれない?(心配だ。眠れなくなりそう...。)
Appleの回答
  • この疑問に対するAppleの回答は、以下のようになっている。

デフォルトでは、OS X Mavericks のアクセシビリティ機能を使用するアプレットは、実行するときにプロパティを保存しません。プロパティを保存するアプレットは、その情報を保存するためにアプレット自体のコンテンツを修正します。この自己修正のために、アプレットが実行されるたびに異なるアプリケーションとして OS X に表示されます。このために認証プロセスが何度もトリガされます。

OS X:Mavericks でアクセシビリティとセキュリティ機能を使った AppleScript の使用方法 - Apple サポート
  • 相変わらず、Appleの文書は分かりにくい...。
    • 「アクセシビリティ機能を使用するアプレットは、実行するときにプロパティを保存しません」と言っているのに、
    • 「自己修正のために、アプレットが実行されるたびに異なるアプリケーションとして OS X に表示されます。」とも言っている。
  • プロパティを保存しないなら、自己修正されないはずなのに、一体どうゆうこと?
プロパティを持つアプレットの実験
  • こうゆう時は、実際に試してみるのが手っ取り早い。
  • 以下のようなプロパティ付きのアプレットにしてみた。

 property counter : 0
 set counter to counter + 1
 display notification counter
 
 delay 1
 
 tell application "System Events"
   tell process "AppleScript Editor"
     set frontmost to true
     click menu bar 1's menu bar item "ファイル"
     click menu bar 1's menu bar item "ファイル"'s menu 1's menu item "最近使った項目を開く"
   end tell
 end tell

  • アプレットをダブルクリックで実行すると、コードが修正されたのだから、当然実行できない。
    • 変数counterの値=1、「補助アクセスは許可されません」のエラーメッセージ。
  • もう一度ダブルクリックで実行してみても、やはり実行できない。
    • 変数counterの値=2、「補助アクセスは許可されません」のエラーメッセージ。
  • ここで、システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティのgui_scripting.appの許可設定を、切・入して再設定してみる。
  • その後にダブルクリックで実行してみると、今度は実行できた!
    • 変数counterの値=2、正常にGUIスクリプティングの実行完了。
  • その後は、何度でも正常に実行できる。
    • 変数counterの値=2、正常にGUIスクリプティングの実行完了。
  • 以上の結果から、以下のように理解した。

GUIスクリプティングが許可されていないアプレットは、プロパティを保存する。

一方、GUIスクリプティングが許可されるアプレットは、プロパティを保存しない。

Appleの抜け道の解説
  • 以上の仕組みでは、プロパティを保存しながらGUIスクリプティングを実行するアプレットは作れないことになってしまう。
  • また、コードを修正する度に、許可を再度設定するのも面倒である。
  • それらを回避する方法として、Appleは以下の方法を解説している。
  • ところが、自分の環境では何度試しても、うまく設定できない...。
    • OSX10.9.1
    • AppleScript2.3

アクセシビリティと永続的なプロパティ値の両方が必要なアプレットがある場合は、以下の手順を行って、再許可がなくても動作できるようにサインインします。


重要:以下の手順を使ってアプレットにサインインすると、不正なソフトウェアがユーザの許可なくアクセシビリティを使用するというセキュリティの脆弱性を招くことがあります。


関連するプロパティリストファイルをダウンロードしてインストールする

  1. この特別なプロパティリストファイル (plist) へのリンクを「option」キーを押しながらクリック、右クリック、または「control」キーを押しながらクリックします。
    ResourceRules-ignoring-Scripts.plist
  2. 表示されるショートカットメニューから、リンク先のファイルをダウンロードするオプションを選択します。
  3. ダウンロードしたプロパティリストファイルを「/ライブラリ/Preferences」フォルダに入れます。
    ヒント:「ライブラリ」フォルダにアクセスするには、キーボードの「option」キーを押しながら Finder で「移動」>「ライブラリ」と選択します。

ターミナルを使って plist ファイルをアクティベートする

  1. ターミナルを起動します。
  2. 次のコマンドを使います。プロパティリストファイルとターゲットの AppleScript アプレットの両方のパスを、実際の正しいパスにします。
codesign -s - --resource-rules=/Users/YourUserNameHere/ResourceRules-ignoring-Scripts.plist /path/to/applet.app

注意:署名 ID がある場合は、-s オプションに「-」の代わりに自分の署名 ID を使うことができます。

OS X:Mavericks でアクセシビリティとセキュリティ機能を使った AppleScript の使用方法 - Apple サポート
プロパティを保存しながらGUIスクリプティングを実行可能なアプレットの作り方

結局、自分は以下の方法に辿り着いた。


  • アプレットの「バンドルの内容」を表示して、識別子をコピーする。

f:id:zariganitosh:20131227081159p:image:w450


  • ターミナルで、以下のコマンドを実行した。
#設定
$ codesign -s - -i com.apple.ScriptEditor.id.gui-test -f $HOME/Library/Scripts/gui_scripting.app

#確認
$ codesign -dv $HOME/Library/Scripts/gui_scripting.app
Executable=/Users/zari/Library/Scripts/gui_scripting.app/Contents/MacOS/applet
Identifier=com.apple.ScriptEditor.id.gui-test
Format=bundle with Mach-O universal (i386 x86_64)
CodeDirectory v=20100 size=203 flags=0x2(adhoc) hashes=3+3 location=embedded
Signature=adhoc
Info.plist entries=13
Sealed Resources version=2 rules=12 files=4
Internal requirements count=0 size=12

  • システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティのgui_scripting.appの許可設定を、切・入して再設定する。

f:id:zariganitosh:20131225103008p:image:w450

  • 切・入のタイミングによってはうまく再設定できないことがあるので、その場合は何度かこの手順を繰り返すと再設定できた。

その後、アプレットをダブルクリックで実行すると、プロパティを保存しつつ、GUIスクリプティングのメニュー操作も実行された!

  • これは、アプレットの識別子(Bundle Identifier)によって、GUIスクリプティングを許可する仕組みのようだ。
    • このコマンド実行後は、アプレットを実行する度に、変数counterは+1ずつ加算され続けた。
    • スクリプトコードを修正しても、アクセシビリティの再設定なしでGUIスクリプティングを実行できた。

システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティのクリア

  • 実験しているうちに、アクセシビリティに無用なアプレットやアプリケーションが追加されてしまった...。
  • そんな時は、tccutil reset Accessibilityコマンドによって、許可項目をすべてクリアできる。
$ tccutil reset Accessibility

注意:上記のコマンドを実行すると、アクセシビリティの許可設定はすべてクリアされてしまう。

システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティに素早く追加する

  • 上記で一旦クリアしてしまった項目を、再びアプリケーションやアプレットでスクリプトを実行しながら追加するのは非常に面倒である。
  • 実は、システム環境設定 >> セキュリティとプライバシー >> プライバシー >> アクセシビリティへは、ドラッグ&ドロップ可能である。
  • システム環境設定 左下の鍵のロックを解除して、アプリケーションのアイコンをドラッグすれば、チェックありの状態で追加できる。

キー操作はGUIスクリプティングとして制限されない

  • 以前からそうなっていたように、キーボードの操作についてはGUIスクリプティングの許可は不要である。
  • 例えば、以下のようなアプレットを作成して、ダブルクリックで実行すると、許可なしで正常に実行される。

 tell application "System Events"
   tell process "AppleScript Editor"
     set frontmost to true
     keystroke "L" using {command down, shift down}
   end tell
 end tell

2013-10-24

必ず見つかるSpotlightにしておく

OSX 10.8 Mountain Lionでは、Spotlightで見つけられないキーワードがあるらしい。具体的には、「じじい」とか「シンガポール」とか「ノーライフキング」など。

OSX 10.8.5 Mountain Lion の Spotlight

本当なのか?実験してみた。

  • ターミナルで、以下のファイルを作成してみた。(コピー&ペーストで実行)
cd ~/Documents
> じじい.txt
> シンガポール.txt
> ノーライフキング.txt

  • テキストエディット.appで、以下の新規テキスト書類(ファイル名:spotlight_test.txt)を作ってみた。
  • 同様にメモ.appで、以下の新規メモを作ってみた。
スポットライトのテスト

じじい
シンガポール
ノーライフキング

  • Spotlightで検索してみると...
現実の結果期待する結果
f:id:zariganitosh:20131023135427p:image:w225NG: じじい.txt
NG: spotlight_test.txt
NG: スポットライトのテスト
f:id:zariganitosh:20131023135429p:image:w225NG: シンガポール.txt
NG: spotlight_test.txt
NG: スポットライトのテスト
f:id:zariganitosh:20131023135428p:image:w225OK: ノーライフキング.txt
NG: spotlight_test.txt
NG: スポットライトのテスト

や、や、や!

  • ことごとく検索をミスっている。唯一「ノーライフキング.txt」というファイル名のみはヒットしているが、それ以外はすべてノーマッチ。
  • 「じじい」と「ノーライフキング」については、Webやメールの履歴がヒットしているが、「シンガポール」では完全なノーマッチ。

これは由々しき問題である!

  • 本来ヒットするはずの項目が、検索結果に上がって来ない...。
  • これほど明白な検索漏れがあると、Spotlightの信頼性が、がた落ちである。
  • SpotlightとTimeMachineの存在こそが、NeXTに始まるOSXが目指したOSの完成形だったはずなのに。
  • 検索漏れのある検索エンジンなんて、計算間違いするCPUみたいなもの。

何とかしなければならない!

OSX 10.6.8 Snow Leopard の Spotlight

  • ちなみに、Spotlightの名誉のために、OSX 10.6.8 Snow Leopardで全く同じ条件で検索してみると...
現実の結果期待する結果
f:id:zariganitosh:20131023135431p:image:w225OK: じじい.txt
OK: spotlight_test.txt
OK: スポットライトのテスト
f:id:zariganitosh:20131023135430p:image:w225OK: シンガポール.txt
OK: spotlight_test.txt
OK: スポットライトのテスト
f:id:zariganitosh:20131023135432p:image:w225OK: ノーライフキング.txt
OK: spotlight_test.txt
OK: スポットライトのテスト
  • すべての検索で期待どおりにヒットした。本来のSpotlight性能は、こうゆうものだと思っている。

OSX 10.8 Mountain LionのSpotlightで、いったい何が起こっているのだろう?

消極的な回避策

以下の方法を覚えておけば、ある程度はまともに検索できるのだけど、あまり良い方法とは言えない...。

そもそもSpotlightとは、こんな小手先の検索技なんて知らなくても、思い浮かんだキーワードで素早く検索できることが売りのはず。また検索するなら、短いキーワードで広範囲にヒットして、そこから条件を追加して絞り込む方が使いやすい。少なくとも、Snow LeopardまでのSpotlightはそのような仕様になっていたはず。

  • ファイル名を検索する時は、「ファイル名:」あるいは「filename:」を付加して検索すると、ちゃんと見つかる。
ファイル名:シンガポール
filename:シンガポール

  • Finderのファイル検索でも、「ファイル名:」あるいは「filename:」を付加して検索すると、ちゃんと見つかる。
  • あるいは、検索条件で「ファイル名」を選択して、その右側に「検索テキスト」を入力すると、ちゃんと見つかる。

f:id:zariganitosh:20131023135433p:image:w450f:id:zariganitosh:20131023135434p:image:w450

  • うっかり「ファイル名 名前が一致:」を選択してしまうと、何もヒットしない...。(誘導されやすいので気をつける)

f:id:zariganitosh:20131023135436p:image:w450f:id:zariganitosh:20131023135435p:image:w450


  • Finderのファイル検索と、control-option-スペースのSpotlightのウィンドウ検索は、同じではない。
    • Finderのファイル検索では、OSやアプリケーションが管理するファイルを検索対象にしない。
      • ライブラリフォルダの中や、iTunesの音楽ファイル、iPhotoの写真ファイル、メールのファイルなど、検索対象にしない。
    • 一方、Spotlightのウィンドウ検索では、すべてのファイルが検索対象になる。
  • この違いを忘れて検索してしまうと、本来見つかるものも、見つからなくなってしまう...。

ファイル名の検索については上記対応でどうにかなるが、テキストファイルの内容検索については無理...。さらに探求する必要がある。

検索インデックスの検証

  • Snow LeopardとMountain Lionで、検索インデックスに違いがあるのか?検証してみた。
  • 空の外付けハードディスクを、まずSnow Leopardに接続する。
  • 上記実験で使った「spotlight_test.txt」をコピーした。
  • Snow LeopardのSpotlightで「スポットライトのテスト」を検索してみると、ちゃんと「spotlight_test.txt」がヒットした。
  • 次に、同じ外付けハードディスクをMountain Lionに接続するのだけど、
  • その前に以下のコマンドを実行して、検索インデックスの作成を一時停止しておく。
$ mdutil -i off
  • 外付けハードディスクを接続したら、Spotlightで「スポットライトのテスト」を検索してみる。何もヒットしない...。
  • 検索インデックスの作成をチェックするため、以下のコマンドを実行中にしておく。
$ sudo opensnoop -n mdworker
Password:
  UID    PID COMM          FD PATH                 
  • この状態で、検索インデックスの作成を再開する。
$ mdutil -i on
  • すると、先ほどの監視コマンドの出力に、テストファイルが見えた!
$ sudo opensnoop -n mdworker
Password:
  UID    PID COMM          FD PATH                 

...中略...

   89  15504 mdworker       4 /Volumes/名称未設定 10/spotlight_test.txt 
   89  15504 mdworker       5 /Volumes/名称未設定 10/ノーライフキング.txt 
   89  15504 mdworker       5 /Volumes/名称未設定 10/シンガポール.txt 
   89  15504 mdworker       5 /Volumes/名称未設定 10/じじい.txt 
   89  15504 mdworker      -1 /Volumes/.DS_Store   
   89  15504 mdworker       4 /Volumes/名称未設定 10/.DS_Store 
  • その後再び、Spotlightで「スポットライトのテスト」を検索してみる。今度は「spotlight_test.txt」がヒットした!

つまり、Mountain Lionは、Snow Leopardの検索インデックスをそのまま使っていない!

  • 上書きしてるか、別の検索インデックスを作っているはずである。
  • Spotlightの検索インデックスは、各ボリュームのルート直下の .Spotlight-V100 に格納されている。
  • そう思って、ls コマンドで探索してみると、二つの検索インデックス(と思われるファイル構造)を発見した!
  • こちらはSnow Leopardの検索インデックス。
$ ls -al /Volumes/名称未設定\ 10/.Spotlight-V100/Store-V1/Stores/D47DAAC7-5E5F-458E-B187-00E6348D1DB9
total 1560
drwx------  61 bebe  staff  -  2074 10 23 15:44 ./
drwx------   3 bebe  staff  -   102 10 21 10:03 ../
-rw-------   1 bebe  staff  - 53248 10 23 15:44 .store.db
-rw-------   1 bebe  staff  - 68484 10 21 10:03 0.indexArrays
-rw-------   1 bebe  staff  -     8 10 21 10:03 0.indexCompactDirectory
-rw-------   1 bebe  staff  -  2056 10 21 10:03 0.indexDirectory
-rw-------   1 bebe  staff  -  2731 10 21 10:14 0.indexGroups
-rw-------   1 bebe  staff  -  4096 10 21 10:14 0.indexHead
-rw-------   1 bebe  staff  - 32768 10 21 10:03 0.indexIds
-rw-------   1 bebe  staff  -    20 10 21 10:03 0.indexPositions
-rw-------   1 bebe  staff  -   752 10 21 10:03 0.indexPostings
-rw-------   1 bebe  staff  -     4 10 21 10:14 0.shadowIndexGroups
-rw-------   1 bebe  staff  -  4096 10 21 10:14 0.shadowIndexHead
-rw-------   1 bebe  staff  -    28 10 23 15:44 indexState
-rw-------   1 bebe  staff  -     0 10 21 10:03 journalExclusion
-rw-------   1 bebe  staff  -     0 10 23 15:42 journalLive
-rw-------   1 bebe  staff  -     0 10 23 15:42 journalSync
-rw-------   1 bebe  staff  - 68600 10 23 15:42 live.0.indexArrays
-rw-------   1 bebe  staff  -     8 10 23 15:42 live.0.indexCompactDirectory
-rw-------   1 bebe  staff  -  2056 10 23 15:42 live.0.indexDirectory
-rw-------   1 bebe  staff  -     6 10 23 15:42 live.0.indexGroups
-rw-------   1 bebe  staff  -  4096 10 23 15:42 live.0.indexHead
-rw-------   1 bebe  staff  -    64 10 23 15:42 live.0.indexIds
-rw-------   1 bebe  staff  -    18 10 23 15:42 live.0.indexPositions
-rw-------   1 bebe  staff  -   813 10 23 15:42 live.0.indexPostings
-rw-------   1 bebe  staff  -    10 10 23 15:42 live.0.indexUpdates
-rw-------   1 bebe  staff  -     6 10 23 15:42 live.0.shadowIndexGroups
-rw-------   1 bebe  staff  -  4096 10 23 15:42 live.0.shadowIndexHead
-rw-------   1 bebe  staff  - 66588 10 23 15:42 live.1.indexArrays
-rw-------   1 bebe  staff  -     8 10 23 15:42 live.1.indexCompactDirectory
-rw-------   1 bebe  staff  -  2056 10 23 15:42 live.1.indexDirectory
-rw-------   1 bebe  staff  -  2731 10 23 15:42 live.1.indexGroups
-rw-------   1 bebe  staff  -  4096 10 23 15:42 live.1.indexHead
-rw-------   1 bebe  staff  - 32768 10 23 15:42 live.1.indexIds
-rw-------   1 bebe  staff  -     4 10 23 15:42 live.1.indexPositions
-rw-------   1 bebe  staff  -   148 10 23 15:42 live.1.indexPostings
-rw-------   1 bebe  staff  -     2 10 23 15:42 live.1.shadowIndexGroups
-rw-------   1 bebe  staff  -  4096 10 23 15:42 live.1.shadowIndexHead
-rw-------   1 bebe  staff  - 65536 10 23 15:42 live.2.indexArrays
-rw-------   1 bebe  staff  -  1024 10 23 15:42 live.2.indexCompactDirectory
-rw-------   1 bebe  staff  -  8224 10 23 15:42 live.2.indexDirectory
-rw-------   1 bebe  staff  -  2731 10 23 15:42 live.2.indexGroups
-rw-------   1 bebe  staff  -  4096 10 23 15:42 live.2.indexHead
-rw-------   1 bebe  staff  - 32768 10 23 15:42 live.2.indexIds
-rw-------   1 bebe  staff  -  8192 10 23 15:42 live.2.indexPositionTable
-rw-------   1 bebe  staff  -  4096 10 23 15:42 live.2.indexPositions
-rw-------   1 bebe  staff  -  4096 10 23 15:42 live.2.indexPostings
-rw-------   1 bebe  staff  -  8192 10 23 15:42 live.2.indexTermIds
-rw-------   1 bebe  staff  -     0 10 23 15:42 live.2.indexUpdates
-rw-------   1 bebe  staff  - 65536 10 23 15:42 live.2.shadowIndexArrays
-rw-------   1 bebe  staff  -     8 10 23 15:42 live.2.shadowIndexCompactDirectory
-rw-------   1 bebe  staff  -  2056 10 23 15:42 live.2.shadowIndexDirectory
-rw-------   1 bebe  staff  -     1 10 23 15:42 live.2.shadowIndexGroups
-rw-------   1 bebe  staff  -  4096 10 23 15:42 live.2.shadowIndexHead
-rw-------   1 bebe  staff  -     0 10 23 15:42 live.2.shadowIndexPositionTable
-rw-------   1 bebe  staff  -     0 10 23 15:42 live.2.shadowIndexTermIds
-rw-------   1 bebe  staff  - 65620 10 23 15:44 permStore
-rw-r--r--   1 bebe  staff  -     4 10 23 15:44 shutdown_time
-rw-------   1 bebe  staff  - 53248 10 23 15:42 store.db
-rw-------   1 bebe  staff  -     6 10 23 15:44 store.updates
-rw-------   1 bebe  staff  -     0 10 21 10:03 tmp.SnowLeopard

  • こちらがMountain Lionの検索インデックス。
$ ls -al /Volumes/名称未設定\ 10/.Spotlight-V100/Store-V2/843F7ECA-ADE0-4063-ADF9-09412B12BDDE/
total 1624
drwx------  58 bebe  staff  -   1972 10 23 15:45 ./
drwx------   3 bebe  staff  -    102 10 23 15:36 ../
-rw-------   1 bebe  staff  - 118784 10 23 15:42 .store.db
-rw-------   1 bebe  staff  -  65536 10 23 15:45 0.directoryStoreFile
-rw-------   1 bebe  staff  -   1088 10 23 15:36 0.directoryStoreFile.shadow
-rw-------   1 bebe  staff  -   1792 10 23 15:36 0.indexArrays
-rw-------   1 bebe  staff  -      8 10 23 15:36 0.indexCompactDirectory
-rw-------   1 bebe  staff  -   2056 10 23 15:36 0.indexDirectory
-rw-------   1 bebe  staff  -   3277 10 23 15:36 0.indexGroups
-rw-------   1 bebe  staff  -   4096 10 23 15:36 0.indexHead
-rw-------   1 bebe  staff  -     48 10 23 15:36 0.indexIds
-rw-------   1 bebe  staff  -    976 10 23 15:36 0.indexPositionTable
-rw-------   1 bebe  staff  -   4096 10 23 15:36 0.indexPositions
-rw-------   1 bebe  staff  -   4096 10 23 15:36 0.indexPostings
-rw-------   1 bebe  staff  -    976 10 23 15:36 0.indexTermIds
-rw-------   1 bebe  staff  -     14 10 23 15:36 0.indexUpdates
-rw-------   1 bebe  staff  -      5 10 23 15:36 0.shadowIndexGroups
-rw-------   1 bebe  staff  -   4096 10 23 15:36 0.shadowIndexHead
-rw-------   1 bebe  staff  -      0 10 23 15:36 Lion.created
-rw-------   1 bebe  staff  -      0 10 23 15:45 Lion.modified
-rw-------   1 bebe  staff  -     28 10 23 15:36 indexState
-rw-------   1 bebe  staff  -      0 10 23 15:45 journalAttr.2
-rw-------   1 bebe  staff  -      0 10 23 15:36 journalExclusion
drwx------   2 bebe  staff  -     68 10 23 15:36 journals.live/
drwx------   2 bebe  staff  -     68 10 23 15:45 journals.repair/
drwx------   3 bebe  staff  -    102 10 23 15:36 journals.scan/
-rw-------   1 bebe  staff  -  65536 10 23 15:45 live.0.directoryStoreFile
-rw-------   1 bebe  staff  -   1088 10 23 15:36 live.0.directoryStoreFile.shadow
-rw-------   1 bebe  staff  -  65536 10 23 15:45 live.0.indexArrays
-rw-------   1 bebe  staff  -   1024 10 23 15:45 live.0.indexCompactDirectory
-rw-------   1 bebe  staff  -   8224 10 23 15:45 live.0.indexDirectory
-rw-------   1 bebe  staff  -   3277 10 23 15:45 live.0.indexGroups
-rw-------   1 bebe  staff  -   4096 10 23 15:36 live.0.indexHead
-rw-------   1 bebe  staff  -  32768 10 23 15:45 live.0.indexIds
-rw-------   1 bebe  staff  -   8192 10 23 15:45 live.0.indexPositionTable
-rw-------   1 bebe  staff  -   4096 10 23 15:45 live.0.indexPositions
-rw-------   1 bebe  staff  -   4096 10 23 15:45 live.0.indexPostings
-rw-------   1 bebe  staff  -   8192 10 23 15:45 live.0.indexTermIds
-rw-------   1 bebe  staff  -      0 10 23 15:36 live.0.indexUpdates
-rw-------   1 bebe  staff  -  65536 10 23 15:36 live.0.shadowIndexArrays
-rw-------   1 bebe  staff  -      8 10 23 15:36 live.0.shadowIndexCompactDirectory
-rw-------   1 bebe  staff  -   2056 10 23 15:36 live.0.shadowIndexDirectory
-rw-------   1 bebe  staff  -      1 10 23 15:36 live.0.shadowIndexGroups
-rw-------   1 bebe  staff  -   4096 10 23 15:36 live.0.shadowIndexHead
-rw-------   1 bebe  staff  -      0 10 23 15:36 live.0.shadowIndexPositionTable
-rw-------   1 bebe  staff  -      0 10 23 15:36 live.0.shadowIndexTermIds
-rw-------   1 bebe  staff  -  65621 10 23 15:45 permStore
-rw-------   1 bebe  staff  -  65536 10 23 15:45 reverseDirectoryStore
-rw-------   1 bebe  staff  -   3136 10 23 15:36 reverseDirectoryStore.shadow
-rw-------   1 bebe  staff  -      2 10 23 15:42 reverseStore.updates
-rw-r--r--   1 bebe  staff  -      4 10 23 15:42 shutdown_time
-rw-------   1 bebe  staff  - 118784 10 23 15:36 store.db
-rw-------   1 bebe  staff  -      8 10 23 15:42 store.updates
-rw-r--r--   1 bebe  staff  -      4 10 23 15:36 store_generation
-rw-------   1 bebe  staff  -      0 10 23 15:36 tmp.Lion
-rw-------   1 bebe  staff  -      0 10 23 15:36 tmp.SnowLeopard
-rw-------   1 bebe  staff  -   6608 10 23 15:36 tmp.spotlight.loc
-rw-------   1 bebe  staff  -   4096 10 23 15:42 tmp.spotlight.state
  • 中身はブラックボックスだけど、修正日時のタイミングから、二つの検索インデックスが使い分けられていることは確かだと思う。
    • Snow Leopardは、/.Spotlight-V100/Store-V1/...
    • Mountain Lionは、/.Spotlight-V100/Store-V2/...

Mountain Lionでは、検索インデックスのバージョンがV2となり、構造が変更されているようだ。

検索クエリ(検索条件)の検証

  • OSXには、Spotlightに対応するコマンドとして、mdfindが用意されている。
  • mdfindのマニュアルには、コントロール-スペースのSpotlightに対応する検索条件が解説されている。
$ man mdfind
...中略...
(* = search* cdw || kMDItemTextContent = search* cdw)
  • 例えば「シンガポール」を検索する場合、以下のmdfindコマンドを実行すると、Spotlightと同等になるのだ。
$ mdfind "* = 'シンガポール*'cdw || kMDItemTextContent = 'シンガポール*'cdw"
  • 上記コマンドを実行しても、Spotlightと同じく、さっぱり何もヒットしない...。
  • ところが、キーワードに続く cdw の d を外してみると、見事、かつてのSpotlightのようにヒットしてしまった!
    • -onlyinオプションを指定して、検索範囲を~/Documentsに限定している。
$ mdfind "* = 'シンガポール*'cw || kMDItemTextContent = 'シンガポール*'"cw -onlyin ~/Documents
/Users/bebe/Documents/シンガポール.txt
/Users/bebe/Documents/spotlight_test.txt
...中略...
cdwの意味
  • そもそも、cdwには何の意味があるのか?
c大文字・小文字を区別しない
dアクセント記号のあり・なしを区別しない
w小文字から大文字へ変化する部分も単語の区切りと見なす

参考ページ:(感謝です!)


  • なるほど、cdwは主にアルファベットの検索条件で役立ちそうな指定である。
  • しかし、なぜd(=アクセント記号のあり・なしを区別しない)指定が日本語の検索に影響してしまうのだろう?
  • 少なくとも、Snow Leopardまでは影響していない。
$ mdfind "* == 'シンガポール*'cdw || kMDItemTextContent == 'シンガポール*'cdw" -onlyin ~/Documents
/Users/bebe/Documents/spotlight_test.txt
/Users/bebe/Documents/シンガポール.txt
...中略...
メタデータの種類
$ mdfind "* == 'シンガポール*'cdw || kMDItemTextContent == 'シンガポール*'cdw" -onlyin ~/Documents
  • 先ほどの検索クエリで、「kMDItemTextContent == 'シンガポール*'cdw」の部分は、メタデータの種類を指定している。
  • kMDItemTextContentは、テキストファイルの内容である。
  • テキストの内容に「シンガポール」で始まる単語が含まれていたら、ヒットするのだ。
  • それでは「* == 'シンガポール*'cdw」の条件は何だろう?メタデータの「*」の部分が?
  • 通常「*」はすべての文字列にマッチすることを意味する。
  • 'シンガポール*'であれば、「シンガポール」で始まる単語すべてにマッチする。
    • 例:「シンガポールスリング」「シンガポール航空」など
  • ならばメタデータの「*」は、すべてのメタデータという意味になるのかもしれない。
  • でも、すべてのメタデータを指定しておきながら、その後でkMDItemTextContentを指定するのは重複するのではないか?
  • そう思って「* == 'シンガポール*'cw」のみで検索してみると... 「シンガポール.txt」しかヒットしなくなった。
$ mdfind "* == 'シンガポール*'cw" -onlyin ~/Documents
/Users/bebe/Documents/シンガポール.txt
  • 「kMDItemTextContent」も含めると、ちゃんとテキスト内容の「シンガポール」もヒットした。
$ mdfind "* == 'シンガポール*'cw || kMDItemTextContent == 'シンガポール*'cw" -onlyin ~/Documents
/Users/bebe/Documents/spotlight_test.txt
/Users/bebe/Documents/シンガポール.txt

$ cat /Users/bebe/Documents/spotlight_test.txt
スポットライトのテスト

じじい
シンガポール
ノーライフキング

参考ページ:(感謝です!)

Spotlightの使い方

  • 以上の検証から、検索インデックスのバージョンが変わったMountain Lionでも、検索クエリの「cdw」の部分を「cw」にすれば、かなり満足できる結果になりそうである。
  • しかし、Spotlightで検索クエリを指定することなんて、果たしてできるのだろうか?
  • Spotlightの基本的な使い方として「メタデータ名称:値」で検索条件を指定できることは知っている。
  • メタデータ名称には、kMDItem...で始まる正式な名称以外に、短縮名称も利用できる。日本語名称も用意されている。

			
入力語句意味
Time Capsule"Time"と"Capsule"の両方を含むファイルにマッチ。
"Time Capsule""Time Capsule"という一連の語句を含むファイルにマッチ。
Time -Capsule"Time"は含むが、"Capsule"を含まないファイルにマッチ。
Time OR Capsule"Time"か"Capsule"のどちらか一方を含むファイルにマッチ。
NOT(Time Capsule)"Time"も"Capsule"も含まないファイルにマッチ。
ファイル名:シンガポールファイル名に"シンガポール"を含むファイルにマッチ。
ファイル名:シンガポール 種類:textファイル名に"シンガポール"を含む、テキストファイルにマッチ。
ファイル名:シンガポール 日付:2013/10/23ファイル名に"シンガポール"を含む、2013/10/23のファイルにマッチ。
ファイル名:シンガポール 日付:>2013/10/23ファイル名に"シンガポール"を含む、2013/10/23以降のファイルにマッチ。
ファイル名:シンガポール 日付:<2013/10/23ファイル名に"シンガポール"を含む、2013/10/23以前のファイルにマッチ。
kMDItemPixelHeight:>3000
height:>3000
高さ:>3000
画像の高さが3000ピクセルより大きいファイルにマッチ。
  • 上記の方法でメタデータを指定することはできる。ところが、生成される検索クエリの詳細な指定(cdwの部分)まではできないのだ...。

参考ページ:(感謝です!)

独自の検索クエリーを指定する

  • Spotlightの入力部分で直接的に指定できないからと言って、諦めるのはまだ早い。
  • 試行錯誤しているうちに、独自の検索クエリを自由に指定する方法を思いついた。
  • まず、Spotlightで「シンガポール」を検索する。

f:id:zariganitosh:20131024101141p:image:w225

  • 相変わらず何もヒットしないのだけど、気にしないで「Finderにすべてを表示します」を選択する。

  • すると、何もヒットしてないウィンドウが表示されるので、検索条件を保存する。

f:id:zariganitosh:20131024101658p:image:w450


  • 「~/ライブラリ/保存済みの検索条件」を開くと、さっき保存した検索条件が見つかる。

f:id:zariganitosh:20131024101814p:image:w450


  • 選択して、command-Iで情報を見てみる。

f:id:zariganitosh:20131024101849p:image:h450

  • すると、クエリー:の項目にマニュアルどおりの条件が記載されている。
  • このクエリーを修正できれば、もっと自由なSpotlight検索ができるはず。

  • この検索条件をテキストエディットで開いてみる。
  • すると、その中身はplistであり、以下の内容が確認できた。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CompatibleVersion</key>
	<integer>1</integer>
	<key>RawQuery</key>
	<string>(true) &amp;&amp; (((* = "シンガポール*"cdw || kMDItemTextContent = "シンガポール*"cdw)))</string>
	<key>RawQueryDict</key>
	<dict>
		<key>FinderFilesOnly</key>
		<false/>
		<key>RawQuery</key>
		<string>(true) &amp;&amp; (((* = "シンガポール*"cdw || kMDItemTextContent = "シンガポール*"cdw)))</string>
		<key>SearchScopes</key>
		<array>
			<string>kMDQueryScopeComputer</string>
		</array>
		<key>UserFilesOnly</key>
		<true/>
	</dict>
...中略...
  • 先ほど見た検索クエリーも確認できる!
  • すかさずcdwの部分をすべてcwに修正。
  • 修正したら、忘れずに保存しておく。

  • そして、Finderから修正後の検索条件をダブルクリックして開いてみると...

f:id:zariganitosh:20131024102618p:image:w450


見事!「シンガポール」を含むあらゆるファイルがヒットしている!

「ノーライフキング」問題

  • 喜んだのもつかのま、「ノーライフキング」については、この検索クエリーの修正でもテキストファイルの内容がヒットしないことに気付いた。
    • ファイル名はヒットした。
  • なぜ「ノーライフキング」はヒットしないのか?
  • 「ノーライフキング」は小説(映画にもなった)のタイトルであるが、おそらく、この文字の並びに秘密がある。
  • 固有名詞なのだけど、「ノーライフキング」を知らない人が読んだら、「ノー」と「ライフ」と「キング」に分割して、必至に意味を探ろうと思うはず。
  • 英語などは単語区切りが明白だが、日本語の場合は先頭から文字を読んで、意味のある語句を認識しながら読み進める。
  • おそらく、Spotlightが検索インデックスを作成する時も同様に、意味のある語句で区切って、検索インデックスに登録するはずである。
  • その時、「ノーライフキング」は「ノー」と「ライフ」と「キング」に分かれて登録されているのかもしれない...。
  • 試しに、テキストファイルの内容を "ノーライフキング" のようにクォートしてみると、ちゃんとヒットした。
  • 少なくともSnow Leopard時代のSpotlightではちゃんとヒットしていたのに、Mountain Lionの検索インデックスはクォートしないとヒットしない、残念な仕様である。
  • 検索インデックスが違ってしまっているのだから、「ノーライフキング」に対しては、なす術がないのかと諦めかけていたが、一つ閃いた。
  • 検索クエリーを「ノー*ライフ*キング*」のように指定してみた。
$ mdfind 'kMDItemTextContent = "ノー*ライフ*キング*"' -onlyin ~/Documents
/Users/zari/Documents/spotlight_test.txt

すると、見事にヒット!

  • 但し、"ノー*ライフ*キング*"'の場合は、クォートされた「"ノーライフキング"」がヒットしない...。
  • よって、より一般的に利用するなら、"ノーライフキング*"'と"ノー*ライフ*キング*"'の両方を指定した方が良さそう。
  • ちなみに、"ノー*ライフ*キング*"'の検索クエリーは、テキストファイル中に「ノー」と「ライフ」と「キング」が順に出現すれば、ヒットしてしまう。
  • 例えば、「ノーと言えない日本人。ライフスタイルは欧米化している。キングコングが好き。」なんて内容でもヒットすることになる。
    • と、予想したのだけど、試してみたらヒットしなかった...。
    • 結果としては嬉しいのだけど、クエリーがマッチする挙動は読み切れてない。
  • しかし、「ノーライフキング」が全くヒットしないよりも、余分なファイルを含んでもヒットした方が嬉しいはず。(と思っている)

スクリプトにまとめる

  • 以上の技を駆使すれば、Mountain LionのSpotlightでも、かなり満足できる検索結果を得られそうだ。
  • 但し、毎回手作業で検索クエリーを修正するなんて言うのは、面倒くさすぎる...。
  • ここはもう、いつものAppleScriptでショートカット一発、素早く満足できる検索結果を手に入れたい。

やってみた。

  • メニューバーのSpotlightで検索して、出力される検索結果に満足できない時に、ショートカットから素早く漏れのない検索結果を出力するのだ。
Spotlight.scpt
  • ~/Library/Scripts/Spotlight.scpt として保存した。
  • 雛形のSpotlight.savedSearch.plistに検索語句を代入して、保存済みの検索条件を作成している。
  • このスクリプトは、メニューバーのSpotlightを利用中に、ショートカットで呼び出す必要がある。
  • よって、Quicksilverなどで、事前にショートカットを割り当てておく必要がある。
    • 自分の場合、control-option-スペースを割り当てた。
    • OSXデフォルトのcontrol-option-スペースは無効にした。

f:id:zariganitosh:20131024114221p:image:w337


 if spotlight_running() then
   expanded_spotlight()
 else
   normal_spotlight()
 end if
 
 
 
 on expanded_spotlight()
   tell application "System Events"
     delay 0.3
     keystroke "a" using {command down}
     delay 0.3
     keystroke "c" using {command down}
     delay 0.3
     keystroke space using {control down}
   end tell
   
   set key_word to the clipboard as text
   set ext_word to join(key_word's text items, "*")
   
   set savedSearch to (path to temporary items)'s POSIX path & "Spotlight.savedSearch"
   set savedSearch_plist to (path to scripts folder)'s POSIX path & "Spotlight.savedSearch.plist"
   
   do shell script "rm -f " & savedSearch
   do shell script "cat " & savedSearch_plist & " | sed s/__KEYWORD__/" & key_word & "/g | sed s/__CONTENT__/" & ext_word & "/g >> " & savedSearch
   do shell script "open " & savedSearch
   tell application "Finder" to activate
   delay 1
   zoom_finder_window1()
 end expanded_spotlight
 
 on normal_spotlight()
   tell application "System Events"
     keystroke space using {control down}
   end tell
 end normal_spotlight 
 
 
 
 --Spotlightが表示中かどうか
 on spotlight_running()
   tell application "System Events"
     tell process "SystemUIServer"
       menu bar 2's menu bar item 1's selected
     end tell
   end tell
 end spotlight_running
 
 --Finderのアクティブなウィンドウをズームする
 on zoom_finder_window1()
   tell application "System Events"
     tell process "Finder"
       click window 1's button 2
     end tell
   end tell
 end zoom_finder_window1
 
 on join(src_list, delimiter)
   set last_delimiter to AppleScript's text item delimiters
   set AppleScript's text item delimiters to delimiter
   set res to src_list as text
   set AppleScript's text item delimiters to last_delimiter
   res
 end join

  • コーディングするにあたり、「ノーライフキング」から「ノー*ライフ*キング*」をどうやって導き出すか?という問題にぶつかった。
  • 人間なら、自分の知識から簡単に「ノー*ライフ*キング*」を導ける。
  • しかし、コンピュータに同じことさせるのは、非常に難しい。
  • ちゃんとやるなら、MeCabなどの形態素解析エンジンを導入して、分かち書きを求める必要がある。
  • しかし、そこまでやるのは面倒だし、インストールの手間もかかる...。
  • 今回の目的は、正しい単語区切りをすることではなく、検索漏れのないSpotlightにすることである。
  • しばし考え、それはちょっとやり過ぎ感もあると思いながら、決断した。

もう単純に「ノ*ー*ラ*イ*フ*キ*ン*グ*」にしてしまえば、いいやと。

  • よって、このスクリプトは、テキストファイル中に検索語の一文字ずつが、順に出現する内容でもヒットしてしまう...。
  • 短い単語だと、この仕様によって、膨大な検索結果がヒットしてしまう。
    • どうして検索語が含まれていないのにヒットするのか悩んだら、おそらくこの原因。
  • しかし、長い単語なら、ほとんど気にならないレベルで正しい検索結果となりそう。
Spotlight.savedSearch.plist
  • このxmlファイルは、保存済みの検索条件の雛形となる。
  • ~/Library/Scripts/Spotlight.savedSearch.plist として保存しておく。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CompatibleVersion</key>
	<integer>1</integer>
	<key>RawQuery</key>
	
	<string>(true) &amp;&amp; (((* = "__KEYWORD__*"cw || kMDItemTextContent = "__KEYWORD__*"cw || kMDItemTextContent = "__CONTENT__*"cw)))</string>
	<key>RawQueryDict</key>
	<dict>
		<key>FinderFilesOnly</key>
		<false/>
		<key>RawQuery</key>
  	
		<string>(true) &amp;&amp; (((* = "__KEYWORD__*"cw || kMDItemTextContent = "__KEYWORD__*"cw || kMDItemTextContent = "__CONTENT__*"cw)))</string>
		<key>SearchScopes</key>
		<array>
			<string>kMDQueryScopeComputer</string>
		</array>
		<key>UserFilesOnly</key>
		<true/>
	</dict>
	<key>SearchCriteria</key>
	<dict>
		<key>FXCriteriaSlices</key>
		<array>
			<dict>
				<key>criteria</key>
				<array>
					<string>com_apple_UserSearchStringAttribute</string>
					<integer>104</integer>
				</array>
				<key>displayValues</key>
				<array>
					<string>Items matching text</string>
					<string>__KEYWORD__</string>
				</array>
				<key>rowType</key>
				<integer>0</integer>
				<key>subrows</key>
				<array/>
			</dict>
		</array>
		<key>FXScope</key>
		<integer>1396925814</integer>
		<key>FXScopeArrayOfPaths</key>
		<array>
			<string>kMDQueryScopeComputer</string>
		</array>
	</dict>
	<key>SuggestedAttributes</key>
	<array/>
  
	<key>ViewSettings</key>
	<dict>
		<key>ExtendedListViewSettings</key>
		<dict>
			<key>calculateAllSizes</key>
			<false/>
			<key>columns</key>
			<array>
				<dict>
					<key>ascending</key>
					<true/>
					<key>identifier</key>
					<string>name</string>
					<key>visible</key>
					<true/>
					<key>width</key>
					<integer>521</integer>
				</dict>
				<dict>
					<key>ascending</key>
					<false/>
					<key>identifier</key>
					<string>dateModified</string>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>181</integer>
				</dict>
				<dict>
					<key>ascending</key>
					<false/>
					<key>identifier</key>
					<string>dateCreated</string>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>181</integer>
				</dict>
				<dict>
					<key>ascending</key>
					<false/>
					<key>identifier</key>
					<string>size</string>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>97</integer>
				</dict>
				<dict>
					<key>ascending</key>
					<true/>
					<key>identifier</key>
					<string>kind</string>
					<key>visible</key>
					<true/>
					<key>width</key>
					<integer>156</integer>
				</dict>
				<dict>
					<key>ascending</key>
					<true/>
					<key>identifier</key>
					<string>label</string>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>100</integer>
				</dict>
				<dict>
					<key>ascending</key>
					<true/>
					<key>identifier</key>
					<string>version</string>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>75</integer>
				</dict>
				<dict>
					<key>ascending</key>
					<true/>
					<key>identifier</key>
					<string>comments</string>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>300</integer>
				</dict>
				<dict>
					<key>ascending</key>
					<false/>
					<key>identifier</key>
					<string>dateLastOpened</string>
					<key>visible</key>
					<true/>
					<key>width</key>
					<integer>141</integer>
				</dict>
				<dict>
					<key>ascending</key>
					<false/>
					<key>identifier</key>
					<string>dateAdded</string>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>181</integer>
				</dict>
			</array>
			<key>iconSize</key>
			<real>16</real>
			<key>showIconPreview</key>
			<true/>
			<key>sortColumn</key>
			<string>dateLastOpened</string>
			<key>textSize</key>
			<real>12</real>
			<key>useRelativeDates</key>
			<true/>
			<key>viewOptionsVersion</key>
			<integer>1</integer>
		</dict>
		<key>ListViewSettings</key>
		<dict>
			<key>calculateAllSizes</key>
			<false/>
			<key>columns</key>
			<dict>
				<key>comments</key>
				<dict>
					<key>ascending</key>
					<true/>
					<key>index</key>
					<integer>7</integer>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>300</integer>
				</dict>
				<key>dateCreated</key>
				<dict>
					<key>ascending</key>
					<false/>
					<key>index</key>
					<integer>2</integer>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>181</integer>
				</dict>
				<key>dateLastOpened</key>
				<dict>
					<key>ascending</key>
					<false/>
					<key>index</key>
					<integer>8</integer>
					<key>visible</key>
					<true/>
					<key>width</key>
					<integer>141</integer>
				</dict>
				<key>dateModified</key>
				<dict>
					<key>ascending</key>
					<false/>
					<key>index</key>
					<integer>1</integer>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>181</integer>
				</dict>
				<key>kind</key>
				<dict>
					<key>ascending</key>
					<true/>
					<key>index</key>
					<integer>4</integer>
					<key>visible</key>
					<true/>
					<key>width</key>
					<integer>156</integer>
				</dict>
				<key>label</key>
				<dict>
					<key>ascending</key>
					<true/>
					<key>index</key>
					<integer>5</integer>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>100</integer>
				</dict>
				<key>name</key>
				<dict>
					<key>ascending</key>
					<true/>
					<key>index</key>
					<integer>0</integer>
					<key>visible</key>
					<true/>
					<key>width</key>
					<integer>521</integer>
				</dict>
				<key>size</key>
				<dict>
					<key>ascending</key>
					<false/>
					<key>index</key>
					<integer>3</integer>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>97</integer>
				</dict>
				<key>version</key>
				<dict>
					<key>ascending</key>
					<true/>
					<key>index</key>
					<integer>6</integer>
					<key>visible</key>
					<false/>
					<key>width</key>
					<integer>75</integer>
				</dict>
			</dict>
			<key>iconSize</key>
			<real>16</real>
			<key>showIconPreview</key>
			<true/>
			<key>sortColumn</key>
			<string>dateLastOpened</string>
			<key>textSize</key>
			<real>12</real>
			<key>useRelativeDates</key>
			<true/>
			<key>viewOptionsVersion</key>
			<integer>1</integer>
		</dict>
		<key>WindowState</key>
		<dict>
			<key>ShowPathbar</key>
			<true/>
			<key>ShowSidebar</key>
			<false/>
			<key>ShowStatusBar</key>
			<true/>
			<key>ShowToolbar</key>
			<true/>
			<key>SidebarWidth</key>
			<integer>161</integer>
			<key>WindowBounds</key>
			<string>{{9999, 9999}, {834, 425}}</string>
		</dict>
	</dict>
  
</dict>
</plist>

以上で、control-option-スペースで素早くSnow Leopard時代の検索性能を手に入れられる!

  • 奇しくも、この日記を書いている途中で、OS X 10.9 Mavericksの配布が開始された。
  • MavericksのSpotlightは、果たして「じじい」「シンガポール」「ノーライフキング」を漏れなく検索できるのか?

OSX 10.9 MavericksのSpotlightで解消された!


アップルが解説するSpotlight

2013-09-09

書いてる途中のはてなダイアリーを絶対に失わない環境を求めて

書いたもの、さいごの行で消えた(;_;)

書き直す気力がないー。

パッと消えたちゃった瞬間に、右クリックの「元に戻す」で出てこないのはなぜー?

はてなのバックアップも「0秒前のバックアップ」とか出る。

えー、ぜんぜんバックアップじゃないじゃーん。

せめて30秒ぐらい残ってて(;_;)

消えたしキレたし - ひきこもり女子いろいろえっち

自分も、過去何度もガックリ...したことがあった。(OSX 10.6 Snow Leopardの頃)

特に、比較的長い記事を数時間に渡って一気に仕上げようと集中して作業した結果の、保存ボタン押した瞬間のSafariフリーズとか、うっかりページを戻す操作をしてしまったり、その時はもう立ち直れないくらいのショックを受ける。

結局、消えたものはもう一度書き直すしかないのだけど...。決して同じ内容にはならない。書く気力さえみなぎってくれば、きっと前回よりもいいものに仕上げてみせるさ!と思えるのだけど、とりあえずその直後は書く気なくなるのは確か。

過去の失った経験

  • 突然、Safariが強制終了した。急いで再起動するも、無情にも真っ白なページしか表示されない...。ガックリ。
    • こまめに保存するように心掛けるようになった。
  • 保存ボタンを押した瞬間に、いくら待ってもページが遷移せず、何を押してもそのページでは反応しなくなった。ページのフリーズか?ガックリ。
    • 一時、保存ボタン恐怖症になった。ページ遷移せずフリーズして消えてしまうのではないか?という心配から、必ずcommand-Cでコピーしてから保存ボタンを押すようになった。
  • うっかりページを移動する操作をしてしまい、急いでページを戻してみるが、表示されるのは真っ白な入力ページ。ガックリ。
  • うっかりタブを閉じてしまい、急いで取り消し操作をするが、表示されるのは真っ白な入力ページ。ガックリ。
  • 入力したテキスト全体を選択した状態で、意図しない入力をして、気が付いた時には内容が消えていた。ガックリ。
    • 保存ボタン恐怖症でテキスト全体をコピーする習慣の副作用かもしれない。コピー操作の後は、すぐにテキスト全体の選択を解除するように心掛けた。

過去の対策

  • Safariのタブ履歴を保存するSIMBLプラグインをインストールした。
    • cyan-stivy.net - SafariTabMemento
    • SafariTabMementoは閉じたタブを記憶しておき、再度開く事ができる。もちろん「戻る」「進む」履歴も同時に復元してくれる。素晴らしい!

最近のブラウザの性能

いろいろ試行錯誤したけど、以上の対策は過去のもの。最近のOSX 10.8 Mountain Lion環境のSafariにおいては、滅多なことでは書いてる途中の内容を失うことはなくなったと思う。実際、Mountain Lionに移行してからのこの一年、書いてる途中の内容を失った苦い記憶はない。最近のSafariは、機能拡張やプラグインなしでも以下の機能を備えているようだ。

  • うっかりページを移動してしまった時でも、ページを戻る操作で入力中の内容も復元される。
  • うっかりページを戻ってしまった時は、ページを進む操作で入力中の内容も復元される。
  • うっかりタブを閉じてしまった時でも、command-Zで直前に閉じたタブが復活する。書いてる途中のテキストも復活!
  • うっかりウィンドウを閉じてしまった時でも、履歴>>最後に閉じたウィンドウを開く、で直前に閉じたウィンドウが復活する。書いてる途中のテキストも復活!
    • 仮に入力中の内容を失う可能性がある場合には、警告された。

f:id:zariganitosh:20130906173628p:image:w380

f:id:zariganitosh:20130906173629p:image:w380

  • 但し、復元されずに入力中の内容が消えてしまう場合もあった。
  • どのような条件で失うのか、不明。(時間的なタイミングか?)

自動バックアップ

  • 素晴らしいことに、記事を書くページには自動バックアップ機能がある。

f:id:zariganitosh:20130906175944p:image:w450

  • 103秒前、2時間16分前、11時間13分前、等のリンクをクリックして過去の履歴から復元することができるのだ。
  • しかし、ミカサさん曰く「0秒前のバックアップ、とか出る。」うまく復元できなかった模様。
  • 自分も経験ある。復元したい時に限って、欲しいバックアップが見当たらないことがある。
  • これはバックアップの容量の問題かもしれないと思い、保存容量設定で10MBにしておいた。(ちなみに初期設定は100KBかも)

f:id:zariganitosh:20130906180047p:image:h309

  • なるほど、はてなの自動バックアップは、Adobe Flash Playerの機能に依存していたのか。

  • 自動バックアップがちゃんと機能してくれたら、これは相当、頼りになるはず。
  • しかし、下書きを書くページには、なぜか自動バックアップ機能は存在しない。
  • 不安になる...。
  • そんな時は、強引に下書きを書くページのタイトルと内容をコピーして、
  • 記事を書くページにペーストして、そこで作業を続けるという技もある。
  • しかし毎回、タイトルと内容の2カ所をコピーするのは面倒でもある。
  • いつしかそんな面倒なことはせず、普通に下書きを書くページで作業するようになってしまった。

「下書きを書く」から「記事を書く」へ移行するAppleScript

  • ならば、いつものAppleScriptを使って、ショートカット一発で下書きを書くから記事を書くに移行してみよう!と思いついた。こんな感じ。

 set blog_title to section_title_value()
 set blog_text to textarea_edit_value()
 open_url("http://d.hatena.ne.jp/" & user_name() & "/edit")
 
 delay 1
 
 "*" & blog_title & "
 " & blog_text
 
 replace(result, "\\", "\\\\")
 replace(result, "
 ", "\\n")
 replace(result, "\"", "\\\"")
 
 write_textarea_edit(result)
 
 
 
 
 on user_name()
   tell application "Safari"
     do JavaScript "
 url = window.location.href;
 url.replace(/http:\\/\\/d\\.hatena\\.ne\\.jp\\/(.{1,})\\/draft\\/?.+/, '$1');
 " in document 1
   end tell
 end user_name
 
 on section_title_value()
   tell application "Safari"
     do JavaScript "
 editText = document.getElementById('section-title').value;
 " in document 1
   end tell
 end section_title_value
 
 on textarea_edit_value()
   tell application "Safari"
     do JavaScript "
 editText = document.getElementById('textarea-edit').value;
 " in document 1
   end tell
 end textarea_edit_value
 
 on open_url(URL_text)
   tell application "Safari"
     do JavaScript "
 window.location.href = '" & URL_text & "';
 " in document 1
   end tell
 end open_url
 
 on write_textarea_edit(str)
   tell application "Safari"
     do JavaScript "
 var el = document.getElementById('textarea-edit');
 el.innerText = \"" & str & "\";
 " in document 1
   end tell
 end write_textarea_edit
 
 --テキストをリストに変換(区切り文字で分割する)
 on split(src_text, delimiter)
   set last_delimiter to AppleScript's text item delimiters
   set AppleScript's text item delimiters to delimiter
   set res to src_text's text items
   set AppleScript's text item delimiters to last_delimiter
   res
 end split
 
 --リストをテキストに変換(区切り文字で接続する)
 on join(src_list, delimiter)
   set last_delimiter to AppleScript's text item delimiters
   set AppleScript's text item delimiters to delimiter
   set res to src_list as text
   set AppleScript's text item delimiters to last_delimiter
   res
 end join
 
 --テキスト置き換え
 on replace(sourceText, text1, text2)
   join(split(sourceText, text1), text2)
 end replace

  • 本来は下書きを書くページにも自動バックアップ機能が備わっていれば、こんなスクリプトは不要になるのだけど...。

自動バックアップの有効・無効

f:id:zariganitosh:20130907151522p:image:w450

OSXのバージョンと再開

  • OSX 10.7 Lion以降には、OS自体がバージョン管理をサポートした。(その機能を「バージョン」と呼ぶ)
  • バージョンに対応したアプリケーションは、保存された過去の内容をブラウズして、好みの内容を復元できる。
  • しかし、差分やコメント機能もないし、戻るべき過去を見失ってしまうお粗末なバージョン管理と思っていた。
  • ところが、OSXのもう一つの新機能「再開」と組み合わされることで、これは信頼性の高い強力なバックアップ環境になることに気付いた。
  • 再開とは...
    • アプリケーションを終了した瞬間の書類ウィンドウの状態を保存しておき、
    • 次にアプリケーションを起動した時、終了時の状態を復元してくれる機能。
  • 例えば、OSX標準のテキストエディット.appは、バージョンと再開に対応している。
  • よってテキストエディットで新規書類を作成して、何かテキストを入力しておけば...
  • たとえ保存していなくても、たとえ強制終了してしまっても、その内容は、確実に保持される。
  • さらにバージョン管理もされるので、過去の内容も追跡できる。
  • これなら気付かないうちに一部を削除してしまったとか、意図せず書き換えてしまった場合にも、救われる可能性があるのだ。
  • つまりバージョンと再開は、入力した内容を決して失うことなく、徹底的に保持する仕組みを提供してくれているのだ。
  • テキストエディット.appは、書いた内容を徹底的に保持する、頼もしいエディタとなる。
  • これはもう、はてなダイアリーの下書きにテキストエディット.appを使わない手はない。

それをサポートするスクリプトを作ってみた。

テキストエディットの内容をはてなダイアリーでプレビュー

  • はてな記法で書いたテキストは、頻繁にプレビューしたくなる。
  • しかし、テキストエディットに、そのプレビュー機能はない...。
  • プレビューするには、テキストエディットの内容を、はてなの下書き編集ページにコピーすることになりそう。(それぐらいしか思いつかない)
  • しかし、頻繁にプレビューするのにコピー&ペーストを繰り返すのは面倒である。きっと、そのうちテキストエディットなんか使わなくなる。
  • そこで、テキストエディット.appの内容を、はてなの下書き編集ページにコピーして、素早くプレビューしてしまうスクリプトを作ってみた。
使い方
  • スクリプト実行時にアクティブなテキストエディット書類の内容を、はてなの下書き編集ページにコピーして、プレビューする。
  • もしものことを考えて、すでに存在する下書き編集ページの内容は、クリップボードにバックアップしてから、実行している。
  • はてなの下書き編集ページで作業している時に誤ってこのスクリプトで内容を書き換えてしまっても、クリップボードのペーストで復元できるのだ。
  • このスクリプトにcommand-option-control-Hを割り当てた。
  • これでテキストエディットの内容はショートカット一発で素早くプレビューされるのだ!
  • ダウンロードしたら、1行目のproperty USERNAME:のXXXXXXの部分を、はてなダイアリーのユーザー名に変更する必要あり。
    • 例:XXXXXX → zariganitosh
スクリプトコード

property USERNAME : "XXXXXX" --はてなダイアリーのユーザー名に変更する
 property DRAFTURL : "http://d.hatena.ne.jp/" & USERNAME & "/draft"
 
 if frontmost_app() ≠ "TextEdit" then
   "編集中のテキストエディット書類を
 アクティブにして、実行してください。"
   display dialog result with icon 1 giving up after 10
   tell application "TextEdit" to activate
   return
 end if
 
 if find_tab(DRAFTURL) then
   set blog_backup to textarea_edit_value()
   write_textarea_edit(editor_text())
   set the clipboard to blog_backup
 else
   open_url(DRAFTURL)
   write_textarea_edit(editor_text())
 end if
 click_preview_tab()
 
 
 
 
 on find_tab(an_url)
   tell application "Safari"
     set winum to 0
     set all_window_urls to windows's tab's URL as list
     repeat with window_urls in all_window_urls
       set winum to winum + 1
       set tabnum to 0
       repeat with an_url in window_urls
         set tabnum to tabnum + 1
         if (an_url as text) = DRAFTURL then
           tell window winum
             set current tab to tab tabnum
             return true
           end tell
         end if
       end repeat
     end repeat
   end tell
   false
 end find_tab
 
 on editor_text()
   tell application "TextEdit"
     document 1's text
   end tell
 end editor_text
 
 on section_title_value()
   tell application "Safari"
     do JavaScript "
 editText = document.getElementById('section-title').value;
 " in document 1
   end tell
 end section_title_value
 
 on textarea_edit_value()
   tell application "Safari"
     do JavaScript "
 editText = document.getElementById('textarea-edit').value;
 " in document 1
   end tell
 end textarea_edit_value
 
 on open_url(URL_text)
   tell application "Safari"
     activate
     open location URL_text
   end tell
   
   repeat 10 times
     delay 0.5
     if textarea_edit_value() ≠ missing value then return true
   end repeat
   false
 end open_url
 
 on write_textarea_edit(str)
   click_edit_tab()
   set the clipboard to str
   
   tell application "Safari"
     do JavaScript "
 document.getElementById('textarea-edit').select();
 " in document 1
   end tell
   
   tell application "System Events"
     tell process "Safari"
       keystroke "v" using {command down}
       delay 0.5
     end tell
   end tell
 end write_textarea_edit
 
 on click_preview_tab()
   tell application "Safari"
     activate
     do JavaScript "
 document.getElementById('preview-tab').click();
 " in document 1
   end tell
 end click_preview_tab
 
 on click_edit_tab()
   tell application "Safari"
     activate
     do JavaScript "
 document.getElementById('edit-tab').click();
 " in document 1
   end tell
 end click_edit_tab
 
 on frontmost_app()
   tell application "System Events"
     (processes whose frontmost is true)'s item 1's name
   end tell
 end frontmost_app

下書き編集ページでバックアップしてからプレビュー

  • たとえテキストエディットの内容をプレビューできるようになったとしても、はてな記法を使う限り、下書き編集ページで入力したい時がある。
  • 写真の挿入や色の設定等は、やはり下書き編集ページのツールバーのアイコンがとっても便利なので。
  • そうゆう訳で、テキストエディットで作業していたはずなのに、気付くといつの間にか下書き編集ページで入力していたなんてことがよくある。
  • ならば素直に下書き編集ページで作業して、テキストエディットはバックアップ用途にしてしまうのも良さそう。
使い方
  • 以下のスクリプトは、下書き編集ページの内容をテキストエディット書類(hatena_diary_draft_backup.txt)に書き出し、その後プレビューする。
  • このスクリプトにcommand-option-control-Pを割り当てた。
  • これで下書き編集ページの内容はショートカット一発で素早くプレビューされるのだ!
  • プレビューの度にテキストエディットへのバックアップも実行されるので安心である!
  • テキストエディット書類(hatena_diary_draft_backup.txt)では、バージョンのブラウズや取り消し(command-Z)操作で、過去の履歴を自由に復元できる。
  • 下書き編集ページに自動バックアップ機能はないけど、このショートカットcommand-option-control-Pでプレビューした内容は、確実に保持されることになる。
スクリプトコード

property BACKUPNAME : "hatena_diary_draft_backup.txt"
 
 if textarea_edit_value() = missing value then
   "はてな下書き編集ページを開いた状態で
 実行してください。"
   display dialog result with icon 1 giving up after 10
   return
 end if
 
 if section_title_value() = "" then
   set blog_data to textarea_edit_value()
 else
   set blog_data to "*" & section_title_value() & "
 " & textarea_edit_value()
 end if
 
 tell application "TextEdit"
   if exists document BACKUPNAME then
     set document BACKUPNAME's text to blog_data
   else
     make new document with properties {name:BACKUPNAME, text:blog_data}
   end if
 end tell
 click_preview_tab()
 
 
 
 
 on section_title_value()
   tell application "Safari"
     do JavaScript "
 editText = document.getElementById('section-title').value;
 " in document 1
   end tell
 end section_title_value
 
 on textarea_edit_value()
   tell application "Safari"
     do JavaScript "
 editText = document.getElementById('textarea-edit').value;
 " in document 1
   end tell
 end textarea_edit_value
 
 on click_preview_tab()
   tell application "Safari"
     activate
     do JavaScript "
 document.getElementById('preview-tab').click();
 " in document 1
   end tell
 end click_preview_tab
 
 on click_edit_tab()
   tell application "Safari"
     activate
     do JavaScript "
 document.getElementById('edit-tab').click();
 " in document 1
   end tell
 end click_edit_tab


しばらくこれらのスクリプトでテキストエディットと連携させながら書いてみる予定。

作業環境

  • MacBook Pro Retina15
  • OSX 10.8.4
  • はてなダイアリー(はてなブログではない)

2013-06-05

いままでAmazonでいくら買い物してきたのか?

Amazonには注文履歴というページがあって、そこでは過去の発注を一覧できる。

ところが、商品ごとの金額は分かるのだけど、合計でいくら使ったのかを知りたかったら、ちまちまと手作業で計算するしかなかった...。

手作業とか大嫌い。面倒だから、今までいくら使ってきたのかなんて計算したこともなかった。そんな時、いままでAmazonでいくら買い物してきたのか計算するブックマークレットに出会った!(素晴らしい!)

こうゆうの大好き!即、試したくなる。さっそく、やってみた。

合計してみた

  • 求められたら、サインイン。
  • 注文履歴のURLにブックマークレットとなるJavaScriptをコピー&ペーストして、実行!(return)
javascript:(function(){var d=document;var s=d.createElement('script');s.src='https://gist.github.com/Molokheiya/5698262/raw/e172fe6f8e1d58021da35dc681cf4f4429b940e6/aitter.js'; d.body.appendChild(s)})();
いままでいくらAmazonで買い物したか合計するブックマークレット書いた - モロ屋

f:id:zariganitosh:20130605134606p:image:w450

  • 西暦何年分の集計をするか訊かれた。

f:id:zariganitosh:20130605134601p:image:w450

  • デフォルトの2012年のまま実行してみると...集計が始まった!

f:id:zariganitosh:20130605134605p:image:w450

  • なるほど、けっこう使ってる。

f:id:zariganitosh:20130605134604p:image:w450

すべての年の合計を知りたい

  • おもしろい、おもしろい!
  • じゃあ、2011年はどうだろう?、2010年は?...
  • そんなふうに繰り返して、各西暦の合計を調べた。
  • どうやら自分のAmazon歴は2006年からのようだ。

f:id:zariganitosh:20130605134607p:image:w234

  • しかし、Amazon歴すべての合計金額を求めようとすると、やっぱり手作業で足し算しなくてはならない...。
  • たかだか8年分なので、電卓使えばすぐできるのだけど、やっぱり面倒だ。
  • 各西暦の総合計も計算できないだろうかと、gistのソースコードを弄ってみたけど、どうもうまく動かない。
  • JavaScriptに慣れていないので、時間がかかりそう。
  • ならば、慣れ親しんだAppleScriptで手作業を再現して、強引にAmazon歴のすべての西暦を合計してみる!

AppleScript

--Safariを起動して、Amazon履歴の西暦リストを求める
 tell application "Safari"
   activate
   make new document with properties {URL:"https://www.amazon.co.jp/gp/css/order-history/"}
   repeat until (document 1's URL as text) contains "https://www.amazon.co.jp/gp/css/order-history"
     delay 1
   end repeat
   delay 1
   do JavaScript "var e=document.getElementById('orderFilter'); filters=e.getElementsByTagName('option'); t=''; for(var i=0; i<filters.length; i++){t=t+filters[i].text};t" in document 1
 end tell
 set history_years to split(result, {"年分の注文", "年の注文"})'s items 2 thru -2
 
 --各西暦ごとに集計する
 set msgs to {}
 repeat with y in history_years
   tell application "Safari"
     activate
     do JavaScript "javascript:(function(){var d=document;var s=d.createElement('script');s.src='https://gist.github.com/Molokheiya/5698262/raw/e172fe6f8e1d58021da35dc681cf4f4429b940e6/aitter.js’; d.body.appendChild(s)})();" in document 1
   end tell
   
   repeat 30 times
     delay 1
     if safari_dialog_exists() then exit repeat
   end repeat
   
   tell application "System Events"
     tell process "Safari"
       keystroke (y as text)
       delay 1
       keystroke return
     end tell
   end tell
   
   repeat 20 times
     delay 1
     if safari_dialog_exists() then exit repeat
   end repeat
   
   tell application "System Events"
     tell process "Safari"
       set msgs's end to window 1's static text 2's name & return
       delay 1
       keystroke return
     end tell
   end tell
 end repeat
 tell application "Safari" to close document 1
 
 --集計結果をテキストエディットに出力する
 tell application "TextEdit"
   activate
   my table_format(msgs)
   result & "========================" & return
   result & tab & " 合計 " & tab & my total_price(msgs)
   make new document with properties {text:result}
   activate
 end tell
 
 
 
 
 on total_price(msg_list)
   set total to 0
   set prices to split(join(msg_list, ""), {"、合計", "円分の買い物をAmazonでしました!"})
   repeat with i in prices
     try
       set total to total + (i as number)
     end try
   end repeat
   number_with_delimiter(total) & "円"
 end total_price
 
 on table_format(msg_list)
   join(msg_list, return)
   split(result, {"あなたは", "、合計", "分の買い物をAmazonでしました!"})
   join(result, tab)
 end table_format
 
 on safari_dialog_exists()
   tell application "System Events"
     tell process "Safari"
       exists (windows whose subrole = "AXDialog")
     end tell
   end tell
 end safari_dialog_exists
 
 on split(src_text, delimiter)
   set last_delimiter to AppleScript's text item delimiters
   set AppleScript's text item delimiters to delimiter
   set res to src_text's text items
   set AppleScript's text item delimiters to last_delimiter
   res
 end split
 
 on join(src_list, delimiter)
   set last_delimiter to AppleScript's text item delimiters
   set AppleScript's text item delimiters to delimiter
   set res to src_list as text
   set AppleScript's text item delimiters to last_delimiter
   res
 end join
 
 on do_ruby_script(code_list)
   set code_list to code_list as list
   set code_list's last item to "puts(" & code_list's last item & ")"
   set shell_code to "ruby -r'jcode' -Ku -e " & quoted form of join(code_list, ";")
   do shell script shell_code
 end do_ruby_script
 
 on number_with_delimiter(num)
   do_ruby_script("('" & num & "' =~ /[-+]?\\d{4,}/) ? ('" & num & "'.reverse.gsub(/\\G((?:\\d+\\.)?\\d{3})(?=\\d)/, '\\1,').reverse) : '" & num & "'")
 end number_with_delimiter

使い方

  • 上記AppleScriptをスクリプトエディタで開いて、実行するだけでOK。
  • Amazon履歴にある西暦を、GUIスクリプティングなどを使って繰り返し集計している。
  • 手作業そのままを忠実にシミュレートしているので、結構時間がかかる。
  • 西暦ごとの集計は、モロ屋さんのgistをそのまま使わせて頂いている。(感謝です!)
  • 集計結果はテキストエディットに出力されるのだ!(はずかしいので数字は適当に弄ってる)

f:id:zariganitosh:20130605143044p:image:w450


たまに集計してみて、ポチる前に自重してみるのも良いかもしれない。

追伸

  • AppleScriptで手作業をシミュレートなんて壮大な遠回りである。
  • 本来はJavaScriptのみで完結するのがベスト。
  • JavaScriptオンリーなら、より多くのマシン環境、ブラウザ環境で幸せな集計ができるはず。
  • いつの日か、JavaScript版がどこかで公開されることに期待です。

2013-05-31

iTunes11でiOSアプリを最新の状態にしておく

iTunesはどんどんアップデートしていく。現在のiTunesは、11.0.3である。以前のスクリプトではもはや自動アップデートできない...。

久しく手動アップデートになっていたのだが、iTunes 11.0.3に対応した自動アップデートスクリプトを作ってみた。

AppleScript(iTunes 11.0.3用のiOSアプリ自動アップデートスクリプト)

delay 10
 tell application "iTunes" to launch
 tell application "iTunes" to reopen
 delay 10
 
 --iTunesウィンドウに対する操作
 tell application "System Events"
   tell process "iTunes"
     set frontmost to true
     delay 1
     tell window "iTunes"
       keystroke "7" using {command down} --ライブラリの「App」選択
       click splitter group 1's radio group 1's radio button 6 -->ラジオボタン,6,アップデート
       click splitter group 1's button 1 -->ボタン,missing value,すべてのAppをアップデート or アップデートを確認
       
       --iTunes ライブラリにある App のアップデートはありませんに対応する
       delay 2
       if my iTunes_dialog_exists() then
         my iTunes_dialog_OK()
         error number -128
       end if
       delay 2
       
       --ダウンロードが完了するまで待ち、年齢認証ダイアログに対応する
       my iTunes_download_waiting()
     end tell
   end tell
 end tell
 
 
 
 
 --ダイアログが表示されているか?
 on iTunes_dialog_exists()
   tell application "System Events"
     tell process "iTunes"
       exists (windows whose subrole = "AXDialog")
     end tell
   end tell
 end iTunes_dialog_exists
 
 --ダイアログでOKボタンを押す
 on iTunes_dialog_OK()
   try
     tell application "System Events"
       tell process "iTunes"
         set frontmost to true
         delay 1
         repeat while my iTunes_dialog_exists()
           click window 1's button "OK"
         end repeat
       end tell
     end tell
   end try
 end iTunes_dialog_OK
 
 --ダウンロードが完了するまで待ち、年齢認証ダイアログに対応する
 on iTunes_download_waiting()
   tell application "System Events"
     tell process "iTunes"
       set frontmost to true
       try
         click window "iTunes"'s button 9 -->ボタン,missing value,ダウンロードウインドウ
       end try
       set app_items to rows of table 1 of scroll area 1 of window "ダウンロード" -->ボタン,missing value,missing value
       set flag to true
       repeat while flag
         delay 10
         my iTunes_dialog_OK()
         set flag to false
         repeat with l in app_items
           try
             if l's group 1's button 1's name = "一時停止" then
               set flag to true
             end if
           end try
         end repeat
       end repeat
     end tell
   end tell
 end iTunes_download_waiting

仕様

  • 上記のスクリプトを実行すると、iTunesを起動して、iOSアプリのアップデートを確認して、必要があればダウンロードするのだ。
    • 年齢認証ダイアログが表示された場合は、OKボタンを押してくれる。
    • iTunes ライブラリにある App のアップデートはありませんダイアログが表示された場合も、OKボタンを押してくれる。
      • iTunesストアには、事前にサインインしておく必要がある。

使い方

  • スクリプトは上記仕様に書かれたことしか実行しない。
    • (iTunesがアップデートされても苦労最小で対応できるようにしておくため)
  • よって便利に使うためには、ちょっとした環境が必要。
タイマーで定期的に実行
  • 具体的には、毎深夜1:01に、timerコマンドで、iOSアプリ自動アップデートスクリプトを実行している。
timer -e 0-6 0101 /usr/bin/osascript /Users/zari/Library/Scripts/iTunes_app_update/iTunes11_app_update.scpt
iPhone等との同期
  • iTunesのデバイス項目で、iPhoneのオプション設定「Wi-Fi経由でこのiPhoneと同期」をチェックありにしておく。
  • すると、MacBookがスリープせずに稼働していて、iTunesが起動している時、
  • iPhoneが電源に繋がって、同じWi-Fiに接続していれば、自動的に同期されるのだ。

一旦設定すれば、日々のアップデートが着々と各iOSデバイスと自動同期されるはず、そんな手間いらずな環境を思い描きながら、しばらく使ってみる。