1. 記事一覧 >
  2. ブログ記事
category logo

カスタムURLスキームのアプリに対応しているか検知する方法

(更新) (公開)

はじめに

URLリンクからアプリを起動できるカスタムURLスキーム(custom url scheme, custom protocol)というのがあります。
※英語サイトの場合、「custom protocol」と言っている場合が多いような気がします。


例として、microsft-edge:https://itccorporation.jpとすると、Edgeが立ち上がり、https://itccorporation.jpを表示します。

カスタムURLスキームでEdgeを起動


一方、例えば、microsft-edgeXXX:に対応するアプリが無い場合、無反応になります。(Chromeの場合)

カスタムURLスキームで無反応


この場合、対応するアプリをインストールしないといけませんが、ユーザーは状況が分かりにくいです。
そこで、今回、対応するアプリの有無を検知する方法を実装しましたので、紹介していきたいと思います。
参考:https://github.com/ismailhabib/custom-protocol-detection


【検証環境】

Windows10 Pro 64bit

 IE11 バージョン 20H2(OSビルド 19042, 1288)

 Edge(Chromium) 95.0.1020.30

 Chrome 95.0.4638.54

 Firefox 93.0

 Windows版Safari 5.0.5

Windows7 Professional Service Pack1

 IE11 11.0.9600.18617 更新バージョン 11.0.40(KB4012204)

 Firefox 63.0b14

【検証環境】以外では、この記事の内容と異なる可能性があります。

Safariについては、Windows版のみ確認しました。iOS(Mac,iPhone,iPad)の場合の挙動は不明です。Chromiumではない旧Edgeについては、全くの考慮外です。


対応するアプリの有無を検知する方法

方法

いきなり結論から先に書きますと、ブラウザによって、対応方法が異なり、以下の方法でできました。


●IE
Navigator.msLaunchUriを使用。
Navigator.msLaunchUriが無いバージョンの場合、Safariと同じ対応。


●Chrome、Firefox(ver 64以上)、Edge(Chromium)
アプリケーションが開いた場合、アプリケーションの方へフォーカスが移るため、onBlur(フォーカスを失ったときのイベント)が発生したかどうかで判定。


●Firefox(ver 64未満)
iframe、try, catchで"NS_ERROR_UNKNOWN_PROTOCOL"エラーを捕捉。


Firefoxはバージョン64.0から挙動が変わったようで、バージョン64未満(例:63.0b14)の場合、以下のように、iframe、try, catchで対応する必要がありました。逆にバージョン64以上の場合、以下の手法は使えず、Chromeと同じ対応が必要になりました。

    var iframe = document.querySelector("#hiddenIframe");

    if (!iframe) {
        iframe = _createHiddenIframe(document.body, "about:blank");
    }

    try {
        iframe.contentWindow.location.href = uri;
        successCb();
    } catch (e) {
        if (e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
            failCb();
        }
    }

●Safari
iframeでカスタムURLスキームを使ったURLを開くのと、onBlur(フォーカスを失ったときのイベント)が発生したかどうかで判定。※iframeで開かないと、対応していない場合、画面遷移してしまう。


これにより、以下のように検知し、本来の動きの代わりにアラートを表示したりできます。

カスタムURLスキーム対応無しの場合アラート


ソースコード

カスタムURLスキーム対応有無を検知全リンク


この動きを実現した全ソースコードです。
microsoft-edge:https://itccorporation.jpは、古いWindowsの場合、関連付け設定されていなくて、エラー判定になると思います。
foobar:https://itccorporation.jpは、関連付け設定が必要ですので、後述します。

<!DOCTYPE html>
<html>
<head lang="ja">
    <meta charset="UTF-8">
    <title>Custom Protocol Detection</title>
</head>
<body>
    <a href="microsoft-edge:https://itccorporation.jp" class="custom_protocol">microsoft-edge:https://itccorporation.jp</a><br />
    <a href="microsoft-edgeXXX:https://itccorporation.jp" class="custom_protocol">microsoft-edgeXXX:https://itccorporation.jp</a><br />
    <a href="foobar:https://itccorporation.jp" class="custom_protocol">foobar:https://itccorporation.jp</a><br />
<script>
Array.prototype.forEach.call(
  document.querySelectorAll('.custom_protocol'),
  function (elem) {
    elem.addEventListener(
      'click',
      function (e) {
        protocolCheck(this.getAttribute('href'), function () {
          alert('protocol not recognized');
        });
        e.preventDefault ? e.preventDefault() : (e.returnValue = false);
      },
      false
    );
  }
);
function _registerEvent(target, eventType, cb) {
  if (target.addEventListener) {
    target.addEventListener(eventType, cb);
    return {
      remove: function () {
        target.removeEventListener(eventType, cb);
      },
    };
  } else {
    target.attachEvent(eventType, cb);
    return {
      remove: function () {
        target.detachEvent(eventType, cb);
      },
    };
  }
}
function _createHiddenIframe(target, uri) {
  var iframe = document.createElement('iframe');
  iframe.src = uri;
  iframe.id = 'hiddenIframe';
  iframe.style.display = 'none';
  target.appendChild(iframe);
  return iframe;
}
function openUriWithHiddenFrame(uri, failCb, successCb) {
  var timeout = setTimeout(function () {
    failCb();
    handler.remove();
  }, 1000);
  var iframe = document.querySelector('#hiddenIframe');
  if (!iframe) {
    iframe = _createHiddenIframe(document.body, 'about:blank');
  }
  var handler = _registerEvent(window, 'blur', onBlur);
  function onBlur() {
    clearTimeout(timeout);
    handler.remove();
    successCb();
  }
  iframe.contentWindow.location.href = uri;
}
function openUriWithTimeoutHack(uri, failCb, successCb) {
  var timeout = setTimeout(function () {
    failCb();
    handler.remove();
  }, 1000);
  //handle page running in an iframe (blur must be registered with top level window)
  var target = window;
  while (target != target.parent) {
    target = target.parent;
  }
  var handler = _registerEvent(target, 'blur', onBlur);
  function onBlur() {
    clearTimeout(timeout);
    handler.remove();
    successCb();
  }
  window.location = uri;
}
function openUriUsingFirefox(uri, failCb, successCb) {
  console.log('openUriUsingFirefox');
  var iframe = document.querySelector('#hiddenIframe');
  if (!iframe) {
    iframe = _createHiddenIframe(document.body, 'about:blank');
  }
  try {
    iframe.contentWindow.location.href = uri;
    successCb();
  } catch (e) {
    if (e.name == 'NS_ERROR_UNKNOWN_PROTOCOL') {
      failCb();
    }
  }
}
function openUriUsingIEInOlderWindows(uri, failCb, successCb) {
  if (getInternetExplorerVersion() === 10) {
    openUriUsingIE10InWindows7(uri, failCb, successCb);
  } else if (
    getInternetExplorerVersion() === 9 ||
    getInternetExplorerVersion() === 11
  ) {
    openUriWithHiddenFrame(uri, failCb, successCb);
  } else {
    openUriInNewWindowHack(uri, failCb, successCb);
  }
}
function openUriUsingIE10InWindows7(uri, failCb, successCb) {
  var timeout = setTimeout(failCb, 1000);
  window.addEventListener('blur', function () {
    clearTimeout(timeout);
    successCb();
  });
  var iframe = document.querySelector('#hiddenIframe');
  if (!iframe) {
    iframe = _createHiddenIframe(document.body, 'about:blank');
  }
  try {
    iframe.contentWindow.location.href = uri;
  } catch (e) {
    failCb();
    clearTimeout(timeout);
  }
}
function openUriInNewWindowHack(uri, failCb, successCb) {
  var myWindow = window.open('', '', 'width=0,height=0');
  myWindow.document.write("<iframe src='" + uri + "'></iframe>");
  setTimeout(function () {
    try {
      myWindow.location.href;
      myWindow.setTimeout('window.close()', 1000);
      successCb();
    } catch (e) {
      myWindow.close();
      failCb();
    }
  }, 1000);
}
function openUriWithMsLaunchUri(uri, failCb, successCb) {
  navigator.msLaunchUri(uri, successCb, failCb);
}
function checkBrowser() {
  var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
  var ua = navigator.userAgent.toLowerCase();
  return {
    isOpera: isOpera,
    isFirefox_OLD:
      typeof InstallTrigger !== 'undefined' &&
      parseInt(
        '' +
          navigator.userAgent.substring(
            navigator.userAgent.indexOf('Firefox') + 8
          ),
        10
      ) < 64,
    isFirefox:
      typeof InstallTrigger !== 'undefined' &&
      parseInt(
        '' +
          navigator.userAgent.substring(
            navigator.userAgent.indexOf('Firefox') + 8
          ),
        10
      ) >= 64,
    isSafari:
      (~ua.indexOf('safari') && !~ua.indexOf('chrome')) ||
      Object.prototype.toString
        .call(window.HTMLElement)
        .indexOf('Constructor') > 0,
    isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream,
    isChrome: !!window.chrome && !isOpera,
    isIE: /*@cc_on!@*/ false || !!document.documentMode, // At least IE6
  };
}
function getInternetExplorerVersion() {
  var rv = -1;
  if (navigator.appName === 'Microsoft Internet Explorer') {
    var ua = navigator.userAgent;
    var re = new RegExp('MSIE ([0-9]{1,}[.0-9]{0,})');
    if (re.exec(ua) != null) rv = parseFloat(RegExp.$1);
  } else if (navigator.appName === 'Netscape') {
    var ua = navigator.userAgent;
    var re = new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})');
    if (re.exec(ua) != null) {
      rv = parseFloat(RegExp.$1);
    }
  }
  return rv;
}
function protocolCheck(uri, failCb, successCb, unsupportedCb) {
  function failCallback() {
    failCb && failCb();
  }
  function successCallback() {
    successCb && successCb();
  }
  if (navigator.msLaunchUri) {
    //for IE and Edge in Win 8 and Win 10
    openUriWithMsLaunchUri(uri, failCb, successCb);
  } else {
    var browser = checkBrowser();
    if (browser.isFirefox_OLD) {
      openUriUsingFirefox(uri, failCallback, successCallback);
    } else if (browser.isFirefox || browser.isChrome || browser.isIOS) {
      openUriWithTimeoutHack(uri, failCallback, successCallback);
    } else if (browser.isIE) {
      openUriUsingIEInOlderWindows(uri, failCallback, successCallback);
    } else if (browser.isSafari) {
      openUriWithHiddenFrame(uri, failCallback, successCallback);
    } else {
      unsupportedCb();
      //not supported, implement please
    }
  }
}
</script>
</body>
</html>

対応していない場合の挙動

<a href="microsoft-edgexxx:https://itccorporation.jp">microsoft-edgexxx</a>のように、hrefが関連付け設定されていなくて対応していないカスタムURLスキームの場合、どうなるのか、検証してみました。
おのおの以下の挙動になりました。


●IE(Navigator.msLaunchUriが使用可能なバージョン)
アプリストアへ遷移を促す画面が表示されます。(外側をクリックするとキャンセル可)

IE カスタムURLスキーム未対応 アプリストアへ遷移を促す画面


●IE(Navigator.msLaunchUriが無いバージョン)

「この Web ページは表示できません」の画面に切り替わります。

IE カスタムURLスキーム未対応 「この Web ページは表示できません」


●Chrome、Firefox(ver 64以上)、Edge(Chromium)
リンクが無反応になります。


●Firefox(ver 64未満)
「アドレスのプロトコルが不明です」の画面に切り替わります。

Firefox カスタムURLスキーム未対応 「アドレスのプロトコルが不明です」


●Safari
「指定されたアドレスを開けません。」の画面に切り替わります。

Safari カスタムURLスキーム未対応 「指定されたアドレスを開けません。」


独自カスタムURLスキーム追加

<a href="foobar:https://itccorporation.jp" class="custom_protocol">foobar:https://itccorporation.jp</a>のように独自のカスタムURLスキームを追加するときの手順です。


追加手順

その1

設定→アプリ→既定のアプリ

設定→アプリ→既定のアプリ1
設定→アプリ→既定のアプリ2

既定のアプリを下の方にスクロールして、「プロトコルごとに既定のアプリを選ぶ」をクリックします。

「プロトコルごとに既定のアプリを選ぶ」をクリック

Windows7の場合は、コントロールパネルから「プログラム」→「既定のプログラム」→「ファイルの種類またはプロトコルのプログラムへの関連付け」です。」

プロトコル名の右側をクリックすると、アプリを変更できます。
ただし、この場合、新規プロトコル追加ができず、決められた範囲のアプリからしか選択できません。
レジストリを解析して、ここに独自のプロトコルを表示させようとしてみましたが、良く分かりませんでした。

プロトコル名の右側をクリック アプリを変更


その2

レジストリを書き換えます。
以下の内容を .reg ファイル(例:foobar.reg)に保存し、ダブルクリックすると、foobarというプロトコルが追加されて、foobar:https://itccorporation.jpのように独自のカスタムURLスキームが使えるようになります。

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\foobar]
@="URL:foobar"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\foobar\shell]

[HKEY_CLASSES_ROOT\foobar\shell\open]

[HKEY_CLASSES_ROOT\foobar\shell\open\command]
@="\"C:\\Users\\admin\\Desktop\\msg.bat\" %1"

[HKEY_CLASSES_ROOT\foobar\shell\open\command] に指定されている、@="\"C:\\Users\\admin\\Desktop\\msg.bat\" %1"は、エスケープされているので、分かりにくいですが、"C:\Users\admin\Desktop\msg.bat" %1 と引数をバッチファイルに渡して起動しています。


regedit.exeで手動で登録しても同じことです。

regedit.exeで手動で登録1
regedit.exeで手動で登録2
regedit.exeで手動で登録3
regedit.exeで手動で登録4

msg.batは、引数の内容をダイアログに表示するバッチファイルです。バッチファイルだけでは、ダイアログを表示できないため、vbsを生成して、起動し、削除しています。

@echo off
echo MsgBox "%1",vbInformation,"info" > %TEMP%\msgbox.vbs & %TEMP%\msgbox.vbs
del /Q %TEMP%\msgbox.vbs

foobarプロトコルでmsg.batを起動 ダイアログ表示


引数%1について

foobar:https://itccorporation.jp

"C:\Users\admin\Desktop\msg.bat" %1
のように起動した場合、
%1 は、foobar:https://itccorporation.jp です。


chrome.exefoobar:https://itccorporation.jpを渡しても、https://itccorporation.jpを開いてくれません。
https://itccorporation.jpを渡したい場合、以下のようにcmd.exeで foobar: 部分を取り除くしか方法は無いようです。

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\foobar]
@="URL:foobar"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\foobar\shell]

[HKEY_CLASSES_ROOT\foobar\shell\open]

[HKEY_CLASSES_ROOT\foobar\shell\open\command]
@="C:\\Windows\\System32\\cmd.exe /k set myvar=%1&call set myvar=%%myvar:foobar:=%%&call start chrome.exe %%myvar%%"

microsoft-edge:について

プロトコル microsoft-edge: について、
HKEY_CLASSES_ROOT\MSEdgeHTM\shell\open\command
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --single-argument %1
が登録されています。


microsoft-edge:https://itccorporation.jp
で起動した場合、
%1 は、microsoft-edge:https://itccorporation.jpで、

"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --single-argument microsoft-edge:https://itccorporation.jp

の意味になり、URL=https://itccorporation.jpの状態で、Edgeが開いてきます。


一方で、 プロトコル foobar:"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --single-argument %1
を登録

foobar:https://itccorporation.jp
で起動した場合、
%1 は、foobar:https://itccorporation.jpで、
URL=https://itccorporation.jpの状態で開きません。URLがfoobar:で始まるからです。
microsoft-edge:のときだけ、Edgeがその後にURLが指定されているとみなして、引数のmicrosoft-edge:を無視しているようです。


ちなみに、Edge自身からは、microsoft-edge:https://itccorporation.jpでEdgeが開かなくて、無反応になります。


loading...