記事一覧はこちら

WebRequest APIには署名が必要です

一ヶ月近く前からWebRequest APIのblocking機能を使ったGoogleChromeの拡張を公開しています。
先日その拡張のアップデートをしたのですが、何故か即公開になりません。
検出の誤作動かなと思って数日待っていたら、メールが来てサインを書いてグーグルに送れと言われました。
どうやらサインを送るまでは、web storeのデベロッパダッシュボードから非公開にする以外の編集は一切できないみたいです。

必要な項目は、名前、会社名(任意)、メールアドレス、eMailアドレス、電話、ファックス、国、webサイトURL、グーグルアカウント、署名、日付。
このメールアドレスというのは、住所を書けという事でしょうか。記入欄も大きいし、そういう事なのかな。
そしてchromeewbstore-reviews@google.comに送れと。
メール本文は
From: "Justin" jnc@google.com
To: ***@gmail.com
Cc: chromewebstore-reviews@google.com
Subject: Review notification for "ニコニコ直ダウンローダー"
Hi,
Thanks for uploading your item to our Google Chrome Web Store. Your item
was placed in a manual review queue because it uses the WebRequest API in
a blocking fashion.
The process for approving your item is quite straightforward. Due to the
greater performance implications of such extensions, we ask that you
complete the attached PDF form and return it to us. Please attach it as a
reply-all to this email (chromewebstore-reviews@google.com). After
receiving your completed form, your item will be published in the store
and a manual review to ascertain any performance issues will be scheduled.
You will be contacted if any concerns are surfaced by that review.
IMPORTANT - PLEASE READ.
If we do not receive a signed agreement, your item may be subject to
removal from the store.
Feel free to reply-all to this email if you have any further questions.
All the best,
Justin
Google Chrome Web Store team
こんな感じ。
この拡張機能は先月から公開して、何回かバージョンアップしてたんだけど、WebRequest APIのblocking機能は最初のバージョンから使ってるんですよね。最新バージョンはオプションページの機能を追加しただけで、特別な権限が必要な処理は一切増やしていません。
3月から規約が変わったのかなと思いますが、どこに情報書いてあるのか。リファレンスにも書いてないっぽいし。
この機能はWebRequest API でワザとリクエスト送信を遅くしてみた - hogehoge @teramakoや、上記のリファレンスに「XmlHttpRequests使うとデッドロックする可能性あるから使うな」って書いてある通り、かなり遅くなるから慎重にする事にしたのかな。
うー、めんどくさい。以下、拡張のコード。機能的にもう完成だから、これから直接落としてもいいかもね。

{
    "name":"ニコニコ直ダウンローダー",
    "description":"外部サーバーに頼らず、ファイル名を自動で付けてニコニコ動画を1クリックでダウンロードします。時報ブロック機能付き。",
    "version": "3.0",
    "options_page":"option.html",
    "background_page":"background.html",
    "icons":{
        "48":"icon48.png",
        "128":"icon128.png"
    },
    "permissions": [
        "contextMenus",
        "webRequest",
        "webRequestBlocking",
        "http://*.nicovideo.jp/",
        "http://res.nimg.jp/"
    ],
    "content_scripts":[{
        "matches":["http://*.nicovideo.jp/*"],
        "js":["contentScript.js"]
    }]
}
BUTTON_CSS_BASE="margin:3px;padding:2px;font-family:monospace;";
BUTTON_CSS_ENA="border:2px solid #90c0f0;background-color:#FFFFFF;color:#000000;";
BUTTON_DOM_ID="chrome_Ex_nico_movie_get_down_buttonR";
vid=null;loadinit=false;
if(document.URL.match(//watch/([0-9a-z]+)/)){vid=RegExp.$1}
if(document.URL.match(//watch/lv/)){vid=null;}
if(vid!=null){
    //dlボタンが有効なら作る。
    console.log("ダウンロードボタンの有効チェック開始");
    chrome.extension.sendRequest({
            action:"enableDlButton",
            args  :[""]
        },function(response){
            console.log("ダウンロードボタンの有効チェック %o",response);
            if(response==true){
                OnLoad();
            }
        }
    );
}
function OnLoad(){
    var _td;
    _td = document.createElement("button");
    _td.setAttribute("style",BUTTON_CSS_BASE+BUTTON_CSS_ENA);
    _td.setAttribute("id",BUTTON_DOM_ID);
    _td.innerHTML ='動画DL';
    _td.addEventListener("click",goMovie,false);
    document.querySelector("#itab td").appendChild(_td);
}
function goMovie(){
    chrome.extension.sendRequest({
            action:"goMovie",
            args  :[vid]
        },function(response){}
    );
}
<html lang="ja">
<meta charset="UTF-8" />
<head>
<title>オプション</title>
<script type="text/javascript" src="backgroundScript.js"></script>
<script>
window.addEventListener("load",init);
function init(){
    var target="",func;
    //時報
    target="signalDisable";
    if(localStorage.getItem(target)=="true"){
        document.querySelector("#"+target+" input[type='checkbox']").checked=true;
    }else{
        document.querySelector("#"+target+" input[type='checkbox']").checked=false;
    }
    func=(function(target){
        return function(){
            if(document.querySelector("#"+target+" input[type='checkbox']").checked){
                localStorage.setItem(target,"true");
                document.querySelector("#"+target+" span.msg").innerHTML="時報を無効化します";
            }else{
                localStorage.setItem(target,"false");
                document.querySelector("#"+target+" span.msg").innerHTML="時報の無効を取り消しました<br>(プレミアム会員の方でプレイヤー側で無効化されていた場合はそちらが優先されます)";
            }
        }
    })(target)
    document.querySelector("#"+target+" input[type='checkbox']").addEventListener("click",func);
    //視聴ページのボタン
    target="dlButton";
    if(localStorage.getItem(target)=="true"){
        document.querySelector("#"+target+" input[type='checkbox']").checked=true;
    }else{
        document.querySelector("#"+target+" input[type='checkbox']").checked=false;
    }
    func=(function(target){
        return function(){
            if(document.querySelector("#"+target+" input[type='checkbox']").checked){
                localStorage.setItem(target,"true");
                document.querySelector("#"+target+" span.msg").innerHTML="視聴ページにダウンロードボタンを追加します";
            }else{
                localStorage.setItem(target,"false");
                document.querySelector("#"+target+" span.msg").innerHTML="視聴ページにダウンロードボタンを追加しません<br>視聴ページに何も手を加えませんので、この拡張機能のせいで他の拡張機能が正常に動かない!という時はオフにして下さい";
            }
        }
    })(target)
    document.querySelector("#"+target+" input[type='checkbox']").addEventListener("click",func);
    //右クリックメニュー
    target="contextMenu";
    if(localStorage.getItem(target)=="true"){
        document.querySelector("#"+target+" input[type='checkbox']").checked=true;
    }else{
        document.querySelector("#"+target+" input[type='checkbox']").checked=false;
    }
    func=(function(target){
        return function(){
            if(document.querySelector("#"+target+" input[type='checkbox']").checked){
                localStorage.setItem(target,"true");
                document.querySelector("#"+target+" span.msg").innerHTML="右クリックメニューに「ニコニコ動画をダウンロード」メニューを追加します。";
                updateContextMenu(true);
            }else{
                localStorage.setItem(target,"false");
                document.querySelector("#"+target+" span.msg").innerHTML="右クリックメニューの「ニコニコ動画をダウンロード」メニューを削除します。";
                updateContextMenu(false);
            }
        }
    })(target)
    document.querySelector("#"+target+" input[type='checkbox']").addEventListener("click",func);
    //保存ファイル名パターン
    target="saveFilePat";
    document.querySelector("#"+target+"T").value=localStorage.getItem(target);
    func=(function(target){
        return function(){
            var saveT=document.querySelector("#"+target+"T").value;
            localStorage.setItem(target,saveT);
            document.querySelector("#"+target+" span.msg").innerHTML="""+saveT+""で保存しました";
        }
    })(target)
    document.querySelector("#"+target+"T").addEventListener("keyup",func);
}
</script>
</head>
<body>
<div id="signalDisable"><label><input type="checkbox">時報を無効化する</label><br><span class="msg">保存は自動で行います</span></div><hr>
<div id="dlButton"><label><input type="checkbox">視聴ページ(/watch)にダウンロードボタンを表示する</label><br><span class="msg">保存は自動で行います</span></div><hr>
<div id="contextMenu"><label><input type="checkbox">右クリックメニューに「ダウンロード」メニューを追加する</label><br><span class="msg">保存は自動で行います</span></div><hr>
<div id="saveFilePat">保存ファイル名<input type="text" id="saveFilePatT" style="font-family:monospace;width:500px;">
<table border="1">
<tr><td>$title</td>      <td>動画タイトル</td><td>ニコニコ</td></tr>
<tr><td>$vid</td>        <td>動画ID</td><td>sm123</td></tr>
<tr><td>$thread</td>     <td>スレッドID</td><td>1234567890</td></tr>
<tr><td>$postYear</td>   <td>投稿した年</td><td>2043</td></tr>
<tr><td>$postMonth</td>  <td>投稿した月</td><td>0~12</td></tr>
<tr><td>$postMonth2</td> <td>投稿した月(2桁)</td><td>00~12</td></tr>
<tr><td>$postDate</td>   <td>投稿した日</td><td>0~31</td></tr>
<tr><td>$postDate2</td>  <td>投稿した日(2桁)</td><td>00~31</td></tr>
<tr><td>$postDay</td>    <td>投稿した曜日</td><td>日~土</td></tr>
<tr><td>$postDayEng</td> <td>投稿した曜日(英語)</td><td>Sun~Sat</td></tr>
<tr><td>$postDayEng2</td><td>投稿した曜日(英語)</td><td>Sunday~Saturday</td></tr>
<tr><td>$postHour</td>   <td>投稿した時</td><td>0~59</td></tr>
<tr><td>$postHour2</td>  <td>投稿した時(2桁)</td><td>00~59</td></tr>
<tr><td>$postMin</td>    <td>投稿した分</td><td>0~59</td></tr>
<tr><td>$postMin2</td>   <td>投稿した分(2桁)</td><td>00~59</td></tr>
<tr><td>$postUser</td>   <td>投稿者名、又はチャンネル名</td><td>ニコニコ</td></tr>
<tr><td>$postUserId</td> <td>投稿者ID、又はチャンネルID</td><td>123456、<a href="http://ch.nicovideo.jp/channel/ch1131" target="_blank">ch1131</a></td></tr>
<tr><td>$category</td>   <td>カテゴリ</td><td>エンターテイメント</td></tr>
<tr><td>$description</td><td>動画説明文</td><td>カブトボーグ メダロット 黄金バット配信中 ch1131</td></tr>
<tr><td>$tags|</td>      <td>タグ名</td><td>$tags-|の場合、タグ1-タグ2-タグ3となる。<br>[$tags,|]と指定すれば[タグ1,タグ2,タグ3]</td></tr>
</table>
改行、タブ文字、htmlコードは削除。連続した空白文字(半角スペース、全角スペース)は一つに纏められます。<br>
<span class="msg">保存は自動で行います</span></div><hr>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="backgroundScript.js"></script>
<script>
//変数初期化
debug=true;
tabId="";
movieTitle="";
//contextMenu
if(localStorage.getItem("contextMenu")=="true"){
    updateContextMenu(true);
}
chrome.extension.onRequest.addListener(function(message,sender,sendResponse){sendResponse(eval(message.action).apply(sender,message.args));});
chrome.webRequest.onHeadersReceived.addListener(
    function(details){
        console.log("webリクエストイベント発火時=%o",details);
        var hasHeader=false;
        var ext,m;
        if(details.tabId!=tabId || tabId==""){
            return;
        }else{
            tabId="";
        }
        for(var i=0;i<details.responseHeaders.length;i++){
            if(details.responseHeaders[i].name=="Content-Disposition"){
                ext="mp4";
                if(m=details.responseHeaders[i].value.match(/.+.([a-z0-9]+)"$/)){//"
                    ext=m[1];
                }
                details.responseHeaders[i].value="attachment; filename=""+movieTitle+"."+ext+""";
                hasHeader=true;
                break;
            }
        }
        if(!hasHeader){
            details.responseHeaders.push({name:"Content-Disposition",value:"attachment; filename=""+movieTitle+".mp4""});
        }
        movieUrl="";
        return {responseHeaders:details.responseHeaders};
    },{
        urls: ["*://*.nicovideo.jp/*"],
        types:["main_frame"]
    },["responseHeaders","blocking"]
);
chrome.webRequest.onBeforeRequest.addListener(
    function(details){
        if(localStorage.getItem("signalDisable")=="true"){
            return {cancel:true};
        }else{
            return {cancel:false};
        }
    },{
        urls: ["http://res.nimg.jp/swf/system/marquee/default/*","http://flapi.nicovideo.jp/api/getmarquee_new*"],
        types:["object"]
    },["blocking"]
);
function goMovie(vid){
    var ifNm=false;
    var fileNamePatObj={};
    var tags=[];
    //vidにurlを全部送ってくるバカがいます
    if(vid.match(/watch/([a-z0-9]+)/)){
        vid=RegExp.$1;
    }
    if(vid.match(/^nm/)){ifNm=true;}
    fileNamePatObj["vid"]=vid;
    //先ずはタイトルを取る
    html2Title(loadTextFile("http://www.nicovideo.jp/watch/"+vid),fileNamePatObj,tags);
    if(fileNamePatObj["title"]==null){
        alert("http://www.nicovideo.jp/watch/"+vid+"nのタイトルの取得に失敗しました。");
        return false;
    }
    //動画urlを取る
    movieUrl=html2url(loadTextFile("http://flapi.nicovideo.jp/api/getflv?v="+vid+(ifNm?"&as3=1":"")),fileNamePatObj);
    if(movieUrl==""){
        alert("http://www.nicovideo.jp/watch/"+vid+"nの動画URLの取得に失敗しました。");
        return false;
    }
    //ファイル名作成
    //先にタグあれこれ
    console.log("fileNamePatObj=%o",fileNamePatObj);
    console.log("tags=%o",tags);
    movieTitle=localStorage.getItem("saveFilePat");
    movieTitle=movieTitle.replace(/$tags(.*?)|/g,function(a,b){
        return tags.join(b);
    });
    movieTitle=movieTitle.replace(/$([a-zA-Z0-9]+)/g,function(a,b){
        if(fileNamePatObj[b]==null){return "";}
        return fileNamePatObj[b];
    });
    movieTitle=fileNameSafe(movieTitle);
    chrome.tabs.create({"url":movieUrl,selected:false},function(detail){
        tabId=detail.id;
        console.log("コンソールのログ=%o",detail);
    });
}
function loadTextFile(url){
    httpObj= new XMLHttpRequest();
    httpObj.open("GET",url,false);
    httpObj.send(null);
    return httpObj.responseText.replace(/(r|n|t)+/g,"").replace(/( | )+/g," ");
}
function removeHtmlTag(str){
    return str.replace(/<.*?>/g,"");
}
function html2url(html,fileNamePatObj){
    var ret="";
    try{ret=decodeURIComponent(/url=([^&]+)/.exec(html)[1]);}catch(e){}
    try{fileNamePatObj["thread"]=decodeURIComponent(/thread_id=([^&]+)/.exec(html)[1]);}catch(e){}
    return ret;
}
function html2Title(html,fileNamePatObj,tags){
    var dateObj;
    try{fileNamePatObj["title"]=removeHtmlTag(/<p id="video_title"><!-- google_ad_section_start -->(.+?)<!-- google_ad_section_end -->/.exec(html)[1]);}catch(e){}
    try{fileNamePatObj["postYear"]=removeHtmlTag(/<p id="video_date">.+?(d+)年(d+)月(d+)日 (d+):(d+)/.exec(html)[1]);}catch(e){}
    try{fileNamePatObj["postMonth"] =removeHtmlTag(/<p id="video_date">.+?(d+)年(d+)月(d+)日 (d+):(d+)/.exec(html)[2]*1+"");}catch(e){}
    try{fileNamePatObj["postMonth2"]=removeHtmlTag(/<p id="video_date">.+?(d+)年(d+)月(d+)日 (d+):(d+)/.exec(html)[2]);}catch(e){}
    try{fileNamePatObj["postDate"]  =removeHtmlTag(/<p id="video_date">.+?(d+)年(d+)月(d+)日 (d+):(d+)/.exec(html)[3]*1+"");}catch(e){}
    try{fileNamePatObj["postDate2"] =removeHtmlTag(/<p id="video_date">.+?(d+)年(d+)月(d+)日 (d+):(d+)/.exec(html)[3]);}catch(e){}
    //曜日
    try{
        html.match(/<p id="video_date">.+?(d+)年(d+)月(d+)日 (d+):(d+)/);
        dateObj=new Date();
        dateObj.setFullYear(RegExp.$1);
        dateObj.setMonth(RegExp.$2-1);
        dateObj.setDate(RegExp.$3);
        fileNamePatObj["postDay"]    =["日","月","火","水","木","金","土"][dateObj.getDay()];
        fileNamePatObj["postDayEng"] =["Sun","Mon","Thu","Wed","Thu","Fri","Sat"][dateObj.getDay()];
        fileNamePatObj["postDayEng2"]=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][dateObj.getDay()];
    }catch(e){}
    try{fileNamePatObj["postHour"]   =removeHtmlTag(/<p id="video_date">.+?(d+)年(d+)月(d+)日 (d+):(d+)/.exec(html)[4]*1+"");}catch(e){}
    try{fileNamePatObj["postHour2"]  =removeHtmlTag(/<p id="video_date">.+?(d+)年(d+)月(d+)日 (d+):(d+)/.exec(html)[4]);}catch(e){}
    try{fileNamePatObj["postMin"]    =removeHtmlTag(/<p id="video_date">.+?(d+)年(d+)月(d+)日 (d+):(d+)/.exec(html)[5]*1+"");}catch(e){}
    try{fileNamePatObj["postMin2"]   =removeHtmlTag(/<p id="video_date">.+?(d+)年(d+)月(d+)日 (d+):(d+)/.exec(html)[5]);}catch(e){}
    //投稿者名 又はチャンネル名
    try{fileNamePatObj["postUser"]   =removeHtmlTag(/<p class="font12"><a href="user/(d+)"><strong>(.+?)</strong>/.exec(html)[2]);}catch(e){}
    try{fileNamePatObj["postUser"]   =removeHtmlTag(/<p class="font12"><a href="http://ch.nicovideo.jp/channel/(.+?)"><strong>(.+?)</strong>/.exec(html)[2]);}catch(e){}
    //投稿者ID 又はチャンネルID
    try{fileNamePatObj["postUserId"] =removeHtmlTag(/<p class="font12"><a href="user/(d+)"><strong>(.+?)</strong>/.exec(html)[1]);}catch(e){}
    try{fileNamePatObj["postUserId"]   =removeHtmlTag(/<p class="font12"><a href="http://ch.nicovideo.jp/channel/(.+?)"><strong>(.+?)</strong>/.exec(html)[1]);}catch(e){}
    try{fileNamePatObj["category"]   =removeHtmlTag(/<span style="color:#C9CFCF;">…</span> <strong style="color:#393F3F;">(.+?)</.exec(html)[1]);}catch(e){}
    try{fileNamePatObj["description"]=removeHtmlTag(/<div id="itab_description" class="info in"><p class="font12" style="padding:4px;"><!-- google_ad_section_start -->(.+?)<!-- google_ad_section_end -->/.exec(html)[1]);}catch(e){}
    //タグ
    try{
        var h,h2;
        h=html.match(/<nobr>(<img.+?>)?<a href="tag/.+?" rel="tag" class="nicopedia">(.+?)</a>/g);
        for(var i=0;i<h.length;i++){
            h[i].match(/<a href="tag/.+?" rel="tag" class="nicopedia">(.+?)</a>/);
            tags.push(RegExp.$1);
        }
    }catch(e){}
}
function fileNameSafe(str){
    str=str.replace(/\/g,"¥");
    str=str.replace(///g,"/");
    str=str.replace(/:/g,":");
    str=str.replace(/*/g,"*");
    str=str.replace(/?/g,"?");
    str=str.replace(/"/g,"”");//"
    str=str.replace(/</g,"<");
    str=str.replace(/>/g,">");
    str=str.replace(/|/g,"|");
    return str;
}
</script>
</head>
</html>
//localStorage初期化
initLocalStorage();
function initLocalStorage(){
    if(localStorage.getItem("signalDisable")==null){
        localStorage.setItem("signalDisable","true");
    }
    if(localStorage.getItem("dlButton")==null){
        localStorage.setItem("dlButton","true");
    }
    if(localStorage.getItem("contextMenu")==null){
        localStorage.setItem("contextMenu","true");
    }
    if(localStorage.getItem("saveFilePat")==null){
        localStorage.setItem("saveFilePat","$title - [$vid]");
    }
}
//DLボタンが有効かどうかreturnする
function enableDlButton(){
    console.log("dlボタンの有効チェックが来ました");
    if(localStorage.getItem("dlButton")=="true"){
        return true;
    }else{
        return false;
    }
}
function updateContextMenu(enable){
    chrome.contextMenus.removeAll(function(){});
    if(enable){
        chrome.contextMenus.create({"title":"リンク先のニコニコ動画を保存","contexts":["link"],"targetUrlPatterns":["http://www.nicovideo.jp/watch/*"],"onclick":function(details){
            goMovie(details.linkUrl);
        }});
        chrome.contextMenus.create({"title":"今のページのニコニコ動画を保存","contexts":["page"],"documentUrlPatterns":["http://www.nicovideo.jp/watch/*"],"onclick":function(details){
            goMovie(details.pageUrl);
        }});
    }
}

2012-05-13追記
Google ChromeのAPIについて、最新情報や開発のノウハウなどを日本語で共有するGoogle 準公式のコミュニティがあるらしい。そこで直談判したらGoogleの方から返事が来て、公開されるようになりました。

以前のはてブ