Repository URL to install this package:
|
Version:
4.0.1641 ▾
|
0
/ ®
3$ ¯
B °
° ±
!Ë ²
bë ³
õ ´
ù
µ
» ¶
·
¸
£ ¹
ª¥ º
l© »
P¬ ¼
M³ ½
˹ ¾
½ ¿
\Û À
Þâ Á
Ïí Â
ó Ã
ý Ä
| Å
ª Æ
E
Ç
 È
& É
U( Ê
}, Ë
Þ3 Ì
²@ Í
_U Î
f Ï
! Ð
ْ Ñ
¸ Ò
Ó
ø Ô
{¡ Õ
¹² Ö
]Æ ×
S× Ø
+Ú Ù
ëÞ Ú
è Û
»ð Ü
kò Ý
!ú Þ
M ß
à
á
J â
ã
Û' ä
Û/ å
7 æ
§9 ç
ô> è
´K töW u±X |òY }nZ ~ÛZ $[ [ \ {] V^ È_
r m > У © ² 2³ y¶ 9 4¿ (Æ È ¶Ì ¡Ð Ô ¸× äÚ ØÞ Ìâ $í kñ ô ø ·û ¢ÿ ¡é ¢ã £Þ
¤ ¥ ¦## §Æ' ¨) ©¹, ªÚ0 «ó7 ¬8 £; ®Ã> ¯ªB °uH ±I ²L ³°O ´ØR µ V ¶(Y ·P\ ¸x_ ¹g ºj »§m ¼Ïp ½÷s ¾w ¿?z À¯{ Á Â~
Ã_ Äҍ ÅP¨ ÆF¯ Çå° È+Ä É× ÊØ Ë´Ø Ì_Ù ÍÚ Î§Ü Ï«Ý ÐàÞ Ñeß Ò6à Óàà Ô`â Õæã ÖSå ×ëæ Ø£è ÙÑé ÚÍë Û¤î Üð Ý`ñ Þ¾ò ßô àzõ áØö â ú ãþ äU å² æ çq èRg é¢h êñi ë@k ìl í¯o î$r ï<s ðÉs ñ-t òýt ów ôzx õÅy öÚz ÷ï{ ø} ùÌ} úq~ û ü« ýx þô ÿo < ¸
3 d © , / ̙ ½
ªÕ (Ö ñÖ
¹× Ø çØ FÙ ¨Ù Ú cÚ ´Ú Û gÛ Û óÛ 9Ü Ü ÅÜ Ý QÝ Ý ÝÝ #Þ !iÞ "¯Þ #õÞ $;ß %ß &Çß '
à (Sà )à *ßà +%á ,ká -±á .÷á /=â 0â 1Éâ 2ã 3Uã 4ã 5áã 6'ä 7mä 8³ä 9ùä :?å ;å <Ùå =)æ >qæ ?¹æ @ç AIç Bç Cñç D6è Eè Föè G;é Hé IÅé J#ê Kê LÏê Më Nnë O¸ë Pì QQì Rì Sîì T<í Uí VØí W)î Xsî Y¿î Zï [Zï \©ï ]÷ï ^Eð _ð `äð a.ñ bzñ cÇñ dò edò f²ò gô h>ö iè÷ j kî+ lÛ. m~3 C <!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<script>
function setMessage(msg) {
document.getElementById('message').textContent = msg;
}
function notifyDidFinishLoading() {
if (plugin.didFinishLoading)
plugin.didFinishLoading();
}
</script>
<style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file. */
html, body {
-webkit-user-select: none;
font-family: sans-serif;
height: 100%;
margin: 0;
overflow: hidden;
text-align: center;
width: 100%;
}
h1 {
font-size: 10pt;
font-weight: normal;
padding: 0pt 10pt;
visibility: hidden;
}
#outer:hover h1, #outer:hover #close {
visibility: visible;
}
p {
font-size: 8pt;
padding: 0pt 14pt;
}
#outer {
align-items: center;
border: 1px black solid;
box-sizing: border-box;
display: flex;
height: 100%;
justify-content: center;
position: absolute;
width: 100%;
}
#close {
background-image: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg==) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAA9UlEQVR4Xu3UsWrCUByH0fMEouiuhrg4xohToJVGH0CHLBncEwfx/VvIFHLJBWmHDvKbv7PcP9f3L/fXwBsApZSRpUpEgbOnxwiReng6x4AvjdrNXRLkibubWqMcB9Yujk7qjhjmtZOji/U4wELuoBwQXa50kFsQA5jK+kQ/l5kSA4ZEK5Fo+3kcCIlGM8ijQEhUqkEeBUKiUPTyl4C5vZ1cbmdv/iqwclXY6aZwtXoFSLQqhVwmkytUWglxAMG7T0yCu4gD0v7ZBKeVxoEwFxIxYBPmIWEzDnyEeUj4HAfYdvmMcGYdsSUGsOzlIbHEv/uV38APrreiBRBIs3QAAAAASUVORK5CYII=) 2x);
background-position: right top;
background-repeat: no-repeat;
cursor: pointer;
height: 14px;
position: absolute;
right: 3px;
top: 3px;
visibility: hidden;
width: 14px;
}
#close[data-plugin-type='document'] {
display: none;
}
#close:hover {
background-image: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqUlEQVR4XqWRMQqEMBBF/1E8Ra6x6V3FRnS9QbCxtJg6Z7CzE9lTiIXXyUb3C8EULixDIMM8Zt4kcDfxM5A45U+cgeXnC1tREgkzAgob3hiq3CUHvGLG4FTQoSgxQGDrzN8WTLBGnx2IVDksen9GH7Z9hA5E6uxABMJyCHDMCEGHzugLQPPlBCBNGq+5YtpnGw1Bv+te15ypljTpVzdak5Opy+z+qf//zQ+Lg+07ay5KsgAAAABJRU5ErkJggg==) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAB4UlEQVR42u2VsWoCQRBAh+MUFP0C1V9QD4NEOxs9xBQHQVCwSJFWVBAtBNXCxk6wTkBJYUTwEwQLC61E8QP0NzZzt5g5726DkC7EYWHZ8T3WndkV2C/jLwn4hwVYBIdLn9vkLp79QcBCTDMiy3w2gQ9XeTYkEHA8vqj2rworXu3HF1YFfSWgp5QFnKVLvYvzDEKEZ5hW70oXOCtcEbQLIkx7+IQtfMBSOjU6XEF4oyOdYInZbXyOuajjDlpNeQgleIUJKUz4BDMledhqOu/AzVSmzZ49CUjCC0yvim98iqtJT2L2jKsqczsdok9XrHNexaww415lnTNwn6CM/KxJIR8bnUZHPhLO6yMoIyk2pNjLewFuE5AiY1KMMQx8Q7hQYFek4AkjxXFe1rsF84I/BTFQMGL+1Lxwl4DwdtM1gjwKohgxyLtG7SYpxALqugOMcfOKN+bFXeBsLB1uulNcRqq7/tt36k41zoL6QlxGjtd6lrahiqCi1iOFYyvXuxY8yzK33VnvUivbLlOlj/jktm0s3YnXrNIXXufHNxuOGasi8S68zkwrlnV8ZcJJsTIUxbLgQcFZWE8N0gau2p40VVcM0gYeFpSRK6445UhBuKiRgiyKw+34rLt59nb1/7+RwReVkaFtqvNBuwAAAABJRU5ErkJggg==) 2x);
}
#close:active {
background-image: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAQklEQVR4AWP4TwBSTQGDHcMZIIYAKA9VwRkwtINJgyCaCTAlCBaKAoQ+hFmoCqBKENKkK8C0gpAjCXuTyICiQ2QBAPSwyG3ByZlCAAAAAElFTkSuQmCC) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAA/ElEQVR4Xu3UsWrCUBiG4efGlIBoIMFbcnYolYJ3pg4iKGrGYFTRwaUFhYAekiDt0EG++X2W83N8/3J/DbwBMJJSsdQItcDY1VlCOImzq3Ed8OmicHASB3ns5KBw8VUNpDJrW7uAiJ3sbK1l0mqArpmFTUlQ5jYWZrrUAUSmT0SZm4qoA56JvVhs/5g3A7RLolA85A1ASOTye65NMxASK6syfxGITMzvMxG9CvRkliWwlOm9AsSOcitzU1NzK7mjuBkQvHtLK7iLBiB5PhttJSGpB8I8vM6kDuiHeUjoVwMfYR4SRtUAw1veIZzOjRhSBzCoyKFjgH/3K7+BHzg+Cgw0eSW3AAAAAElFTkSuQmCC) 2x);
}
</style>
<style>
body {
background-color: rgb(187, 187, 187);
}
#plugin_icon {
opacity: .6;
}
</style>
</head>
<body id="t" onload="notifyDidFinishLoading();">
<div i18n-values="title:name" id="outer">
<div id="inner">
<div><img id="plugin_icon" src="data:image/png;base64,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"></div>
<h1 id="message" i18n-content="message"></h1>
</div>
<div id="close" i18n-values="title:hide;data-plugin-type:pluginType"
onclick="plugin.hide();">
</div>
</div>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<script>
function insertLink() {
// Replace the chrome://plugins text with a working link (i18n_template
// doesn't allow raw HTML in template data).
var link = document.getElementById("enable_link");
var link_html = link.innerHTML;
link.parentNode.removeChild(link);
var message = document.getElementById("message");
var message_html = message.innerHTML;
message.innerHTML = message_html.replace('chrome://plugins', link_html);
}
</script>
<style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file. */
html, body {
-webkit-user-select: none;
font-family: sans-serif;
height: 100%;
margin: 0;
overflow: hidden;
text-align: center;
width: 100%;
}
h1 {
font-size: 10pt;
font-weight: normal;
padding: 0pt 10pt;
visibility: hidden;
}
#outer:hover h1, #outer:hover #close {
visibility: visible;
}
p {
font-size: 8pt;
padding: 0pt 14pt;
}
#outer {
align-items: center;
border: 1px black solid;
box-sizing: border-box;
display: flex;
height: 100%;
justify-content: center;
position: absolute;
width: 100%;
}
#close {
background-image: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg==) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAA9UlEQVR4Xu3UsWrCUByH0fMEouiuhrg4xohToJVGH0CHLBncEwfx/VvIFHLJBWmHDvKbv7PcP9f3L/fXwBsApZSRpUpEgbOnxwiReng6x4AvjdrNXRLkibubWqMcB9Yujk7qjhjmtZOji/U4wELuoBwQXa50kFsQA5jK+kQ/l5kSA4ZEK5Fo+3kcCIlGM8ijQEhUqkEeBUKiUPTyl4C5vZ1cbmdv/iqwclXY6aZwtXoFSLQqhVwmkytUWglxAMG7T0yCu4gD0v7ZBKeVxoEwFxIxYBPmIWEzDnyEeUj4HAfYdvmMcGYdsSUGsOzlIbHEv/uV38APrreiBRBIs3QAAAAASUVORK5CYII=) 2x);
background-position: right top;
background-repeat: no-repeat;
cursor: pointer;
height: 14px;
position: absolute;
right: 3px;
top: 3px;
visibility: hidden;
width: 14px;
}
#close[data-plugin-type='document'] {
display: none;
}
#close:hover {
background-image: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqUlEQVR4XqWRMQqEMBBF/1E8Ra6x6V3FRnS9QbCxtJg6Z7CzE9lTiIXXyUb3C8EULixDIMM8Zt4kcDfxM5A45U+cgeXnC1tREgkzAgob3hiq3CUHvGLG4FTQoSgxQGDrzN8WTLBGnx2IVDksen9GH7Z9hA5E6uxABMJyCHDMCEGHzugLQPPlBCBNGq+5YtpnGw1Bv+te15ypljTpVzdak5Opy+z+qf//zQ+Lg+07ay5KsgAAAABJRU5ErkJggg==) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAB4UlEQVR42u2VsWoCQRBAh+MUFP0C1V9QD4NEOxs9xBQHQVCwSJFWVBAtBNXCxk6wTkBJYUTwEwQLC61E8QP0NzZzt5g5726DkC7EYWHZ8T3WndkV2C/jLwn4hwVYBIdLn9vkLp79QcBCTDMiy3w2gQ9XeTYkEHA8vqj2rworXu3HF1YFfSWgp5QFnKVLvYvzDEKEZ5hW70oXOCtcEbQLIkx7+IQtfMBSOjU6XEF4oyOdYInZbXyOuajjDlpNeQgleIUJKUz4BDMledhqOu/AzVSmzZ49CUjCC0yvim98iqtJT2L2jKsqczsdok9XrHNexaww415lnTNwn6CM/KxJIR8bnUZHPhLO6yMoIyk2pNjLewFuE5AiY1KMMQx8Q7hQYFek4AkjxXFe1rsF84I/BTFQMGL+1Lxwl4DwdtM1gjwKohgxyLtG7SYpxALqugOMcfOKN+bFXeBsLB1uulNcRqq7/tt36k41zoL6QlxGjtd6lrahiqCi1iOFYyvXuxY8yzK33VnvUivbLlOlj/jktm0s3YnXrNIXXufHNxuOGasi8S68zkwrlnV8ZcJJsTIUxbLgQcFZWE8N0gau2p40VVcM0gYeFpSRK6445UhBuKiRgiyKw+34rLt59nb1/7+RwReVkaFtqvNBuwAAAABJRU5ErkJggg==) 2x);
}
#close:active {
background-image: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAQklEQVR4AWP4TwBSTQGDHcMZIIYAKA9VwRkwtINJgyCaCTAlCBaKAoQ+hFmoCqBKENKkK8C0gpAjCXuTyICiQ2QBAPSwyG3ByZlCAAAAAElFTkSuQmCC) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAA/ElEQVR4Xu3UsWrCUBiG4efGlIBoIMFbcnYolYJ3pg4iKGrGYFTRwaUFhYAekiDt0EG++X2W83N8/3J/DbwBMJJSsdQItcDY1VlCOImzq3Ed8OmicHASB3ns5KBw8VUNpDJrW7uAiJ3sbK1l0mqArpmFTUlQ5jYWZrrUAUSmT0SZm4qoA56JvVhs/5g3A7RLolA85A1ASOTye65NMxASK6syfxGITMzvMxG9CvRkliWwlOm9AsSOcitzU1NzK7mjuBkQvHtLK7iLBiB5PhttJSGpB8I8vM6kDuiHeUjoVwMfYR4SRtUAw1veIZzOjRhSBzCoyKFjgH/3K7+BHzg+Cgw0eSW3AAAAAElFTkSuQmCC) 2x);
}
</style>
<style>
body {
background-color: rgb(187, 187, 187);
}
#plugin_icon {
opacity: .6;
}
</style>
</head>
<body id="t" onLoad="insertLink()">
<div i18n-values="title:name" id="outer">
<div id="inner">
<div><img id="plugin_icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEEAAABBCAQAAAAk/gHOAAAIhklEQVR4Xr2ZXYidRxnHfzPvxzl7dpPdZLfaZGOzkqQ1kUAtrWkQ1FAI1kJDoSLpjfRGQUUR9K5eibdi9ULJjQjeWCJY0haMNHdS1MYKSqtma82ar2bbTXaTc/bsOe87IwzDAw8PJ3vnvMy7u/P1/Of5mv+86yLwWr381WvPDo+72rNdialO7oNI1KNxuU3GjHhjz9mDZ06PAFzkpcU3XmkerqgocWgQEYfT07VAC0AqAii9dQsB9/cHn/z6FXCv1uffjEc7dCgoBQC4Cfs0Io12LJio2locjpYG/69nHznWdy9+89JPp+jQxVPgcEq8IwJOizJ7NCAsNAETpK2hZfGH3/5+eeXLVQJQUuDxOLYvUWoCZ/wj4EQkqgaZAeC4/BQ/KR77eV10qagSBG0Al6oz4p3SkR2HALCQtZb681/8hXd1kYQ70CZQv2PaHfKAmh2lTQyIQFZ/e6iY9h4vC2oADsQ9NcTAiAF9BoxlTyIOp3arAToZL6HqvBUv6GUvsqvs0TUHOMJhDjBDn8bGj967AJOi/vZ2KPf82TLHCb7A0zzD05xkP5uMzC7FXDZ/mHavABj12R1Mc5IjdAk0VBzkFPu4SyvjJGyteNGkzqNeATAer70YIkfZR0PIwsbs5BAwFpgWihQVjgKRErt7nZ7VsmMeoiWqJQugIcqyNkmjAelMglcoLRgFB1o6xq1aWoLds8DXa9ji75kDTch5rlKCOniucYsa8RoZKSOk2MQeLQQscuWOBedZoxCLwjkuMEspoBJgAkFlAZ2aooJXYtxFB1QSL0AK1vkl38iL1bzGr1hkFk9QlnaWKcgaQZ0tUIposT55mMfhTXbosMkmvTT+Llco2E0HR1Qu3BIJOQQta3DiOwEoRUni09Cyh8Aqq7QJSJkhtLT0OMF0EhGZYS+OQQbdphrTqN1M0WWdMaUyqQagtaB2scUSn05LBoY4+rl/ipIuSFhu8gSf4iZjajxTuFS7+CzgRbaYwWnSYqKjRBQk6YKAx+f3DNATmJ6oF2SOuYmM6R32q6jSviK+oHBNICEOsaFlTnq+CrjbLDLWEUBUY1R2jHgkrg2J1fuUat9SIuv4DFoEqwdCAigQHEHYQIdqG0aIFZ+qPqIKvNGdBhJAfEH4nksQukQrYLIODNSg+KLAkzdoX5AOgUNQIWT5n4yfbCYtSHqtL3iZojyhEBFBCcLsxPBnI3A7g5aW47bsSMnHTrDRHW2/4c/WHfWs0l5GCm6zzv2wjf21eCu8osntDQ0hZ9loWVO0eBnTZIuX1HQobRQoSm7DDWCKXhINe1liKW1qaE9KvCASfIE+m0Cky1Xe4iIrFEQrcJKSJQqe4gYDljjFlzjNMzxMZNPMLJXb5ONnxIiCKd7kVW5QsMgRnmRLRFlfsEEbgJNscR9HmWFMyywnGPFHGnF2laD1zir+yQYt59lggQ43eY9ZHssAJ13xrd93eZ5GSH5DyTHe4wo71Zwy70zxnA6X+RsDujxANxlhhnMcpdJkzrqWucTeVvDGfIQu/Xx6CoR81iuC5uiwmzkKikxkugz4C48TwAbaxBxq80RFZIuQTSEJOiQISZQwHUeFA+Xfyzxu438bPWhCR9a2NmDZSiMERd0cXg2eMeLiNulpmlvMM6CR3n7yCK1B3+g0S0g28+wiqMlDPiMnhwnI1BOMJn7EC/yAf1NDJnqXuEpPG4pSuDJIgq74HA8ReYn/0KMCRtzPPGFSnlTnScjr3eAPLHCDc5zmY7R43uW3rLGgyH1O0EExpi0OcIhpIs9xhsv0iOzmOcJEgiYFJ7aHLWrm6bDBGbrsYpUbwC7RiUDA8ls8BS2Biue5zpiKjzA10QmlgoqYHi6tVDPiFjdpEw+pcFoiJTrrS7BEYpq0X/WItXGUNLQTaVtM/YmqUlPRyiUpCkVCDKFYExR8yDpTmrwq1XmGXGeVfSwkfdnz0uHYIR8BHEgmUJc7gWDukoFG8UGn9jnkZc4xRwXAKZ5giLMsi8iI1mRR5T+aO0bB5tngDk5bWGj8mLP8jiV6FEnIK8DnGSPw5e34BI2l+gJI+YL+PONZzxAsNw78l99wlB1UQKQGVnifBYLiz6kyy6rocDLf9FiHyvdDewJAYIW99Cilr2bIECSwg2jNsUCjVrbJHKGv5rbT0OqJkjnXqClwBGnrUon6dewcYGhuUagWMUR6ZDpAS7AQUj3MBcYUuaUFlpiX0doUe2kzTM2ttBbEF3QJBJt4AMfH+SRXKShyKv4sx7L6tcEAegR9kbexkemrzfaZ6QhuxfS+wjx3icCIQzxKJJg1HA6fnpZctAFsdozq21DBNQbMMRI2mXMbjpLIHlaYwSef38m6fK92+YkMGTFkjVLrUwvXxC3gkeCh4AP+xN7keD2qJAoCLXe4zQpv08n2fZ+bTGfXHDCkzx36DLjDZnp38UCwGgXlCyqxAjiGnKdJ0T9Hl5AvNmNWucYVdrELl6Au8zIfTVQMNujzAWt8yIiaTqo9CoIRHuVQj3JY60SMo0oxPWbE9bT7f0jaqtlPhQfA0/A2FxmnvjI5aYcHxCTgRCz2N+ELimbIpCSs1gEJxrUKdjCj/EgdbhPygTgt6O8L5O9LOroRWIYlSl8xMf/FSdTWuqPQV7WjAOYDJjbr6xaVJ+2V3mq0ZEQtwYI330oFijWDVawWmB4jXDvkAEoutscrTUDVRHX2KbFmeW1AgRQNcPE5esuEcvHXN4+3FKhiVa4EWRqXi7G8BRBAgDYc/D2b5ZGfXftaebi1/z0w1g6gdmGF258RXaJy7tHyd8+y5iI/3vfn17sPdinwRD14u38D2/dEwRGUcRq2lr/3rUcv846LwFvTF1549+TGI0i5l3qxDnZPXdgSmP3rode/c3Zmg0uMXR7kuI85dvD/K3e5lXgd/wNMUxeBJGbvdAAAAABJRU5ErkJggg==" /></div>
<h1 id="message" i18n-content="message"></h1>
<div id="enable_link">
<a href="#" onclick="plugin.openAboutPlugins();">chrome://plugins</a>
</div>
</div>
<div id="close" i18n-values="title:hide;data-plugin-type:pluginType"
onclick="plugin.hide();">
</div>
</div>
</body>
</html>
<!doctype html>
<html i18n-values="dir:textdirection;lang:language">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0,
maximum-scale=1.0, user-scalable=no">
<title i18n-content="title"></title>
<style>/* Copyright 2014 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. */
a {
color: #585858;
}
.bad-clock .icon {
background-image: -webkit-image-set(
url(data:image/png;base64,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) 1x,
url(data:image/png;base64,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) 2x);
}
body {
background-color: #f7f7f7;
color: #646464;
}
body.safe-browsing {
background-color: rgb(206, 52, 38);
color: white;
}
button {
-webkit-user-select: none;
background: rgb(76, 142, 250);
border: 0;
border-radius: 2px;
box-sizing: border-box;
color: #fff;
cursor: pointer;
float: right;
font-size: .875em;
margin: 0;
padding: 10px 24px;
transition: box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
[dir='rtl'] button {
float: left;
}
button:active {
background: rgb(50, 102, 213);
outline: 0;
}
button:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, .50);
}
#debugging {
display: inline;
overflow: auto;
}
.debugging-content {
line-height: 1em;
margin-bottom: 0;
margin-top: 1em;
}
.debugging-title {
font-weight: bold;
}
#details {
color: #696969;
margin: 45px 0 50px;
}
#details p:not(:first-of-type) {
margin-top: 20px;
}
#details-button {
background: inherit;
border: 0;
float: none;
margin: 0;
padding: 10px 0;
text-decoration: underline;
}
#details-button:hover {
box-shadow: inherit;
}
.error-code {
color: #777;
display: inline;
font-size: .86667em;
margin-top: 15px;
opacity: .5;
text-transform: uppercase;
}
#error-debugging-info {
font-size: 0.8em;
}
h1 {
color: #333;
font-size: 1.6em;
font-weight: normal;
line-height: 1.25em;
margin-bottom: 16px;
}
h2 {
font-size: 1.2em;
font-weight: normal;
}
.hidden {
display: none;
}
html {
-webkit-text-size-adjust: 100%;
font-size: 125%;
}
.icon {
background-repeat: no-repeat;
background-size: 100%;
height: 72px;
margin: 0 0 40px;
width: 72px;
}
input[type=checkbox] {
opacity: 0;
}
input[type=checkbox]:focus ~ .checkbox {
outline: -webkit-focus-ring-color auto 5px;
}
.interstitial-wrapper {
box-sizing: border-box;
font-size: 1em;
line-height: 1.6em;
margin: 100px auto 0;
max-width: 600px;
width: 100%;
}
#main-message > p {
display: inline;
}
#extended-reporting-opt-in {
font-size: .875em;
margin-top: 39px;
}
#extended-reporting-opt-in label {
position: relative;
}
.nav-wrapper {
margin-top: 51px;
}
.nav-wrapper::after {
clear: both;
content: '';
display: table;
width: 100%;
}
.safe-browsing :-webkit-any(
a, #details, #details-button, h1, h2, p, .small-link) {
color: white;
}
.safe-browsing button {
background-color: rgba(255, 255, 255, .15);
}
.safe-browsing button:active {
background-color: rgba(255, 255, 255, .25);
}
.safe-browsing button:hover {
box-shadow: 0 2px 3px rgba(0, 0, 0, .5);
}
.safe-browsing .error-code {
display: none;
}
.safe-browsing .icon {
background-image: -webkit-image-set(
url(data:image/png;base64,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) 1x,
url(data:image/png;base64,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) 2x);
}
.small-link {
color: #696969;
font-size: .875em;
}
.ssl .icon {
background-image: -webkit-image-set(
url(data:image/png;base64,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) 1x,
url(data:image/png;base64,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) 2x);
}
.captive-portal .icon {
background-image: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAQAAAD/5HvMAAAEyElEQVR4Xu3afWhVZRwH8LvdmjVSjKyQbKazia1k1sL6YzXoZdy28/J8f5XRC7hwEVk0hZQCXY2SipCgYgappXMQ5Ya9ELZqoEM3VHqxBkapm21jOBWmbLfudk+0Peeu2+mc3zk9514WbN9/zz18ds75Pc/vPM+JTP9NZzrT+b9Fn2nGxHrainb8ggEaRgLn0IOjaMFroloU/Ssg1yxBLZpxBD00QufpODrRiFX6IiUKXY8XcRAJsryC3nRL7EpsQJ/r0V3iSS0/MKU4D6uoQwLY/P1q4i0aYX9xVqyPRH1jbr8UtfSbE8CDqIK60wBjOEb7aDf24CBO/gN12LjJF4dM+dPAoNgMdNkUxLENQltAFViJdbTWfFTcaRbiMXyCpE1CM4vR5uAzdwB/hcxCDI5jXqGFeAadNJoGH0ErVhg3YPs46Ad9JldNS3GCAbDPkFGG/eJGWktn3PA4IUB3oxPzGY4AXSBLCSTLgfZxx2MLV1Q5tFHeWxWQDNalHulWeo4q9GW4VVShng5J0KgZ8+bkYpcEqIJk6H2y0CiW4H58gGMYQhyn8DlqUUltZIk1XF01kKUW5yhm3gVB3Q76MDaRyXBQR1aIIJnyi6jNZUgs8uTQfUiGDJIxZ9OPfxEwhAO0F70To5NR5skxrsUgWaGDZLQCHBHVxnKxWjwuivRS7McK7na1kpUZkEwUzXbFiZfZcVk8pE6xZ3u34AV5TFJUsV2ObBHU0+Q5wu0eJ21krw+eD4lzBvOYf7wLeyI5DEfLx2l1DPqoSXK8SItis/gm41mlago/9P2UApklZE0pkHhdAv6YIiB8NwEQL9HvUwCkzZHz15g5Gw8jmR0QtsizvOoACUjAIdlOZQUkHpCgDqd1gwRslhX3djZAmDsBwpATJPtDUZPqGFuyUWU453IWu7fFHZOvhjiQ+aqiDheQ/bJjXpf2oP+caRA+cgPJpkxckeZfiIHMgmirC8gee4rz0qF6KV3IJEi86R8kg0oazQ6Iv2Uyoiajt4x/qJ1BfcCe6JQZC/BQ82XvDLYHJPUEK3tmYHTmlotpbzCSwsDomDpc+mD6VgHETB3M5Or2c5xUBvGTq7P98DjBEjqrBOLbD2eDZj7o+aJdhnhooJxUbd/r1cJ+yl1mJMMBGWUSP6zlezT5SIirGNKacEB4bwKEXcxrEN7gThUGSCtILaZXMC+KiGN+5kHYITndkSj7Ko3GTIPMEvtJFKt9LDYgiZtVQTLnqUPUOJcV8KWE95df4m85ps0+CQ/ig68xN72lscF4yveCFTa5g9AXnDR5lczC1Jr+4UhuCsAv6YlH3EDURFbgPCG3Y2altmPG9FIJ8LnoOYLbXNDzgi+Myvkqii9sIuocAHZZuN9tAKi6hnaiBwka9f942y2rzFcut4tZOO+3r5J6xcVmYEeKc9xlPuC3FhDHynBA6LQ5GGA2ftnNl82RqDrIDgb1ZU4AnxzUTc7raDeWhwPCUW0BA/C7gYeP9cWqILSUX8YAgmxxIoF3tYL/DkK9Y/znwm8CI4lvRLV+dVAQThOpOPht8p+ogZ4W9/jijFFD5eXhWPgPCXjMh/pSVYD6pxaywPGOKOIBWfkYhXYKozgvy5/rYBva8avzcx19sSrgT2jJnjJMVMiUAAAAAElFTkSuQmCC) 1x,
url(data:image/png;base64,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) 2x);
}
.checkbox {
background: transparent;
border: 1px solid white;
border-radius: 2px;
display: block;
height: 14px;
left: 0;
position: absolute;
right: 0;
top: -1px;
width: 14px;
}
.checkbox::before {
background: transparent;
border: 2px solid white;
border-right-width: 0;
border-top-width: 0;
content: '';
height: 4px;
left: 2px;
opacity: 0;
position: absolute;
top: 3px;
transform: rotate(-45deg);
width: 9px;
}
.ssl-opt-in .checkbox {
border-color: #696969;
}
.ssl-opt-in .checkbox::before {
border-color: #696969;
}
input[type=checkbox]:checked ~ .checkbox::before {
opacity: 1;
}
@media (max-width: 700px) {
.interstitial-wrapper {
padding: 0 10%;
}
#error-debugging-info {
overflow: auto;
}
}
@media (max-height: 600px) {
.error-code {
margin-top: 10px;
}
}
@media (max-width: 420px) {
button,
[dir='rtl'] button,
.small-link {
float: none;
font-size: .825em;
font-weight: 400;
margin: 0;
text-transform: uppercase;
width: 100%;
}
#details {
margin: 20px 0 20px 0;
}
#details p:not(:first-of-type) {
margin-top: 10px;
}
#details-button {
display: block;
margin-top: 20px;
text-align: center;
width: 100%;
}
.interstitial-wrapper {
padding: 0 5%;
}
#extended-reporting-opt-in {
margin-top: 24px;
}
.nav-wrapper {
margin-top: 30px;
}
}
/**
* Mobile specific styling.
* Navigation buttons are anchored to the bottom of the screen.
* Details message replaces the top content in its own scrollable area.
*/
@media (max-width: 420px) and (max-height: 736px) and (orientation: portrait) {
#details-button {
border: 0;
margin: 8px 0 0;
}
.secondary-button {
-webkit-margin-end: 0;
margin-top: 16px;
}
}
/* Fixed nav. */
@media (min-width: 240px) and (max-width: 420px) and
(min-height: 401px) and (max-height: 736px) and (orientation:portrait),
(min-width: 421px) and (max-width: 736px) and (min-height: 240px) and
(max-height: 420px) and (orientation:landscape) {
body .nav-wrapper {
background: #f7f7f7;
bottom: 0;
box-shadow: 0 -22px 40px rgb(247, 247, 247);
left: 0;
margin: 0;
max-width: 736px;
padding-left: 24px;
padding-right: 24px;
position: fixed;
z-index: 1;
}
body.safe-browsing .nav-wrapper {
background: rgb(206, 52, 38);
box-shadow: 0 -22px 40px rgb(206, 52, 38);
}
.interstitial-wrapper {
max-width: 736px;
}
#details,
#main-content {
padding-bottom: 40px;
}
}
@media (max-width: 420px) and (max-height: 736px) and (orientation: portrait),
(max-width: 736px) and (max-height: 420px) and (orientation: landscape) {
body {
margin: 0 auto;
}
button,
[dir='rtl'] button,
button.small-link {
font-family: Roboto-Regular,Helvetica;
font-size: .933em;
font-weight: 600;
margin: 6px 0;
text-transform: uppercase;
}
.nav-wrapper {
box-sizing: border-box;
padding-bottom: 8px;
width: 100%;
}
.error-code {
margin-top: 0;
}
#details {
box-sizing: border-box;
height: auto;
margin: 0;
opacity: 1;
transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1);
}
#details.hidden,
#main-content.hidden {
display: block;
height: 0;
opacity: 0;
overflow: hidden;
}
#details-button {
padding-bottom: 16px;
padding-top: 16px;
}
h1 {
font-size: 1.5em;
margin-bottom: 8px;
}
.icon {
margin-bottom: 12px;
}
.interstitial-wrapper {
box-sizing: border-box;
margin: 24px auto 12px;
padding: 0 24px;
position: relative;
}
.interstitial-wrapper p {
font-size: .95em;
line-height: 1.61em;
margin-top: 8px;
}
#main-content {
margin: 0;
transition: opacity 100ms cubic-bezier(0.4, 0, 0.2, 1);
}
.small-link {
border: 0;
}
.suggested-left > #control-buttons,
.suggested-right > #control-buttons {
float: none;
margin: 0;
}
}
@media (min-height: 400px) and (orientation:portrait) {
body:not(.extended-reporting-has-checkbox) .interstitial-wrapper {
margin-bottom: 145px;
}
}
@media (min-height: 299px) and (orientation:portrait) {
.nav-wrapper {
padding-bottom: 16px;
}
}
@media (min-height: 405px) and (max-height: 736px) and
(max-width: 420px) and (orientation:portrait) {
.icon {
margin-bottom: 24px;
}
.interstitial-wrapper {
margin-top: 64px;
}
}
@media (min-height: 480px) and (max-width: 420px) and
(max-height: 736px) and (orientation: portrait),
(min-height: 338px) and (max-height: 420px) and (max-width: 736px) and
(orientation: landscape) {
.icon {
margin-bottom: 24px;
}
.nav-wrapper {
padding-bottom: 24px;
}
}
@media (min-height: 500px) and (max-width: 414px) and (orientation: portrait) {
:not(.extended-reporting-has-checkbox) .interstitial-wrapper {
margin-top: 96px;
}
}
/* Phablet sizing */
@media (min-width: 375px) and (min-height: 641px) and (max-height: 736px) and
(max-width: 414px) and (orientation: portrait) {
button,
[dir='rtl'] button,
.small-link {
font-size: 1em;
padding-bottom: 12px;
padding-top: 12px;
}
body:not(.offline) .icon {
height: 80px;
width: 80px;
}
#details-button {
margin-top: 28px;
}
h1 {
font-size: 1.7em;
}
.icon {
margin-bottom: 28px;
}
.interstitial-wrapper {
padding: 28px;
}
.interstitial-wrapper p {
font-size: 1.05em;
}
.nav-wrapper {
padding: 28px;
}
}
@media (min-width: 420px) and (max-width: 736px) and
(min-height: 240px) and (max-height: 298px) and
(orientation:landscape) {
body:not(.offline) .icon {
height: 50px;
width: 50px;
}
.icon {
padding-top: 0;
}
.interstitial-wrapper {
margin-top: 16px;
}
.nav-wrapper {
padding: 0 24px 8px;
}
}
@media (min-width: 420px) and (max-width: 736px) and
(min-height: 240px) and (max-height: 420px) and
(orientation:landscape) {
#details-button {
margin: 0;
}
.interstitial-wrapper {
margin-bottom: 70px;
}
.nav-wrapper {
margin-top: 0;
}
#extended-reporting-opt-in {
margin-top: 0;
}
}
/* Phablet landscape */
@media (min-width: 680px) and (max-height: 414px) {
.interstitial-wrapper {
margin: 24px auto;
}
.nav-wrapper {
margin: 16px auto 0;
}
}
@media (max-height: 240px) and (orientation: landscape),
(max-height: 480px) and (orientation: portrait),
(max-width: 419px) and (max-height: 323px) {
body:not(.offline) .icon {
height: 56px;
width: 56px;
}
.icon {
margin-bottom: 16px;
}
}
/* Small mobile screens. No fixed nav. */
@media (max-height: 400px) and (orientation: portrait),
(max-height: 239px) and (orientation: landscape),
(max-width: 419px) and (max-height: 399px) {
.interstitial-wrapper {
display: flex;
flex-direction: column;
margin-bottom: 0;
}
#details {
flex: 1 1 auto;
order: 0;
}
#main-content {
flex: 1 1 auto;
order: 0;
}
.nav-wrapper {
flex: 0 1 auto;
margin-top: 8px;
order: 1;
padding-left: 0;
padding-right: 0;
position: relative;
width: 100%;
}
}
/* Extended reporting opt-in. No fixed nav. */
@media (max-height: 736px) and (orientation: portrait),
(max-height: 360px) and (max-width: 680px) and (orientation: landscape) {
.extended-reporting-has-checkbox .interstitial-wrapper {
display: flex;
flex-direction: column;
margin-bottom: 0;
}
.extended-reporting-has-checkbox #details {
flex: 1 1 auto;
order: 0;
}
.extended-reporting-has-checkbox #main-content {
flex: 1 1 auto;
order: 0;
padding-bottom: 0;
}
.extended-reporting-has-checkbox #extended-reporting-opt-in {
margin-bottom: 8px;
}
body.extended-reporting-has-checkbox .nav-wrapper {
flex: 0 1 auto;
margin-top: 0;
order: 1;
padding-left: 0;
padding-right: 0;
position: relative;
width: 100%;
}
}
@media (max-width: 239px) and (orientation: portrait) {
.nav-wrapper {
padding-left: 0;
padding-right: 0;
}
}
</style>
<style>/* Copyright 2013 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file. */
/* Don't use the main frame div when the error is in a subframe. */
html[subframe] #main-frame-error {
display: none;
}
/* Don't use the subframe error div when the error is in a main frame. */
html:not([subframe]) #sub-frame-error {
display: none;
}
#diagnose-button {
-webkit-margin-start: 0;
float: none;
margin-bottom: 10px;
margin-top: 20px;
}
h1 {
margin-top: 0;
}
h2 {
color: #666;
font-size: 1.2em;
font-weight: normal;
margin: 10px 0;
}
a {
color: rgb(17, 85, 204);
text-decoration: none;
}
.icon {
-webkit-user-select: none;
content: '';
}
.icon-generic {
/**
* Can't access chrome://theme/IDR_ERROR_NETWORK_GENERIC from an untrusted
* renderer process, so embed the resource manually.
*/
content: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAENJREFUeF7tzbEJACEQRNGBLeAasBCza2lLEGx0CxFGG9hBMDDxRy/72O9FMnIFapGylsu1fgoBdkXfUHLrQgdfrlJN1BdYBjQQm3UAAAAASUVORK5CYII=) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQAQMAAADdiHD7AAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAFJJREFUeF7t0cENgDAMQ9FwYgxG6WjpaIzCCAxQxVggFuDiCvlLOeRdHR9yzjncHVoq3npu+wQUrUuJHylSTmBaespJyJQoObUeyxDQb3bEm5Au81c0pSCD8HYAAAAASUVORK5CYII=) 2x);
}
.icon-offline {
content: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAABlBMVEUAAABTU1OoaSf/AAAAAXRSTlMAQObYZgAAAGxJREFUeF7tyMEJwkAQRuFf5ipMKxYQiJ3Z2nSwrWwBA0+DQZcdxEOueaePp9+dQZFB7GpUcURSVU66yVNFj6LFICatThZB6r/ko/pbRpUgilY0Cbw5sNmb9txGXUKyuH7eV25x39DtJXUNPQGJtWFV+BT/QAAAAABJRU5ErkJggg==) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACQBAMAAAAVaP+LAAAAGFBMVEUAAABTU1NNTU1TU1NPT09SUlJSUlJTU1O8B7DEAAAAB3RSTlMAoArVKvVgBuEdKgAAAJ1JREFUeF7t1TEOwyAMQNG0Q6/UE+RMXD9d/tC6womIFSL9P+MnAYOXeTIzMzMzMzMzaz8J9Ri6HoITmuHXhISE8nEh9yxDh55aCEUoTGbbQwjqHwIkRAEiIaG0+0AA9VBMaE89Rogeoww936MQrWdBr4GN/z0IAdQ6nQ/FIpRXDwHcA+JIJcQowQAlFUA0MfQpXLlVQfkzR4igS6ENjknm/wiaGhsAAAAASUVORK5CYII=) 2x);
position: relative;
}
.icon-disabled {
content: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHAAAABICAMAAAAZF4G5AAAABlBMVEVMaXFTU1OXUj8tAAAAAXRSTlMAQObYZgAAASZJREFUeAHd11Fq7jAMRGGf/W/6PoWB67YMqv5DybwG/CFjRuR8JBw3+ByiRjgV9W/TJ31P0tBfC6+cj1haUFXKHmVJo5wP98WwQ0ZCbfUc6LQ6VuUBz31ikADkLMkDrfUC4rR6QGW+gF6rx7NaHWCj1Y/W6lf4L7utvgBSt3rBFSS/XBMPUILcJINHCBWYUfpWn4NBi1ZfudIc3rf6/NGEvEA+AsYTJozmXemjXeLZAov+mnkN2HfzXpMSVQDnGw++57qNJ4D1xitA2sJ+VAWMygSEaYf2mYPTjZfk2K8wmP7HLIH5Mg4/pP+PEcDzUvDMvYbs/2NWwPO5vBdMZE4EE5UTQLiBFDaUlTDPBRoJ9HdAYIkIo06og3BNXtCzy7zA1aXk5x+tJARq63eAygAAAABJRU5ErkJggg==) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOAAAACQAQMAAAArwfVjAAAABlBMVEVMaXFTU1OXUj8tAAAAAXRSTlMAQObYZgAAAYdJREFUeF7F1EFqwzAUBNARAmVj0FZe5QoBH6BX+dn4GlY2PYNzGx/A0CvkCIJuvIraKJKbgBvzf2g62weDGD7CYggpfFReis4J0ey9EGFIiEQQojFSlA9kSIiqd0KkFjKsewgRbStEN19mxUPTtmW9HQ/h6tyqNQ8NlSMZdzyE6qkoE0trVYGFm0n1WYeBhduzwbwBC7voS+vIxfeMjeaiLxsMMtQNwMPtuew+DjzcTHk8YMfDknEcIUOtf2lVfgVH3K4Xv5PRYAXRVMtItIJ3rfaCIVn9DsTH2NxisAVRex2Hh3hX+/mRUR08bAwPEYsI51ZxWH4Q0SpicQRXeyEaIug48FEdegARfMz/tADVsRciwTAxW308ehmC2gLraC+YCbV3QoTZexa+zegAEW5PhhgYfmbvJgcRqngGByOSXdFJcLk2JeDPEN0kxe1JhIt5FiFA+w+ItMELsUyPF2IaJ4aILqb4FbxPwhImwj6JauKgDUCYaxmYIsd4KXdMjIC9ItB5Bn4BNRwsG0XM2nwAAAAASUVORK5CYII=) 2x);
width: 112px;
}
.error-code {
display: block;
}
#content-top {
margin: 20px;
}
#help-box-inner {
background-color: #f9f9f9;
border-top: 1px solid #EEE;
color: #444;
padding: 20px;
text-align: start;
}
.hidden {
display: none;
}
#suggestion {
margin-top: 15px;
}
#short-suggestion {
margin-top: 5px;
}
#sub-frame-error-details {
color: #8F8F8F;
/* Not done on mobile for performance reasons. */
text-shadow: 0 1px 0 rgba(255,255,255,0.3);
}
[jscontent=failedUrl] {
overflow-wrap: break-word;
}
#search-container {
/* Prevents a space between controls. */
display: flex;
margin-top: 20px;
}
#search-box {
border: 1px solid #cdcdcd;
flex-grow: 1;
font-size: 16px;
height: 26px;
margin-right: 0;
padding: 1px 9px;
}
#search-box:focus {
border: 1px solid rgb(93, 154, 255);
outline: none;
}
#search-button {
border: none;
border-bottom-left-radius: 0;
border-top-left-radius: 0;
box-shadow: none;
display: flex;
height: 30px;
margin: 0;
padding: 0;
width: 60px;
}
#search-image {
content:
-webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAPCAQAAAB+HTb/AAAArElEQVR4Xn3NsUoCUBzG0XvB3U0chR4geo5qihpt6gkCx0bXFsMERWj2KWqIanAvmlUUoQapwU6g4l8H5bd9Z/iSPS0hu/RqZqrncBuzLl7U3Rn4cSpQFTeroejJl1Lgs7f4ceDPdeBMXYp86gaONYJkY83AnqHiGk9wHnjk16PKgo5N9BUCkzPf5j6M0PfuVg5MymoetFwoaKAlB26WdXAvJ7u5mezitqtkT//7Sv/u96CaLQAAAABJRU5ErkJggg==) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAeCAQAAACVzLYUAAABYElEQVR4Xr3VMUuVURzH8XO98jgkGikENkRD0KRGDUVDQy0h2SiC4IuIiktL4AvQt1CDBJUJwo1KXXS6cWdHw7tcjWwoC5Hrx+UZgnNO5CXiO/75jD/+QZf9MzjskVU7DrU1zRv9G9ir5hsA4Nii83+GA9ZI1nI1D6tWAE1TRlQMuuuFDthzMQefgo4nKr+f3dIGDdUUHPYD1ISoMQdgJgUfgqaKEOcxWE/BVTArJBvwC0cGY7gNLgiZNsD1GP4EPVn4EtyLYRuczcJ34HYMP4E7GdajDS7FcB48z8AJ8FmI4TjouBkzZ2yBuRQMlsButIZ+dfDVUBqOaIHvavpLVHXfFmAqv45r9gEHNr3y3hcAfLSgSMPgiiZR+6Z9AMuKNAwqpjUcA2h55pxgAfBWkYRlQ254YMJloaxPHbCkiGCymL5RlLA7GnRDXyuC7uhicLoKdRyaDE5Pl00K//93nABqPgBDK8sfWgAAAABJRU5ErkJggg==) 2x);
margin: auto;
}
.secondary-button {
-webkit-margin-end: 16px;
background: #d9d9d9;
color: #696969;
}
.snackbar {
background: #323232;
border-radius: 2px;
bottom: 24px;
box-sizing: border-box;
color: #fff;
font-size: .87em;
left: 24px;
max-width: 568px;
min-width: 288px;
opacity: 0;
padding: 16px 24px 12px;
position: fixed;
transform: translateY(90px);
will-change: opacity, transform;
z-index: 999;
}
.snackbar-show {
-webkit-animation:
show-snackbar .25s cubic-bezier(0.0, 0.0, 0.2, 1) forwards,
hide-snackbar .25s cubic-bezier(0.4, 0.0, 1, 1) forwards 5s;
}
@-webkit-keyframes show-snackbar {
100% {
opacity: 1;
transform: translateY(0);
}
}
@-webkit-keyframes hide-snackbar {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(90px);
}
}
.suggestions {
margin-top: 18px;
}
.suggestion-header {
font-weight: bold;
margin-bottom: 4px;
}
.suggestion-body {
color: #777;
}
/* Increase line height at higher resolutions. */
@media (min-width: 641px) and (min-height: 641px) {
#help-box-inner {
line-height: 18px;
}
}
/* Decrease padding at low sizes. */
@media (max-width: 640px), (max-height: 640px) {
h1 {
margin: 0 0 15px;
}
#content-top {
margin: 15px;
}
#help-box-inner {
padding: 20px;
}
.suggestions {
margin-top: 10px;
}
.suggestion-header {
margin-bottom: 0;
}
.error-code {
margin-top: 10px;
}
}
/* Don't allow overflow when in a subframe. */
html[subframe] body {
overflow: hidden;
}
#sub-frame-error {
-webkit-align-items: center;
background-color: #DDD;
display: -webkit-flex;
-webkit-flex-flow: column;
height: 100%;
-webkit-justify-content: center;
left: 0;
position: absolute;
top: 0;
transition: background-color .2s ease-in-out;
width: 100%;
}
#sub-frame-error:hover {
background-color: #EEE;
}
#sub-frame-error .icon-generic {
margin: 0 0 16px;
}
#sub-frame-error-details {
margin: 0 10px;
text-align: center;
visibility: hidden;
}
/* Show details only when hovering. */
#sub-frame-error:hover #sub-frame-error-details {
visibility: visible;
}
/* If the iframe is too small, always hide the error code. */
/* TODO(mmenke): See if overflow: no-display works better, once supported. */
@media (max-width: 200px), (max-height: 95px) {
#sub-frame-error-details {
display: none;
}
}
/* Adjust icon for small embedded frames in apps. */
@media (max-height: 100px) {
#sub-frame-error .icon-generic {
height: auto;
margin: 0;
padding-top: 0;
width: 25px;
}
}
/* details-button is special; it's a <button> element that looks like a link. */
#details-button {
box-shadow: none;
min-width: 0;
}
/* Styles for platform dependent separation of controls and details button. */
.suggested-left > #control-buttons,
.suggested-left #stale-load-button,
.suggested-right > #details-button {
float: left;
}
.suggested-right > #control-buttons,
.suggested-right #stale-load-button,
.suggested-left > #details-button {
float: right;
}
.suggested-left .secondary-button {
-webkit-margin-end: 0px;
-webkit-margin-start: 16px;
}
#details-button.singular {
float: none;
}
#buttons::after {
clear: both;
content: '';
display: block;
width: 100%;
}
/* Offline page */
.offline .interstitial-wrapper {
color: #2b2b2b;
font-size: 1em;
line-height: 1.55;
margin: 0 auto;
max-width: 600px;
padding-top: 100px;
width: 100%;
}
.offline .runner-container {
height: 150px;
max-width: 600px;
overflow: hidden;
position: absolute;
top: 35px;
width: 44px;
}
.offline .runner-canvas {
height: 150px;
max-width: 600px;
opacity: 1;
overflow: hidden;
position: absolute;
top: 0;
z-index: 2;
}
.offline .controller {
background: rgba(247,247,247, .1);
height: 100vh;
left: 0;
position: absolute;
top: 0;
width: 100vw;
z-index: 1;
}
#offline-resources {
display: none;
}
@media (max-width: 420px) {
.suggested-left > #control-buttons,
.suggested-right > #control-buttons {
float: none;
}
.snackbar {
left: 0;
bottom: 0;
width: 100%;
border-radius: 0;
}
}
@media (max-height: 350px) {
h1 {
margin: 0 0 15px;
}
.icon-offline {
margin: 0 0 10px;
}
.interstitial-wrapper {
margin-top: 5%;
}
.nav-wrapper {
margin-top: 30px;
}
}
@media (min-width: 600px) and (max-width: 736px) and (orientation: landscape) {
.offline .interstitial-wrapper {
margin-left: 0;
margin-right: 0;
}
}
@media (min-width: 420px) and (max-width: 736px) and
(min-height: 240px) and (max-height: 420px) and
(orientation:landscape) {
.interstitial-wrapper {
margin-bottom: 100px;
}
}
@media (min-height: 240px) and (orientation: landscape) {
.offline .interstitial-wrapper {
margin-bottom: 90px;
}
.icon-offline {
margin-bottom: 20px;
}
}
@media (max-height: 320px) and (orientation: landscape) {
.icon-offline {
margin-bottom: 0;
}
.offline .runner-container {
top: 10px;
}
}
@media (max-width: 240px) {
button {
padding-left: 12px;
padding-right: 12px;
}
.interstitial-wrapper {
overflow: inherit;
padding: 0 8px;
}
}
@media (max-width: 120px) {
button {
width: auto;
}
}</style>
<script>// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var mobileNav = false;
/**
* For small screen mobile the navigation buttons are moved
* below the advanced text.
*/
function onResize() {
var helpOuterBox = document.querySelector('#details');
var mainContent = document.querySelector('#main-content');
var mediaQuery = '(min-width: 240px) and (max-width: 420px) and ' +
'(max-height: 736px) and (orientation: portrait),' +
'(max-width: 736px) and (max-height: 420px) and (orientation: landscape)';
var detailsHidden = helpOuterBox.classList.contains('hidden');
var runnerContainer = document.querySelector('.runner-container');
// Check for change in nav status.
if (mobileNav != window.matchMedia(mediaQuery).matches) {
mobileNav = !mobileNav;
// Handle showing the top content / details sections according to state.
if (mobileNav) {
mainContent.classList.toggle('hidden', !detailsHidden);
helpOuterBox.classList.toggle('hidden', detailsHidden);
if (runnerContainer) {
runnerContainer.classList.toggle('hidden', !detailsHidden);
}
} else if (!detailsHidden) {
// Non mobile nav with visible details.
mainContent.classList.remove('hidden');
helpOuterBox.classList.remove('hidden');
if (runnerContainer) {
runnerContainer.classList.remove('hidden');
}
}
}
}
function setupMobileNav() {
window.addEventListener('resize', onResize);
onResize();
}
document.addEventListener('DOMContentLoaded', setupMobileNav);
</script>
<script>// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
function toggleHelpBox() {
var helpBoxOuter = document.getElementById('details');
helpBoxOuter.classList.toggle('hidden');
var detailsButton = document.getElementById('details-button');
if (helpBoxOuter.classList.contains('hidden'))
detailsButton.innerText = detailsButton.detailsText;
else
detailsButton.innerText = detailsButton.hideDetailsText;
// Details appears over the main content on small screens.
if (mobileNav) {
document.getElementById('main-content').classList.toggle('hidden');
var runnerContainer = document.querySelector('.runner-container');
if (runnerContainer) {
runnerContainer.classList.toggle('hidden');
}
}
}
function diagnoseErrors() {
var extensionId = 'idddmepepmjcgiedknnmlbadcokidhoa';
var diagnoseFrame = document.getElementById('diagnose-frame');
diagnoseFrame.innerHTML =
'<iframe src="chrome-extension://' + extensionId +
'/index.html"></iframe>';
}
// Subframes use a different layout but the same html file. This is to make it
// easier to support platforms that load the error page via different
// mechanisms (Currently just iOS).
if (window.top.location != window.location)
document.documentElement.setAttribute('subframe', '');
// Re-renders the error page using |strings| as the dictionary of values.
// Used by NetErrorTabHelper to update DNS error pages with probe results.
function updateForDnsProbe(strings) {
var context = new JsEvalContext(strings);
jstProcess(context, document.getElementById('t'));
}
// Given the classList property of an element, adds an icon class to the list
// and removes the previously-
function updateIconClass(classList, newClass) {
var oldClass;
if (classList.hasOwnProperty('last_icon_class')) {
oldClass = classList['last_icon_class'];
if (oldClass == newClass)
return;
}
classList.add(newClass);
if (oldClass !== undefined)
classList.remove(oldClass);
classList['last_icon_class'] = newClass;
if (newClass == 'icon-offline') {
document.body.classList.add('offline');
new Runner('.interstitial-wrapper');
} else {
document.body.classList.add('neterror');
}
}
// Does a search using |baseSearchUrl| and the text in the search box.
function search(baseSearchUrl) {
var searchTextNode = document.getElementById('search-box');
document.location = baseSearchUrl + searchTextNode.value;
return false;
}
// Use to track clicks on elements generated by the navigation correction
// service. If |trackingId| is negative, the element does not come from the
// correction service.
function trackClick(trackingId) {
// This can't be done with XHRs because XHRs are cancelled on navigation
// start, and because these are cross-site requests.
if (trackingId >= 0 && errorPageController)
errorPageController.trackClick(trackingId);
}
// Called when an <a> tag generated by the navigation correction service is
// clicked. Separate function from trackClick so the resources don't have to
// be updated if new data is added to jstdata.
function linkClicked(jstdata) {
trackClick(jstdata.trackingId);
}
// Implements button clicks. This function is needed during the transition
// between implementing these in trunk chromium and implementing them in
// iOS.
function reloadButtonClick(url) {
if (window.errorPageController) {
errorPageController.reloadButtonClick();
} else {
location = url;
}
}
function showSavedCopyButtonClick() {
if (window.errorPageController) {
errorPageController.showSavedCopyButtonClick();
}
}
function detailsButtonClick() {
if (window.errorPageController)
errorPageController.detailsButtonClick();
}
/**
* Replace the reload button with the Google cached copy suggestion.
*/
function setUpCachedButton(buttonStrings) {
var reloadButton = document.getElementById('reload-button');
reloadButton.textContent = buttonStrings.msg;
var url = buttonStrings.cacheUrl;
var trackingId = buttonStrings.trackingId;
reloadButton.onclick = function(e) {
e.preventDefault();
trackClick(trackingId);
if (window.errorPageController) {
errorPageController.trackCachedCopyButtonClick(
buttonStrings.defaultLabel);
}
location = url;
};
reloadButton.style.display = '';
document.getElementById('control-buttons').hidden = false;
}
var primaryControlOnLeft = true;
primaryControlOnLeft = false;
function onDocumentLoad() {
var controlButtonDiv = document.getElementById('control-buttons');
var reloadButton = document.getElementById('reload-button');
var detailsButton = document.getElementById('details-button');
var showSavedCopyButton = document.getElementById('show-saved-copy-button');
var primaryButton, secondaryButton;
if (showSavedCopyButton.primary) {
primaryButton = showSavedCopyButton;
secondaryButton = reloadButton;
} else {
primaryButton = reloadButton;
secondaryButton = showSavedCopyButton;
}
// Sets up the proper button layout for the current platform.
if (primaryControlOnLeft) {
buttons.classList.add('suggested-left');
controlButtonDiv.insertBefore(secondaryButton, primaryButton);
} else {
buttons.classList.add('suggested-right');
controlButtonDiv.insertBefore(primaryButton, secondaryButton);
}
if (reloadButton.style.display == 'none' &&
showSavedCopyButton.style.display == 'none') {
detailsButton.classList.add('singular');
}
// Hide the details button if there are no details to show.
if (loadTimeData.valueExists('summary') &&
!loadTimeData.getValue('summary').msg) {
detailsButton.style.display = 'none';
}
// Show control buttons.
if (loadTimeData.valueExists('reloadButton') &&
loadTimeData.getValue('reloadButton').msg ||
loadTimeData.valueExists('showSavedCopyButton') &&
loadTimeData.getValue('showSavedCopyButton').msg) {
controlButtonDiv.hidden = false;
// Set the secondary button state in the cases of two call to actions.
if (loadTimeData.valueExists('reloadButton') &&
loadTimeData.getValue('reloadButton').msg &&
loadTimeData.valueExists('showSavedCopyButton') &&
loadTimeData.getValue('showSavedCopyButton').msg) {
secondaryButton.classList.add('secondary-button');
}
}
// Add a main message paragraph.
if (loadTimeData.valueExists('primaryParagraph')) {
var p = document.querySelector('#main-message p');
p.innerHTML = loadTimeData.getString('primaryParagraph');
p.hidden = false;
}
// Check for Google cached copy suggestion.
if (loadTimeData.valueExists('cacheButton')) {
setUpCachedButton(loadTimeData.getValue('cacheButton'));
}
}
document.addEventListener('DOMContentLoaded', onDocumentLoad);
</script>
<script>// Copyright (c) 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function() {
'use strict';
/**
* T-Rex runner.
* @param {string} outerContainerId Outer containing element id.
* @param {Object} opt_config
* @constructor
* @export
*/
function Runner(outerContainerId, opt_config) {
// Singleton
if (Runner.instance_) {
return Runner.instance_;
}
Runner.instance_ = this;
this.outerContainerEl = document.querySelector(outerContainerId);
this.containerEl = null;
this.snackbarEl = null;
this.detailsButton = this.outerContainerEl.querySelector('#details-button');
this.config = opt_config || Runner.config;
this.dimensions = Runner.defaultDimensions;
this.canvas = null;
this.canvasCtx = null;
this.tRex = null;
this.distanceMeter = null;
this.distanceRan = 0;
this.highestScore = 0;
this.time = 0;
this.runningTime = 0;
this.msPerFrame = 1000 / FPS;
this.currentSpeed = this.config.SPEED;
this.obstacles = [];
this.started = false;
this.activated = false;
this.crashed = false;
this.paused = false;
this.resizeTimerId_ = null;
this.playCount = 0;
// Sound FX.
this.audioBuffer = null;
this.soundFx = {};
// Global web audio context for playing sounds.
this.audioContext = null;
// Images.
this.images = {};
this.imagesLoaded = 0;
if (this.isDisabled()) {
this.setupDisabledRunner();
} else {
this.loadImages();
}
}
window['Runner'] = Runner;
/**
* Default game width.
* @const
*/
var DEFAULT_WIDTH = 600;
/**
* Frames per second.
* @const
*/
var FPS = 60;
/** @const */
var IS_HIDPI = window.devicePixelRatio > 1;
/** @const */
var IS_IOS = window.navigator.userAgent.indexOf('CriOS') > -1 ||
window.navigator.userAgent == 'UIWebViewForStaticFileContent';
/** @const */
var IS_MOBILE = window.navigator.userAgent.indexOf('Mobi') > -1 || IS_IOS;
/** @const */
var IS_TOUCH_ENABLED = 'ontouchstart' in window;
/**
* Default game configuration.
* @enum {number}
*/
Runner.config = {
ACCELERATION: 0.001,
BG_CLOUD_SPEED: 0.2,
BOTTOM_PAD: 10,
CLEAR_TIME: 3000,
CLOUD_FREQUENCY: 0.5,
GAMEOVER_CLEAR_TIME: 750,
GAP_COEFFICIENT: 0.6,
GRAVITY: 0.6,
INITIAL_JUMP_VELOCITY: 12,
MAX_CLOUDS: 6,
MAX_OBSTACLE_LENGTH: 3,
MAX_OBSTACLE_DUPLICATION: 2,
MAX_SPEED: 13,
MIN_JUMP_HEIGHT: 35,
MOBILE_SPEED_COEFFICIENT: 1.2,
RESOURCE_TEMPLATE_ID: 'audio-resources',
SPEED: 6,
SPEED_DROP_COEFFICIENT: 3
};
/**
* Default dimensions.
* @enum {string}
*/
Runner.defaultDimensions = {
WIDTH: DEFAULT_WIDTH,
HEIGHT: 150
};
/**
* CSS class names.
* @enum {string}
*/
Runner.classes = {
CANVAS: 'runner-canvas',
CONTAINER: 'runner-container',
CRASHED: 'crashed',
ICON: 'icon-offline',
SNACKBAR: 'snackbar',
SNACKBAR_SHOW: 'snackbar-show',
TOUCH_CONTROLLER: 'controller'
};
/**
* Sprite definition layout of the spritesheet.
* @enum {Object}
*/
Runner.spriteDefinition = {
LDPI: {
CACTUS_LARGE: {x: 332, y: 2},
CACTUS_SMALL: {x: 228, y: 2},
CLOUD: {x: 86, y: 2},
HORIZON: {x: 2, y: 54},
PTERODACTYL: {x: 134, y: 2},
RESTART: {x: 2, y: 2},
TEXT_SPRITE: {x: 484, y: 2},
TREX: {x: 677, y: 2}
},
HDPI: {
CACTUS_LARGE: {x: 652,y: 2},
CACTUS_SMALL: {x: 446,y: 2},
CLOUD: {x: 166,y: 2},
HORIZON: {x: 2,y: 104},
PTERODACTYL: {x: 260,y: 2},
RESTART: {x: 2,y: 2},
TEXT_SPRITE: {x: 954,y: 2},
TREX: {x: 1338,y: 2}
}
};
/**
* Sound FX. Reference to the ID of the audio tag on interstitial page.
* @enum {string}
*/
Runner.sounds = {
BUTTON_PRESS: 'offline-sound-press',
HIT: 'offline-sound-hit',
SCORE: 'offline-sound-reached'
};
/**
* Key code mapping.
* @enum {Object}
*/
Runner.keycodes = {
JUMP: {'38': 1, '32': 1}, // Up, spacebar
DUCK: {'40': 1}, // Down
RESTART: {'13': 1} // Enter
};
/**
* Runner event names.
* @enum {string}
*/
Runner.events = {
ANIM_END: 'webkitAnimationEnd',
CLICK: 'click',
KEYDOWN: 'keydown',
KEYUP: 'keyup',
MOUSEDOWN: 'mousedown',
MOUSEUP: 'mouseup',
RESIZE: 'resize',
TOUCHEND: 'touchend',
TOUCHSTART: 'touchstart',
VISIBILITY: 'visibilitychange',
BLUR: 'blur',
FOCUS: 'focus',
LOAD: 'load'
};
Runner.prototype = {
/**
* Whether the easter egg has been disabled. CrOS enterprise enrolled devices.
* @return {boolean}
*/
isDisabled: function() {
return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');
},
/**
* For disabled instances, set up a snackbar with the disabled message.
*/
setupDisabledRunner: function() {
this.containerEl = document.createElement('div');
this.containerEl.className = Runner.classes.SNACKBAR;
this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
this.outerContainerEl.appendChild(this.containerEl);
// Show notification when the activation key is pressed.
document.addEventListener(Runner.events.KEYDOWN, function(e) {
if (Runner.keycodes.JUMP[e.keyCode]) {
this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);
document.querySelector('.icon').classList.add('icon-disabled');
}
}.bind(this));
},
/**
* Setting individual settings for debugging.
* @param {string} setting
* @param {*} value
*/
updateConfigSetting: function(setting, value) {
if (setting in this.config && value != undefined) {
this.config[setting] = value;
switch (setting) {
case 'GRAVITY':
case 'MIN_JUMP_HEIGHT':
case 'SPEED_DROP_COEFFICIENT':
this.tRex.config[setting] = value;
break;
case 'INITIAL_JUMP_VELOCITY':
this.tRex.setJumpVelocity(value);
break;
case 'SPEED':
this.setSpeed(value);
break;
}
}
},
/**
* Cache the appropriate image sprite from the page and get the sprite sheet
* definition.
*/
loadImages: function() {
if (IS_HIDPI) {
Runner.imageSprite = document.getElementById('offline-resources-2x');
this.spriteDef = Runner.spriteDefinition.HDPI;
} else {
Runner.imageSprite = document.getElementById('offline-resources-1x');
this.spriteDef = Runner.spriteDefinition.LDPI;
}
this.init();
},
/**
* Load and decode base 64 encoded sounds.
*/
loadSounds: function() {
if (!IS_IOS) {
this.audioContext = new AudioContext();
var resourceTemplate =
document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
for (var sound in Runner.sounds) {
var soundSrc =
resourceTemplate.getElementById(Runner.sounds[sound]).src;
soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
var buffer = decodeBase64ToArrayBuffer(soundSrc);
// Async, so no guarantee of order in array.
this.audioContext.decodeAudioData(buffer, function(index, audioData) {
this.soundFx[index] = audioData;
}.bind(this, sound));
}
}
},
/**
* Sets the game speed. Adjust the speed accordingly if on a smaller screen.
* @param {number} opt_speed
*/
setSpeed: function(opt_speed) {
var speed = opt_speed || this.currentSpeed;
// Reduce the speed on smaller mobile screens.
if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
this.config.MOBILE_SPEED_COEFFICIENT;
this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
} else if (opt_speed) {
this.currentSpeed = opt_speed;
}
},
/**
* Game initialiser.
*/
init: function() {
// Hide the static icon.
document.querySelector('.' + Runner.classes.ICON).style.visibility =
'hidden';
this.adjustDimensions();
this.setSpeed();
this.containerEl = document.createElement('div');
this.containerEl.className = Runner.classes.CONTAINER;
// Player canvas container.
this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
this.dimensions.HEIGHT, Runner.classes.PLAYER);
this.canvasCtx = this.canvas.getContext('2d');
this.canvasCtx.fillStyle = '#f7f7f7';
this.canvasCtx.fill();
Runner.updateCanvasScaling(this.canvas);
// Horizon contains clouds, obstacles and the ground.
this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,
this.config.GAP_COEFFICIENT);
// Distance meter
this.distanceMeter = new DistanceMeter(this.canvas,
this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);
// Draw t-rex
this.tRex = new Trex(this.canvas, this.spriteDef.TREX);
this.outerContainerEl.appendChild(this.containerEl);
if (IS_MOBILE) {
this.createTouchController();
}
this.startListening();
this.update();
window.addEventListener(Runner.events.RESIZE,
this.debounceResize.bind(this));
},
/**
* Create the touch controller. A div that covers whole screen.
*/
createTouchController: function() {
this.touchController = document.createElement('div');
this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
},
/**
* Debounce the resize event.
*/
debounceResize: function() {
if (!this.resizeTimerId_) {
this.resizeTimerId_ =
setInterval(this.adjustDimensions.bind(this), 250);
}
},
/**
* Adjust game space dimensions on resize.
*/
adjustDimensions: function() {
clearInterval(this.resizeTimerId_);
this.resizeTimerId_ = null;
var boxStyles = window.getComputedStyle(this.outerContainerEl);
var padding = Number(boxStyles.paddingLeft.substr(0,
boxStyles.paddingLeft.length - 2));
this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
// Redraw the elements back onto the canvas.
if (this.canvas) {
this.canvas.width = this.dimensions.WIDTH;
this.canvas.height = this.dimensions.HEIGHT;
Runner.updateCanvasScaling(this.canvas);
this.distanceMeter.calcXPos(this.dimensions.WIDTH);
this.clearCanvas();
this.horizon.update(0, 0, true);
this.tRex.update(0);
// Outer container and distance meter.
if (this.activated || this.crashed) {
this.containerEl.style.width = this.dimensions.WIDTH + 'px';
this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
this.distanceMeter.update(0, Math.ceil(this.distanceRan));
this.stop();
} else {
this.tRex.draw(0, 0);
}
// Game over panel.
if (this.crashed && this.gameOverPanel) {
this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
this.gameOverPanel.draw();
}
}
},
/**
* Play the game intro.
* Canvas container width expands out to the full width.
*/
playIntro: function() {
if (!this.started && !this.crashed) {
this.playingIntro = true;
this.tRex.playingIntro = true;
// CSS animation definition.
var keyframes = '@-webkit-keyframes intro { ' +
'from { width:' + Trex.config.WIDTH + 'px }' +
'to { width: ' + this.dimensions.WIDTH + 'px }' +
'}';
document.styleSheets[0].insertRule(keyframes, 0);
this.containerEl.addEventListener(Runner.events.ANIM_END,
this.startGame.bind(this));
this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
this.containerEl.style.width = this.dimensions.WIDTH + 'px';
if (this.touchController) {
this.outerContainerEl.appendChild(this.touchController);
}
this.activated = true;
this.started = true;
} else if (this.crashed) {
this.restart();
}
},
/**
* Update the game status to started.
*/
startGame: function() {
this.runningTime = 0;
this.playingIntro = false;
this.tRex.playingIntro = false;
this.containerEl.style.webkitAnimation = '';
this.playCount++;
// Handle tabbing off the page. Pause the current game.
window.addEventListener(Runner.events.VISIBILITY,
this.onVisibilityChange.bind(this));
window.addEventListener(Runner.events.BLUR,
this.onVisibilityChange.bind(this));
window.addEventListener(Runner.events.FOCUS,
this.onVisibilityChange.bind(this));
},
clearCanvas: function() {
this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
this.dimensions.HEIGHT);
},
/**
* Update the game frame.
*/
update: function() {
this.drawPending = false;
var now = getTimeStamp();
var deltaTime = now - (this.time || now);
this.time = now;
if (this.activated) {
this.clearCanvas();
if (this.tRex.jumping) {
this.tRex.updateJump(deltaTime);
}
this.runningTime += deltaTime;
var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
// First jump triggers the intro.
if (this.tRex.jumpCount == 1 && !this.playingIntro) {
this.playIntro();
}
// The horizon doesn't move until the intro is over.
if (this.playingIntro) {
this.horizon.update(0, this.currentSpeed, hasObstacles);
} else {
deltaTime = !this.started ? 0 : deltaTime;
this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);
}
// Check for collisions.
var collision = hasObstacles &&
checkForCollision(this.horizon.obstacles[0], this.tRex);
if (!collision) {
this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
if (this.currentSpeed < this.config.MAX_SPEED) {
this.currentSpeed += this.config.ACCELERATION;
}
} else {
this.gameOver();
}
var playAcheivementSound = this.distanceMeter.update(deltaTime,
Math.ceil(this.distanceRan));
if (playAcheivementSound) {
this.playSound(this.soundFx.SCORE);
}
}
if (!this.crashed) {
this.tRex.update(deltaTime);
this.raq();
}
},
/**
* Event handler.
*/
handleEvent: function(e) {
return (function(evtType, events) {
switch (evtType) {
case events.KEYDOWN:
case events.TOUCHSTART:
case events.MOUSEDOWN:
this.onKeyDown(e);
break;
case events.KEYUP:
case events.TOUCHEND:
case events.MOUSEUP:
this.onKeyUp(e);
break;
}
}.bind(this))(e.type, Runner.events);
},
/**
* Bind relevant key / mouse / touch listeners.
*/
startListening: function() {
// Keys.
document.addEventListener(Runner.events.KEYDOWN, this);
document.addEventListener(Runner.events.KEYUP, this);
if (IS_MOBILE) {
// Mobile only touch devices.
this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
this.touchController.addEventListener(Runner.events.TOUCHEND, this);
this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
} else {
// Mouse.
document.addEventListener(Runner.events.MOUSEDOWN, this);
document.addEventListener(Runner.events.MOUSEUP, this);
}
},
/**
* Remove all listeners.
*/
stopListening: function() {
document.removeEventListener(Runner.events.KEYDOWN, this);
document.removeEventListener(Runner.events.KEYUP, this);
if (IS_MOBILE) {
this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
} else {
document.removeEventListener(Runner.events.MOUSEDOWN, this);
document.removeEventListener(Runner.events.MOUSEUP, this);
}
},
/**
* Process keydown.
* @param {Event} e
*/
onKeyDown: function(e) {
// Prevent native page scrolling whilst tapping on mobile.
if (IS_MOBILE) {
e.preventDefault();
}
if (e.target != this.detailsButton) {
if (!this.crashed && (Runner.keycodes.JUMP[e.keyCode] ||
e.type == Runner.events.TOUCHSTART)) {
if (!this.activated) {
this.loadSounds();
this.activated = true;
errorPageController.trackEasterEgg();
}
if (!this.tRex.jumping && !this.tRex.ducking) {
this.playSound(this.soundFx.BUTTON_PRESS);
this.tRex.startJump(this.currentSpeed);
}
}
if (this.crashed && e.type == Runner.events.TOUCHSTART &&
e.currentTarget == this.containerEl) {
this.restart();
}
}
if (this.activated && !this.crashed && Runner.keycodes.DUCK[e.keyCode]) {
e.preventDefault();
if (this.tRex.jumping) {
// Speed drop, activated only when jump key is not pressed.
this.tRex.setSpeedDrop();
} else if (!this.tRex.jumping && !this.tRex.ducking) {
// Duck.
this.tRex.setDuck(true);
}
}
},
/**
* Process key up.
* @param {Event} e
*/
onKeyUp: function(e) {
var keyCode = String(e.keyCode);
var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
e.type == Runner.events.TOUCHEND ||
e.type == Runner.events.MOUSEDOWN;
if (this.isRunning() && isjumpKey) {
this.tRex.endJump();
} else if (Runner.keycodes.DUCK[keyCode]) {
this.tRex.speedDrop = false;
this.tRex.setDuck(false);
} else if (this.crashed) {
// Check that enough time has elapsed before allowing jump key to restart.
var deltaTime = getTimeStamp() - this.time;
if (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||
(deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
Runner.keycodes.JUMP[keyCode])) {
this.restart();
}
} else if (this.paused && isjumpKey) {
// Reset the jump state
this.tRex.reset();
this.play();
}
},
/**
* Returns whether the event was a left click on canvas.
* On Windows right click is registered as a click.
* @param {Event} e
* @return {boolean}
*/
isLeftClickOnCanvas: function(e) {
return e.button != null && e.button < 2 &&
e.type == Runner.events.MOUSEUP && e.target == this.canvas;
},
/**
* RequestAnimationFrame wrapper.
*/
raq: function() {
if (!this.drawPending) {
this.drawPending = true;
this.raqId = requestAnimationFrame(this.update.bind(this));
}
},
/**
* Whether the game is running.
* @return {boolean}
*/
isRunning: function() {
return !!this.raqId;
},
/**
* Game over state.
*/
gameOver: function() {
this.playSound(this.soundFx.HIT);
vibrate(200);
this.stop();
this.crashed = true;
this.distanceMeter.acheivement = false;
this.tRex.update(100, Trex.status.CRASHED);
// Game over panel.
if (!this.gameOverPanel) {
this.gameOverPanel = new GameOverPanel(this.canvas,
this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,
this.dimensions);
} else {
this.gameOverPanel.draw();
}
// Update the high score.
if (this.distanceRan > this.highestScore) {
this.highestScore = Math.ceil(this.distanceRan);
this.distanceMeter.setHighScore(this.highestScore);
}
// Reset the time clock.
this.time = getTimeStamp();
},
stop: function() {
this.activated = false;
this.paused = true;
cancelAnimationFrame(this.raqId);
this.raqId = 0;
},
play: function() {
if (!this.crashed) {
this.activated = true;
this.paused = false;
this.tRex.update(0, Trex.status.RUNNING);
this.time = getTimeStamp();
this.update();
}
},
restart: function() {
if (!this.raqId) {
this.playCount++;
this.runningTime = 0;
this.activated = true;
this.crashed = false;
this.distanceRan = 0;
this.setSpeed(this.config.SPEED);
this.time = getTimeStamp();
this.containerEl.classList.remove(Runner.classes.CRASHED);
this.clearCanvas();
this.distanceMeter.reset(this.highestScore);
this.horizon.reset();
this.tRex.reset();
this.playSound(this.soundFx.BUTTON_PRESS);
this.update();
}
},
/**
* Pause the game if the tab is not in focus.
*/
onVisibilityChange: function(e) {
if (document.hidden || document.webkitHidden || e.type == 'blur') {
this.stop();
} else if (!this.crashed) {
this.tRex.reset();
this.play();
}
},
/**
* Play a sound.
* @param {SoundBuffer} soundBuffer
*/
playSound: function(soundBuffer) {
if (soundBuffer) {
var sourceNode = this.audioContext.createBufferSource();
sourceNode.buffer = soundBuffer;
sourceNode.connect(this.audioContext.destination);
sourceNode.start(0);
}
}
};
/**
* Updates the canvas size taking into
* account the backing store pixel ratio and
* the device pixel ratio.
*
* See article by Paul Lewis:
* http://www.html5rocks.com/en/tutorials/canvas/hidpi/
*
* @param {HTMLCanvasElement} canvas
* @param {number} opt_width
* @param {number} opt_height
* @return {boolean} Whether the canvas was scaled.
*/
Runner.updateCanvasScaling = function(canvas, opt_width, opt_height) {
var context = canvas.getContext('2d');
// Query the various pixel ratios
var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
var ratio = devicePixelRatio / backingStoreRatio;
// Upscale the canvas if the two ratios don't match
if (devicePixelRatio !== backingStoreRatio) {
var oldWidth = opt_width || canvas.width;
var oldHeight = opt_height || canvas.height;
canvas.width = oldWidth * ratio;
canvas.height = oldHeight * ratio;
canvas.style.width = oldWidth + 'px';
canvas.style.height = oldHeight + 'px';
// Scale the context to counter the fact that we've manually scaled
// our canvas element.
context.scale(ratio, ratio);
return true;
} else if (devicePixelRatio == 1) {
// Reset the canvas width / height. Fixes scaling bug when the page is
// zoomed and the devicePixelRatio changes accordingly.
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
}
return false;
};
/**
* Get random number.
* @param {number} min
* @param {number} max
* @param {number}
*/
function getRandomNum(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Vibrate on mobile devices.
* @param {number} duration Duration of the vibration in milliseconds.
*/
function vibrate(duration) {
if (IS_MOBILE && window.navigator.vibrate) {
window.navigator.vibrate(duration);
}
}
/**
* Create canvas element.
* @param {HTMLElement} container Element to append canvas to.
* @param {number} width
* @param {number} height
* @param {string} opt_classname
* @return {HTMLCanvasElement}
*/
function createCanvas(container, width, height, opt_classname) {
var canvas = document.createElement('canvas');
canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
opt_classname : Runner.classes.CANVAS;
canvas.width = width;
canvas.height = height;
container.appendChild(canvas);
return canvas;
}
/**
* Decodes the base 64 audio to ArrayBuffer used by Web Audio.
* @param {string} base64String
*/
function decodeBase64ToArrayBuffer(base64String) {
var len = (base64String.length / 4) * 3;
var str = atob(base64String);
var arrayBuffer = new ArrayBuffer(len);
var bytes = new Uint8Array(arrayBuffer);
for (var i = 0; i < len; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes.buffer;
}
/**
* Return the current timestamp.
* @return {number}
*/
function getTimeStamp() {
return IS_IOS ? new Date().getTime() : performance.now();
}
//******************************************************************************
/**
* Game over panel.
* @param {!HTMLCanvasElement} canvas
* @param {Object} textImgPos
* @param {Object} restartImgPos
* @param {!Object} dimensions Canvas dimensions.
* @constructor
*/
function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.canvasDimensions = dimensions;
this.textImgPos = textImgPos;
this.restartImgPos = restartImgPos;
this.draw();
};
/**
* Dimensions used in the panel.
* @enum {number}
*/
GameOverPanel.dimensions = {
TEXT_X: 0,
TEXT_Y: 13,
TEXT_WIDTH: 191,
TEXT_HEIGHT: 11,
RESTART_WIDTH: 36,
RESTART_HEIGHT: 32
};
GameOverPanel.prototype = {
/**
* Update the panel dimensions.
* @param {number} width New canvas width.
* @param {number} opt_height Optional new canvas height.
*/
updateDimensions: function(width, opt_height) {
this.canvasDimensions.WIDTH = width;
if (opt_height) {
this.canvasDimensions.HEIGHT = opt_height;
}
},
/**
* Draw the panel.
*/
draw: function() {
var dimensions = GameOverPanel.dimensions;
var centerX = this.canvasDimensions.WIDTH / 2;
// Game over text.
var textSourceX = dimensions.TEXT_X;
var textSourceY = dimensions.TEXT_Y;
var textSourceWidth = dimensions.TEXT_WIDTH;
var textSourceHeight = dimensions.TEXT_HEIGHT;
var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
var textTargetWidth = dimensions.TEXT_WIDTH;
var textTargetHeight = dimensions.TEXT_HEIGHT;
var restartSourceWidth = dimensions.RESTART_WIDTH;
var restartSourceHeight = dimensions.RESTART_HEIGHT;
var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
var restartTargetY = this.canvasDimensions.HEIGHT / 2;
if (IS_HIDPI) {
textSourceY *= 2;
textSourceX *= 2;
textSourceWidth *= 2;
textSourceHeight *= 2;
restartSourceWidth *= 2;
restartSourceHeight *= 2;
}
textSourceX += this.textImgPos.x;
textSourceY += this.textImgPos.y;
// Game over text from sprite.
this.canvasCtx.drawImage(Runner.imageSprite,
textSourceX, textSourceY, textSourceWidth, textSourceHeight,
textTargetX, textTargetY, textTargetWidth, textTargetHeight);
// Restart button.
this.canvasCtx.drawImage(Runner.imageSprite,
this.restartImgPos.x, this.restartImgPos.y,
restartSourceWidth, restartSourceHeight,
restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
dimensions.RESTART_HEIGHT);
}
};
//******************************************************************************
/**
* Check for a collision.
* @param {!Obstacle} obstacle
* @param {!Trex} tRex T-rex object.
* @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
* collision boxes.
* @return {Array<CollisionBox>}
*/
function checkForCollision(obstacle, tRex, opt_canvasCtx) {
var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
// Adjustments are made to the bounding box as there is a 1 pixel white
// border around the t-rex and obstacles.
var tRexBox = new CollisionBox(
tRex.xPos + 1,
tRex.yPos + 1,
tRex.config.WIDTH - 2,
tRex.config.HEIGHT - 2);
var obstacleBox = new CollisionBox(
obstacle.xPos + 1,
obstacle.yPos + 1,
obstacle.typeConfig.width * obstacle.size - 2,
obstacle.typeConfig.height - 2);
// Debug outer box
if (opt_canvasCtx) {
drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
}
// Simple outer bounds check.
if (boxCompare(tRexBox, obstacleBox)) {
var collisionBoxes = obstacle.collisionBoxes;
var tRexCollisionBoxes = tRex.ducking ?
Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
// Detailed axis aligned box check.
for (var t = 0; t < tRexCollisionBoxes.length; t++) {
for (var i = 0; i < collisionBoxes.length; i++) {
// Adjust the box to actual positions.
var adjTrexBox =
createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
var adjObstacleBox =
createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
var crashed = boxCompare(adjTrexBox, adjObstacleBox);
// Draw boxes for debug.
if (opt_canvasCtx) {
drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
}
if (crashed) {
return [adjTrexBox, adjObstacleBox];
}
}
}
}
return false;
};
/**
* Adjust the collision box.
* @param {!CollisionBox} box The original box.
* @param {!CollisionBox} adjustment Adjustment box.
* @return {CollisionBox} The adjusted collision box object.
*/
function createAdjustedCollisionBox(box, adjustment) {
return new CollisionBox(
box.x + adjustment.x,
box.y + adjustment.y,
box.width,
box.height);
};
/**
* Draw the collision boxes for debug.
*/
function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
canvasCtx.save();
canvasCtx.strokeStyle = '#f00';
canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
canvasCtx.strokeStyle = '#0f0';
canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
obstacleBox.width, obstacleBox.height);
canvasCtx.restore();
};
/**
* Compare two collision boxes for a collision.
* @param {CollisionBox} tRexBox
* @param {CollisionBox} obstacleBox
* @return {boolean} Whether the boxes intersected.
*/
function boxCompare(tRexBox, obstacleBox) {
var crashed = false;
var tRexBoxX = tRexBox.x;
var tRexBoxY = tRexBox.y;
var obstacleBoxX = obstacleBox.x;
var obstacleBoxY = obstacleBox.y;
// Axis-Aligned Bounding Box method.
if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
tRexBox.x + tRexBox.width > obstacleBoxX &&
tRexBox.y < obstacleBox.y + obstacleBox.height &&
tRexBox.height + tRexBox.y > obstacleBox.y) {
crashed = true;
}
return crashed;
};
//******************************************************************************
/**
* Collision box object.
* @param {number} x X position.
* @param {number} y Y Position.
* @param {number} w Width.
* @param {number} h Height.
*/
function CollisionBox(x, y, w, h) {
this.x = x;
this.y = y;
this.width = w;
this.height = h;
};
//******************************************************************************
/**
* Obstacle.
* @param {HTMLCanvasCtx} canvasCtx
* @param {Obstacle.type} type
* @param {Object} spritePos Obstacle position in sprite.
* @param {Object} dimensions
* @param {number} gapCoefficient Mutipler in determining the gap.
* @param {number} speed
*/
function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
gapCoefficient, speed) {
this.canvasCtx = canvasCtx;
this.spritePos = spriteImgPos;
this.typeConfig = type;
this.gapCoefficient = gapCoefficient;
this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
this.dimensions = dimensions;
this.remove = false;
this.xPos = 0;
this.yPos = 0;
this.width = 0;
this.collisionBoxes = [];
this.gap = 0;
this.speedOffset = 0;
// For animated obstacles.
this.currentFrame = 0;
this.timer = 0;
this.init(speed);
};
/**
* Coefficient for calculating the maximum gap.
* @const
*/
Obstacle.MAX_GAP_COEFFICIENT = 1.5;
/**
* Maximum obstacle grouping count.
* @const
*/
Obstacle.MAX_OBSTACLE_LENGTH = 3,
Obstacle.prototype = {
/**
* Initialise the DOM for the obstacle.
* @param {number} speed
*/
init: function(speed) {
this.cloneCollisionBoxes();
// Only allow sizing if we're at the right speed.
if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
this.size = 1;
}
this.width = this.typeConfig.width * this.size;
this.xPos = this.dimensions.WIDTH - this.width;
// Check if obstacle can be positioned at various heights.
if (Array.isArray(this.typeConfig.yPos)) {
var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :
this.typeConfig.yPos;
this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
} else {
this.yPos = this.typeConfig.yPos;
}
this.draw();
// Make collision box adjustments,
// Central box is adjusted to the size as one box.
// ____ ______ ________
// _| |-| _| |-| _| |-|
// | |<->| | | |<--->| | | |<----->| |
// | | 1 | | | | 2 | | | | 3 | |
// |_|___|_| |_|_____|_| |_|_______|_|
//
if (this.size > 1) {
this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
this.collisionBoxes[2].width;
this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
}
// For obstacles that go at a different speed from the horizon.
if (this.typeConfig.speedOffset) {
this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
-this.typeConfig.speedOffset;
}
this.gap = this.getGap(this.gapCoefficient, speed);
},
/**
* Draw and crop based on size.
*/
draw: function() {
var sourceWidth = this.typeConfig.width;
var sourceHeight = this.typeConfig.height;
if (IS_HIDPI) {
sourceWidth = sourceWidth * 2;
sourceHeight = sourceHeight * 2;
}
// X position in sprite.
var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
this.spritePos.x;
// Animation frames.
if (this.currentFrame > 0) {
sourceX += sourceWidth * this.currentFrame;
}
this.canvasCtx.drawImage(Runner.imageSprite,
sourceX, this.spritePos.y,
sourceWidth * this.size, sourceHeight,
this.xPos, this.yPos,
this.typeConfig.width * this.size, this.typeConfig.height);
},
/**
* Obstacle frame update.
* @param {number} deltaTime
* @param {number} speed
*/
update: function(deltaTime, speed) {
if (!this.remove) {
if (this.typeConfig.speedOffset) {
speed += this.speedOffset;
}
this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
// Update frame
if (this.typeConfig.numFrames) {
this.timer += deltaTime;
if (this.timer >= this.typeConfig.frameRate) {
this.currentFrame =
this.currentFrame == this.typeConfig.numFrames - 1 ?
0 : this.currentFrame + 1;
this.timer = 0;
}
}
this.draw();
if (!this.isVisible()) {
this.remove = true;
}
}
},
/**
* Calculate a random gap size.
* - Minimum gap gets wider as speed increses
* @param {number} gapCoefficient
* @param {number} speed
* @return {number} The gap size.
*/
getGap: function(gapCoefficient, speed) {
var minGap = Math.round(this.width * speed +
this.typeConfig.minGap * gapCoefficient);
var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
return getRandomNum(minGap, maxGap);
},
/**
* Check if obstacle is visible.
* @return {boolean} Whether the obstacle is in the game area.
*/
isVisible: function() {
return this.xPos + this.width > 0;
},
/**
* Make a copy of the collision boxes, since these will change based on
* obstacle type and size.
*/
cloneCollisionBoxes: function() {
var collisionBoxes = this.typeConfig.collisionBoxes;
for (var i = collisionBoxes.length - 1; i >= 0; i--) {
this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
collisionBoxes[i].y, collisionBoxes[i].width,
collisionBoxes[i].height);
}
}
};
/**
* Obstacle definitions.
* minGap: minimum pixel space betweeen obstacles.
* multipleSpeed: Speed at which multiples are allowed.
* speedOffset: speed faster / slower than the horizon.
* minSpeed: Minimum speed which the obstacle can make an appearance.
*/
Obstacle.types = [
{
type: 'CACTUS_SMALL',
width: 17,
height: 35,
yPos: 105,
multipleSpeed: 4,
minGap: 120,
minSpeed: 0,
collisionBoxes: [
new CollisionBox(0, 7, 5, 27),
new CollisionBox(4, 0, 6, 34),
new CollisionBox(10, 4, 7, 14)
]
},
{
type: 'CACTUS_LARGE',
width: 25,
height: 50,
yPos: 90,
multipleSpeed: 7,
minGap: 120,
minSpeed: 0,
collisionBoxes: [
new CollisionBox(0, 12, 7, 38),
new CollisionBox(8, 0, 7, 49),
new CollisionBox(13, 10, 10, 38)
]
},
{
type: 'PTERODACTYL',
width: 46,
height: 40,
yPos: [ 100, 75, 50 ], // Variable height.
yPosMobile: [ 100, 50 ], // Variable height mobile.
multipleSpeed: 999,
minSpeed: 8.5,
minGap: 150,
collisionBoxes: [
new CollisionBox(15, 15, 16, 5),
new CollisionBox(18, 21, 24, 6),
new CollisionBox(2, 14, 4, 3),
new CollisionBox(6, 10, 4, 7),
new CollisionBox(10, 8, 6, 9)
],
numFrames: 2,
frameRate: 1000/6,
speedOffset: .8
}
];
//******************************************************************************
/**
* T-rex game character.
* @param {HTMLCanvas} canvas
* @param {Object} spritePos Positioning within image sprite.
* @constructor
*/
function Trex(canvas, spritePos) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.spritePos = spritePos;
this.xPos = 0;
this.yPos = 0;
// Position when on the ground.
this.groundYPos = 0;
this.currentFrame = 0;
this.currentAnimFrames = [];
this.blinkDelay = 0;
this.animStartTime = 0;
this.timer = 0;
this.msPerFrame = 1000 / FPS;
this.config = Trex.config;
// Current status.
this.status = Trex.status.WAITING;
this.jumping = false;
this.ducking = false;
this.jumpVelocity = 0;
this.reachedMinHeight = false;
this.speedDrop = false;
this.jumpCount = 0;
this.jumpspotX = 0;
this.init();
};
/**
* T-rex player config.
* @enum {number}
*/
Trex.config = {
DROP_VELOCITY: -5,
GRAVITY: 0.6,
HEIGHT: 47,
HEIGHT_DUCK: 25,
INIITAL_JUMP_VELOCITY: -10,
INTRO_DURATION: 1500,
MAX_JUMP_HEIGHT: 30,
MIN_JUMP_HEIGHT: 30,
SPEED_DROP_COEFFICIENT: 3,
SPRITE_WIDTH: 262,
START_X_POS: 50,
WIDTH: 44,
WIDTH_DUCK: 59
};
/**
* Used in collision detection.
* @type {Array<CollisionBox>}
*/
Trex.collisionBoxes = {
DUCKING: [
new CollisionBox(1, 18, 55, 25)
],
RUNNING: [
new CollisionBox(22, 0, 17, 16),
new CollisionBox(1, 18, 30, 9),
new CollisionBox(10, 35, 14, 8),
new CollisionBox(1, 24, 29, 5),
new CollisionBox(5, 30, 21, 4),
new CollisionBox(9, 34, 15, 4)
]
};
/**
* Animation states.
* @enum {string}
*/
Trex.status = {
CRASHED: 'CRASHED',
DUCKING: 'DUCKING',
JUMPING: 'JUMPING',
RUNNING: 'RUNNING',
WAITING: 'WAITING'
};
/**
* Blinking coefficient.
* @const
*/
Trex.BLINK_TIMING = 7000;
/**
* Animation config for different states.
* @enum {Object}
*/
Trex.animFrames = {
WAITING: {
frames: [44, 0],
msPerFrame: 1000 / 3
},
RUNNING: {
frames: [88, 132],
msPerFrame: 1000 / 12
},
CRASHED: {
frames: [220],
msPerFrame: 1000 / 60
},
JUMPING: {
frames: [0],
msPerFrame: 1000 / 60
},
DUCKING: {
frames: [262, 321],
msPerFrame: 1000 / 8
}
};
Trex.prototype = {
/**
* T-rex player initaliser.
* Sets the t-rex to blink at random intervals.
*/
init: function() {
this.blinkDelay = this.setBlinkDelay();
this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
Runner.config.BOTTOM_PAD;
this.yPos = this.groundYPos;
this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
this.draw(0, 0);
this.update(0, Trex.status.WAITING);
},
/**
* Setter for the jump velocity.
* The approriate drop velocity is also set.
*/
setJumpVelocity: function(setting) {
this.config.INIITAL_JUMP_VELOCITY = -setting;
this.config.DROP_VELOCITY = -setting / 2;
},
/**
* Set the animation status.
* @param {!number} deltaTime
* @param {Trex.status} status Optional status to switch to.
*/
update: function(deltaTime, opt_status) {
this.timer += deltaTime;
// Update the status.
if (opt_status) {
this.status = opt_status;
this.currentFrame = 0;
this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
this.currentAnimFrames = Trex.animFrames[opt_status].frames;
if (opt_status == Trex.status.WAITING) {
this.animStartTime = getTimeStamp();
this.setBlinkDelay();
}
}
// Game intro animation, T-rex moves in from the left.
if (this.playingIntro && this.xPos < this.config.START_X_POS) {
this.xPos += Math.round((this.config.START_X_POS /
this.config.INTRO_DURATION) * deltaTime);
}
if (this.status == Trex.status.WAITING) {
this.blink(getTimeStamp());
} else {
this.draw(this.currentAnimFrames[this.currentFrame], 0);
}
// Update the frame position.
if (this.timer >= this.msPerFrame) {
this.currentFrame = this.currentFrame ==
this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
this.timer = 0;
}
// Speed drop becomes duck if the down key is still being pressed.
if (this.speedDrop && this.yPos == this.groundYPos) {
this.speedDrop = false;
this.setDuck(true);
}
},
/**
* Draw the t-rex to a particular position.
* @param {number} x
* @param {number} y
*/
draw: function(x, y) {
var sourceX = x;
var sourceY = y;
var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
this.config.WIDTH_DUCK : this.config.WIDTH;
var sourceHeight = this.config.HEIGHT;
if (IS_HIDPI) {
sourceX *= 2;
sourceY *= 2;
sourceWidth *= 2;
sourceHeight *= 2;
}
// Adjustments for sprite sheet position.
sourceX += this.spritePos.x;
sourceY += this.spritePos.y;
// Ducking.
if (this.ducking && this.status != Trex.status.CRASHED) {
this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
sourceWidth, sourceHeight,
this.xPos, this.yPos,
this.config.WIDTH_DUCK, this.config.HEIGHT);
} else {
// Crashed whilst ducking. Trex is standing up so needs adjustment.
if (this.ducking && this.status == Trex.status.CRASHED) {
this.xPos++;
}
// Standing / running
this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
sourceWidth, sourceHeight,
this.xPos, this.yPos,
this.config.WIDTH, this.config.HEIGHT);
}
},
/**
* Sets a random time for the blink to happen.
*/
setBlinkDelay: function() {
this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
},
/**
* Make t-rex blink at random intervals.
* @param {number} time Current time in milliseconds.
*/
blink: function(time) {
var deltaTime = time - this.animStartTime;
if (deltaTime >= this.blinkDelay) {
this.draw(this.currentAnimFrames[this.currentFrame], 0);
if (this.currentFrame == 1) {
// Set new random delay to blink.
this.setBlinkDelay();
this.animStartTime = time;
}
}
},
/**
* Initialise a jump.
* @param {number} speed
*/
startJump: function(speed) {
if (!this.jumping) {
this.update(0, Trex.status.JUMPING);
// Tweak the jump velocity based on the speed.
this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
this.jumping = true;
this.reachedMinHeight = false;
this.speedDrop = false;
}
},
/**
* Jump is complete, falling down.
*/
endJump: function() {
if (this.reachedMinHeight &&
this.jumpVelocity < this.config.DROP_VELOCITY) {
this.jumpVelocity = this.config.DROP_VELOCITY;
}
},
/**
* Update frame for a jump.
* @param {number} deltaTime
* @param {number} speed
*/
updateJump: function(deltaTime, speed) {
var msPerFrame = Trex.animFrames[this.status].msPerFrame;
var framesElapsed = deltaTime / msPerFrame;
// Speed drop makes Trex fall faster.
if (this.speedDrop) {
this.yPos += Math.round(this.jumpVelocity *
this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
} else {
this.yPos += Math.round(this.jumpVelocity * framesElapsed);
}
this.jumpVelocity += this.config.GRAVITY * framesElapsed;
// Minimum height has been reached.
if (this.yPos < this.minJumpHeight || this.speedDrop) {
this.reachedMinHeight = true;
}
// Reached max height
if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
this.endJump();
}
// Back down at ground level. Jump completed.
if (this.yPos > this.groundYPos) {
this.reset();
this.jumpCount++;
}
this.update(deltaTime);
},
/**
* Set the speed drop. Immediately cancels the current jump.
*/
setSpeedDrop: function() {
this.speedDrop = true;
this.jumpVelocity = 1;
},
/**
* @param {boolean} isDucking.
*/
setDuck: function(isDucking) {
if (isDucking && this.status != Trex.status.DUCKING) {
this.update(0, Trex.status.DUCKING);
this.ducking = true;
} else if (this.status == Trex.status.DUCKING) {
this.update(0, Trex.status.RUNNING);
this.ducking = false;
}
},
/**
* Reset the t-rex to running at start of game.
*/
reset: function() {
this.yPos = this.groundYPos;
this.jumpVelocity = 0;
this.jumping = false;
this.ducking = false;
this.update(0, Trex.status.RUNNING);
this.midair = false;
this.speedDrop = false;
this.jumpCount = 0;
}
};
//******************************************************************************
/**
* Handles displaying the distance meter.
* @param {!HTMLCanvasElement} canvas
* @param {Object} spritePos Image position in sprite.
* @param {number} canvasWidth
* @constructor
*/
function DistanceMeter(canvas, spritePos, canvasWidth) {
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.image = Runner.imageSprite;
this.spritePos = spritePos;
this.x = 0;
this.y = 5;
this.currentDistance = 0;
this.maxScore = 0;
this.highScore = 0;
this.container = null;
this.digits = [];
this.acheivement = false;
this.defaultString = '';
this.flashTimer = 0;
this.flashIterations = 0;
this.config = DistanceMeter.config;
this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
this.init(canvasWidth);
};
/**
* @enum {number}
*/
DistanceMeter.dimensions = {
WIDTH: 10,
HEIGHT: 13,
DEST_WIDTH: 11
};
/**
* Y positioning of the digits in the sprite sheet.
* X position is always 0.
* @type {Array<number>}
*/
DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
/**
* Distance meter config.
* @enum {number}
*/
DistanceMeter.config = {
// Number of digits.
MAX_DISTANCE_UNITS: 5,
// Distance that causes achievement animation.
ACHIEVEMENT_DISTANCE: 100,
// Used for conversion from pixel distance to a scaled unit.
COEFFICIENT: 0.025,
// Flash duration in milliseconds.
FLASH_DURATION: 1000 / 4,
// Flash iterations for achievement animation.
FLASH_ITERATIONS: 3
};
DistanceMeter.prototype = {
/**
* Initialise the distance meter to '00000'.
* @param {number} width Canvas width in px.
*/
init: function(width) {
var maxDistanceStr = '';
this.calcXPos(width);
this.maxScore = this.maxScoreUnits;
for (var i = 0; i < this.maxScoreUnits; i++) {
this.draw(i, 0);
this.defaultString += '0';
maxDistanceStr += '9';
}
this.maxScore = parseInt(maxDistanceStr);
},
/**
* Calculate the xPos in the canvas.
* @param {number} canvasWidth
*/
calcXPos: function(canvasWidth) {
this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
(this.maxScoreUnits + 1));
},
/**
* Draw a digit to canvas.
* @param {number} digitPos Position of the digit.
* @param {number} value Digit value 0-9.
* @param {boolean} opt_highScore Whether drawing the high score.
*/
draw: function(digitPos, value, opt_highScore) {
var sourceWidth = DistanceMeter.dimensions.WIDTH;
var sourceHeight = DistanceMeter.dimensions.HEIGHT;
var sourceX = DistanceMeter.dimensions.WIDTH * value;
var sourceY = 0;
var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
var targetY = this.y;
var targetWidth = DistanceMeter.dimensions.WIDTH;
var targetHeight = DistanceMeter.dimensions.HEIGHT;
// For high DPI we 2x source values.
if (IS_HIDPI) {
sourceWidth *= 2;
sourceHeight *= 2;
sourceX *= 2;
}
sourceX += this.spritePos.x;
sourceY += this.spritePos.y;
this.canvasCtx.save();
if (opt_highScore) {
// Left of the current score.
var highScoreX = this.x - (this.maxScoreUnits * 2) *
DistanceMeter.dimensions.WIDTH;
this.canvasCtx.translate(highScoreX, this.y);
} else {
this.canvasCtx.translate(this.x, this.y);
}
this.canvasCtx.drawImage(this.image, sourceX, sourceY,
sourceWidth, sourceHeight,
targetX, targetY,
targetWidth, targetHeight
);
this.canvasCtx.restore();
},
/**
* Covert pixel distance to a 'real' distance.
* @param {number} distance Pixel distance ran.
* @return {number} The 'real' distance ran.
*/
getActualDistance: function(distance) {
return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
},
/**
* Update the distance meter.
* @param {number} distance
* @param {number} deltaTime
* @return {boolean} Whether the acheivement sound fx should be played.
*/
update: function(deltaTime, distance) {
var paint = true;
var playSound = false;
if (!this.acheivement) {
distance = this.getActualDistance(distance);
// Score has gone beyond the initial digit count.
if (distance > this.maxScore && this.maxScoreUnits ==
this.config.MAX_DISTANCE_UNITS) {
this.maxScoreUnits++;
this.maxScore = parseInt(this.maxScore + '9');
} else {
this.distance = 0;
}
if (distance > 0) {
// Acheivement unlocked
if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
// Flash score and play sound.
this.acheivement = true;
this.flashTimer = 0;
playSound = true;
}
// Create a string representation of the distance with leading 0.
var distanceStr = (this.defaultString +
distance).substr(-this.maxScoreUnits);
this.digits = distanceStr.split('');
} else {
this.digits = this.defaultString.split('');
}
} else {
// Control flashing of the score on reaching acheivement.
if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
this.flashTimer += deltaTime;
if (this.flashTimer < this.config.FLASH_DURATION) {
paint = false;
} else if (this.flashTimer >
this.config.FLASH_DURATION * 2) {
this.flashTimer = 0;
this.flashIterations++;
}
} else {
this.acheivement = false;
this.flashIterations = 0;
this.flashTimer = 0;
}
}
// Draw the digits if not flashing.
if (paint) {
for (var i = this.digits.length - 1; i >= 0; i--) {
this.draw(i, parseInt(this.digits[i]));
}
}
this.drawHighScore();
return playSound;
},
/**
* Draw the high score.
*/
drawHighScore: function() {
this.canvasCtx.save();
this.canvasCtx.globalAlpha = .8;
for (var i = this.highScore.length - 1; i >= 0; i--) {
this.draw(i, parseInt(this.highScore[i], 10), true);
}
this.canvasCtx.restore();
},
/**
* Set the highscore as a array string.
* Position of char in the sprite: H - 10, I - 11.
* @param {number} distance Distance ran in pixels.
*/
setHighScore: function(distance) {
distance = this.getActualDistance(distance);
var highScoreStr = (this.defaultString +
distance).substr(-this.maxScoreUnits);
this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
},
/**
* Reset the distance meter back to '00000'.
*/
reset: function() {
this.update(0);
this.acheivement = false;
}
};
//******************************************************************************
/**
* Cloud background item.
* Similar to an obstacle object but without collision boxes.
* @param {HTMLCanvasElement} canvas Canvas element.
* @param {Object} spritePos Position of image in sprite.
* @param {number} containerWidth
*/
function Cloud(canvas, spritePos, containerWidth) {
this.canvas = canvas;
this.canvasCtx = this.canvas.getContext('2d');
this.spritePos = spritePos;
this.containerWidth = containerWidth;
this.xPos = containerWidth;
this.yPos = 0;
this.remove = false;
this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
Cloud.config.MAX_CLOUD_GAP);
this.init();
};
/**
* Cloud object config.
* @enum {number}
*/
Cloud.config = {
HEIGHT: 14,
MAX_CLOUD_GAP: 400,
MAX_SKY_LEVEL: 30,
MIN_CLOUD_GAP: 100,
MIN_SKY_LEVEL: 71,
WIDTH: 46
};
Cloud.prototype = {
/**
* Initialise the cloud. Sets the Cloud height.
*/
init: function() {
this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
Cloud.config.MIN_SKY_LEVEL);
this.draw();
},
/**
* Draw the cloud.
*/
draw: function() {
this.canvasCtx.save();
var sourceWidth = Cloud.config.WIDTH;
var sourceHeight = Cloud.config.HEIGHT;
if (IS_HIDPI) {
sourceWidth = sourceWidth * 2;
sourceHeight = sourceHeight * 2;
}
this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
this.spritePos.y,
sourceWidth, sourceHeight,
this.xPos, this.yPos,
Cloud.config.WIDTH, Cloud.config.HEIGHT);
this.canvasCtx.restore();
},
/**
* Update the cloud position.
* @param {number} speed
*/
update: function(speed) {
if (!this.remove) {
this.xPos -= Math.ceil(speed);
this.draw();
// Mark as removeable if no longer in the canvas.
if (!this.isVisible()) {
this.remove = true;
}
}
},
/**
* Check if the cloud is visible on the stage.
* @return {boolean}
*/
isVisible: function() {
return this.xPos + Cloud.config.WIDTH > 0;
}
};
//******************************************************************************
/**
* Horizon Line.
* Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
* @param {HTMLCanvasElement} canvas
* @param {Object} spritePos Horizon position in sprite.
* @constructor
*/
function HorizonLine(canvas, spritePos) {
this.spritePos = spritePos;
this.canvas = canvas;
this.canvasCtx = canvas.getContext('2d');
this.sourceDimensions = {};
this.dimensions = HorizonLine.dimensions;
this.sourceXPos = [this.spritePos.x, this.spritePos.x +
this.dimensions.WIDTH];
this.xPos = [];
this.yPos = 0;
this.bumpThreshold = 0.5;
this.setSourceDimensions();
this.draw();
};
/**
* Horizon line dimensions.
* @enum {number}
*/
HorizonLine.dimensions = {
WIDTH: 600,
HEIGHT: 12,
YPOS: 127
};
HorizonLine.prototype = {
/**
* Set the source dimensions of the horizon line.
*/
setSourceDimensions: function() {
for (var dimension in HorizonLine.dimensions) {
if (IS_HIDPI) {
if (dimension != 'YPOS') {
this.sourceDimensions[dimension] =
HorizonLine.dimensions[dimension] * 2;
}
} else {
this.sourceDimensions[dimension] =
HorizonLine.dimensions[dimension];
}
this.dimensions[dimension] = HorizonLine.dimensions[dimension];
}
this.xPos = [0, HorizonLine.dimensions.WIDTH];
this.yPos = HorizonLine.dimensions.YPOS;
},
/**
* Return the crop x position of a type.
*/
getRandomType: function() {
return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
},
/**
* Draw the horizon line.
*/
draw: function() {
this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
this.spritePos.y,
this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
this.xPos[0], this.yPos,
this.dimensions.WIDTH, this.dimensions.HEIGHT);
this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
this.spritePos.y,
this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
this.xPos[1], this.yPos,
this.dimensions.WIDTH, this.dimensions.HEIGHT);
},
/**
* Update the x position of an indivdual piece of the line.
* @param {number} pos Line position.
* @param {number} increment
*/
updateXPos: function(pos, increment) {
var line1 = pos;
var line2 = pos == 0 ? 1 : 0;
this.xPos[line1] -= increment;
this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
if (this.xPos[line1] <= -this.dimensions.WIDTH) {
this.xPos[line1] += this.dimensions.WIDTH * 2;
this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
}
},
/**
* Update the horizon line.
* @param {number} deltaTime
* @param {number} speed
*/
update: function(deltaTime, speed) {
var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
if (this.xPos[0] <= 0) {
this.updateXPos(0, increment);
} else {
this.updateXPos(1, increment);
}
this.draw();
},
/**
* Reset horizon to the starting position.
*/
reset: function() {
this.xPos[0] = 0;
this.xPos[1] = HorizonLine.dimensions.WIDTH;
}
};
//******************************************************************************
/**
* Horizon background class.
* @param {HTMLCanvasElement} canvas
* @param {Object} spritePos Sprite positioning.
* @param {Object} dimensions Canvas dimensions.
* @param {number} gapCoefficient
* @constructor
*/
function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
this.canvas = canvas;
this.canvasCtx = this.canvas.getContext('2d');
this.config = Horizon.config;
this.dimensions = dimensions;
this.gapCoefficient = gapCoefficient;
this.obstacles = [];
this.obstacleHistory = [];
this.horizonOffsets = [0, 0];
this.cloudFrequency = this.config.CLOUD_FREQUENCY;
this.spritePos = spritePos;
// Cloud
this.clouds = [];
this.cloudSpeed = this.config.BG_CLOUD_SPEED;
// Horizon
this.horizonLine = null;
this.init();
};
/**
* Horizon config.
* @enum {number}
*/
Horizon.config = {
BG_CLOUD_SPEED: 0.2,
BUMPY_THRESHOLD: .3,
CLOUD_FREQUENCY: .5,
HORIZON_HEIGHT: 16,
MAX_CLOUDS: 6
};
Horizon.prototype = {
/**
* Initialise the horizon. Just add the line and a cloud. No obstacles.
*/
init: function() {
this.addCloud();
this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
},
/**
* @param {number} deltaTime
* @param {number} currentSpeed
* @param {boolean} updateObstacles Used as an override to prevent
* the obstacles from being updated / added. This happens in the
* ease in section.
*/
update: function(deltaTime, currentSpeed, updateObstacles) {
this.runningTime += deltaTime;
this.horizonLine.update(deltaTime, currentSpeed);
this.updateClouds(deltaTime, currentSpeed);
if (updateObstacles) {
this.updateObstacles(deltaTime, currentSpeed);
}
},
/**
* Update the cloud positions.
* @param {number} deltaTime
* @param {number} currentSpeed
*/
updateClouds: function(deltaTime, speed) {
var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
var numClouds = this.clouds.length;
if (numClouds) {
for (var i = numClouds - 1; i >= 0; i--) {
this.clouds[i].update(cloudSpeed);
}
var lastCloud = this.clouds[numClouds - 1];
// Check for adding a new cloud.
if (numClouds < this.config.MAX_CLOUDS &&
(this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
this.cloudFrequency > Math.random()) {
this.addCloud();
}
// Remove expired clouds.
this.clouds = this.clouds.filter(function(obj) {
return !obj.remove;
});
}
},
/**
* Update the obstacle positions.
* @param {number} deltaTime
* @param {number} currentSpeed
*/
updateObstacles: function(deltaTime, currentSpeed) {
// Obstacles, move to Horizon layer.
var updatedObstacles = this.obstacles.slice(0);
for (var i = 0; i < this.obstacles.length; i++) {
var obstacle = this.obstacles[i];
obstacle.update(deltaTime, currentSpeed);
// Clean up existing obstacles.
if (obstacle.remove) {
updatedObstacles.shift();
}
}
this.obstacles = updatedObstacles;
if (this.obstacles.length > 0) {
var lastObstacle = this.obstacles[this.obstacles.length - 1];
if (lastObstacle && !lastObstacle.followingObstacleCreated &&
lastObstacle.isVisible() &&
(lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
this.dimensions.WIDTH) {
this.addNewObstacle(currentSpeed);
lastObstacle.followingObstacleCreated = true;
}
} else {
// Create new obstacles.
this.addNewObstacle(currentSpeed);
}
},
/**
* Add a new obstacle.
* @param {number} currentSpeed
*/
addNewObstacle: function(currentSpeed) {
var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
var obstacleType = Obstacle.types[obstacleTypeIndex];
// Check for multiples of the same type of obstacle.
// Also check obstacle is available at current speed.
if (this.duplicateObstacleCheck(obstacleType.type) ||
currentSpeed < obstacleType.minSpeed) {
this.addNewObstacle(currentSpeed);
} else {
var obstacleSpritePos = this.spritePos[obstacleType.type];
this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
obstacleSpritePos, this.dimensions,
this.gapCoefficient, currentSpeed));
this.obstacleHistory.unshift(obstacleType.type);
if (this.obstacleHistory.length > 1) {
this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
}
}
},
/**
* Returns whether the previous two obstacles are the same as the next one.
* Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
* @return {boolean}
*/
duplicateObstacleCheck: function(nextObstacleType) {
var duplicateCount = 0;
for (var i = 0; i < this.obstacleHistory.length; i++) {
duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
duplicateCount + 1 : 0;
}
return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
},
/**
* Reset the horizon layer.
* Remove existing obstacles and reposition the horizon line.
*/
reset: function() {
this.obstacles = [];
this.horizonLine.reset();
},
/**
* Update the canvas width and scaling.
* @param {number} width Canvas width.
* @param {number} height Canvas height.
*/
resize: function(width, height) {
this.canvas.width = width;
this.canvas.height = height;
},
/**
* Add a new cloud to the horizon.
*/
addCloud: function() {
this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
this.dimensions.WIDTH));
}
};
})();
</script>
</head>
<body id="t" i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize">
<div id="main-frame-error" class="interstitial-wrapper">
<div id="main-content">
<img class="icon"
jseval="updateIconClass(this.classList, iconClass)">
<div id="main-message">
<h1 i18n-content="heading"></h1>
<p hidden></p>
<div class="error-code" jscontent="errorCode"></div>
</div>
</div>
<div id="buttons" class="nav-wrapper">
<div id="control-buttons" hidden>
<button id="reload-button"
class="blue-button text-button"
onclick="trackClick(this.trackingId);
reloadButtonClick(this.url);"
jsselect="reloadButton"
jsvalues=".url:reloadUrl; .trackingId:reloadTrackingId"
jscontent="msg"></button>
<button id="show-saved-copy-button"
class="blue-button text-button"
onclick="showSavedCopyButtonClick()"
jsselect="showSavedCopyButton"
jscontent="msg" jsvalues="title:title; .primary:primary">
</button>
</div>
<button id="details-button" class="text-button small-link"
onclick="detailsButtonClick(); toggleHelpBox()"
jsdisplay="details" jscontent="details"
jsvalues=".detailsText:details; .hideDetailsText:hideDetails;"></button>
</div>
<div id="details" class="hidden">
<div jsselect="summary">
<span jsvalues=".innerHTML:msg"></span>
</div>
<div class="suggestions" jsselect="suggestions">
<div class="suggestion-header" jsvalues=".innerHTML:header"></div>
<div class="suggestion-body" jsvalues=".innerHTML:body"></div>
</div>
<button class="text-button" id="diagnose-button"
onclick="diagnoseErrors()" jscontent="diagnose"
jsdisplay="diagnose"></button>
<div id="diagnose-frame" class="hidden"></div>
<form class="suggestions" jsdisplay="searchUrl"
jsvalues=".url:searchUrl; .trackingId:searchTrackingId"
onsubmit="trackClick(this.trackingId); return search(this.url);">
<div class="suggestion-header" jscontent="searchHeader"></div>
<div id="search-container">
<input type="text" name="q" id="search-box"
jsvalues=".value:searchTerms" />
<button type="submit" id="search-button" class="blue-button"
jsvalues="aria-label:searchHeader">
<img id="search-image">
</button>
</div>
</form>
</div>
</div>
<div id="sub-frame-error">
<!-- Show details when hovering over the icon, in case the details are
hidden because they're too large. -->
<img class="icon" jseval="updateIconClass(this.classList, iconClass)"
jsvalues=".title:errorDetails">
<div id="sub-frame-error-details" jsvalues=".innerHTML:errorDetails"></div>
</div>
<div id="offline-resources">
<img id="offline-resources-1x" src="data:image/png;base64,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">
<img id="offline-resources-2x" src="data:image/png;base64,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">
<template id="audio-resources">
<audio id="offline-sound-press" src="data:audio/mpeg;base64,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"></audio>
<audio id="offline-sound-hit" src="data:audio/mpeg;base64,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"></audio>
<audio id="offline-sound-reached" src="data:audio/mpeg;base64,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"></audio>
</template>
</div>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<script>
window.onload = function() {
if (plugin.didFinishLoading)
plugin.didFinishLoading();
};
window.onkeydown = function(e) {
if (e.keyIdentifier == 'Enter' || e.keyIdentifier == 'U+0020') {
plugin.load();
e.preventDefault();
}
};
</script>
<style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file. */
html, body {
-webkit-user-select: none;
font-family: sans-serif;
height: 100%;
margin: 0;
overflow: hidden;
text-align: center;
width: 100%;
}
h1 {
font-size: 10pt;
font-weight: normal;
padding: 0pt 10pt;
visibility: hidden;
}
#outer:hover h1, #outer:hover #close {
visibility: visible;
}
p {
font-size: 8pt;
padding: 0pt 14pt;
}
#outer {
align-items: center;
border: 1px black solid;
box-sizing: border-box;
display: flex;
height: 100%;
justify-content: center;
position: absolute;
width: 100%;
}
#close {
background-image: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg==) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAA9UlEQVR4Xu3UsWrCUByH0fMEouiuhrg4xohToJVGH0CHLBncEwfx/VvIFHLJBWmHDvKbv7PcP9f3L/fXwBsApZSRpUpEgbOnxwiReng6x4AvjdrNXRLkibubWqMcB9Yujk7qjhjmtZOji/U4wELuoBwQXa50kFsQA5jK+kQ/l5kSA4ZEK5Fo+3kcCIlGM8ijQEhUqkEeBUKiUPTyl4C5vZ1cbmdv/iqwclXY6aZwtXoFSLQqhVwmkytUWglxAMG7T0yCu4gD0v7ZBKeVxoEwFxIxYBPmIWEzDnyEeUj4HAfYdvmMcGYdsSUGsOzlIbHEv/uV38APrreiBRBIs3QAAAAASUVORK5CYII=) 2x);
background-position: right top;
background-repeat: no-repeat;
cursor: pointer;
height: 14px;
position: absolute;
right: 3px;
top: 3px;
visibility: hidden;
width: 14px;
}
#close[data-plugin-type='document'] {
display: none;
}
#close:hover {
background-image: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqUlEQVR4XqWRMQqEMBBF/1E8Ra6x6V3FRnS9QbCxtJg6Z7CzE9lTiIXXyUb3C8EULixDIMM8Zt4kcDfxM5A45U+cgeXnC1tREgkzAgob3hiq3CUHvGLG4FTQoSgxQGDrzN8WTLBGnx2IVDksen9GH7Z9hA5E6uxABMJyCHDMCEGHzugLQPPlBCBNGq+5YtpnGw1Bv+te15ypljTpVzdak5Opy+z+qf//zQ+Lg+07ay5KsgAAAABJRU5ErkJggg==) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAB4UlEQVR42u2VsWoCQRBAh+MUFP0C1V9QD4NEOxs9xBQHQVCwSJFWVBAtBNXCxk6wTkBJYUTwEwQLC61E8QP0NzZzt5g5726DkC7EYWHZ8T3WndkV2C/jLwn4hwVYBIdLn9vkLp79QcBCTDMiy3w2gQ9XeTYkEHA8vqj2rworXu3HF1YFfSWgp5QFnKVLvYvzDEKEZ5hW70oXOCtcEbQLIkx7+IQtfMBSOjU6XEF4oyOdYInZbXyOuajjDlpNeQgleIUJKUz4BDMledhqOu/AzVSmzZ49CUjCC0yvim98iqtJT2L2jKsqczsdok9XrHNexaww415lnTNwn6CM/KxJIR8bnUZHPhLO6yMoIyk2pNjLewFuE5AiY1KMMQx8Q7hQYFek4AkjxXFe1rsF84I/BTFQMGL+1Lxwl4DwdtM1gjwKohgxyLtG7SYpxALqugOMcfOKN+bFXeBsLB1uulNcRqq7/tt36k41zoL6QlxGjtd6lrahiqCi1iOFYyvXuxY8yzK33VnvUivbLlOlj/jktm0s3YnXrNIXXufHNxuOGasi8S68zkwrlnV8ZcJJsTIUxbLgQcFZWE8N0gau2p40VVcM0gYeFpSRK6445UhBuKiRgiyKw+34rLt59nb1/7+RwReVkaFtqvNBuwAAAABJRU5ErkJggg==) 2x);
}
#close:active {
background-image: -webkit-image-set(
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAQklEQVR4AWP4TwBSTQGDHcMZIIYAKA9VwRkwtINJgyCaCTAlCBaKAoQ+hFmoCqBKENKkK8C0gpAjCXuTyICiQ2QBAPSwyG3ByZlCAAAAAElFTkSuQmCC) 1x,
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAA/ElEQVR4Xu3UsWrCUBiG4efGlIBoIMFbcnYolYJ3pg4iKGrGYFTRwaUFhYAekiDt0EG++X2W83N8/3J/DbwBMJJSsdQItcDY1VlCOImzq3Ed8OmicHASB3ns5KBw8VUNpDJrW7uAiJ3sbK1l0mqArpmFTUlQ5jYWZrrUAUSmT0SZm4qoA56JvVhs/5g3A7RLolA85A1ASOTye65NMxASK6syfxGITMzvMxG9CvRkliWwlOm9AsSOcitzU1NzK7mjuBkQvHtLK7iLBiB5PhttJSGpB8I8vM6kDuiHeUjoVwMfYR4SRtUAw1veIZzOjRhSBzCoyKFjgH/3K7+BHzg+Cgw0eSW3AAAAAElFTkSuQmCC) 2x);
}
</style>
<style>
#outer {
border: none;
cursor: pointer;
}
#shielding {
background-color: rgba(0, 0, 0, 0.5);
height: 100%;
left: 0px;
position: absolute;
top: 0px;
width: 100%;
z-index: 2;
}
#plugin-icon {
display: block;
opacity: 0.8;
}
#plugin-icon:hover {
opacity: 0.95;
}
#poster {
height: 100%;
object-fit: contain;
width: 100%;
z-index: 1;
}
#inner-container {
align-items: center;
display: flex;
justify-content: center;
left: 0px;
position: absolute;
top: 0px;
z-index: 2;
}
</style>
<base i18n-values="href:baseurl">
</head>
<body>
<div i18n-values="title:name" id="outer">
<img id="poster" i18n-values="srcset:poster">
<div id="shielding"></div>
<div id="inner-container"
i18n-values=".style.width:visibleWidth;.style.height:visibleHeight">
<div id="inner">
<img id="plugin-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAA8dJREFUaAXdWs9vEkEUZkvZQloNxJoY40kxJsDNo9GDif+Bf4CHpjEmJPoPeMce1F408dYjQePBgycv3vRG4GBMPREPLSlpCUWggN+3ncVl6PJrZ5a6k0x2Z2fnve/befPmzcwaIbVpGeLCyEsiG5L4Pso9kbu4nkj1CysSaAQ5trm5uV6pVB4eHx+/brVan3u93s9+v19Fbolc5TPW8R2+yzZsK2TIpPFYf6LS6Pb29rX9/f0NgPsIsA3kWVODbSmDsigT2TdCZjabvVyv158D9eGsyMe8f0iZlA0yJrK2FE4mkxer1eoTmMjvMYA8VVE2dVAXmHC8KU1mPp9Pdjqd755QztCYuqgTLJT1TrRYLN7T2Qtu/KiTukGGY8dTiu3t7T2CoqabMh+eN4kBLOjd5kpRQcIHrJNVCDIz94zJLoX4RfaEzK4pzGzqMRPmIFvEmJCRy2ViEg5gsjej2/PTO8lgJ5WJTbjmsePFpA+fJGzR9cQIFq4mZnBWPY8mJX84YhQRwJnhTFSEHXK7mcrNZvMdGjBQ1JqIFb0y4sUMBm3Q7Dl2KpVKd5jx1Xa1MgFWEWgO9UqEEagKxSSBL7WSy+WuIrJ9r0Kmmwxihi4uIwYpJkJxtzZTPxdE7IXVaq1We4bGWkyNmMHAmvGpMIQFzqppmg8GtNTccCXYSCQSb8rl8n2Q+aVG7D8pxEzs9pNlrtam/uQTXnT0iC2f14guUyN2yOcSO7TCpecEfFNXuxChHvY+Te0phCkzNWInB8uWw+HwLWrSnGxTe6vS1AT2JYtIJBK5oZmEU/yfTCbzbWtr62673f7grJjnXmC3xvoaupq7HUrSGNOScaoytSoEr1lMcHNB1uJDWZWpWdhtIj7g1qvCcltQUUe+pFfViHR+xBi82EY8Hn+Be9dodqTl8ANit1wix0ZtuE57ifPKFczMOyDxCtrmJRES2PvskR4WK7uYJZPa4Z8qiMIh3E6lUjuGYVz3qpPYyYHd2+t2uz+8CpyiPXVxQnycTqe/qCBBnQK7RaR7cHDwdQogXl5xmtJLCJrblGQQAjt39q2gcR22Ns8mNMfXUDpjHqEp6VqfNMSO/oCfzjBeaWzl/GojYTzonBwdHX0a0PJ+o82UnNAE5qHDosAsdUk0EJsPJBKY7SCSCcQGHYmEgrJlSi7B2MQmE6RAHCucUoEXC8JBj00mEEdvNplAHIbaZMxCoXDTzwMg6lJ9PG2TCcQPAzYZXv/7XzicZALxU41M6Fz85jR02uNEOOc9NzN4dMz1ObMsX9uPZ38BvwZw6z92PI4AAAAASUVORK5CYII="
i18n-values=".style.maxWidth:visibleWidth;
.style.maxHeight:visibleHeight" />
</div>
</div>
</div>
<script>
document.getElementById('poster').onerror = function() {
this.hidden = true;
};
document.getElementById('outer').onclick = function() {
plugin.load();
};
window.setPosterMargin = function(marginLeft, marginTop) {
var container = document.getElementById('inner-container');
container.style.marginLeft = marginLeft;
container.style.marginTop = marginTop;
};
</script>
</body>
</html>
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var chrome;
if (!chrome)
chrome = {};
if (!chrome.embeddedSearch) {
chrome.embeddedSearch = new function() {
this.searchBox = new function() {
// =======================================================================
// Private functions
// =======================================================================
native function Focus();
native function GetDisplayInstantResults();
native function GetMostVisitedItemData();
native function GetQuery();
native function GetSearchRequestParams();
native function GetRightToLeft();
native function GetStartMargin();
native function GetSuggestionToPrefetch();
native function IsFocused();
native function IsKeyCaptureEnabled();
native function Paste();
native function SetVoiceSearchSupported();
native function StartCapturingKeyStrokes();
native function StopCapturingKeyStrokes();
// =======================================================================
// Exported functions
// =======================================================================
this.__defineGetter__('displayInstantResults', GetDisplayInstantResults);
this.__defineGetter__('isFocused', IsFocused);
this.__defineGetter__('isKeyCaptureEnabled', IsKeyCaptureEnabled);
this.__defineGetter__('rtl', GetRightToLeft);
this.__defineGetter__('startMargin', GetStartMargin);
this.__defineGetter__('suggestion', GetSuggestionToPrefetch);
this.__defineGetter__('value', GetQuery);
Object.defineProperty(this, 'requestParams',
{ get: GetSearchRequestParams });
this.focus = function() {
Focus();
};
// This method is restricted to chrome-search://most-visited pages by
// checking the invoking context's origin in searchbox_extension.cc.
this.getMostVisitedItemData = function(restrictedId) {
var item = GetMostVisitedItemData(restrictedId);
if (item) {
var sizeInPx = Math.floor(48 * window.devicePixelRatio + 0.5);
// Populate large icon and fallback icon data, if they exist. We'll
// render everything here, once these become available by default.
if (item.largeIconUrl) {
item.largeIconUrl +=
sizeInPx + "/" + item.renderViewId + "/" + item.rid;
}
if (item.fallbackIconUrl) {
item.fallbackIconUrl +=
sizeInPx + ",,,,/" + item.renderViewId + "/" + item.rid;
}
}
return item;
};
this.paste = function(value) {
Paste(value);
};
this.setVoiceSearchSupported = function(supported) {
SetVoiceSearchSupported(supported);
};
this.startCapturingKeyStrokes = function() {
StartCapturingKeyStrokes();
};
this.stopCapturingKeyStrokes = function() {
StopCapturingKeyStrokes();
};
this.onfocuschange = null;
this.onkeycapturechange = null;
this.onmarginchange = null;
this.onsubmit = null;
this.onsuggestionchange = null;
this.ontogglevoicesearch = null;
//TODO(jered): Remove this empty method when google no longer requires it.
this.setRestrictedValue = function() {};
};
this.newTabPage = new function() {
// =======================================================================
// Private functions
// =======================================================================
native function CheckIsUserSignedInToChromeAs();
native function CheckIsUserSyncingHistory();
native function DeleteMostVisitedItem();
native function GetAppLauncherEnabled();
native function GetDispositionFromClick();
native function GetMostVisitedItems();
native function GetThemeBackgroundInfo();
native function IsInputInProgress();
native function LogEvent();
native function LogMostVisitedImpression();
native function LogMostVisitedNavigation();
native function NavigateContentWindow();
native function UndoAllMostVisitedDeletions();
native function UndoMostVisitedDeletion();
function GetMostVisitedItemsWrapper() {
var mostVisitedItems = GetMostVisitedItems();
for (var i = 0, item; item = mostVisitedItems[i]; ++i) {
item.faviconUrl = GenerateFaviconURL(item.renderViewId, item.rid);
// These properties are private data and should not be returned to
// the page. They are only accessible via getMostVisitedItemData().
delete item.url;
delete item.title;
delete item.domain;
delete item.direction;
delete item.renderViewId;
delete item.largeIconUrl;
delete item.fallbackIconUrl;
}
return mostVisitedItems;
}
function GenerateFaviconURL(renderViewId, rid) {
return "chrome-search://favicon/size/16@" +
window.devicePixelRatio + "x/" +
renderViewId + "/" + rid;
}
// =======================================================================
// Exported functions
// =======================================================================
this.__defineGetter__('appLauncherEnabled', GetAppLauncherEnabled);
this.__defineGetter__('isInputInProgress', IsInputInProgress);
this.__defineGetter__('mostVisited', GetMostVisitedItemsWrapper);
this.__defineGetter__('themeBackgroundInfo', GetThemeBackgroundInfo);
this.deleteMostVisitedItem = function(restrictedId) {
DeleteMostVisitedItem(restrictedId);
};
this.getDispositionFromClick = function(middle_button,
alt_key,
ctrl_key,
meta_key,
shift_key) {
return GetDispositionFromClick(middle_button,
alt_key,
ctrl_key,
meta_key,
shift_key);
};
this.checkIsUserSignedIntoChromeAs = function(identity) {
CheckIsUserSignedInToChromeAs(identity);
};
this.checkIsUserSyncingHistory = function() {
CheckIsUserSyncingHistory();
};
// This method is restricted to chrome-search://most-visited pages by
// checking the invoking context's origin in searchbox_extension.cc.
this.logEvent = function(histogram_name) {
LogEvent(histogram_name);
};
// This method is restricted to chrome-search://most-visited pages by
// checking the invoking context's origin in searchbox_extension.cc.
this.logMostVisitedImpression = function(position, provider) {
LogMostVisitedImpression(position, provider);
};
// This method is restricted to chrome-search://most-visited pages by
// checking the invoking context's origin in searchbox_extension.cc.
this.logMostVisitedNavigation = function(position, provider) {
LogMostVisitedNavigation(position, provider);
};
this.navigateContentWindow = function(destination, disposition) {
NavigateContentWindow(destination, disposition);
};
this.undoAllMostVisitedDeletions = function() {
UndoAllMostVisitedDeletions();
};
this.undoMostVisitedDeletion = function(restrictedId) {
UndoMostVisitedDeletion(restrictedId);
};
this.onsignedincheckdone = null;
this.onhistorysynccheckdone = null;
this.oninputcancel = null;
this.oninputstart = null;
this.onmostvisitedchange = null;
this.onthemechange = null;
};
// TODO(jered): Remove when google no longer expects this object.
chrome.searchBox = this.searchBox;
};
}
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the app API.
var GetAvailability = requireNative('v8_context').GetAvailability;
if (!GetAvailability('app').is_available) {
exports.binding = {};
exports.onInstallStateResponse = function(){};
return;
}
var appNatives = requireNative('app');
var process = requireNative('process');
var extensionId = process.GetExtensionId();
var logActivity = requireNative('activityLogger');
function wrapForLogging(fun) {
if (!extensionId)
return fun; // nothing interesting to log without an extension
return function() {
// TODO(ataly): We need to make sure we use the right prototype for
// fun.apply. Array slice can either be rewritten or similarly defined.
logActivity.LogAPICall(extensionId, "app." + fun.name,
$Array.slice(arguments));
return $Function.apply(fun, this, arguments);
};
}
// This becomes chrome.app
var app = {
getIsInstalled: wrapForLogging(appNatives.GetIsInstalled),
getDetails: wrapForLogging(appNatives.GetDetails),
getDetailsForFrame: wrapForLogging(appNatives.GetDetailsForFrame),
runningState: wrapForLogging(appNatives.GetRunningState)
};
// Tricky; "getIsInstalled" is actually exposed as the getter "isInstalled",
// but we don't have a way to express this in the schema JSON (nor is it
// worth it for this one special case).
//
// So, define it manually, and let the getIsInstalled function act as its
// documentation.
app.__defineGetter__('isInstalled', wrapForLogging(appNatives.GetIsInstalled));
// Called by app_bindings.cc.
function onInstallStateResponse(state, callbackId) {
var callback = callbacks[callbackId];
delete callbacks[callbackId];
if (typeof(callback) == 'function') {
try {
callback(state);
} catch (e) {
console.error('Exception in chrome.app.installState response handler: ' +
e.stack);
}
}
}
// TODO(kalman): move this stuff to its own custom bindings.
var callbacks = {};
var nextCallbackId = 1;
app.installState = function getInstallState(callback) {
var callbackId = nextCallbackId++;
callbacks[callbackId] = callback;
appNatives.GetInstallState(callbackId);
};
if (extensionId)
app.installState = wrapForLogging(app.installState);
exports.binding = app;
exports.onInstallStateResponse = onInstallStateResponse;
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom bindings for the automation API.
var AutomationNode = require('automationNode').AutomationNode;
var AutomationRootNode = require('automationNode').AutomationRootNode;
var automation = require('binding').Binding.create('automation');
var automationInternal =
require('binding').Binding.create('automationInternal').generate();
var eventBindings = require('event_bindings');
var Event = eventBindings.Event;
var forEach = require('utils').forEach;
var lastError = require('lastError');
var logging = requireNative('logging');
var nativeAutomationInternal = requireNative('automationInternal');
var GetRoutingID = nativeAutomationInternal.GetRoutingID;
var GetSchemaAdditions = nativeAutomationInternal.GetSchemaAdditions;
var schema = GetSchemaAdditions();
/**
* A namespace to export utility functions to other files in automation.
*/
window.automationUtil = function() {};
// TODO(aboxhall): Look into using WeakMap
var idToAutomationRootNode = {};
var idToCallback = {};
var DESKTOP_TREE_ID = 0;
automationUtil.storeTreeCallback = function(id, callback) {
if (!callback)
return;
var targetTree = idToAutomationRootNode[id];
if (!targetTree) {
// If we haven't cached the tree, hold the callback until the tree is
// populated by the initial onAccessibilityEvent call.
if (id in idToCallback)
idToCallback[id].push(callback);
else
idToCallback[id] = [callback];
} else {
callback(targetTree);
}
};
/**
* Global list of tree change observers.
* @type {Array<TreeChangeObserver>}
*/
automationUtil.treeChangeObservers = [];
automation.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
// TODO(aboxhall, dtseng): Make this return the speced AutomationRootNode obj.
apiFunctions.setHandleRequest('getTree', function getTree(tabID, callback) {
var routingID = GetRoutingID();
// enableTab() ensures the renderer for the active or specified tab has
// accessibility enabled, and fetches its ax tree id to use as
// a key in the idToAutomationRootNode map. The callback to
// enableTab is bound to the callback passed in to getTree(), so that once
// the tree is available (either due to having been cached earlier, or after
// an accessibility event occurs which causes the tree to be populated), the
// callback can be called.
var params = { routingID: routingID, tabID: tabID };
automationInternal.enableTab(params,
function onEnable(id) {
if (lastError.hasError(chrome)) {
callback();
return;
}
automationUtil.storeTreeCallback(id, callback);
});
});
var desktopTree = null;
apiFunctions.setHandleRequest('getDesktop', function(callback) {
desktopTree =
idToAutomationRootNode[DESKTOP_TREE_ID];
if (!desktopTree) {
if (DESKTOP_TREE_ID in idToCallback)
idToCallback[DESKTOP_TREE_ID].push(callback);
else
idToCallback[DESKTOP_TREE_ID] = [callback];
var routingID = GetRoutingID();
// TODO(dtseng): Disable desktop tree once desktop object goes out of
// scope.
automationInternal.enableDesktop(routingID, function() {
if (lastError.hasError(chrome)) {
delete idToAutomationRootNode[
DESKTOP_TREE_ID];
callback();
return;
}
});
} else {
callback(desktopTree);
}
});
function removeTreeChangeObserver(observer) {
var observers = automationUtil.treeChangeObservers;
for (var i = 0; i < observers.length; i++) {
if (observer == observers[i])
observers.splice(i, 1);
}
}
apiFunctions.setHandleRequest('removeTreeChangeObserver', function(observer) {
removeTreeChangeObserver(observer);
});
function addTreeChangeObserver(observer) {
removeTreeChangeObserver(observer);
automationUtil.treeChangeObservers.push(observer);
}
apiFunctions.setHandleRequest('addTreeChangeObserver', function(observer) {
addTreeChangeObserver(observer);
});
});
// Listen to the automationInternal.onAccessibilityEvent event, which is
// essentially a proxy for the AccessibilityHostMsg_Events IPC from the
// renderer.
automationInternal.onAccessibilityEvent.addListener(function(data) {
var id = data.treeID;
var targetTree = idToAutomationRootNode[id];
if (!targetTree) {
// If this is the first time we've gotten data for this tree, it will
// contain all of the tree's data, so create a new tree which will be
// bootstrapped from |data|.
targetTree = new AutomationRootNode(id);
idToAutomationRootNode[id] = targetTree;
}
if (!privates(targetTree).impl.onAccessibilityEvent(data))
return;
// If we're not waiting on a callback to getTree(), we can early out here.
if (!(id in idToCallback))
return;
// We usually get a 'placeholder' tree first, which doesn't have any url
// attribute or child nodes. If we've got that, wait for the full tree before
// calling the callback.
// TODO(dmazzoni): Don't send down placeholder (crbug.com/397553)
if (id != DESKTOP_TREE_ID && !targetTree.attributes.url &&
targetTree.children.length == 0) {
return;
}
// If the tree wasn't available when getTree() was called, the callback will
// have been cached in idToCallback, so call and delete it now that we
// have the complete tree.
for (var i = 0; i < idToCallback[id].length; i++) {
console.log('calling getTree() callback');
var callback = idToCallback[id][i];
callback(targetTree);
}
delete idToCallback[id];
});
automationInternal.onAccessibilityTreeDestroyed.addListener(function(id) {
var targetTree = idToAutomationRootNode[id];
if (targetTree) {
privates(targetTree).impl.destroy();
delete idToAutomationRootNode[id];
} else {
logging.WARNING('no targetTree to destroy');
}
delete idToAutomationRootNode[id];
});
exports.binding = automation.generate();
// Add additional accessibility bindings not specified in the automation IDL.
// Accessibility and automation share some APIs (see
// ui/accessibility/ax_enums.idl).
forEach(schema, function(k, v) {
exports.binding[k] = v;
});
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var utils = require('utils');
var AutomationEventImpl = function(type, target) {
this.propagationStopped = false;
// TODO(aboxhall): make these read-only properties
this.type = type;
this.target = target;
this.eventPhase = Event.NONE;
};
AutomationEventImpl.prototype = {
stopPropagation: function() {
this.propagationStopped = true;
}
};
exports.AutomationEvent = utils.expose(
'AutomationEvent',
AutomationEventImpl,
{ functions: ['stopPropagation'],
readonly: ['type', 'target', 'eventPhase'] });
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var AutomationEvent = require('automationEvent').AutomationEvent;
var automationInternal =
require('binding').Binding.create('automationInternal').generate();
var IsInteractPermitted =
requireNative('automationInternal').IsInteractPermitted;
var lastError = require('lastError');
var logging = requireNative('logging');
var schema = requireNative('automationInternal').GetSchemaAdditions();
var utils = require('utils');
/**
* A single node in the Automation tree.
* @param {AutomationRootNodeImpl} root The root of the tree.
* @constructor
*/
function AutomationNodeImpl(root) {
this.rootImpl = root;
this.childIds = [];
// Public attributes. No actual data gets set on this object.
this.attributes = {};
// Internal object holding all attributes.
this.attributesInternal = {};
this.listeners = {};
this.location = { left: 0, top: 0, width: 0, height: 0 };
}
AutomationNodeImpl.prototype = {
id: -1,
role: '',
state: { busy: true },
isRootNode: false,
get root() {
return this.rootImpl.wrapper;
},
get parent() {
return this.hostTree || this.rootImpl.get(this.parentID);
},
get firstChild() {
return this.childTree || this.rootImpl.get(this.childIds[0]);
},
get lastChild() {
var childIds = this.childIds;
return this.childTree || this.rootImpl.get(childIds[childIds.length - 1]);
},
get children() {
if (this.childTree)
return [this.childTree];
var children = [];
for (var i = 0, childID; childID = this.childIds[i]; i++) {
logging.CHECK(this.rootImpl.get(childID));
children.push(this.rootImpl.get(childID));
}
return children;
},
get previousSibling() {
var parent = this.parent;
if (parent && this.indexInParent > 0)
return parent.children[this.indexInParent - 1];
return undefined;
},
get nextSibling() {
var parent = this.parent;
if (parent && this.indexInParent < parent.children.length)
return parent.children[this.indexInParent + 1];
return undefined;
},
doDefault: function() {
this.performAction_('doDefault');
},
focus: function() {
this.performAction_('focus');
},
makeVisible: function() {
this.performAction_('makeVisible');
},
setSelection: function(startIndex, endIndex) {
this.performAction_('setSelection',
{ startIndex: startIndex,
endIndex: endIndex });
},
showContextMenu: function() {
this.performAction_('showContextMenu');
},
domQuerySelector: function(selector, callback) {
automationInternal.querySelector(
{ treeID: this.rootImpl.treeID,
automationNodeID: this.id,
selector: selector },
this.domQuerySelectorCallback_.bind(this, callback));
},
find: function(params) {
return this.findInternal_(params);
},
findAll: function(params) {
return this.findInternal_(params, []);
},
matches: function(params) {
return this.matchInternal_(params);
},
addEventListener: function(eventType, callback, capture) {
this.removeEventListener(eventType, callback);
if (!this.listeners[eventType])
this.listeners[eventType] = [];
this.listeners[eventType].push({callback: callback, capture: !!capture});
},
// TODO(dtseng/aboxhall): Check this impl against spec.
removeEventListener: function(eventType, callback) {
if (this.listeners[eventType]) {
var listeners = this.listeners[eventType];
for (var i = 0; i < listeners.length; i++) {
if (callback === listeners[i].callback)
listeners.splice(i, 1);
}
}
},
toJSON: function() {
return { treeID: this.treeID,
id: this.id,
role: this.role,
attributes: this.attributes };
},
dispatchEvent: function(eventType) {
var path = [];
var parent = this.parent;
while (parent) {
path.push(parent);
parent = parent.parent;
}
var event = new AutomationEvent(eventType, this.wrapper);
// Dispatch the event through the propagation path in three phases:
// - capturing: starting from the root and going down to the target's parent
// - targeting: dispatching the event on the target itself
// - bubbling: starting from the target's parent, going back up to the root.
// At any stage, a listener may call stopPropagation() on the event, which
// will immediately stop event propagation through this path.
if (this.dispatchEventAtCapturing_(event, path)) {
if (this.dispatchEventAtTargeting_(event, path))
this.dispatchEventAtBubbling_(event, path);
}
},
toString: function() {
var impl = privates(this).impl;
if (!impl)
impl = this;
return 'node id=' + impl.id +
' role=' + this.role +
' state=' + $JSON.stringify(this.state) +
' parentID=' + impl.parentID +
' childIds=' + $JSON.stringify(impl.childIds) +
' attributes=' + $JSON.stringify(this.attributes);
},
dispatchEventAtCapturing_: function(event, path) {
privates(event).impl.eventPhase = Event.CAPTURING_PHASE;
for (var i = path.length - 1; i >= 0; i--) {
this.fireEventListeners_(path[i], event);
if (privates(event).impl.propagationStopped)
return false;
}
return true;
},
dispatchEventAtTargeting_: function(event) {
privates(event).impl.eventPhase = Event.AT_TARGET;
this.fireEventListeners_(this.wrapper, event);
return !privates(event).impl.propagationStopped;
},
dispatchEventAtBubbling_: function(event, path) {
privates(event).impl.eventPhase = Event.BUBBLING_PHASE;
for (var i = 0; i < path.length; i++) {
this.fireEventListeners_(path[i], event);
if (privates(event).impl.propagationStopped)
return false;
}
return true;
},
fireEventListeners_: function(node, event) {
var nodeImpl = privates(node).impl;
var listeners = nodeImpl.listeners[event.type];
if (!listeners)
return;
var eventPhase = event.eventPhase;
for (var i = 0; i < listeners.length; i++) {
if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
continue;
if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
continue;
try {
listeners[i].callback(event);
} catch (e) {
console.error('Error in event handler for ' + event.type +
'during phase ' + eventPhase + ': ' +
e.message + '\nStack trace: ' + e.stack);
}
}
},
performAction_: function(actionType, opt_args) {
// Not yet initialized.
if (this.rootImpl.treeID === undefined ||
this.id === undefined) {
return;
}
// Check permissions.
if (!IsInteractPermitted()) {
throw new Error(actionType + ' requires {"desktop": true} or' +
' {"interact": true} in the "automation" manifest key.');
}
automationInternal.performAction({ treeID: this.rootImpl.treeID,
automationNodeID: this.id,
actionType: actionType },
opt_args || {});
},
domQuerySelectorCallback_: function(userCallback, resultAutomationNodeID) {
// resultAutomationNodeID could be zero or undefined or (unlikely) null;
// they all amount to the same thing here, which is that no node was
// returned.
if (!resultAutomationNodeID) {
userCallback(null);
return;
}
var resultNode = this.rootImpl.get(resultAutomationNodeID);
if (!resultNode) {
logging.WARNING('Query selector result not in tree: ' +
resultAutomationNodeID);
userCallback(null);
}
userCallback(resultNode);
},
findInternal_: function(params, opt_results) {
var result = null;
this.forAllDescendants_(function(node) {
if (privates(node).impl.matchInternal_(params)) {
if (opt_results)
opt_results.push(node);
else
result = node;
return !opt_results;
}
});
if (opt_results)
return opt_results;
return result;
},
/**
* Executes a closure for all of this node's descendants, in pre-order.
* Early-outs if the closure returns true.
* @param {Function(AutomationNode):boolean} closure Closure to be executed
* for each node. Return true to early-out the traversal.
*/
forAllDescendants_: function(closure) {
var stack = this.wrapper.children.reverse();
while (stack.length > 0) {
var node = stack.pop();
if (closure(node))
return;
var children = node.children;
for (var i = children.length - 1; i >= 0; i--)
stack.push(children[i]);
}
},
matchInternal_: function(params) {
if (Object.keys(params).length == 0)
return false;
if ('role' in params && this.role != params.role)
return false;
if ('state' in params) {
for (var state in params.state) {
if (params.state[state] != (state in this.state))
return false;
}
}
if ('attributes' in params) {
for (var attribute in params.attributes) {
if (!(attribute in this.attributesInternal))
return false;
var attrValue = params.attributes[attribute];
if (typeof attrValue != 'object') {
if (this.attributesInternal[attribute] !== attrValue)
return false;
} else if (attrValue instanceof RegExp) {
if (typeof this.attributesInternal[attribute] != 'string')
return false;
if (!attrValue.test(this.attributesInternal[attribute]))
return false;
} else {
// TODO(aboxhall): handle intlist case.
return false;
}
}
}
return true;
}
};
// Maps an attribute to its default value in an invalidated node.
// These attributes are taken directly from the Automation idl.
var AutomationAttributeDefaults = {
'id': -1,
'role': '',
'state': {},
'location': { left: 0, top: 0, width: 0, height: 0 }
};
var AutomationAttributeTypes = [
'boolAttributes',
'floatAttributes',
'htmlAttributes',
'intAttributes',
'intlistAttributes',
'stringAttributes'
];
/**
* Maps an attribute name to another attribute who's value is an id or an array
* of ids referencing an AutomationNode.
* @param {!Object<string>}
* @const
*/
var ATTRIBUTE_NAME_TO_ID_ATTRIBUTE = {
'aria-activedescendant': 'activedescendantId',
'aria-controls': 'controlsIds',
'aria-describedby': 'describedbyIds',
'aria-flowto': 'flowtoIds',
'aria-labelledby': 'labelledbyIds',
'aria-owns': 'ownsIds'
};
/**
* A set of attributes ignored in the automation API.
* @param {!Object<boolean>}
* @const
*/
var ATTRIBUTE_BLACKLIST = {'activedescendantId': true,
'childTreeId': true,
'controlsIds': true,
'describedbyIds': true,
'flowtoIds': true,
'labelledbyIds': true,
'ownsIds': true
};
function defaultStringAttribute(opt_defaultVal) {
return { default: undefined, reflectFrom: 'stringAttributes' };
}
function defaultIntAttribute(opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : 0;
return { default: defaultVal, reflectFrom: 'intAttributes' };
}
function defaultFloatAttribute(opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : 0;
return { default: defaultVal, reflectFrom: 'floatAttributes' };
}
function defaultBoolAttribute(opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : false;
return { default: defaultVal, reflectFrom: 'boolAttributes' };
}
function defaultHtmlAttribute(opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : '';
return { default: defaultVal, reflectFrom: 'htmlAttributes' };
}
function defaultIntListAttribute(opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : [];
return { default: defaultVal, reflectFrom: 'intlistAttributes' };
}
function defaultNodeRefAttribute(idAttribute, opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : null;
return { default: defaultVal,
idFrom: 'intAttributes',
idAttribute: idAttribute,
isRef: true };
}
function defaultNodeRefListAttribute(idAttribute, opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : [];
return { default: [],
idFrom: 'intlistAttributes',
idAttribute: idAttribute,
isRef: true };
}
// Maps an attribute to its default value in an invalidated node.
// These attributes are taken directly from the Automation idl.
var DefaultMixinAttributes = {
description: defaultStringAttribute(),
help: defaultStringAttribute(),
name: defaultStringAttribute(),
value: defaultStringAttribute(),
htmlTag: defaultStringAttribute(),
hierarchicalLevel: defaultIntAttribute(),
controls: defaultNodeRefListAttribute('controlsIds'),
describedby: defaultNodeRefListAttribute('describedbyIds'),
flowto: defaultNodeRefListAttribute('flowtoIds'),
labelledby: defaultNodeRefListAttribute('labelledbyIds'),
owns: defaultNodeRefListAttribute('ownsIds'),
wordStarts: defaultIntListAttribute(),
wordEnds: defaultIntListAttribute()
};
var ActiveDescendantMixinAttribute = {
activedescendant: defaultNodeRefAttribute('activedescendantId')
};
var LinkMixinAttributes = {
url: defaultStringAttribute()
};
var DocumentMixinAttributes = {
docUrl: defaultStringAttribute(),
docTitle: defaultStringAttribute(),
docLoaded: defaultStringAttribute(),
docLoadingProgress: defaultFloatAttribute()
};
var ScrollableMixinAttributes = {
scrollX: defaultIntAttribute(),
scrollXMin: defaultIntAttribute(),
scrollXMax: defaultIntAttribute(),
scrollY: defaultIntAttribute(),
scrollYMin: defaultIntAttribute(),
scrollYMax: defaultIntAttribute()
};
var EditableTextMixinAttributes = {
textSelStart: defaultIntAttribute(-1),
textSelEnd: defaultIntAttribute(-1),
type: defaultHtmlAttribute()
};
var RangeMixinAttributes = {
valueForRange: defaultFloatAttribute(),
minValueForRange: defaultFloatAttribute(),
maxValueForRange: defaultFloatAttribute()
};
var TableMixinAttributes = {
tableRowCount: defaultIntAttribute(),
tableColumnCount: defaultIntAttribute()
};
var TableCellMixinAttributes = {
tableCellColumnIndex: defaultIntAttribute(),
tableCellColumnSpan: defaultIntAttribute(1),
tableCellRowIndex: defaultIntAttribute(),
tableCellRowSpan: defaultIntAttribute(1)
};
var LiveRegionMixinAttributes = {
containerLiveAtomic: defaultBoolAttribute(),
containerLiveBusy: defaultBoolAttribute(),
containerLiveRelevant: defaultStringAttribute(),
containerLiveStatus: defaultStringAttribute(),
};
/**
* AutomationRootNode.
*
* An AutomationRootNode is the javascript end of an AXTree living in the
* browser. AutomationRootNode handles unserializing incremental updates from
* the source AXTree. Each update contains node data that form a complete tree
* after applying the update.
*
* A brief note about ids used through this class. The source AXTree assigns
* unique ids per node and we use these ids to build a hash to the actual
* AutomationNode object.
* Thus, tree traversals amount to a lookup in our hash.
*
* The tree itself is identified by the accessibility tree id of the
* renderer widget host.
* @constructor
*/
function AutomationRootNodeImpl(treeID) {
AutomationNodeImpl.call(this, this);
this.treeID = treeID;
this.axNodeDataCache_ = {};
}
AutomationRootNodeImpl.prototype = {
__proto__: AutomationNodeImpl.prototype,
isRootNode: true,
treeID: -1,
get: function(id) {
if (id == undefined)
return undefined;
return this.axNodeDataCache_[id];
},
unserialize: function(update) {
var updateState = { pendingNodes: {}, newNodes: {} };
var oldRootId = this.id;
if (update.nodeIdToClear < 0) {
logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear);
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
return false;
} else if (update.nodeIdToClear > 0) {
var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
if (!nodeToClear) {
logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
' (not in cache)');
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
return false;
}
if (nodeToClear === this.wrapper) {
this.invalidate_(nodeToClear);
} else {
var children = nodeToClear.children;
for (var i = 0; i < children.length; i++)
this.invalidate_(children[i]);
var nodeToClearImpl = privates(nodeToClear).impl;
nodeToClearImpl.childIds = []
updateState.pendingNodes[nodeToClearImpl.id] = nodeToClear;
}
}
for (var i = 0; i < update.nodes.length; i++) {
if (!this.updateNode_(update.nodes[i], updateState))
return false;
}
if (Object.keys(updateState.pendingNodes).length > 0) {
logging.WARNING('Nodes left pending by the update: ' +
$JSON.stringify(updateState.pendingNodes));
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
return false;
}
// Notify tree change observers of new nodes.
// TODO(dmazzoni): Notify tree change observers of changed nodes,
// and handle subtreeCreated and nodeCreated properly.
var observers = automationUtil.treeChangeObservers;
if (observers.length > 0) {
for (var nodeId in updateState.newNodes) {
var node = updateState.newNodes[nodeId];
var treeChange =
{target: node, type: schema.TreeChangeType.nodeCreated};
for (var i = 0; i < observers.length; i++) {
try {
observers[i](treeChange);
} catch (e) {
console.error('Error in tree change observer for ' +
treeChange.type + ': ' + e.message +
'\nStack trace: ' + e.stack);
}
}
}
}
return true;
},
destroy: function() {
if (this.hostTree)
this.hostTree.childTree = undefined;
this.hostTree = undefined;
this.dispatchEvent(schema.EventType.destroyed);
this.invalidate_(this.wrapper);
},
onAccessibilityEvent: function(eventParams) {
if (!this.unserialize(eventParams.update)) {
logging.WARNING('unserialization failed');
return false;
}
var targetNode = this.get(eventParams.targetID);
if (targetNode) {
var targetNodeImpl = privates(targetNode).impl;
targetNodeImpl.dispatchEvent(eventParams.eventType);
} else {
logging.WARNING('Got ' + eventParams.eventType +
' event on unknown node: ' + eventParams.targetID +
'; this: ' + this.id);
}
return true;
},
toString: function() {
function toStringInternal(node, indent) {
if (!node)
return '';
var output =
new Array(indent).join(' ') +
AutomationNodeImpl.prototype.toString.call(node) +
'\n';
indent += 2;
for (var i = 0; i < node.children.length; i++)
output += toStringInternal(node.children[i], indent);
return output;
}
return toStringInternal(this, 0);
},
invalidate_: function(node) {
if (!node)
return;
// Notify tree change observers of the removed node.
var observers = automationUtil.treeChangeObservers;
if (observers.length > 0) {
var treeChange = {target: node, type: schema.TreeChangeType.nodeRemoved};
for (var i = 0; i < observers.length; i++) {
try {
observers[i](treeChange);
} catch (e) {
console.error('Error in tree change observer for ' + treeChange.type +
': ' + e.message + '\nStack trace: ' + e.stack);
}
}
}
var children = node.children;
for (var i = 0, child; child = children[i]; i++) {
// Do not invalidate into subrooted nodes.
// TODO(dtseng): Revisit logic once out of proc iframes land.
if (child.root != node.root)
continue;
this.invalidate_(child);
}
// Retrieve the internal AutomationNodeImpl instance for this node.
// This object is not accessible outside of bindings code, but we can access
// it here.
var nodeImpl = privates(node).impl;
var id = nodeImpl.id;
for (var key in AutomationAttributeDefaults) {
nodeImpl[key] = AutomationAttributeDefaults[key];
}
nodeImpl.attributesInternal = {};
for (var key in DefaultMixinAttributes) {
var mixinAttribute = DefaultMixinAttributes[key];
if (!mixinAttribute.isRef)
nodeImpl.attributesInternal[key] = mixinAttribute.default;
}
nodeImpl.childIds = [];
nodeImpl.id = id;
delete this.axNodeDataCache_[id];
},
deleteOldChildren_: function(node, newChildIds) {
// Create a set of child ids in |src| for fast lookup, and return false
// if a duplicate is found;
var newChildIdSet = {};
for (var i = 0; i < newChildIds.length; i++) {
var childId = newChildIds[i];
if (newChildIdSet[childId]) {
logging.WARNING('Node ' + privates(node).impl.id +
' has duplicate child id ' + childId);
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
return false;
}
newChildIdSet[newChildIds[i]] = true;
}
// Delete the old children.
var nodeImpl = privates(node).impl;
var oldChildIds = nodeImpl.childIds;
for (var i = 0; i < oldChildIds.length;) {
var oldId = oldChildIds[i];
if (!newChildIdSet[oldId]) {
this.invalidate_(this.axNodeDataCache_[oldId]);
oldChildIds.splice(i, 1);
} else {
i++;
}
}
nodeImpl.childIds = oldChildIds;
return true;
},
createNewChildren_: function(node, newChildIds, updateState) {
logging.CHECK(node);
var success = true;
for (var i = 0; i < newChildIds.length; i++) {
var childId = newChildIds[i];
var childNode = this.axNodeDataCache_[childId];
if (childNode) {
if (childNode.parent != node) {
var parentId = -1;
if (childNode.parent) {
var parentImpl = privates(childNode.parent).impl;
parentId = parentImpl.id;
}
// This is a serious error - nodes should never be reparented.
// If this case occurs, continue so this node isn't left in an
// inconsistent state, but return failure at the end.
logging.WARNING('Node ' + childId + ' reparented from ' +
parentId + ' to ' + privates(node).impl.id);
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
success = false;
continue;
}
} else {
childNode = new AutomationNode(this);
this.axNodeDataCache_[childId] = childNode;
privates(childNode).impl.id = childId;
updateState.pendingNodes[childId] = childNode;
updateState.newNodes[childId] = childNode;
}
privates(childNode).impl.indexInParent = i;
privates(childNode).impl.parentID = privates(node).impl.id;
}
return success;
},
setData_: function(node, nodeData) {
var nodeImpl = privates(node).impl;
// TODO(dtseng): Make into set listing all hosting node roles.
if (nodeData.role == schema.RoleType.webView ||
nodeData.role == schema.RoleType.embeddedObject) {
if (nodeImpl.childTreeID !== nodeData.intAttributes.childTreeId)
nodeImpl.pendingChildFrame = true;
if (nodeImpl.pendingChildFrame) {
nodeImpl.childTreeID = nodeData.intAttributes.childTreeId;
automationUtil.storeTreeCallback(nodeImpl.childTreeID, function(root) {
nodeImpl.pendingChildFrame = false;
nodeImpl.childTree = root;
privates(root).impl.hostTree = node;
if (root.attributes.docLoadingProgress == 1)
privates(root).impl.dispatchEvent(schema.EventType.loadComplete);
nodeImpl.dispatchEvent(schema.EventType.childrenChanged);
});
automationInternal.enableFrame(nodeImpl.childTreeID);
}
}
for (var key in AutomationAttributeDefaults) {
if (key in nodeData)
nodeImpl[key] = nodeData[key];
else
nodeImpl[key] = AutomationAttributeDefaults[key];
}
// Set all basic attributes.
this.mixinAttributes_(nodeImpl, DefaultMixinAttributes, nodeData);
// If this is a rootWebArea or webArea, set document attributes.
if (nodeData.role == schema.RoleType.rootWebArea ||
nodeData.role == schema.RoleType.webArea) {
this.mixinAttributes_(nodeImpl, DocumentMixinAttributes, nodeData);
}
// If this is a scrollable area, set scrollable attributes.
for (var scrollAttr in ScrollableMixinAttributes) {
var spec = ScrollableMixinAttributes[scrollAttr];
if (this.findAttribute_(scrollAttr, spec, nodeData) !== undefined) {
this.mixinAttributes_(nodeImpl, ScrollableMixinAttributes, nodeData);
break;
}
}
// If this is inside a live region, set live region mixins.
var attr = 'containerLiveStatus';
var spec = LiveRegionMixinAttributes[attr];
if (this.findAttribute_(attr, spec, nodeData) !== undefined) {
this.mixinAttributes_(nodeImpl, LiveRegionMixinAttributes, nodeData);
}
// If this is a link, set link attributes
if (nodeData.role == 'link') {
this.mixinAttributes_(nodeImpl, LinkMixinAttributes, nodeData);
}
// If this is an editable text area, set editable text attributes.
if (nodeData.role == schema.RoleType.textField ||
nodeData.role == schema.RoleType.spinButton) {
this.mixinAttributes_(nodeImpl, EditableTextMixinAttributes, nodeData);
}
// If this is a range type, set range attributes.
if (nodeData.role == schema.RoleType.progressIndicator ||
nodeData.role == schema.RoleType.scrollBar ||
nodeData.role == schema.RoleType.slider ||
nodeData.role == schema.RoleType.spinButton) {
this.mixinAttributes_(nodeImpl, RangeMixinAttributes, nodeData);
}
// If this is a table, set table attributes.
if (nodeData.role == schema.RoleType.table) {
this.mixinAttributes_(nodeImpl, TableMixinAttributes, nodeData);
}
// If this is a table cell, set table cell attributes.
if (nodeData.role == schema.RoleType.cell) {
this.mixinAttributes_(nodeImpl, TableCellMixinAttributes, nodeData);
}
// If this has an active descendant, expose it.
if ('intAttributes' in nodeData &&
'activedescendantId' in nodeData.intAttributes) {
this.mixinAttributes_(nodeImpl, ActiveDescendantMixinAttribute, nodeData);
}
for (var i = 0; i < AutomationAttributeTypes.length; i++) {
var attributeType = AutomationAttributeTypes[i];
for (var attributeName in nodeData[attributeType]) {
nodeImpl.attributesInternal[attributeName] =
nodeData[attributeType][attributeName];
if (ATTRIBUTE_BLACKLIST.hasOwnProperty(attributeName) ||
nodeImpl.attributes.hasOwnProperty(attributeName)) {
continue;
} else if (
ATTRIBUTE_NAME_TO_ID_ATTRIBUTE.hasOwnProperty(attributeName)) {
this.defineReadonlyAttribute_(nodeImpl,
nodeImpl.attributes,
attributeName,
true);
} else {
this.defineReadonlyAttribute_(nodeImpl,
nodeImpl.attributes,
attributeName);
}
}
}
},
mixinAttributes_: function(nodeImpl, attributes, nodeData) {
for (var attribute in attributes) {
var spec = attributes[attribute];
if (spec.isRef)
this.mixinRelationshipAttribute_(nodeImpl, attribute, spec, nodeData);
else
this.mixinAttribute_(nodeImpl, attribute, spec, nodeData);
}
},
mixinAttribute_: function(nodeImpl, attribute, spec, nodeData) {
var value = this.findAttribute_(attribute, spec, nodeData);
if (value === undefined)
value = spec.default;
nodeImpl.attributesInternal[attribute] = value;
this.defineReadonlyAttribute_(nodeImpl, nodeImpl, attribute);
},
mixinRelationshipAttribute_: function(nodeImpl, attribute, spec, nodeData) {
var idAttribute = spec.idAttribute;
var idValue = spec.default;
if (spec.idFrom in nodeData) {
idValue = idAttribute in nodeData[spec.idFrom]
? nodeData[spec.idFrom][idAttribute] : idValue;
}
// Ok to define a list attribute with an empty list, but not a
// single ref with a null ID.
if (idValue === null)
return;
nodeImpl.attributesInternal[idAttribute] = idValue;
this.defineReadonlyAttribute_(
nodeImpl, nodeImpl, attribute, true, idAttribute);
},
findAttribute_: function(attribute, spec, nodeData) {
if (!('reflectFrom' in spec))
return;
var attributeGroup = spec.reflectFrom;
if (!(attributeGroup in nodeData))
return;
return nodeData[attributeGroup][attribute];
},
defineReadonlyAttribute_: function(
node, object, attributeName, opt_isIDRef, opt_idAttribute) {
if (attributeName in object)
return;
if (opt_isIDRef) {
$Object.defineProperty(object, attributeName, {
enumerable: true,
get: function() {
var idAttribute = opt_idAttribute ||
ATTRIBUTE_NAME_TO_ID_ATTRIBUTE[attributeName];
var idValue = node.attributesInternal[idAttribute];
if (Array.isArray(idValue)) {
return idValue.map(function(current) {
return node.rootImpl.get(current);
}, this);
}
return node.rootImpl.get(idValue);
}.bind(this),
});
} else {
$Object.defineProperty(object, attributeName, {
enumerable: true,
get: function() {
return node.attributesInternal[attributeName];
}.bind(this),
});
}
if (object instanceof AutomationNodeImpl) {
// Also expose attribute publicly on the wrapper.
$Object.defineProperty(object.wrapper, attributeName, {
enumerable: true,
get: function() {
return object[attributeName];
},
});
}
},
updateNode_: function(nodeData, updateState) {
var node = this.axNodeDataCache_[nodeData.id];
var didUpdateRoot = false;
if (node) {
delete updateState.pendingNodes[privates(node).impl.id];
} else {
if (nodeData.role != schema.RoleType.rootWebArea &&
nodeData.role != schema.RoleType.desktop) {
logging.WARNING(String(nodeData.id) +
' is not in the cache and not the new root.');
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
return false;
}
// |this| is an AutomationRootNodeImpl; retrieve the
// AutomationRootNode instance instead.
node = this.wrapper;
didUpdateRoot = true;
updateState.newNodes[this.id] = this.wrapper;
}
this.setData_(node, nodeData);
// TODO(aboxhall): send onChanged event?
logging.CHECK(node);
if (!this.deleteOldChildren_(node, nodeData.childIds)) {
if (didUpdateRoot) {
this.invalidate_(this.wrapper);
}
return false;
}
var nodeImpl = privates(node).impl;
var success = this.createNewChildren_(node,
nodeData.childIds,
updateState);
nodeImpl.childIds = nodeData.childIds;
this.axNodeDataCache_[nodeImpl.id] = node;
return success;
}
};
var AutomationNode = utils.expose('AutomationNode',
AutomationNodeImpl,
{ functions: ['doDefault',
'find',
'findAll',
'focus',
'makeVisible',
'matches',
'setSelection',
'showContextMenu',
'addEventListener',
'removeEventListener',
'domQuerySelector',
'toString' ],
readonly: ['parent',
'firstChild',
'lastChild',
'children',
'previousSibling',
'nextSibling',
'isRootNode',
'role',
'state',
'location',
'attributes',
'indexInParent',
'root'] });
var AutomationRootNode = utils.expose('AutomationRootNode',
AutomationRootNodeImpl,
{ superclass: AutomationNode });
exports.AutomationNode = AutomationNode;
exports.AutomationRootNode = AutomationRootNode;
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the browserAction API.
var binding = require('binding').Binding.create('browserAction');
var setIcon = require('setIcon').setIcon;
var getExtensionViews = requireNative('runtime').GetExtensionViews;
var sendRequest = require('sendRequest').sendRequest;
var lastError = require('lastError');
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequest('setIcon', function(details, callback) {
setIcon(details, function(args) {
sendRequest(this.name, [args, callback], this.definition.parameters);
}.bind(this));
});
apiFunctions.setCustomCallback('openPopup',
function(name, request, callback, response) {
if (!callback)
return;
if (lastError.hasError(chrome)) {
callback();
} else {
var views = getExtensionViews(-1, 'POPUP');
callback(views.length > 0 ? views[0] : null);
}
});
});
exports.binding = binding.generate();
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the Cast Streaming RtpStream API.
var binding = require('binding').Binding.create('cast.streaming.rtpStream');
var natives = requireNative('cast_streaming_natives');
binding.registerCustomHook(function(bindingsAPI, extensionId) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequest('destroy',
function(transportId) {
natives.DestroyCastRtpStream(transportId);
});
apiFunctions.setHandleRequest('getSupportedParams',
function(transportId) {
return natives.GetSupportedParamsCastRtpStream(transportId);
});
apiFunctions.setHandleRequest('start',
function(transportId, params) {
natives.StartCastRtpStream(transportId, params);
});
apiFunctions.setHandleRequest('stop',
function(transportId) {
natives.StopCastRtpStream(transportId);
});
apiFunctions.setHandleRequest('toggleLogging',
function(transportId, enable) {
natives.ToggleLogging(transportId, enable);
});
apiFunctions.setHandleRequest('getRawEvents',
function(transportId, extraData, callback) {
natives.GetRawEvents(transportId, extraData, callback);
});
apiFunctions.setHandleRequest('getStats',
function(transportId, callback) {
natives.GetStats(transportId, callback);
});
});
exports.binding = binding.generate();
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the Cast Streaming Session API.
var binding = require('binding').Binding.create('cast.streaming.session');
var natives = requireNative('cast_streaming_natives');
binding.registerCustomHook(function(bindingsAPI, extensionId) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequest('create',
function(audioTrack, videoTrack, callback) {
natives.CreateSession(audioTrack, videoTrack, callback);
});
});
exports.binding = binding.generate();
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the Cast Streaming UdpTransport API.
var binding = require('binding').Binding.create('cast.streaming.udpTransport');
var natives = requireNative('cast_streaming_natives');
binding.registerCustomHook(function(bindingsAPI, extensionId) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequest('destroy', function(transportId) {
natives.DestroyCastUdpTransport(transportId);
});
apiFunctions.setHandleRequest('setDestination',
function(transportId, destination) {
natives.SetDestinationCastUdpTransport(transportId, destination);
});
apiFunctions.setHandleRequest('setOptions',
function(transportId, options) {
natives.SetOptionsCastUdpTransport(transportId, options);
});
});
exports.binding = binding.generate();
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the Cast Streaming Session API.
var binding = require('binding').Binding.create(
'cast.streaming.receiverSession');
var natives = requireNative('cast_streaming_natives');
binding.registerCustomHook(function(bindingsAPI, extensionId) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequest('createAndBind',
function(ap, vp, local, weidgth, height, fr, url, cb, op) {
natives.StartCastRtpReceiver(
ap, vp, local, weidgth, height, fr, url, cb, op);
});
});
exports.binding = binding.generate();
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var Event = require('event_bindings').Event;
var sendRequest = require('sendRequest').sendRequest;
var validate = require('schemaUtils').validate;
function extendSchema(schema) {
var extendedSchema = $Array.slice(schema);
extendedSchema.unshift({'type': 'string'});
return extendedSchema;
}
function ChromeDirectSetting(prefKey, valueSchema) {
this.get = function(details, callback) {
var getSchema = this.functionSchemas.get.definition.parameters;
validate([details, callback], getSchema);
return sendRequest('types.private.ChromeDirectSetting.get',
[prefKey, details, callback],
extendSchema(getSchema));
};
this.set = function(details, callback) {
var setSchema = $Array.slice(
this.functionSchemas.set.definition.parameters);
setSchema[0].properties.value = valueSchema;
validate([details, callback], setSchema);
return sendRequest('types.private.ChromeDirectSetting.set',
[prefKey, details, callback],
extendSchema(setSchema));
};
this.clear = function(details, callback) {
var clearSchema = this.functionSchemas.clear.definition.parameters;
validate([details, callback], clearSchema);
return sendRequest('types.private.ChromeDirectSetting.clear',
[prefKey, details, callback],
extendSchema(clearSchema));
};
this.onChange = new Event('types.private.ChromeDirectSetting.' +
prefKey +
'.onChange');
};
exports.ChromeDirectSetting = ChromeDirectSetting;
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var Event = require('event_bindings').Event;
var sendRequest = require('sendRequest').sendRequest;
var validate = require('schemaUtils').validate;
function extendSchema(schema) {
var extendedSchema = $Array.slice(schema);
extendedSchema.unshift({'type': 'string'});
return extendedSchema;
}
function ChromeSetting(prefKey, valueSchema) {
this.get = function(details, callback) {
var getSchema = this.functionSchemas.get.definition.parameters;
validate([details, callback], getSchema);
return sendRequest('types.ChromeSetting.get',
[prefKey, details, callback],
extendSchema(getSchema));
};
this.set = function(details, callback) {
var setSchema = $Array.slice(
this.functionSchemas.set.definition.parameters);
setSchema[0].properties.value = valueSchema;
validate([details, callback], setSchema);
return sendRequest('types.ChromeSetting.set',
[prefKey, details, callback],
extendSchema(setSchema));
};
this.clear = function(details, callback) {
var clearSchema = this.functionSchemas.clear.definition.parameters;
validate([details, callback], clearSchema);
return sendRequest('types.ChromeSetting.clear',
[prefKey, details, callback],
extendSchema(clearSchema));
};
this.onChange = new Event('types.ChromeSetting.' + prefKey + '.onChange');
};
exports.ChromeSetting = ChromeSetting;
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var binding = require('binding').Binding.create('chromeWebViewInternal');
var contextMenusHandlers = require('contextMenusHandlers');
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
var handlers = contextMenusHandlers.create(true /* isWebview */);
apiFunctions.setHandleRequest('contextMenusCreate',
handlers.requestHandlers.create);
apiFunctions.setCustomCallback('contextMenusCreate',
handlers.callbacks.create);
apiFunctions.setCustomCallback('contextMenusUpdate',
handlers.callbacks.update);
apiFunctions.setCustomCallback('contextMenusRemove',
handlers.callbacks.remove);
apiFunctions.setCustomCallback('contextMenusRemoveAll',
handlers.callbacks.removeAll);
});
exports.ChromeWebView = binding.generate();
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This module implements chrome-specific <webview> API.
// See web_view_api_methods.js for details.
var ChromeWebView = require('chromeWebViewInternal').ChromeWebView;
var ChromeWebViewSchema =
requireNative('schema_registry').GetSchema('chromeWebViewInternal');
var CreateEvent = require('guestViewEvents').CreateEvent;
var EventBindings = require('event_bindings');
var GuestViewInternalNatives = requireNative('guest_view_internal');
var idGeneratorNatives = requireNative('id_generator');
var Utils = require('utils');
var WebViewImpl = require('webView').WebViewImpl;
// This is the only "webViewInternal.onClicked" named event for this renderer.
//
// Since we need an event per <webview>, we define events with suffix
// (subEventName) in each of the <webview>. Behind the scenes, this event is
// registered as a ContextMenusEvent, with filter set to the webview's
// |viewInstanceId|. Any time a ContextMenusEvent is dispatched, we re-dispatch
// it to the subEvent's listeners. This way
// <webview>.contextMenus.onClicked behave as a regular chrome Event type.
var ContextMenusEvent = CreateEvent('chromeWebViewInternal.onClicked');
// See comment above.
var ContextMenusHandlerEvent =
CreateEvent('chromeWebViewInternal.onContextMenuShow');
// -----------------------------------------------------------------------------
// ContextMenusOnClickedEvent object.
// This event is exposed as <webview>.contextMenus.onClicked.
function ContextMenusOnClickedEvent(webViewInstanceId,
opt_eventName,
opt_argSchemas,
opt_eventOptions) {
var subEventName = GetUniqueSubEventName(opt_eventName);
EventBindings.Event.call(this,
subEventName,
opt_argSchemas,
opt_eventOptions,
webViewInstanceId);
var view = GuestViewInternalNatives.GetViewFromID(webViewInstanceId);
if (!view) {
return;
}
view.events.addScopedListener(ContextMenusEvent, function() {
// Re-dispatch to subEvent's listeners.
$Function.apply(this.dispatch, this, $Array.slice(arguments));
}.bind(this), {instanceId: webViewInstanceId});
}
ContextMenusOnClickedEvent.prototype.__proto__ = EventBindings.Event.prototype;
function ContextMenusOnContextMenuEvent(webViewInstanceId,
opt_eventName,
opt_argSchemas,
opt_eventOptions) {
var subEventName = GetUniqueSubEventName(opt_eventName);
EventBindings.Event.call(this,
subEventName,
opt_argSchemas,
opt_eventOptions,
webViewInstanceId);
var view = GuestViewInternalNatives.GetViewFromID(webViewInstanceId);
if (!view) {
return;
}
view.events.addScopedListener(ContextMenusHandlerEvent, function(e) {
var defaultPrevented = false;
var event = {
'preventDefault': function() { defaultPrevented = true; }
};
// Re-dispatch to subEvent's listeners.
$Function.apply(this.dispatch, this, [event]);
if (!defaultPrevented) {
// TODO(lazyboy): Remove |items| parameter completely from
// ChromeWebView.showContextMenu as we don't do anything useful with it
// currently.
var items = [];
var guestInstanceId = GuestViewInternalNatives.
GetViewFromID(webViewInstanceId).guest.getId();
ChromeWebView.showContextMenu(guestInstanceId, e.requestId, items);
}
}.bind(this), {instanceId: webViewInstanceId});
}
ContextMenusOnContextMenuEvent.prototype.__proto__ =
EventBindings.Event.prototype;
// -----------------------------------------------------------------------------
// WebViewContextMenusImpl object.
// An instance of this class is exposed as <webview>.contextMenus.
function WebViewContextMenusImpl(viewInstanceId) {
this.viewInstanceId_ = viewInstanceId;
}
WebViewContextMenusImpl.prototype.create = function() {
var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments));
return $Function.apply(ChromeWebView.contextMenusCreate, null, args);
};
WebViewContextMenusImpl.prototype.remove = function() {
var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments));
return $Function.apply(ChromeWebView.contextMenusRemove, null, args);
};
WebViewContextMenusImpl.prototype.removeAll = function() {
var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments));
return $Function.apply(ChromeWebView.contextMenusRemoveAll, null, args);
};
WebViewContextMenusImpl.prototype.update = function() {
var args = $Array.concat([this.viewInstanceId_], $Array.slice(arguments));
return $Function.apply(ChromeWebView.contextMenusUpdate, null, args);
};
var WebViewContextMenus = Utils.expose(
'WebViewContextMenus', WebViewContextMenusImpl,
{ functions: ['create', 'remove', 'removeAll', 'update'] });
// -----------------------------------------------------------------------------
WebViewImpl.prototype.maybeSetupContextMenus = function() {
if (!this.contextMenusOnContextMenuEvent_) {
var eventName = 'chromeWebViewInternal.onContextMenuShow';
var eventSchema =
Utils.lookup(ChromeWebViewSchema.events, 'name', 'onShow');
var eventOptions = {supportsListeners: true};
this.contextMenusOnContextMenuEvent_ = new ContextMenusOnContextMenuEvent(
this.viewInstanceId, eventName, eventSchema, eventOptions);
}
var createContextMenus = function() {
return this.weakWrapper(function() {
if (this.contextMenus_) {
return this.contextMenus_;
}
this.contextMenus_ = new WebViewContextMenus(this.viewInstanceId);
// Define 'onClicked' event property on |this.contextMenus_|.
var getOnClickedEvent = function() {
return this.weakWrapper(function() {
if (!this.contextMenusOnClickedEvent_) {
var eventName = 'chromeWebViewInternal.onClicked';
var eventSchema =
Utils.lookup(ChromeWebViewSchema.events, 'name', 'onClicked');
var eventOptions = {supportsListeners: true};
var onClickedEvent = new ContextMenusOnClickedEvent(
this.viewInstanceId, eventName, eventSchema, eventOptions);
this.contextMenusOnClickedEvent_ = onClickedEvent;
return onClickedEvent;
}
return this.contextMenusOnClickedEvent_;
});
}.bind(this);
$Object.defineProperty(
this.contextMenus_,
'onClicked',
{get: getOnClickedEvent(), enumerable: true});
$Object.defineProperty(
this.contextMenus_,
'onShow',
{
get: this.weakWrapper(function() {
return this.contextMenusOnContextMenuEvent_;
}),
enumerable: true
});
return this.contextMenus_;
});
}.bind(this);
// Expose <webview>.contextMenus object.
// TODO(lazyboy): Add documentation for contextMenus:
// http://crbug.com/470979.
$Object.defineProperty(
this.element,
'contextMenus',
{
get: createContextMenus(),
enumerable: true
});
};
function GetUniqueSubEventName(eventName) {
return eventName + '/' + idGeneratorNatives.GetNextId();
}
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the contentSettings API.
var sendRequest = require('sendRequest').sendRequest;
var validate = require('schemaUtils').validate;
function extendSchema(schema) {
var extendedSchema = $Array.slice(schema);
extendedSchema.unshift({'type': 'string'});
return extendedSchema;
}
function ContentSetting(contentType, settingSchema) {
this.get = function(details, callback) {
var getSchema = this.functionSchemas.get.definition.parameters;
validate([details, callback], getSchema);
return sendRequest('contentSettings.get',
[contentType, details, callback],
extendSchema(getSchema));
};
this.set = function(details, callback) {
var setSchema = $Array.slice(
this.functionSchemas.set.definition.parameters);
setSchema[0].properties.setting = settingSchema;
validate([details, callback], setSchema);
return sendRequest('contentSettings.set',
[contentType, details, callback],
extendSchema(setSchema));
};
this.clear = function(details, callback) {
var clearSchema = this.functionSchemas.clear.definition.parameters;
validate([details, callback], clearSchema);
return sendRequest('contentSettings.clear',
[contentType, details, callback],
extendSchema(clearSchema));
};
this.getResourceIdentifiers = function(callback) {
var schema =
this.functionSchemas.getResourceIdentifiers.definition.parameters;
validate([callback], schema);
return sendRequest(
'contentSettings.getResourceIdentifiers',
[contentType, callback],
extendSchema(schema));
};
}
exports.ContentSetting = ContentSetting;
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the declarativeContent API.
var binding = require('binding').Binding.create('declarativeContent');
var utils = require('utils');
var validate = require('schemaUtils').validate;
var canonicalizeCompoundSelector =
requireNative('css_natives').CanonicalizeCompoundSelector;
var setIcon = require('setIcon').setIcon;
binding.registerCustomHook( function(api) {
var declarativeContent = api.compiledApi;
// Returns the schema definition of type |typeId| defined in |namespace|.
function getSchema(typeId) {
return utils.lookup(api.schema.types,
'id',
'declarativeContent.' + typeId);
}
// Helper function for the constructor of concrete datatypes of the
// declarative content API.
// Makes sure that |this| contains the union of parameters and
// {'instanceType': 'declarativeContent.' + typeId} and validates the
// generated union dictionary against the schema for |typeId|.
function setupInstance(instance, parameters, typeId) {
for (var key in parameters) {
if ($Object.hasOwnProperty(parameters, key)) {
instance[key] = parameters[key];
}
}
instance.instanceType = 'declarativeContent.' + typeId;
var schema = getSchema(typeId);
validate([instance], [schema]);
}
function canonicalizeCssSelectors(selectors) {
for (var i = 0; i < selectors.length; i++) {
var canonicalizedSelector = canonicalizeCompoundSelector(selectors[i]);
if (canonicalizedSelector == '') {
throw new Error(
'Element of \'css\' array must be a ' +
'list of valid compound selectors: ' +
selectors[i]);
}
selectors[i] = canonicalizedSelector;
}
}
// Setup all data types for the declarative content API.
declarativeContent.PageStateMatcher = function(parameters) {
setupInstance(this, parameters, 'PageStateMatcher');
if ($Object.hasOwnProperty(this, 'css')) {
canonicalizeCssSelectors(this.css);
}
};
declarativeContent.ShowPageAction = function(parameters) {
setupInstance(this, parameters, 'ShowPageAction');
};
declarativeContent.RequestContentScript = function(parameters) {
setupInstance(this, parameters, 'RequestContentScript');
};
// TODO(rockot): Do not expose this in M39 stable. Making this restriction
// possible will take some extra work. See http://crbug.com/415315
declarativeContent.SetIcon = function(parameters) {
setIcon(parameters, function (data) {
setupInstance(this, data, 'SetIcon');
}.bind(this));
};
});
exports.binding = binding.generate();
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the desktopCapture API.
var binding = require('binding').Binding.create('desktopCapture');
var sendRequest = require('sendRequest').sendRequest;
var idGenerator = requireNative('id_generator');
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
var pendingRequests = {};
function onRequestResult(id, result) {
if (id in pendingRequests) {
var callback = pendingRequests[id];
delete pendingRequests[id];
callback(result);
}
}
apiFunctions.setHandleRequest('chooseDesktopMedia',
function(sources, target_tab, callback) {
// |target_tab| is an optional parameter.
if (callback === undefined) {
callback = target_tab;
target_tab = undefined;
}
var id = idGenerator.GetNextId();
pendingRequests[id] = callback;
sendRequest(this.name,
[id, sources, target_tab, onRequestResult.bind(null, id)],
this.definition.parameters, {});
return id;
});
apiFunctions.setHandleRequest('cancelChooseDesktopMedia', function(id) {
if (id in pendingRequests) {
delete pendingRequests[id];
sendRequest(this.name, [id], this.definition.parameters, {});
}
});
});
exports.binding = binding.generate();
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the developerPrivate API.
var binding = require('binding').Binding.create('developerPrivate');
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
// Converts the argument of |functionName| from DirectoryEntry to URL.
function bindFileSystemFunction(functionName) {
apiFunctions.setUpdateArgumentsPostValidate(
functionName, function(directoryEntry, callback) {
var fileSystemName = directoryEntry.filesystem.name;
var relativePath = $String.slice(directoryEntry.fullPath, 1);
var url = directoryEntry.toURL();
return [fileSystemName, relativePath, url, callback];
});
}
bindFileSystemFunction('loadDirectory');
// developerPrivate.enable is the same as chrome.management.setEnabled.
// TODO(devlin): Migrate callers off developerPrivate.enable.
bindingsAPI.compiledApi.enable = chrome.management.setEnabled;
apiFunctions.setHandleRequest('allowFileAccess',
function(id, allow, callback) {
chrome.developerPrivate.updateExtensionConfiguration(
{extensionId: id, fileAccess: allow}, callback);
});
apiFunctions.setHandleRequest('allowIncognito',
function(id, allow, callback) {
chrome.developerPrivate.updateExtensionConfiguration(
{extensionId: id, incognitoAccess: allow}, callback);
});
apiFunctions.setHandleRequest('inspect', function(options, callback) {
var renderViewId = options.render_view_id;
if (typeof renderViewId == 'string') {
renderViewId = parseInt(renderViewId);
if (isNaN(renderViewId))
throw new Error('Invalid value for render_view_id');
}
var renderProcessId = options.render_process_id;
if (typeof renderProcessId == 'string') {
renderProcessId = parseInt(renderProcessId);
if (isNaN(renderProcessId))
throw new Error('Invalid value for render_process_id');
}
chrome.developerPrivate.openDevTools({
extensionId: options.extension_id,
renderProcessId: renderProcessId,
renderViewId: renderViewId,
incognito: options.incognito
}, callback);
});
});
exports.binding = binding.generate();
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom bindings for the downloads API.
var binding = require('binding').Binding.create('downloads');
var downloadsInternal = require('binding').Binding.create(
'downloadsInternal').generate();
var eventBindings = require('event_bindings');
eventBindings.registerArgumentMassager(
'downloads.onDeterminingFilename',
function massage_determining_filename(args, dispatch) {
var downloadItem = args[0];
// Copy the id so that extensions can't change it.
var downloadId = downloadItem.id;
var suggestable = true;
function isValidResult(result) {
if (result === undefined)
return false;
if (typeof(result) != 'object') {
console.error('Error: Invocation of form suggest(' + typeof(result) +
') doesn\'t match definition suggest({filename: string, ' +
'conflictAction: string})');
return false;
} else if ((typeof(result.filename) != 'string') ||
(result.filename.length == 0)) {
console.error('Error: "filename" parameter to suggest() must be a ' +
'non-empty string');
return false;
} else if ([undefined, 'uniquify', 'overwrite', 'prompt'].indexOf(
result.conflictAction) < 0) {
console.error('Error: "conflictAction" parameter to suggest() must be ' +
'one of undefined, "uniquify", "overwrite", "prompt"');
return false;
}
return true;
}
function suggestCallback(result) {
if (!suggestable) {
console.error('suggestCallback may not be called more than once.');
return;
}
suggestable = false;
if (isValidResult(result)) {
downloadsInternal.determineFilename(
downloadId, result.filename, result.conflictAction || "");
} else {
downloadsInternal.determineFilename(downloadId, "", "");
}
}
try {
var results = dispatch([downloadItem, suggestCallback]);
var async = (results &&
results.results &&
(results.results.length != 0) &&
(results.results[0] === true));
if (suggestable && !async)
suggestCallback();
} catch (e) {
suggestCallback();
throw e;
}
});
exports.binding = binding.generate();
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the enterprise.platformKeys API.
// The platformKeys API consists of two major parts:
// - the certificate management.
// - the key generation and crypto operations and
// The former is implemented without custom binding as static functions.
// The latter is exposed by implementing WebCrypto's SubtleCrypto interface.
// The internal API provides the key and crypto operations through static
// functions expecting token IDs and this custom binding adds the SubtleCrypto
// wrapper.
// The Token object holds the token id and the SubtleCrypto member.
var binding = require('binding').Binding.create('enterprise.platformKeys');
var Token = require('enterprise.platformKeys.Token').Token;
var internalAPI = require('enterprise.platformKeys.internalAPI');
binding.registerCustomHook(function(api) {
var apiFunctions = api.apiFunctions;
var ret = apiFunctions.setHandleRequest('getTokens', function(callback) {
internalAPI.getTokens(function(tokenIds) {
callback($Array.map(tokenIds,
function(tokenId) { return new Token(tokenId); }));
});
});
});
exports.binding = binding.generate();
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var binding = require('binding')
.Binding.create('enterprise.platformKeysInternal')
.generate();
exports.getTokens = binding.getTokens;
exports.generateKey = binding.generateKey;
exports.sign = binding.sign;
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var utils = require('utils');
var intersect = require('platformKeys.utils').intersect;
var keyModule = require('platformKeys.Key');
var Key = keyModule.Key;
var KeyType = keyModule.KeyType;
var KeyUsage = keyModule.KeyUsage;
/**
* Implementation of WebCrypto.KeyPair used in enterprise.platformKeys.
* @param {ArrayBuffer} publicKeySpki The Subject Public Key Info in DER
* encoding.
* @param {KeyAlgorithm} algorithm The algorithm identifier.
* @param {KeyUsage[]} usages The allowed key usages.
* @constructor
*/
var KeyPairImpl = function(publicKeySpki, algorithm, usages) {
this.publicKey = new Key(KeyType.public,
publicKeySpki,
algorithm,
intersect([KeyUsage.verify], usages),
true /* extractable */);
this.privateKey = new Key(KeyType.private,
publicKeySpki,
algorithm,
intersect([KeyUsage.sign], usages),
false /* not extractable */);
};
exports.KeyPair = utils.expose('KeyPair',
KeyPairImpl,
{readonly:['publicKey', 'privateKey']});
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var utils = require('utils');
var internalAPI = require('enterprise.platformKeys.internalAPI');
var intersect = require('platformKeys.utils').intersect;
var subtleCryptoModule = require('platformKeys.SubtleCrypto');
var SubtleCrypto = subtleCryptoModule.SubtleCrypto;
var SubtleCryptoImpl = subtleCryptoModule.SubtleCryptoImpl;
var KeyPair = require('enterprise.platformKeys.KeyPair').KeyPair;
var KeyUsage = require('platformKeys.Key').KeyUsage;
var normalizeAlgorithm =
requireNative('platform_keys_natives').NormalizeAlgorithm;
// This error is thrown by the internal and public API's token functions and
// must be rethrown by this custom binding. Keep this in sync with the C++ part
// of this API.
var errorInvalidToken = "The token is not valid.";
// The following errors are specified in WebCrypto.
// TODO(pneubeck): These should be DOMExceptions.
function CreateNotSupportedError() {
return new Error('The algorithm is not supported');
}
function CreateInvalidAccessError() {
return new Error('The requested operation is not valid for the provided key');
}
function CreateDataError() {
return new Error('Data provided to an operation does not meet requirements');
}
function CreateSyntaxError() {
return new Error('A required parameter was missing or out-of-range');
}
function CreateOperationError() {
return new Error('The operation failed for an operation-specific reason');
}
// Catches an |internalErrorInvalidToken|. If so, forwards it to |reject| and
// returns true.
function catchInvalidTokenError(reject) {
if (chrome.runtime.lastError &&
chrome.runtime.lastError.message == errorInvalidToken) {
reject(chrome.runtime.lastError);
return true;
}
return false;
}
// Returns true if |array| is a BigInteger describing the standard public
// exponent 65537. In particular, it ignores leading zeros as required by the
// BigInteger definition in WebCrypto.
function equalsStandardPublicExponent(array) {
var expected = [0x01, 0x00, 0x01];
if (array.length < expected.length)
return false;
for (var i = 0; i < array.length; i++) {
var expectedDigit = 0;
if (i < expected.length) {
// |expected| is symmetric, endianness doesn't matter.
expectedDigit = expected[i];
}
if (array[array.length - 1 - i] !== expectedDigit)
return false;
}
return true;
}
/**
* Implementation of WebCrypto.SubtleCrypto used in enterprise.platformKeys.
* Derived from platformKeys.SubtleCrypto.
* @param {string} tokenId The id of the backing Token.
* @constructor
*/
var EnterpriseSubtleCryptoImpl = function(tokenId) {
SubtleCryptoImpl.call(this, tokenId);
};
EnterpriseSubtleCryptoImpl.prototype =
Object.create(SubtleCryptoImpl.prototype);
EnterpriseSubtleCryptoImpl.prototype.generateKey =
function(algorithm, extractable, keyUsages) {
var subtleCrypto = this;
return new Promise(function(resolve, reject) {
// TODO(pneubeck): Apply the algorithm normalization of the WebCrypto
// implementation.
if (extractable) {
// Note: This deviates from WebCrypto.SubtleCrypto.
throw CreateNotSupportedError();
}
if (intersect(keyUsages, [KeyUsage.sign, KeyUsage.verify]).length !=
keyUsages.length) {
throw CreateDataError();
}
var normalizedAlgorithmParameters =
normalizeAlgorithm(algorithm, 'GenerateKey');
if (!normalizedAlgorithmParameters) {
// TODO(pneubeck): It's not clear from the WebCrypto spec which error to
// throw here.
throw CreateSyntaxError();
}
// normalizeAlgorithm returns an array, but publicExponent should be a
// Uint8Array.
normalizedAlgorithmParameters.publicExponent =
new Uint8Array(normalizedAlgorithmParameters.publicExponent);
if (normalizedAlgorithmParameters.name !== 'RSASSA-PKCS1-v1_5' ||
!equalsStandardPublicExponent(
normalizedAlgorithmParameters.publicExponent)) {
// Note: This deviates from WebCrypto.SubtleCrypto.
throw CreateNotSupportedError();
}
internalAPI.generateKey(subtleCrypto.tokenId,
normalizedAlgorithmParameters.modulusLength,
function(spki) {
if (catchInvalidTokenError(reject))
return;
if (chrome.runtime.lastError) {
reject(CreateOperationError());
return;
}
resolve(new KeyPair(spki, normalizedAlgorithmParameters, keyUsages));
});
});
};
exports.SubtleCrypto =
utils.expose('SubtleCrypto',
EnterpriseSubtleCryptoImpl,
{
superclass: SubtleCrypto,
functions: ['generateKey']
// ['sign', 'exportKey'] are exposed by the base class
});
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var utils = require('utils');
var SubtleCrypto = require('enterprise.platformKeys.SubtleCrypto').SubtleCrypto;
/**
* Implementation of enterprise.platformKeys.Token.
* @param {string} id The id of the new Token.
* @constructor
*/
var TokenImpl = function(id) {
this.id = id;
this.subtleCrypto = new SubtleCrypto(id);
};
exports.Token =
utils.expose('Token', TokenImpl, {readonly:['id', 'subtleCrypto']});
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom bindings for the feedbackPrivate API.
var binding = require('binding').Binding.create('feedbackPrivate');
var blobNatives = requireNative('blob_natives');
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setUpdateArgumentsPostValidate(
"sendFeedback", function(feedbackInfo, callback) {
var attachedFileBlobUuid = '';
var screenshotBlobUuid = '';
if (feedbackInfo.attachedFile)
attachedFileBlobUuid =
blobNatives.GetBlobUuid(feedbackInfo.attachedFile.data);
if (feedbackInfo.screenshot)
screenshotBlobUuid =
blobNatives.GetBlobUuid(feedbackInfo.screenshot);
feedbackInfo.attachedFileBlobUuid = attachedFileBlobUuid;
feedbackInfo.screenshotBlobUuid = screenshotBlobUuid;
return [feedbackInfo, callback];
});
});
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the fileBrowserHandler API.
var binding = require('binding').Binding.create('fileBrowserHandler');
var eventBindings = require('event_bindings');
var fileBrowserNatives = requireNative('file_browser_handler');
var GetExternalFileEntry = fileBrowserNatives.GetExternalFileEntry;
var fileBrowserHandlerInternal = require('binding').Binding.create(
'fileBrowserHandlerInternal').generate();
eventBindings.registerArgumentMassager('fileBrowserHandler.onExecute',
function(args, dispatch) {
if (args.length < 2) {
dispatch(args);
return;
}
var fileList = args[1].entries;
if (!fileList) {
dispatch(args);
return;
}
// The second parameter for this event's payload is file definition
// dictionary that we used to reconstruct File API's Entry instance
// here.
for (var i = 0; i < fileList.length; i++)
fileList[i] = GetExternalFileEntry(fileList[i]);
dispatch(args);
});
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequest('selectFile',
function(selectionParams, callback) {
function internalCallback(externalCallback, internalResult) {
if (!externalCallback)
return;
var result = undefined;
if (internalResult) {
result = { success: internalResult.success, entry: null };
if (internalResult.success)
result.entry = GetExternalFileEntry(internalResult.entry);
}
externalCallback(result);
}
return fileBrowserHandlerInternal.selectFile(
selectionParams, $Function.bind(internalCallback, null, callback));
});
});
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the fileManagerPrivate API.
// Bindings
var binding = require('binding').Binding.create('fileManagerPrivate');
var eventBindings = require('event_bindings');
// Natives
var fileManagerPrivateNatives = requireNative('file_manager_private');
var fileBrowserHandlerNatives = requireNative('file_browser_handler');
// Internals
var fileManagerPrivateInternal =
require('binding').Binding.create('fileManagerPrivateInternal').generate();
// Shorthands
var GetFileSystem = fileManagerPrivateNatives.GetFileSystem;
var GetExternalFileEntry = fileBrowserHandlerNatives.GetExternalFileEntry;
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setCustomCallback('searchDrive',
function(name, request, callback, response) {
if (response && !response.error && response.entries) {
response.entries = response.entries.map(function(entry) {
return GetExternalFileEntry(entry);
});
}
// So |callback| doesn't break if response is not defined.
if (!response)
response = {};
if (callback)
callback(response.entries, response.nextFeed);
});
apiFunctions.setCustomCallback('searchDriveMetadata',
function(name, request, callback, response) {
if (response && !response.error) {
for (var i = 0; i < response.length; i++) {
response[i].entry =
GetExternalFileEntry(response[i].entry);
}
}
// So |callback| doesn't break if response is not defined.
if (!response)
response = {};
if (callback)
callback(response);
});
apiFunctions.setHandleRequest('resolveIsolatedEntries',
function(entries, callback) {
var urls = entries.map(function(entry) {
return fileBrowserHandlerNatives.GetEntryURL(entry);
});
fileManagerPrivateInternal.resolveIsolatedEntries(urls, function(
entryDescriptions) {
callback(entryDescriptions.map(function(description) {
return GetExternalFileEntry(description);
}));
});
});
apiFunctions.setHandleRequest('getEntryProperties',
function(entries, names, callback) {
var urls = entries.map(function(entry) {
return fileBrowserHandlerNatives.GetEntryURL(entry);
});
fileManagerPrivateInternal.getEntryProperties(urls, names, callback);
});
apiFunctions.setHandleRequest('addFileWatch', function(entry, callback) {
var url = fileBrowserHandlerNatives.GetEntryURL(entry);
fileManagerPrivateInternal.addFileWatch(url, callback);
});
apiFunctions.setHandleRequest('removeFileWatch', function(entry, callback) {
var url = fileBrowserHandlerNatives.GetEntryURL(entry);
fileManagerPrivateInternal.removeFileWatch(url, callback);
});
});
eventBindings.registerArgumentMassager(
'fileManagerPrivate.onDirectoryChanged', function(args, dispatch) {
// Convert the entry arguments into a real Entry object.
args[0].entry = GetExternalFileEntry(args[0].entry);
dispatch(args);
});
exports.binding = binding.generate();
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var fileSystemNatives = requireNative('file_system_natives');
var GetIsolatedFileSystem = fileSystemNatives.GetIsolatedFileSystem;
var sendRequest = require('sendRequest');
var lastError = require('lastError');
var GetModuleSystem = requireNative('v8_context').GetModuleSystem;
// TODO(sammc): Don't require extension. See http://crbug.com/235689.
var GetExtensionViews = requireNative('runtime').GetExtensionViews;
// For a given |apiName|, generates object with two elements that are used
// in file system relayed APIs:
// * 'bindFileEntryCallback' function that provides mapping between JS objects
// into actual FileEntry|DirectoryEntry objects.
// * 'entryIdManager' object that implements methods for keeping the tracks of
// previously saved file entries.
function getFileBindingsForApi(apiName) {
// Fallback to using the current window if no background page is running.
var backgroundPage = GetExtensionViews(-1, 'BACKGROUND')[0] || window;
var backgroundPageModuleSystem = GetModuleSystem(backgroundPage);
// All windows use the bindFileEntryCallback from the background page so their
// FileEntry objects have the background page's context as their own. This
// allows them to be used from other windows (including the background page)
// after the original window is closed.
if (window == backgroundPage) {
var bindFileEntryCallback = function(functionName, apiFunctions) {
apiFunctions.setCustomCallback(functionName,
function(name, request, callback, response) {
if (callback) {
if (!response) {
callback();
return;
}
var entries = [];
var hasError = false;
var getEntryError = function(fileError) {
if (!hasError) {
hasError = true;
lastError.run(
apiName + '.' + functionName,
'Error getting fileEntry, code: ' + fileError.code,
request.stack,
callback);
}
}
// Loop through the response entries and asynchronously get the
// FileEntry for each. We use hasError to ensure that only the first
// error is reported. Note that an error can occur either during the
// loop or in the asynchronous error callback to getFile.
$Array.forEach(response.entries, function(entry) {
if (hasError)
return;
var fileSystemId = entry.fileSystemId;
var baseName = entry.baseName;
var id = entry.id;
var fs = GetIsolatedFileSystem(fileSystemId);
try {
var getEntryCallback = function(fileEntry) {
if (hasError)
return;
entryIdManager.registerEntry(id, fileEntry);
entries.push(fileEntry);
// Once all entries are ready, pass them to the callback. In the
// event of an error, this condition will never be satisfied so
// the callback will not be called with any entries.
if (entries.length == response.entries.length) {
if (response.multiple) {
sendRequest.safeCallbackApply(
apiName + '.' + functionName, request, callback,
[entries]);
} else {
sendRequest.safeCallbackApply(
apiName + '.' + functionName, request, callback,
[entries[0]]);
}
}
}
// TODO(koz): fs.root.getFile() makes a trip to the browser
// process, but it might be possible avoid that by calling
// WebDOMFileSystem::createV8Entry().
if (entry.isDirectory) {
fs.root.getDirectory(baseName, {}, getEntryCallback,
getEntryError);
} else {
fs.root.getFile(baseName, {}, getEntryCallback, getEntryError);
}
} catch (e) {
if (!hasError) {
hasError = true;
lastError.run(apiName + '.' + functionName,
'Error getting fileEntry: ' + e.stack,
request.stack,
callback);
}
}
});
}
});
};
var entryIdManager = require('entryIdManager');
} else {
// Force the fileSystem API to be loaded in the background page. Using
// backgroundPageModuleSystem.require('fileSystem') is insufficient as
// requireNative is only allowed while lazily loading an API.
backgroundPage.chrome.fileSystem;
var bindFileEntryCallback = backgroundPageModuleSystem.require(
apiName).bindFileEntryCallback;
var entryIdManager = backgroundPageModuleSystem.require('entryIdManager');
}
return {bindFileEntryCallback: bindFileEntryCallback,
entryIdManager: entryIdManager};
}
exports.getFileBindingsForApi = getFileBindingsForApi;
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the fileSystem API.
var binding = require('binding').Binding.create('fileSystem');
var sendRequest = require('sendRequest');
var getFileBindingsForApi =
require('fileEntryBindingUtil').getFileBindingsForApi;
var fileBindings = getFileBindingsForApi('fileSystem');
var bindFileEntryCallback = fileBindings.bindFileEntryCallback;
var entryIdManager = fileBindings.entryIdManager;
var fileSystemNatives = requireNative('file_system_natives');
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
var fileSystem = bindingsAPI.compiledApi;
function bindFileEntryFunction(functionName) {
apiFunctions.setUpdateArgumentsPostValidate(
functionName, function(fileEntry, callback) {
var fileSystemName = fileEntry.filesystem.name;
var relativePath = $String.slice(fileEntry.fullPath, 1);
return [fileSystemName, relativePath, callback];
});
}
$Array.forEach(['getDisplayPath', 'getWritableEntry', 'isWritableEntry'],
bindFileEntryFunction);
$Array.forEach(['getWritableEntry', 'chooseEntry', 'restoreEntry'],
function(functionName) {
bindFileEntryCallback(functionName, apiFunctions);
});
apiFunctions.setHandleRequest('retainEntry', function(fileEntry) {
var id = entryIdManager.getEntryId(fileEntry);
if (!id)
return '';
var fileSystemName = fileEntry.filesystem.name;
var relativePath = $String.slice(fileEntry.fullPath, 1);
sendRequest.sendRequest(this.name, [id, fileSystemName, relativePath],
this.definition.parameters, {});
return id;
});
apiFunctions.setHandleRequest('isRestorable',
function(id, callback) {
var savedEntry = entryIdManager.getEntryById(id);
if (savedEntry) {
sendRequest.safeCallbackApply(
'fileSystem.isRestorable',
{},
callback,
[true]);
} else {
sendRequest.sendRequest(
this.name, [id, callback], this.definition.parameters, {});
}
});
apiFunctions.setUpdateArgumentsPostValidate('restoreEntry',
function(id, callback) {
var savedEntry = entryIdManager.getEntryById(id);
if (savedEntry) {
// We already have a file entry for this id so pass it to the callback and
// send a request to the browser to move it to the back of the LRU.
sendRequest.safeCallbackApply(
'fileSystem.restoreEntry',
{},
callback,
[savedEntry]);
return [id, false, null];
} else {
// Ask the browser process for a new file entry for this id, to be passed
// to |callback|.
return [id, true, callback];
}
});
apiFunctions.setCustomCallback('requestFileSystem',
function(name, request, callback, response) {
var fileSystem;
if (response && response.file_system_id) {
fileSystem = fileSystemNatives.GetIsolatedFileSystem(
response.file_system_id, response.file_system_path);
}
sendRequest.safeCallbackApply(
'fileSystem.requestFileSystem',
request,
callback,
[fileSystem]);
});
// TODO(benwells): Remove these deprecated versions of the functions.
fileSystem.getWritableFileEntry = function() {
console.log("chrome.fileSystem.getWritableFileEntry is deprecated");
console.log("Please use chrome.fileSystem.getWritableEntry instead");
$Function.apply(fileSystem.getWritableEntry, this, arguments);
};
fileSystem.isWritableFileEntry = function() {
console.log("chrome.fileSystem.isWritableFileEntry is deprecated");
console.log("Please use chrome.fileSystem.isWritableEntry instead");
$Function.apply(fileSystem.isWritableEntry, this, arguments);
};
fileSystem.chooseFile = function() {
console.log("chrome.fileSystem.chooseFile is deprecated");
console.log("Please use chrome.fileSystem.chooseEntry instead");
$Function.apply(fileSystem.chooseEntry, this, arguments);
};
});
exports.bindFileEntryCallback = bindFileEntryCallback;
exports.binding = binding.generate();
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the fileSystemProvider API.
var binding = require('binding').Binding.create('fileSystemProvider');
var fileSystemProviderInternal =
require('binding').Binding.create('fileSystemProviderInternal').generate();
var eventBindings = require('event_bindings');
var fileSystemNatives = requireNative('file_system_natives');
var GetDOMError = fileSystemNatives.GetDOMError;
/**
* Maximum size of the thumbnail in bytes.
* @type {number}
* @const
*/
var METADATA_THUMBNAIL_SIZE_LIMIT = 32 * 1024 * 1024;
/**
* Regular expression to validate if the thumbnail URI is a valid data URI,
* taking into account allowed formats.
* @type {RegExp}
* @const
*/
var METADATA_THUMBNAIL_FORMAT = new RegExp(
'^data:image/(png|jpeg|webp);', 'i');
/**
* Annotates a date with its serialized value.
* @param {Date} date Input date.
* @return {Date} Date with an extra <code>value</code> attribute.
*/
function annotateDate(date) {
// Copy in case the input date is frozen.
var result = new Date(date.getTime());
result.value = result.toString();
return result;
}
/**
* Verifies if the passed image URI is valid.
* @param {*} uri Image URI.
* @return {boolean} True if valid, valse otherwise.
*/
function verifyImageURI(uri) {
// The URI is specified by a user, so the type may be incorrect.
if (typeof uri != 'string' && !(uri instanceof String))
return false;
return METADATA_THUMBNAIL_FORMAT.test(uri);
}
/**
* Annotates an entry metadata by serializing its modifiedTime value.
* @param {EntryMetadata} metadata Input metadata.
* @return {EntryMetadata} metadata Annotated metadata, which can be passed
* back to the C++ layer.
*/
function annotateMetadata(metadata) {
var result = {
isDirectory: metadata.isDirectory,
name: metadata.name,
size: metadata.size,
modificationTime: annotateDate(metadata.modificationTime)
};
if ('mimeType' in metadata)
result.mimeType = metadata.mimeType;
if ('thumbnail' in metadata)
result.thumbnail = metadata.thumbnail;
return result;
}
/**
* Massages arguments of an event raised by the File System Provider API.
* @param {Array<*>} args Input arguments.
* @param {function(Array<*>)} dispatch Closure to be called with massaged
* arguments.
*/
function massageArgumentsDefault(args, dispatch) {
var executionStart = Date.now();
var options = args[0];
var onSuccessCallback = function(hasNext) {
fileSystemProviderInternal.operationRequestedSuccess(
options.fileSystemId, options.requestId, Date.now() - executionStart);
};
var onErrorCallback = function(error) {
fileSystemProviderInternal.operationRequestedError(
options.fileSystemId, options.requestId, error,
Date.now() - executionStart);
}
dispatch([options, onSuccessCallback, onErrorCallback]);
}
eventBindings.registerArgumentMassager(
'fileSystemProvider.onUnmountRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onGetMetadataRequested',
function(args, dispatch) {
var executionStart = Date.now();
var options = args[0];
var onSuccessCallback = function(metadata) {
var error;
// It is invalid to return a thumbnail when it's not requested. The
// restriction is added in order to avoid fetching the thumbnail while
// it's not needed.
if (!options.thumbnail && metadata.thumbnail)
error = 'Thumbnail data provided, but not requested.';
// Check the format and size. Note, that in the C++ layer, there is
// another sanity check to avoid passing any evil URL.
if ('thumbnail' in metadata && !verifyImageURI(metadata.thumbnail))
error = 'Thumbnail format invalid.';
if ('thumbnail' in metadata &&
metadata.thumbnail.length > METADATA_THUMBNAIL_SIZE_LIMIT) {
error = 'Thumbnail data too large.';
}
if (error) {
console.error(error);
fileSystemProviderInternal.operationRequestedError(
options.fileSystemId, options.requestId, 'FAILED',
Date.now() - executionStart);
return;
}
fileSystemProviderInternal.getMetadataRequestedSuccess(
options.fileSystemId,
options.requestId,
annotateMetadata(metadata),
Date.now() - executionStart);
};
var onErrorCallback = function(error) {
fileSystemProviderInternal.operationRequestedError(
options.fileSystemId, options.requestId, error,
Date.now() - executionStart);
}
dispatch([options, onSuccessCallback, onErrorCallback]);
});
eventBindings.registerArgumentMassager(
'fileSystemProvider.onGetActionsRequested',
function(args, dispatch) {
var executionStart = Date.now();
var options = args[0];
var onSuccessCallback = function(actions) {
fileSystemProviderInternal.getActionsRequestedSuccess(
options.fileSystemId,
options.requestId,
actions,
Date.now() - executionStart);
};
var onErrorCallback = function(error) {
fileSystemProviderInternal.operationRequestedError(
options.fileSystemId, options.requestId, error,
Date.now() - executionStart);
}
dispatch([options, onSuccessCallback, onErrorCallback]);
});
eventBindings.registerArgumentMassager(
'fileSystemProvider.onReadDirectoryRequested',
function(args, dispatch) {
var executionStart = Date.now();
var options = args[0];
var onSuccessCallback = function(entries, hasNext) {
var annotatedEntries = entries.map(annotateMetadata);
// It is invalid to return a thumbnail when it's not requested.
var error;
annotatedEntries.forEach(function(metadata) {
if (metadata.thumbnail) {
var error =
'Thumbnails must not be provided when reading a directory.';
return;
}
});
if (error) {
console.error(error);
fileSystemProviderInternal.operationRequestedError(
options.fileSystemId, options.requestId, 'FAILED',
Date.now() - executionStart);
return;
}
fileSystemProviderInternal.readDirectoryRequestedSuccess(
options.fileSystemId, options.requestId, annotatedEntries, hasNext,
Date.now() - executionStart);
};
var onErrorCallback = function(error) {
fileSystemProviderInternal.operationRequestedError(
options.fileSystemId, options.requestId, error,
Date.now() - executionStart);
}
dispatch([options, onSuccessCallback, onErrorCallback]);
});
eventBindings.registerArgumentMassager(
'fileSystemProvider.onOpenFileRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onCloseFileRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onReadFileRequested',
function(args, dispatch) {
var executionStart = Date.now();
var options = args[0];
var onSuccessCallback = function(data, hasNext) {
fileSystemProviderInternal.readFileRequestedSuccess(
options.fileSystemId, options.requestId, data, hasNext,
Date.now() - executionStart);
};
var onErrorCallback = function(error) {
fileSystemProviderInternal.operationRequestedError(
options.fileSystemId, options.requestId, error,
Date.now() - executionStart);
}
dispatch([options, onSuccessCallback, onErrorCallback]);
});
eventBindings.registerArgumentMassager(
'fileSystemProvider.onCreateDirectoryRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onDeleteEntryRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onCreateFileRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onCopyEntryRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onMoveEntryRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onTruncateRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onWriteFileRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onAbortRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onObserveDirectoryRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onUnobserveEntryRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onAddWatcherRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onRemoveWatcherRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onConfigureRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onExecuteActionRequested',
massageArgumentsDefault);
eventBindings.registerArgumentMassager(
'fileSystemProvider.onMountRequested',
function(args, dispatch) {
var onSuccessCallback = function() {
// TODO(mtomasz): To be implemented.
};
var onErrorCallback = function(error) {
// TODO(mtomasz): To be implemented.
}
dispatch([onSuccessCallback, onErrorCallback]);
});
exports.binding = binding.generate();
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the GCM API.
var binding = require('binding').Binding.create('gcm');
var forEach = require('utils').forEach;
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
var gcm = bindingsAPI.compiledApi;
apiFunctions.setUpdateArgumentsPostValidate(
'send', function(message, callback) {
// Validate message.data.
var payloadSize = 0;
forEach(message.data, function(property, value) {
if (property.length == 0)
throw new Error("One of data keys is empty.");
var lowerCasedProperty = property.toLowerCase();
// Issue an error for forbidden prefixes of property names.
if (lowerCasedProperty.indexOf("goog.") == 0 ||
lowerCasedProperty.indexOf("google") == 0 ||
property.indexOf("collapse_key") == 0) {
throw new Error("Invalid data key: " + property);
}
payloadSize += property.length + value.length;
});
if (payloadSize > gcm.MAX_MESSAGE_SIZE)
throw new Error("Payload exceeded allowed size limit. Payload size is: "
+ payloadSize);
if (payloadSize == 0)
throw new Error("No data to send.");
return arguments;
});
});
exports.binding = binding.generate();
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the Identity API.
var binding = require('binding').Binding.create('identity');
binding.registerCustomHook(function(binding, id, contextType) {
var apiFunctions = binding.apiFunctions;
var identity = binding.compiledApi;
apiFunctions.setHandleRequest('getRedirectURL', function(path) {
if (path === null || path === undefined)
path = '/';
else
path = String(path);
if (path[0] != '/')
path = '/' + path;
return 'https://' + id + '.chromiumapp.org' + path;
});
});
exports.binding = binding.generate();
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the image writer private API.
var binding = require('binding').Binding.create('imageWriterPrivate');
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setUpdateArgumentsPostValidate(
'writeFromFile', function(device, fileEntry, options, callback) {
var fileSystemName = fileEntry.filesystem.name;
var relativePath = $String.slice(fileEntry.fullPath, 1);
return [device, fileSystemName, relativePath, callback];
});
});
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the input ime API. Only injected into the
// v8 contexts for extensions which have permission for the API.
var binding = require('binding').Binding.create('input.ime');
var Event = require('event_bindings').Event;
binding.registerCustomHook(function(api) {
var input_ime = api.compiledApi;
input_ime.onKeyEvent.dispatchToListener = function(callback, args) {
var engineID = args[0];
var keyData = args[1];
var result = false;
try {
result = $Function.call(Event.prototype.dispatchToListener,
this, callback, args);
} catch (e) {
console.error('Error in event handler for onKeyEvent: ' + e.stack);
}
if (!input_ime.onKeyEvent.async) {
input_ime.keyEventHandled(keyData.requestId, result);
}
};
input_ime.onKeyEvent.addListener = function(cb, opt_extraInfo) {
input_ime.onKeyEvent.async = false;
if (opt_extraInfo instanceof Array) {
for (var i = 0; i < opt_extraInfo.length; ++i) {
if (opt_extraInfo[i] == "async") {
input_ime.onKeyEvent.async = true;
}
}
}
$Function.call(Event.prototype.addListener, this, cb);
};
});
exports.binding = binding.generate();
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the logPrivate API.
var binding = require('binding').Binding.create('logPrivate');
var sendRequest = require('sendRequest');
var getFileBindingsForApi =
require('fileEntryBindingUtil').getFileBindingsForApi;
var fileBindings = getFileBindingsForApi('logPrivate');
var bindFileEntryCallback = fileBindings.bindFileEntryCallback;
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
var fileSystem = bindingsAPI.compiledApi;
$Array.forEach(['dumpLogs'],
function(functionName) {
bindFileEntryCallback(functionName, apiFunctions);
});
});
exports.bindFileEntryCallback = bindFileEntryCallback;
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the Media Gallery API.
var binding = require('binding').Binding.create('mediaGalleries');
var blobNatives = requireNative('blob_natives');
var mediaGalleriesNatives = requireNative('mediaGalleries');
var blobsAwaitingMetadata = {};
var mediaGalleriesMetadata = {};
function createFileSystemObjectsAndUpdateMetadata(response) {
var result = [];
mediaGalleriesMetadata = {}; // Clear any previous metadata.
if (response) {
for (var i = 0; i < response.length; i++) {
var filesystem = mediaGalleriesNatives.GetMediaFileSystemObject(
response[i].fsid);
$Array.push(result, filesystem);
var metadata = response[i];
delete metadata.fsid;
mediaGalleriesMetadata[filesystem.name] = metadata;
}
}
return result;
}
binding.registerCustomHook(function(bindingsAPI, extensionId) {
var apiFunctions = bindingsAPI.apiFunctions;
// getMediaFileSystems, addUserSelectedFolder, and addScanResults use a
// custom callback so that they can instantiate and return an array of file
// system objects.
apiFunctions.setCustomCallback('getMediaFileSystems',
function(name, request, callback, response) {
var result = createFileSystemObjectsAndUpdateMetadata(response);
if (callback)
callback(result);
});
apiFunctions.setCustomCallback('addScanResults',
function(name, request, callback, response) {
var result = createFileSystemObjectsAndUpdateMetadata(response);
if (callback)
callback(result);
});
apiFunctions.setCustomCallback('addUserSelectedFolder',
function(name, request, callback, response) {
var fileSystems = [];
var selectedFileSystemName = "";
if (response && 'mediaFileSystems' in response &&
'selectedFileSystemIndex' in response) {
fileSystems = createFileSystemObjectsAndUpdateMetadata(
response['mediaFileSystems']);
var selectedFileSystemIndex = response['selectedFileSystemIndex'];
if (selectedFileSystemIndex >= 0) {
selectedFileSystemName = fileSystems[selectedFileSystemIndex].name;
}
}
if (callback)
callback(fileSystems, selectedFileSystemName);
});
apiFunctions.setCustomCallback('dropPermissionForMediaFileSystem',
function(name, request, callback, response) {
var galleryId = response;
if (galleryId) {
for (var key in mediaGalleriesMetadata) {
if (mediaGalleriesMetadata[key].galleryId == galleryId) {
delete mediaGalleriesMetadata[key];
break;
}
}
}
if (callback)
callback();
});
apiFunctions.setHandleRequest('getMediaFileSystemMetadata',
function(filesystem) {
if (filesystem && filesystem.name &&
filesystem.name in mediaGalleriesMetadata) {
return mediaGalleriesMetadata[filesystem.name];
}
return {
'name': '',
'galleryId': '',
'isRemovable': false,
'isMediaDevice': false,
'isAvailable': false,
};
});
apiFunctions.setUpdateArgumentsPostValidate('getMetadata',
function(mediaFile, options, callback) {
var blobUuid = blobNatives.GetBlobUuid(mediaFile)
// Store the blob in a global object to keep its refcount nonzero -- this
// prevents the object from being garbage collected before any metadata
// parsing gets to occur (see crbug.com/415792).
blobsAwaitingMetadata[blobUuid] = mediaFile;
return [blobUuid, options, callback];
});
apiFunctions.setCustomCallback('getMetadata',
function(name, request, callback, response) {
if (response.attachedImagesBlobInfo) {
for (var i = 0; i < response.attachedImagesBlobInfo.length; i++) {
var blobInfo = response.attachedImagesBlobInfo[i];
var blob = blobNatives.TakeBrowserProcessBlob(
blobInfo.blobUUID, blobInfo.type, blobInfo.size);
response.metadata.attachedImages.push(blob);
}
}
if (callback)
callback(response.metadata);
// The UUID was in position 0 in the setUpdateArgumentsPostValidate
// function.
var uuid = request.args[0];
delete blobsAwaitingMetadata[uuid];
});
});
exports.binding = binding.generate();
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom bindings for the notifications API.
//
var binding = require('binding').Binding.create('notifications');
var sendRequest = require('sendRequest').sendRequest;
var exceptionHandler = require('uncaught_exception_handler');
var imageUtil = require('imageUtil');
var lastError = require('lastError');
var notificationsPrivate = requireNative('notifications_private');
function imageDataSetter(context, key) {
var f = function(val) {
this[key] = val;
};
return $Function.bind(f, context);
}
// A URL Spec is an object with the following keys:
// path: The resource to be downloaded.
// width: (optional) The maximum width of the image to be downloaded in device
// pixels.
// height: (optional) The maximum height of the image to be downloaded in
// device pixels.
// callback: A function to be called when the URL is complete. It
// should accept an ImageData object and set the appropriate
// field in |notificationDetails|.
function getUrlSpecs(imageSizes, notificationDetails) {
var urlSpecs = [];
// |iconUrl| might be optional for notification updates.
if (notificationDetails.iconUrl) {
$Array.push(urlSpecs, {
path: notificationDetails.iconUrl,
width: imageSizes.icon.width * imageSizes.scaleFactor,
height: imageSizes.icon.height * imageSizes.scaleFactor,
callback: imageDataSetter(notificationDetails, 'iconBitmap')
});
}
// |appIconMaskUrl| is optional.
if (notificationDetails.appIconMaskUrl) {
$Array.push(urlSpecs, {
path: notificationDetails.appIconMaskUrl,
width: imageSizes.appIconMask.width * imageSizes.scaleFactor,
height: imageSizes.appIconMask.height * imageSizes.scaleFactor,
callback: imageDataSetter(notificationDetails, 'appIconMaskBitmap')
});
}
// |imageUrl| is optional.
if (notificationDetails.imageUrl) {
$Array.push(urlSpecs, {
path: notificationDetails.imageUrl,
width: imageSizes.image.width * imageSizes.scaleFactor,
height: imageSizes.image.height * imageSizes.scaleFactor,
callback: imageDataSetter(notificationDetails, 'imageBitmap')
});
}
// Each button has an optional icon.
var buttonList = notificationDetails.buttons;
if (buttonList && typeof buttonList.length === 'number') {
var numButtons = buttonList.length;
for (var i = 0; i < numButtons; i++) {
if (buttonList[i].iconUrl) {
$Array.push(urlSpecs, {
path: buttonList[i].iconUrl,
width: imageSizes.buttonIcon.width * imageSizes.scaleFactor,
height: imageSizes.buttonIcon.height * imageSizes.scaleFactor,
callback: imageDataSetter(buttonList[i], 'iconBitmap')
});
}
}
}
return urlSpecs;
}
function replaceNotificationOptionURLs(notification_details, callback) {
var imageSizes = notificationsPrivate.GetNotificationImageSizes();
var url_specs = getUrlSpecs(imageSizes, notification_details);
if (!url_specs.length) {
callback(true);
return;
}
var errors = 0;
imageUtil.loadAllImages(url_specs, {
onerror: function(index) {
errors++;
},
oncomplete: function(imageData) {
if (errors > 0) {
callback(false);
return;
}
for (var index = 0; index < url_specs.length; index++) {
var url_spec = url_specs[index];
url_spec.callback(imageData[index]);
}
callback(true);
}
});
}
function genHandle(name, failure_function) {
return function(id, input_notification_details, callback) {
// TODO(dewittj): Remove this hack. This is used as a way to deep
// copy a complex JSON object.
var notification_details = $JSON.parse(
$JSON.stringify(input_notification_details));
var that = this;
var stack = exceptionHandler.getExtensionStackTrace();
replaceNotificationOptionURLs(notification_details, function(success) {
if (success) {
sendRequest(that.name,
[id, notification_details, callback],
that.definition.parameters, {stack: stack});
return;
}
lastError.run(name,
'Unable to download all specified images.',
stack,
failure_function, [callback || function() {}, id]);
});
};
}
var handleCreate = genHandle('notifications.create',
function(callback, id) { callback(id); });
var handleUpdate = genHandle('notifications.update',
function(callback, id) { callback(false); });
var notificationsCustomHook = function(bindingsAPI, extensionId) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequest('create', handleCreate);
apiFunctions.setHandleRequest('update', handleUpdate);
};
binding.registerCustomHook(notificationsCustomHook);
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the omnibox API. Only injected into the v8 contexts
// for extensions which have permission for the omnibox API.
var binding = require('binding').Binding.create('omnibox');
var eventBindings = require('event_bindings');
var sendRequest = require('sendRequest').sendRequest;
// Remove invalid characters from |text| so that it is suitable to use
// for |AutocompleteMatch::contents|.
function sanitizeString(text, shouldTrim) {
// NOTE: This logic mirrors |AutocompleteMatch::SanitizeString()|.
// 0x2028 = line separator; 0x2029 = paragraph separator.
var kRemoveChars = /(\r|\n|\t|\u2028|\u2029)/gm;
if (shouldTrim)
text = text.trimLeft();
return text.replace(kRemoveChars, '');
}
// Parses the xml syntax supported by omnibox suggestion results. Returns an
// object with two properties: 'description', which is just the text content,
// and 'descriptionStyles', which is an array of style objects in a format
// understood by the C++ backend.
function parseOmniboxDescription(input) {
var domParser = new DOMParser();
// The XML parser requires a single top-level element, but we want to
// support things like 'hello, <match>world</match>!'. So we wrap the
// provided text in generated root level element.
var root = domParser.parseFromString(
'<fragment>' + input + '</fragment>', 'text/xml');
// DOMParser has a terrible error reporting facility. Errors come out nested
// inside the returned document.
var error = root.querySelector('parsererror div');
if (error) {
throw new Error(error.textContent);
}
// Otherwise, it's valid, so build up the result.
var result = {
description: '',
descriptionStyles: []
};
// Recursively walk the tree.
function walk(node) {
for (var i = 0, child; child = node.childNodes[i]; i++) {
// Append text nodes to our description.
if (child.nodeType == Node.TEXT_NODE) {
var shouldTrim = result.description.length == 0;
result.description += sanitizeString(child.nodeValue, shouldTrim);
continue;
}
// Process and descend into a subset of recognized tags.
if (child.nodeType == Node.ELEMENT_NODE &&
(child.nodeName == 'dim' || child.nodeName == 'match' ||
child.nodeName == 'url')) {
var style = {
'type': child.nodeName,
'offset': result.description.length
};
$Array.push(result.descriptionStyles, style);
walk(child);
style.length = result.description.length - style.offset;
continue;
}
// Descend into all other nodes, even if they are unrecognized, for
// forward compat.
walk(child);
}
};
walk(root);
return result;
}
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setUpdateArgumentsPreValidate('setDefaultSuggestion',
function(suggestResult) {
if (suggestResult.content != undefined) { // null, etc.
throw new Error(
'setDefaultSuggestion cannot contain the "content" field');
}
return [suggestResult];
});
apiFunctions.setHandleRequest('setDefaultSuggestion', function(details) {
var parseResult = parseOmniboxDescription(details.description);
sendRequest(this.name, [parseResult], this.definition.parameters);
});
apiFunctions.setUpdateArgumentsPostValidate(
'sendSuggestions', function(requestId, userSuggestions) {
var suggestions = [];
for (var i = 0; i < userSuggestions.length; i++) {
var parseResult = parseOmniboxDescription(
userSuggestions[i].description);
parseResult.content = userSuggestions[i].content;
$Array.push(suggestions, parseResult);
}
return [requestId, suggestions];
});
});
eventBindings.registerArgumentMassager('omnibox.onInputChanged',
function(args, dispatch) {
var text = args[0];
var requestId = args[1];
var suggestCallback = function(suggestions) {
chrome.omnibox.sendSuggestions(requestId, suggestions);
};
dispatch([text, suggestCallback]);
});
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the pageAction API.
var binding = require('binding').Binding.create('pageAction');
var setIcon = require('setIcon').setIcon;
var sendRequest = require('sendRequest').sendRequest;
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequest('setIcon', function(details, callback) {
setIcon(details, function(args) {
sendRequest(this.name, [args, callback], this.definition.parameters);
}.bind(this));
});
});
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the pageCapture API.
var binding = require('binding').Binding.create('pageCapture');
var handleUncaughtException = require('uncaught_exception_handler').handle;
var pageCaptureNatives = requireNative('page_capture');
var CreateBlob = pageCaptureNatives.CreateBlob;
var SendResponseAck = pageCaptureNatives.SendResponseAck;
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setCustomCallback('saveAsMHTML',
function(name, request, callback, response) {
if (response)
response = CreateBlob(response.mhtmlFilePath, response.mhtmlFileLength);
try {
callback(response);
} catch (e) {
handleUncaughtException(
'Error in chrome.pageCapture.saveAsMHTML callback', e, request.stack);
} finally {
// Notify the browser. Now that the blob is referenced from JavaScript,
// the browser can drop its reference to it.
SendResponseAck(request.id);
}
});
});
exports.binding = binding.generate();
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the platformKeys API.
var binding = require('binding').Binding.create('platformKeys');
var SubtleCrypto = require('platformKeys.SubtleCrypto').SubtleCrypto;
var getPublicKey = require('platformKeys.getPublicKey').getPublicKey;
var internalAPI = require('platformKeys.internalAPI');
var keyModule = require('platformKeys.Key');
var Key = keyModule.Key;
var KeyType = keyModule.KeyType;
var KeyUsage = keyModule.KeyUsage;
function createPublicKey(publicKeySpki, algorithm) {
return new Key(KeyType.public, publicKeySpki, algorithm, [KeyUsage.verify],
true /* extractable */);
}
function createPrivateKey(publicKeySpki, algorithm) {
return new Key(KeyType.private, publicKeySpki, algorithm, [KeyUsage.sign],
false /* not extractable */);
}
binding.registerCustomHook(function(api) {
var apiFunctions = api.apiFunctions;
var subtleCrypto = new SubtleCrypto('' /* tokenId */);
apiFunctions.setHandleRequest(
'selectClientCertificates', function(details, callback) {
internalAPI.selectClientCertificates(details, function(matches) {
if (chrome.runtime.lastError) {
callback([]);
return;
}
callback($Array.map(matches, function(match) {
// internalAPI.selectClientCertificates returns publicExponent as
// ArrayBuffer, but it should be a Uint8Array.
if (match.keyAlgorithm.publicExponent) {
match.keyAlgorithm.publicExponent =
new Uint8Array(match.keyAlgorithm.publicExponent);
}
return match;
}));
});
});
apiFunctions.setHandleRequest(
'subtleCrypto', function() { return subtleCrypto });
apiFunctions.setHandleRequest(
'getKeyPair', function(cert, params, callback) {
getPublicKey(cert, params, function(publicKey, algorithm) {
if (chrome.runtime.lastError) {
callback();
return;
}
callback(createPublicKey(publicKey, algorithm),
createPrivateKey(publicKey, algorithm));
});
});
});
exports.binding = binding.generate();
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var internalAPI = require('platformKeys.internalAPI');
var normalizeAlgorithm =
requireNative('platform_keys_natives').NormalizeAlgorithm;
// Returns the normalized parameters of |importParams|.
// Any unknown parameters will be ignored.
function normalizeImportParams(importParams) {
if (!importParams.name ||
Object.prototype.toString.call(importParams.name) != '[object String]') {
throw new Error('Algorithm: name: Missing or not a String');
}
var filteredParams = { name: importParams.name };
var hashIsNone = false;
if (importParams.hash) {
if (importParams.hash.name.toLowerCase() === 'none') {
hashIsNone = true;
// Temporarily replace |hash| by a valid WebCrypto Hash for normalization.
// This will be reverted to 'none' after normalization.
filteredParams.hash = { name: 'SHA-1' };
} else {
filteredParams.hash = { name: importParams.hash.name }
}
}
// Apply WebCrypto's algorithm normalization.
var resultParams = normalizeAlgorithm(filteredParams, 'ImportKey');
if (!resultParams ) {
throw new Error('A required parameter was missing or out-of-range');
}
if (hashIsNone) {
resultParams.hash = { name: 'none' };
}
return resultParams;
}
function combineAlgorithms(algorithm, importParams) {
// internalAPI.getPublicKey returns publicExponent as ArrayBuffer, but it
// should be a Uint8Array.
if (algorithm.publicExponent) {
algorithm.publicExponent = new Uint8Array(algorithm.publicExponent);
}
algorithm.hash = importParams.hash;
return algorithm;
}
function getPublicKey(cert, importParams, callback) {
importParams = normalizeImportParams(importParams);
internalAPI.getPublicKey(
cert, importParams.name, function(publicKey, algorithm) {
if (chrome.runtime.lastError) {
callback();
return;
}
var combinedAlgorithm = combineAlgorithms(algorithm, importParams);
callback(publicKey, combinedAlgorithm);
});
}
exports.getPublicKey = getPublicKey;
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var binding = require('binding')
.Binding.create('platformKeysInternal')
.generate();
exports.selectClientCertificates = binding.selectClientCertificates;
exports.sign = binding.sign;
exports.getPublicKey = binding.getPublicKey;
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var utils = require('utils');
/**
* Enum of possible key types (subset of WebCrypto.KeyType).
* @enum {string}
*/
var KeyType = {
public: 'public',
private: 'private'
};
/**
* Enum of possible key usages (subset of WebCrypto.KeyUsage).
* @enum {string}
*/
var KeyUsage = {
sign: 'sign',
verify: 'verify'
};
/**
* Implementation of WebCrypto.Key used in enterprise.platformKeys.
* @param {KeyType} type The type of the new key.
* @param {ArrayBuffer} publicKeySpki The Subject Public Key Info in DER
* encoding.
* @param {KeyAlgorithm} algorithm The algorithm identifier.
* @param {KeyUsage[]} usages The allowed key usages.
* @param {boolean} extractable Whether the key is extractable.
* @constructor
*/
var KeyImpl = function(type, publicKeySpki, algorithm, usages, extractable) {
this.type = type;
this.spki = publicKeySpki;
this.algorithm = algorithm;
this.usages = usages;
this.extractable = extractable;
};
var KeyBase = function() {};
Object.defineProperty(KeyBase.prototype, 'algorithm', {
enumerable: true,
get: function() {
return utils.deepCopy(privates(this).impl.algorithm);
}
});
var Key = utils.expose(
'Key',
KeyImpl,
{superclass: KeyBase, readonly: ['extractable', 'type', 'usages']});
/**
* Returns |key|'s Subject Public Key Info. Throws an exception if |key| is not
* a valid Key object.
* @param {Key} key
* @return {ArrayBuffer} The Subject Public Key Info in DER encoding of |key|.
*/
function getSpki(key) {
if (!privates(key))
throw new Error('Invalid key object.');
var keyImpl = privates(key).impl;
if (!keyImpl || !keyImpl.spki)
throw new Error('Invalid key object.');
return keyImpl.spki;
}
exports.Key = Key;
exports.KeyType = KeyType;
exports.KeyUsage = KeyUsage;
exports.getSpki = getSpki;
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var utils = require('utils');
var internalAPI = require('platformKeys.internalAPI');
var keyModule = require('platformKeys.Key');
var getSpki = keyModule.getSpki;
var KeyUsage = keyModule.KeyUsage;
var normalizeAlgorithm =
requireNative('platform_keys_natives').NormalizeAlgorithm;
// This error is thrown by the internal and public API's token functions and
// must be rethrown by this custom binding. Keep this in sync with the C++ part
// of this API.
var errorInvalidToken = "The token is not valid.";
// The following errors are specified in WebCrypto.
// TODO(pneubeck): These should be DOMExceptions.
function CreateNotSupportedError() {
return new Error('The algorithm is not supported');
}
function CreateInvalidAccessError() {
return new Error('The requested operation is not valid for the provided key');
}
function CreateDataError() {
return new Error('Data provided to an operation does not meet requirements');
}
function CreateSyntaxError() {
return new Error('A required parameter was missing or out-of-range');
}
function CreateOperationError() {
return new Error('The operation failed for an operation-specific reason');
}
// Catches an |internalErrorInvalidToken|. If so, forwards it to |reject| and
// returns true.
function catchInvalidTokenError(reject) {
if (chrome.runtime.lastError &&
chrome.runtime.lastError.message == errorInvalidToken) {
reject(chrome.runtime.lastError);
return true;
}
return false;
}
/**
* Implementation of WebCrypto.SubtleCrypto used in platformKeys and
* enterprise.platformKeys.
* @param {string} tokenId The id of the backing Token.
* @constructor
*/
var SubtleCryptoImpl = function(tokenId) {
this.tokenId = tokenId;
};
SubtleCryptoImpl.prototype.sign = function(algorithm, key, dataView) {
var subtleCrypto = this;
return new Promise(function(resolve, reject) {
if (key.type != 'private' || key.usages.indexOf(KeyUsage.sign) == -1)
throw CreateInvalidAccessError();
var normalizedAlgorithmParameters =
normalizeAlgorithm(algorithm, 'Sign');
if (!normalizedAlgorithmParameters) {
// TODO(pneubeck): It's not clear from the WebCrypto spec which error to
// throw here.
throw CreateSyntaxError();
}
// Create an ArrayBuffer that equals the dataView. Note that dataView.buffer
// might contain more data than dataView.
var data = dataView.buffer.slice(dataView.byteOffset,
dataView.byteOffset + dataView.byteLength);
internalAPI.sign(subtleCrypto.tokenId,
getSpki(key),
key.algorithm.hash.name,
data,
function(signature) {
if (catchInvalidTokenError(reject))
return;
if (chrome.runtime.lastError) {
reject(CreateOperationError());
return;
}
resolve(signature);
});
});
};
SubtleCryptoImpl.prototype.exportKey = function(format, key) {
return new Promise(function(resolve, reject) {
if (format == 'pkcs8') {
// Either key.type is not 'private' or the key is not extractable. In both
// cases the error is the same.
throw CreateInvalidAccessError();
} else if (format == 'spki') {
if (key.type != 'public')
throw CreateInvalidAccessError();
resolve(getSpki(key));
} else {
// TODO(pneubeck): It should be possible to export to format 'jwk'.
throw CreateNotSupportedError();
}
});
};
// Required for subclassing.
exports.SubtleCryptoImpl = SubtleCryptoImpl
exports.SubtleCrypto =
utils.expose('SubtleCrypto',
SubtleCryptoImpl,
{functions:['sign', 'exportKey']});
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Returns the intersection of the arrays |a| and |b|, which do not have to be
// sorted.
function intersect(a, b) {
var result = [];
for (var i = 0; i < a.length; i++) {
if (b.indexOf(a[i]) >= 0)
result.push(a[i]);
}
return result;
};
exports.intersect = intersect;
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the syncFileSystem API.
var binding = require('binding').Binding.create('syncFileSystem');
var eventBindings = require('event_bindings');
var fileSystemNatives = requireNative('file_system_natives');
var syncFileSystemNatives = requireNative('sync_file_system');
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
// Functions which take in an [instanceOf=FileEntry].
function bindFileEntryFunction(functionName) {
apiFunctions.setUpdateArgumentsPostValidate(
functionName, function(entry, callback) {
var fileSystemUrl = entry.toURL();
return [fileSystemUrl, callback];
});
}
$Array.forEach(['getFileStatus'], bindFileEntryFunction);
// Functions which take in a FileEntry array.
function bindFileEntryArrayFunction(functionName) {
apiFunctions.setUpdateArgumentsPostValidate(
functionName, function(entries, callback) {
var fileSystemUrlArray = [];
for (var i=0; i < entries.length; i++) {
$Array.push(fileSystemUrlArray, entries[i].toURL());
}
return [fileSystemUrlArray, callback];
});
}
$Array.forEach(['getFileStatuses'], bindFileEntryArrayFunction);
// Functions which take in an [instanceOf=DOMFileSystem].
function bindFileSystemFunction(functionName) {
apiFunctions.setUpdateArgumentsPostValidate(
functionName, function(filesystem, callback) {
var fileSystemUrl = filesystem.root.toURL();
return [fileSystemUrl, callback];
});
}
$Array.forEach(['getUsageAndQuota'], bindFileSystemFunction);
// Functions which return an [instanceOf=DOMFileSystem].
apiFunctions.setCustomCallback('requestFileSystem',
function(name, request, callback, response) {
var result = null;
if (response) {
result = syncFileSystemNatives.GetSyncFileSystemObject(
response.name, response.root);
}
if (callback)
callback(result);
});
// Functions which return an array of FileStatusInfo object
// which has [instanceOf=FileEntry].
apiFunctions.setCustomCallback('getFileStatuses',
function(name, request, callback, response) {
var results = [];
if (response) {
for (var i = 0; i < response.length; i++) {
var result = {};
var entry = response[i].entry;
result.fileEntry = fileSystemNatives.GetFileEntry(
entry.fileSystemType,
entry.fileSystemName,
entry.rootUrl,
entry.filePath,
entry.isDirectory);
result.status = response[i].status;
result.error = response[i].error;
$Array.push(results, result);
}
}
if (callback)
callback(results);
});
});
eventBindings.registerArgumentMassager(
'syncFileSystem.onFileStatusChanged', function(args, dispatch) {
// Make FileEntry object using all the base string fields.
var fileEntry = fileSystemNatives.GetFileEntry(
args[0].fileSystemType,
args[0].fileSystemName,
args[0].rootUrl,
args[0].filePath,
args[0].isDirectory);
// Combine into a single dictionary.
var fileInfo = new Object();
fileInfo.fileEntry = fileEntry;
fileInfo.status = args[1];
if (fileInfo.status == "synced") {
fileInfo.action = args[2];
fileInfo.direction = args[3];
}
dispatch([fileInfo]);
});
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the systemIndicator API.
// TODO(dewittj) Refactor custom binding to reduce redundancy between the
// extension action APIs.
var binding = require('binding').Binding.create('systemIndicator');
var setIcon = require('setIcon').setIcon;
var sendRequest = require('sendRequest').sendRequest;
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequest('setIcon', function(details, callback) {
setIcon(details, function(args) {
sendRequest(this.name, [args, callback], this.definition.parameters);
}.bind(this));
});
});
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the Tab Capture API.
var binding = require('binding').Binding.create('tabCapture');
binding.registerCustomHook(function(bindingsAPI, extensionId) {
var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setCustomCallback('capture',
function(name, request, callback, response) {
if (!callback)
return;
if (response) {
var options = {};
if (response.audioConstraints)
options.audio = response.audioConstraints;
if (response.videoConstraints)
options.video = response.videoConstraints;
try {
navigator.webkitGetUserMedia(options,
function(stream) { callback(stream); },
function() { callback(null); });
} catch (e) {
callback(null);
}
} else {
callback(null);
}
});
});
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the tabs API.
var binding = require('binding').Binding.create('tabs');
var messaging = require('messaging');
var tabsNatives = requireNative('tabs');
var OpenChannelToTab = tabsNatives.OpenChannelToTab;
var sendRequestIsDisabled = requireNative('process').IsSendRequestDisabled();
var forEach = require('utils').forEach;
binding.registerCustomHook(function(bindingsAPI, extensionId) {
var apiFunctions = bindingsAPI.apiFunctions;
var tabs = bindingsAPI.compiledApi;
apiFunctions.setHandleRequest('connect', function(tabId, connectInfo) {
var name = '';
var frameId = -1;
if (connectInfo) {
name = connectInfo.name || name;
frameId = connectInfo.frameId;
if (typeof frameId == 'undefined' || frameId < 0)
frameId = -1;
}
var portId = OpenChannelToTab(tabId, frameId, extensionId, name);
return messaging.createPort(portId, name);
});
apiFunctions.setHandleRequest('sendRequest',
function(tabId, request, responseCallback) {
if (sendRequestIsDisabled)
throw new Error(sendRequestIsDisabled);
var port = tabs.connect(tabId, {name: messaging.kRequestChannel});
messaging.sendMessageImpl(port, request, responseCallback);
});
apiFunctions.setHandleRequest('sendMessage',
function(tabId, message, options, responseCallback) {
var connectInfo = {
name: messaging.kMessageChannel
};
if (options) {
forEach(options, function(k, v) {
connectInfo[k] = v;
});
}
var port = tabs.connect(tabId, connectInfo);
messaging.sendMessageImpl(port, message, responseCallback);
});
});
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
function watchForTag(tagName, cb) {
if (!document.body)
return;
function findChildTags(queryNode) {
$Array.forEach(queryNode.querySelectorAll(tagName), function(node) {
cb(node);
});
}
// Query tags already in the document.
findChildTags(document.body);
// Observe the tags added later.
var documentObserver = new MutationObserver(function(mutations) {
$Array.forEach(mutations, function(mutation) {
$Array.forEach(mutation.addedNodes, function(addedNode) {
if (addedNode.nodeType == Node.ELEMENT_NODE) {
if (addedNode.tagName == tagName)
cb(addedNode);
findChildTags(addedNode);
}
});
});
});
documentObserver.observe(document, {subtree: true, childList: true});
}
// Expose a function to watch the |tagName| introduction via mutation observer.
//
// We employee mutation observer to watch on any introduction of |tagName|
// within document so that we may handle it accordingly (either creating it or
// reporting error due to lack of permission).
// Think carefully about when to call this. On one hand, mutation observer
// functions on document, so we need to make sure document is finished
// parsing. To satisfy this, document.readyState has to be "interactive" or
// after. On the other hand, we intend to do this as early as possible so that
// developer would have no chance to bring in any conflicted property. To meet
// this requirement, we choose "readystatechange" event of window and use
// capturing way.
function addTagWatcher(tagName, cb) {
var useCapture = true;
window.addEventListener('readystatechange', function listener(event) {
if (document.readyState == 'loading')
return;
watchForTag(tagName, cb);
window.removeEventListener(event.type, listener, useCapture);
}, useCapture);
}
exports.addTagWatcher = addTagWatcher;
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the tts API.
var binding = require('binding').Binding.create('tts');
var idGenerator = requireNative('id_generator');
var sendRequest = require('sendRequest').sendRequest;
var lazyBG = requireNative('lazy_background_page');
binding.registerCustomHook(function(api) {
var apiFunctions = api.apiFunctions;
var tts = api.compiledApi;
var handlers = {};
function ttsEventListener(event) {
var eventHandler = handlers[event.srcId];
if (eventHandler) {
eventHandler({
type: event.type,
charIndex: event.charIndex,
errorMessage: event.errorMessage
});
if (event.isFinalEvent) {
delete handlers[event.srcId];
// Balanced in 'speak' handler.
lazyBG.DecrementKeepaliveCount();
}
}
}
// This file will get run if an extension needs the ttsEngine permission, but
// it doesn't necessarily have the tts permission. If it doesn't, trying to
// add a listener to chrome.tts.onEvent will fail.
// See http://crbug.com/122474.
try {
tts.onEvent.addListener(ttsEventListener);
} catch (e) {}
apiFunctions.setHandleRequest('speak', function() {
var args = arguments;
if (args.length > 1 && args[1] && args[1].onEvent) {
var id = idGenerator.GetNextId();
args[1].srcId = id;
handlers[id] = args[1].onEvent;
// Keep the page alive until the event finishes.
// Balanced in eventHandler.
lazyBG.IncrementKeepaliveCount();
}
sendRequest(this.name, args, this.definition.parameters);
return id;
});
});
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the ttsEngine API.
var binding = require('binding').Binding.create('ttsEngine');
var eventBindings = require('event_bindings');
eventBindings.registerArgumentMassager('ttsEngine.onSpeak',
function(args, dispatch) {
var text = args[0];
var options = args[1];
var requestId = args[2];
var sendTtsEvent = function(event) {
chrome.ttsEngine.sendTtsEvent(requestId, event);
};
dispatch([text, options, sendTtsEvent]);
});
exports.binding = binding.generate();
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the webrtcDesktopCapturePrivate API.
var binding = require('binding').Binding.create('webrtcDesktopCapturePrivate');
var sendRequest = require('sendRequest').sendRequest;
var idGenerator = requireNative('id_generator');
binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
var pendingRequests = {};
function onRequestResult(id, result) {
if (id in pendingRequests) {
var callback = pendingRequests[id];
delete pendingRequests[id];
callback(result);
}
}
apiFunctions.setHandleRequest('chooseDesktopMedia',
function(sources, request, callback) {
var id = idGenerator.GetNextId();
pendingRequests[id] = callback;
sendRequest(this.name,
[id, sources, request, onRequestResult.bind(null, id)],
this.definition.parameters, {});
return id;
});
apiFunctions.setHandleRequest('cancelChooseDesktopMedia', function(id) {
if (id in pendingRequests) {
delete pendingRequests[id];
sendRequest(this.name, [id], this.definition.parameters, {});
}
});
});
exports.binding = binding.generate();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom binding for the webstore API.
var webstoreNatives = requireNative('webstore');
var Event = require('event_bindings').Event;
function Installer() {
this._pendingInstall = null;
this.onInstallStageChanged =
new Event(null, [{name: 'stage', type: 'string'}], {unmanaged: true});
this.onDownloadProgress =
new Event(null, [{name: 'progress', type: 'number'}], {unmanaged: true});
}
Installer.prototype.install = function(url, onSuccess, onFailure) {
if (this._pendingInstall)
throw new Error('A Chrome Web Store installation is already pending.');
if (url !== undefined && typeof(url) !== 'string') {
throw new Error(
'The Chrome Web Store item link URL parameter must be a string.');
}
if (onSuccess !== undefined && typeof(onSuccess) !== 'function')
throw new Error('The success callback parameter must be a function.');
if (onFailure !== undefined && typeof(onFailure) !== 'function')
throw new Error('The failure callback parameter must be a function.');
// Since we call Install() with a bool for if we have listeners, listeners
// must be set prior to the inline installation starting (this is also
// noted in the Event documentation in
// chrome/common/extensions/api/webstore.json).
var installId = webstoreNatives.Install(
this.onInstallStageChanged.hasListeners(),
this.onDownloadProgress.hasListeners(),
url,
onSuccess,
onFailure);
if (installId !== undefined) {
this._pendingInstall = {
installId: installId,
onSuccess: onSuccess,
onFailure: onFailure
};
}
};
Installer.prototype.onInstallResponse =
function(installId, success, error, resultCode) {
var pendingInstall = this._pendingInstall;
if (!pendingInstall || pendingInstall.installId != installId) {
// TODO(kalman): should this be an error?
return;
}
try {
if (success && pendingInstall.onSuccess)
pendingInstall.onSuccess();
else if (!success && pendingInstall.onFailure)
pendingInstall.onFailure(error, resultCode);
} catch (e) {
console.error('Exception in chrome.webstore.install response handler: ' +
e.stack);
} finally {
this._pendingInstall = null;
}
};
Installer.prototype.onInstallStageChanged = function(installStage) {
this.onInstallStageChanged.dispatch(installStage);
};
Installer.prototype.onDownloadProgress = function(progress) {
this.onDownloadProgress.dispatch(progress);
};
var installer = new Installer();
var chromeWebstore = {
install: function (url, onSuccess, onFailure) {
installer.install(url, onSuccess, onFailure);
},
onInstallStageChanged: installer.onInstallStageChanged,
onDownloadProgress: installer.onDownloadProgress
};
exports.binding = chromeWebstore;
// Called by webstore_bindings.cc.
exports.onInstallResponse =
Installer.prototype.onInstallResponse.bind(installer);
exports.onInstallStageChanged =
Installer.prototype.onInstallStageChanged.bind(installer);
exports.onDownloadProgress =
Installer.prototype.onDownloadProgress.bind(installer);
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
exports.didCreateDocumentElement = function() {
var root = document.documentElement.createShadowRoot();
root.appendChild(document.createElement('style')).innerText =
// TODO(jeremya): switch this to use automatic inlining once grit
// supports inlining into JS. See http://crbug.com/146319.
"x-titlebar { height: 24px; width: 100%; " +
"position: fixed; left: 0; top: 0; }\n" +
"div { margin-top: 24px; position: absolute; top: 0px; width: 100%; " +
"-webkit-widget-region: region(control rectangle); }\n" +
":-webkit-full-screen * { display: none; }\n" +
":-webkit-full-screen-document * { display: none; }\n" +
"div:-webkit-full-screen, div:-webkit-full-screen-document { " +
"margin-top: 0; }\n" +
"button { -webkit-widget-region: region(control rectangle); }\n" +
"button.close { border: 0; background-color: transparent; " +
"width: 16px; height: 16px; " +
"position: absolute; right: 4px; top: 4px; }\n" +
"button.close { background-image: url(data:image/png;base64," +
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA9ElEQVQ4T7VTQQ6CMBCk0H" +
"AyIfAQbiZ+QHyDL/QLxqvx4MWDB+MvFAWMAuKsacmmSjkQSDbQ2Z3Z3WkQzsBHDOQ7owgs" +
"MdUacTGmi3BeIFYcNycgciGlfFRVtcd3qoojz/PmdV0XOD8RGy1iCoQgT5G8IyREjni7IC" +
"cg58ilwA7A8i4BwgMUxkKIV9M0PggTAoFlJpnwLhO5iEuFapq2s20CyoWIGbpeaRICyrI8" +
"89FtAtqwGxdQ65yYsV8NcwVN5obR/uTJW4mQsfp2fgToGjPqbBjWeoJVfNRsbSskSO7+7B" +
"sAiznZdgu6Qe97lH+htysv+AA10msRAt5JYQAAAABJRU5ErkJggg==); }\n" +
"button.close:hover { background-image: url(data:image/png;base64," +
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABTElEQVQ4T2NkoBAwUqifAc" +
"WA////KwANFAPiV4yMjA+QDcclBzcApCA6Otpz2bJluQkJCf3z58/fDTMEnxyyAWZADQuA" +
"tj4B4ncpKSnbZs+efQjkCqjBmUDmMyD+ADSwD6j2FEgOxQWJiYmuCxYscIYawpWamnr89+" +
"/fHECxbKjmB2VlZbs6OzsvwFyHEQZATXZz5syxAGr4BMR8QCwJDYvn1dXVO1taWi4ihw9G" +
"LID8m5aWZgt0viXUEBaQAUDNh9E1o3gBFuIgA6Be8QKK3QXiLyA5oNMvIDsdph7DC9AASw" +
"cquI9sAJDNk5GRcX769OlHsXoBKapAoQ2KiQcgPwMDkbGrq8sGyP8DChNQwM6aNeswRiAC" +
"DYBF4yOgwnuwAAM5NTMz03rGjBnWsIAFql2ANxqB/l2B7F/kgCUYjUBbyEvKsFAllaY4Nw" +
"IAmJDPEd+LFvYAAAAASUVORK5CYII=); }\n" +
"button.close:active { background-image: url(data:image/png;base64," +
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAZ0lEQVQ4T2NkoBAwUqifge" +
"oG2AFd1AfERUB8CM11WOXQXXAGSROyITDNMGkTGAPdAHSFIENAAOQqGEBxHbYwQDcE2ScY" +
"XsMViNgMwRYuOGOBIgMo8gLFgUi1aCQ7IZGcNaieF0h2AQCMABwRdsuhtQAAAABJRU5Erk" +
"Jggg==); }\n"
var titlebar = root.appendChild(document.createElement('x-titlebar'));
var closeButton = titlebar.appendChild(document.createElement('button'));
closeButton.className = 'close'
closeButton.addEventListener('click', function() { window.close(); });
var container = root.appendChild(document.createElement('div'));
container.appendChild(document.createElement('content'));
}
PNG
IHDR T T aøR PLTE ÿÿÿÝÏÒ tRNS @æØf `IDATx^íÕ1
0aïç%÷˒SºdZDÈ Å(ozÐám;Ǝ⮙zìYE;CQÌÐÑþ¾û!ÑbWàۚVU`]æ-jÌñ^ëқ&úæj¯5ʄ<o IEND®B`PNG
IHDR Rüa PLTE ÿÿÿmHt çIDATx^íÙÑ1áiËÞÏCõý_©75ԔXb8ÿU,ù¬ÜÄomª¡Èz%¢B ÔS(¡d9Ef)KJtÒ6Iyj]3
Å3©¡ÖÒP øR¸GÍQ|MAuUZ}v¤ äÅ%ÓËSªB7xî£^=ö½ðÞ4ª\û(q®ÔQ>< jý7Å)(¥rJe)IBK(A²J¨ÿnrë. ÕRC¡2$£Ü2×GCRN© Uy 0׃C
åZ%h;o>8üܪ+VÅM IEND®B`PNG
IHDR |
5ô CIDATx^íÖ1 0ÂP$ýs%XèPô}@ÃbRÂ,ó²)s±¤Ð
BéôÃküpB®³ÚS{Y/¡÷ºvP° IEND®B`PNG
IHDR E¢ 4IDATxc ¨éÿ
x'ÿq¾ñfÊüGø
.ð2¡8ÅpÌéú0§ þx4À IEND®B`PNG
IHDR ÊD IDATxcþ 9P]¤ÌÍ IEND®B`PNG
IHDR E¢ 4IDATxcþÿïb 2þCñ7° À;Ñ~¡ü§\ ÓНCvº>Èå xdz¿ IEND®B`PNG
IHDR : : K²£ ºIDATx^íԱ Â@amB,²Whùcr»Ä*5,@ë¤ dµL¬>[Ë;~Ew§{'øADß"J(Q¢Sß>
F0EZ4îíú8oö^zXéȐ{µB¤Q¢鐣ó¬BtP£2pQ6g BÍIÎnÛö"(<sÏöúØ ÖÎ!A½w;ÔHþf<Ä#ñH<Ä#ñH<ҷ"J(Q¢D_¸0l9õ IEND®B`PNG
IHDR : : K²£ ¾IDATx^íԱ
PáZÄoà^H햮q$1Ҽ=f/¨ nÁ+jq¦¯µñ~!÷íáý(Q¢DM
u ,s¤¨Ûúó3ÓóSëÕ¡*lh$F°D°¢Q·FҰ)¢Nê>GiQÑêY{d¨.RtòLëÛ[ÃvïúþJàJçà!ÆѸbx6Gâx$Gâx$iø%J(Ñsáÿ:Ûý IEND®B`PNG
IHDR : : K²£ ¢IDATx^íÔ1
Â@@QµÆ"] °EN³gg±ðZç@
Úæ"¶)«±µÜa>ÿ ¯ûûA o.kû*IթïõX=ºÃԛÆÐԎÞ´£¥ *³9jñ¢ZÂh>{Ñ|¢ëÛ6_êAyæë}cCcí«ÛÙ`Íß́#q$đ8GâH#}+PPPPPÐeCw8Ê5 IEND®B`PNG
IHDR 0 0 ý1 9IDATx^í=RÃ0
¸ÃÜQ¤JöG¶)ÜÂLBÍ85T\%ÃÄCP ì&ÒµµOo÷(HäШS`à:a-Uù¥Ä×CErÆáJ]ϸáÆ8røKÙyhù*þ`;ygI`ùçD¼îäÖÓqh猠 ÏN¾%=; oÌ-¹ß4¯ÓàÆÔ%}yy¶´Èc²ßâ}}*`LZîîq\þi¬ïLgLjp"ð¬°4qèùÁw|ŷ¼ԣ}kq;3ÜüCñ+
Ðdê½MÅmõC_B,*Èêea§ò>ìÄâÚ[ìãZlàSu!62¡U¹ï
ØÐ÷SY=Nd×X©+ÑÅ
¾\5ù6G"Ã:uPîu{w IEND®B`PNG
IHDR <º£R IDATx^í]mlÕ+`ÁvmyY¥¨ÀeúÁmèâdS²`²Mãb|ý¢1~Xh$#n¾ÄDP
6Êxµu-¥ii¹@_n齷¼"¡À}öÿmç<=÷>}ÚÛ{¹÷9=&ÿ`Ûóÿïÿù¿ç<ßsÇS¬Xù_7Hzb±
æ?AÉ÷]7q?Ã~ªш!ñ'BމãÇgv<yw$ù]4ýs,Ý(²GÚ+Ò-Ò+Ò/â(éW¿ëF´Å3x} /ô¾uâXä6!ò:ÚÛˣÈcèZ}Ҧ gHz0Ƥá¼,A²Loº¾»»{©ô¶´iI䩞çì3N__300à\¼xѹté388è\¹rŹzõ*ĉÇãþ¿¡
Úâ<>Ðúô"Mæ9anא, $ÅÁúú)îîećÒæ ӧN9}çÎ9.\ ° gBÐ7ÆÀXcëd99b®3Éb Fqoápxü å8ÞÞ^XX MÁ0ÌI'K7æ5¸·!K4CÂûÅZl6q*¼÷ìY á\¾|Àä¢`n#æê&JkZ|b âCbÅê]ʅ ³ %3æî^Ö&küÖJ¢XÀb(§³S8pþ<É@րµ`M\֊5kÅD#G^W8¼PµÃM ÀDÁÚt¢TB(Æ=Ah5%ðisÈ 1k=}ú4r ºNhMÌ&¿ºXڄh5úûúRÙJØzúú(!GÖl
b5Ö«g¨JçstàãÐtEMúáÎÎE1ÔAh5úû5 ¬@'ÔtÑ71 zè*ÞúJi3À\grË
tãΡ@wîØ{÷îÍsù&Ì&ó#À
tåÞrނ.I`DR%ے¦-Ån9ÑèVè$ "AHÅ(C"áë`ʎ¨è:dr
º¥_èä+V^c¯X\n¡c$!9ÚÛÚæËÚ錦¯ìnº¤ó
C×$INäh;qbLü8p®·7ý/+Ð)tKÎI$Éq,*ã¶қirX@Ç$I+tOäAHoJèÂ^rX@×t\Ia(»k)Ñh´éµõ9¬OBÇ ÀÙ"!CÙìD+6ºa, IM`NvuýÒì¦ÎmjW`lQ6Bë1±�ä\Ôf\/Hø{0Vה tJ¿®©)ÇèpnÖVlíØ #:'îwD"¿2bÉ- ¬0²ֹý~OX¿Ãú#ÕCôG2Jn-êêJ)
ßaý`̸Õd ÜZº»ßd¦4·°ÂL+0ãV)y¤zøcpP$gHíWn5À˴[N
Uòôy °¬@«ÜjÒF:¦˙-
VÅÖketXÓEZ)WÀ {©É
0cn¤XҊ tL%Túq`
zëÂtº²è<°ïÊZv´"iø BëÑÑѱ
|ÆÔ
0Vď ¾¾#\[`´ CF4/2:0r9zôèíL©¹؈)x`K+2j0ïэ¬©ÕZ[ílßb^dTaÖô/¾(ÉffMmvØcfWGCë:;:åüÇyí5ÇyôQÇY¼ØqV¬ÀÏN|ÿ~'.&Ùäã ñ£ÀûDm§Jç£cp{¤ð5kxO±!/0ÖyGJI»wïÁKj<,UNçÉ'Ayâ ´7î3/ûÖÀ|Dáö"÷ÿÒà>¶?RA{c kn3ѷñp×»½´µôQKüØ1#ë3ÀڽÍø¦¦@ұ!cnޜA6n4òX"°æÀ~Xp{i:|xËúFn/3AuëÓæ¹Íx©u }Vã!|ÁÀH¼ýv*Ás¦é3Ü]ÍԻAè\/÷s®ÇCø̅Ù±ÆÀ{ú!^ÁTUoè'\e blVU0ÿ°þÇæ͛åóÅ9Kswç=ý°8'·çý9}ڣSKÓXs`G)íííñê¨ñAK^eì騺 2ÄA
Ã/x¾1g bôxÀÞ� Cdâͮó9{j bháØ3a E
¥º·ñÐqAK`ÍÊî¿T$3yAHE#¯|Yê{pL2K<Üâ[±1²&ìÁ/äàË46öBK]5v1p \Ð GÄb±~$%¡É2`ï"HNĿӤá@ ²{÷P9t( kצ ëc9: Q
k/
êóÏ'¶ùàÔÈs }½ô»
Ƥވ»A¦ +WlժÄv_Aª«Ç[½ÚÝã Ó}ì-ÆyñšÝ¿×_úº|GEp둝$Úî
½Åø$ðNjí#^Uخ±Ñq, 9ÐíûøòKm(¸Nª/Aæ9ðÓO'o»w/}/ÁßÑ.¹µzöY½=ÆtëGi$Êàw$·gOòö]]óÆÖ?¿þº½ɕ¸
(ób"HuàSí[·&÷È#ÿ!ãØF¦â_·ô"Պ$üüó@§ځ½'A(w$¶¾X78ü¹§¢Úº;ÌÁÅï0n u}
(KLµwuuý͈r}="äÛ@[[j}?x<