До списку уроків
11 клас · Урок 11 з 14

Анкета, форма на сайті та обробник на сервері

виконано0%
Крок 1

Сьогодні на твоєму сайті зʼявиться форма для відгуків

Форма на сайті, обробник на сервері, приватний файл із відповідями і одне число на лендингу.

Досі ти тільки розповідав про продукт: бриф, пітч, дек, лендинг. Сьогодні все навпаки. Продукт починає приймати відповіді від людей.

На твоєму сервері зʼявиться файл. У ньому лежатимуть імена й слова живих людей. Тримати цей файл у безпеці — тепер твоя робота.

За 120 хвилин ти збереш ланцюг із чотирьох ланок. Перша — анкета: самі питання, які ти справді поставиш людям. Друга — форма feedback.html: сторінка з цими питаннями й кнопкою «Надіслати».

Третя ланка — обробник ~/bin/form-handler. Це маленька програма на сервері. Вона приймає відповіді, перевіряє їх і дописує один рядок у файл.

Четверта ланка — сам файл ~/data/responses.csv. Саме тут проходить головна межа уроку. Обробник і файл лежать поза текою ~/www/, тому завантажити їх з інтернету не вийде.

Що вже вміє 9 клас — і що тут інакше Будову форми, типи полів і <label> ти вже знаєш. Різницю між GET і POST теж. І те, що required у браузері — зручність для людини, а не захист від зловмисника. Сьогодні до цього додається три речі. Як скласти питання, відповіді на які тобі справді знадобляться. Як програма на сервері безпечно записує чужий текст у файл. І як із того файлу виходить одне число для лендинга.

Після уроку ти вмієш

Хід уроку

ЕтапХвЩо робимо
Розігрів: погана анкета10Шукаємо навідні й подвійні питання в 6 прикладах
Типи питань і валідація15Шкала / множинний вибір / відкрите — проєктуємо 7 питань своєї анкети
Етика й приватність15Персональні дані, згода, анонімність — викреслюємо зайві поля
Google Forms і збір20Публікуємо форму, збираємо ≥10 відповідей одне в одного
Потік даних15Конвеєр «Форма → Обробник → CSV», читаємо сирий POST
feedback.html20Верстаємо форму на токенах бренду з <label> до кожного поля
Практика: обробник15Дописуємо валідацію й дедуплікацію в bin/form-handler
Автоперевірка й підсумок10check11 7 (частина) з маркером __autocheck__
Разом120

Слова CGI, агрегат і CSV-injection поки нічого не означають — розберемо кожне тоді, коли воно знадобиться.

ФайлДе лежитьХто бачить
feedback.html~/www/усі в інтернеті — так і задумано
form-handler~/bin/ніхто; сервер його запускає, але не віддає
responses.csv~/data/тільки ти й учитель
feedback-stats.json~/www/data/усі — але там лише кількість і середнє
Питання 1

Однокласник каже: «Навіщо морочитися з ~/bin/ і ~/data/? Покладу обробник і CSV поруч із feedback.html, воно ж однаково працює». Що саме зламається?

Крок 2

Скільки питань витримає жива людина

Анкета — це не список того, що тобі цікаво. Це бюджет чужої уваги.

Компанія SurveyMonkey виміряла час відповіді на понад 100 000 опитувань. Закономірність вийшла проста: що далі питання від початку, то менше секунд людина на нього витрачає. Не тому, що питання простіші, — тому, що вона втомилася.

Позиція питанняСекунд на питанняЩо це означає
1-ше75читає уважно, вникає в контекст
2-ге40уже зорієнтувався
3–10-те30робочий темп
11–15-те25починає поспішати
16–25-те21клікає, майже не читаючи
26–30-те19відповіді вже не варті довіри

Порахуй: анкета з 10 питань — це приблизно 5 хвилин. За даними того самого дослідження, після 7–8 хвилин заповнення частка тих, хто дійшов до кінця, падає на 5–20 %. І падає нерівномірно: першими зникають ті, кому не дуже цікаво, — тобто саме ті, чия думка тобі найпотрібніша.

Пастка виживання Довга анкета не просто збирає менше відповідей — вона збирає зміщені відповіді. До кінця доходять твої друзі й фанати продукту. Середня оцінка росте, а причина росту — не продукт, а довжина анкети.

Для відгуку про продукт робоча межа — 5–7 питань. За таблицею вище це приблизно чотири хвилини. Це вміщає одну оцінку, один вибір, одне відкрите питання і мінімум контексту. Усе інше — наступна анкета, після того як ти щось зміниш у продукті.

Питання 2

Учениця зробила анкету на 22 питання. Прийшло 40 людей, дійшли до кінця 12, середня оцінка продукту — 4,8 з 5. Вона пише в пітч: «Продукт подобається користувачам, середня оцінка 4,8». Що з цим висновком не так?

Крок 3

Закриті, відкриті й ті, що не варті рядка

Форма питання змінює відповідь сильніше, ніж сам продукт.

Закрите питання дає перелік варіантів. Його легко порахувати, легко порівняти між людьми — і воно нічого не розповість поза списком, який ти сам склав. Відкрите дає текст своїми словами: там живуть причини, назви й формулювання, до яких ти б не додумався.

Pew Research Center поставив одне й те саме питання двома способами у передвиборчому опитуванні 2008 року. Коли «економіку» запропонували у списку, її обрали 58 %. Коли те саме питання лишили відкритим, економіку самі назвали 35 %. Питання одне, різниця — 23 пункти, і вся вона в формі.

Правило, яке з цього випливає Список варіантів — це підказка. Люди обирають із того, що бачать, а не з того, що думають. Тому перший цикл відгуків роблять із відкритим питанням, дивляться, що люди пишуть самі, і вже потім перетворюють найчастіші відповіді на варіанти закритого.

У відкритих питань є ціна: на них частіше не відповідають зовсім. У панелі Pew частка пропусків для відкритих питань коливається від 3 % до понад 50 % залежно від формулювання. Тому в анкеті на 5–7 пунктів відкрите питання лишай одне. І хай це буде головне питання, а не «щось іще?» наприкінці.

Питання, яке нічого не дає

«Вам сподобалось?» — найпопулярніше і найбезкорисніше питання світу. Воно дає «так» від 90 % людей, бо відповідати «ні» незручно, і не дає жодної підказки, що робити далі. Порівняй його з питанням «що вас зупинило?»:

«Вам сподобалось?»

Відповідь: «так», «норм», «класно».

Що робити завтра: невідомо.

Соціальний тиск: людині незручно сказати «ні» тому, хто стоїть поруч.

«Що вас зупинило?»

Відповідь: «не зрозумів, як почати», «дорого», «немає української мови».

Що робити завтра: перший екран, ціна, локалізація.

Питання дає дозвіл на критику — і тому її отримує.

Різниця не в тоні, а в напрямку. Перше питання просить оцінити тебе, друге — описати перешкоду. Перешкода — це те, що ти можеш прибрати, оцінка — ні.

Навідне питання: коли ти сам вписав відповідь

Навідне питання підказує «правильну» реакцію інтонацією або словом. Ось класичний приклад із опитувань центру Pew. Одне й те саме рішення назвали двома способами.

Перше формулювання: «зробити законним для лікарів надавати невиліковно хворим засіб завершити життя». Його підтримали 51 %. Друге: «допомога в самогубстві». Підтримали 44 %. Змінилося одне слово.

ДефектЯк звучить у твоїй анкетіЯк переписати
Навідне«Погодьтеся, наш зручний сервіс економить час?»«Скільки часу ви витратили, щоб зробити перше замовлення?»
Подвійне«Чи зручний і швидкий застосунок?»Два окремі питання: про зручність і про швидкість
Порожнє«Вам сподобалось?»«Що вас зупинило?»
«Погоджуюсь / не погоджуюсь»«Продукт корисний: так чи ні?»Вибір між двома твердженнями: «радше зекономив час» / «радше додав роботи»
Про майбутнє«Ви б платили 200 грн на місяць?»«Скільки ви платите зараз за те, чим користуєтесь замість нас?»

Останній рядок найважчий. Люди щиро й помилково передбачають власну майбутню поведінку: «звісно, платив би» коштує нуль гривень. Питання про минуле («що ви робили», «скільки платите») дає перевірюваний факт.

Порівняння марних і корисних питань анкети МАРНА АНКЕТА · 6 ПИТАНЬ КОРИСНА АНКЕТА · 6 ПИТАНЬ 1. Вам сподобалось? → «так» від усіх. Рішення: жодного 2. Чи зручний і швидкий сервіс? → подвійне: незрозуміло, що саме оцінили 3. Оцініть нас від 1 до 10 → 7 і 8 означають різне в різних людей 4. Ваше прізвище → не потрібне для жодного висновку 5. Ваш телефон → перетворює файл на список контактів Підсумок: 0 рішень, 2 персональні поля 1. Оцінка продукту, 1–5 → одне число, порівнюване між людьми 2. Що вас зупинило? (відкрите) → називає конкретну перешкоду 3. Якої функції бракує? (список + «інше») → прямо лягає в беклог уроку 9 4. Скільки ви платите за аналог зараз? → факт із минулого, а не обіцянка 5. Імʼя або нік (не обовʼязково) → щоб подякувати; можна пропустити Підсумок: 4 рішення, 0 зайвих полів
Обидві анкети коштують людині однаково — приблизно три хвилини. Різниця в тому, що з них можна зробити наступного дня.
Питання 3

В анкеті стоїть питання «Чи погоджуєтесь ви, що наш застосунок зручніший за паперовий щоденник?». 84 % відповіли «так». Учень несе це число в пітч як доказ переваги над конкурентом. Де помилка?

Питання 4

Ти вперше збираєш відгуки й не знаєш, яких функцій людям бракує. Яке питання поставити в цьому першому циклі?

Крок 4

Шкала оцінки: чому в тебе саме пʼять поділок

Не тому, що «пʼять — класика». Тому, що в тебе буде 15 відповідей, а не 15 тисяч.

Класичне дослідження Престона й Колмана (2000) дало 149 людям оцінити ті самі речі. Шкали брали різні: від 2 поділок до 11.

Результат такий. Шкали на 2–4 поділки працюють погано. Далі якість оцінок швидко росте приблизно до 7 поділок. А після сімки майже не змінюється: між 7, 8, 9 і 10 різниці практично немає. Найбільше самим людям сподобалася 10-бальна.

Чесно про те, чого дослідження не каже Воно не каже, що 5 краще за 10. Воно каже, що 10 не краще за 7, і що люди 10-бальну люблять. Аргументи на користь 5 у твоєму випадку зовсім інші — вони не про психометрію, а про твої 15 відповідей.

Три причини взяти 5 саме тобі

  1. Кожній поділці можна дати слово. 1 — «зовсім ні», 3 — «нормально», 5 — «дуже». Що таке 7 із 10 словами? Ніхто не знає, і різні люди мають на увазі різне.
  2. Точність, якої в тебе немає. На 15 відповідях різниця середніх 7,2 і 7,6 — це шум від однієї людини. 10-бальна шкала малює точність, якої у твоїх даних не існує.
  3. Шкала має збігатися з дією. Ти все одно поділиш відповіді на «погано / нормально / добре». Пʼять поділок роблять цей поділ прямо, десять — вимагають від тебе домовленості, де межа.
Пʼятибальна і десятибальна шкали на 15 відповідях Шкала 1–5: кожна поділка має назву 1 2 3 4 5 зовсім ні скоріше ні нормально добре дуже Шкала 1–10: поділки 6, 7, 8 назвати неможливо 1 2 3 4 5 6 7 8 9 10 зона, де в різних людей різні означення На 15 відповідях: зсув середньої на 0,4 бала = одна людина змінила думку. Точність шкали не додає точності даним — вона додає точності вигляду.
Ілюстрація автора сторінки. Числа «15 відповідей» і «0,4 бала» — типова ситуація на класному опитуванні, а не результат дослідження.

У розмітці це одне поле: <input type="number" name="rating" min="1" max="5" step="1" required>. Автоперевірка дивиться саме на min і max. Якщо тут стоїть текстове поле замість числового, система поки не прийме роботу — заміни тип поля й запусти перевірку ще раз. Спроби не обмежені.

Питання 5

Учень зібрав 14 відгуків за 10-бальною шкалою, середня — 7,4. Наступного тижня додав 2 відгуки по 9, середня стала 7,6. Він пише в звіті: «Після оновлення інтерфейсу задоволеність зросла». Що тут не так насамперед?

Крок 5

Мінімізація: кожне поле має довести, що воно потрібне

Не «що ще спитати», а «без чого саме я не зроблю висновок».

Правило називається мінімізацією даних, і воно записане в законі. У GDPR, стаття 5(1)(c): дані мають бути «адекватними, релевантними й обмеженими тим, що необхідно для цілей обробки». В українському законі «Про захист персональних даних», стаття 6: склад і зміст даних мають бути «не надмірними стосовно визначеної мети їх обробки».

Формулювання юридичне, застосування просте. Для кожного поля відповідай на одне питання: яке рішення про продукт я не зможу ухвалити без нього? Якщо відповіді немає — поля немає.

ПолеЯке рішення даєВердикт
Оцінка 1–5чи стало краще після змінипотрібне
Якої функції бракуєщо робити наступнимпотрібне
Що зупинилощо прибрати з дорогипотрібне
Імʼя або нікподякувати, побачити повторипотрібне, але не обовʼязкове
Прізвищежодногозайве
Класжодного — сегментів у тебе немаєзайве
Телефон, месенджержодногозайве й небезпечне
Вік, дата народженняжодного на цьому етапізайве

Прізвище й клас разом дають конкретну людину в конкретній школі. Кожне поле окремо здається невинним — а разом вони складаються. Файл із думками перетворюється на файл про людей, і статус усього файлу міняється назавжди.

Різниця відчутна на простому порівнянні. Пароль після витоку міняють за хвилину. Номер телефону — місяцями. А прізвище не міняють узагалі.

Абзац під формою, який варто написати «Це відгук про назва продукту. Відповіді потрібні мені, щоб вирішити, що доробити. Імʼя можна не вказувати. Відповіді зберігаються в приватному файлі на моєму сервері, на сайт потрапляють лише кількість відгуків і середня оцінка. Напиши мені — і я видалю твій рядок.» Це не формальність: саме на це людина погоджується, натискаючи кнопку.

Межа між публічним і приватним

Тека ~/www/ — це буквально те, що вебсервер віддає в інтернет. Усе, що туди потрапило, доступне за прямою адресою будь-кому, хто її вгадає, і може потрапити в пошуковий індекс. Ти вже клав туди market.csv — і правильно: там знеособлені числа про ринок.

Відповіді людей туди класти не можна. Тому сьогоднішній ланцюг перетинає межу рівно двічі — і обидва рази свідомо.

Перший перетин — униз. Людина натиснула «Надіслати», і сервер запускає ~/bin/form-handler. Відповідь іде з публічної адреси /submit у приватну теку й лягає рядком у ~/data/responses.csv.

Другий перетин — угору. Скрипт читає той самий файл і виносить із нього два числа: скільки відгуків усього і яка середня оцінка. Тільки ці два числа потрапляють у ~/www/data/feedback-stats.json і на лендинг.

Більше межу не перетинає ніщо. Ані імʼя, ані коментар, ані оцінка окремої людини нагору не піднімаються.

Шлях відгуку від форми до лендинга через межу публічного і приватного ПУБЛІЧНО · ~/www/ · віддається по HTTP ПРИВАТНО · поза веб-корінням · HTTP-адреси немає межа feedback.html форма, 7 полів method="post" /submit адреса є файлу за нею — немає index.html «12 відгуків, середня 4,3» ~/bin/form-handler валідація · дедуп екранування ~/data/responses.csv імена, коментарі права 600 агрегат count = 12 avg = 4,3 POST сервер запускає скрипт 1 рядок рахуємо два числа, самі відповіді лишаються тут лише 2 числа
Два перетини межі. Униз, у приватну теку, іде відповідь людини. Угору, на лендинг, повертаються тільки два числа: кількість відгуків і середня оцінка.

Перевір межу власноруч

Три запити, три різні відповіді. Виконай їх після того, як усе залив на сервер, — це і є доказ, що межа стоїть.

перевірка межі · Git Bash, macOS, Linux
$ curl -s -o /dev/null -w "%{http_code}\n" http://91.219.61.4/s/<логін>/data/market.csv 200 ← знеособлені числа, публічні навмисно $ curl -s -o /dev/null -w "%{http_code}\n" http://91.219.61.4/s/<логін>/data/responses.csv 404 ← файла у www немає і бути не може $ curl -s -o /dev/null -w "%{http_code}\n" http://91.219.61.4/s/<логін>/bin/form-handler 404 ← скрипт запускається, але не завантажується
Windows: пиши curl.exe У Windows PowerShell 5.1 curl — це псевдонім Invoke-WebRequest, і ключі -s, -o, -w, -X, -d там не працюють. Пиши curl.exe — тоді запуститься справжній curl, який є у Windows 10 і 11.

Чому 404, а не 403? Бо для сервера цих шляхів просто не існує. Веб-корінь — це ~/www/. Усе, що поза ним, не має адреси взагалі.

403 ти побачив би в іншому випадку: файл лежить усередині веб-кореня, а сервер відмовляє в доступі. Ця ситуація гірша. Файл там, і одна помилка в налаштуваннях робить його публічним.

Питання 6

Однокласниця пропонує додати в анкету поле «клас» — «щоб потім побачити, кому продукт заходить більше». Відповідей планується 15. Що відповісти?

Питання 7

Учень скопіював responses.csv у ~/www/data/, «щоб зручніше дивитися з телефона», і перейменував на d2.txt. Що змінилося?

Крок 6

Конструктор анкети: збери питання — забери feedback.html

Сторінка не вгадує змісту твого продукту. Вона ловить дефекти формулювань, рахує час і збирає розмітку.

Стартовий набір уже лежить у конструкторі — це мінімальна робоча анкета відгуку. Твоє завдання: замінити питання на свої, додати одне-два під свій продукт і втримати сім полів. Конструктор перевіряє кожне питання за пʼятьма ознаками й показує, що з ним не так.

Що ловитьЗа чим упізнаєЧому це важливо
Навідне«погодьтеся», «чи не правда», «зручний», «чудовий», «наш кращий»відповідь уже вписана в питання
Подвійнедва прикметники через «і» / «та» в одному питаннінезрозуміло, що саме оцінили
Порожнє«сподобал», «як вам», «оцініть нас»дає «так» і жодного рішення
Про майбутнє«чи будете», «чи платили б», «порекомендуєте»люди помиляються про себе завтрашніх
Персональне полепрізвище, телефон, клас, адреса, вік, поштаміняє статус усього файлу
Як рахується час Секунди на позицію взяті з вимірів SurveyMonkey (75 · 40 · 30 · 25 · 21 · 19). Надбавка 15 секунд за відкрите питання — оцінка автора сторінки: писати текст довше, ніж клікати. Число орієнтовне, але порядок правильний.
Конструктор анкети

Швидко додати для проби:

    Полів0
    Відкритих0
    Персональних0
    Проблемних0
    Час заповнення0 с

    Згенерований файл — це чернетка, а не готова робота. Підключи до нього brand.css. Впиши під формою свій текст про те, як ти використаєш дані. І перевір, що action указує на адресу твого обробника.

    Питання 8

    Конструктор позначив питання «Чи будете ви рекомендувати наш зручний сервіс друзям?» одразу трьома мітками. Якими — і яка з них найгірша для дослідження?

    Крок 7

    Обробник: перевірки, яких браузер зробити не може

    Браузер підказує людині. Сервер вирішує. Це різні задачі, і другу за тебе ніхто не зробить.

    required, min, max і type="email" у формі зроблені для зручності. Людина бачить помилку одразу, не чекаючи відповіді сервера.

    Але POST-запит можна зібрати й руками, без твоєї сторінки. Досить однієї команди curl — і в обробник прилітає порожнє імʼя й оцінка «сто».

    перевір сам: форму можна обійти
    $ curl -s -o /dev/null -w "%{http_code}\n" -X POST http://91.219.61.4/s/<логін>/submit --data-urlencode "rating=100" --data-urlencode "comment=" 400 ← так має відповідати правильний обробник

    Як обробник узагалі запускається

    Файл лежить у ~/bin/, поза веб-корінням. Учитель один раз прописав у конфігурації сервера, що адреса /submit означає «запусти цей скрипт». Тому адреса є, а файлу за нею немає: завантажити form-handler не можна, запустити — можна.

    Тепер про те, як сервер передає скриптові дані з форми. Домовленість тут дуже стара й дуже проста, зветься вона CGI. Сервер не викликає жодних функцій — він просто запускає твій файл, як ти запускаєш будь-яку команду.

    А потрібне скрипт бере з трьох місць. Хто прийшов і яким методом — зі змінних оточення REQUEST_METHOD і CONTENT_LENGTH. Самі відповіді з форми — зі стандартного вводу, того самого, куди ти зазвичай друкуєш із клавіатури. А те, що скрипт друкує на екран, сервер віддає браузерові як відповідь.

    Усе інше в обробнику — звичайний bash, який ти вже бачив.

    Порядок перевірок в обробнику форми POST /submit метод POST? тіло ≤ 8 КБ? коментар є? оцінка 1–5? не дубль? відбиток за 10 хв екранування лапки · = + @ - запис під flock ~/data/responses.csv 303 See Other → /feedback.html 400 Bad Request рядка у файлі немає 303, але без запису дубль тихо відкинуто Порядок не випадковий: спершу дешеві перевірки, потім дорогі; запис — останній.
    Червоні пунктири — виходи без запису у файл. Обробник, який завжди відповідає «дякую», не відрізняється від обробника, який нічого не робить.

    Екранування: RFC 4180 у трьох рядках

    CSV виглядає простим, поки в коментарі немає коми. Стандарт RFC 4180 описує рівно три правила, і всі три має виконувати той, хто пише файл.

    У відповіді людиниЩо робимоЯк лягає у файл
    дорого, але зручнокома всередині — беремо в лапки"дорого, але зручно"
    сказав "супер"кожну лапку подвоюємо"сказав ""супер"""
    текст із переносом рядкапереноси міняємо на пробілодин запис = один рядок
    Чому переноси прибираємо, а не лишаємо За стандартом перенос усередині лапок дозволений, і правильний парсер його зрозуміє. Але report.sh наступного уроку читатиме файл порядково, як і wc -l, і tail. Один коментар із двома абзацами перетворить 12 відгуків на 14 рядків. Прибрати переноси дешевше, ніж лагодити кожен інструмент.

    Коди відповіді: чому 303, а не 200

    Після успішного POST обробник не малює сторінку «дякую». Він відповідає 303 See Other і дає адресу, на яку піти. Браузер робить туди звичайний GET.

    Такий обхід зветься POST → Redirect → GET. Потрібен він з однієї причини. Після редиректа кнопка «оновити» повторює вже GET, а не POST. Без нього кожне натискання F5 додавало б ще один відгук.

    КодКолиЩо бачить людина
    303відгук записаносторінку подяки; оновлення нічого не додає
    303дубль за 10 хвилинту саму подяку — про дубль їй знати не треба
    400порожній коментар, оцінка не 1–5текст помилки й що виправити
    405прийшов GET замість POSTнічого корисного — це не людина, а бот

    Повний обробник

    Читай його зверху вниз: кожен блок — одна відповідальність. Це той самий файл, який ти доробиш на практиці.

    ~/bin/form-handler · bash · chmod 700
    #!/bin/bash
    # обробник форми відгуків. Запускається вебсервером за адресою /submit.
    set -u
    CSV="$HOME/data/responses.csv"
    SEEN="$HOME/data/.seen"
    MAX=500
    
    # --- відповіді ------------------------------------------------
    fail(){ printf 'Status: 400 Bad Request\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n%s\n' "$1"; exit 0; }
    deny(){ printf 'Status: 405 Method Not Allowed\r\nAllow: POST\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n%s\n' "$1"; exit 0; }
    back(){ printf 'Status: 303 See Other\r\nLocation: /feedback.html?ok=%s\r\n\r\n' "$1"; exit 0; }
    
    # --- 1. метод і розмір ----------------------------------------
    [ "${REQUEST_METHOD:-}" = POST ] || deny 'приймаю тільки POST'
    len=${CONTENT_LENGTH:-0}
    [ "$len" -gt 0 ] && [ "$len" -le 8192 ] || fail 'порожнє або завелике тіло'
    body=$(head -c "$len")
    
    # --- 2. розкодування %XX і «+» --------------------------------
    urldecode(){
      local s=${1//+/ }
      s=${s//\\/\\\\}                # інакше printf %b зʼїсть зворотну скісну
      printf '%b' "${s//%/\\x}"
    }
    field(){
      local key=$1 pair pairs
      IFS='&' read -ra pairs <<< "$body"
      for pair in "${pairs[@]}"; do
        [ "${pair%%=*}" = "$key" ] && { urldecode "${pair#*=}"; return; }
      done
    }
    
    # --- 3. валідація: усе, що прийшло, — заявка, а не факт --------
    name=$(field name);       rating=$(field rating)
    feature=$(field feature); comment=$(field comment)
    [ -n "$comment" ] || fail 'коментар порожній'
    case "$rating" in 1|2|3|4|5) ;; *) fail 'оцінка має бути числом 1–5';; esac
    [ -n "$name" ] || name='анонім'
    
    # --- 4. екранування одного значення для CSV -------------------
    csv(){
      local v=$1
      v=${v//$'\r'/ }; v=${v//$'\n'/ }; v=${v//$'\t'/ }   # один запис = один рядок
      v=${v:0:$MAX}                                       # межа довжини
      case "$v" in [=+@-]*) v="'$v" ;; esac                 # гасимо формулу Excel
      v=${v//\"/\"\"}                                     # RFC 4180: лапка подвоюється
      printf '"%s"' "$v"
    }
    
    # --- 5. дедуплікація: той самий відгук за 10 хвилин ------------
    now=$(date +%s)
    fp=$(printf '%s|%s|%s|%s' "$name" "$rating" "$feature" "$comment" | sha256sum | cut -c1-16)
    touch "$SEEN"
    awk -v t=$((now-600)) '$1 > t' "$SEEN" > "$SEEN.tmp" && mv "$SEEN.tmp" "$SEEN"
    grep -q " $fp\$" "$SEEN" && back dup
    printf '%s %s\n' "$now" "$fp" >> "$SEEN"
    
    # --- 6. запис: заголовок один раз, рядок — під замком ----------
    ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    [ -s "$CSV" ] || echo 'ts,name,rating,feature,comment' > "$CSV"
    {
      flock 9
      printf '%s,%s,%s,%s,%s\n' "$(csv "$ts")" "$(csv "$name")" \
             "$(csv "$rating")" "$(csv "$feature")" "$(csv "$comment")" >> "$CSV"
    } 9>> "$CSV"
    chmod 600 "$CSV"
    back 1
    Навіщо flock Два відгуки можуть прийти в одну секунду. Без замка два процеси пишуть у файл одночасно, і рядки перемішуються посеред слова. Замок коштує мілісекунду і рятує файл.
    Питання 9

    Обробник пише значення у файл «як є», через echo "$name,$rating,$comment". Усе працює тижнями, аж поки хтось не написав у коментарі дорого, але зручно. Що тепер у файлі?

    Питання 10

    Учень зробив форму без action на обробник: сторінка показує «Дякуємо!» скриптом на самій сторінці, нікуди нічого не надсилаючи. Тестує — усе гарно. Що він побачить у responses.csv?

    Крок 8

    CSV-injection: коли чужий текст стає твоєю формулою

    Файл лежить смирно. Небезпечним його робить програма, у якій його відкриють.

    CSV — це просто текст. Але майже ніхто не читає його як текст. Учитель, інвестор і ти сам двічі клацнете по файлу. Він відкриється в Excel, LibreOffice Calc або Google Sheets.

    У цю мить правила міняються. Для таблиці клітинка, що починається зі знака =, — уже не текст, а формула. І програма її виконує.

    OWASP описує це як окремий клас уразливості — CSV Injection, вона ж Formula Injection. Формулу вмикають не тільки =:

    Символ на початку клітинкиПрикладЩо зробить таблиця
    ==1+1покаже 2
    ++1+1покаже 2
    --1+1покаже 0
    @@SUM(1,1)покаже 2
    табуляція, CR, LFневидиміте саме, але ти цього не бачиш

    Показати «2» замість «=1+1» — нешкідливо. Небезпечно те, що поруч із арифметикою в тій самій клітинці працюють функції, які виходять назовні. Три сходинки, від нудної до серйозної:

    1. Псує дані

    =1+1 перетворюється на 2. Відгук зник, підрахунок зіпсовано, і ніхто не помітив.

    2. Обманює людину

    =HYPERLINK("http://...","Оновіть доступ") малює в клітинці звичайне синє посилання. Клацнув — і ти вже на чужому сайті.

    3. Виносить дані

    Є функції, які самі звертаються на чужий сервер. У таку адресу можна підставити вміст сусідніх клітинок — тобто чужі відгуки.

    Чому «в мене Excel питає дозволу» — не відповідь Сучасні версії справді попереджають про частину небезпечних функцій. Але, по-перше, попередження бачить той, хто відкрив файл, а не ти: ризик ти перекладаєш на іншу людину. По-друге, арифметика й HYPERLINK нічого не питають. По-третє, OWASP окремо зазначає: універсальної безпечної схеми немає, поведінка залежить від програми й версії. Тому єдина надійна дія — не пустити символ у файл.
    Один і той самий файл очима текстового редактора і очима Excel Той самий файл у «Блокноті» ts,name,rating,feature,comment 2026-09-02T10:14:03Z,Оксана,5,експорт,=1+1 Той самий файл у таблиці ts name rating feature comment 2026-09-02… Оксана 5 експорт 2 Відгуку «=1+1» більше не існує: у клітинці лежить результат, а не текст. Текст у файлі не змінився. Змінилося те, хто його читає. Після екранування у файлі '=1+1 — і таблиця показує «=1+1» як звичайний текст.
    Ліворуч і праворуч — той самий файл, байт у байт. Небезпечний не сам файл, а програма, яка відкриває його як таблицю.

    Лабораторія: спробуй сам

    Впиши в поле «коментар» щось, що починається з =, +, - або @. Обидві панелі беруть поля в лапки за стандартом RFC 4180.

    Тобто ліворуч не найгірший обробник світу. Це звичайний акуратний обробник, який просто не чув про CSV-injection. Різниця між панелями — рівно один апостроф. Таблиця праворуч рахує по-справжньому: арифметику обчислює, посилання розпізнає.

    Лабораторія CSV-injection

    Готові приклади з переліку OWASP:

    Лапки є, формулу не гасимо

    namecomment

    Повне екранування

    namecomment

    захист · та сама функція, що в обробнику
    csv(){
      local v=$1
      v=${v//$'\r'/ }; v=${v//$'\n'/ }; v=${v//$'\t'/ }
      v=${v:0:500}
      case "$v" in [=+@-]*) v="'$v" ;; esac   # 1. гасимо формулу
      v=${v//\"/\"\"}                        # 2. подвоюємо лапки
      printf '"%s"' "$v"                     # 3. беремо все в лапки
    }
    Ціна рішення, про яку варто знати Апостроф ставиться при записі, тому він фізично лежить у файлі. У текстовому редакторі ти побачиш '=1+1. Таблиця покаже =1+1, без апострофа. Є й інший шлях: зберігати значення як є, а екранувати вже під час експорту. Він гнучкіший. Але тоді небезпечний файл лежить на диску, і досить один раз відкрити не той. Ми обираємо безпеку файлу, а не його красу. І памʼятаємо: на уроці 12 скрипт звіту має цей апостроф зрізати.
    Питання 11

    Учасник опитування написав у полі «коментар» рядок =1+1. Учитель відкрив responses.csv в Excel і побачив у клітинці «2». Що це означає — і який варіант того самого був би значно гіршим?

    Питання 12

    Учень вирішив захиститися інакше: у формі поставив pattern="[^=+@-].*", щоб браузер не дав почати коментар з небезпечного символу. Обробник лишив без змін. Наскільки він захищений?

    Крок 9

    Дедуплікація: чому та сама людина надсилає двічі

    Майже завжди не зі зла й навіть не свідомо.

    Уяви: людина натиснула «Надіслати», сторінка думає півсекунди, нічого не змінилося. Що вона зробить? Натисне ще раз. Це не рідкісний випадок, а звичайна поведінка — і твій файл отримає два однакові рядки.

    Звідки береться дубльЩо бачить людинаЩо лікує
    Подвійний клік по кнопцінічого; вона просто нетерплячавимкнути кнопку після першого кліку
    F5 після відповіді«надіслати форму повторно?»відповідь 303: оновлюється вже GET
    Кнопка «назад» і ще раз «Надіслати»здається, що не спрацюваловідбиток вмісту на сервері
    Поганий мобільний інтернет, повтор запитукрутилка й нічоговідбиток вмісту на сервері
    Людина справді передумала й пише іншеусе гаразднічого — це не дубль

    Останній рядок — ключ до правильного правила. Дубль визначають не за людиною, а за вмістом. Якщо Оксана надіслала два різні коментарі — це дві думки, обидві цінні. Якщо той самий текст із тією самою оцінкою прилетів двічі за десять секунд — це один клік, порахований двічі.

    Як обробник це помічає

    Спершу обробник склеює чотири відповіді в один рядок: Оксана|5|експорт|дорого. Далі йому потрібна коротка позначка, за якою такий самий рядок упізнається вдруге.

    Позначку роблять так. Беруть увесь текст і рахують із нього одне довге число. Змінилася хоч одна літера — число виходить зовсім інше. Це схоже на відбиток пальця: сам палець із відбитка не відновиш, зате два відбитки легко порівняти.

    Спосіб рахувати таке число зветься sha256, а рахує його на сервері команда sha256sum. Повний результат — 64 символи. Обробник бере від нього перші 16 і саме їх зберігає.

    Чому вистачає 16? Кожен символ — одна з 16 позначок: цифри від 0 до 9 і літери від a до f. Шістнадцять таких символів дають 16 у шістнадцятому степені різних відбитків. У цьому числі двадцять цифр. Твої 15 відгуків на такому просторі не зіткнуться.

    Відбитки останніх десяти хвилин лежать у службовому файлі ~/data/.seen. Старші рядки викидаються при кожному запуску, тож файл не росте.

    Три надсилання, два відбитки, один запис у файл 10:14:03 10:14:05 10:21:40 надсилання 1 Оксана · 5 · «дорого» a4f0c9d1… → записано надсилання 2 Оксана · 5 · «дорого» a4f0c9d1… → відкинуто надсилання 3 Оксана · 4 · «немає темної теми» 7b2e10c4… → записано вікно памʼяті ~/data/.seen — 600 секунд старші відбитки викидаються Та сама людина. Дубль — тільки середнє: збігся весь вміст, а не імʼя.
    Третє надсилання зроблене через сім хвилин і з іншим текстом — це друга думка тієї самої людини, і вона потрапляє у файл.
    Дедуплікація за іменем — найпоширеніша помилка Правило «одна людина — один відгук» звучить логічно й тихо викидає половину корисного. Ще гірше: у класі буває дві Оксани. Відбиток вмісту не знає ані про імена, ані про людей — і саме тому не помиляється.

    Що робити з дублями, які вже у файлі

    Поки обробник був недописаний, дублі туди потрапили. Знайти їх можна однією командою: беремо все, крім першої колонки з часом, і шукаємо повтори.

    показати повторювані рядки, не змінюючи файл
    $ cut -d, -f2- ~/data/responses.csv | sort | uniq -d "Оксана","5","експорт","дорого" $ wc -l < ~/data/responses.csv 14

    Команда cut -d, ріже по комі й не розуміє лапок, тому на коментарі з комою вона помиляється. Для швидкого погляду цього достатньо; для звіту на уроці 12 файл читатимуть справжнім парсером CSV.

    Питання 13

    У файлі 20 рядків, але різних імен лише 12. Учень збирається видалити все, де імʼя повторюється, щоб лишилося «по одному відгуку на людину». Що він втратить?

    Крок 10

    Агрегат: два числа, які можна показувати

    Лендинг має право знати, скільки людей відповіли. Він не має права знати, хто й що написав.

    Ти зібрав 12 відгуків. На лендингу хочеться показати живе, а не вигадане: «12 відгуків, середня оцінка 4,3». Це агрегат — число, пораховане з багатьох рядків, за яким не відновити жодного окремого.

    Різницю добре видно на прикладі. Ліворуч — те, що не можна публікувати ніколи, навіть «на хвилинку, щоб перевірити». Праворуч — те, заради чого все й робилося.

    Що з відповідей потрапляє на лендинг, а що не потрапляє ніколи ЛИШАЄТЬСЯ В ~/data/ імʼя або нік текст коментаря оцінка окремої людини час надсилання цитата «як сказав один користувач…» цитату можна взяти тільки з дозволу автора і без імені, якщо він про це не просив ІДЕ В ~/www/data/feedback-stats.json count — скільки відгуків avg — середня оцінка updated — коли рахували {"count": 12, "avg": 4.33, "updated": "2026-09-02"} 72 байти, у яких немає жодної людини Σ рахуємо і забуваємо Перевірка простим питанням: чи можу я за опублікованим числом сказати, що саме поставив конкретний однокласник? Якщо так — це не агрегат. На 2 відповідях середня видає майже все. Публікуй агрегат від 5 відгуків.
    Агрегат — це не «те саме, тільки коротше». Це інший тип даних: з нього неможливо відновити початковий рядок.
    Чому агрегат не публікують з двох відповідей Якщо відгуків два, а середня 4,5 — кожен, хто знає свою оцінку, вирахує чужу. Це називають ризиком реідентифікації. Тому агрегат публікують від пʼяти відповідей, а до того на сторінці чесно пишуть «збираємо відгуки».

    Скрипт, що рахує агрегат

    Читати CSV із лапками вручну не варто. У мові Python модуль csv уже вміє все, що описує RFC 4180. Скрипт бере приватний файл і віддає публічний JSON. Жодного тексту відгуків він не переносить.

    ~/bin/stats · рахує агрегат для лендинга
    #!/usr/bin/env python3
    # приватне -> публічне: сюди заходять відгуки, звідси виходять два числа
    import csv, json, datetime, pathlib
    
    home = pathlib.Path.home()
    src  = home / 'data' / 'responses.csv'          # приватно
    dst  = home / 'www' / 'data' / 'feedback-stats.json'   # публічно
    
    rows = list(csv.DictReader(src.open(encoding='utf-8', newline='')))
    marks = [int(r['rating']) for r in rows if r['rating'].isdigit()]
    
    out = {
        'count':   len(rows),
        'avg':     round(sum(marks) / len(marks), 2) if marks else None,
        'updated': datetime.date.today().isoformat()
    }
    dst.write_text(json.dumps(out, ensure_ascii=False), encoding='utf-8')
    print(out)

    На лендингу це три рядки: fetch('/data/feedback-stats.json'), підстановка count і avg у текст. На уроці 12 ту саму ідею розгорнемо у повний звіт, який оновлює себе за розкладом cron.

    Чому JSON, а не число прямо в HTML Число у розмітці треба правити руками щоразу. JSON оновлює скрипт, і сторінка завжди показує сьогоднішній стан. Це та сама межа «дані окремо, оформлення окремо», яку ти вже бачив на tasks.json і roadmap.html.
    Питання 14

    Учень хоче зробити лендинг переконливішим і виводить на нього три останні коментарі з іменами — «це ж просто відгуки, вони не таємні». Що не так?

    Крок 11

    Практика: анкета, форма, обробник, приватний файл

    Виконуй по черзі. Галочки зберігаються — сторінку можна закрити.

    Що робиш локально, що їде по FTP Анкету складаєш у конструкторі на цій сторінці. Файли feedback.html і form-handler доводиш у редакторі в себе на компʼютері. По FTP: feedback.html~/www/, form-handler~/upload/, а звідти по SSH переносиш у ~/bin/ і ставиш біт виконання. responses.csv не заливаєш ніколи — його створює сам обробник.
    Файл із розширенням і файл без нього FTP-приймальня пропускає .sh, тому заливай скрипт як form-handler.sh, а вже на сервері перейменуй у form-handler. Розширення в Linux нічого не вирішує — програму обирає перший рядок файлу, #!/bin/bash.
    перенос, права і перша перевірка
    $ mkdir -p ~/bin ~/data $ mv ~/upload/form-handler.sh ~/bin/form-handler $ chmod 700 ~/bin/form-handler $ chmod 700 ~/data $ bash -n ~/bin/form-handler (мовчить — синтаксис у порядку)
    тестовий POST зі свого ж сервера
    $ curl -s -o /dev/null -w "%{http_code}\n" -X POST http://91.219.61.4/s/<логін>/submit --data-urlencode "name=Тест" --data-urlencode "rating=4" --data-urlencode "feature=експорт" --data-urlencode "comment=перевірка" 303 $ tail -n 2 ~/data/responses.csv "2026-09-02T10:14:03Z","Оксана","5","експорт","дорого, але зручно" "2026-09-02T10:15:41Z","Тест","4","експорт","перевірка"

    Тестовий рядок після перевірки прибери: grep -v '"Тест"' ~/data/responses.csv > /tmp/r && mv /tmp/r ~/data/responses.csv. У Windows PowerShell 5.1 пиши curl.exe, а не curl.

    Крок 12

    Автоперевірка check11 7, перша частина

    Система зробить справжній POST на твою адресу, подивиться, що зʼявилося у файлі, і прибере за собою. Спроби не обмежені.

    Перевірка check11 7 одна на два уроки. Сьогодні вона дивиться на форму, обробник і приватний файл — це перша частина. На уроці 12 та сама команда прожене другу частину: звіт report.html і розклад запуску. Імʼя команди там таке саме, тому не лякайся, побачивши його вдруге.

    Демонстраційний режим: результат згенеровано для показу. На сервері ця кнопка запускає check11 7 від імені учня і читає ~/.progress/11-7.json.

    Маркер __autocheck__ Перевірка надсилає відгук, у якому в полі comment є слово __autocheck__, і потім видаляє саме цей рядок. Тому не бійся, що чекер зіпсує статистику. Але й не пиши це слово сам: свій відгук із таким текстом буде прибрано разом із маркерним.
    Два обовʼязкові критерії private блокує оцінку: якщо відповіді людей віддаються по HTTP, решта зробленого не має значення. no_media діє наскрізно весь модуль. На уроці 14 check11 8 прожене обидва повторно.

    Що вчитель дивиться на екрані

    Крок 13

    Шкала виконання

    Це те, що бачить учитель, коли виставляє оцінку.

    СкладникВагаРезультат
    Виконано
    0%
    Рекомендований бал за 12-бальною

    Пʼять речень, які варто запамʼятати

    Домашнє завдання

    1. Зібрати ще 5 відгуків поза класом і переконатися, що жоден не створив дубля: cut -d, -f2- ~/data/responses.csv | sort | uniq -d має мовчати.
    2. Надіслати через власну форму відгук, що починається з =, і показати рядок у файлі: апостроф має бути на місці.
    3. Дописати під формою абзац про те, які дані збираються, навіщо і як їх видалити на прохання.
    4. Порахувати кількість відгуків і середню оцінку, покласти їх у feedback-stats.json у теці ~/www/data/. Сьогодні ти рахуєш це руками. На уроці 12 ті самі два числа рахуватиме скрипт report.sh за розкладом, без тебе.
    Анонс уроку 12 Наступного разу файл із відгуками почне писати звіт сам. Ти зробиш шаблон сторінки з порожніми місцями під числа. Далі напишеш скрипт report.sh: він рахує середню оцінку, розподіл оцінок і три функції, яких просять найчастіше. А cron — це розклад на сервері — запускатиме скрипт щогодини без тебе. Наприкінці буде режим «Зламай конвеєр»: учитель псує одну ланку, а ти шукаєш яку.
    Джерела

    Звідки взяті факти й числа

    Кожне число, яке ти можеш перевірити, має посилання. Перевіряти дозволено й корисно.

    Надбавка 15 секунд за відкрите питання в конструкторі, приклади рядків у файлі, імена, дати й числа «12 відгуків, середня 4,3» — ілюстрації автора сторінки, а не результати вимірювань. Повний список джерел із поясненнями — у файлі urok-11-джерела.md поруч із цією сторінкою.