Перейти к содержанию

Проверка сообщений между контекстами

Оглавление серии

Чтение вкладки возвращает название, адрес и цитату, а popup добавляет собственную заметку. Прежде чем сохранять объект, определим договор сообщения. Результат главы — worker принимает только PREVIEW версии 1 от собственного popup, проверяет поля и возвращает безопасную структуру. Данные ещё не пишутся в storage: отдельный этап проверки помогает понять границу полномочий до появления постоянного эффекта.

Сообщение между контекстами не является доверенным лишь потому, что runtime передал его обработчику. Нужно проверить, кто отправил команду, что она означает и какие данные несёт. Особенно опасен универсальный обработчик, которому можно передать имя произвольного API или адрес для сетевого запроса. В «Заметках читателя» такого механизма нет. Получатель имеет короткий список известных действий и не исполняет инструкции страницы как код расширения.

Отправитель и версия договора

Worker сравнивает sender.id с chrome.runtime.id. Затем проверяет точное sender.url, равное runtime.getURL("popup.html"). Эти сведения предоставляет браузер, а не объект payload. Если клиент положит строку popup.html внутрь message, это не заменит реальный sender. Такая проверка различает собственную страницу интерфейса и content script во вкладке. Последний не получает право самостоятельно записывать privileged состояние по произвольной команде.

Version=1 относится к протоколу сообщения. Type="PREVIEW" выбирает известную операцию. Иная версия или type отклоняется с ok=false. Не вызываем chrome[message.api] и не используем eval. Даже собственный popup должен говорить на конкретном языке команд. Когда появятся LIST, SAVE и настройки, каждая ветка будет иметь собственную проверку формы данных. Версия пакета в manifest и версия сообщения остаются разными числами с разным назначением.

Ответ имеет оболочку ok и data либо error. Это удобный договор для известных предметных отказов, который не зависит от меняющихся правил передачи исключений разных версий браузера. Мы всё равно ловим отказ Promise у отправителя, поскольку канал может не завершиться нормально. Известное ok=false и техническое отклонение sendMessage не равны: первое сформировал наш обработчик, второе может произойти на уровне доставки или контекста.

Полные файлы шага

Показанные файлы полностью совпадают с выбранным состоянием архива. Остальные согласованные файлы находятся в examples/browser-extension-lab/lesson-05/.

data.js

export function validateNote(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new Error("Ожидался объект заметки");
  }
  for (const [key, max] of [["title", 200], ["quote", 2000], ["note", 2000]]) {
    if (typeof value[key] !== "string" || value[key].length > max) {
      throw new Error(`Некорректное поле ${key}`);
    }
  }
  if (typeof value.url !== "string" || value.url.length > 4096) {
    throw new Error("Некорректный адрес");
  }
  const url = new URL(value.url);
  if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
    throw new Error("Поддерживаются обычные веб-страницы без данных входа");
  }
  url.hash = "";
  return { title: value.title, url: url.href, quote: value.quote, note: value.note };
}


export function validateStored(value) {
  const note = validateNote(value);
  if (typeof value.id !== "string" ||
      !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
        .test(value.id)) {
    throw new Error("Некорректный идентификатор");
  }
  if (typeof value.createdAt !== "string" ||
      !Number.isFinite(Date.parse(value.createdAt))) {
    throw new Error("Некорректная дата");
  }
  return { ...note, id: value.id, createdAt: new Date(value.createdAt).toISOString() };
}

worker.js

import { validateNote } from "./data.js";

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (sender.id !== chrome.runtime.id ||
      sender.url !== chrome.runtime.getURL("popup.html") ||
      message?.version !== 1 || message?.type !== "PREVIEW") {
    sendResponse({ ok: false, error: "Команда отклонена" });
    return false;
  }
  try { sendResponse({ ok: true, data: validateNote(message.payload) }); }
  catch (error) { sendResponse({ ok: false, error: error.message }); }
  return false;
});

Ожидаемый результат

Это ожидаемый договор для самостоятельного исследования; расширение не устанавливалось и не запускалось.

PREVIEW собственного popup с версией 1 → ok=true и нормализованные поля.
Неверный отправитель, type или данные → ok=false.
javascript: и URL с username/password отклоняются.

Поля проходят собственную проверку

validateNote сначала требует обычный объект, исключая null и массив. Затем проверяет title, quote и note как строки с конкретными пределами длины. Ограничения нужны не только из-за размера: они задают форму записи, с которой дальше работает интерфейс. Число вместо title не превращаем молча в строку, потому что импорт и сообщения должны следовать ясному договору. Читатель тогда может объяснить причину отказа, а не гадать, почему объект изменился.

url имеет отдельную проверку типа и длины. new URL разбирает адрес; разрешены только http и https без username/password. Поэтому строка javascript: не становится кликабельным действием, а адрес с встроенными учётными данными не проходит нормализацию. Hash удаляется, потому что выбранный формат хранит адрес страницы без фрагмента. Query пока остаётся; его обработку отдельно разберём в главе о приватности, не приписывая этому валидатору распознавание секретов.

Возвращаем новый объект из известных полей. Лишние свойства входа не копируются spread-операцией в окончательный результат. Например, page может случайно содержать html или имя будущего API, но validateNote не включает их в нормализованную форму. Этот приём одновременно документирует выход и удерживает ненужные данные за границей. Он не отменяет проверку отправителя: безопасная проекция содержания и право отправить команду относятся к разным уровням.

Валидация хранённой формы

В data.js также объявлен validateStored. Он понадобится при чтении и импорте: сохранённая заметка дополнительно содержит UUID версии 4 и createdAt. Обычное намерение не выбирает эти значения, их назначит worker в следующем шаге. Разделение двух функций показывает, почему вход SAVE и объект storage не должны слепо совпадать. Пользователь вводит содержание; расширение добавляет идентичность и время создания. PREVIEW пока использует только validateNote.

Дата в validateStored проверяется как разбираемая строка. Это умеренно строгий учебный договор, а не проверка всех возможных календарных требований. При добавлении импорта не станем исполнять строку даты или использовать её как путь файла. UUID проверяется отдельным выражением и дальше служит частью ключа storage. Так предметные значения не получают дополнительных технических полномочий только из-за того, что находятся в JSON.

Ожидаемый сценарий PREVIEW возвращает нормализованные title, url, quote и note. Неверная длина или неподдерживаемый протокол возвращают ok=false, и popup показывает отказ. Неизвестная команда также не сохраняет ничего. Если сайт добавил разметку в заголовок, она остаётся строкой; последующий интерфейс продолжает использовать textContent. Здесь нет мостов к window.postMessage и onMessageExternal, поэтому страница не получает универсальный вход к возможностям расширения.

Проверка перед эффектом

Полные data.js и worker.js на этой странице показывают договор целиком. Состояние lesson-05 также содержит popup, который отправляет PREVIEW только после результата executeScript и добавляет текущий noteInput. Автор эти сообщения не отправлял в установленном расширении; описанные ответы ожидаемые. Для самостоятельного разбора найдите три независимых отказа: неверный отправитель, неизвестный type, неправильный url. Каждый возникает до постоянной записи, которой пока нет.

Такой порядок полезен не только для безопасности, но и для ясного объяснения. Если сначала записывать сырой payload, а затем пытаться привести его к нормальной форме при выводе, storage накопит несовместимые записи. Получатель должен определить допустимый объект до эффекта. Следующая глава заменит PREVIEW на SAVE, добавит локальную запись и покажет, почему глобальная очередь текущего worker может согласовывать операции, но не хранить заметки после его завершения.

Оболочка версии проверяется до содержательного разбора payload. Такой порядок полезен при развитии формата: неизвестный протокол не должен случайно попасть в ветку старого SAVE с иными полями. Однако одно число не объясняет всю совместимость. Получатель обязан иметь определения поддерживаемых команд и известные пределы каждого поля. В этом шаге поддерживается только PREVIEW, поэтому успешный ответ не означает разрешение любой будущей операции. Клиентский текст не может повысить себе полномочия добавлением свойства с названием новой команды внутрь обычной заметки.

Документация и исходники

Механизмы главы сверены с первичными источниками: документация 1, документация 2, документация 3. Полный архив «Заметок читателя» содержит все двенадцать состояний. Установка, запуск и публикация не выполнялись.