Введение

Коллега отправляет две версии контракта и просит сравнить их. Вы загружаете оба файла в сервис сравнения, получаете результат, и всё выглядит нормально. То, чего вы не видели, — один из документов содержит связанную картинку, указывающую на URL, и ваш сервер обращается к этому хосту в момент открытия файла. Никакая часть вывода не сообщает, что это произошло.

Это не дефект — таково корректное загрузка документа. Файл OOXML может ссылаться на изображение, находящееся на веб‑сервере, а не внутри пакета, и как Word, так и любая библиотека, правильно загружающая документ, разрешает эту ссылку. GroupDocs.Comparison for .NET предоставляет два свойства в LoadOptions, позволяющих решить, будет ли это происходить: SkipExternalResources и WhitelistedResources.

Вместе они дают три конфигурации, и в этой статье сравниваются все три — разрешающая по умолчанию, блокирующая всё и блокирующая всё, кроме указанных ссылок. К концу вы узнаете, какую выбрать для конкретного источника документа, а также две ошибки, из‑за которых эти настройки могут казаться неработающими.

💡 Полный рабочий пример: block-external-resources-on-document-load-dotnet — исполняемый консольный проект, который сам обслуживает запрашиваемые изображения и регистрирует каждый запрос, чтобы вы могли увидеть, как срабатывает каждая настройка.

Где скрываются внешние ссылки

Прежде чем выбрать настройку, стоит понять, что именно вы выбираете. Файл .docx содержит внешние ссылки в двух разных местах, и их легко пропустить, потому что ни одна из них не видна в тексте документа.

Первое — отношение в word/_rels/document.xml.rels с TargetMode="External" и абсолютным URL. Изображение появляется в теле как рисунок, который ссылается на отношение по ID, поэтому сам URL никогда не появляется рядом с тем содержимым, которое он влияет.

Второе — поле кода INCLUDEPICTURE в теле документа, где URL хранится внутри инструкции поля. Word разрешает его при рендеринге страницы; библиотека сравнения разрешает его при загрузке документа.

Оба механизма учитывают два параметра загрузки, обсуждаемых ниже, что важно, потому что документ может использовать любой из них или оба. Ссылка, которую вы заметили в файле отношений, не является доказательством отсутствия второй ссылки в поле кода.

Подход 1: По умолчанию — ссылки разрешаются

SkipExternalResources по умолчанию имеет значение false, поэтому документ, загруженный без дополнительной конфигурации, будет разрешать свои удалённые ссылки:

LoadOptions loadOptions = new LoadOptions
{
    SkipExternalResources = false
};

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

Это обеспечивает наивысшую точность: сравниваемые документы содержат всё, на что они ссылаются, точно так же, как их отобразит Word. Для документов, созданных вашим приложением или шаблонами, где каждый URL указывает на инфраструктуру, которой вы управляете, это правильный выбор — и отсутствие связанного изображения может сделать сравнение вводящим в заблуждение.

Стоимость такого подхода в том, что каждый ресурс запрашивается, независимо от того, кто его разместил. Есть также временной расход, не связанный с доверием: URL, который больше не разрешается, заставит загрузку ждать завершения полной попытки соединения при каждом сравнении.

Подход 2: Блокировать каждый внешний ресурс

Одно свойство отключает разрешение удалённых ссылок для данного документа:

LoadOptions loadOptions = new LoadOptions
{
    SkipExternalResources = true
};

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

Никакой запрос не отправляется. Ссылающиеся изображения отсутствуют в результате, и — это важно уточнить — ничего больше не меняется. Настройка управляет тем, что загружается, а не тем, как находятся различия, поэтому текстовые и структурные изменения между двумя документами обнаруживаются точно так же, как и раньше. Единственное, что вы теряете, — возможность обнаружить изменение внутри связанного изображения, которое никогда не загружалось.

Эта конфигурация должна служить базовым уровнем для документов, которые вы не создавали: загрузки пользователями в веб‑приложении, файлы, полученные по электронной почте, любые сравнения на сборочном агенте, где исходящий запрос почти никогда не нужен. Это «всё или ничего», однако — нужное вам связанное изображение будет заблокировано вместе со всеми остальными, и результат просто будет его без указания причины.

Подход 3: Блокировать всё, кроме указанных ссылок

Третья конфигурация — самая избирательная. WhitelistedResources принимает List<string> и учитывается только когда SkipExternalResources равно true:

LoadOptions loadOptions = new LoadOptions
{
    SkipExternalResources = true,
    WhitelistedResources = new List<string> { "includepicture-field.png" }
};

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

Элементы списка — фрагменты URL, а не имена файлов. Каждый из них сравнивается с URL ссылки, и совпадение в любой части URL допускает этот ресурс. Именно это делает «белый список» переносимым: "includepicture-field.png" допускает изображение независимо от схемы, хоста и пути, предшествующего ему, так что один и тот же список работает и в разработке, и в продакшене без правок.

Тот же параметр работает и в обратную сторону. Краткий или слишком общий фрагмент — logo.png или, что ещё хуже, .png — может совпасть с ссылками, которые вы не собирались разрешать. Выберите фрагмент, достаточно специфичный, чтобы однозначно идентифицировать нужный ресурс.

В примере ссылки эта конфигурация загружает изображение из белого списка и оставляет второе ссылочное изображение, которое не покрывается ни одним элементом списка, заблокированным. Журнал запросов показывает три запроса, тогда как разрешающая конфигурация по умолчанию генерировала пять, и выводятся только имена файлов из белого списка.

Какую конфигурацию выбрать?

Подберите настройку в зависимости от источника документа. Документы, созданные вашим приложением или шаблонами, могут оставаться с настройкой по умолчанию, потому что каждый URL указывает на инфраструктуру, которой вы уже управляете. Всё, что поступает извне — загрузки пользователями, вложения электронной почты, файлы третьих сторон — требует SkipExternalResources = true. Добавляйте узкий фрагмент в WhitelistedResources только тогда, когда действительно нужен один доверенный ресурс.

Сравнение трёх вариантов

Пункт По умолчанию Блокировать всё Блокировать + белый список
Свойства для установки 0 1 2
Исходящие запросы все ссылки нет только из белого списка
Управление каждой ссылкой нет нет есть
Нерабочий URL увеличивает время загрузки да нет только из белого списка
Лучшее применение документы, созданные вами документы из любого другого источника доверенные шаблоны среди недоверенного контента

Решение зависит от происхождения документа, а не от производительности. Документы, созданные вашими системами, могут оставаться с настройкой по умолчанию. Документы из внешних источников требуют блокировки. Белый список используйте только в том случае, когда конкретная ссылка действительно должна разрешаться — например, корпоративный шаблон, берущий заголовочное изображение из внутреннего URL, среди отчётов, где авторы вставляли картинки откуда угодно.

Две ошибки

Обе эти ошибки дают одинаковый симптом: вы задаёте параметр, а он, кажется, ничего не делает.

Белый список без переключателя. WhitelistedResources учитывается только когда SkipExternalResources равно true. Если задать его отдельно, он ничего не делает — нет блокировки, к которой можно было бы сделать исключение. Если кажется, что белый список игнорируется, проверьте это в первую очередь.

Параметры только для исходного документа. Это более тонкая ошибка. Параметры загрузки описывают, как один документ загружается. Конструктор Comparer принимает параметры для исходного документа; каждый вызов Add() принимает параметры для соответствующего целевого документа:

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

Если передать их только в конструктор и забыть про Add(), то исходный документ будет защищён, а каждый целевой всё равно будет запрашивать свои ссылки. Сравнение проходит, результат выглядит правдоподобно, но половина ваших документов всё ещё обращается к сети. Когда источнику и цели требуется разное обращение, передавайте отдельные экземпляры LoadOptions — именно для этого API принимает их по документу.

Как убедиться, что всё работает

Заблокированный ресурс оставляет почти никаких следов. В результирующем документе просто отсутствует изображение, что выглядит как документ, у которого его изначально не было. Поэтому чтение результата — плохой способ подтвердить, что настройка сработала.

Наблюдайте за стороной, обслуживающей запросы. Пример ссылки специально использует такой подход: он запускает небольшой HTTP‑слушатель на свободном порту loopback, пишет демонстрационные документы, указывающие на этот порт, и регистрирует каждый полученный запрос, выводя количество запросов на каждое сравнение. Пять запросов, затем ноль, затем три. Сетевой трассировщик против реальных источников ваших документов даст такой же уровень уверенности.

Заключение

Три конфигурации, одно правило выбора: оставляйте документы, созданные вами, с настройкой по умолчанию, ставьте SkipExternalResources = true для всего остального и добавляйте узкий фрагмент URL в белый список только там, где конкретная доверенная ссылка всё ещё должна разрешаться.

Затем проверьте две вещи, которые тихо отменяют вашу работу — белый список без SkipExternalResources = true и параметры, переданные в конструктор Comparer, но не в каждый вызов Add() — и проверяйте всё со стороны сервера, а не из выходного файла.

Дополнительные ресурсы