From d191dfd1dfe5cc1c77d02aefa461fb3d3096d0c1 Mon Sep 17 00:00:00 2001 From: nabilmuafa <109450293+nabilmuafa@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:04:15 +0700 Subject: [PATCH] add: tutorial 5 and assignment 6 --- assignments/individual/assignment-6.md | 47 + docs/tutorial-5.md | 836 ++++++++++++++++++ .../current/individual/assignment-6.md | 50 ++ .../current/tutorial-5.md | 834 +++++++++++++++++ static/img/5-alert-box-xss.png | Bin 0 -> 5257 bytes static/img/5-form-error.png | Bin 0 -> 2695 bytes 6 files changed, 1767 insertions(+) create mode 100644 assignments/individual/assignment-6.md create mode 100644 docs/tutorial-5.md create mode 100644 i18n/en/docusaurus-plugin-content-docs-assignments/current/individual/assignment-6.md create mode 100644 i18n/en/docusaurus-plugin-content-docs/current/tutorial-5.md create mode 100644 static/img/5-alert-box-xss.png create mode 100644 static/img/5-form-error.png diff --git a/assignments/individual/assignment-6.md b/assignments/individual/assignment-6.md new file mode 100644 index 0000000..497c7a5 --- /dev/null +++ b/assignments/individual/assignment-6.md @@ -0,0 +1,47 @@ +--- +sidebar_label: Tugas 6 +sidebar_position: 6 +Path: assignments/individual/assignment-6 +--- + +# Tugas 6: JavaScript dan AJAX + +Pemrograman Berbasis Platform (CSGE602022) — diselenggarakan oleh Fakultas Ilmu Komputer Universitas Indonesia, Semester Ganjil 2024/2025 + +--- + +## Deskripsi Tugas + +Pada tugas ini, kamu akan mengimplementasikan AJAX pada aplikasi yang telah kamu buat pada tugas sebelumnya. + +*Checklist* untuk tugas ini adalah sebagai berikut: +- [ ] Mengubah tugas 5 yang telah dibuat sebelumnya menjadi menggunakan AJAX. + - [ ] AJAX `GET` + - [ ] Ubahlah kode `cards` data _mood_ agar dapat mendukung AJAX `GET`. + - [ ] Lakukan pengambilan data _mood_ menggunakan AJAX `GET`. Pastikan bahwa data yang diambil hanyalah data milik pengguna yang _logged-in_. + - [ ] AJAX `POST` + - [ ] Buatlah sebuah tombol yang membuka sebuah modal dengan form untuk menambahkan _mood_. + :::note + Modal di-_trigger_ dengan menekan suatu tombol pada halaman utama. Saat penambahan _mood_ berhasil, modal harus ditutup dan _input_ form harus dibersihkan dari data yang sudah dimasukkan ke dalam form sebelumnya. Jika penambahan gagal, tampilkan pesan _error_. + ::: + - [ ] Buatlah fungsi _view_ baru untuk menambahkan _mood_ baru ke dalam basis data. + - [ ] Buatlah _path_ `/create-ajax/` yang mengarah ke fungsi _view_ yang baru kamu buat. + - [ ] Hubungkan form yang telah kamu buat di dalam modal kamu ke _path_ `/create-ajax/`. + - [ ] Lakukan _refresh_ pada halaman utama secara asinkronus untuk menampilkan daftar _mood_ terbaru tanpa reload halaman utama secara keseluruhan. + :::warning + Pastikan AJAX `GET` dan `POST` dapat dilakukan secara aman. + ::: + +- [ ] Menjawab beberapa pertanyaan berikut pada `README.md` pada *root folder* (silakan modifikasi `README.md` yang telah kamu buat sebelumnya; tambahkan subjudul untuk setiap tugas). + - [ ] Jelaskan manfaat dari penggunaan JavaScript dalam pengembangan aplikasi web! + - [ ] Jelaskan fungsi dari penggunaan `await` ketika kita menggunakan `fetch()`! Apa yang akan terjadi jika kita tidak menggunakan `await`? + - [ ] Mengapa kita perlu menggunakan _decorator_ `csrf_exempt` pada _view_ yang akan digunakan untuk AJAX `POST`? + - [ ] Pada tutorial PBP minggu ini, pembersihan data _input_ pengguna dilakukan di belakang (_backend_) juga. Mengapa hal tersebut tidak dilakukan di _frontend_ saja? + - [ ] Jelaskan bagaimana cara kamu mengimplementasikan *checklist* di atas secara *step-by-step* (bukan hanya sekadar mengikuti tutorial)! +- [ ] Melakukan `add`-`commit`-`push` ke GitHub. + +## Tenggat Waktu Pengerjaan + +Tenggat waktu pengerjaan Tugas 6 adalah **Rabu, 9 Oktober 2024, pukul 12.00 siang**. + +Asisten dosen akan mengecek *last commit* dari repositori tugas lab, sehingga kamu tidak perlu mengumpulkan tautan repositori ke dalam slot submisi. diff --git a/docs/tutorial-5.md b/docs/tutorial-5.md new file mode 100644 index 0000000..1722233 --- /dev/null +++ b/docs/tutorial-5.md @@ -0,0 +1,836 @@ +--- +sidebar_label: Tutorial 5 +sidebar_position: 7 +Path: docs/tutorial-5 +--- + +# Tutorial 5: JavaScript dan AJAX + +Pemrograman Berbasis Platform (CSGE602022) — diselenggarakan oleh Fakultas Ilmu Komputer Universitas Indonesia, Semester Ganjil 2024/2025 + +--- + +## Tujuan Pembelajaran + +Setelah menyelesaikan tutorial ini, mahasiswa diharapkan untuk dapat: + +- Memahami fungsi JavaScript pada *front-end development* +- Menggunakan JavaScript secara dasar +- Menerapkan AJAX dan Fetch API dengan aman + +## JavaScript + +### Pengenalan JavaScript + +JavaScript merupakan bahasa pemrograman multi-paradigma tingkat tinggi lintas platform (*cross-platform high-level multi-paradigm programming language*). Sifat multi-paradigma membuat JavaScript mendukung konsep pemrograman berbasis obyek, pemrograman imperatif, dan pemrograman fungsional. JavaScript sendiri merupakan implementasi dari ECMAScript, yang merupakan inti dari bahasa JavaScript. Beberapa implementasi lain dari ECMAScript yang mirip dengan JavaScript antara lain JScript (Microsoft) dan ActionScript (Adobe). + +JavaScript, bersama dengan HTML dan CSS, menjadi tiga teknologi utama yang dipakai pada pengembangan web. Pada dasarnya, keuntungan menggunakan JavaScript dalam pengembangan web adalah manipulasi halaman web dapat dilakukan secara dinamis dan interaksi antara halaman web dengan pengguna dapat meningkat. Oleh karena itu, hampir semua situs web modern saat ini menggunakan JavaScript dalam halaman web mereka untuk memberikan pengalaman terbaik kepada pengguna. Beberapa contoh yang dapat kita lakukan dengan menggunakan JavaScript antara lain menampilkan informasi berdasarkan waktu, mengenali jenis peramban pengguna, melakukan validasi form atau data, membuat *cookies* (bukan kue, namun [HTTP *cookies*](https://en.wikipedia.org/wiki/HTTP_cookie)), mengganti *styling* dan CSS suatu *element* secara dinamis, dan lain sebagainya. + +Pada pengembangan web umumnya kode JavaScript digunakan pada *client-side* suatu web (*client-side JavaScript*), namun beberapa jenis kode JavaScript saat ini digunakan pada *server-side* suatu web (*server-side JavaScript*) seperti **node.js**. Istilah *client-side* menunjukkan bahwa kode JavaScript akan dieksekusi atau dijalankan pada peramban pengguna, bukan pada server situs web. Hal ini berarti kompleksitas kode JavaScript tidak akan memengaruhi performa server situs web tersebut namun memengaruhi performa peramban web dan komputer; semakin kompleks kode JavaScript, maka semakin banyak memori komputer yang dikonsumsi oleh peramban web. + +Pada mata kuliah PBP, kita hanya akan fokus kepada kode *client-side JavaScript*. + +### Tahapan Eksekusi JavaScript oleh Peramban + +Perhatikan diagram berikut untuk mengamati tahapan eksekusi JavaScript oleh peramban web. + +![javascript-works](https://preview.ibb.co/e258TG/Screenshot_from_2017_10_31_14_29_13.png) + +Setelah peramban mengunduh halaman HTML web maka tepat dimana tag `` berada, peramban akan melihat *tag script* tersebut, apakah tag tersebut berisi kode *embedded* JavaScript atau merujuk berkas eksternal JavaScript. Jika merujuk pada berkas eksternal JavaScript, maka peramban akan mengunduh berkas tersebut terlebih dahulu. + +### Penulisan JavaScript + +Penulisan JavaScript dapat dilakukan dengan ***embedded JavaScript*** atau ***external JavaScript***. Kode JavaScript dapat didefinisikan atau dituliskan secara *embedded* pada berkas HTML maupun secara terpisah pada berkas tersendiri. Jika ditulis dalam berkas terpisah dari HTML, ekstensi berkas yang digunakan untuk berkas JavaScript adalah `.js`. Berikut contoh beberapa pendefinisian dari JavaScript. + +JavaScript dapat diletakkan pada *head* atau *body* dari halaman HTML. Selain itu, kode JavaScript **harus** dimasukkan di antara tag ``. Kamu dapat meletakkan lebih dari satu *tag script* yang berisi JavaScript pada suatu berkas HTML. + +#### Embedded JavaScript pada HTML + +```html title="index.html" + +``` + +#### External JavaScript pada HTML + +```html title="index.html" + +``` + +```javascript title="js/script.js" +alert("Hello World!"); +``` + +Pada berkas eksternal JavaScript, tag ` + ``` + **Penjelasan Kode:** + + - Fungsi ini menggunakan `fetch()` API ke data JSON secara *asynchronous*. + - Setelah data di-*fetch*, fungsi `then()` digunakan untuk melakukan *parse* pada data JSON menjadi objek JavaScript. + +4. Buatlah **fungsi baru** pada *block* ` + ``` + + **Penjelasan Kode:** + + - `document.getElementById("mood_entry_cards")` digunakan untuk mendapatkan elemen berdasarkan ID nya. Pada baris kode ini, elemen yang dituju adalah tag `
` dengan ID `mood_entry_cards` yang sudah kamu buat pada tahapan sebelumnya. + - `innerHTML` digunakan untuk mengisi *child element* dari elemen yang dituju. Jika `innerHTML = ""`, maka akan mengosongkan isi *child element* dari elemen yang dituju. + - `className` digunakan untuk mengisi *class name* dari elemen yang dituju. + - `moodEntries.forEach((item))` digunakan untuk melakukan *for each loop* pada data *moods* yang diambil menggunakan fungsi `getMoodEntries()`. Kemudian, `htmlString` kita konkatenasi dengan data moods untuk mengisi container dengan *cards* seperti pada tutorial sebelumnya. + - `refreshMoodEntries()` digunakan untuk memanggil fungsi tersebut pada setiap kali membuka halaman web. + +## Tutorial: Membuat Modal Sebagai Form untuk Menambahkan _Mood_ + +1. Tambahkan kode berikut untuk mengimplementasikan modal (_Tailwind_) pada aplikasi kamu. Kamu dapat meletakkan potongan kode berikut dibawah `div` dengan `id` `mood_entry_cards` yang telah kamu tambahkan sebelumnya. + + ```html title="main/templates/main.html" + ... + + ... + ``` + + +2. Karena kita menggunakan _vanilla Tailwind CSS_, tidak ada _class_ modal yang _built-in_. Oleh karena itu, agar modal dapat berfungsi, kita perlu menambahkan fungsi-fungsi JavaScript berikut. + ```html title="main/templates/main.html" + + ``` + :::info + Form dan fungsi JavaScript pada modal tersebut sudah disesuaikan untuk model pada aplikasi `mental_health_tracker`. Apabila kamu ingin menggunakan model lain yang juga memiliki field lain, perhatikan kembali nilai-nilai dari atribut HTML terkait. + ::: + +3. Ubahlah bagian tombol _Add New Mood Entry_ yang sudah kamu tambahkan di tutorial sebelumnya seperti berikut. Kemudian, tambahkan tombol baru untuk melakukan penambahan data dengan AJAX. + + ```html title="main/templates/main.html" + ... + + Add New Mood Entry + + + ... + ``` + +## Tutorial: Menambahkan Data _Mood_ dengan AJAX + +Modal dengan form yang telah kamu buat sebelumnya belum bisa digunakan untuk menambahkan data _mood_. Oleh karena itu, kamu perlu membuat fungsi JavaScript baru untuk menambahkan data berdasarkan _input_ ke basis data secara AJAX. + +1. Buatlah fungsi baru pada block ` + ``` + **Penjelasan Kode:** + - `new FormData(document.querySelector('#moodEntryForm'))` digunakan untuk membuat sebuah `FormData` baru yang datanya diambil dari _form_ pada modal. Objek `FormData` dapat digunakan untuk mengirimkan data _form_ tersebut ke server. + - `document.getElementById("moodEntryForm").reset()` digunakan untuk mengosongkan isi *field* form modal setelah di-*submit*. + +2. Tambahkan fungsi `onclick` pada *button* "Add Product" pada modal untuk menjalankan fungsi `addMoodEntry()` dengan menambahkan kode berikut. + ```html title="main/templates/main.html" + + ``` + **Penjelasan Kode:** + - `document.getElementById("submitMoodEntry").onclick = addMoodEntry`: Apabila tombol _save_ pada modal (``) di-_click_, maka aplikasi ada akan memanggil fungsi `addMoodEntry()`. + +Selamat! Kamu telah berhasil membuat aplikasi yang dapat menambahkan data dengan menggunakan AJAX. Bukalah [http://localhost:8000/](http://localhost:8000/) dan cobalah untuk menambahkan data _mood entry_ baru pada aplikasi. Seharusnya, sekarang aplikasi tidak perlu melakukan *reload* setiap kali data _mood entry_ baru ditambahkan. + +## Tutorial: Melindungi Aplikasi dari _Cross Site Scripting (XSS)_ + +### Mencoba XSS + +Tahukah kamu bahwa kode yang baru saja kamu tambahkan ternyata membuka celah keamanan pada aplikasimu? Untuk melihat _severity_ dari celah keamanan ini, cobalah ikuti langkah-langkah berikut. + +1. Tambahkan data _mood_ baru dengan nilai _field_ `mood` sebagai berikut. _Field_ lain dapat diisi sesuai dengan keinginan kalian. + ```html + + ``` + +2. Tekan tombol simpan dan jika penyimpanan berhasil dilakukan, kamu akan mendapatkan _alert_ dengan nilai `XSS!` seperti di gambar berikut. + + ![Alert Box XSS](/img/5-alert-box-xss.png) + +### Menambahkan `strip_tags` untuk "Membersihkan" Data Baru + +Dari _input_ di atas, dapat dilihat bahwa orang yang berniat jahat dapat menambahkan data baru yang dapat mengeksekusi JavaScript di _browser_ pengguna aplikasi. Celah keamanan ini biasa disebut sebagai _Cross Site Scripting_ atau XSS. Lebih spesifiknya, dalam aplikasi ini terdapat _Stored XSS_, yang berarti _payload_ untuk XSS nya tersimpan dalam basis data aplikasi. Tentu saja kita tidak mau aplikasi yang kita buat untuk memiliki celah keamanan tersebut. Untuk menutup celah keamanan ini, kamu dapat mengikuti langkah-langkah berikut. + +1. Bukalah berkas `views.py` dan `forms.py` dan tambahkan impor berikut. + ```python title="main/views.py, main/forms.py" + from django.utils.html import strip_tags + ``` + +2. Pada fungsi `add_mood_entry_ajax` di `views.py`, gunakanlah fungsi `strip_tags` pada data `mood` dan `feelings` sebelum data tersebut dimasukkan ke dalam `MoodEntry`. + ```python title="main/views.py" + ... + @csrf_exempt + @require_POST + def add_mood_entry_ajax(request): + # highlight-start + mood = strip_tags(request.POST.get("mood")) # strip HTML tags! + feelings = strip_tags(request.POST.get("feelings")) # strip HTML tags! + # highlight-end + ... + ``` + + **Penjelasan Kode**: + - Fungsi `strip_tags` akan menghilangkan semua _tag_ HTML yang terdapat pada data yang dikirimkan pengguna melalui `POST` _request_, sehingga data yang disimpan dalam basis data adalah data yang sudah "bersih". Misal `data = "Pemrograman Platform Ganjil 2025"`, `strip_tags(data)` akan mengembalikan `Pemrograman Berbasis Platform Ganjil 2025`. + - Data `mood_intensity` tidak dibersihkan dengan `strip_tags` karena _field_ tersebut berupa `IntegerField` dan jika isinya bukan `int`, Django tidak akan menyimpannya ke basis data. + +3. Pada _class_ `MoodEntryForm` di `forms.py` tambahkan kedua _method_ berikut. + ```python title="main/forms.py" + ... + class MoodEntryForm(ModelForm): + class Meta: + ... + + # highlight-start + def clean_mood(self): + mood = self.cleaned_data["mood"] + return strip_tags(mood) + + def clean_feelings(self): + feelings = self.cleaned_data["feelings"] + return strip_tags(feelings) + # highlight-end + ... + ``` + + **Penjelasan Kode**: + - _Method_ `clean_mood` dan `clean_feelings` akan dipanggil ketika melakukan `form.is_valid()`, sehingga dengan menambahkan kedua _method_ tersebut, kamu sudah melakukan validasi untuk fungsi `create_mood_entry` dan `edit_mood`. + +4. Setelah menambahkan `strip_tags`, hapuslah data _mood_ yang tadi kamu tambahkan dan coba tambahkan kembali. Jika kamu dihadapkan dengan _error_ pada form yang mengatakan _field_ `mood` tidak boleh kosong, maka selamat, kamu sudah menambahkan pertahanan terhadap XSS! Jika kamu tidak dihadapkan dengan _error_ tersebut, periksa kembali apakah kamu sudah mengikuti langkah-langkah sebelumnya dengan sesuai. + + ![Form Error](/img/5-form-error.png) + +### Membersihkan Data dengan DOMPurify + +Fungsi `strip_tags` yang kamu tambahkan akan "membersihkan" semua data **baru**, tetapi data yang lama harus kita bersihkan sendiri atau kita me-_reset_ basis data. Atau, kita juga bisa menggunakan _library_ JavaScript [DOMPurify](https://github.com/cure53/DOMPurify) untuk melakukan pembersihan di _frontend_. Perlu diperhatikan bahwa DOMPurify hanya akan bekerja jika data diambil untuk ditampilkan dengan HTML di aplikasi kita. Jika ada yang menggunakan API `/json` atau `/xml` dari aplikasi kita, maka data yang didapatkan masih data yang "kotor". + +1. Bukalah berkas `main.html` dan tambahkan potongan kode berikut pada _block_ `meta`. + ```html title="main/templates/main.html" + {% block meta %} + ... + + ... + {% endblock meta %} + ``` + +2. Setelah itu, pada fungsi `refreshMoodEntries` yang telah kamu tambahkan sebelumnya, tambahkan potongan kode berikut. + ```html title="main/templates/main.html" + + ``` + + :::note + Jangan lupa untuk mengubah semua kemunculan `item.fields.mood` menjadi `mood` dan `item.fields.feelings` menjadi `feelings`. + ::: + +3. _Refresh_ halaman utama dan jika kamu sebelumnya memiliki data yang "kotor" seperti yang memunculkan _alert box_, _alert box_ tersebut seharusnya tidak muncul lagi pada _browser_ mu. + +## Penutup + +- Setelah menjalankan tutorial di atas, Anda seharusnya memiliki struktur direktori lokal seperti berikut. +``` + mental-health-tracker + ├── .github\workflows + │ └── push.yml + ├── env + ├── main + │ ├── migrations + │ │ ├── __init__.py + │ │ ├── 0001_initial.py + │ │ ├── 0002_alter_moodentry_id.py + │ │ └── 0003_moodentry_user.py + │ ├── templates + │ │ ├── create_mood_entry.html + │ │ ├── card_info.html + │ │ ├── card_mood.html + │ │ ├── edit_mood.html + │ │ ├── login.html + │ │ ├── main.html + │ │ └── register.html + │ ├── __init__.py + │ ├── admin.py + │ ├── apps.py + │ ├── forms.py + │ ├── models.py + │ ├── tests.py + │ ├── urls.py + │ └── views.py + ├── mental_health_tracker + │ ├── __init__.py + │ ├── asgi.py + │ ├── settings.py + │ ├── urls.py + │ └── wsgi.py + ├── templates + │ └── base.html + │ └── navbar.html + ├── static + │ └── css + │ └── global.css + │ └── image + │ └── sedih-banget.png + ├── .gitignore + ├── manage.py + └── requirements.txt +``` +## Referensi Tambahan + +- [HackTricks Cross Site Scripting](https://book.hacktricks.xyz/pentesting-web/xss-cross-site-scripting) +- [OWASP Cross Site Scripting](https://owasp.org/www-community/attacks/xss/) + +## Kontributor + +- Juan Maxwell Tanaya +- Muhammad Faishal Adly Nelwan +- Muhammad Irfan Firmansyah + +## Credits + +Tutorial ini dikembangkan berdasarkan [PBP Ganjil 2024](https://github.com/pbp-fasilkom-ui/ganjil-2024) dan [PBP Genap 2024](https://github.com/pbp-fasilkom-ui/genap-2024) yang ditulis oleh Tim Pengajar Pemrograman Berbasis Platform 2024. Segala tutorial serta instruksi yang dicantumkan pada repositori ini dirancang sedemikian rupa sehingga mahasiswa yang sedang mengambil mata kuliah Pemrograman Berbasis Platform dapat menyelesaikan tutorial saat sesi lab berlangsung. diff --git a/i18n/en/docusaurus-plugin-content-docs-assignments/current/individual/assignment-6.md b/i18n/en/docusaurus-plugin-content-docs-assignments/current/individual/assignment-6.md new file mode 100644 index 0000000..2b3509f --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs-assignments/current/individual/assignment-6.md @@ -0,0 +1,50 @@ +--- +sidebar_label: Assignment 6 +sidebar_position: 6 +Path: assignment/individual/assignment-6 +--- + +# Assignment 6: JavaScript and Asynchronous JavaScript + +Platform-Based Programming (CSGE602022) — Organized by the Faculty of Computer Science Universitas Indonesia, Odd Semester 2024/2025 + +--- + +## Assignment Description + +In this assignment, you have to implement AJAX in the application that you created in the previous assignment. + +Checklist for this assignment is as follows: + +- [ ] Modify the previously created assignment 5 to use AJAX. + - [ ] AJAX GET + - [ ] Modify the codes in data cards to able to use AJAX GET. + - [ ] Retrieve data using AJAX GET. Make sure that the datas retrieved are only the datas belonging to the logged in user. + - [ ] AJAX POST + - [ ] Create a button that opens a modal with a form for adding a mood entry. + + :::note + The modal is triggered by clicking a button on the main page. When adding a mood entry successfully, the modal should be closed, and the form input should be cleared from the data entered before. If adding the mood entry fails, show an error message. + ::: + + - [ ] Create a new view function to add a new mood entry to the database. + - [ ] Create a `/create-ajax/` path that routes to the new view function you created. + - [ ] Connect the form you created inside the modal to the `/create-ajax/` path. + - [ ] Perform asynchronous refresh on the main page to display the latest item list without reloading the entire main page. + :::warning + Make sure the AJAX `GET` and `POST` can be done securely. + ::: +- [ ] Answer the following questions in `README.md` in the root folder (please modify the `README.md` you have created; add subheadings for each task). + - [ ] Explain the benefits of using JavaScript in developing web applications! + - [ ] Explain why we need to use `await` when we call `fetch()`! What would happen if we don't use `await`? + - [ ] Why do we need to use the `csrf_exempt` decorator on the view used for AJAX `POST`? + - [ ] On this week's tutorial, the user input sanitization is done in the back-end as well. Why can't the sanitization be done just in the front-end? + - [ ] Explain how you implemented the checklist above step-by-step (not just following the tutorial)! + +- [ ] Perform `add`-`commit`-`push` to GitHub. + +## Deadline + +The deadline for Assignment 6 is **Wednesday, 9th October 2024, at 12:00 p.m.** + +The teaching assistants will check the last commit of the lab assignment repository, so you do not need to submit the repository link separately. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/tutorial-5.md b/i18n/en/docusaurus-plugin-content-docs/current/tutorial-5.md new file mode 100644 index 0000000..9745391 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/tutorial-5.md @@ -0,0 +1,834 @@ +--- +sidebar_label: Tutorial 5 +sidebar_position: 7 +Path: docs/tutorial-5 +--- + +# Tutorial 5: JavaScript and AJAX + +Platform-Based Programming (CSGE602022) — Organized by the Faculty of Computer Science Universitas Indonesia, Odd Semester 2024/2025 + +--- + +## Learning Objectives + +After completing this tutorial, students are expected to be able to: + +- Understand the usage of JavaScript in the context of front-end development +- Understand the basics of JavaScript +- Apply AJAX and Fetch API safely + +## JavaScript + +### Introduction to JavaScript + +JavaScript is a high-level, multi-paradigm programming language. The multi-paradigm feature of JavaScript allows it to support object-oriented programming, imperative programming, and functional programming. JavaScript itself is an implementation of ECMAScript, that serves as a baseline for the JavaScript language. Other implementations of ECMAScript that is similar to JavaScript are JScript (Microsoft) and ActionScript (Adobe). + +JavaScript, with HTML and CSS, is one of the three main technologies used in web development. The benefit of using JavaScript in web development is that dynamic page manipulation can be done and interaction between web pages and users can be increased. That is why most modern websites are using JavaScript on their web page to give the best experience to the user. Some things that we could do with JavaScript are to display information based on time, recognizing the type of device used, do validation on forms or data, creating cookies (not literally, but [HTTP cookies](https://en.wikipedia.org/wiki/HTTP_cookie)), change the CSS of an element dynamically, etc. + +Usually, JavaScript is used on the client-side of a web (client-side JavaScript), but there are some types of JavaScript that are used on the server-side of a web (server-side JavaScript) like **node.js**. Client-side means that the JavaScript code will be executed on the user's system, not on the website's server. This means that the JavaScript code complexity will not affect the server's performance but will affect the user's system performance; where more complex JavaScript will force the browser to use more memory. + +In the PBD course, we will only focus on the client-side JavaScript. + +### JavaScript Execution by Browser + +Take a look at the diagram below to understand how JavaScript execution is done by the browser. + +![javascript-works](https://preview.ibb.co/e258TG/Screenshot_from_2017_10_31_14_29_13.png) + +After the browser downloads the HTML web page, the browser will look for the `` tag, and the browser will see the tag script if the tag contains embedded JavaScript or external JavaScript. If the script tag refers to an external JavaScript code, the browser will download that file first. + +### JavaScript Writing + +JavaScript could be added as an **embedded JavaScript** or **external JavaScript**. JavaScript code could be defined or written as an *embedded* code in an HTML file or as a separate file. If written outside of the HTML, the file extension for the JavaScript file is `.js`. Below is an example of how to define a JavaScript code. + +JavaScript could be placed in the head or body of an HTML file. In addition, the JavaScript code **must** be placed between the `` tags. You can place more than one script tag containing JavaScript code in an HTML file. + +#### Embedded JavaScript pada HTML + +```html title="index.html" + +``` + +#### External JavaScript pada HTML + +```html title="index.html" + +``` + +```javascript title="js/script.js" +alert("Hello World!"); +``` + +On the external JavaScript file, the ` + ``` + **Code Explanation:** + - This function uses the `fetch()` API to fetch the JSON data asynchronously. + - After the data is fetched, the `then()` function is used to parse the JSON data into a JavaScript object. + +5. Create a **new function** on the ` + ``` + + **Code Explanation:** + - `document.getElementById("mood_entry_cards")` is used to get the element based on its ID. In this code, the element that is being targeted is the `
` tag with the ID `mood_entry_cards` that you have created previously. + - `innerHTML` is used to fill in the child element of the element that is being targeted. If `innerHTML = ""`, then it will clear the contents of the child element of the element that is being targeted. + - `className` is used to fill in the class name of the element that is being targeted. + - `moodEntries.forEach((item))` is used to perform a *for each loop* on the data *moods* that is retrieved using the `getMoodEntries()` function. Then, `htmlString` will be concatenated with the data moods to display in the container with the *cards* like in the previous tutorial. + - `refreshMoodEntries()` is used to call the function above when the page is loaded. + +## Tutorial: Creating Modal as a Form to Add a Mood + +1. Add the following code to implement the modal (Tailwind) on your application. You can place the following code below the `div` with the `id` `mood_entry_cards` that you have added previously. + + ```html title="main/templates/main.html" + ... + + ... + ``` +2. Because we are using vanilla Tailwind CSS, there is no built-in modal class. Therefore, to make the modal work, we need to add the following JavaScript functions. + + ```html title="main/templates/main.html" + + ``` + :::info + The form and JavaScript functions in the modal that we have created are adapted to the model in the `mental_health_tracker` application. + + To create a modal without using JavaScript, we can use a built-in class in a Tailwind CSS JavaScript Library, one of which is [_Flowbite_](https://flowbite.com/). You can read more about the modal on the _Flowbite Tailwind CSS_ at [here](https://flowbite.com/docs/components/modal/). + ::: + +3. Change the Add New Mood Entry button that you have added in the tutorial above and add a new button to perform the addition of data with AJAX. + + ```html title="main/templates/main.html" + ... + + Add New Mood Entry + + + ... + ``` + +## Tutorial: Adding Data Mood with AJAX + +The modal with the form that you have created previously cannot be used to add data mood. Therefore, you need to create a new JavaScript function to add data based on the input to the data base in an AJAX. + +1. Create a new function in the block ` + ``` + **Code Explanation:** + - `new FormData(document.querySelector('#moodEntryForm'))` is used to create a new `FormData` object that contains the data from the form in the modal. The `FormData` object can be used to send the form data to the server. + - `document.getElementById("moodEntryForm").reset()` is used to clear the contents of the field form modal after submitting. + +2. Add the `onclick` function to the "Add Product" button in the modal to run the `addMoodEntry()` function with the following code. + ```html title="main/templates/main.html" + + ``` + + **Code Explanation:** + - `document.getElementById("submitMoodEntry").onclick = addMoodEntry`: When the save button in the modal (``) is clicked, then the application will call the `addMoodEntry()` function. + +Congratulations! You have successfully created an application that can add data using AJAX. Go to [http://localhost:8000/](http://localhost:8000/) and try to add a new mood entry to the application. Now, the application should not need a reload every time a new mood entry has been added. + +## Tutorial: Protecting the Application from Cross Site Scripting (XSS) + +### Trying XSS + +Did you notice that the code that you have just added actually opens a security hole in the application? To see the severity of the security hole, follow the steps below. + +1. Add a new mood entry with the value of the `mood` field as follows. Other fields can be filled in according to your preference. + ```html + + ``` + +2. Press the save button and if the storage is successful, you will get an alert with the value `XSS!` as shown in the figure below. + + ![Alert Box XSS](/img/5-alert-box-xss.png) + +### Adding `strip_tags` to "Clean Up" New Data + +From the input above, it can be seen that a hacker can add new data that can execute JavaScript in the user's browser application. The security hole is usually called Cross Site Scripting or XSS. More specifically, in this application, there is a Stored XSS, which is a payload for XSS that is stored in the application's data base. Of course, we do not want the application that we are creating to have a security hole. To close the security hole, you can follow the steps below. + +1. Open the `views.py` and `forms.py` files and add the following imports. + ```python title="main/views.py, main/forms.py" + from django.utils.html import strip_tags + ``` + +2. In the `add_mood_entry_ajax` function in the `views.py` file, use the `strip_tags` function on the `mood` and `feelings` data before the data is inserted into the `MoodEntry`. + ```python title="main/views.py" + ... + @csrf_exempt + @require_POST + def add_mood_entry_ajax(request): + # highlight-start + mood = strip_tags(request.POST.get("mood")) # strip HTML tags! + feelings = strip_tags(request.POST.get("feelings")) # strip HTML tags! + # highlight-end + ... + ``` + **Code Explanation**: + - The `strip_tags` function will remove all HTML tags that are present in the data sent by the user through the `POST` request, so that the data that is stored in the data base is clean. For example, `data = "Programming Platform Genap 2025"`, `strip_tags(data)` will return `Programming Basic Platform Genap 2025`. + - The `mood_intensity` field is not cleaned up with `strip_tags` because the field is an `IntegerField` and if it is not an `int`, Django will not store it in the data base. + +3. On the `MoodEntryForm` class in the `forms.py` file, add the following two methods. + ```python title="main/forms.py" + ... + class MoodEntryForm(ModelForm): + class Meta: + ... + + # highlight-start + def clean_mood(self): + mood = self.cleaned_data["mood"] + return strip_tags(mood) + + def clean_feelings(self): + feelings = self.cleaned_data["feelings"] + return strip_tags(feelings) + # highlight-end + ... + ``` + + **Code Explanation**: + - The `clean_mood` and `clean_feelings` methods will be called when `form.is_valid()` is called, so by adding the two methods, you have done validation for the `create_mood_entry` and `edit_mood` functions. + +4. After adding `strip_tags`, remove the data that you have just added and try to add it again. If you get an error on the form that says the `mood` field cannot be empty, then congratulations, you have added a security hole against XSS! If you do not get an error, check again whether you have followed the steps above. + + ![Form Error](/img/5-form-error.png) + +### Sanitizing Data with DOMPurify + +The `strip_tags` function that you have added will "clean up" all new data, but old data must be cleaned up itself or we can use a JavaScript library called [DOMPurify](https://github.com/cure53/DOMPurify) for cleaning up on the frontend side. It is important to note that DOMPurify will only work if the data is retrieved to be displayed with HTML on the application's frontend. If there is an API `/json` or `/xml` used by the application, then the data that is obtained will still be "dirty". + +1. Open the `main.html` file and add the following code to the `meta` block. + ```html title="main/templates/main.html" + {% block meta %} + ... + + ... + {% endblock meta %} + ``` + +2. After that, add the following code to the `refreshMoodEntries` function that you have added previously. + ```html title="main/templates/main.html" + + ``` + + :::note + Don't forget to change all occurrences of `item.fields.mood` to `mood` and `item.fields.feelings` to `feelings`. + ::: + +4. Refresh the main page and if you have previously had dirty data like the alert box that shows up, then the alert box should no longer appear on the browser. + +## Closing + +- After running the tutorial above, you should have the local directory structure as follows. +``` + mental-health-tracker + ├── .github\workflows + │ └── push.yml + ├── env + ├── main + │ ├── migrations + │ │ ├── __init__.py + │ │ ├── 0001_initial.py + │ │ ├── 0002_alter_moodentry_id.py + │ │ └── 0003_moodentry_user.py + │ ├── templates + │ │ ├── create_mood_entry.html + │ │ ├── card_info.html + │ │ ├── card_mood.html + │ │ ├── edit_mood.html + │ │ ├── login.html + │ │ ├── main.html + │ │ └── register.html + │ ├── __init__.py + │ ├── admin.py + │ ├── apps.py + │ ├── forms.py + │ ├── models.py + │ ├── tests.py + │ ├── urls.py + │ └── views.py + ├── mental_health_tracker + │ ├── __init__.py + │ ├── asgi.py + │ ├── settings.py + │ ├── urls.py + │ └── wsgi.py + ├── templates + │ └── base.html + │ └── navbar.html + ├── static + │ └── css + │ └── global.css + │ └── image + │ └── sedih-banget.png + ├── .gitignore + ├── manage.py + └── requirements.txt +``` + +## Additional References + +- [HackTricks Cross Site Scripting](https://book.hacktricks.xyz/pentesting-web/xss-cross-site-scripting) +- [OWASP Cross Site Scripting](https://owasp.org/www-community/attacks/xss/) + +## Contributors + +- Juan Maxwell Tanaya +- Muhammad Faishal Adly Nelwan +- Muhammad Irfan Firmansyah +- Muhammad Oka (EN Translation) + +## Credits + +This tutorial was developed based on [PBP Odd 2024](https://github.com/pbp-fasilkom-ui/ganjil-2024) and [PBP Even 2024](https://github.com/pbp-fasilkom-ui/genap-2024) written by the 2024 Platform-Based Programming Teaching Team. All tutorials and instructions included in this repository are designed so that students who are taking Platform-Based Programming courses can complete the tutorials during lab sessions. diff --git a/static/img/5-alert-box-xss.png b/static/img/5-alert-box-xss.png new file mode 100644 index 0000000000000000000000000000000000000000..fe22e509db0ebda2c32c9aaa56b94997106b835a GIT binary patch literal 5257 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 EKXO!Lxc~qF literal 0 HcmV?d00001 diff --git a/static/img/5-form-error.png b/static/img/5-form-error.png new file mode 100644 index 0000000000000000000000000000000000000000..ffb4b1d84ec11369899b4f2908d8e9362f67aa9d GIT binary patch literal 2695 zcmb7Gc`)0H7XBsnwY|^Q+LxlY#J;3biYm3#($v~YN|kCX38i*Y4=o;5wZs-`t5)q? zyF4wSl&Yc{u`4c#Jw$}({`3BM_uYB#%$#lJ%$Yee-#6!bZfkAE$1TbY005u)4U<~{ z0LnY9-8tD$qes<)#nS`|yk%w#R1Jx*o;IvrMmLQBpf;7~$c^o^=R)6b4g>)H4}Um_ z=>Nt80Jz1?O^j}bASh%qN_^rx5yRA`i##snxn^|lqLYf<#dC>9MIznoSuv4tWM3IH z7-L&la`3v-1^z-^oo%8dKF73CrS-x+&N{mnzjmjgEH60O8J-OZE7;Kp^rQ`T_O+QQ ztunY`wN#e}apxIy3eKnR;|ao_3jm4>&+jnL_%Bh0s?I`zm^2wQ0670qDRYVghQh4; zfHV)719-uPWdZu05yXPj?|*y7d{2cV{=l28#U)6x&0lOcwrwtOa?yhsu?O!c=1du>dzv~1-%*ZZ0( z*c7CT?T_Eo3Wco&oP4@WkBhvns}$%c*_V_*6bbJ-cJhlpf}I}JYa34pl~+9pvsS3C z{0@XN_6IcfICjEe+wUM-_=(&LsFg@&v^*>U%zp-_5hE@@wqc4}$buh9qmmvxyzq^8!n;H0!YNZwt zX8BXgX;8UL4n3sY&*SUaLckqru@3q7SVeK`pvzomS1$WqQ9*R}56B}bKm_O~K`7Ne z=a_-RU1j)Jjv*`Z-*nxADd8h2hzc@ZcMcw2O2cv$>(lLy>EV&&rVGQr?R)sQL6Yj* zZ?VK+UO~`{T>l%zL(hF}nn5PJcCi6!)CNx!Wn9$2N{&bTrkR8qFpK^wYA}c!>I0jVdkp#j zvzuQQT1E2Q9XgBIM0L*hHzTtZN4QnqONtWE zIXyrN=8j!J#94ha2+fSMK$d2^FI*k7UfC%6N0sA`>!iKCf&4Hd?uOwN=$*o;P?s`J zAyR{(5K*2R@ie9F;DR5ioV1FmLe_trm`$1&fbo-&kU~t`r%0@r{GBz;-NHYb!NQ4&lgsX^5iPbstD#* zLnO00%4(>LGYil5GxklcGZ{K#>fd5jB`qU-_ttb0(WzOxjU^>XQOu~nGZw3BZ|$Op z3Qv11d{dk3Hym}Tk{5GA!6C^WGg2mg>Kz4^Gz&yzmyx%|>Rw!ha?It=E<| zDt=aHszV*EM-mESvnDpK9M>RV4y*dq=HA^#r>2v-)R4TT9n7$t{r7NoYvNw$ zbM)%^yGdyxm^hZko7Y-rJI3vvMZgeE+3MyjS(*q53yH?#?>5J2@?(i_eesW$Kh)gk zHSK727)MczXC&BKo;-WZzSvePBXF3~Sv7SOF*r}myL8l}S;y2$u9ZW-HQMtivb+Ia z8K~vVbDI@d9~qgbuT0o67VidVv4qv6s7pTus3Rk5p{uPeBgdVmyG@=7 z-=>6*Jz!GBCR>k-BC-y;+tEipXDcSP<0~*A^j;xm>LDOx|NHm zMxK1C<}F)2rcQNaPEA|FqXD%yX~XDXYScxsk!aX%6vwH4+Zl3P{e z9o~rS>IjBp)E*EeCN--}1)3l(>fRU1}NC%-inNMyA6OsQ&G`z?Xzr+?-(CuweUIRgDl^Ey4%&61zb9 zyUlX&$h>zGiSV5JO?pV_I3~GxS?RD+$gR9RevxL<)q^2}6}g3VueB=VZaE#@>arPA zZ{AwFds*0Rcv88Eh#A?3twWYelc z9;`|65RyVNo87&1G^SnxChiGQQP56$j@9=;T;aQ94uD;^30wSo4_uywC_M79@*-;r zS!z!RVMn(oCYRDIAc82RN27Q1;3O3DQTFbh#d}^hi3G@jUc3n>(Z^6QGIJuIe>ot! z8kNX+KP3XJjDBZJfmF=}+04U(rjYU_K56{b&m*v^U}8;=-Y#jg6l)>n^4;~t7>QRH zYFavTaBe0#QaP->K(*mH&5>#&8}XP|%#({)Le0q4Z>5D*`fV)d_`ozXby@jiBF}}M p>Y>6vVZvXErT