Как поиск вписывается в интерфейс RESTful?


137

При разработке интерфейса RESTful семантика типов запросов считается жизненно важной для проекта.

  • GET - список коллекции или получить элемент
  • PUT - заменить коллекцию или элемент
  • POST - Создать коллекцию или элемент
  • УДАЛИТЬ - Ну, ну, удалить коллекцию или элемент

Тем не менее, это не похоже на понятие «поиск».

Например, при разработке набора веб-служб, поддерживающих сайт поиска работы, у вас могут быть следующие требования:

  • Получить индивидуальную работу объявления
    • GET дляdomain/Job/{id}/
  • Создать объявление о работе
    • POST вdomain/Job/
  • Обновить вакансию
    • Положить наdomain/Job/
  • Удалить объявление о работе
    • УДАЛИТЬ кdomain/Job/

«Получить все рабочие места» также просто:

  • GET дляdomain/Jobs/

Однако как «поиск» попадает в эту структуру?

Вы можете утверждать, что это вариант «коллекции списков» и реализовывать как:

  • GET дляdomain/Jobs/

Однако поиск может быть сложным, и вполне возможно произвести поиск, который генерирует длинную строку GET. То есть, ссылаясь на вопрос SO здесь , возникают проблемы с использованием строк GET длиннее, чем около 2000 символов.

Примером может служить граненый поиск - продолжение примера «работа».

Я могу разрешить поиск по аспектам - «Технология», «Должность», «Дисциплина», а также ключевые слова в свободном тексте, возраст работы, местоположение и зарплата.

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

Настройте этот пример на резюме, а не на вакансии, добавьте еще больше граней, и вы можете легко представить поиск с выбранной сотней граней или даже с 40 гранями, каждая из которых имеет длину 50 символов (например, названия должностей, названия университетов, Имена работодателя).

В этой ситуации может быть желательно переместить PUT или POST, чтобы обеспечить правильную отправку данных поиска. Например:

  • POST вdomain/Jobs/

Но семантически это инструкция по созданию коллекции.

Вы могли бы также сказать , что вы будете выражать это как создание поиска:

  • POST вdomain/Jobs/Search/

или (как предложено ниже)

  • POST вdomain/JobSearch/

С семантической точки зрения это может иметь смысл, но вы на самом деле ничего не создаете, вы делаете запрос на данные.

Итак, семантически это GET , но GET не гарантирует поддержку того, что вам нужно.

Итак, вопрос в том, чтобы попытаться сохранить как можно более верный дизайн RESTful, и в то же время убедиться, что я соблюдаю ограничения HTTP, каков наиболее подходящий дизайн для поиска?


3
Я часто собираюсь использовать GET domain/Jobs?keyword={keyword} . Это прекрасно работает для меня :) Я надеюсь, что SEARCHглагол станет стандартом. programmers.stackexchange.com/questions/233158/…
Кнерд

Да, я вижу, что для тривиального примера нет проблемы. Но в инструменте, который мы создаем, на самом деле не так уж невероятно, что мы в конечном итоге получим сложный поиск, в результате которого строка GET будет длиннее 2000 символов. Что тогда?
Роб Бэйли

На самом деле очень хороший момент. Как насчет указания технологии сжатия?
Кнерд

2
GET с телом разрешен спецификацией HTTP, может поддерживаться или не поддерживаться промежуточным программным обеспечением (иногда нет);) и не рекомендуется как практика. Это периодически появляется на Stackexchange. stackoverflow.com/questions/978061/http-get-with-request-body
Роб

2
Я закончил тем, что POST JobSearch создал реальную сущность поиска и возвратил jobSearchId. Затем GET jobs? JobSearch = jobSearchId возвращает фактическую коллекцию вакансий.
Cerad

Ответы:


93

Не следует забывать, что запросы GET имеют некоторые превосходящие преимущества перед другими решениями:

1) GET-запросы можно скопировать из строки URL, они перевариваются поисковыми системами, они «дружественные». Где «дружественный» означает, что обычно запрос GET не должен ничего изменять внутри вашего приложения (идемпотент) . Это стандартный случай для поиска.

2) Все эти концепции очень важны не только с точки зрения пользователя и поисковой системы, но и с точки зрения архитектуры и дизайна API .

3) Если вы создадите обходной путь с помощью POST / PUT, у вас будут проблемы, о которых вы сейчас не думаете. Например, в случае браузера нажмите кнопку возврата / обновить страницу / историю. Конечно, это можно решить, но это будет другой обходной путь, потом еще и еще ...

Учитывая все это, мой совет будет:

а) Вы должны уметь вписываться в ваш GET, используя умную структуру параметров . В крайнем случае, вы даже можете использовать такую ​​тактику, как поиск в Google, где я установил множество параметров, но это очень короткий URL.

б) Создайте еще одну сущность в вашем приложении, например, JobSearch . Предполагая, что у вас есть так много вариантов, вполне вероятно, что вам нужно будет также сохранить эти запросы и управлять ими, так что это просто очистит ваше приложение. Вы можете работать с объектами JobSearch как с единым целым, то есть вы можете протестировать его / использовать его проще .


Лично я попытался бы бороться со всеми моими когтями, чтобы сделать это с a), и когда вся надежда была потеряна, я отполз со слезами на глазах к варианту b) .


4
Для пояснения этот вопрос предназначен для дизайна веб-сервисов, а не дизайна веб-сайта. Таким образом, хотя поведение браузера представляет интерес для более широкой области интерпретации вопроса, в описанном конкретном случае оно не имеет значения. (хотя интересный момент).
Роб Бэйли

@RobBaillie Ye браузер был просто случай использования. Я хотел бы выразить тот факт, что ваш поиск в целом представлен строкой URL. Который имеет большой комфорт в удобстве использования наряду с другими пунктами позже в ответе.
p1100i

В точке Ь, это простое изменение моей ссылки на POST к domain/Jobs/Search/, может быть , с domain/JobsSearch/вместо этого, или же вы имеете в виду что - то другое? Вы можете уточнить?
Роб Бэйли

7
Почему у меня создается впечатление, что REST довольно часто является частью проблемы, а не частью решения?
JensG

1
«Запрос GET не должен ничего изменять внутри вашего приложения (идемпотент)», в то время как GET является идемпотентом, соответствующее слово « безопасно » здесь. Идемпотент означает, что выполнение GET для ресурса дважды аналогично выполнению GET для этого ресурса один раз. PUT также идемпотентен, но не безопасен, например.
Jasmijn

12

TL; DR: GET для фильтрации, POST для поиска

Я делаю различие между фильтрацией результатов из списка коллекции и сложным поиском. Лакмусовый тест, который я использую, в основном, если мне нужно больше, чем фильтрация (положительный, отрицательный или диапазон), я считаю это более сложным поиском, требующим POST.

Это имеет тенденцию быть усиленным, думая о том, что будет возвращено. Я обычно использую только GET, если ресурс имеет в основном полный жизненный цикл (PUT, DELETE, GET, collection GET) . Обычно в коллекции GET я возвращаю список URI, которые являются ресурсами REST, которые составляют эту коллекцию. В сложном запросе я могу извлекать данные из нескольких ресурсов, чтобы создать ответ (например, SQL-соединение), поэтому я не буду отправлять обратно URI, а только реальные данные. Проблема в том, что данные не будут представлены в ресурсе, поэтому мне всегда придется возвращать данные. Мне кажется, это очевидный случай, когда требуется POST.

-

Это было некоторое время, и мой оригинальный пост был немного небрежным, поэтому я думал, что буду обновлять.

GET - это интуитивно понятный выбор для возврата большинства видов данных, коллекций ресурсов REST, структурированных данных ресурса, даже единичных полезных нагрузок (изображений, документов и т. Д.).

POST - это метод catchall для всего, что не подходит под GET, PUT, DELETE и т. Д.

На данный момент я думаю, что простой поиск, фильтрация имеют смысл с помощью GET. Сложный поиск зависит от личных предпочтений, особенно если вы добавляете функции агрегации, взаимные корреляции (объединения), реформаторы и т. Д. Я бы сказал, что параметры GET не должны быть слишком длинными, и это большой запрос (однако он структурирован) ) часто может иметь больше смысла как тело запроса POST.

Я также рассматриваю опыт использования API. Как правило, я хочу сделать большинство методов максимально простыми и интуитивно понятными. Я вставлю вызовы, которые являются более гибкими (и, следовательно, более сложными), в POST и в другой URI ресурса, особенно если это не согласуется с поведением других ресурсов REST в том же API.

В любом случае, согласованность, вероятно, важнее, чем если вы выполняете поиск в GET или POST.

Надеюсь это поможет.


1
Поскольку REST предназначен для абстрагирования базовой реализации (например, ресурс не обязательно является строкой в ​​базе данных или файлом на жестком диске, но может быть чем угодно ), я не знаю, имеет ли смысл использовать POST поверх ПОЛУЧИТЕ когда дело доходит до выполнения соединений SQL. Предположим, у вас есть таблица школ и таблица детей, и вы хотите класс (одна школа, несколько детей). Вы можете легко определить виртуальный ресурс и GET /class?queryParams. С точки зрения пользователя, «класс» всегда был чем-то особенным, и вам не нужно было делать какие-то странные SQL-объединения.
stevendesu

Нет разницы между «фильтрацией» и «поиском».
Николас Шенкс

1
Да, фильтр основан на существующих полях. Поиск может содержать гораздо более сложные шаблоны, объединяющие поля, вычисляющие значения
прилагательных

Точно @stevendesu, поэтому я использую POST для обоих (создание поиска) :-)
ymajoros

@ymajoros Если вы не сохраняете условия поиска и результаты поиска где-то, я не знаю, что POST имеет смысл семантически. Когда вы выполняете поиск, вы делаете запрос на информацию, вы не предоставляете новую информацию, которая будет храниться где-либо.
stevendesu

10

В REST определение ресурса очень широкое. Однако на самом деле вы хотите связать некоторые данные.

  • Полезно рассматривать поисковый ресурс как коллекционный ресурс. Параметры запроса, иногда называемые поисковой частью URI, сужают ресурс до элементов, в которых заинтересован клиент.

Например, основной Google URI указывает на ресурс коллекции «ссылок на каждый сайт в Интернете». Параметры запроса сужают его до тех сайтов, которые вы хотите увидеть.

(URI = универсальный идентификатор ресурса, из которых URL = универсальный локатор ресурса, где знакомый «http: //» является форматом по умолчанию для URI. Таким образом, URL является локатором, но в REST целесообразно обобщить это для идентификатора ресурса Люди используют их взаимозаменяемо, хотя.)

  • Поскольку ресурс, который вы ищете в своем примере, является коллекцией вакансий, имеет смысл искать с

ПОЛУЧИТЬ сайт / рабочие места? Type = blah & location = here & etc = etc

(возвращение) {jobs: [{job: ...}]}

А затем используйте POST, который является глаголом добавления или обработки, чтобы добавить новые элементы в эту коллекцию:

POST сайт / вакансии

{работа: ...}

  • Обратите внимание, что jobв каждом случае это одна и та же структура объекта. Клиент может ПОЛУЧИТЬ набор заданий, используя параметры запроса для сужения поиска, а затем использовать тот же формат для одного из элементов, чтобы ПОСТАВИТЬ новое задание. Или может взять один из этих элементов и PUT на его URI, чтобы обновить этот.

  • Для действительно длинных или сложных строк запросов соглашение позволяет вместо этого отправлять их как запросы POST. Объедините параметры запроса в виде пар имя / значение или вложенных объектов в структуре JSON или XML и отправьте их в теле запроса. Например, если ваш запрос содержит вложенные данные вместо набора пар имя / значение. Спецификация HTTP для POST описывает его как глагол добавления или процесса. (Если вы хотите проплыть линкор через лазейку в REST, используйте POST.)

Я бы использовал это как запасной план.

Что вы теряете, когда делаете это, хотя это: а) GET является нульпотентным - то есть он ничего не меняет - POST - нет. Поэтому, если вызов не удался, промежуточное ПО не будет автоматически повторять или кэшировать результаты, и 2) с параметрами поиска в теле, вы не сможете больше вырезать и вставить URI. То есть URI не является конкретным идентификатором для поиска, который вы хотите.

Чтобы отличить «создать» от «поиска». Есть несколько вариантов, которые соответствуют практике REST:

  • Вы можете сделать это в URI, добавив что-то к имени коллекции, например, поиск работы вместо рабочих мест. Это просто означает, что вы рассматриваете коллекцию поиска как отдельный ресурс.

  • Так как семантика POST является процессом добавления ИЛИ, вы можете идентифицировать тела поиска с полезной нагрузкой. Как {работа: ...} против {поиск: ...}. Это зависит от логики POST, чтобы разместить или обработать его соответствующим образом.

Это в значительной степени предпочтение дизайна / реализации. Я не думаю, что есть четкое соглашение.

Итак, как вы уже изложили, идея состоит в том, чтобы определить ресурс коллекции для jobs

сайт / работы

Поиск с GET + запрос параметров, чтобы сузить поиск. Длинные или структурированные запросы данных попадают в тело POST (возможно, в отдельную коллекцию поиска). Создать с помощью POST, чтобы добавить в коллекцию. И обновить с PUT для конкретного URI.

(FWIW соглашение о стиле с URI состоит в том, чтобы использовать все строчные буквы со словами, разделенными дефисами. Но это не значит, что вы должны делать это таким образом.)

(Кроме того, я должен сказать, что из вашего вопроса ясно, что вы далеко продвинулись по этому пути. Я прописал некоторые вещи просто для того, чтобы выстроить их в ряд, но ваш вопрос уже затрагивал большинство семантических проблем в этом ответ. Я просто связывал это с некоторыми соглашениями и практикой.)


Это интересная идея - я бы не стал использовать полезную нагрузку для дифференциации. Это кажется немного незаметным! Но я думаю, что схема URI на самом деле не содержит никаких глаголов - это тип запроса, который определяет глагол. Может быть, полезная нагрузка семантически ближе к типу запроса, чем URI. Единственная проблема - это прозрачно для пользователя API?
Роб Бэйли

С точки зрения реализации (мы используем Node и Express), это может означать, что на routeсамом деле не может справиться с выбором обработки. Я должен был бы взглянуть на это ...
Роб Бэйли

У меня такое же чувство, что разделение на URI кажется чище. Я вроде иду туда и обратно; это решение суда. Однако семантика HTTP позволила бы поместить его в тело. Я хотел бы сказать, что REST смоделирован по аналогии с World Wide Web, а WWW был создан с использованием GET и POST.
Роб

8

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

Вы используете токены, такие как $select=и $filter=поэтому вы получите URI, который выглядит примерно так:

/users?$select=Id,Name$filter=endswith(Name, 'Smith')

Вы также можете сделать пейджинг с помощью $skipи $topи заказ.

Для получения дополнительной информации, проверьте OData.org . Вы не указали, какой язык вы используете, но если это ASP.NET, платформа WebApi поддерживает запросы OData - для других (PHP и т. Д.), Вероятно, есть библиотеки, которые вы можете использовать для перевода их в запросы к базе данных.


6
Интересная ссылка, которую стоит посмотреть, но решает ли она описанную фундаментальную проблему: GET-запросы не поддерживают более 2000 символов в строке запроса, и вполне возможно, что запрос может быть намного длиннее, чем этот?
Роб Бэйли

@RobBaillie Я не думаю, что это все еще вызов GET со строкой запроса. Я бы посоветовал использовать OData везде, где это возможно, поскольку это стандарт для запросов к источникам веб-данных, и в тех случаях, когда запрос должен быть настолько сложным, что его невозможно уместить в запросе из 2000 символов, создайте специальный конечная точка, в которую вы звоните GET
Тревор Пилли

Можете ли вы объяснить свой подход к «конкретной конечной точке, к которой вы обращаетесь GET»? Как вы можете представить, что конечная точка будет выглядеть?
Роб Бэйли

@RobBaillie уверен - опять же я не уверен, какую технологию вы используете, но в ASP.NET я бы создал конкретный контроллер с именем JobsNearMeAddedInTheLast7Daysили что-то еще для инкапсуляции запроса, который слишком длинный / сложный для OData, а затем выставил бы его только через вызовы GET ,
Тревор Пилли

1
Понимаю. Еще одна интересная мысль, которая, вероятно, имеет некоторые ножки, хотя я не уверен, что это помогло бы в моем конкретном случае - многогранный поиск с большим количеством типов фасетов и множеством возможных значений фасетов
Роб Билли

5

Один из подходов, который следует рассмотреть, - это обработка множества возможных запросов как ресурса коллекции, например /jobs/filters.

POSTзапросы к этому ресурсу, с параметрами запроса в теле, либо создать новый ресурс или определить существующий эквивалентный фильтр и возвращают URL , содержащий его идентификатор: /jobs/filters/12345.

Идентификатор может быть использован в запросе GET для заданий: /jobs?filter=12345. Последующие GETзапросы к ресурсу фильтра будут возвращать определение фильтра.

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

Недостатком этого подхода является то, что вы теряете удобочитаемость URL-адреса (хотя это может быть смягчено путем получения определения посредством GETзапроса ресурса фильтра). По этой причине вы также можете захотеть поддерживать те же или подмножество параметров запроса для /jobsресурса, что и для ресурса фильтра. Это может быть использовано для более коротких запросов. Если эта функция предоставляется, чтобы поддерживать кешируемость между двумя типами фильтрации, при использовании параметров запроса к /jobsресурсу реализация должна внутренне создавать / повторно использовать ресурс фильтра и возвращать состояние 302или, 303указывающее URL-адрес в форме /jobs?filter=12345.


Моя первая реакция на это заключается в том, что, хотя это хорошая информация, на самом деле это всего лишь вариант ответа, предоставленного @burninggramma. По сути, это «создайте новую сущность с именем filter / search, позвоните, чтобы создать ее, а затем позвоните, чтобы получить ее». Разница в том, что вызов для его получения больше похож на вызов для применения его к коллекции. Интересно. Однако и ваш ответ, и ответ Burninggramma страдают от одной и той же проблемы - у меня нет желания создавать фильтры. Их будет огромное количество, и их не нужно хранить, кроме как для реализации RESTful.
Роб Бэйли

2
Очевидно, что параметры запроса являются лучшим решением, но в вашем вопросе конкретно задается вопрос о том, как обращаться с определениями фильтров, превышающими ограничение на URL-адреса, налагаемое некоторыми серверами. Чтобы обойти ограничение длины, вам нужно будет либо как-то сжать строку запроса, либо использовать метод запроса, который поддерживает указание тела произвольной длины. Если вы не хотите рассматривать фильтры как ресурс, просто поддержите интерфейс без перерыва, в котором определения фильтров размещены POST. Вы потеряете кешируемость, но если ваши данные достаточно изменчивы, кеширование все равно не принесет пользы.
прагам

Вы можете преодолеть необходимость хранить фильтры, просто ... не сохраняя их. Ничто в REST не гарантирует его постоянство. Вы можете сделать запрос GET /jobs/37и получить результат, затем кто-то удалит ресурс, и через 2 секунды тот же запрос вернет 404. Аналогично, если вы POST /searchesи вы перенаправлены на результат поиска (поиск создан, и вы получаете 201 с Заголовок местоположения ресурса), через 2 секунды этот результат может быть удален из памяти и должен быть восстановлен. Нет необходимости в длительном хранении.
stevendesu

5

Это старый ответ, но я все еще могу внести небольшой вклад в обсуждение. Я очень часто наблюдал недопонимание REST, RESTful и Architecture. RESTful никогда не упоминает ничего о НЕ построении поиска, в RESTful ничего нет об архитектуре, это набор принципов или критериев проектирования.

Чтобы лучше описать поиск, нам нужно поговорить об архитектуре, в частности, и лучше всего подходит ресурсно-ориентированная архитектура (ROA).

В RESTful есть принципы для разработки, идемпотент не означает, что результат не может измениться, как я прочитал в некоторых ответах, это означает, что результат независимого запроса не зависит от того, сколько раз выполнено. Это может измениться, давайте представим, что я постоянно обновляю базу данных, снабжая ее некоторыми данными, которые обслуживаются RESTful Api, выполнение того же GET может изменить результат, но это не зависит от того, сколько раз он был выполнен. Если я могу заморозить мир, это означает, что в сервисе нет состояния, преобразования или чего-либо еще, когда я запрашиваю ресурс, который приводит к другому результату.

По определению, ресурс - это все, что важно, чтобы на него ссылались как на вещи.

В архитектуре, ориентированной на ресурсы (далее для краткости будем называть ее ROA), мы ориентируемся на ресурс, который может быть многим:

  • Версия документа
  • Последняя обновленная версия документа
  • Результат, полученный в результате поиска
  • Список объектов
  • Первая статья, которую я купил в электронной коммерции

То, что делает его уникальным с точки зрения ресурса, это доступность, что означает, что он имеет только один URI

Таким образом, поиск идеально подходит для RESTful, учитывая ROA . Мы должны использовать GET, потому что я предполагаю, что ваш поиск - это обычный поиск, и он ничего не меняет, поэтому он идемпотентен (даже если он возвращает разные вещи в зависимости от добавленных новых элементов). Таким образом, здесь возникает путаница, потому что я могу придерживаться RESTful, а не ROA, это означает, что я могу следовать шаблону, который создает поиск и возвращать разные вещи с одинаковыми параметрами, потому что я не использую принцип адресуемости ROA. Как так? Хорошо, если вы отправляете поисковые фильтры в теле или заголовке, ресурс НЕ АДРЕСОВАН.

Вы можете найти принципы того, что именно и URI в оригинальном документе W3:

https://www.w3.org/DesignIssues/Axioms

Любой URL в этой архитектуре должен быть информативным. Это необходимо, если вы следуете принципам, чтобы обращаться ко всему в URI, это означает, что вы можете использовать / (косую черту) для разделения всего, что вам нужно или запроса параметров. Мы знаем, что на это есть ограничения, но это образец архитектуры.

Следуя шаблону ROA в RESTful, поиск не более чем любой другой ресурс, единственное отличие состоит в том, что ресурсы поступают из вычислений, а не из прямой связи с самим объектом. Основываясь на этом принципе, я мог бы обратиться и получить простую услугу арифметических вычислений на основе следующей схемы:

http://myapi.com/sum/1/2

Там, где суммы, 1 и 2 могут быть изменены, но результат вычислений является уникальным и адресуемым, каждый раз, когда я звоню с одинаковыми параметрами, я получаю одно и то же, и в сервисе ничего не меняется. Ресурсы / sum / 1/2 и / substract / 5/4 отлично придерживаются принципов.


3

GET в порядке, если у вас есть статическая коллекция, которая всегда возвращает одинаковые результаты (представление) для одного URI. Это также подразумевает, что данные, генерирующие эти представления, никогда не изменяются. Источник - база данных только для чтения.

Если GET возвращает разные результаты для одного и того же URI, это нарушает идемпотентность / безопасность и принцип CoolURI и, следовательно, не является RESTful . Идемпотентные глаголы могут быть записаны в базу данных, но они никогда не должны влиять на представление.

Общий поиск начинается с запроса POST, который возвращает ссылку на результат. Он генерирует результат (он новый и может быть получен с последующим GET). Этот результат, конечно, может быть иерархическим (дополнительные ссылки с URI, которые вы можете получить GET) и может повторно использовать элементы более ранних поисков, если это имеет смысл для приложения.

Кстати, я знаю, что люди делают это по-другому. Вам не нужно объяснять мне, как удобно нарушать REST.


Aaaaaaaah - так вот как это должно работать! Спасибо!
Роб Бэйли

1
Идемпотентность не означает, что она должна всегда возвращать одно и то же, она должна возвращать одно и то же, если НИЧЕГО не меняется. Поиск можно считать результатом вычислений, а сам ресурс.
Максимилиано Риос

Идемпотентность на самом деле означает, что результат остается прежним. Вы можете, и это практически возможно, использовать управление кэшем. И вы можете, конечно, использовать DELETE, который мешает последующим GET. Однако, если агенту необходимо хранить знания о внутренней работе приложения, он больше не является RESTful. Выше я говорил о самой экстремальной идее REST. На практике люди могут нарушать многие его аспекты. Они платят цену, когда кеши больше не работают.
Мартин Сугиоарто

«Идемпотентность на самом деле означает, что результат остается прежним.» ... после запроса. Дело в том, я считаю, что запрос не меняет данные.
АндрейМотинга
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.