OAuthではてなブックマークするchrome extentionを作る

OAuthを利用してはてなブックマークするための
chrome extentionを作成しました。

うちの会社からはてなブックマークが禁止されています。
閲覧はできるので、どうやらはてなブックマークへのPOST送信が
禁止されているようです。

httpsで送信すればいいかなと思いましたが、うまくいきません。
はてなの公式のchrome extentionを使用してみましたが、ブックマークできません・・・

これでは何かと不便なのでブックマークできないか考えていました。

はてなブックマークAPI

はてなブックマークドキュメント一覧を見てみると、
REST APIAtomAPIが使えるようです。
はてなブックマークドキュメント一覧 - Hatena Developer Center
f:id:pppurple:20160221010739j:plain

認証はそれぞれ、
REST APIOAuth認証
Atom API⇒WSSE認証またはOAuth認証
となっています。

WSSE認証が簡単そうなので、WSSEで認証してAtomAPIを使用してみたのですが、
こちらもPOST認証が禁止されているようで、うまくいきません・・・

なので、REST APIを利用することにしました。

OAuth認証

REST APIを利用するにはOAuth認証が必要です。
Consumer key を取得して OAuth 開発をはじめよう - Hatena Developer Center

OAuthではRequest TokenやAccess Tokenを取得する必要があるのですが、
何かライブラリがないかと探していたらありました。

Tutorial: OAuth - Google Chrome

これを使用することにしました。

Chrome OAuth Extension

使い方はAPIのURLとconsumer keyとconsumer secretを設定すれば、
Request TokenやAccess Tokenの取得をよろしくやってくれるので
簡単だなぁ~と思っていたらうまくいきませんでした・・・・

var oauth = ChromeExOAuth.initBackgroundPage({
  'request_url': <OAuth request URL>,
  'authorize_url': <OAuth authorize URL>,
  'access_url': <OAuth access token URL>,
  'consumer_key': <OAuth consumer key>,
  'consumer_secret': <OAuth consumer secret>,
  'scope': <scope of data access, not used by all OAuth providers>,
  'app_name': <application name, not used by all OAuth providers>
});

どうやら、Request Tokenを取得する際にoauth_callbackを指定するのですが、
このOAuth Extensionではchromeで生成したタブページのURLになっており、
chrome://xxxxxxxxxxxxxxxxという値になっています。

hatena REST APIではこのURLを受け付けてないようで、500エラーになってしまいます。

OAuth1.0の仕様を確認してみると、oauth_callbackは
httpである必要があるようなのでhatenaの動きは正しいようです。

どうしようかなぁ~と思い、hatena OAuthのマニュアルを見ていると、
下記のような記述が。

URL の代わりに "oob" が指定されていた場合は oauth_verifier の値をユーザーに表示します

ということでoauth_callbackには"oob"を指定します。

これで実行すると、下記のような画面が表示されます。

f:id:pppurple:20160221010453j:plain:w500

ここで表示されたoauth_verifierをつけてAPIをコールし
アクセストークンを取得します。

verifierの自動取得

毎回verifierを手で付けるのは面倒なので自動で取得するようにします。

pin.js

window.onload = function() {
  var pin = document.querySelector(".verifier");

  if ((pin !== undefined && pin !== null)) {
    chrome.extension.sendRequest({ 
      "action" : "getAccessToken",
      "verifier": pin.innerText.replace(/\r?\n/g,"")
    });
    chrome.extension.sendRequest({
      "action" : "closeTab"
    });
  }
}

backgrount.jsのイベントリスナーはこんな感じ。

background.js

chrome.extension.onRequest.addListener(function(req, sender) {
  switch(req.action) {
    case 'getAccessToken':
      var reqToken = oauth.getReqToken();
      var bookmarkInfo = JSON.parse(oauth.getBookmarkInfo());
      oauth.getAccessToken(reqToken, encodeURIComponent(req.verifier), sendPost);
      break;
    case 'bookmark':
      oauth.setBookmarkInfo(JSON.stringify(req));
      sendPost(); 
      break;
    case 'closeTab':
      chrome.tabs.remove(sender.tab.id, function() {});
  }
});

これで自動でverifierを取得し、タブを閉じることができました。

popupの作成

Browser actionsのiconを押したときに表示するpopupを作ります。

ブックマークコメントと非公開のチェックボックスを付けました。
素のHTMLだと素っ気ないので軽量なcssを当てました。
軽量cssはたくさんありますが、よさそうなpapier.cssにしました。

Papier CSS library

papier.min.cssはわずか11kbです。

popup.html

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <link rel="stylesheet" type="text/css" href="./css/papier.min.css" />
  <link rel="stylesheet" type="text/css" href="popup.css" />
</head>
<body>
  <div id="content">
    <div id="bookmark">
      <h2>はてなブックマーク</h2>
      <textarea id="comment" name="comment" placeholder="comment..." rows="3" cols="30"></textarea>
      <p>
        <input type="checkbox" id="secret" name="secret"><img src="./img/lock_large_locked.png" title="ブックマークを他のユーザに公開しない">非公開
      </p>
      <button class="m bg-light-blue" type="button">ブックマーク</button>
    </div>
  </div>
  <script type="text/javascript" src="popup.js"></script>
</body>
</html>

popup.js

var tabUrl;

window.onload = function() {
  chrome.tabs.getSelected(null, function (tab) {
    tabUrl = tab.url;
    console.debug("tabUrl: " + tabUrl);
  });
};

function bookmark() {
  var secret = 0;
  var comment = document.getElementById("comment").value;
  if (document.getElementById("secret").checked) {
    secret = 1;
  }
  chrome.extension.sendRequest({
    "action" : "bookmark",
    "bookmarkUrl" : tabUrl,
    "comment" : comment,
    "secret" : secret
  });
  window.close();
};

document.addEventListener('DOMContentLoaded', function () {
  document.querySelector('button').addEventListener('click', bookmark);
});

ポップアップはこんな感じ。
f:id:pppurple:20160222170058j:plain

APIコール

popupのブックマークボタンを押した後に
background.jsではてなブックマークAPIをコールします。

background.js

function sendPost () {
  var bookmarkInfo = JSON.parse(oauth.getBookmarkInfo());
  oauth.authorize(function() {
    setIcon();
    var restUrl = "http://api.b.hatena.ne.jp/1/my/bookmark";
    var request = {
      'method': 'POST',
      'parameters': {
        // はてなAPI側でURLを正規化してくれるため、そのまま送信
        'url': bookmarkInfo.bookmarkUrl,
        'comment' : bookmarkInfo.comment,
        'private' : bookmarkInfo.secret
      }
    };
    oauth.sendSignedRequest(restUrl, showResult, request);
  });
};
dialogの作成

完了後のダイアログを作成します。
background.jsではてなブックマークAPIのレスポンスを取得し、dialogを表示します。
dialogにもpapier.cssを適用しました。

background.js

var bookmarkRes = {};

function showResult(text, xhr) {

  bookmarkRes.status = xhr.status;
  bookmarkRes.statusText = xhr.statusText;

  chrome.tabs.create({
    url: chrome.extension.getURL('dialog.html'),
    active: false
  }, function(tab) {
    chrome.windows.create({
      tabId: tab.id,
      type: 'popup',
      top: 20,
      height: 150,
      width: 400,
      focused: true
    });
  });
};

dialog.html

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <link rel="stylesheet" type="text/css" href="./css/papier.min.css" />
  <title>Dialog</title>
</head>
<body>
  <section class="panel">
    <header>
      <div id="output"></div>
    </header>
    <main>
      <div class="center">
        <button class="m" type="button">OK</button>
      </div>
    </main>
  </section>
  <script src="dialog.js"></script>
</body>
</html>

dialog.js

window.onload = function() {
  var output = document.querySelector('#output');
  var header = document.querySelector('header');
  var button = document.querySelector('button');

  var div = document.createElement('div');
  var message = document.createElement('h4');
  var errorMessage = document.createElement('p');

  var background = chrome.extension.getBackgroundPage();
  if (background.bookmarkRes.status == 200) {
    header.classList.add("bg-blue");
    button.classList.add("bg-light-blue");
    message.innerText = "ブックマークしました!";
  } else {
    header.classList.add("bg-red");
    button.classList.add("bg-blue-grey");
    message.innerText = "ブックマークに失敗しました・・・";
    errorMessage.innerText = background.bookmarkRes.status + " : " + background.bookmarkRes.statusText;
  };
  div.appendChild(message);
  div.appendChild(errorMessage);
  output.appendChild(div);
};

document.addEventListener('DOMContentLoaded', function () {
  document.querySelector('button').addEventListener('click', function () {
    window.close();
  });
});

成功時
f:id:pppurple:20160222172537j:plain

失敗時
f:id:pppurple:20160222172545j:plain

manifest.json

manifest.jsonでicon、background、コンテントスクリプト、ブラウザアクション、
パーミッションの設定をして完了。

manifest.json

{
  "manifest_version": 2,
  "name": "hatena bookmarker",
  "version": "0.1.0",
  "author": ["pppurple"],
  "description": "Enable to bookmark at hatena",
  "icons": { "48": "img/fav.png",
      "128": "img/fav.png" },
  "background": {
    "scripts": [
      "chrome_ex_oauthsimple.js",
      "chrome_ex_oauth.js",
      "background.js"
    ]
  },
  "content_scripts": [ {
    "js": [ "pin.js" ],
    "matches": [ "https://www.hatena.ne.jp/oauth/authorize" ],
    "run_at": "document_end"
  } ],
  "browser_action": {
    "default_title": "hatena bookmarker",
    "default_icon": "img/hatebu_icon_off.png",
    "default_popup": "popup.html"
  },
  "permissions": [
    "tabs",
    "https://www.hatena.com/oauth/initiate",
    "https://www.hatena.ne.jp/oauth/authorize",
    "https://www.hatena.com/oauth/token",
    "http://api.b.hatena.ne.jp/1/my/bookmark/*"
  ],
  "web_accessible_resources": [
  ]
}

これではてなブックマークすることができました。

ソースはこちらにあげときました。
github.com