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 uX |}Y }ëY ~AZ Z ßZ x[ \ ©\ q] Ðd l «r :s p| m~ ¨ Ն B æ ! \ Ĝ ¬£ ¥ "ª ® à± µ ¸ ¼ ð¿ <Ê wÎ Ñ Õ Ø ~Ü ¡¹ß ¢§á £ê ¤Yó ¥>ú ¦·ÿ §¼ ¨¤ © ªý «# ¬ ø ®h ¯J °À ±c ²á ³u ´ µ ¶ · ¸' ¹ º(! »¥" ¼7$ ½´% ¾H' ¿¸( ÀY) Ák+ Â- ñ/ ÄW1 Å3< Æ%? Çô? ÈnE ÉîJ ÊPK ËÌK ÌLL Í M ÎïM ÏnN ÐéN ÑNO ÒÕO Ó`P Ô6Q ÕR ÖÚR ׿S زT ÙU ÚkV ÛgX Ü<Y ÝZ ÞæZ ß»[ à\ áe] âE_ ã#a äc å×c æ¬d çe è< é êʘ ë ì~ íã îa ïԝ ð* ñ ò% óN ô+¡ õü¡ ö¢ ÷^£ ø¤ ù¤ ú¥ û¡¥ ü¦ ý¦¦ þ|§ ÿR¨ ݨ ³© ª ¼« ⬠õ® ʰ .² ³ vÄ ±Û Ü Ü Ý yÝ ÓÝ &Þ |Þ ÍÞ ß dß ©ß ÿß Dà à Îà á Xá á âá 'â lâ ±â !öâ ";ã #ã $Åã % ä &Oä 'ä (Ùä )å *cå +¨å ,íå -2æ .wæ /¼æ 0ç 1Fç 2ç 3Ðç 4è 5Zè 6è 7äè 8)é 9né :³é ;ùé <Eê =ê >Ôê ?ë @`ë A¦ë Bóë C>ì Dì EÐì F í Gcí H¦í Iéí J9î Kî LÕî M ï Nmï O¶ï P ð QKð Rð Sáð T,ñ Uxñ VÃñ Wò XYò Y£ò Zîò [9ó \ó ]Ïó ^ô _fô `³ô aüô bFõ cõ dÜõ e'ö frö g©÷ hø iù jk kz lø mý å <!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,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" /></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,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAFo0lEQVR4Xu3cS1OTVxwG8Ha6dsZNt/0S7ozX+wUSGKN7ycIvkJ2OiNcdbvwMfABXLS1VvLXFSMWUgFAh1oJICCEGq8UFp8+fPu87J4S3vrmcvIfOceaZMKOSnN/8z/+c95Yv3B8XFxcXFxcXFzNRZ89+rZLJJNKLDCAZdfp0Hini5zWJ/Izk5e+QAf7bpPzf/yvKLkD0I1lArCOqwazL70D65Xdub5RUagcA0kDJCYCJ8Hen5b22D8y5czvVmTN9gCkRwHzwXvKe8t72wij1JWB6AFMIBDAPVZDPIJ/Fth7zDWAeESD64LPIZ7ICBwAxZBFRlmURiUWLk0ymuCwrG8NtQyqKKfUVYG4RwP7gs8pnbhsOAO4gapvlTluQzFaO+Uoy3nNMAayurlalcOqUKaSUudUKTa9dQN/t3m0EiWOItX6fw6W8nUCDyFJHh5ktAMbUuh0yN4HtBPpWgGIxNYQUTSBhTC3ZccvW3QNoawUR6Afk7p49ZpAwtuYPPHlsFUkFEehHAA0jy61GwthkjM1UTx8BIq2gIeDcQ+7v3atKnZ2trqK+xs/n8JRFVBUEJL+CBGgYQD/t369WWomEMTZ0PgkAaQ8g6ik2RJyH+/apxwD6BSm3tpLS9QPxTGDUFTQkTZrT6wGApIJGkKcHD6p38XjLzkzWfw6ZADb0oHvAEaBHAPpZKohAv7YSCWOup3r6CRBpBX3v9R+pHuSxAB04oDKI4EjGDh1qDRLGXA9Q1iagYQFigxagJwR6BpwxptIsEsYc+roVANajBhpkcxag+3qDBs5TRMd5jowfPtws0rqMPUz1JAkQbQ8i0F1vBWP/GQHOqEwvgWGywPmNWW0GCWMPA9RrA9Cg339YPQR6AhwBGtuEM47kjhxRE0jDSBh7GKABC4CqGzSBZHplNCDA6PGBXhw9qt4nEo0ADYQBytgCJLvnB97yLs2ZQM+Q59WV4+NM8nWqESSMPcwOOm8BEA8v2KC5QcxIgxYgTq0sgSY8HFaP4Ewjvx87Vi9SPgxQ0QYgHqD6q9cIK2jU7z2sHIY4NUAzyF/hkYphptiaDUBjFy6oYa//aLtnArHn1FaOjvMSkdfZsEgY+7YBqpTLKnvxonrI/jPiTS/ijBPHAxKYFxrONDLD5I8f33j9AKQQQHZOsaVr1wSlBmkcSN7hxagGlPtM5Xg4swR6xXwGqWhnk2aWr1/fEmni0iUfiNPLX84nQ+L8wbw+ceK/kPKGlnnzSFNA8nbPOQBNalOKOEw1DiMwfuaQj11dDS7z3ChGmVIA0nRvr8oCaKPvbMJ5ycxqOLObKudP4syfPCmpRcLYDRxqmMlKANLM5cvSf/zqmUI4rQik9ZwAnDnkDbKA/K0jYeyhD1ZtRnoFJKxSWuVwWjHEERjiyCsrR8N5iyu3i4iPhLEbON1hNuUApNdXrgAooHJqgHQchDiSApE+dXXxdIeBE2am8+7GjS2R5oBU1ZCDp5WfBR2HweVtueaWNXDKNXqkeSARRlutiMP404pZJJCHU0RwKanf8El786kEIL0BEisnsOfoOJICIzjLSLmjY5fFl33CZ/XmzSCkwMqZD5hWgkOgnIELh/Yhvb16Vc0RqXa1Ympx1EoikTZw6TnavA9AWujpERgdh0B+5bAp+72npBKJHQZuXrAPqXD7dlDlEIdVw+BGCKmePgO3v9iHBJzaymF0nCUNB9f3C6q7e6eBG6jsyofz57V9DrOpIRc0GMkKUuns7DFwC56dwYGnvpTX9hzGwynF47wFz8BNnLYGx1ScUn64WlXjlOPxxY8Yk6HbgO1H0nsOo+OsVRKJmNEbyW3PGpAAhGg9hwFOyj2KQCR/WjG4TeaWe5hlE5LAlBG8bvUwi3sc6lN390blGMBxD9S5RzINxD3U6x4Ld18s4L6awt64LzdxX4/zDxj9/IEueAvhAAAAAElFTkSuQmCC) 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 * * oÊÐ `IDATx^í×1 CÑÜÿÒ nò. ´3(¥¼Aé $ހìÉC;Ëߎ DŽ,`ºíN3QlÃyJ ÝÿúAõg uJ!îC7 £'*膟.Msß* IEND®B`PNG IHDR J A ÌuÏ µIDATx^µÓQj1@QÁ¸î-º!áý ·¯#©#¥ÓK¾($y ,;Ùltó¿YvÓÙBz¦«ÀOó(#DQ6«&«}úɝE'¥Ú̲®¦°Ú,9VF|۲á&ÓÑòï&Ýä~¯«½!jY͑ Èa¶¼RoFZ7'Ê!fg¿7m¼>wÓ%Zëá§Ùa--4 çȿõޤI]Ï IEND®B`PNG IHDR >