Colophon

The HTML version of this book can be found here and the PDF here.

1. Introduction

From Michael Nielsen (emphasis mine):

Technologies come and technologies go, but insight is forever.

and

Struggling with a project you care about will teach you far more than working through any number of set problems. Emotional commitment is a key to achieving mastery.

Before we learn to develop web applications, we need a basic understanding of the Internet and the World Wide Web. For a more in-depth explanation of these two terms, take a look at en.wikipedia.org/wiki/Internet and en.wikipedia.org/wiki/Hypertext_Transfer_Protocol. The Internet is a globally distributed network of networks. It connects billions of computers and devices allowing them to communicate with each other. The World Wide Web (WWW) is just one of many services running on the Internet. It is a huge collection of documents and other resources interlinked via hyperlinks. Each resource has a uniform resource locator (URL), which gives access to this resource. Typically we use browsers (e.g. Mozilla Firefox, Google Chrome, Microsoft Edge, Apple Safari) to access the Internet. Browsers use the Hypertext Transfer Protocol (HTTP) to communicate with other computers, so called web servers, on the Internet. The Internet uses a whole suite of protocols that are split into several layers. At the top level, the application layer, we have HTTP and many other protocols. Below, on the transport layer, we have the Transport Control Protocol (TCP). Beneath this layer we have the Internet Protocol (IP) on the Internet layer.

The WWW has evolved significantly since the early nineties. Today the web browser and related technologies are increasingly becoming the platform of choice for application development, for a number of reasons:

  1. Write once run anywhere. A web browser is installed by default on virtually every desktop, tablet, smartphone and other devices. A web application will run on all of these devices without requiring the user to download and install anything or the developer to provide executables for different operating systems.

  2. Updates are instantaneous, i.e. the next time the user uses the application, he/she will automatically be using the latest version.

  3. The performance of browser JavaScript engines rivals the best Java just in time compilers (JIT) and the gap to compiled C++ and assembler is dwindling. Today’s web apps use multithreading, accelerated 3D graphics and many other techniques that make full use of the available hardware.

  4. There are a large amount of standard application programming interfaces (API) as well as highly sophisticated open source libraries for all kinds of purposes.

  5. A virtually unlimited amount of documentation is available.

The following is a small sample list of web applications to provide a glimpse of what can be done:

The next step in terms of Internet development appears to be decentralization, see for instance web3.foundation.

2. Operating systems

Some recommendations on how to choose between Linux and Windows as a server platform can be found at www.inap.com/blog/linux-servers-vs-microsoft-windows-servers and www.1and1.com/digitalguide/server/know-how/linux-vs-windows-the-big-server-check among many others.

2.1. Ubuntu

What are Linux and Ubuntu? To find out see en.wikipedia.org/wiki/Linux_distribution, en.wikipedia.org/wiki/Ubuntu and help.ubuntu.com. A great overview of Linux software can be found at www.linuxlinks.com.

2.1.1. Installation and configuration

For a quick start, see here. The server guide provides the details. Get the ISO from Ubuntu and create a bootable USB stick using for instance Rufus.

If you want to upgrade an existing installation, see www.cyberciti.biz/faq/upgrade-ubuntu-18-04-to-20-04-lts-using-command-line.

2.1.1.1. Missing .Xauthority file

If you log in via SSH and get this message use

ssh -X user@host

to have the file created (source).

2.1.1.2. Nvidia GPU

If you are installing a GPU next to a processor with on-board graphics, your BIOS might switch automatically to the GPU. If you have no display connected to the GPU, you’ll only see a blank screen. Check the corresponding BIOS setting. On an MSI motherboard this may be called "Initiate Graphic Adapter" with a choice between "PEG" and "IGD". The latter one refers to the integrated graphics (cf. forums.tomshardware.com/threads/msi-motherboard-igd-only-no-peg-bios-settings.3307018/#post-20227120). Also, given the size of today’s GPUs, chances are that you will not be able to remove it from the PCIe socket without breaking the plastic lock.

It is strongly recommended to only use a supported LTS version of Ubuntu, particularly if you intend to use CUDA, see docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html.

You might have to disable secure boot in your BIOS in order to be able to install the Nvidia driver.

If ubuntu-drivers list returns nothing, use apt to install the Nvidia driver:

apt list nvidia-driver*
apt install nvidia-driver-x nvidia-prime

Now you can use prime-select or the Nvidia X Server Settings GUI to switch between GPUs (cf. askubuntu.com/questions/661922/how-am-i-supposed-to-use-nvidia-prime#661924).

lspci -k | grep -EA3 'VGA|3D|Display' lists all your VGA controllers (cf. askubuntu.com/questions/1253127/nvidia-driver-is-not-detected-in-ubuntu-20-04-lts).

To install cuDNN you need to think about how you want to use it. If you are planning to use it from Python you should not install it using apt but using mamba or conda in a virtual environment, like so:

mamba create -n gpu
mamba activate gpu
mamba install tensorflow-gpu torch torchvision

If you want to use cuDNN outside Python, you might have to install it using apt. However, this may create problems when trying to also use it in Python, see askubuntu.com/questions/1493317/how-to-install-or-enable-cuda-and-cudnn-in-ubuntu-23-10:

apt install nvidia-cudnn nvidia-cuda-toolkit

If this leads to unmet dependencies, you might have to run apt install -f, cf. fedingo.com/how-to-resolve-unmet-dependencies-in-ubuntu.

If the CUDA samples (cf. www.freecodecamp.org/news/how-to-install-nvidia-cuda-toolkit-on-ubuntu) makefile cannot find nvcc, you might have to

mkdir -p /usr/local/cuda/bin
ln -s /usr/bin/nvcc /usr/local/cuda/bin/nvcc
To monitor the usage of your Nvidia GPU, use nvtop which can be installed via apt install nvtop.

ubuntu.com/server/docs/nvidia-drivers-installation

www.yodiw.com/install-cuda-11-on-ubuntu-23-10

ubuntuhandbook.org/index.php/2021/06/install-nvidia-driver-switch-between-intel-nvidia-ubuntu

devicetests.com/nvidia-driver-ubuntu

www.golinuxcloud.com/install-nvidia-drivers-on-ubuntu/#Checking_GPU_and_Driver_Compatibility

discourse.ubuntu.com/t/nvidia-drivers-installation/39022

forums.developer.nvidia.com/t/issues-installing-550-drivers-under-ubuntu-23-10-mantic/280713/2

www.youtube.com/watch?v=UHwjD6T1kzQ

docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html

www.freecodecamp.org/news/how-to-install-nvidia-cuda-toolkit-on-ubuntu

developer.nvidia.com/cuda-gpus

docs.nvidia.com/deeplearning/cudnn/reference/support-matrix.html#support-matrix

medium.com/@juancrrn/installing-cuda-and-cudnn-in-ubuntu-20-04-for-deep-learning-dad8841714d6

2.1.2. Administration

To simplify package management you might want to install wajig.

2.1.2.1. Command line

A quick reference of useful administration commands can be found here.

To learn the Linux command line study linuxcommand.org.

2.1.2.2. Useful commands
2.1.2.2.1. List all files in a directory and subdirectories
ls -alR
2.1.2.2.2. Recursively search for strings in files
grep -rHn "string" /path
2.1.2.2.3. Kernel version
uname -r
2.1.2.2.4. Last logged in users
last
2.1.2.2.5. Determine Ubuntu version
lsb_release -a

or

cat /etc/issue
2.1.2.2.6. Locate a file
whereis <filename>
2.1.2.2.7. List all files that were created today
find . -daystart -ctime 0 -print
2.1.2.2.8. find without permission denied messages
find / -name <name> -print 2>&-

which is equivalent to (cf. source):

find / -name <name> -print 2>/dev/null
2.1.2.2.9. chmod

To chmod all directories but not files:

find . -type d -exec chmod o+rx {} +

To chmod all executable files:

find . -executable -type f -exec chmod o+rx {} +

To apply the same permissions:

chmod --reference=source target
2.1.2.2.10. chown

To apply the same ownership:

chown --reference=source target
2.1.2.2.11. Delete all files that match a pattern
find . -name '*.orig' -print -delete
2.1.2.2.12. Activate root

sudo passwd root and give root a password. Afterwards, you can for instance run su -.

2.1.2.2.15. Setting umask permanently
2.1.2.2.16. Reconfigure package

dpkg-reconfigure package

2.1.2.2.17. Uninstall package completely

apt purge package

2.1.2.2.20. Nano jump to beginning or end of file

To jump to the beginning use Ctrl+W followed by Ctrl+Y. To jump to the end use Ctrl+W followed by Ctrl+V.

2.1.2.2.22. Nano configuration file

www.nano-editor.org/dist/latest/nanorc.5.html

2.1.2.2.23. Monitor socket connections

ss

2.1.2.2.25. View contents of compressed files
2.1.2.2.26. Exclude files/folders from archives
# https://stackoverflow.com/questions/984204/shell-command-to-tar-directory-excluding-certain-files-folders
# Note the files/folders excluded are relative to the root of your tar.
tar cfz /media/Backup232/www`date +%a`.tar.gz --exclude='owncloud' --exclude='everling.lu/WAD' /var/www 2>/dev/null
2.1.2.2.27. Redirect output streams
  1. Redirect stdout to one file and stderr to another file: command > out 2>error

  2. Redirect stderr to stdout (&1), and then redirect stdout to a file: command >out 2>&1

  3. Redirect both to a file: command &> out

2.1.2.2.28. Use ? wget to recursively download all files of a type
To use wget to mirror a web site, see www.eff.org/keeping-your-site-alive/mirroring-your-site.
2.1.2.2.29. User login history

lastlog showing data from /var/log/lastlog.

To see detailed info view /var/log/auth.log.

2.1.2.2.31. Display your public IP address
2.1.2.2.34. Count the number of files in a directory
2.1.2.2.36. List all installed packages
apt list --installed
2.1.2.2.39. Fix the GPG error "NO_PUBKEY"
2.1.2.2.40. Inhibit EMS messages at login
2.1.2.2.42. How to get CPU info
lscpu
2.1.2.2.43. Find out which filesystem is used by a device
blkid
2.1.2.2.44. Set the shell history to unlimited
HISTSIZE=-1
HISTFILESIZE=-1
2.1.2.4. Turn airplane mode on/off via terminal
2.1.2.6. Security

www.cyberciti.biz/tips/linux-security.html

www.debian.org/doc/manuals/securing-debian-manual/index.en.html

bookofzeus.com

2.1.2.6.1. SSL/TLS

Proceed as follows to create a self signed certificate to be able to use HTTPS (cf. websiteforstudents.com/create-ssl-tls-self-signed-certificates-on-ubuntu-16-04-18-04-18-10):

openssl genrsa -aes256 -out server.key 4096
openssl rsa -in server.key -out server.key
openssl req -new -days 2000 -key server.key -out server.csr
openssl x509 -req -sha512 -days 2000 -in server.csr -signkey server.key -out server.crt
2.1.2.6.2. Firewall

help.ubuntu.com/community/UFW

www.tecmint.com/setup-ufw-firewall-on-ubuntu-and-debian

To delete several rules at once, we can use a loop:

for i in {28..5};do yes|ufw delete $i;done
2.1.2.6.3. Antivirus
2.1.2.6.4. VPN

askubuntu.com/questions/1107723/how-to-connect-to-the-vpn-automatically-on-system-startup

If you are getting "Cannot import VPN connection …​ Error: the plugin does not support import capability" use nmcli connection import type openvpn file <filename> to detect errors in the file. If there are none the connection will be added (cf. askubuntu.com/questions/760345/cannot-import-saved-openvpn-configuration-file-in-ubuntu-16-04-lts).
2.1.2.7. Networking
2.1.2.7.1. Problem solving

If Ubuntu loses the Ethernet connection, apt install r8168-dkms may help (cf. www.technewstoday.com/ubuntu-ethernet-not-working and askubuntu.com/questions/111751/how-do-i-correctly-install-r8169-network-driver).

2.1.2.8. Main directories
2.1.2.10. Using USB drives

Find out what the drive is called using fdisk -l, then mount the drive using mount <drive> /media/usb. To unmount use umount /media/usb.

To have a drive mounted automatically, add it to /etc/fstab. Use lsblk -O or fdisk -l to get the required information for your drive. After a system reboot, your drive should be available.

2.1.2.11. Backup

System backup is essential. Install storeBackup, create a directory for your backups and add a crontab task using crontab -e. Here is an example crontab entry where an email is sent after backup completion (cf. how-to-sendmail):

* 3 * * 1 /opt/storeBackup/bin/storeBackup.pl --sourceDir /var/www --backupDir /root/backup
| sed 's/^/To: mail address\nSubject: backup\n\n/' | sendmail -t

Alternatively you can set up a systemd timer. A discussion about pros and cons can be found at cron vs systemd timers.

Instead of or in addition to local backup you might consider cloud backup using Duplicity, preferably with encryption.

2.1.2.12. Recover deleted files

Install and use extundelete.

2.1.2.13. Mail

Whilst you may not want to run your own mail server, if you want to enable your server to send emails, install Postfix or better a complete mail server (also see here). For Postfix configuration read easyengine.io/tutorials/mail/postfix-debugging. To send an email, create a file with content structured as in the following example and then use sendmail recipient < file:

Subject: everling.lu backup job

Backup has been run

With regards to debugging Postfix, also see www.cyberciti.biz/faq/linux-unix-start-stop-restart-postfix

You can manage emails using mail or more comfortably using mutt.

When using mail, use d * followed by quit to delete all mail. Or see devanswers.co/you-have-mail-how-to-read-mail-in-ubuntu.

sharadchhetri.com/2014/02/06/how-to-delete-mail-queue-in-postfix

2.1.2.14. Remote copy
2.1.2.14.1. rsync

rsync is the command of choice offering the greatest flexibility.

See man rsync. The r, l, v and t options can be particularly useful.

rsync -rlvt source target

source can include the user name, e.g.

rsync -rlvt evegi144@students.btsi.lu:WAD .

would connect as user evegi144, ask for the password and then copy the WAD directory to the local folder, including symbolic links and modification times.

2.1.2.14.2. rcp and scp

See man rcp. The p, r and v options can be particularly useful.

rcp -prv source target

source can include the user name, e.g.

rcp -prv evegi144@students.btsi.lu:WAD .

would connect as user evegi144, ask for the password and then copy the WAD directory to the local folder.

Another example, copying a single file:

scp -pv ./filename evegi144@students.btsi.lu:
2.1.2.16. Grub2

Grub2 is the default boot loader and manager for Ubuntu.

2.1.2.17. cURL
git clone https://github.com/curl/curl.git
apt install autoconf libtool
./buildconf
./configure
2.1.2.19. Check disk health
apt install smartmontools
fdisk -l
smartctl -c /dev/sdX
smartctl -t short /dev/sdX
smartctl -H /dev/sdX
2.1.2.22. Running GUI applications as root
2.1.2.24. Benchmarking
2.1.2.24.1. Check 3d performance

apt install nux-tools /usr/lib/nux/unity_support_test -p

2.1.2.27. Disable keyring password
2.1.2.30. Terminal
2.1.2.30.2. Change bash prompt
2.1.2.32. Reset root password
2.1.2.34. Show all devices which need drivers
ubuntu-drivers devices
2.1.2.35. Prevent airplane mode from getting turned off on restart
2.1.2.36. Enable open as administrator option
apt install nautilus-admin
nautilus -q
2.1.2.37. Login as root on Ubuntu desktop
2.1.2.41. Ubuntu desktop keyboard shortcuts
2.1.2.42. Workspaces
2.1.2.43. List all disks, partitions and sizes
2.1.2.44. Check disk health
2.1.2.47. Fix “/sbin/init : no such file or directory”
2.1.2.50. Install Firefox as a traditional deb package (without snap) in Ubuntu

askubuntu.com/questions/1399383/how-to-install-firefox-as-a-traditional-deb-package-without-snap-in-ubuntu-22

2.1.2.52. Create desktop launcher

askubuntu.com/questions/64222/how-can-i-create-launchers-on-my-desktop

2.1.2.52.1. AppImage launcher and integration

github.com/TheAssassin/AppImageLauncher

2.2. Windows

2.2.1. Server

2.2.1.1. Introduction
Windows Server is the platform for building an infrastructure of connected applications, networks, and web services, from the workgroup to the data center.

Windows Server is used to serve all kinds of applications, data and services a client might require. Microsoft calls these roles and features. Roles are major capabilities required. For instance Active Directory to authenticate users or the Domain Name System (DNS) to locate other computers. Features can add additional functionality to the OS or enhance specific roles. Here you can find a list of the roles and features provided by Windows Server. There you can also see the differences between Windows Server Standard and Windows Server Datacenter. One of the major differences is that with the standard version you can only host 2 virtual machines per license.

The two Windows Server versions are available with different footprints either with desktop (Desktop Experience) or without. The advantage of the former being ease of use and of the latter performance and storage requirements.

This article provides much more in-depth information on the different Windows Server versions and is a must read.

Even without desktop experience it is still possible to run a number of GUI applications as illustrated in medium.com/@RealNetwork/windows-server-core-2019-gui-management-sysinternals-utilities-datacenter-standard-hyper-v-dashboard-265801412c89.

The latest Windows Server version is 2022. This page explains what’s new.

www.youtube.com/playlist?list=PLcRhfKiWZmM8L6r2vysrNaIz8inmdNQJk

www.microsoft.com/en-us/cloud-platform/windows-server

docs.microsoft.com/en-us/windows-server

www.microsoft.com/en-us/learning/companion-moc.aspx

www.classcentral.com/course/coursera-windows-server-management-and-security-9320

social.technet.microsoft.com/wiki/contents/articles/11608.e-book-gallery-for-microsoft-technologies-en.aspx

blogs.msdn.microsoft.com/microsoft_press/2016/09/26/free-ebook-introducing-windows-server-2016

www.discudemy.com/category/Windows%20Server

www.youtube.com/channel/UCP2uPp7TUXwJCicB0byzDnQ

winintro.ru

www.server-world.info/en/note?os=Windows_Server_2019

2.2.1.2. Download
2.2.1.3. Planning

Starting with Windows Server 2016, we cannot convert between Core and Desktop anymore. Quote from docs.microsoft.com/en-us/windows-server/get-started/getting-started-with-server-with-desktop-experience:

Unlike some previous releases of Windows Server, you cannot convert between Server Core and Server with Desktop Experience after installation. If you install Server with Desktop Experience and later decide to use Server Core, you should do a fresh installation.
2.2.1.3.1. Migration

If you are planning to migrate Windows Server be sure to study docs.microsoft.com/en-us/windows-server/get-started/migrate-roles-and-features.

2.2.1.4. Installation

To be able to have several virtual machines communicate set the network mode in VirtualBox to NAT Network as described in www.techrepublic.com/article/how-to-create-multiple-nat-networks-in-virtualbox. Also see www.virtualbox.org/manual/ch06.html and www.nakivo.com/blog/virtualbox-network-setting-guide.

If you discover that your guest OS takes up a full core all the time, try assigning 2 cores to the guest VM via System → Processor in the VirtualBox Manager (cf. forums.virtualbox.org/viewtopic.php?f=6&t=87991&start=45).
winserver2022inst1
winserver2022inst2
winserver2022inst3

In previous Windows Server versions updates took ages so it was sometimes useful to disable them. This is not the case for Windows Server 2022 anymore, as the update speed has increased significantly. If you still want to disable updates, run sconfig in the command line, select menu 5 and then set updates to manual.

For the GUI version you could alternatively launch Services from the search box:

winserver2016conf1

Then run gpedit.msc → Computer Configuration → Administrative Templates → Windows Components → Windows Update and disable Configure Automatic Updates.

winserver2016conf2

Set the correct timezone:

winserver2016conf3

Now remove the disc from the virtual drive and select Devices → Insert Guest additions CD image…​ and run the guest additions. From the command line you’ll have to change to the CD drive and then run VBoxWindowsAdditions.exe.

To see detailed info about the server run Get-CimInstance Win32_OperatingSystem|fl *.

To enable ping use netsh advfirewall firewall add rule name="ICMP Allow incoming V4 echo request" protocol=icmpv4:8,any dir=in action=allow or the deprecated, but shorter, netsh firewall set icmpsetting 8 (cf. community.spiceworks.com/how_to/70758-enable-ping-reply-for-windows-server-2012-r2-core).

To enable RPC communication between server and client start your Windows client computer, run gpedit.msc and enable Computer Configuration → Administrative Templates → Network → Network Connections → Windows Firewall → Domain Profile → Windows Firewall: Allow inbound remote administration exception.

On older Windows Server versions install a real browser on both servers and disable Internet Explorer using dism /online /Disable-Feature /FeatureName:Internet-Explorer-Optional-amd64 in an elevated command prompt or PowerShell (see support.microsoft.com/en-us/help/4013567/how-to-disable-internet-explorer-on-windows).

Verify that remote management is enabled. This can be done via sconfig or Configure-SMRemoting -get in PowerShell or via the Server Manager GUI.

If you want to manage Windows Server installations remotely from a Windows client you can use the Remote Server Administration Tools (RSAT).

You need to enable some firewall rules on a core server for remote management to work: Enable-PSRemoting Enable-NetFirewallRule -DisplayName "Windows Management Instrumentation (DCOM-In)" Enable-NetFirewallRule -DisplayGroup "Remote Event Log Management" Enable-NetFirewallRule -DisplayGroup "Remote Service Management" Enable-NetFirewallRule -DisplayGroup "Remote Volume Management" Enable-NetFirewallRule -DisplayGroup "Windows Defender Firewall Remote Management" Enable-NetFirewallRule -DisplayGroup "Remote Event Log Management" Enable-NetFirewallRule -DisplayGroup "Remote Scheduled Tasks Management"

To connect to a remote server you might need to follow nokitel.im/index.php/2016/03/11/windows-10-and-server-2012-r2-server-manager-winrm-negotiate-authentication-error and study the following:

github.com/MicrosoftDocs/windowsserverdocs/blob/master/WindowsServerDocs/administration/server-manager/add-servers-to-server-manager.md

docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/hh831453(v=ws.11)

social.technet.microsoft.com/wiki/contents/articles/13444.windows-server-2012-server-manager-troubleshooting-guide-part-ii-troubleshoot-manageability-status-errors-in-server-manager.aspx

docs.microsoft.com/en-us/powershell/module/netconnection/get-netconnectionprofile

serverfault.com/questions/704497/domain-joined-server-is-not-identified-as-domain-joined-on-the-network-profile/704768

www.itprotoday.com/powershell/how-force-network-type-windows-using-powershell

To execute cmdlets on remote servers, you need to specify the credentials as described in stackoverflow.com/questions/34768795/pass-password-into-credential:
$pass=ConvertTo-SecureString -AsPlainText 'password' -Force
$cred=New-Object System.Management.Automation.PsCredential('user@domain',$pass)
PSCommand -credential $cred -computer <computer>

To disable the monitor timeout on a server without GUI run powercfg -change -monitor-timeout-ac 0 (cf. ss64.com/nt/powercfg.html).

To see a list of installed roles and features, use Get-WindowsFeature|Where Installed or Get-WindowsFeature|Where-Object InstallState -eq Installed.

You can be more specific, for example select only roles and features that begin with "Print" on computer server1 like this:

Get-WindowsFeature -Name Print* -ComputerName server1 -Credential (Get-Credential CORP\Administrator)|Where Installed

Install roles and features using Install-WindowsFeature, for example:

Install-WindowsFeature -Name DHCP -ComputerName server1

To remove use Uninstall-WindowsFeature.

To get a list of all PowerShell commands use Get-Command.

To get help on a command use help.

Start-Job {Get-WindowsFeature}

Get-Job allows us to see the state of a job.

To run a background job and see its output use Receive-Job:

$job=Start-Job {Get-WindowsFeature}
Receive-Job -Job $job

A new job can be scheduled:

$trigger=New-JobTrigger -Once -At 8:05AM
Register-ScheduledJob -Name ListFeatures {Get-WindowsFeature} -Trigger $trigger
job=Start-Job {Get-WindowsFeature}

We can create persistent sessions:

$script1={Start-Job {Install-WindowsFeature -Name Windows-Server-Backup}}
$script2={Start-Job {Uninstall-WindowsFeature -Name Windows-Server-Backup}}
$srv1=New-PSSession -ComputerName server1
Invoke-Command $srv1 $script1
Disconnect-PSSession -Name $srv1
Get-WindowsFeature -Name Windows-Server-Backup -ComputerName server1
Connect-PSSession -Name $srv1
Invoke-Command $srv1 $script2
Get-PSSession|Remove-PSSession

To see which user you are logged in with use set or whoami.

To extend the Windows Server evaluation period, use slmgr -dlv to see the current status and slmgr -rearm to rearm.

To disable IPv6 using PowerShell see giritharan.com/disable-ipv6.

2.2.1.7. Deployment
2.2.1.7.2. System Preparation Tool (Sysprep)
A very useful guide can be found at askme4tech.com/sysprep-and-capture-windows-10-dism.
Do not install any roles onto the server as the Sysprep process could cause problems due to some roles binding themselves to the hostname.
When running Sysprep it is important to select the generalize and shutdown options. The former removes all the unique system information (SID) to make the image installable on multiple target machines and the latter shuts down the system, which is critical as a reboot would regenerate unique system information and thus prevent the image from being used to install multiple systems.
To convert a WIM to an ISO file, you can either do it yourself (cf. www.techwalla.com/articles/how-to-make-a-bootable-cd) or make your life easy and use an application, many of which are described in listoffreeware.com/free-wim-to-iso-converter-software-windows.

blog.ctglobalservices.com/os-deployment/mip/create-iso-using-waik-and-oscdimg

helpdeskgeek.com/how-to/create-an-iso-file-from-a-folder-in-windows

docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/capture-and-apply-windows-using-a-single-wim

www.tenforums.com/tutorials/133098-dism-create-bootable-iso-multiple-windows-10-images.html

2.2.1.7.4. Create an offline installation ISO file
  1. Configure your Windows server/client installation exactly the way you want, but remember to not install any server roles. Install all updates.

  2. If you’re running a virtual machine, perform a snapshot.

  3. Run sysprep with the OOBE, generalize and shutdownoptions.

  4. After the system has turned off, insert the original Windows installation ISO and boot from it. Follow askme4tech.com/sysprep-and-capture-windows-10-dism up to and including the dism part.

  5. Boot into the new installation and retrieve the WIM file.

  6. Use MDT to import the new WIM file into your deployment share and follow www.vkernel.ro/blog/creating-an-offline-mdt-deployment-media.

2.2.1.8. System Center Configuration Manager (SCCM)

docs.microsoft.com/en-us/sccm/core/understand/introduction

2.2.1.9. Networking

Set static IP address: New-NetIPAddress -AddressFamily 'IPv4' -IPAddess '10.0.2.45' -PrefixLength 24 -DefaultGateway '10.0.2.1'

Set DNS server: Set-DnsClientServerAddress -InterfaceIndex 5 -ServerAddresses @("10.0.2.37", "192.168.178.1")

Change the default gateway: Get-NetIPConfiguration get-netroute -DestinationPrefix '0.0.0.0/0' remove-netroute -ifIndex 5 -DestinationPrefix '0.0.0.0/0' -NextHop 10.0.2.1 new-netroute -ifIndex 5 -DestinationPrefix '0.0.0.0/0' -NextHop 10.0.2.37 get-netroute -DestinationPrefix '0.0.0.0/0'

Remove default DNS server IP: Get-DnsClientServerAddress -InterfaceIndex $NICIndexnumber -AddressFamily IPv6 | Set-DnsClientServerAddress -ResetServerAddresses

If the Windows Defender Firewall is set to private although your server is domain joined, disable the network connection and reenable it (cf. serverfault.com/questions/704497/domain-joined-server-is-not-identified-as-domain-joined-on-the-network-profile/704768).
To handle the Windows Server Core 2022 "Failed to release DHCP lease" bug, see paularquette.com/windows-server-2022-core-networking-error
To remove a default gateway using PowerShell, see www.youtube.com/watch?v=mr2iVbD2MNI.

www.slideshare.net/FabioAlmeida8/98-366-mva-slides-lesson-1

en.wikipedia.org/wiki/Private_network

www.lifewire.com/what-is-a-private-ip-address-2625970

www.microsoft.com/en-us/download/details.aspx?id=8781

simpledns.com/private-ipv6

docs.microsoft.com/en-us/archive/blogs/jlosey/why-you-should-leave-ipv6-alone

osric.com/chris/accidental-developer/2018/09/setting-a-static-ip-default-gateway-and-nameservers-via-powershell

stackoverflow.com/questions/49702121/windows-powershell-set-ip-address-on-network-adapter

2.2.1.9.4. Private IP addresses

We can use the following IP address ranges as private or local IP addresses, which will not be visible on the internet.

IPv4:

  1. 10.0.0.0 to 10.255.255.255

  2. 172.16.0.0 to 172.31.255.255

  3. 192.168.0.0 to 192.168.255.255

IPv6: Unique local addresses always begin with FD00::/8. The next 40 bits represent the randomly generated global identifier, followed by 16 bits to identify the subnet. The final 64 bits specify the interface.

2.2.1.9.5. NetBIOS

From Microsoft:

NetBIOS: A particular network transport that is part of the LAN Manager protocol suite. NetBIOS uses a broadcast communication style that was applicable to early segmented local area networks. A protocol family including name resolution, datagram, and connection services. For more information, see [RFC1001] and [RFC1002].

www.lifewire.com/netbios-software-protocol-818229

docs.microsoft.com/en-us/windows-server/administration/windows-commands/nbtstat

security.stackexchange.com/questions/63945/when-does-one-require-netbios

2.2.1.9.6. Networking tools
  1. ping

  2. tracert

  3. pathping

  4. Test-Connection (PowerShell)

  5. telnet

  6. Test-NetConnection (PowerShell)

  7. Get-NetAdapter (PowerShell)

2.2.1.9.12. Preboot Execution Environment (PXE)
2.2.1.10. Active Directory Domain Services (AD DS)

According to this recommended introduction by Microsoft:

AD DS provides a centralized system for managing users, computers, and other resources on a network.

From "Installing and Configuring Windows Server 2012 Training Guide" by Mitch Tulloch:

Active Directory Domain Services (AD DS) provides a distributed database and directory service that stores and manages information about the users, computers, groups, shares, printers, and other types of objects that comprise an organization’s IT infrastructure.
For a good explanation of the difference between LDAP and AD DS, see www.varonis.com/blog/the-difference-between-active-directory-and-ldap.

Windows Server 2019 does not provide any new AD functionality or even forest level (cf. www.virtualizationhowto.com/2018/12/upgrading-windows-server-2016-domain-controller-dc-to-windows-server-2019).

First we need to install the AD DS role via Server Manager or PowerShell. Then the server needs to be promoted to a domain controller. See blogs.technet.microsoft.com/canitpro/2017/02/22/step-by-step-setting-up-active-directory-in-windows-server-2016 and www.moderndeployment.com/windows-server-2019-active-directory-installation-beginners-guide.

In PS we can use Install-ADDSForest to create a new forest, domain and domain controller.

To join a computer to an AD domain remotely via PowerShell:

Install-WindowsFeature -name AD-Domain-Services -ComputerName winsecore -Credential (Get-Credential CORP\Administrator)

Invoke-Command –ComputerName winsecore -credential (get-credential CORP\Administrator) {Import-Module ADDSDeployment;Install-ADDSDomainController –NoGlobalCatalog:$False –CreateDNSDelegation:$False –Credential (Get-Credential CORP\Administrator) –CriticalReplicationOnly:$False –DatabasePath “C:\Windows\NTDS” –DomainName “corp.contoso.com” –InstallDNS:$True –LogPath “C:\Windows\NTDS” –NoRebootOnCompletion:$False –SiteName “Default-First-Site-Name” –SysVolPath “C:\Windows\SysVol” }

Don’t forget to set the DNS server.

Problems with AD almost always result from a DNS misconfiguration.
In order to successfully promote a second server as domain controller several reboots of the server and the existing domain controller are usually required.

If you encounter an error mentioning a duplicate SID, for instance because you cloned a VM, you need to generate a new SID (cf. www.mustbegeek.com/generate-new-sid-server-2012), which will reset a lot of settings.

Useful commands include Get-Command -AD, Get-ADDomain|fl name,DomainMode, Get-ADForest|fl name, DomainMode.

You can navigate the AD using PowerShell like so:

CD AD:

DIR

CD "DC=corp,DC=contoso,DC=com"

DIR

CD CN=Users

DIR | FT -a

Get-ADUser -Filter {name -like "*"}

Here’s an example of getting all user objects and seeing what would happen if you wanted to delete their department info (see stackoverflow.com/questions/39574986/how-to-remove-a-field-in-a-users-id-in-active-directory-using-powershell): Get-ADUser -filter *|Set-ADUser -clear Department -whatif

You can also use $null to clear a property value.

When you add a new non-admin user and try to log them in on a domain controller, you’re likely to get the following error message: "The sign-in method you are trying to use isn’t allowed. For more info, contact your network administrator". This is due to the default domain controllers policy (cf. link). To solve this problem, open the Default Domain Controllers Policy and check who is allowed to log on locally:
winserver2019LocalLogOnGPO1

In this case you can see, that the user is only member of the group Domain Users which is not allowed to log on locally. So either you add the user to a group that is allowed to log on locally or you add at least one of the user’s groups to the allowed ones. Then you run gpupdate /force and the user should be able to log in.

If you cannot contact a domain controller, see theitbros.com/active-directory-domain-controller-could-not-be-contacted.

To fix "the security database on the server does not have a computer account for this workstation trust relationship" error, remove the PC from the domain and rejoin it (cf. appuals.com/how-to-fix-the-security-database-on-the-server-does-not-have-a-computer-account-for-this-workstation-trust-relationship-error-on-windows).

cp-mlxprod-static.microsoft.com/05329-1025/en-us/content/content_8xaxrrjy_1704984382/04232015094701.pptx

channel9.msdn.com/Series/Using-PowerShell-for-Active-Directory

channel9.msdn.com/Series/Active-Directory-Video-Series/ADs-Reliance-on-DNS

www.slideshare.net/nishadsukumaran/active-directory-training

www.youtube.com/watch?v=nKcrVtvZvpk

www.youtube.com/watch?v=J8uw3GNZxzQ

cloudblogs.microsoft.com/industry-blog/en-gb/technetuk/2016/06/08/setting-up-active-directory-via-powershell

activedirectorypro.com/powershell-commands

2.2.1.10.7. Replication

To list commands relevant to replication: Get-Command Replication To list replication partners: Get-ADReplicationPartnerMetaData -target corp.contoso.com

The AD Replication Status Tool can be quite useful.
2.2.1.10.15. Recycle bin

Use the following command on the domain controller that holds the naming flexible single master operations (FSMO) role: Enable-ADOptionalFeature -Identity "Recycle Bin Feature" -Scope ForestOrConfigurationSet -target 'contoso.com'

2.2.1.10.16. Domain service (DS) commands

Example usage of the DS commands:

dsquery user to list all users.

Dsquery user “OU=IT, DC=Adatum,DC=com” | Dsmod user –dept IT to move all users in the IT OU to the IT department.

Dsmod user "cn=Joe Healy,ou=Managers, dc=adatum,dc=com" –dept IT to move Joe Healy to the IT department.

Dsget user "cn=Joe Healy,ou=Managers, dc=adatum,dc=com" –email to get the email address of Joe Healy.

Dsrm "cn=Joe Healy,ou=Managers,dc=adatum,dc=com" to delete user Joe Healy.

Dsadd user "cn=Joe Healy,ou=Managers,dc=adatum,dc=com" to create a new user.

2.2.1.10.17. Protected accounts and groups
2.2.1.11. Group policy
The official group policy settings reference can be found at tinyurl.com/policysettings-xls.

docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn789193(v%3Dws.11)

4sysops.com/archives/four-ways-to-search-for-group-policy-settings

www.linkedin.com/learning/windows-server-2012-r2-manage-group-policy/configure-group-policy-preferences

docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc784268(v=ws.10)

docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc786524(v%3dws.10)

docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/ee461027(v=technet.10)

docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/cc733087(v%3dws.10)

social.technet.microsoft.com/wiki/contents/articles/51876.group-policy-filtering-and-permission.aspx

www.infoworld.com/article/3117286/tutorial-the-joys-of-windows-servers-group-policies.html

www.grouppolicy.biz/tutorials

www.mdmandgpanswers.com/blogs

sdmsoftware.com/gpoguy

sdmsoftware.com/gpoguy/free-tools/video-training

To redirect the default location of new users and computers, see support.microsoft.com/kb/324949.

The Local Users and Groups snap-in is not accessible on a domain controller. Use the Group Policy Management Editor to manage local users and groups as shown below.
Use gpresult /s computer /user <username> /z to get a detailed command line view of the applied group policies. Example: gpresult /s win10.contoso.com /user contoso\marketingguru /z

www.grouppolicy.biz/2010/05/how-to-apply-a-group-policy-object-to-individual-users-or-computer

woshub.com/group-policy-not-applied-troubleshooting

docs.microsoft.com/en-us/previous-versions/windows/desktop/policy/filtering-the-scope-of-a-gpo

superuser.com/questions/1082522/gpo-denied-security-filter-not-applying

Group policy applies only to users or computers in an OU, not a group. Groups are designed to grant access to data and organizational units are designed to control objects (delegation and group policy settings), as explained in techgenix.com/back-basics-groups-vs-organizational-units-active-directory.

See www.grouppolicy.biz/2010/05/how-to-apply-a-group-policy-object-to-individual-users-or-computer on how to apply a group policy object to individual users or computers.

If you’re getting "GPO Denied (Security Filter) not applying" see superuser.com/questions/1082522/gpo-denied-security-filter-not-applying and azurecloudai.blog/2018/12/31/most-common-mistakes-in-active-directory-and-domain-services-part-1. You need to make sure that Authenticated Users have read access to the GPO.

interactivelogon

2.2.1.11.4. Administrative templates
2.2.1.11.7. Local users and groups

The easiest way to configure local users and groups is va the Group Policy Management Editor:

GPMELocalUsersAndGroups
Creating new local users is not possible anymore for security reasons, see here and here.
Study this guide and this.
2.2.1.11.9. Restricted Groups policy
Restricted Groups is a client configuration means and cannot be used with Domain Groups. Restricted Groups is designed specifically to work with Local Groups.
The only situation where it makes sense to use Restricted Groups policy is when you want to ensure that certain users are members of an Active Directory group. Otherwise use the Local Users and Groups parts of the GPME.
GPMERestrictedGroups
There’s no merging between Restricted Groups policy settings set up at multiple levels in Active Directory. The last applied policy wins.
Read this excellent article.
2.2.1.11.10. Software restriction policies and AppLocker

docs.microsoft.com/en-us/windows-server/identity/software-restriction-policies/administer-software-restriction-policies

docs.microsoft.com/en-us/windows/security/threat-protection/windows-defender-application-control/applocker/applocker-policies-deployment-guide

social.technet.microsoft.com/wiki/contents/articles/5211.how-to-configure-applocker-group-policy-to-prevent-software-from-running.aspx

docs.microsoft.com/en-us/windows/security/threat-protection/windows-defender-application-control/applocker/understanding-applocker-allow-and-deny-actions-on-rules

www.petri.com/setting-application-control-policies-microsofts-applocker

michaelfirsov.wordpress.com/applocker-in-windows-10-enterprise

docs.microsoft.com/en-us/windows/security/threat-protection/windows-defender-application-control/applocker/configure-the-application-identity-service

docs.microsoft.com/en-us/windows/configuration/lock-down-windows-10-applocker

In order to use AppLocker, you need to start the Application Identity service and create the default rules, particularly for packages, otherwise your start menu may be blocked (cf. social.technet.microsoft.com/Forums/en-US/fd6ad6b5-0a83-4deb-ac36-8560f7164c55/applocker-breaks-start-menu-microsoftwindowsshellexperiencehost).

Policy can be configured in GPME: Computer Configuration –> Policies –> Windows Settings –> Security Settings –> Application Control Policies –> AppLocker

Applocker does not work on Windows 10 Pro (cf. community.spiceworks.com/topic/2216848-applocker-not-working).

To force the download of the latest domain policies from the domain controllers use gpupdate /force.

2.2.1.11.11. Group Policy Management Console Scripting Samples
2.2.1.11.15. GPO auditing

To audit file and folder access we first need to specify the auditing settings on the files and folders by modifying the system access control list (SACL). Right click file/folder → properties → Security → Advanced → Auditing. Then we need to enable audit policy in the GPO, which can be done in Computer Configuration → Policies → Windows Settings → Security Settings → Local Policies → Audit Policy → Audit object access (cf. docs.microsoft.com/en-us/windows/security/threat-protection/auditing/basic-audit-object-access).

2.2.1.13. Hyper-V
Create a NAT network.

www.youtube.com/watch?v=9KB5KHIIiT4

superuser.com/questions/709874/change-server-2012-display-resolution-inside-of-a-hyper-v-vm

www.tenforums.com/tutorials/56563-create-hyper-v-manager-shortcut-windows-10-a.html

docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/get-started/create-a-virtual-switch-for-hyper-v-virtual-machines

learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/setup-nat-network

learn.microsoft.com/en-us/powershell/module/netnat/new-netnat

4sysops.com/wiki/create-a-hyper-v-virtual-switch-with-powershell

www.scriptersinc.com/configuring-a-hyper-v-lab-internal-network

www.altaro.com/hyper-v/enhanced-session-mode-client-hyper-v

learn.microsoft.com/en-us/windows-server/virtualization/hyper-v/manage/remotely-manage-hyper-v-hosts

4sysops.com/archives/hyper-v-security-best-practices

superuser.com/questions/985736/how-to-access-a-hyper-v-virtual-machine-remotely-with-remote-desktop

ubuntuhandbook.org/index.php/2022/04/ubuntu-22-04-remote-desktop-control

superuser.com/questions/1660150/change-screen-resolution-of-ubuntu-vm-in-hyper-v

learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/setup-nat-network

woshub.com/hyper-v-remote-management

learn.microsoft.com/en-us/archive/blogs/virtual_pc_guy/changing-ubuntu-screen-resolution-in-a-hyper-v-vm

thunderysteak.github.io/hyperv-direct-vm-rdp

Hyper-V can currently not be run inside VirtualBox, see forums.virtualbox.org/viewtopic.php?t=95302.

To determine whether your processor is Hyper-V capable, you can use Systeminfo.exe, cf. docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/system-requirements-for-hyper-v-on-windows or Coreinfo from Sysinternals.

To enable nested virtualization, i.e. run Hyper-V inside a Hyper-VM, see learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/nested-virtualization and particularly learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/enable-nested-virtualization. Set-VMProcessor -VMName <VMName> -ExposeVirtualizationExtensions $true
2.2.1.13.3. System Center Virtual Machine Manager

www.nakivo.com/blog/what-is-system-center-virtual-machine-manager-scvmm

2.2.1.14. Windows Admin Center

www.microsoft.com/en-us/cloud-platform/windows-admin-center

Cannot be installed on domain controller machines. But if you install it before installing the DC it will still work afterwards!
2.2.1.15. Security
2.2.1.15.1. Security principals
get-acl gets the security descriptor for a resource, such as a file or registry key.
2.2.1.15.2. Firewall

If your Windows Defender Firewall profile is always switched to private even though you are connected to a domain, you need to add the Netlogon service as a dependency to the Network location Awareness service, like so:

  1. Open Registry editor

  2. Go to Computer → HKEY_LOCAL_MACHINE → SYSTEM → CurrentControlSet → Services → NlaSvc

  3. Add Netlogon to the DependOnService value and ignore the warning

References:

Get-NetFirewallProfile -Name Domain -PolicyStore ActiveStore

Disable firewall: Set-NetFirewallProfile -profile domain,public,private -enabled false

Get-NetFirewallRule -policystore activestore -displaygroup "Network Discovery" -direction inbound |ft name,displayname,enabled,action -autosize

New-NetFirewallRule -DisplayName "Block Outbound Port 80" -Direction Outbound -LocalPort 80 -Protocol tcp -Action Block

Get-NetFirewallProfile -name domain -PolicyStore contoso.com\Sales | Set-NetFirewallProfile -NotifyOnListen True

2.2.1.16. BitLocker
2.2.1.17. EFS
2.2.1.21. Tips & Tricks
2.2.1.21.2. See computer name

hostname

2.2.1.21.3. Rename computer using PS

Rename-Computer -NewName <xyz> -Restart

2.2.1.21.4. Get current IP configuration

Get-NetIPAddress

2.2.1.21.5. Assign a new IP address

New-NetIPAddress -InterfaceIndex <x> -IPAddress <y> -PrefixLength <z> -DefaultGateway <abc>

2.2.1.21.6. Renew IP configuration

ipconfig /renew

2.2.1.21.7. Restart computer

shutdown /r /t 0

2.2.1.21.9. List available printers

To enable localhost:631/printers run cupsctl WebInterface=yes in a terminal.

2.2.2. Client

A fantastic tutorial collection can be found at www.tenforums.com/tutorials/1977-windows-10-tutorial-index.html.

To get the ISO file and create a bootable USB you can use Rufus or Microsoft’s own tool.

2.2.2.1. Administration
2.2.2.1.1. DNS

Display data in the DNS resolver cache:

ipconfig /displaydns

Flush DNS resolver cache:

ipconfig /flushdns

Renew all DHCP leases and reregister all DNS names:

ipconfig /registerdns

Trace a route to a server:

tracert hostname
pathping hotname
2.2.2.1.3. Robocopy

Useful options include /S /PURGE /COPYALL /DCOPY:T /R:2 /W:1 /A-:SH /xj.

To get rid of undeletable recursive directories (cf. answers.microsoft.com/en-us/windows/forum/windows_7-files/windows-7-infinite-loop-while-using-robocopy/20f32f0c-4cb9-4125-923d-6a57e4d27232) we create an empty dir and then robocopy empty dest /MIR.

2.2.2.1.6. Debugging

The Windows Performance Toolkit can be very useful to debug a number of issues, for instance high CPU usage. See details here.

2.2.2.2. Tips & Tricks

Get rid of language bar icon

www.askvg.com/fix-input-indicator-icon-comes-back-in-taskbar-notification-area-after-restarting-windows

www.howtogeek.com/howto/windows/bring-misplaced-off-screen-windows-back-to-your-desktop-keyboard-trick

Fix DPC WATCHDOG VIOLATION

www.youtube.com/watch?v=VXgAFmPI21g

How to Add Programs, Files, and Folders to System Startup in Windows

www.howtogeek.com/208224/how-to-add-programs-files-and-folders-to-system-startup-in-windows-8.1

List of Windows tools

www.ghacks.net/2017/06/11/list-of-windows-tools

windowsreport.com/weather-app-live-tile-not-working-windows-10

How to access the BIOS

www.addictivetips.com/windows-tips/access-bios-pc

Get rid of hiberfil.sys

www.howtogeek.com/howto/15140/what-is-hiberfil.sys-and-how-do-i-delete-it

How to convert Windows installations to virtual machine images

lt3000.blogspot.com/2018/09/on-chinas-putative-real-estate.html

15 Windows 10 Run Commands Everyone Should Learn

helpdeskgeek.com/windows-10/windows-10-run-commands-everyone-should-learn

How to launch apps automatically during startup on Windows 10

www.windowscentral.com/how-launch-apps-automatically-during-startup-windows-10

stackoverflow.com/questions/8976287/recursive-unzipping-with-7z-exe

Disk drive defragmentation

www.tenforums.com/performance-maintenance/118761-optimize-drives-says-i-have-ssd-when-i-have-hdd.html

PowerToys is a set of utilities for power users to tune and streamline their Windows experience for greater productivity

github.com/microsoft/PowerToys

www.wikihow.com/Delete-a-User-Account-Picture-in-Windows-10

How to run Linux desktop on Windows

www.makeuseof.com/tag/linux-desktop-windows-subsystem

Windows 10 keyboard shortcuts

helpdeskgeek.com/windows-10/windows-10-keyboard-shortcuts-the-ultimate-guide

Windows 10 wakes up from sleep

windowsreport.com/windows-8-windows-10-wakes-sleep-fix

How to remove admin rights

thycotic.com/company/blog/2019/04/09/how-to-remove-admin-rights

www.windowscentral.com/how-properly-update-device-drivers-windows-10

Enable automatic logon

docs.microsoft.com/en-us/sysinternals/downloads/autologon

How does Windows link HTML files to folders?

stackoverflow.com/questions/45614929/how-does-windows-link-html-files-to-folders

Enable the built-in administrator account

www.partitionwizard.com/partitionmanager/enable-administrator-account.html

woshub.com/how-to-clean-up-system-volume-information-folder

Pin batch files to taskbar

www.askvg.com/windows-tip-pin-batch-bat-files-to-taskbar-and-start-menu

Determine maximum memory capacity

superuser.com/questions/973417/command-wmic-memphysical-get-maxcapacity-gives-wrong-number

How to display CPU, GPU and RAM usage natively on Windows 11

www.ghacks.net/2021/12/13/how-to-display-cpu-gpu-and-ram-usage-natively-on-windows-11

How to delete large folder extremely fast on Windows 10

pureinfotech.com/delete-large-folder-fast-windows-10

Exiting fullscreen mode of Hyper V client

serverfault.com/questions/65777/exiting-fullscreen-mode-of-hyper-v-client

How to set up a mirrored volume for file redundancy on Windows 10

www.windowscentral.com/how-set-mirrored-volume-file-redundancy-windows-10

Download Windows 10 ISO files

www.itechtics.com/windows-10-download-iso

Keyboard shortcuts in Windows

support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec

Keyboard language shortcut in Windows 10

www.digitalcitizen.life/keyboard-language-shortcut

2.2.2.2.1. To start a program automatically

Create a shortcut, press Win + R, type shell:startup and put it there.

3. Tools of the trade

3.1. Portable work environment

3.1.1. Vagrant

Vagrant serves to isolate dependencies and their configuration within a single disposable, consistent environment, without sacrificing any of the tools you are used to working with (editors, browsers, debuggers, etc.). Once you or someone else creates a single Vagrantfile, you just need to vagrant up and everything is installed and configured for you to work. Other members of your team create their development environments from the same configuration, so whether you are working on Linux, Mac OS X, or Windows, all your team members are running code in the same environment, against the same dependencies, all configured the same way. Say goodbye to "works on my machine" bugs.

3.1.2. Docker

Docker containers wrap a piece of software in a complete filesystem that contains everything needed to run: code, runtime, system tools, system libraries – anything that can be installed on a server. This guarantees that the software will always run the same, regardless of its environment.

If you run into docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: run usermod -a -G docker <user> (cf. techoverflow.net/2017/03/01/solving-docker-permission-denied-while-trying-to-connect-to-the-docker-daemon-socket).

Kill all docker containers at once: gist.github.com/evanscottgray/8571828

3.1.3. VirtualBox

The techniques and features that VirtualBox provides are useful for several scenarios:

  • Running multiple operating systems simultaneously. VirtualBox allows you to run more than one operating system at a time. This way, you can run software written for one operating system on another (for example, Windows software on Linux or a Mac) without having to reboot to use it. Since you can configure what kinds of "virtual" hardware should be presented to each such operating system, you can install an old operating system such as DOS or OS/2 even if your real computer’s hardware is no longer supported by that operating system.

  • Easier software installations. Software vendors can use virtual machines to ship entire software configurations. For example, installing a complete mail server solution on a real machine can be a tedious task. With VirtualBox, such a complex setup (then often called an "appliance") can be packed into a virtual machine. Installing and running a mail server becomes as easy as importing such an appliance into VirtualBox.

  • Testing and disaster recovery. Once installed, a virtual machine and its virtual hard disks can be considered a "container" that can be arbitrarily frozen, woken up, copied, backed up, and transported between hosts.

  • On top of that, with the use of another VirtualBox feature called "snapshots", one can save a particular state of a virtual machine and revert back to that state, if necessary. This way, one can freely experiment with a computing environment. If something goes wrong (e.g. after installing misbehaving software or infecting the guest with a virus), one can easily switch back to a previous snapshot and avoid the need of frequent backups and restores.

  • Any number of snapshots can be created, allowing you to travel back and forward in virtual machine time. You can delete snapshots while a VM is running to reclaim disk space.

  • Infrastructure consolidation. Virtualization can significantly reduce hardware and electricity costs. Most of the time, computers today only use a fraction of their potential power and run with low average system loads. A lot of hardware resources as well as electricity is thereby wasted. So, instead of running many such physical computers that are only partially used, one can pack many virtual machines onto a few powerful hosts and balance the loads between them.

druss.co/2015/06/fix-vt-x-is-not-available-verr_vmx_no_vmx-in-virtualbox

www.nakivo.com/blog/virtualbox-network-setting-guide

www.oracle.com/technical-resources/articles/it-infrastructure/admin-manage-vbox-cli.html

www.youtube.com/watch?v=PB6zlMd0DkM

docs.oracle.com/html/E50247_08/vmiug-install-pxe-general.html

www.youtube.com/watch?v=xgNgGEEXCRk

datacadamia.com/virtualbox/pxe

3.1.3.1. Compact VDI file size
dd if=/dev/zero of=/var/tmp/bigemptyfile bs=4096k ; rm /var/tmp/bigemptyfile
VBoxManage.exe modifymedium --compact c:\path\to\thedisk.vdi

3.2. Integrated Development Environments

3.2.2. PhpStorm

PhpStorm is the ideal IDE for web app development. It provides full database and server integration.

3.2.2.1. Portable installation

To install PhpStorm on a portable drive, go to JetBrains and click the Download button. Cancel the automatic download of the .exe file and right click direct link, select Copy Link Location, paste the link into a new tab and replace the exe extension with zip, then press enter. This will download and open the zipped version of PhpStorm. Extract it to your portable drive.

Open the file bin/idea.properties, replace the line starting with #idea.config.path with idea.config.path=${idea.home}/.WebIde/config, #idea.system.path with idea.system.path=${idea.home}/.WebIde/system, #idea.plugins.path with idea.plugins.path=${idea.config.path}/plugins and #idea.log.path with idea.log.path=${idea.system.path}/log.

In order to avoid having to reenter your SSH password after every logout set the following:

phpStormPortable1

This works only for project deployment servers, not for global ones. Unfortunately this does not work for DB sources for which you’ll have to reenter the SSH password.

If you have settings from another PhpStorm installation that you’d like to import, you can do this via File → Import Settings…​.

3.2.2.2. Project setup

First, we set all file encodings to UTF-8 in order to avoid any problems with special characters. Search for file encodings in the search box. Given the constant PhpStorm UI changes, your file encoding settings may be located in a different place:

File encodings

Now we configure a new project:

phpStorm1
phpStorm2
phpStorm3
phpStorm4
phpStorm5
phpStorm6
phpStorm7
phpStorm8
phpStorm9
phpStorm10
phpStorm11
phpStorm12
phpStorm13
3.2.2.3. Database connection setup

First we need to make sure that the drivers are loaded:

createDB2

Then we need to create a data source:

createDB3

Make sure to right click the connection and select Make Global so that you don’t need to configure it for each project:

createDB4
3.2.2.4. Template adjustment
PhpStorm template adjustment

3.2.3. NetBeans

Download NetBeans.

NetBeansDownload1

If this is the first time you install NetBeans on your device, you need to install the Java Development Kit (JDK) first (point 1 on the screenshot). This will open the following screen:

JDKDownload1
JDKDownload2

Accept the license agreement and select the right JDK version for your operating system.

When the JDK is installed you can install NetBeans. For our purposes we only need the HTML5 + PHP version (point 2).

Alternatively you can install the NetBeans Java SE bundle (point 3), which includes the JDK and NetBeans. This will taker you to the following screen:

NetBeansJDKDownload1

Accept the license agreement and select the right JDK version for your operating system.

3.2.3.1. Portable installation

If you want to install NetBeans on a portable device, you can download it as a zip file (point 4 on the first screenshot in the previous subsection.) This will take you to the following screen:

NetBeansPortable1
3.2.3.2. HTML5 Project setup

Click on NetBeansNewProjectIcon or File → New Project or Ctrl+Shift+N:

NetBeansHTML5Project1

Specify the name of your project and where you’d like to save it:

Name and locate a new NetBeans HTML5 project

We don’t use site templates:

Site template selection

We also don’t use a JavaScript library, so we can just click on Finish:

JavaScript library selection

You can now see your new HTML5 project structure in the upper left corner. NetBeans has also opened the index.html file, which is the default name of the main project HTML5 file:

Our new HTML5 project

The content of this file is defined in the corresponding template. See NetBeans templates for guidance on how to change this.

In order to add a new file to your project, right click on Site Root and select the file type:

Adding a new file to the HTML5 project
3.2.3.3. PHP Project setup

Click on NetBeansNewProjectIcon or File → New Project…​ or Ctrl + Shift + N. If you are using an older version of NetBeans (< 7.4), you may need to install the PHP plugin via Tools → Plugins → Available Plugins and select PHP. After restarting NetBeans, you should get the following screen when creating a new project:

Create a new NetBeans PHP project

Specify the name of your project and where you’d like to save it. Choose the latest PHP version and keep UTF-8 as the default encoding. The latter makes sure, that non-English characters such as é or ä are handled correctly:

Name and locate a new NetBeans project

If you want to run your project on your local web server, select the corresponding option. We’ll run our projects on Foxi, thus we specify Remote Web Site (FTP, SFTP):

NetBeans run configuration

In order to be able to upload our files to Foxi, we need to define a remote connection, thus we need to click on Manage…​.

Create new connection

The host name is foxi.ltam.lu. The port number needs to be 22. Enter your IAM code as user name and your 11-digit matricule as password (cf. FOXI_login_2017.pdf) to learn how to change your password). The initial directory should be set to /www/your class/your IAM code:

Manage remote NetBeans connection

Now click the connection test button. You should get the following prompt, which you should confirm:

Foxi certificate confirmation

The following dialog should appear:

Foxi connection success

Now you can click on OK. We do not use any PHP framework, so you can click the Finish button in the final dialog. In the projects window (top left) you should now see your new PHP project. You can expand the project structure by clicking on the +-sign in front of it. Under Source Files you’ll see your new PHP project:

New PHP project

You can now start working on your project. When you save your changes, you’ll have to confirm that you want to connect to Foxi. You’ll then see confirmation in the output window that the file has been uploaded to Foxi:

NetBeans output window

We can verify this using our SFTP client (cf. SFTP):

Check NetBeans file upload with WinSCP

We can add a new file to our project by right clicking on Source Files and selecting New  PHP File…​:

NetBeans project setup
3.2.3.4. Database connection setup

In order to connect to MySQL on Foxi we need to open a secure shell (SSH) tunnel. Given that NetBeans does not provide a built-in tunnel functionality, we need to use an external SSH client, for instance Putty.

Enter the tunnel data:

Tunnel data

Then add the tunnel:

Tunnel

Save the session:

Tunnel session

Create a new DB connection:

New DB connection 1
New DB connection 2
New DB connection 3

Set the default DB:

Default DB 1
Default DB 2

View table data:

View table data 1
View table data 2
3.2.3.5. Template adjustment
Template 1
Template 2
Template 3
3.2.3.6. Useful NetBeans shortcuts

Pressing Alt+Shift+F or selecting Source  Format will reformat the source code according to the settings in Tools  Options:

Reformat the source code

3.2.5. Glitch

3.2.6. CodePen

3.2.7. CodeSandbox

3.2.8. JSFiddle

3.2.10. API IDEs

insomnia.rest

www.postman.com

3.3. SSH

3.3.1. Clients

You can connect to your server using a secure shell (SSH) client such as Putty, which is however rather rudimentary. Therefore you might prefer to use more convenient GUIs, most of which use Putty in the background:

A great overview of PuTTY alternatives can be found here.

3.3.2. SFTP

SFTP is the secure, i.e. encrypted, version of FTP, the file transfer protocol. As its name suggests, it is used to transfer files from our development machine to the server and vice versa. There are a number of SFTP clients freely available, e.g. Filezilla. I prefer WinSCP. Download the portable executable and unzip it to a folder of your choice. Set the configuration storage option on the storage page in the preferences dialog to INI file. This will lead WinSCP to save its configuration in an ini file in the same folder where the program itself is located. If you have stored WinSCP on your USB stick, the configuration will also be stored there, so you won’t have to reenter the server data every time you use the program:

Set WinSCP storage preferences

Set up your server connection and click save. Click login to connect to the server and begin transferring your files:

Configure WinSCP login

3.4. Browsers

3.4.1. Firefox

If you incur problems loading a web page that works with other browsers, the source is often to be found with a specific add-on. To confirm this is the case, close Firefox and start it in safe mode, for instance by holding the Shift key pressed whilst launching Firefox (cf. Safe Mode).

To see all preferences that can be set, enter about:config in the address bar.

3.5. Documentation

3.5.1. Jupyter notebooks

jupyter.org

github.com/n-riesco/ijavascript

3.5.2. Asciidoctor

Asciidoctor

Asciidoctor Docs

Asciidoclive

Asciidoctor PDF

Cheatsheet

Awesome Asciidoctor Notebook

Syntax quick reference

intellij-asciidoc-plugin.ahus1.de/docs/users-guide/index.html

Asciidoctor IntelliJ plugin

Asciidoctor Diagram

tinyapps.org/blog/misc/201509260700_asciidoctor.html

dzone.com/articles/publishing-our-asciidoc-developer-guide-as-a-book

stackoverflow.com/questions/20559255/error-while-installing-json-gem-mkmf-rb-cant-find-header-files-for-ruby

www.ahus1.de/post/asciidoctor-intro-and-deep-dive

doctoolchain.github.io/docToolchain

www.vogella.com/tutorials/AsciiDoc/article.html

firebirdsql.org/file/documentation/reference_manuals/user_manuals/Firebird-Docwriting-Guide.pdf

stackoverflow.com/questions/74440197/phpstorm-how-to-make-the-inspector-debugger-ignore-stuff-in-comments

3.5.2.1. Installation on Ubuntu

The easiest way is to use Docker: github.com/asciidoctor/docker-asciidoctor

If you prefer a manual installation (but be aware, that installing Asciidoctor Mathematical manually is a nightmare and works only on Linux, if at all, cf. docs.asciidoctor.org/asciidoctor/latest/stem/mathematical):

apt install asciidoctor
gem install asciidoctor-pdf
gem install coderay
gem install rouge
apt install ruby-dev # required for pygments
gem install pygments.rb
apt install ruby`ruby -e 'puts RUBY_VERSION[/\d+\.\d+/]'`-dev
apt-get -qq -y install bison flex libffi-dev libxml2-dev libgdk-pixbuf2.0-dev libcairo2-dev libpango1.0-dev fonts-lyx cmake
gem install asciidoctor-mathematical
apt-add-repository -y ppa:rael-gc/rvm
apt-get update
# Run this as non root!
sudo apt-get install rvm
sudo usermod -a -G rvm $USER
# https://github.com/rvm/ubuntu_rvm -> add this to .bashrc for each user to use RVM
source "/etc/profile.d/rvm.sh"

# https://stackoverflow.com/questions/70672711/whats-the-right-string-to-use-when-installing-ruby-3-1-through-rvm-on-mac-os-bi
rvm install 3.1.0
# For each non manager user, i.e. not root, run
rvm user gemsets

Now we can install everything using gem:

gem install asciidoctor
gem install asciidoctor-pdf
gem install coderay
gem install rouge
gem install pygments.rb
gem install asciidoctor-mathematical # This usually fails on the latest Ubuntu versions -> try Docker version
# https://github.com/asciidoctor/docker-asciidoctor
3.5.2.2. Create an automatically expanding table of contents

For a nice table of contents, use Tocbot, as explained in github.com/asciidoctor/asciidoctor/issues/699.

Add the following to the docinfo.html file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<style>
    #tocbot a.toc-link.node-name--H1 {
        font-style: italic
    }

    @media screen {
        #tocbot > ul.toc-list {
            margin-bottom: 0.5em;
            margin-left:   0.125em
        }

        #tocbot ul.sectlevel0, #tocbot a.toc-link.node-name--H1 + ul {
            padding-left: 0
        }

        #tocbot a.toc-link {
            height: 100%
        }

        .is-collapsible {
            max-height: 3000px;
            overflow:   hidden;
        }

        .is-collapsed {
            max-height: 0
        }

        .is-active-link {
            font-weight: 700
        }
    }

    @media print {
        #tocbot a.toc-link.node-name--H4 {
            display: none
        }
    }
</style>
<script defer src=tocbot.min.js></script>
<script type=module src=tocbotify.js></script>
Make sure to use TocBot version 3, not 4.

Here’s tocbotify.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Tocbot dynamic TOC, works with tocbot 3.0.2 */
const oldtoc = document.getElementById('toctitle').nextElementSibling
const newtoc = document.createElement('div')
newtoc.setAttribute('id', 'tocbot')
newtoc.setAttribute('class', 'js-toc')
oldtoc.parentNode.replaceChild(newtoc, oldtoc)
tocbot.init({
  contentSelector: '#content',
  headingSelector: 'h1, h2, h3, h4, h5, h6',
  smoothScroll: false
})
const handleTocOnResize = () => {
  const width = window.innerWidth
    || document.documentElement.clientWidth
    || document.body.clientWidth
  tocbot.refresh({
    contentSelector: '#content',
    headingSelector: 'h1, h2, h3, h4, h5, h6'
  })
}
window.addEventListener('resize', handleTocOnResize)
handleTocOnResize()
For an effective dark/light theme switcher, use zajo.github.io/asciidoctor_skin. An excellent stylesheet, including a dark theme, can be found here. For different skins see github.com/darshandsoni/asciidoctor-skins.
File names may not contain spaces.
3.5.2.4. Create a PDF

Use asciidoctor-pdf. If you use STEM and URL images in your document, you can use the following instructions to generate an optimized PDF:

asciidoctor-pdf -v -r asciidoctor-mathematical -a mathematical-format=svg -a pdf-theme=WADtheme.yml -a allow-uri-read WAD.adoc --trace
hexapdf optimize --compress-pages --force WAD.pdf WAD.pdf

If you use inline images in table cells, make sure that the table column width adjusts automatically via a

Other approaches fail, at least in the case of this book.
3.5.2.4.1. Customising the default PDF theme
The layout and styling of the PDF is driven by a YAML configuration file.

See the Asciidoctor PDF Theme Guide. You can copy the default them file to your working directory. On Linux it might be located in someplace like /var/lib/gems/2.7.0/gems/asciidoctor-pdf-1.6.0/data/themes/default-theme.yml. Modify and apply your new theme.

If you want to have the author’s email address included on the title page, see github.com/asciidoctor/asciidoctor-pdf/issues/800.

3.5.2.5. Admonitions

See docs.asciidoctor.org/asciidoc/latest/blocks/admonitions. You can choose between NOTE, TIP, IMPORTANT, CAUTION and WARNING.

3.5.2.6. Tips & tricks
[cols="1,2a", options=header]
|===
|Name|Description
|https://js.tensorflow.org/api/latest/#layers.elu[Exponential Linear Unit (elu)^]|https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html#elu[image:https://ml-cheatsheet.readthedocs.io/en/latest/_images/elu.png[ELU, 604, 529]^]
|hardSigmoid|https://paperswithcode.com/method/hard-sigmoid[^]
|linear|image:https://ml-cheatsheet.readthedocs.io/en/latest/_images/linear.png[Linear, 657, 658]
|===

3.5.4. Pandoc

Use Pandoc to convert files from one markup format into another.

3.6. Content Management Systems

According to Wikipedia:

A content management system (CMS) is a computer software used to manage the creation and modification of digital content (content management). A CMS is typically used for enterprise content management (ECM) and web content management (WCM).

ECM typically supports multiple users in a collaborative environment by integrating document management, digital asset management, and record retention.

Alternatively, WCM is the collaborative authoring for websites and may include text and embed graphics, photos, video, audio, maps, and program code that display content and interact with the user. ECM typically includes a WCM function. CMS is a web template to create your own website.

Basically a CMS aims to enable the creation and maintenance of a web site without any knowledge of HTML, CSS, JavaScript etc. There are many CMS out there, but Wordpress seems to be the most popular by a wide margin. According to W3Techs as of September 18, 2023:

31.6% of the websites use none of the content management systems that we monitor. WordPress is used by 43.1% of all the websites, that is a content management system market share of 63.0%.

However, it is important to choose a CMS based on your specific requirements. As this article argues, there’s no "best" CMS, each CMS has its strengths and weaknesses and is best suited for specific types of projects and environments.

Using an open-source CMS such as WordPress may not always be the best solution, see for instance 7 Solid Reasons not to Use WordPress.

3.6.1. WordPress

Wordpress provide detailed installation instructions. After installation you should study first steps, wordpress.org/support/article/roles-and-capabilities and wordpress.org/support/article/introduction-to-blogging.

Download the latest WordPress package from wordpress.org/download. On Linux you need to make sure that the WordPress directory and all subdirectories belong to the www-data group. There you should manually create a wordpress directory, verify that its group is www-data and the group has read, write and setgid bit enabled. If not use chmod g+rws to enable it. Now you can extract the WordPress package using unzip.
To get a clear understanding of the difference between posts and pages, see wordpress.org/support/article/pages.
For security reasons, you should disable Xmlrpc.php.

www.siteground.com/kb/fix-sorry-not-allowed-upload-file-type-error-wordpress

cloudincome.com/why-is-wordpress-slow

wpwebsitetools.com/why-is-my-wordpress-site-suddenly-very-slow-quick-fixes

www.wpbeginner.com/wp-tutorials/how-to-completely-disable-comments-in-wordpress

Elementor toggle - Hide Or Show Section or text On Button Click

www.youtube.com/watch?v=T5wCfZreero

How to stop Elementor contact form spam completely (5 methods)

theplusaddons.com/blog/elementor-contact-form-spam

Great SMTP mail plugin

wordpress.org/plugins/fluent-smtp

3.7. Revision control

3.7.2. GitLab

about.gitlab.com

4. Client side programming

For a very high level overview, see roadmap.sh/frontend.

4.1. HTML5

HTML stands for Hyper Text Markup Language. HTML5 is the latest version of this markup language for describing web documents or pages. It consists of a series of tags. An element or tag is like a command or instruction that tells the browser about the structure and meaning (also called semantics) of the content of a specific part of our web page.

Take a look at this very gentle, enjoyable and effective introduction to the subject of web design.
Here is a great listing and explanation of all tags with examples. The official standard can be found here and the complete set of HTML elements here. Cheat sheets can be very helpful.

4.1.1. Basic structure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>HTML5 Skeleton</title>
    <meta charset=UTF-8>
    <link href=style1.css rel=stylesheet>
    <script src=script1.js></script>
  </head>
  <body>

  </body>
</html>
1
2
3
body {
  background-color: lightseagreen;
}
1
window.alert("Our first JavaScript has been loaded and executed!")

The first line of an HTML5 document should tell the browser how to process the document by specifying the Document Type Definition (DTD).

After the DTD comes the <html> tag, which specifies the language using the lang attribute. See tag list for a list of available language codes. The <html> tag encompasses the whole HTML document consisting of a <head> and a <body> part.

4.1.1.1. <head>

In the head part we specify the title and the character encoding, which for our purposes will be UTF-8. UTF-8 has the advantage that it handles special characters, e.g. ö and é, correctly. To learn more about character encodings, see www.w3.org/International/tutorials/tutorial-char-enc. Then we include our external CSS and JavaScript files (more on those in the following chapters).

The following elements can go inside the <head> element:

  • <title> (this element is required in the head section)

  • <style>

  • <base>

  • <link>

  • <meta>

  • <script>

  • <noscript>

4.1.1.2. <body>

The body part contains the actual page content.

4.1.1.3. Opening and closing tags

For most, but not all, of the HTML5 tags, there is an opening and a closing tag, as in <body></body>. There are a few standalone tags, such as <hr> to display a horizontal line.

4.1.1.4. Tabs, new lines and spaces

Browsers ignore tabs, new lines and most spaces. For instance, the following two HTML documents produce exactly the same output except for the words "a well", which have been replaced with "an ill":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Example of a well formatted HTML document</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      This is a well formatted HTML document.
    </main>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html



              lang=en>




  <head><title>Example

    of       an ill          formatted
  HTML document</title>
    <meta charset=UTF-8></head><body><main>This is    an
  ill formatted HTML document.</main></body></html>
4.1.1.5. Comments

In order to help others (and ourselves) understand our HTML documents, it is a good idea to include comments where appropriate. Comments are embedded between <!-- and -->. A comment can span several lines and is not displayed by the browser.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html> <!-- The document type is compulsory. -->
<html lang=en> <!-- Don't forget to specify the language. -->
  <head>
    <!--
      This is a very simple illustration of comment usage in HTML5.
      You do not have to use comments.
      Use them where it makes sense.
    -->
    <title>This is an example of comment usage in HTML5</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <!-- The main part is currently empty. If you have a good idea
         on how to fill it, let me know! :D
      -->
    </main>
  </body>
</html>

4.1.2. Validation

In order to be sure that our HTML5 document complies with the official standard and should thus run according to our plans in all compliant browsers, we need to validate our HTML5 and CSS3 files either using the official validators at validator.w3.org and jigsaw.w3.org/css-validator or using the Firefox extension addons.mozilla.org/en-US/firefox/addon/html-validator. The latter will install a validation button in the Firefox add-on bar. A simple double click will display the source code of the current page and run it automatically through the official HTML5 validator. With a right click on the button we launch the CSS3 validator via the Advanced menu. Here are the outputs of the validators for the solution of the next exercise:

The W3C HTML validator
The HTML validation Firefox extension
Launching the W3C CSS validator
The W3C CSS validator

The warning messages can be safely ignored. They just tell us that the validator is still experimental. Given that the HTML5 standard is not expected to be finalized for many years, this is unlikely to change any time soon.

4.1.3. Planning

In order to produce a top notch web site, we need to plan our work carefully.

4.1.3.1. Brain storming

First we need to think about the purpose of our web site. What are the big concepts and ideas that will drive our content? A useful tool in this respect is a mind map. We’ll be using the open source FreeMind software, which is available from freemind.sourceforge.net/wiki/index.php/Main_Page.

As an example, here’s the mind map for WMOTU Lab v1:

mind map
4.1.3.2. Blueprint

Now that we have clarified the big picture content of our site, it’s time to sketch out the rough and basic structure. For this purpose we’ll use an open source vector drawing software named Inkscape.

WMOTU Lab v1 structure
4.1.3.3. Requirements specification

The standard professional approach to project planning is to produce a requirements specification. Such a document specifies the project requirements, including:

  1. Functionality

  2. Prototype/model

  3. Logical site structure

  4. Physical site structure

  5. Time plan

  6. Development environment and technologies

Here is a minimalist example for the WMOTU Address Book app developed in WMOTU Address Book:

4.1.3.3.1. Functionality

The app serves as an electronic address book. New users need to sign up by providing a login name and password. After logging in, the user enters the main page, where he can logout and view a listing of all his addresses. He can delete or edit each address as well as add a new one. All addresses are stored in a MySQL database on the server.

4.1.3.3.4. Physical site structure
WMOTU Address Book Physical
4.1.3.3.5. Time plan

The final product will be delivered electronically on 24.6.14.

4.1.3.3.6. Development environment and technologies

Development will be done mainly with PhpStorm. Main technologies used will be HTML5, CSS3, PHP5 and MySQL5.

4.1.4. <br>

As we have seen, new lines in our source code are converted to a single space by the browser. To split our text into different lines, we use the <br> tag, which inserts a line break. As already mentioned in this chapter, for most, but not all, HTML5 tags there is an opening and a closing tag. <br> is one of the exceptions. It is a so called empty tag, meaning it has no closing tag. This tag should not be used to separate paragraphs. For the latter purpose we use the <p> tag, cf. <p>.

Example:

br
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Break row example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      This is the first line.
      This should be the second one.<br>
      This is the second one.
    </main>
  </body>
</html>

4.1.5. <p>

This tag is used to mark up a paragraph. The browser automatically adds margin above and below each paragraph (see section Block vs inline elements).

Example:

p1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Paragraph example</title>
    <meta charset=UTF-8>
    <style>
      #p1 {
        background-color: green;
      }

      #p2 {
        background-color: gold;
      }
    </style>
  </head>
  <body>
    <main>
      <p id=p1>
        This is the first paragraph. Note that the text always occupies the full
        width of the browser window. If you change the width of your browser window,
        the number of lines that your paragraph occupies changes too.
      </p>
      <p id=p2>
        This is the second paragraph. Use the Firefox console to inspect the margins
        used by your browser.
      </p>
    </main>
  </body>
</html>

Resize your browser and observe the behavior of your paragraphs. Note that we’ve given each paragraph an id attribute. This allows us to style each paragraph’s background color individually using CSS. More on this in CSS3.

4.1.6. Phrase tags

Phrase tags are used to convey special meaning to text:

Name Description

<em>

emphasized text

<strong>

important text

<dfn>

definition term

<code>

computer code

<samp>

sample output from a computer program

<kbd>

keyboard input

<var>

variable

Here is a simple application:

Phrase tags
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Phrase tags example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <p>I'd like to emphasize <em>the following</em>. This is particularly
      <strong>important</strong>.</p>
      <p><dfn>HTML5</dfn> is the definition of greatness!</p>
      <p>The secret of the universe looks like this:
        <code>if (sunshine === true) window.alert("Smile!");</code>
      This will hopefully produce this output: <samp>Smile!</samp></p>
      <p>Enter <kbd>WMOTU</kbd> as your user name. It will be stored in
        <var>userName</var></p>
    </main>
  </body>
</html>

4.1.7. HTML entities

Some characters, such as < or >, are reserved in HTML. We thus cannot use them directly in our text as the browser would try to interpret them as part of a tag.

To get around this problem, we use character entities. A character entity has the form &entity_name or &#entity_number. The following table lists the reserved characters and their corresponding entities (see dev.w3.org/html5/html-author/charref for the complete list, www.toptal.com/designers/htmlarrows for a delightful reference and digitalmediaminute.com/reference/entity for the Unicode codes):

Character Entity number Entity name Description

"

&#34;

&quot;

quotation mark

'

&#39;

&apos;

apostrophe

&

&#38;

&amp;

ampersand

<

&#60;

&lt;

less than

>

&#62;

&gt;

greater than

Application example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Entities usage example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      The HTML expert said &#34;Use HTML entities to display special characters in
      HTML&quot;. As you know, HTML tags start with an &lt; and close with a &gt;, as in
      <code>&lt;a&gt;</code>.
    </main>
  </body>
</html>

4.1.8. <header>

The <header> tag specifies a header for a document or section. It should be used for introductory content or navigation elements. You can have several of these in one document, but they cannot be placed within a <footer>, <address> or another <header> element.

4.1.8.1. <h1> …​ <h6>

These tags specify headings at different levels:

A heading
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Heading Example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <header>
      <h1>Heading level 1</h1>
      <h2>Heading level 2</h2>
      <h3>Heading level 3</h3>
      <h4>Heading level 4</h4>
      <h5>Heading level 5</h5>
      <h6>Heading level 6</h6>
    </header>
  </body>
</html>

4.1.9. Lists

We can choose between unordered and ordered lists. In each case, every list item is enclosed in <li></li> tags.

A paragraph may not contain lists.
4.1.9.1. <ul>

ul1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Unordered List Example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <header>
        <h1>Today's shopping list</h1>
      </header>
      <ul>
        <li>Meat</li>
        <li>Cheese</li>
        <li>Vegetables</li>
        <li>Water</li>
        <li>Bin bags</li>
      </ul>
    </main>
  </body>
</html>
4.1.9.2. <ol>

This element supports the following particular attributes:

Name Value Description

reversed

descending list order

start

number

start value

type

1, A, a, I, i

list marker

Ordered list
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Ordered List Example</title>
    <meta charset=UTF-8>
    <style>
      #l3 {
        list-style-type: upper-alpha;
      }
    </style>
  </head>
  <body>
    <main>
      <header>
        <h1>Today's shopping lists</h1>
      </header>
      <ol style="float: left">
        <li>Meat</li>
        <li>Cheese</li>
        <li>Vegetables</li>
        <li>Water</li>
        <li>Bin bags</li>
      </ol>
      <ol style="float: left" reversed>
        <li>Meat</li>
        <li>Cheese</li>
        <li>Vegetables</li>
        <li>Water</li>
        <li>Bin bags</li>
      </ol>
      <ol id=l3 style="float: left" start=3>
        <li>Meat</li>
        <li>Cheese</li>
        <li>Vegetables</li>
        <li>Water</li>
        <li>Bin bags</li>
      </ol>
    </main>
  </body>
</html>

The style attribute value of float: left means that the list is floated left. As a result, the following element is placed right to the list instead of underneath. Remove the three style attributes and compare the result.

4.1.9.3. Nested lists

We can nest lists. This means we can have a list inside a list inside a list inside a list …​ as many times as we want. The only thing we need to watch is the correct nesting, i.e. we need to close the last opened list tag before we close the second last etc.

Example:

Nested list
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Nested list example</title>
    <meta charset=UTF-8>
    <style>
      body {
        background-color: black;
        color:            gold;
      }

      main {
        position:  relative;
        width:     550px;
        height:    230px;
        animation: move 30s linear 0s infinite alternate;
      }

      /* cf. http://www.w3schools.com/css/tryit.asp?filename=trycss3_animation5 */
      @keyframes move {
        0% {
          background-color: blue;
          left:             0px;
          top:              0px;
        }
        25% {
          background-color: white;
          left:             550px;
          top:              0px;
        }
        50% {
          background-color: red;
          left:             550px;
          top:              230px;
        }
        75% {
          background-color: darkgray;
          left:             0px;
          top:              230px;
        }
        100% {
          background-color: red;
          left:             0px;
          top:              0px;
        }
      }
    </style>
  </head>
  <body>
    <main>
      <ul type=A>
        <li>Human life has several purposes:
          <ol>
            <li>To learn:
              <ol type=a>
                <li>HTML</li>
                <li>CSS</li>
                <li>JavaScript</li>
                <li>PHP</li>
                <li>MySQL</li>
              </ol>
            </li>
            <li>To have fun.</li>
            <li>To become a WMOTU.</li>
            <li>But the mother of all purposes is to become the ultimate problem
              solver.
            </li>
          </ol>
        </li>
        <li>Let's do it!</li>
      </ul>
    </main>
  </body>
</html>

Like the animation? We’ll do plenty of these in section Transformation and animation.

4.1.9.4. <dl>
The dl element represents an association list consisting of zero or more name-value groups (a description list). A name-value group consists of one or more names (dt elements) followed by one or more values (dd elements), ignoring any nodes other than dt and dd elements. Within a single dl element, there should not be more than one dt element for each name.
dl1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Description List Example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <dl>
        <dt>HTML5</dt>
        <dd>HTML5 is a markup language used for structuring and presenting content for the
          World Wide Web and a core technology of the Internet (cf.
          <a href=http://en.wikipedia.org/wiki/HTML5>http://en.wikipedia.org/wiki/HTML5</a>).
        </dd>
        <dt>CSS3</dt>
        <dd>Cascading Style Sheets (CSS) is a style sheet language used for describing the
          presentation semantics (the look and formatting) of a document written in a
          markup language. Its most common application is to style web pages written in
          HTML and XHTML, but the language can also be applied to any kind of XML
          document, including plain XML, SVG and XUL (cf. <a
            href=https://en.wikipedia.org/wiki/CSS>
            https://en.wikipedia.org/wiki/CSS</a>).
        </dd>
        <dt>JavaScript</dt>
        <dd>JavaScript (JS) is an interpreted computer programming language. As part of
          web browsers, implementations allow client-side scripts to interact with the
          user, control the browser, communicate asynchronously, and alter the document
          content that is displayed. It has also become common in server-side
          programming, game development and the creation of desktop applications
          (cf. <a href=https://en.wikipedia.org/wiki/JavaScript>https://en.wikipedia
            .org/wiki/JavaScript</a>).
        </dd>
        <dt>PHP5</dt>
        <dd>PHP is a server-side scripting language designed for web development but
          also used as a general-purpose programming language. PHP is now installed on
          more than 244 million websites and 2.1 million web servers (cf. <a
            href=https://en.wikipedia.org/wiki/PHP5>
            https://en.wikipedia.org/wiki/PHP5</a>).
        </dd>
      </dl>
    </main>
  </body>
</html>

4.1.10. <a>

The <a> tag defines a hyperlink, which is used to link from one page to another.

The most important attribute of the <a> element is the href attribute, which indicates the link’s destination.

By default, links will appear as follows in all browsers:

  1. unvisited link

  2. visited link

  3. active link

This element supports the following particular attributes:

Name Value Description

download

filename

target will be downloaded instead of opened. If filename is omitted it will be saved under the original filename. This works only for files located on the same server than the current page.

href

URL

URL of the page

hreflang

language_code

language of the linked document

media

media_query

the medium that the document is optimized for

rel

alternate, author, bookmark, help, license , next, nofollow, noreferrer, noopener, prefetch, prev, search, tag

Relationship between the current and the linked document. rel=noreferrer will prevent the destination page from learning which page you came from. See developer.mozilla.org/en-US/docs/Web/HTML/Link_types for an excellent overview of all link types. Why are noreferrer or noopener important? See mathiasbynens.github.io/rel-noopener.

target

_blank, _parent, _self, _top framename

where to open the linked document

Here is an example that illustrates different values for the href attribute:

a1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>This is a simple hyperlink example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <header>
      <h1>Welcome to <a href=https://www.ltam.lu target=_blank>LAM</a>.</h1>
    </header>
    <main>
      <a href=a1contact.html>Contact us</a>
    </main>
  </body>
</html>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>This is a simple hyperlink example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <a href="http://www.ltam.lu/index.php?portal=26" target=_blank>How to find us</a>
      <a href=a1.html>Home</a>
      <a
        href="mailto:gilles.everling@education.lu?cc=everybody@world.com&bcc=spy@NSA.gov&subject=Top%20Secret%20Message&body=This%20is%20the%20message%20body">Email the author</a>
    </main>
  </body>
</html>

Note that we need to provide the correct path to the file that the hyperlink is linking to via the href attribute. If we are linking to a page on the Internet, we need to specify the complete Unified Resource Locator (URL, cf. en.wikipedia.org/wiki/URL), which consists of the protocol, a colon, two slashes, a host, normally given as a domain name but sometimes as a literal Internet Protocol (IP) address, optionally a port number and finally the full path of the resource. The protocol used to access Internet pages is called Hypertext Transfer Protocol (HTTP).

If we link to a file within our web site, we use a relative URL, as shown in line 12 of a1.html. If we link to a file on another server, we need to provide an absolute URL as shown in line 9.

If we want to allow the user to send an email by clicking on a hyperlink, we use mailto, as shown in lines 11 to 12 of a1contact.html. Note that we can pass additional parameters such as carbon copy (cc) and black carbon copy (bcc) as well as the subject and body. We put a ? in front of the parameters. We assign a value to a parameter using = and each parameter/value pair is separated by a &. Also note that we need to encode spaces using %20, which is the corresponding hexadecimal (32 decimal) ASCII code (cf. www.w3schools.com/charsets/ref_html_ascii.asp). The example above gives a validation error because of the new lines in the href attribute string. The whole string should be in a single line, but this would not allow the clean printing of the code in the book. See also stackoverflow.com/questions/10356329/mailto-link-multiple-body-lines.

A hyperlink can also point to another place on the same page. For this purpose, we can use the id attribute on any tag. This is useful if our page contains a huge amount of content and we want to give the user the option to jump directly to a specific location on the page, instead of having to scroll down manually.

To nest a hyperlink inside another hyperlink we can use the <object> tag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>This is a nested hyperlink example</title>
    <meta charset=UTF-8>
    <style>
      main > a > div {
        margin:           10px;
        padding:          20px;
        background-color: greenyellow;
      }

      main > a > div > object > a {
        background-color: yellowgreen;
      }
    </style>
  </head>
  <body>
    <main>
      <a href=https://students.btsi.lu target=_blank>
        <div>students.btsi.lu (<object type=text/html><a
                  href=https://students.btsi.lu/evegi144/WAD/WAD.html target=_blank>WAD</a>)
          </object>
        </div>
      </a>
    </main>
  </body>
</html>

4.1.11. <img>

The <img> tag is used to insert an image. This element supports the following particular attributes:

Name Value Description

alt

text

alternate text for image, required for successful validation

crossorigin

anonymous, use-credentials

use third-party site images with canvas (cf. developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes)

height

pixels

image height

ismap

ismap

image is a server-side map (cf. www.w3schools.com/tags/att_img_ismap.asp)

src

URL

image URL

usemap

#mapname

image is a client-side map (cf. www.w3schools.com/tags/att_img_usemap.asp

width

pixels

image width

The alt attribute is required for successful validation. It can be used by screen readers, search engines and others. It will also be displayed by the browser in case the image cannot be displayed.

We should always specify the exact width and height of an image, as they allow the browser to allocate the space required to display the image before the image is loaded. Without them, the browser will have to adjust the page layout after the image has finished loading.

We do not use the width and height attributes to change the size of an image, as the full image will still be loaded by the browser. In order to produce a thumbnail for instance, we use a drawing program such as GIMP (www.gimp.org) or an online editor, e.g. pixlr.com/editor (cf. Image resizing). Be careful to specify the correct path and name of the image. Use .. to go up to the parent folder and / to separate folder and file names. We can embed images inside hyperlinks, like so:

img1
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Image example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <a href="https://www.iconfinder.com/icons/131462/automatic_automatic_machine_automaton_dog_machine_machine_gun_robot_shadow_with_icon#size=512">
        <img src=dog_robot_with_shadow.png width=512 height=512
           alt="https://www.iconfinder.com/icons/131462/automatic_automatic_machine_automaton_dog_machine_machine_gun_robot_shadow_with_icon#size=512">
      </a>
    </main>
  </body>
</html>

Notice that we have to use quotes here for the value of the href and alt attributes, as they span several lines. The validator won’t like this.

4.1.11.1. Image formats

The three main image formats used on the Web are JPEG, PNG and GIF. There is a new kid on the block, called Scalable Vector Graphics (SVG), which requires a much deeper understanding to handle but offers a number of advantages, that we’ll look into in section SVG.

The key characteristics of the main image formats are summarized in the following table:

Format Compression Colors Transparency Animation

PNG

lossless

256 (8 bit), 16.7 million (24 bit) or 4.3 billion (32 bit)

yes

no

APNG

lossless

256 (8 bit), 16.7 million (24 bit) or 4.3 billion (32 bit)

yes

yes

WebP

lossy or lossless

256 (8 bit), 16.7 million (24 bit) or 4.3 billion (32 bit)

yes

yes

JPEG

lossy

16.7 million (24 bit)

no

no

GIF

lossless

256 (8 bit)

yes

yes

If you don’t need animation, PNG is the preferred format, particularly for web graphics.

If you want to create animated GIFs, take a loot at GifCam (blog.bahraniapps.com/gifcam), which allows you create and edit screencasts and save them as compact GIF images that can be easily embedded in your HTML5, like so:

4.1.11.2. Image resizing

Resizing an image with GIMP is easy:

GIMP1
GIMP2

After you’ve resized the image, export it under a new name:

GIMP3
4.1.11.3. Adding transparency

We can either use a program such as Online Image Editor (cf. www.online-image-editor.com/help/transparency) or use GIMP (www.bogotobogo.com/Gif/gimp-tutorial-transparency.php).

4.1.11.5. <map>

The <map> tag is used to define a client-side image-map. See www.w3schools.com/tags/att_img_usemap.asp for the details.

Here is a simple example application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Image map example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <img src=dog_robot_with_shadow.png width=512 height=512 usemap=#dogmap
         alt="https://www.iconfinder.com/icons/131462#size=512">
      <map name=dogmap>
        <area shape=rect coords=10,0,250,150 href=map1head.html alt=Head>
        <area shape=rect coords=50,151,400,340 href=map1body.html alt=Body>
        <area shape=rect coords=0,350,512,512 href=map1leg.html alt=Leg>
      </map>
    </main>
  </body>
</html>
4.1.11.6. Logo creation

With Inkscape we can create a logo very easily.

inkscape1
inkscape1a
inkscape1b
inkscape2
inkscape2a
inkscape3
inkscape4
inkscape5
inkscape6

Export the bitmap using Ctrl+Shift+E and select page as export area.

inkscape7
4.1.11.7. figure and figcaption

4.1.12. <nav>

This element is used to create the main navigation on a site:

navigation1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Navigation</title>
    <meta charset=UTF-8>
    <link href=navigation1.css rel=stylesheet>
  </head>
  <body>
    <header>
      <h1>Navigation</h1>
      <nav>
        <ul>
          <li><a href=#>Home</a></li>
          <li><a href=#>About</a></li>
          <li><a href=#>Team</a></li>
          <li><a href=#>Shareholders</a></li>
          <li><a href=#>Contact</a></li>
        </ul>
      </nav>
    </header>
  </body>
</html>

Here the CSS3 file (don’t worry, we’ll address that topic in the next chapter):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
body {
  background-color: lightseagreen;
}

h1 {
  margin: 0;
}

ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

li {
  display: inline;
}

4.1.13. <main>

The main element represents the main content section of the body of a document or application. The main content section consists of content that is directly related to or expands upon the central topic of a document or central functionality of an application.

Note: the main element is not sectioning content and has no effect on the document outline.

The main content section of a document includes content that is unique to that document and excludes content that is repeated across a set of documents such as site navigation links, copyright information, site logos and banners and search forms (unless the document or applications main function is that of a search form).

Authors MUST NOT include more than one main element in a document.

Authors MUST NOT include the main element as a child of an article, aside, footer, header or nav element.

4.1.14. <section>

The section element represents a generic section of a document or application. A section, in this context, is a thematic grouping of content, typically with a heading.

Example:

section1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Section example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <header>
    </header>
    <main>
      <section>
        <h1>Section 1</h1>
        This is a section with some content.
      </section>
      <section>
        <h1>Section 2</h1>
        This is another section with some content.
      </section>
    </main>
  </body>
</html>

The footer element represents a footer for its nearest ancestor sectioning content or sectioning root element. A footer typically contains information about its section such as who wrote it, links to related documents, copyright data, and the like.

When the footer element contains entire sections, they represent appendices, indexes, long colophons, verbose license agreements, and other such content.\end{quote}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Footer example</title>
    <meta charset=utf-8>
  </head>
  <body>
    <header>
      <nav></nav>
    </header>
    <main>

    </main>
    <footer>&copy; 2014 WMOTU</footer>
  </body>
</html>

4.1.16. <article>

The article element represents a complete, or self-contained, composition in a document, page, application, or site and that is, in principle, independently distributable or reusable, e.g. in syndication. This could be a forum post, a magazine or newspaper article, a blog entry, a user-submitted comment, an interactive widget or gadget, or any other independent item of content.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Article example</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <article>
        <h1>HTSTA</h1>
        <p>HTSTA is the first step to a fulfilling web developer career.</p>
      </article>
    </main>
  </body>
</html>

4.1.17. <aside>

The aside element represents a section of a page that consists of content that is tangentially related to the content around the aside element, and which could be considered separate from that content. Such sections are often represented as sidebars in printed typography.

The element can be used for typographical effects like pull quotes or sidebars, for advertising, for groups of nav elements, and for other content that is considered separate from the main content of the page.

The link above provides usage examples.

4.1.18. <div>

The div element has no special meaning at all. It represents its children. It can be used with the class, lang, and title attributes to mark up semantics common to a group of consecutive elements.

Note: Authors are strongly encouraged to view the div element as an element of last resort, for when no other element is suitable. Use of more appropriate elements instead of the div element leads to better accessibility for readers and easier maintainability for authors.

4.1.19. <q>

To mark up a short quotation, we use the <q> tag. This element has one special attribute, cite, that can be used to specify the source URL of the quote.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Quote example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      George Orwell's <q>Politics and the English Language</q>  from 1946
    </main>
  </body>
</html>

4.1.20. <blockquote>

To mark up a longer quotation from another source, use the <blockquote> tag:

blockquote1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Blockquote example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      George Orwell, in his <q>Politics and the English Language</q> from 1946,
      provided the following insight:
      <blockquote>
        Political language… is designed to make lies sound truthful and murder
        respectable,
        and to give an appearance of solidity to pure wind.
      </blockquote>
    </main>
  </body>
</html>

4.1.22. Firefox console and Firebug

By pressing Shift+F2 you open the Firefox console, which is a great tool to analyse web pages:

FirefoxConsole1

The console tab displays information, warning and error messages and will be one of our most important development tools throughout our web app development journey. The inspector enables us to take a closer look at the styling of a particular element. The debugger will be very helpful to track errors in our JavaScript adventures. The style editor permits the real time changing of the current web page’s styles. Try it! The profiler serves to analyse the performance of our app and detect bottleneck. The network tab displays detailed information about what happens on the network. This will be very helpful once we start using HTTP forms and Ajax.

By pressing F12 you open Firebug, if this plugin is installed in your Firefox browser. If not, you can install it by selecting Tools  Add-ons. Search for Firebug and install it.

Firebug provides even more advanced analysis functionality than the console.

4.1.24.1. Style sheet

We have already seen at the beginning of this chapter an example of an external style sheet inclusion. We just have to include a link with the correct relationship attribute in the head of our document:

1
<link rel=stylesheet href=style.css>
4.1.24.2. Favicon
favicon1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Favicon example 1</title>
    <meta charset=UTF-8>
    <link rel=icon href=favicon.ico>
  </head>
  <body>
    <main>
    </main>
  </body>
</html>

We can even have animated favicons, at least in Firefox:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Favicon example 2</title>
    <meta charset=UTF-8>
    <link rel=icon href=bear.gif type=image/gif>
  </head>
  <body>
    <main>
    </main>
  </body>
</html>

Icon files can be 16×16, 32×32, 48×48, or 64×64 pixels in size, and 8-bit, 24-bit, or 32-bit in color depth.

4.1.25. <meta>

The <meta> tag is used to provide metadata, i.e. data that describes the document. This data is not displayed on the page, but can be processed by the browser, search engines or other web services (cf. developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/The_head_metadata_in_HTML).

This element supports the following particular attributes:

Name Value Description

charset

character set

character encoding for the document, we use utf-8

content

text

value associated with the http-equiv or name attribute

http-equiv

content-type, default-style, refresh

create HTTP header for content attribute

name

application-name, author, description, generator, keywords

name for the metadata

We use the charset attribute to specify the character encoding of our document. This should be set to Unicode, i.e. utf-8, as it allows us to use language specific characters such as é and ä. A list of all available character encodings can be found at www.iana.org/assignments/character-sets/character-sets.xhtml.

Before the advent of HTML5, http-equiv was used to set the character encoding, but no more. The value default-style can be used to specify the preferred stylesheet from a selection of link or style elements in case there are several in your document. The value refresh can be used to specify after how many seconds a page should be automatically refreshed (i.e. reloaded) or if it should redirect to another page. This can be useful for a site whose content changes rapidly and where you don’t want to use JavaScript. For instance, let’s assume you have a new web site and you want users to be automatically transferred from your old site to the new one. On your old site, you’d have the following main page:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Redirection example</title>
    <meta charset=UTF-8>
    <meta http-equiv=refresh content="3; url=metaredirect2.html">
  </head>
  <body>
    <main>
      <header>
        <h1>You'll be redirected to my new site in 3 seconds.</h1>
      </header>
    </main>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Redirection example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <header>
        <h1>Welcome to my new site.</h1>
        Hope you like it!
      </header>
    </main>
  </body>
</html>

The name attribute can take one of the following values:

Value Description

application-name

name of the Web application

author

document author

description

description of the page content, can be used by search engines

generator

if the page was generated by a specific software

keywords

comma-separated list of keywords relevant to the page content targeted at search engines

Let’s look at an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Meta name usage example</title>
    <meta charset=UTF-8>
    <meta name=application-name content="Meta name usage example">
    <meta name=author content="Gilles Everling">
    <meta name=description content="Meta name usage">
    <meta name=keywords content=meta,name,HTML5>
  </head>
  <body>
    <main>
      <header>
        <h1>A simple application of the <code>meta name</code> attribute.</h1>
      </header>
    </main>
  </body>
</html>
See github.com/joshbuchea/HEAD for much more insight into what can go into the head of your HTML document.

4.1.26. <table>

Tables are used to display tabular data, for instance the current national football league rankings. HTML5 tables must not be used for layout purposes. We’ll look at a number of appropriate ways to create a tabular layout later on.

A table consists of rows (<tr>) and cells (<td>). Thus, a Tic Tac Toe table would look like this:

table1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>A simple Tic Tac Toe table</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <table>
        <tr>
          <td>X</td>
          <td>X</td>
          <td>O</td>
        </tr>
        <tr>
          <td>X</td>
          <td>X</td>
          <td>O</td>
        </tr>
        <tr>
          <td>O</td>
          <td>O</td>
          <td>X</td>
        </tr>
      </table>
    </main>
  </body>
</html>

The <table> tag supports only one attribute, border. It can have no value, "" or 1. In each of these cases, the table and each cell will have a border 1 pixel wide. Without this attribute the table and cells will have no border.

In most cases it is useful to have a table header, which contains the name or description of the data for each column. We may also need a table footer (cf. www.w3.org/TR/html51/tabular-data.html#the-tfoot-element). For this purpose, we split the table into a head, a body and a footer part, using the <thead>, <tbody> and <tfoot> tags. Header cells are displayed centered and bold using the <th> tag instead of <td>. We can add a caption using the `<caption> tag. Here’s an example:

table2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>A simple table with caption, header, body and footer</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <table border>
        <caption>This term's grades</caption>
        <thead>
          <tr>
            <th>Subject</th>
            <th>Grade</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>SYSEX1</td>
            <td>51</td>
          </tr>
          <tr>
            <td>MATHE1</td>
            <td>45</td>
          </tr>
        </tbody>
        <tfoot>
          <tr>
            <td>Average</td>
            <td>48</td>
          </tr>
        </tfoot>
      </table>
    </main>
  </body>
</html>

We can have cells span several columns and/or several rows, using the colspan and rowspan attributes. Example:

table3
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>colspan and rowspan example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <table border>
        <thead>
          <tr>
            <th>Name</th>
            <th>Village</th>
            <th colspan=2>Phone numbers</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>Asterix</td>
            <td rowspan=2>Gaul</td>
            <td>123 456</td>
            <td>621 123 456</td>
          </tr>
          <tr>
            <td>Obelix</td>
            <td>123 457</td>
            <td>621 123 457</td>
          </tr>
        </tbody>
      </table>
    </main>
  </body>
</html>

The <th> and <td> tags can have a headers attribute. It links a cell to a given header cell. For this to work, the header cell needs an id. This has no impact on the page display, but may be used by screen readers. See www.w3schools.com/tags/att_th_headers.asp for an example.

The <th> tag can have a scope attribute, which indicates whether a header cell is a header for a column, row, or group of columns or rows. See www.w3schools.com/tags/att_th_scope.asp and developer.mozilla.org/en-US/docs/Learn/HTML/Tables/Advanced.

For formatting purposes, we can use the <colgroup> tag, see www.w3schools.com/tags/tag_colgroup.asp.

Soon, we’ll see how we can style tables with CSS. Here’s a little foretaste:

table4
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
<!DOCTYPE html>
<html lang=de>
  <head>
    <title>HTML and CSS Table Demo</title>
    <meta charset=UTF-8>
    <style>
      body {
        background: radial-gradient(rgb(200, 50, 20), rgb(255, 255, 60), rgb(255, 50, 20),
        black) no-repeat fixed;
        overflow:   hidden;
      }

      h1 {
        font:        6em impact fantasy;
        text-shadow: 3px 3px white;
        animation:   introAnimation 7s;
        -webkit-animation: introAnimation 7s;
        overflow:    hidden;
        text-align:  center;
        position:    absolute;
      }

      @keyframes introAnimation {
        0% {
          top:       400px;
          font-size: 0.1em;
          transform: rotate(0deg);
        }
        100% {
          transform: rotate(720deg);
          top:       0;
        }
      }

      @-webkit-keyframes introAnimation {
        0% {
          top:       400px;
          font-size: 0.1em;
          transform: rotate(0deg);
        }
        100% {
          transform: rotate(720deg);
          top:       0;
        }
      }

      table {
        position:       absolute;
        overflow:       hidden;
        top:            200px;
        left:           0;
        color:          white;
        border:         2px ridge red;
        border-spacing: 0;
        transition:     left 5s;
        text-shadow:    1px 1px black;
      }

      table:hover {
        color: gold;
        left:  5000px;
      }

      table caption {
        font-size: 2em;
      }

      th {
        background-color: lightblue;
        text-align:       left;
        padding:          5px;
        margin:           0;
      }

      td {
        padding: 5px;
      }

      tr {
        padding: 5px;
      }

      tr:nth-of-type(odd) {
        background-color: green;
      }
    </style>
    <script type=module>
      document.querySelector("h1").style.left = (window.innerWidth - 566) / 2 + "px";
      addEventListener('resize', () => {
        document.querySelector("h1").style.left = (window.innerWidth - 566) / 2 + "px";
      });
    </script>
  </head>
  <body>
    <header>
      <h1>T0IF HTSTA</h1>
    </header>
    <main>
      <table>
        <caption>Nationale Fußballtabelle</caption>
        <thead>
          <tr>
            <th></th>
            <th>Rang</th>
            <th>Mannschaft</th>
            <th>Punkte</th>
            <th>Tore A</th>
            <th>Tore B</th>
            <th>Diff.</th>
            <th>Spiele</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td rowspan=14 style="padding: 0; transform: rotate(-90deg);
            font-size: 3em;">22.5.13
            </td>
            <td>1.</td>
            <td>Fola</td>
            <td>53</td>
            <td>58</td>
            <td>20</td>
            <td>38</td>
            <td>24</td>
          </tr>
          <tr>
            <td>2.</td>
            <td>F91</td>
            <td>49</td>
            <td>45</td>
            <td>17</td>
            <td>28</td>
            <td>24</td>
          </tr>
          <tr>
            <td>3.</td>
            <td>Jeunesse</td>
            <td>47</td>
            <td>50</td>
            <td>25</td>
            <td>25</td>
            <td>24</td>
          </tr>
          <tr>
            <td>4.</td>
            <td>RM Hamm Benfica</td>
            <td>42</td>
            <td>47</td>
            <td>34</td>
            <td>13</td>
            <td>24</td>
          </tr>
          <tr>
            <td>5.</td>
            <td>Déifferdeng03</td>
            <td>41</td>
            <td>46</td>
            <td>31</td>
            <td>15</td>
            <td>24</td>
          </tr>
          <tr>
            <td>6.</td>
            <td>Gréiwemaacher</td>
            <td>36</td>
            <td>40</td>
            <td>32</td>
            <td>8</td>
            <td>24</td>
          </tr>
          <tr>
            <td>7.</td>
            <td>Kanech</td>
            <td>34</td>
            <td>38</td>
            <td>33</td>
            <td>5</td>
            <td>24</td>
          </tr>
          <tr>
            <td>8.</td>
            <td>Käerjéng</td>
            <td>31</td>
            <td>42</td>
            <td>48</td>
            <td>-6</td>
            <td>24</td>
          </tr>
          <tr>
            <td>9.</td>
            <td>RFCUL</td>
            <td>30</td>
            <td>38</td>
            <td>37</td>
            <td>1</td>
            <td>24</td>
          </tr>
          <tr>
            <td>10.</td>
            <td>Wolz 71</td>
            <td>30</td>
            <td>39</td>
            <td>61</td>
            <td>-22</td>
            <td>24</td>
          </tr>
          <tr>
            <td>11.</td>
            <td>Progrès Nidderkuer</td>
            <td>24</td>
            <td>26</td>
            <td>41</td>
            <td>-15</td>
            <td>24</td>
          </tr>
          <tr>
            <td>12.</td>
            <td>Etzella</td>
            <td>24</td>
            <td>35</td>
            <td>55</td>
            <td>-20</td>
            <td>24</td>
          </tr>
          <tr>
            <td>13.</td>
            <td>Peiteng</td>
            <td>14</td>
            <td>20</td>
            <td>50</td>
            <td>-30</td>
            <td>24</td>
          </tr>
          <tr>
            <td>14.</td>
            <td>Kayl/Teiteng Union05</td>
            <td>12</td>
            <td>27</td>
            <td>67</td>
            <td>-40</td>
            <td>24</td>
          </tr>
        </tbody>
      </table>
    </main>
  </body>
</html>

4.1.27. Forms

HTML forms (www.w3.org/TR/html51/semantics.html#forms) are used to pass data to a server. They are the building blocks that allow the user to provide data to our application. When you use a search engine or buy something in an online shop, you use forms to enter your information.

An HTML form can consist of different input elements such as text fields, check boxes, radio-buttons, submit buttons, selection lists, text areas, labels etc.

Here’s the full list of form tags to be used within <form>:

Tag Description

<input>

single line text field

<textarea>

multi line text area

<label>

label, i.e. a text to be displayed next to a form element

<fieldset>

groups related elements

<legend>

caption (short description) for a field set

<select>

drop-down list

<optgroup>

group of related options in a drop-down list

<option>

option in a drop-down list

<button>

clickable button

<datalist>

option list

<keygen>

key-pair generator field

<output>

result of a calculation

All form elements are enclosed within the <form> tag. Here’s a simple example:

HTML forms1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>A first form example using post</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <form method=post action=forms1.php>
        First name: <input name=first_name required><br>
        Last name: <input name=last_name required><br>
        <input type=submit>
      </form>
    </main>
  </body>
</html>

When you submit your input, the forms1.php script gets executed on the server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>A first form example using post</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <?php
        if (isset($_POST['first_name'], $_POST['last_name']))
          echo '<p>Hi ' . $_POST['first_name'] . ' ' . $_POST['last_name'] . '</p>';
      ?>
    </main>
  </body>
</html>
 

Don’t worry about the PHP part, we’ll get into that later. Our example uses two of the most important <form> tag attributes: action and method. The former specifies the script on the server that should receive the form data. The latter indicates the method that should be used to send the data to the server, either GET, which sends the data via the URL or POST, which sends the data embedded within the HTTP request. Run our first form example, open the Firefox console and select the Network tab. Now enter your first and last names and submit the form. You should see something similar to this:

forms1

Now click on the forms1.php line and select the Params tab on the right:

forms2

As you can see, your input has been sent as form data, i.e. embedded inside the form.

Now let’s check what happens if we use the GET method: students.btsi.lu/evegi144/WAD/HTML5/forms2.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>A first form example using get</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <form action=forms2.php>
        First name: <input name=first_name required><br>
        Last name: <input name=last_name required><br>
        <input type=submit>
      </form>
    </main>
  </body>
</html>

When you submit your input, the forms2.php script gets executed on the server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>A first form example using get</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <?php
        if (isset($_GET['first_name']) && isset($_GET['last_name']))
          echo '<p>Hi ' . $_GET['first_name'] . ' ' . $_GET['last_name'] . '</p>';
      ?>
    </main>
  </body>
</html>
 

If you perform the same network analysis as before, you get this:

forms3

You should notice two changes:

  1. The URL contains a ? and a & as well as the data you entered in the form.

  2. The parameter tab says that the data has been sent as query string, i.e. as part of the URL and not embedded as form data.

This means that the form data is visible to everyone and can be easily intercepted, whereas for the POST method this is a little bit more difficult. GET submissions can be bookmarked and URLs are usually stored in log files on the server, whereas the body of HTTP requests usually is not and can also not be bookmarked. We therefore prefer to use the POST method.

Here is an overview of all the attributes specific to the <form> tag:

Attribute Value Description

accept-charset

character set

character encoding to be used for form submission

action

URL

script to receive the form data

autocomplete

on/off

turn autocomplete on or off

enctype

application/x-www-form-urlencoded, multipart/form-data, text/plain

how the data should be encoded (only for POST method)

method

GET or POST

HTTP method to be used

name

text

name of the form

novalidate

the form should not be validated upon submission

target

_blank, _self, _parent, _top

display response from server in a new, the current, the parent or the top window/tab

Given the excellent documentation on W3Schools regarding all form elements, I will not repeat the details here but instead refer you to their site www.w3schools.com/html/html_forms.asp. Go through the examples and get a feel for forms. We’ll use them throughout our Web app development journey.

Here is a more complete example:

HTML forms3
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>A more advanced form example</title>
    <meta charset=utf-8>
    <style>
      form {
        width:        350px;
        margin-left:  auto;
        margin-right: auto;
      }

      form label {
        float:         left;
        width:         150px;
        text-align:    right;
        padding-right: 10px;
        margin-top:    10px;
      }

      form input {
        margin-top:    10px;
        text-shadow:   1px 1px 1px white;
        border-radius: 5px;
      }

      form input:focus {
        background-color: yellow;
      }

      form input[type=submit], form input[type=button], form input[type=reset] {
        background:  linear-gradient(to bottom right, yellow, red);
        margin-left: 160px;
        width:       190px;
      }

      form input[type=submit]:focus, form input[type=button]:focus,
      form input[type=reset]:focus {
        border: 2px solid grey;
      }

      form input::-moz-focus-inner {
        border: 0;
      }

      form legend {
        font-weight: bold;
      }

      form select {
        border-radius: 5px;
      }

      form select optgroup {
        background-color: yellow;
      }

      form select optgroup option {
        background-color: greenyellow;
      }
      form fieldset {
        border: 1px solid black;
      }
    </style>
  </head>
  <body>
    <main>
      <form method=post>
        <fieldset>
          <legend>Personal data</legend>
          <label>First name:</label>
          <input list=names name=first_name required autofocus>
          <datalist id=names>
            <option>Donald</option>
            <option>Mickey</option>
          </datalist><br>
          <label>Last name:</label>
          <input name=last_name required><br>
          <label>Password:</label>
          <!-- oncopy, onpaste and oncut are not part of the HTML5 standard,
          so they should not be used! -->
          <input type=password name=password required oncopy='return false;'
               onpaste='return false;' oncut='return false;'><br>
          <label>Email:</label>
          <input type=email name=email required><br>
          <label></label> <!-- Just used for layout purposes. -->
          <input type=radio name=sex value=male checked>Male<br>
          <label></label>
          <input type=radio name=sex value=female>Female<br>
          <label></label>
          <input type=checkbox name=bike value=Bike>I have a bike<br>
          <label></label>
          <input type=checkbox name=roller value=Roller>I have a roller
        </fieldset>
        <fieldset>
          <legend>Other data</legend>
          <select name=car>
            <optgroup label="Swedish Cars">
              <option value=volvo>Volvo</option>
              <option value=saab selected>Saab</option>
            </optgroup>
            <optgroup label="German Cars">
              <option value=mercedes>Mercedes</option>
              <option value=audi>Audi</option>
            </optgroup>
          </select>
          <textarea rows=3 cols=50 name=my_area maxlength=500
                placeholder="Short description of yourself (max. 500 chars)"></textarea><br>
          <label>Color:</label>
          <input type=color name=color><br>
          <label>Number (1-10):</label>
          <input type=number name=number min=1 max=10><br>
          <label>Number range:</label>
          <input type=range name=range min=1 max=100 value=50 step=5><br>
        </fieldset>
        <input type=submit value=Submit>
        <input type=reset>
      </form>
      <form oninput="x.value = parseInt(a.value) + parseInt(b.value)">0
        <input type=range id=a value=50>
        + <input type=number id=b value=50>
        =
        <output name=x for="a b"></output>
      </form>
    </main>
  </body>
</html>
4.1.27.1. <input>
The input element represents a typed data field, usually with a form control to allow the user to edit the data.

Excellent examples can be found at www.w3schools.com/tags/tag_input.asp.

See www.w3schools.com/html/html_form_attributes.asp for details on input attributes.

To capture photos using the user’s camera you can use the capture attribute, like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>Camera capture example</title>
  </head>
  <body>
    <form>
      <input type=file accept=image/* capture=user>
    </form>
  </body>
</html>

Please note that you can prevent the copying, pasting and cutting of input field contents by setting the corresponding event handlers (cf. Events) as shown in the password input in the example above.

4.1.27.2. <textarea>

The textarea element represents a multiline plain text edit control for the element’s raw value. The contents of the control represent the control’s default value.

The raw value of a \texttt{textarea} control must be initially the empty string.

4.1.27.3. <label>
The label element represents a caption in a user interface. The caption can be associated with a specific form control, known as the label element’s labeled control, either using the for attribute, or by putting the form control inside the label element itself.
4.1.27.4. <fieldset>

The fieldset element represents a set of form controls optionally grouped under a common name.

The name of the group is given by the first legend element that is a child of the fieldset element, if any. The remainder of the descendants form the group.

4.1.27.5. <legend>
The legend element represents a caption for the rest of the contents of the legend element’s parent fieldset element, if any.
4.1.27.6. <select>

The select element represents a control for selecting amongst a set of options.

The multiple attribute is a boolean attribute. If the attribute is present, then the select element represents a control for selecting zero or more options from the list of options. If the attribute is absent, then the select element represents a control for selecting a single option from the list of options.

The size attribute gives the number of options to show to the user. The size attribute, if specified, must have a value that is a valid non-negative integer greater than zero.

The display size of a select element is the result of applying the rules for parsing non-negative integers to the value of element’s size attribute, if it has one and parsing it is successful. If applying those rules to the attribute’s value is not successful, or if the size attribute is absent, then the element’s display size is 4 if the element’s multiple content attribute is present, and 1 otherwise.

The list of options for a select element consists of all the option element children of the select element, and all the option element children of all the optgroup element children of the select element, in tree order.

4.1.27.7. <optgroup>
The optgroup element represents a group of option elements with a common label.
4.1.27.8. <option>
The option element represents an option in a select element or as part of a list of suggestions in a datalist element.
4.1.27.9. <button>
The button element represents a button labeled by its contents.
4.1.27.10. <datalist>
The datalist element represents a set of option elements that represent predefined options for other controls.
4.1.27.11. <keygen>
The keygen element represents a key pair generator control. When the control’s form is submitted, the private key is stored in the local keystore, and the public key is packaged and sent to the server.
4.1.27.12. <output>
The output element represents the result of a calculation or user action.

4.1.28. <dialog>

This element represents a dialog box or other interactive component, such as a dismissable alert, inspector, or subwindow. It is supported by most browsers.

developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog

www.w3schools.com/tags/tag_dialog.asp

4.1.29. Block vs inline elements

A block element is an element that takes up the full width available, and has a line break before and after it.

Examples of block elements:

  • <h1>

  • <p>

  • <div>

An inline element only takes up as much width as necessary, and does not force line breaks.

Examples of inline elements:

  • <span>

  • <a>

display1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Inline vs block display example</title>
    <meta charset=utf-8>
    <style>
      main {
        border:  2px solid red;
        padding: 5px;
      }

      h1 {
        border:  1px inset blue;
        padding: 3px;
      }

      span {
        border: 1px dashed green;
      }

      .inlineList {
        display: inline;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>This is a heading level 1, a <strong>block</strong> element.</h1>
      This is a <span>span</span>, which is an <strong>inline</strong> element.
      <p>Sometimes it is useful to change the default display settings. For
      example, we might want list items to be stacked horizontally instead of
      vertically. So instead of this:
      </p>
      <ul>
        <li>Item1</li>
        <li>Item2</li>
        <li>Item3</li>
        <li>Item4</li>
      </ul>
      We might prefer this:
      <ul>
        <li class=inlineList>Item1</li>
        <li class=inlineList>Item2</li>
        <li class=inlineList>Item3</li>
        <li class=inlineList>Item4</li>
      </ul>
    </main>
  </body>
</html>

A complete list of block elements can be found at developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements. Likewise a list of inline elements is at developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements.

4.1.30. <video>

The <video> tag does what its name suggests, i.e. it defines video. It supports the following particular attributes:

Name Value Description

autoplay

play video automatically

controls

display video controls

height

pixels

height of the video player

loop

video will loop indefinitely (if supported by browser)

muted

muted video output

poster

URL

image to be shown while video is downloading and playback has not started

preload

auto, metadata, none

how the video should be loaded (cf. www.w3schools.com/tags/att_audio_preload.asp)

src

URL

video URL

width

pixels

width of the video player

4.1.30.1. Video file formats

There are three video file formats currently supported. They are MP4, WebM and OGG. Here is an overview of browser support:

MP4 WebM OGG

Firefox

yes

yes

yes

Chrome

yes

yes

yes

Internet Explorer

yes

no

no

Safari

yes

no

no

MIME type

video/mp4

video/webm

video/ogg

Within the <video> tag we use the <source> tag to specify multiple video sources that the browser can choose from, based on its file format support.

If we need to convert between the different video file formats we can use a number of free tools, such as Miro Video Converter (www.mirovideoconverter.com) or Handbrake (handbrake.fr).

Here is a simple example of the <video> tag in action:

video1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Video example</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <video controls autoplay loop
        src=http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4>
        Your browser does not support the video tag.
      </video>
    </main>
  </body>
</html>

Text inside the <video> opening and closing tag will be shown in browsers that do not support the tag.

4.1.31. <audio>

The <audio> tag does what its name suggests, i.e. it defines sound. It supports the following particular attributes:

Name Value Description

autoplay

play audio automatically

controls

display audio controls

loop

audio will loop indefinitely (if supported by browser)

muted

muted audio output

preload

auto, metadata, none

how the audio should be loaded (cf. www.w3schools.com/tags/att_audio_preload.asp)

src

URL

audio URL

4.1.31.1. Audio file formats

There are three audio file formats currently supported. They are MP3, WAV and OGG. Here is an overview of browser support and main format features:

MP3 OGG WAV

Firefox

yes

yes

yes

Chrome

yes

yes

yes

Internet Explorer

yes

no

no

Safari

yes

yes

no

MIME type

audio/mpeg

audio/ogg

audio/wav

lossless

no

no

usually yes

compressed

yes

yes

usually no

If we need to convert between the different audio file formats we can use a number of free tools, such as Audacity (www.audacityteam.org). To produce our own music, we can use the outstanding LMMS (lmms.io) or AudioTool (www.audiotool.com).

Here is a simple example of the <audio> tag in action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Audio example</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <audio controls autoplay loop>
        <source src=https://api.audiotool.com/track/ge_trance_1/mixdown.ogg
                type=audio/ogg>
        <source src=https://api.audiotool.com/track/ge_trance_1/mixdown.mp3
            type=audio/mpeg>
        Your browser does not support the audio tag.
      </audio>
    </main>
  </body>
</html>

Note that the browser will use the first file format that it supports from those listed. The current versions of the main browsers all support MP3. If we only want to support those we can omit the <source> tag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Audio example</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <audio controls autoplay loop
           src=http://api.audiotool.com/track/ge_trance_1/9/mixdown.mp3>
        Your browser does not support the audio tag.
      </audio>
    </main>
  </body>
</html>

Text inside the <audio> opening and closing tag will be shown in browsers that do not support the tag.

4.1.32. Additional elements

4.1.32.1. <pre>

The <pre> tag defines preformatted text. Text is displayed in a fixed-width font and preserves both spaces and line breaks (cf. www.w3schools.com/tags/tag_pre.asp).

Example:

pre1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Preformatted Text Example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <pre>
This    is           preformatted text.
Spaces and linebreaks are preserved.
The font used by the browser has fixed width.
    </pre>
  </body>
</html>
4.1.32.2. <mark>

This element is used to highlight text (cf. www.w3schools.com/tags/tag_mark.asp). Example:

mark1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Mark example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <p>The <mark>mark element</mark> is used to highlight text in HTML5.</p>
    </main>
  </body>
</html>
4.1.32.3. <address>

This element is used to display contact information for a person, people or organization. It should include physical and/or digital location/contact information and a means of identifying a person(s) or organization the information pertains to. (cf. www.w3.org/TR/html5/grouping-content.html#elementdef-address).

Example:

address1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Address example</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <footer>
      <address>
        <a href=mailto:gilles.everling@education.lu>Gilles Everling</a>
      </address>
    </footer>
  </body>
</html>
4.1.32.4. <time>
4.1.32.5. <ins>
4.1.32.6. <del>
4.1.32.7. <iframe>
iframe1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>iframe example</title>
    <meta charset=utf-8>
    <style>
      nav {
        position:         fixed;
        left:             0;
        top:              0;
        bottom:           0;
        width:            100px;
        background-color: lightgoldenrodyellow;
      }

      main {
        position:          absolute;
        left:              100px;
        top:               0;
        bottom:            0;
        right:             0;
        background-color:  lightblue;
        animation:         animate 60s linear 5s infinite;
      }

      iframe {
        display: block;
        width:   100%;
        height:  100%;
        border:  none;
      }

      @keyframes animate {
        to {
          transform: rotateX(360deg) rotateY(360deg);
        }
      }

      ul {
        list-style-type: none;
        padding-left:    10px;
      }

      li {
        padding-top: 5px;
      }
    </style>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href=https://students.btsi.lu/evegi144/WAD/WMOTUInvadersOO
             target=myFrame>WMOTU Invaders</a></li>
        <li><a href=https://students.btsi.lu/evegi144/WAD/WMOTUQuack
             target=myFrame>WMOTU Quack</a></li>
      </ul>
    </nav>
    <main>
      <iframe name=myFrame></iframe>
    </main>
  </body>
</html>
iframe2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE html>
<html lang=en>
  <head>
    <title></title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <iframe width=560 height=315 src=https://www.youtube.com/embed/-H2x_tGAxSM?rel=0
          allowfullscreen></iframe>
    </main>
  </body>
</html>

4.1.33. <embed>

This tag creates a container for a plug-in, such as Adobe Reader:

embed1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Embed example</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <embed src=T-IF-WEB2-WSERS1_LP.pdf width=1000 height=800>
    </main>
  </body>
</html>

4.1.37. Quiz

Take the w3schools quiz at www.w3schools.com/quiztest/quiztest.asp?qtest=HTML5 as a fun way to check you are as good as you think you are.

4.2. CSS3

CSS is a style sheet language that allows authors and users to attach style (e.g., fonts and spacing) to structured documents (e.g., HTML documents and XML applications). By separating the presentation style of documents from the content of documents, CSS simplifies Web authoring and site maintenance.

The official CSS3 specifications can be found at www.w3.org/Style/CSS/current-work.

What can be done with CSS? Here's an example.

A fantastic CSS resource is at developer.mozilla.org/en-US/docs/Web/CSS

4.2.1. Include CSS3

There are four ways we can style our web page with CSS3.

4.2.1.1. Inline

We use the style attribute of a specific element to style it.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Inline styling with CSS3</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main style='background-color: gold; color: black'>
      This element is style with inline CSS3.
    </main>
  </body>
</html>
4.2.1.2. Embedded

We use the <style> tag to embed CSS in the head of our document.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Embedded CSS3</title>
    <meta charset=utf-8>
    <style>
      main {
        background-color: lightcoral;
      }

      p {
        border: 2px dashed green;
        color: lime;
      }
    </style>
  </head>
  <body>
    <main>
      <p>All elements styled by embedded CSS3.</p>
    </main>
  </body>
</html>
4.2.1.3. External

We use the <link> tag to include an external CSS style sheet in the head of our document.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>External CSS3 style sheet</title>
    <meta charset=utf-8>
    <link href=external1.css rel=stylesheet>
  </head>
  <body>
    <main>
      <p>All elements styled via an external CSS3 style sheet.</p>
    </main>
  </body>
</html>
1
2
3
4
5
6
7
8
main {
  background-color: lightcoral;
}

p {
  border: 2px dashed green;
  color: lime;
}
4.2.1.4. Imported

We use the @import rule to include an external CSS style sheet in the current style sheet. See developer.mozilla.org/en-US/docs/Web/CSS/@import and www.w3.org/TR/CSS2/cascade.html#at-import.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Imported CSS3 style sheet</title>
    <meta charset=utf-8>
    <style>
      @import url('external1.css');
      aside {
        padding: 10px;
        background-color: mediumorchid;
      }
    </style>
  </head>
  <body>
    <main>
      <p>Styled via an imported CSS3 style sheet.</p>
      <aside>Styled via embedded CSS.</aside>
    </main>
  </body>
</html>
1
2
3
4
5
6
7
8
main {
  background-color: lightcoral;
}

p {
  border: 2px dashed green;
  color: lime;
}
4.2.1.5. Rules of precedence

A browser processes styles in the order described at www.w3.org/TR/css3-cascade/#cascading-origins. A somewhat simplified illustration of the cascade looks like this:

CSSCascade

However, we can influence rule precedence by either changing the order of inclusion or by using the !important annotation.

If we include an external style sheet after the declaration of the embedded style sheet in the head of our document, the external styles will overwrite the embedded ones.

In the following example, the paragraph background color would normally be green, given that inline styling takes precedence over embedded styling. However, the !important annotation gives the embedded style a higher priority:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Changing the cascade with !important</title>
    <meta charset=utf-8>
    <style>
      p {
        background-color: gold !important;
      }
    </style>
  </head>
  <body>
    <main>
      <p style='background-color: green;'>Some text</p>
    </main>
  </body>
</html>

You can find the default style sheets used by the main browsers at the following links (cf. stackoverflow.com/questions/6867254/browsers-default-css-for-html-elements):

Firefox

searchfox.org/mozilla-central/source/layout/style/res/html.css

Chrome

trac.webkit.org/browser/trunk/Source/WebCore/css/html.css

HTML5 recommendation

www.w3.org/TR/html5/rendering.html

4.2.2. Syntax

CSSSyntax
This site is a CSS3 learner’s paradise: www.w3schools.com/css/css_examples.asp
4.2.2.1. Comments

You can comment out parts of a line or even several lines by enclosing them between /* and */:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>CSS comment example 1</title>
    <meta charset=utf-8>
    <style>
      body {
        background-color: green;
        /*display: none;
        opacity: 0.5;*/
      }
    </style>
  </head>
  <body>
  </body>
</html>

Comments cannot be nested, i.e. this won’t work:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>CSS comment example 1</title>
    <meta charset=utf-8>
    <style>
      body {
        background-color: green;
        /*display: /*block*/none;
        opacity: 0.5;*/
      }
    </style>
  </head>
  <body>
  </body>
</html>
4.2.2.2. Variables

Variables are available since Firefox 29 and enabled by default starting in Firefox 31. Here are the details: developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables

4.2.2.3. Functions

A great guide on CSS functions can be found at developer.mozilla.org/en-US/docs/Web/CSS.

4.2.4. Properties

At www.w3schools.com/cssref/default.asp you can find easy to understand explanations as well as examples for pretty much every CSS property. This is an extremely useful resource that you should use.

www.w3.org/TR/css-2010/#properties and meiert.com/en/indices/css-properties provide an almost complete list of CSS properties with links to the specifications and in-depth explanations, which are useful for WMOTUs.

4.2.4.1. object-fit

Used to specify how an <img> or <video> should be resized to fit its container. See www.w3schools.com/css/css3_object-fit.asp. Also take a look at object-position.

4.2.4.2. overflow
4.2.4.3. background

Scaling an image to its maximum displayable size without distorting the proportions can be done like this:

backgroundmaxsize1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Maximum size background image scaling without distortion</title>
    <meta charset=utf-8>
    <style>
      main {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-image: url(DSC00538.JPG);
        background-repeat: no-repeat;
        background-size: contain;
      }
    </style>
  </head>
  <body>
    <main>
    </main>
  </body>
</html>
4.2.4.4. content

A great list of Unicode symbols can be found at inamidst.com/stuff/unidata.

content1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Content demo</title>
    <meta charset=UTF-8>
    <style>
      button::before {
        content: '\2709';
      }
    </style>
  </head>
  <body>
    <button> Email us</button>
  </body>
</html>
4.2.4.7. word-wrap

word-wrap allows us to solve some space constraint problems, as shown in this example:

word wrap1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Word wrap example 1</title>
    <meta charset=utf-8>
    <style>
      section {
        width: 50px;
        background-color: blueviolet;
      }

      #section2 {
        word-wrap: break-word;
      }
    </style>
  </head>
  <body>
    <main>
      <section>
        <h1>Section 1</h1>
        dassssssssssssssssssssssssssssssssssssssssss
      </section>
      <hr>
      <section id=section2>
        <h1>Section 2</h1>
        dassssssssssssssssssssssssssssssssssssssssss
      </section>
    </main>
  </body>
</html>
4.2.4.8. word-break

In a table you might have to use word-break instead of word-wrap and set it to break-word or break-all. You also must set the width or max-width of the cell for this to work.

4.2.4.11. contenteditable
The contenteditable attribute specifies whether the content of an element is editable or not.
4.2.4.12. resize

The resize (www.cssportal.com/css-properties/resize.php and developer.mozilla.org/en-US/docs/Web/CSS/resize) property allows us to make an element resizable. Note that in order for the resizing to work, you need to set the overflow property to something different from the standard visible.

4.2.5. Selectors

Study the excellent table at www.w3schools.com/cssref/css_selectors.asp to learn the power and expressiveness of CSS selectors.

Here is a simple example:

hover1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Hover demo</title>
    <meta charset=utf-8>
    <style>
      p {
        display: none;
      }

      main {
        width: 500px;
        height: 500px;
        background-color: black;
      }
      main:hover > p {
        background-color: red;
        display: inline-block;
      }
    </style>
  </head>
  <body>
    <main>
      <p>Paragraph</p>
    </main>
  </body>
</html>

If you want to change the styling if a checkbox is selected:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang=en>
<head>
    <meta charset=UTF-8>
    <title>Input checked style</title>
    <style>
        label {
            display: none;
        }

        input[type=checkbox]:checked + label {
            display: block;
        }
    </style>
</head>
<body>
    <input type=checkbox name=c1>
    <label for=c1>asdsadsa</label>
</body>
</html>

4.2.6. Box model

boxmodel1
boxmodel2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>CSS3 box model illustration</title>
    <meta charset=utf-8>
    <style>
      body {
        background-color: black;
      }

      header {
        background-color: green;
        border: 20px groove red;
        text-align: center;
      }

      h1 {
        background-color: gold;
        margin: 50px;
        padding: 30px;
        border: 10px dotted steelblue;
      }

      section {
        background-color: khaki;
        text-align: center;
        margin: 20px;
        padding: 40px;
        border: 10px double blue;
      }
    </style>
  </head>
  <body>
    <header>
      This is the header.
      <h1>This is the heading (h1).</h1>
    </header>
    <main>
      <section>
        This is a section.
      </section>
    </main>
  </body>
</html>
By default, i.e. if you have not changed box-sizing, the width and height properties in CSS3 do NOT include the padding, border and margin! Thus, if an element has content width of 200px, padding of 5px, a border width of 2px and a margin of 10px, the real width of the element will be 200 + 2 * 5 + 2 * 2 + 2 * 10 = 234px.

4.2.7. Layout

4.2.7.1. Normal flow

A browser renders our HTML code line by line in the order it appears in our HTML document. This is called normal flow. The following is a simple example, illustrating normal flow and the nesting of elements:

normalflow1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Normal flow example 1</title>
    <meta charset=utf-8>
    <style>
      section {
        width: 400px;
        height: 250px;
        border: 2px dashed green;
        margin: 5px;
        padding: 5px;
      }

      article {
        width: 300px;
        height: 150px;
        border: 1px inset blue;
        margin: 3px;
        padding: 3px;
      }

      p, aside {
        background-color: lightgoldenrodyellow;
        border: 1px groove gold;
        margin: 3px;
        padding: 3px;
      }
    </style>
  </head>
  <body>
    <main>
      <section>
        Section 1 does not contain any elements.
      </section>
      <section>
        Section 2 contains 3 nested elements:
        <article>
          This is an article
          <p>This is a paragraph nested inside an article nested inside a
            section nested inside the main element nested inside the body.</p>
        </article>
        <aside>This is an aside nested inside section 2 ...</aside>
      </section>
    </main>
  </body>
</html>

Now let’s try to build the following layout:

float
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Normal flow example 2</title>
    <meta charset=utf-8>
    <style>
      nav {
        border: 2px dotted blueviolet;
        width: 10%;
        height: 80px;
        margin: 5px;
        padding: 5px;
      }

      section {
        width: 40%;
        border: 2px solid #90ff65;
        margin: 5px;
        padding: 5px;
      }

      header {
        border: 2px ridge black;
        text-align: center;
      }

      footer {
        border: 2px outset hotpink;
        text-align: center;
      }
    </style>
  </head>
  <body>
    <header>Header</header>
    <nav>Navigation</nav>
    <main>
      <section>Section 1</section>
      <section>Section 2</section>
    </main>
    <footer>Footer</footer>
  </body>
</html>

Unfortunately, this is not exactly what we want.

4.2.7.2. Floats

To solve our layout problem from the previous subsection, we need to take the navigation and two section elements out of the normal flow. We can do this using the float and clear properties:

float1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Float example 1</title>
    <meta charset=utf-8>
    <style>
      nav {
        float:   left;
        border:  2px dotted blueviolet;
        width:   10%;
        height:  80px;
        margin:  5px;
        padding: 5px;
      }

      section {
        float:   left;
        width:   40%;
        border:  2px solid #90ff65;
        margin:  5px;
        padding: 5px;
      }

      header {
        border:     2px ridge black;
        text-align: center;
      }

      footer {
        clear:      left;
        border:     2px outset hotpink;
        text-align: center;
      }
    </style>
  </head>
  <body>
    <header>Header</header>
    <nav>Navigation</nav>
    <main>
      <section>Section 1</section>
      <section>Section 2</section>
    </main>
    <footer>Footer</footer>
  </body>
</html>

It is important to understand that float takes the floated element out of the normal flow and allows to float it on the left or right side of its container. Only block elements can be floated. To return to the normal flow, we use the clear property. We can clear only the left, only the right or both sides.

Remove the clear property from the footer rule and see what happens.

Instead of using clear we can use the overflow property on the containing element:

float2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Float example 2</title>
    <meta charset=utf-8>
    <style>
      nav {
        float:   left;
        border:  2px dotted blueviolet;
        width:   10%;
        height:  80px;
        margin:  5px;
        padding: 5px;
      }

      section {
        float:   left;
        width:   40%;
        border:  2px solid #90ff65;
        margin:  5px;
        padding: 5px;
      }

      header {
        border:     2px ridge black;
        text-align: center;
      }

      footer {
        border:     2px outset hotpink;
        text-align: center;
      }

      main {
        overflow: auto;
      }

      article {
        background-color: chartreuse;
      }
    </style>
  </head>
  <body>
    <header>Header</header>
    <nav>Navigation</nav>
    <main>
      <section>Section 1</section>
      <section>Section 2
        <header>Header</header>
        <article>Article 1</article>
        <article>Article 2</article>
        <footer>Footer</footer>
      </section>
    </main>
    <footer>Footer</footer>
  </body>
</html>

Floats can be very helpful in solving some simple layout problems, like in this vertical alignment example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Float example 3</title>
    <meta charset=utf-8>
    <style>
      #a1 {
        float: left;
      }

      span {
        display: inline-block;
      }
    </style>
  </head>
  <body>
    <main> <!-- How do we get link1 to be aligned at the top instead of at the bottom?-->
      <a>link1</a><span>a<br>b</span>
      <br>
      <a id=a1>link1</a><span>a<br>b</span>
    </main>
  </body>
</html>
4.2.7.3. Positioning

The position property allows us to take an element out of the normal flow and position it exactly as we like (cf. developer.mozilla.org/en-US/docs/Web/CSS/position).

It is important to note that an element with position absolute is positioned relative to the nearest positioned ancestor (instead of positioned relative to the viewport, like fixed). However; if an absolute positioned element has no positioned ancestors, it uses the document body, and moves along with page scrolling. Note: A "positioned" element is one whose position is anything except static.

Let’s have a look at some examples:

Positioning1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Positioning example 1</title>
    <meta charset=UTF-8>
    <style>
      body {
        margin: 0;
      }

      header {
        background-color: gold;
        text-align:       center;
      }

      nav {
        background-color: navajowhite;
      }

      section {
        background-color: lightgrey;
      }

      footer {
        background-color: aqua;
        text-align:       center;
      }

      #article1 {
        background-color: yellow;
        opacity:          0.3;
      }

      #article2 {
        background-color: indianred;
        opacity:          0.3;
      }

      #article3 {
        background-color: yellowgreen;
      }

      #article4 {
        background-color: white;
      }

      aside {
        background-color: red;
      }

      h1 {
        margin: 0;
      }

      ul {
        padding: 0;
      }
    </style>
  </head>
  <body>
    <header>
      <h1>LAM T0IF WMOTU</h1>
    </header>
    <nav>
      <ul>
        <li><a href=#>Link 1</a></li>
        <li><a href=#>Link 2</a></li>
        <li><a href=#>Link 3</a></li>
        <li><a href=#>Link 4</a></li>
      </ul>
    </nav>
    <main>
      <section>
        <article id=article1></article>
        <aside>I'm so sticky</aside>
        <article id=article2></article>
        <article id=article3></article>
        <article id=article4></article>
      </section>
    </main>
    <footer>&copy; 2018 LAM T0IF</footer>
  </body>
</html>
Positioning2
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Positioning example 2</title>
    <meta charset=UTF-8>
    <style>
      body {
        margin:   0;
        overflow: hidden;
      }

      main {
        overflow: auto;
        position: absolute;
        left:     100px;
        top:      40px;
        right:    0;
        bottom:   20px;
      }

      header {
        background-color: gold;
        text-align:       center;
        position:         fixed;
        top:              0;
        left:             0;
        width:            100%;
        height:           40px;
      }

      nav {
        background-color: navajowhite;
        position:         fixed;
        top:              40px;
        left:             0;
        width:            100px;
        bottom:           20px;
      }

      section {
        background-color: lightgrey;
      }

      footer {
        background-color: aqua;
        text-align:       center;
        position:         fixed;
        bottom:           0;
        left:             0;
        height:           20px;
        width:            100%;
      }

      #article1 {
        background-color: yellow;
        position:         relative;
        left:             50px;
        top:              100px;
      }

      #article2 {
        background-color: indianred;
        position:         absolute;
        left:             20px;
        top:              420px;
      }

      #article3 {
        background-color: yellowgreen;
        position:         fixed;
        left:             50px;
        top:              300px;
      }

      #article4 {
        background-color: white;
      }

      aside {
        position:         sticky;
        top:              0;
        background-color: red;
      }

      h1 {
        margin: 0;
      }

      ul {
        padding: 0;
      }
    </style>
  </head>
  <body>
    <header>
      <h1>LAM T0IF WMOTU</h1>
    </header>
    <nav>
      <ul>
        <li><a href=#>Link 1</a></li>
        <li><a href=#>Link 2</a></li>
        <li><a href=#>Link 3</a></li>
        <li><a href=#>Link 4</a></li>
      </ul>
    </nav>
    <main>
      <section>
        <article id=article1></article>
        <aside>I'm so sticky</aside>
        <article id=article2></article>
        <article id=article3></article>
        <article id=article4></article>
      </section>
    </main>
    <footer>&copy; 2018 LAM T0IF</footer>
  </body>
</html>

On scroll-linked effects:

firefox-source-docs.mozilla.org/performance/scroll-linked_effects.html

staktrace.com/spout/entry.php?id=834

4.2.7.5. Table layout
4.2.7.5.1. Scrollable HTML5 table with fixed header

This is a classic problem that has not had many fully satisfying solutions so far. Some interesting discussions on the topic can be found here:

stackoverflow.com/questions/673153/html-table-with-fixed-headers/25902860

stackoverflow.com/questions/673153/html-table-with-fixed-headers/25818428

stackoverflow.com/questions/17584702/how-to-add-a-scrollbar-to-an-html5-table

stackoverflow.com/questions/8423768/freeze-the-top-row-for-an-html-table-only-fixed-table-header-scrolling

www.sitepoint.com/community/t/flexible-html-table-with-fixed-header-and-footer-around-a-scrollable-body/271162/31

stackoverflow.com/questions/19559197/how-to-make-scrollable-table-with-fixed-headers-using-css

salzerdesign.com/test/fixedTable.html

www.w3docs.com/snippets/html/how-to-create-a-table-with-a-fixed-header-and-scrollable-body.html

The following table lists some solutions I’ve found and gives a brief comment:

students.btsi.lu/evegi144/WAD/CSS3/table1.html

Does not work if the window is too small and a horizontal scroll bar appears.

students.btsi.lu/evegi144/WAD/CSS3/table2.html

Same issue.

students.btsi.lu/evegi144/WAD/CSS3/table3.html

Same issue.

students.btsi.lu/evegi144/WAD/CSS3/table4.html

Needs setting column width.

students.btsi.lu/evegi144/WAD/CSS3/table5.html

Simple but does not work.

students.btsi.lu/evegi144/WAD/CSS3/table6.html

Works very well but requires a huge amount of CSS and divs.

students.btsi.lu/evegi144/WAD/CSS3/table7.html

Very simple solution that works but not very smoothly on mobiles as for the reasons explained above under scroll-linked effects. Until position:sticky works correctly in the main browsers this will be my first choice.

students.btsi.lu/evegi144/WAD/CSS3/table8.html

This is the ideal solution.

doctorDestructo’s fiddles are worth having a look.

4.2.7.5.2. CSS tables

CSS allows us to style elements as tables, which opens up a very powerful layout mechanism. See www.w3.org/TR/CSS21/visuren.html#propdef-display and www.w3.org/TR/CSS21/tables.html for the details.

Example:

TableLayout1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Table layout example 1</title>
    <meta charset=UTF-8>
    <style>
      main {
        display: table;
      }

      section {
        display: table-row;
      }

      article {
        display: table-cell;
      }
    </style>
  </head>
  <body>
    <main>
      <section>
        <article></article>
        <article></article>
      </section>
      <section>
        <article>afadsfadsfdas</article>
        <article>afadsfadsadasdsadasddsfdas</article>
      </section>
      <section>
        <article>sadsa</article>
        <article>sadsadsasadsadsa</article>
      </section>
    </main>
  </body>
</html>
4.2.7.6. Vertical centering

www.w3.org/Style/Examples/007/center

www.vanseodesign.com/css/vertical-centering

Flexbox, as described in a later section, is by far the easiest way to center any element.

Example:

VerticalCentering1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Centering example</title>
    <meta charset=UTF-8>
    <style>
      main {
        position:         absolute;
        top:              0;
        left:             0;
        height:           100%;
        width:            100%;
        background-color: lightblue;
        display:          flex;
      }

      img {
        margin: auto;
      }
    </style>
  </head>
  <body>
    <main>
      <img src=logo_ltam.gif alt="LTAM Logo">
    </main>
  </body>
</html>

Here a more generic way to center any element without styling it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Centering example</title>
    <meta charset=UTF-8>
    <style>
      main {
        position:         absolute;
        top:              0;
        left:             0;
        height:           100%;
        width:            100%;
        background-color: lightblue;
        display:          flex;
        align-items:      center;
        justify-content:  center;
        /*text-align:       center;*/
        flex-flow:        column;
      }
    </style>
  </head>
  <body>
    <main>
      <section>
        <img src=logo_ltam.gif alt="LTAM Logo">
      </section>
      <section>Section 2</section>
    </main>
  </body>
</html>
4.2.7.8. Flexible box layout

According to www.w3.org/TR/css3-flexbox, flexible box layout is

a CSS box model optimized for user interface design. In the flex layout model, the children of a flex container can be laid out in any direction, and can “flex” their sizes, either growing to fill unused space or shrinking to avoid overflowing the parent. Both horizontal and vertical alignment of the children can be easily manipulated. Nesting of these boxes (horizontal inside vertical, or vertical inside horizontal) can be used to build layouts in two dimensions.

It is a big step forward in terms of GUI development and will be our preferred approach.

To learn Flexbox in a fun way, take a look at www.flexboxdefense.com, flexboxfroggy.com and codingfantasy.com/games/flexboxadventure.

You can find in-depth information and examples at the following links:

developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout

www.w3schools.com/cssref/css3_pr_flex.asp

bocoup.com/weblog/dive-into-flexbox

www.w3.org/TR/css3-flexbox

css-tricks.com/snippets/css/a-guide-to-flexbox

html5please.com/\#flexbox

philipwalton.github.io/solved-by-flexbox

www.sketchingwithcss.com/samplechapter

www.sketchingwithcss.com/samplechapter/cheatsheet.html

www.smashingmagazine.com/2013/05/22/centering-elements-with-flexbox

designshack.net/articles/css/build-a-web-page-with-css3-flexbox

www.html5rocks.com/en/tutorials/flexbox/quick

Let’s start with a simple header - main - footer layout, where the main part contains a navigation, a section and an aside. Note that the order of the navigation and aside are changed using CSS.

Study the comments in the CSS file together with the links above to gain a deeper understanding of flex layout.

FlexTest1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Flexible Box Layout Test 1</title>
    <meta charset=utf-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <style>
      /* This will make sure that our GUI fills the whole browser window. */
      html, body {
        width:  100%;
        height: 100%;
        margin: 0;
      }

      body {
        background: #999999;
        display:    flex; /* This element is a flex container. */
        flex-flow:  column; /* Change the main axis to column instead of row. */
        overflow:   hidden; /* We don't want scrollbars. */
      }

      main {
        margin:   0;
        padding:  0;
        display:  flex; /* This is another flex container. */
        flex:     auto;
        overflow: hidden; /* We don't want scrollbars. */
      }

      main > section {
        margin:        4px;
        padding:       5px;
        border:        1px solid #cccc33;
        border-radius: 7pt;
        background:    #dddd88;
        flex:          3 1 60%; /* It will grow 3 times faster and shrink at the same speed as the others. */
        order:         2; /* This element will be displayed in second position. */
        overflow:      auto; /* We want scrollbars when required by the content. */
      }

      main > nav {
        margin:        4px;
        padding:       5px;
        border:        1px solid #8888bb;
        border-radius: 7pt;
        background:    #ccccff;
        flex:          1 6 20%;
        order:         3; /* This element will be displayed in third position. */
      }

      main > aside {
        margin:        4px;
        padding:       5px;
        border:        1px solid #8888bb;
        border-radius: 7pt;
        background:    #ccccff;
        flex:          1 6 20%;
        order:         1; /* This element will be displayed in first position. */
      }

      header, footer {
        margin:        4px;
        padding:       5px;
        border:        1px solid #eebb55;
        border-radius: 7pt;
        background:    #ffeebb;
        height:        50px;
      }

      /* Too narrow to support three columns */
      @media all and (max-width: 640px) {
        main {
          flex-flow: column;
        }

        main > section, main > nav, main > aside {
          /* Return them to document order */
          order: 0;
        }

        main > nav, main > aside, header, footer {
          min-height: 50px;
          max-height: 50px;
        }
      }

    </style>
  </head>
  <body>
    <header>header</header>
    <main>
      <nav>nav</nav>
      <section>section</section>
      <aside>aside</aside>
    </main>
    <footer>footer</footer>
  </body>
</html>

Now we let the user resize some of the elements:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Flexible Box Layout Test 2</title>
    <meta charset=utf-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <style>
      body {
        position:   absolute;
        top:        0;
        left:       0;
        width:      100%;
        height:     100%;
        background: #999999;
        display:    flex;
        flex-flow:  column;
        margin:     0;
        overflow:   hidden;
      }

      main {
        min-height: 50px;
        margin:     0;
        padding:    0;
        display:    flex;
        flex:       auto;
      }

      main > article {
        margin:        4px;
        padding:       5px;
        border:        1px solid #cccc33;
        border-radius: 7pt;
        background:    #dddd88;
        flex:          auto;
        min-width:     50px;
      }

      main > nav {
        margin:        4px;
        padding:       5px;
        border:        1px solid #8888bb;
        border-radius: 7pt;
        background:    #ccccff;
        min-width:     150px;
      }

      main > aside {
        margin:        4px;
        padding:       5px;
        border:        1px solid #8888bb;
        border-radius: 7pt;
        background:    #ccccff;
        min-width:     50px;
        flex:          auto;
      }

      header, footer {
        display:       block;
        margin:        4px;
        padding:       5px;
        min-height:    100px;
        border:        1px solid #eebb55;
        border-radius: 7pt;
        background:    #ffeebb;
      }

      .splitter {
        border-left: 2px solid grey;
        width:       2px;
        min-width:   2px;
        cursor:      col-resize;
      }

      /* Too narrow to support three columns */
      @media all and (max-width: 640px) {
        main {
          flex-flow:      column;
        }

        main > nav, main > aside, header, footer {
          min-height: 50px;
          max-height: 50px;
        }
      }

    </style>
    <script type=module src=flextest2.js></script>
  </head>
  <body>
    <header>header</header>
    <main>
      <nav>nav</nav>
      <article>article</article>
      <div class="splitter"></div>
      <aside>aside</aside>
    </main>
    <footer>footer</footer>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
let lastFirstElWidth = 0, lastThirdElWidth = 0

// https://hacks.mozilla.org/2013/12/application-layout-with-css3-flexible-box-module
class Splitter {
    constructor(handler, leftEl, rightEl) {
        this.lastX = 0
        this.dragListener = null
        this.endDragListener = null
        this.leftEl = leftEl
        this.rightEl = rightEl

        handler.addEventListener('mousedown', evt => {
            evt.preventDefault()
            this.lastX = evt.clientX
            // http://msdn.microsoft.com/en-us/library/windows/apps/hh703713.aspx
            this.dragListener = this.drag
            this.endDragListener = this.endDrag
            addEventListener('mousemove', this.dragListener)
            addEventListener('mouseup', this.endDragListener)
        })

        this.drag = evt => {
            let wL, wR
            const wDiff = evt.clientX - this.lastX
            wL = getComputedStyle(this.leftEl).width
            wR = getComputedStyle(this.rightEl).width
            wL = parseInt(wL) + wDiff
            wR = parseInt(wR) - wDiff
            this.leftEl.style.width = wL + 'px'
            this.rightEl.style.width = wR + 'px'
            this.lastX = evt.clientX
            lastFirstElWidth = wL
            lastThirdElWidth = wR
        }

        this.endDrag = () => {
            removeEventListener('mousemove', this.dragListener)
            removeEventListener('mouseup', this.endDragListener)
        }
    };
}

const init = () => {
    const splitter = new Splitter(document.getElementsByClassName('splitter')[0],
        document.getElementsByTagName('article')[0],
        document.getElementsByTagName('aside')[0])

    /* Our CSS switches flex flow to column if window width <= 640.
     In this case we need to remove the width set by the splitter dragging,
     otherwise the layout won't be right aligned on small screens.
     If window width increases again, we want the previous widths back.
     */
    const handleResize = () => {
        if (innerWidth <= 640) {
            document.querySelector('article').style.width = ''
            document.querySelector('aside').style.width = ''
        } else {
            document.querySelector('article').style.width = lastFirstElWidth + 'px'
            document.querySelector('aside').style.width = lastThirdElWidth + 'px'
        }
    }

    window.addEventListener('resize', handleResize)
}

init()

Some more resizability:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Flexible Box Layout Test 3</title>
    <meta charset=utf-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <style>
      body {
        position:   absolute;
        top:        0;
        left:       0;
        width:      100%;
        height:     100%;
        background: #999999;
        display:    flex;
        flex-flow:  column;
        margin:     0;
        overflow:   hidden;
      }

      main {
        min-height: 50px;
        margin:     0;
        padding:    0;
        display:    flex;
        flex:       auto;
      }

      main > article {
        margin:        4px;
        padding:       5px;
        border:        1px solid #cccc33;
        border-radius: 7pt;
        background:    #dddd88;
        flex:          auto;
        min-width:     50px;
      }

      main > nav {
        margin:        4px;
        padding:       5px;
        border:        1px solid #8888bb;
        border-radius: 7pt;
        background:    #ccccff;
        min-width:     50px;
      }

      main > aside {
        margin:        4px;
        padding:       5px;
        border:        1px solid #8888bb;
        border-radius: 7pt;
        background:    #ccccff;
        flex:          auto;
        min-width:     50px;
      }

      header, footer {
        margin:        4px;
        padding:       5px;
        min-height:    50px;
        border:        1px solid #eebb55;
        border-radius: 7pt;
        background:    #ffeebb;
      }

      .verticalSplitter {
        border-left: 2px solid grey;
        width:       2px;
        min-width:   2px;
        cursor:      col-resize;
      }

      .horizontalSplitter {
        border-top: 2px solid grey;
        height:     2px;
        min-height: 2px;
        cursor:     row-resize;
      }

      /* Too narrow to support three columns */
      @media all and (max-width: 640px) {
        main {
          flex-flow: column;
        }

        main > nav, header, footer {
          height: 50px;
        }

        .horizontalSplitter, .verticalSplitter {
          display: none;
        }
      }

    </style>
    <script type=module src=flextest3.js></script>
  </head>
  <body>
    <header>header</header>
    <div id=hs1 class=horizontalSplitter></div>
    <main>
      <nav>nav</nav>
      <article>article</article>
      <div class=verticalSplitter></div>
      <aside>aside</aside>
    </main>
    <div id=hs2 class=horizontalSplitter></div>
    <footer>footer</footer>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
let lastFirstElWidth = 0, lastThirdElWidth = 0

// https://hacks.mozilla.org/2013/12/application-layout-with-css3-flexible-box-module
class Splitter {
  constructor(vertical) {
    this.vertical = vertical
    this.lastCoord = 0

    this.init = (splitter, firstEl, thirdEl) => {
      this.firstEl = firstEl
      this.thirdEl = thirdEl
      splitter.addEventListener('mousedown', evt => {
        evt.preventDefault()
        if (vertical) this.lastCoord = evt.clientX
        else this.lastCoord = evt.clientY
        this.dragListener = this.drag
        this.endDragListener = this.endDrag
        addEventListener('mousemove', this.dragListener)
        addEventListener('mouseup', this.endDragListener)
      })
    }

    this.drag = evt => {
      let coord1, coord3, coordDiff
      if (vertical) {
        coordDiff = evt.clientX - this.lastCoord
        coord1 = getComputedStyle(this.firstEl).width
        coord3 = getComputedStyle(this.thirdEl).width
      } else {
        coordDiff = evt.clientY - this.lastCoord
        coord1 = getComputedStyle(this.firstEl).height
        coord3 = getComputedStyle(this.thirdEl).height
      }
      coord1 = parseInt(coord1) + coordDiff
      coord3 = parseInt(coord3) - coordDiff
      if (vertical) {
        this.firstEl.style.width = coord1 + 'px'
        this.thirdEl.style.width = coord3 + 'px'
        this.lastCoord = evt.clientX
        lastFirstElWidth = coord1
        lastThirdElWidth = coord3
        console.log(lastFirstElWidth + ' ' + lastThirdElWidth)
      } else {
        this.firstEl.style.height = coord1 + 'px'
        this.thirdEl.style.height = coord3 + 'px'
        this.lastCoord = evt.clientY
      }
    }

    this.endDrag = () => {
      removeEventListener('mousemove', this.dragListener)
      removeEventListener('mouseup', this.endDragListener)
    }
  }
}

const init = () => {
  const verticalSplitter = new Splitter(true)
  verticalSplitter.init(document.getElementsByClassName('verticalSplitter')[0],
    document.querySelector('article'), document.querySelector('aside'))
  const horizontalSplitter1 = new Splitter(false)
  horizontalSplitter1.init(document.getElementById('hs1'),
    document.querySelector('header'), document.querySelector('main'))
  const horizontalSplitter2 = new Splitter(false)
  horizontalSplitter2.init(document.getElementById('hs2'),
    document.querySelector('main'), document.querySelector('footer'))

  /* Our CSS switches flex flow to column if window width <= 640.
     In this case we need to remove the width set by the splitter dragging,
      otherwise the layout won't be right aligned on small screens.
     If window width increases again, we want the previous widths back.
   */
  const handleResize = () => {
    if (innerWidth <= 640) {
      document.querySelector('article').style.width = ''
      document.querySelector('aside').style.width = ''
    } else {
      document.querySelector('article').style.width = lastFirstElWidth + 'px'
      document.querySelector('aside').style.width = lastThirdElWidth + 'px'
    }
  }

  window.addEventListener('resize', handleResize)
}

init()

The following example illustrates how to achieve specific scrollbar behavior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html>
  <head>
    <title>Flexible Box Layout Test 4</title>
    <meta charset=utf-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <style type="text/css">
      html, body {
        height:  100%;
        width:   100%;
        padding: 0;
        margin:  0;
      }

      body {
        overflow:       hidden;
        display:        flex;
        flex-direction: column;
      }

      header {
        height:     75px;
        min-height: 75px;
      }

      footer {
        height:     25px;
        min-height: 25px;
      }

      main {
        display:      flex;
        flex:         auto;
        border:       solid grey;
        border-width: 1px 0;
        overflow:     hidden;
      }

      nav {
        width:     150px;
        min-width: 150px;
      }

      section {
        border:       solid grey;
        border-width: 0 0 0 1px;
        flex:         auto;
        overflow:     auto;
      }
    </style>
  </head>
  <body>
    <header>header</header>
    <main>
      <nav>nav</nav>
      <section>article<br>read <a
        href="https://hacks.mozilla.org/2013/12/application-layout-with-css3-flexible-box-module/"
        target="_blank">Application Layout with CSS3 Flexible Box Module</a>
        <p style="width:1000px;">
          <?php require_once 'flextest4.txt'; ?>
        </p>
      </section>
    </main>
    <footer>footer</footer>
  </body>
</html>

Finally a more advanced example combining flexbox, splitters and scrollbars:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Flexible Box Layout Test 5</title>
    <meta charset=utf-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <style>
      html, body {
        width:  100%;
        height: 100%;
      }

      body {
        background: #999999;
        display:    flex;
        flex-direction:  column;
        margin:     0;
        overflow:   hidden;
      }

      main {
        margin:  0;
        padding: 0;
        display: flex;
        flex:    auto;
        overflow: auto;
      }

      main > nav {
        margin:        4px;
        padding:       5px;
        border:        1px solid #8888bb;
        border-radius: 7pt;
        background:    #ccccff;
        min-width:     50px;
        flex:          auto;
      }

      main > section {
        margin:         4px;
        padding:        5px;
        border:         1px solid #8888bb;
        border-radius:  7pt;
        background:     #ccccff;
        display:        flex;
        flex-direction: column;
        min-width:      50px;
        overflow:       hidden;
      }

      main > section > section {
        display:   flex;
        flex-flow: column;
        flex:      auto;
        overflow:  hidden;
      }

      header, footer {
        margin:        4px;
        padding:       5px;
        min-height:    50px;
        border:        1px solid #eebb55;
        border-radius: 7pt;
        background:    #ffeebb;
      }

      #s1 {
        flex:     auto;
        flex-flow: column;
        overflow: hidden;
      }

      #s1 > article, #s2 {
        overflow: auto;
      }

      .verticalSplitter {
        border-left: 2px solid grey;
        width:       2px;
        min-width:   2px;
        cursor:      col-resize;
      }

      .horizontalSplitter {
        border-top: 2px solid grey;
        height:     2px;
        min-height: 2px;
        cursor:     row-resize;
      }

      /* Too narrow to support three columns */
      @media all and (max-width: 640px) {
        main {
          flex-flow: column;
        }

        .horizontalSplitter, .verticalSplitter {
          display: none;
        }
      }
    </style>
    <script type=module src=flextest5.js></script>
  </head>
  <body>
    <header>header</header>
    <main>
      <nav>nav</nav>
      <div class=verticalSplitter></div>
      <section>
        <section id=s1>
          <header>Header s1</header>
          <article><?php require 'flextest4.txt'; ?></article>
        </section>
        <div id=hs1 class=horizontalSplitter></div>
        <section id=s2><?php require 'flextest4.txt'; ?></section>
      </section>
    </main>
    <footer>footer</footer>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
let verticalSplitter, horizontalSplitter

// https://hacks.mozilla.org/2013/12/application-layout-with-css3-flexible-box-module
class Splitter {
  constructor(vertical) {
    this.vertical = vertical
    this.lastCoord = 0
    if (vertical) {
      this.lastFirstElWidth = 0
      this.lastThirdElWidth = 0
    }

    this.init = (splitter, firstEl, thirdEl) => {
      this.firstEl = firstEl
      this.thirdEl = thirdEl
      splitter.addEventListener('mousedown', evt => {
        evt.preventDefault()
        if (vertical) this.lastCoord = evt.clientX
        else this.lastCoord = evt.clientY
        this.dragListener = this.drag
        this.endDragListener = this.endDrag
        addEventListener('mousemove', this.dragListener)
        addEventListener('mouseup', this.endDragListener)
      })
    }

    this.drag = evt => {
      let coord1, coord3, coordDiff
      if (vertical) {
        coordDiff = evt.clientX - this.lastCoord
        coord1 = getComputedStyle(this.firstEl).width
        coord3 = getComputedStyle(this.thirdEl).width
      } else {
        coordDiff = evt.clientY - this.lastCoord
        coord1 = getComputedStyle(this.firstEl).height
        coord3 = getComputedStyle(this.thirdEl).height
      }
      coord1 = parseInt(coord1) + coordDiff
      coord3 = parseInt(coord3) - coordDiff
      if (vertical) {
        this.firstEl.style.width = coord1 + 'px'
        this.thirdEl.style.width = coord3 + 'px'
        this.lastCoord = evt.clientX
        this.lastFirstElWidth = coord1
        this.lastThirdElWidth = coord3
      } else {
        this.firstEl.style.height = coord1 + 'px'
        this.thirdEl.style.height = coord3 + 'px'
        this.lastCoord = evt.clientY
      }
    }

    this.endDrag = () => {
      removeEventListener('mousemove', this.dragListener)
      removeEventListener('mouseup', this.endDragListener)
    }
  }
}

const init = () => {
  const verticalSplitter = new Splitter(true)
  const horizontalSplitter = new Splitter(false)
  verticalSplitter.init(document.getElementsByClassName('verticalSplitter')[0],
    document.querySelector('nav'), document.querySelector('section'))
  horizontalSplitter.init(document.getElementById('hs1'),
    document.getElementById('s1'), document.getElementById('s2'))

  /* Our CSS switches flex flow to column if window width <= 640.
     In this case we need to remove the width set by the splitter dragging,
      otherwise the layout won't be right aligned on small screens.
     If window width increases again, we want the previous widths back.
   */
  const handleResize = () => {
    if (innerWidth <= 640) {
      document.querySelector('article').style.width = ''
      document.querySelector('aside').style.width = ''
    } else {
      document.querySelector('article').style.width = lastFirstElWidth + 'px'
      document.querySelector('aside').style.width = lastThirdElWidth + 'px'
    }
  }

  window.addEventListener('resize', handleResize)
}

init()

A flexible navigation menu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <title>Flexible Box Layout Test 6</title>
    <style>
      html { /* http://www.paulirish.com/2012/box-sizing-border-box-ftw */
        box-sizing: border-box;
      }
      *, *:before, *:after {
        box-sizing: inherit;
      }

      html, body {
        width:  100%;
        height: 100%;
        margin: 0;
      }

      body {
        display:   flex;
        flex-flow: column;
        overflow:  hidden;
      }

      main {
        margin:   0;
        overflow: auto;
      }

      nav {
        padding: 0;
      }

      ul {
        list-style:       none;
        padding:          0;
        display:          flex;
        flex:             auto;
        background-color: pink;
        margin:           0;
      }

      li {
        flex: auto;
      }

      nav > ul > li > a {
        text-decoration:  none;
        margin:           0;
        padding:          0;
        display:          inline-block;
        width:            100%;
        text-align:       center;
        background-color: orange;
        border:           solid black 1px;
      }

      nav > ul > li > a:hover {
        background-color: yellow;
      }

      main > section > a {
        text-decoration: none;
        background-color: tomato;
        border-radius: 5px;
        padding: 5px;
      }
    </style>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href=#about>About</a></li>
        <li><a href=#portfolio>Portfolio</a></li>
        <li><a href=#contact>Contact</a></li>
      </ul>
    </nav>
    <main>
      <section id=about>
        <h1>About</h1>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit.
      </section>
      <section id=portfolio>
        <h1>Portfolio</h1>
        <?php require_once 'flextest4.txt'; ?>
      </section>
      <section id=contact>
        <h1>Contact</h1>
        <form>
          Name: <input required>
          Email: <input required><button>Send</button>
        </form>
      </section>
    </main>
  </body>
</html>

Another layout variation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <title>Flexible Box Layout Test 7</title>
    <style>
      nav {
        display:          flex;
        background-color: #0a73a7;
      }

      ul {
        display:          flex;
        flex:             auto;
        list-style:       none;
        background-color: lightgrey;
      }

      li {
        text-align: center;
        display:    block;
      }

      #l1 {
        justify-content:  center;
        background-color: lightgreen;
      }

      #l2 {
        max-width:        130px;
        background-color: lightblue;
      }
    </style>
  </head>
  <body>
    <nav>
      <ul id=l1>
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
      </ul>
      <ul id=l2>
        <li>Item 4</li>
        <li>Item 5</li>
        <li>Item 6</li>
      </ul>
    </nav>
  </body>
</html>
4.2.7.9. Grid layout

Grid layout makes it easy to build the big picture aspects of our layout, i.e. the row AND column layout. We can then use Flexbox to manage the horizontal OR vertical alignment of the content inside the grid elements.

css-tricks.com/snippets/css/complete-guide-grid

caniuse.com/\#search=Grid

www.mozilla.org/en-US/developer/css-grid

developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout

gridbyexample.com

bitsofco.de/how-the-minmax-function-works

tutorialzine.com/2017/03/css-grid-vs-flexbox

Here’s a very simple example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <title>CSS Grid example</title>
    <style>
      html, body {
        width:  100%;
        height: 100%;
        margin: 0;
      }

      main {
        height:                100%;
        display:               grid;
        grid-template-columns: 3fr 1fr 2fr;
        grid-template-rows:    1fr 3fr;
        /*align-items:           stretch;*/
      }

      section {
        background-color: lawngreen;
        border:           darkmagenta solid 2px;
      }
    </style>
  </head>
  <body>
    <main>
      <section>One</section>
      <section>Two</section>
      <section>Three</section>
      <section>Four</section>
      <section>Five</section>
      <section>Six</section>
    </main>
  </body>
</html>

When should we use Grid and when Flexbox? This article dives into the question.

To support browsers without grid see:

www.smashingmagazine.com/2017/11/css-grid-supporting-browsers-without-grid

developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/CSS_Grid_and_Progressive_Enhancement

rachelandrew.co.uk/archives/2016/11/26/should-i-try-to-use-the-ie-implementation-of-css-grid-layout

4.2.7.10. Responsive design

Responsive web design aims at building websites that work on mobile devices, tablets, and desktop screens.

developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries

drafts.csswg.org/mediaqueries

css-tricks.com/dont-overthink-flexbox-grids

www.w3schools.com/cssref/css3_pr_mediaquery.asp

developer.mozilla.org/en-US/docs/Web_Development/Responsive_Web_design

blog.teamtreehouse.com/modern-field-guide-responsive-web-design

screensiz.es/phone

developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag

developer.mozilla.org/en-US/docs/Web/HTML/Element/meta

webdesignerwall.com/tutorials/viewport-meta-tag-for-non-responsive-design

www.smashingmagazine.com/2011/07/22/responsive-web-design-techniques-tools-and-design-strategies

www.quirksmode.org/mobile/metaviewport

googlewebmastercentral.blogspot.co.uk/2012/04/responsive-design-harnessing-power-of.html

css-tricks.com/accessible-simple-responsive-tables

You can even use media queries to have your web site automatically adjust to the user’s preferred color scheme, for instance dark mode: developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme

In Firefox, pressing Ctrl+Shift+M or Tools  Web Developer  Responsive Design View

responsivedesignview1

Let’s look at a practical example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Responsive layout example 1</title>
    <style>
      html { /* http://www.paulirish.com/2012/box-sizing-border-box-ftw */
        box-sizing: border-box;
      }

      *, *:before, *:after {
        box-sizing: inherit;
      }

      ul {
        display:    flex;
        flex-wrap:  wrap;
        padding:    0;
        list-style: none;
      }

      ul li {
        flex:       auto;
        text-align: center;
        padding:    2px;
      }

      @media (min-width: 10em) {
        ul li {
          flex-basis: 33%;
        }
      }

      @media (min-width: 28em) {
        ul li {
          flex-basis: 0;
        }
      }

      ul li a {
        display:         block;
        text-decoration: none;
      }
    </style>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href=#>Item 1</a></li>
        <li><a href=#>Item 2</a></li>
        <li><a href=#>Item 3</a></li>
        <li><a href=#>Item 4</a></li>
        <li><a href=#>Item 5</a></li>
        <li><a href=#>Item 6</a></li>
      </ul>
    </nav>
    <main>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et tempus urna. In sed
      sagittis arcu. Cras in sapien diam. Aenean massa ipsum, rutrum eu facilisis vitae,
      semper in arcu. Morbi vitae tortor sit amet turpis feugiat aliquam. Aliquam eu
      rhoncus odio, quis rutrum magna.
    </main>
  </body>
</html>
4.2.7.10.1. Responsive menus
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Responsive layout example 2</title>
    <meta charset=utf-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <style>
      html { /* http://www.paulirish.com/2012/box-sizing-border-box-ftw */
        box-sizing: border-box;
      }

      *, *:before, *:after {
        box-sizing: inherit;
      }

      nav {
        margin:           4px;
        padding:          4px;
        border:           1px solid #8888bb;
        border-radius:    7pt;
        background-color: #ccccff;
        min-width:        50px;
        flex:             auto;
      }

      ul {
        display:    flex;
        flex-wrap:  wrap;
        margin:     0;
        padding:    0;
        list-style: none;
      }

      ul li {
        flex:             auto;
        text-align:       center;
        padding:          2px;
        background-color: #ffeebb;
      }

      @media (min-width: 10em) {
        ul li {
          flex-basis: 33%;
        }
      }

      @media (min-width: 28em) {
        ul li {
          flex-basis: 0;
        }
      }

      ul li a {
        display:         block;
        text-decoration: none;
      }

      html, body {
        width:  100%;
        height: 100%;
      }

      body {
        background-color: #999999;
        display:          flex;
        flex-direction:   column;
        margin:           0;
        overflow:         hidden;
      }

      main {
        margin:   0;
        padding:  0;
        display:  flex;
        flex:     auto;
        overflow: auto;
      }

      main > nav {
        margin:           4px;
        padding:          5px;
        border:           1px solid #8888bb;
        border-radius:    7pt;
        background-color: #ccccff;
        min-width:        50px;
        flex:             auto;
      }

      main > section {
        margin:           4px;
        padding:          5px;
        border:           1px solid #8888bb;
        border-radius:    7pt;
        background-color: #ccccff;
        display:          flex;
        flex-direction:   column;
        min-width:        50px;
        overflow:         hidden;
      }

      main > section > section {
        display:   flex;
        flex-flow: column;
        flex:      auto;
        overflow:  hidden;
      }

      header, footer {
        margin:           4px;
        padding:          5px;
        min-height:       50px;
        border:           1px solid #eebb55;
        border-radius:    7pt;
        background-color: #ffeebb;
      }

      #s1 {
        flex:      auto;
        flex-flow: column;
        overflow:  hidden;
      }

      #s1 > article, #s2 {
        overflow: auto;
      }

      .verticalSplitter {
        border-left: 1px solid grey;
        width:       1px;
        min-width:   1px;
        cursor:      col-resize;
      }

      .horizontalSplitter {
        border-top: 1px solid grey;
        height:     1px;
        min-height: 1px;
        cursor:     row-resize;
      }

      /* Too narrow to support three columns */
      @media all and (max-width: 640px) {
        main {
          flex-flow: column;
        }

        main > nav, header, footer {
          height: 50px;
        }

        .horizontalSplitter, .verticalSplitter {
          display: none;
        }
      }
    </style>
    <script type=module src=flextest5.js></script>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href=#>Item 1</a></li>
        <li><a href=#>Item 2</a></li>
        <li><a href=#>Item 3</a></li>
        <li><a href=#>Item 4</a></li>
        <li><a href=#>Item 5</a></li>
        <li><a href=#>Item 6</a></li>
      </ul>
    </nav>
    <main>
      <nav>nav</nav>
      <div class=verticalSplitter></div>
      <section>
        <section id=s1>
          <header>Header s1</header>
          <article><?php require 'flextest4.txt'; ?></article>
        </section>
        <div id=hs1 class=horizontalSplitter></div>
        <section id=s2><?php require 'flextest4.txt'; ?></section>
      </section>
    </main>
    <footer>footer</footer>
  </body>
</html>

Here is a hamburger icon menu example:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <meta name=viewport content="width=device-width, initial-scale=1.0">
    <title>Responsive layout example 3</title>
    <style>
      /* Inspiration from http://codepen.io/ricardozea/pen/OPaRZO */
      html { /* http://www.paulirish.com/2012/box-sizing-border-box-ftw */
        box-sizing: border-box;
      }

      *, *:before, *:after {
        box-sizing: inherit;
      }

      #menu-button {
        padding:         0 0.2em 0.2em 0.2em;
        background:      #f6f6f6;
        text-decoration: none;
        color:           #333;
        cursor:          pointer;
        font-size:       3em;
      }

      #menu-button.active {
        background-color: #333;
        color:            #fff;
      }

      #menu {
        overflow:   hidden;
        max-height: 0;
        padding:    0;
        clear:      both;
        transition: all .3s ease-out;
      }

      #menu.active {
        max-height: 17em;
      }

      #menu ul {
        margin:          0;
        padding:         0;
        list-style-type: none;
        border:          1px #999 dotted;
        border-bottom:   none;
        text-align:      center;
      }

      #menu li a {
        display:          block;
        padding:          1em;
        border-bottom:    1px #999 dotted;
        text-decoration:  none;
        color:            #2963BD;
        background-color: #fff;
      }

      @media (min-width: 40em) {
        #menu-button {
          display: none;
        }

        #menu {
          max-height: inherit;
        }

        #menu ul {
          background: #fff;
        }

        #menu li {
          display: inline-block;
          margin:  0 .2em;
        }
      }
    </style>
    <script>
      'use strict';

      const init = () => {
        document.querySelector('button').addEventListener('click', () => {
          document.querySelector('button').classList.toggle('active');
          document.querySelector('#menu').classList.toggle('active');
        });
      };

      window.addEventListener('load', init);
    </script>
  </head>
  <body>
    <button id=menu-button></button>
    <nav id=menu>
      <ul>
        <li><a href=#>Item 1</a></li>
        <li><a href=#>Item 2</a></li>
        <li><a href=#>Item 3</a></li>
        <li><a href=#>Item 4</a></li>
        <li><a href=#>Item 5</a></li>
      </ul>
    </nav>
    <main>
      <section><?php require 'flextest4.txt'; ?></section>
    </main>
  </body>
</html>
4.2.7.12. Navigation menus
navigationmenu1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Navigation Menu</title>
    <meta charset=UTF-8>
    <style>
      nav {
        margin:      0;
        white-space: nowrap;
      }

      nav ul {
        padding:         0;
        margin:          0;
        list-style-type: none;
      }

      nav ul li {
        display: inline-block;
        cursor:  pointer;
      }

      nav ul li ul {
        position: absolute;
        display:  none;
      }

      nav > ul > li ul li, nav > ul > li:hover > ul {
        display: block;
      }

      nav > ul > li ul li:hover > ul {
        display: inline-block;
      }
    </style>
  </head>
  <body>
    <nav>
      <ul>
        <li>
          Menu1
          <ul>
            <li>
              Submenu1
              <ul>
                <li>Subsubmenu1</li>
              </ul>
            </li>
          </ul>
        </li>
        <li>
          Menu2
          <ul>
            <li>
              Submenu1
              <ul>
                <li>Subsubmenu1</li>
                <li>
                  Subsubmenu2
                  <ul>
                    <li>Subsubsubmenu1</li>
                    <li>
                      Subsubsubmenu2
                      <ul>
                        <li>Subsubsubsubmenu1</li>
                      </ul>
                    </li>
                    <li>Subsubsubmenu3</li>
                  </ul>
                </li>
              </ul>
            </li>
          </ul>
        </li>
      </ul>
    </nav>
  </body>
</html>
navigationmenu2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Navigation Menu</title>
    <meta charset=UTF-8>
    <style>
      nav a {
        text-decoration: none;
        cursor:          pointer;
        border:          2px solid blue;
      }

      nav ul {
        padding:    0;
        margin:     0;
        list-style: none;
      }

      nav > ul > li {
        float:       left;
        clear:       left;
        white-space: nowrap;
      }

      nav ul li ul {
        position:   absolute;
        display:    inline-block;
        visibility: hidden;
      }

      nav li:hover > ul {
        visibility: visible;
      }
    </style>
  </head>
  <body>
    <nav>
      <ul>
        <li>
          <a>Menu 1</a>
          <ul>
            <li>
              <a>Menu 1 Sub 1</a>
              <ul>
                <li><a>Menu 1 Sub Sub 1</a></li>
              </ul>
            </li>
          </ul>
        </li>
        <li>
          <a>Menu 2</a>
          <ul>
            <li>
              <a>Menu 2 Sub 1</a>
              <ul>
                <li><a>Menu 2 Sub Sub 1</a></li>
                <li>
                  <a>Menu 2 Sub Sub 2</a>
                  <ul>
                    <li><a>Menu 2 Sub Sub Sub 1</a></li>
                    <li>
                      <a>Menu 2 Sub Sub Sub 2</a>
                      <ul>
                        <li><a>Menu 2 Sub Sub Sub Sub 1</a></li>
                      </ul>
                    </li>
                    <li><a>Menu 2 Sub Sub Sub 3</a></li>
                  </ul>
                </li>
              </ul>
            </li>
          </ul>
        </li>
        <li>
          <a>Menu 3</a>
        </li>
        <li>
          <a>Menu 4</a>
        </li>
      </ul>
    </nav>
  </body>
</html>
navigationmenu3
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Navigation Menu</title>
    <meta charset=UTF-8>
    <style>
      body {
        background: linear-gradient(red, white, blue) fixed;
      }

      nav {
        position:   absolute;
        top:        0;
        left:       0;
        transition: 50s;
      }

      nav:hover {
        left: 500px;
        top:  500px;
      }

      nav a {
        text-decoration:       none;
        cursor:                pointer;
        padding:               5px;
        background:            radial-gradient(rgb(0, 255, 0), rgb(255, 0, 255));
        display:               inline-block;
        border:                2px outset black;
        /* stackoverflow.com/questions/826782/css-rule-to-disable-text-selection-
        highlighting */
        -webkit-touch-callout: none;
        -webkit-user-select:   none;
        -khtml-user-select:    none;
        -moz-user-select:      none;
        -ms-user-select:       none;
        /*user-select:           none;*/
      }

      nav a:hover {
        background-color: green;
      }

      nav ul {
        padding:    0;
        margin:     0;
        list-style: none;
      }

      nav > ul ul li {
        padding-left: 10px;
      }

      nav > ul ul li:before {
        content:       '';
        position:      absolute;
        left:          1px;
        top:           9px;
        border-bottom: 8px solid transparent;
        border-top:    8px solid transparent;
        border-left:   8px solid lightgreen;
      }

      nav > ul > li {
        float:       left;
        clear:       left;
        white-space: nowrap;
      }

      nav ul li ul {
        position: absolute;
        display:  none;
      }

      nav > ul li:hover > ul {
        display: inline-block;
      }
    </style>
  </head>
  <body>
    <nav>
      <ul>
        <li>
          <a>Menu 1</a>
          <ul>
            <li>
              <a>Menu 1 Sub 1</a>
              <ul>
                <li><a>Menu 1 Sub Sub 1</a></li>
              </ul>
            </li>
          </ul>
        </li>
        <li>
          <a>Menu 2</a>
          <ul>
            <li>
              <a>Menu 2 Sub 1</a>
              <ul>
                <li><a>Menu 2 Sub Sub 1</a></li>
                <li>
                  <a>Menu 2 Sub Sub 2</a>
                  <ul>
                    <li><a>Menu 2 Sub Sub Sub 1</a></li>
                    <li>
                      <a>Menu 2 Sub Sub Sub 2</a>
                      <ul>
                        <li><a>Menu 2 Sub Sub Sub Sub 1</a></li>
                      </ul>
                    </li>
                    <li><a>Menu 2 Sub Sub Sub 3</a></li>
                  </ul>
                </li>
              </ul>
            </li>
          </ul>
        </li>
        <li>
          <a>Menu 3</a>
        </li>
        <li>
          <a>Menu 4</a>
        </li>
      </ul>
    </nav>
  </body>
</html>

Useful tutorials and templates can be found at www.html5xcss3.com/responsive-menus-tutorials.

4.2.8. Make it look good

4.2.8.1. Gradients

Gradients are a great alternative to standard images. www.w3schools.com/css/css3_gradients.asp and developer.mozilla.org/en-US/docs/Web/Guide/CSS/Using_CSS_gradients provide excellent information and examples on the subject. The official specification can be found at dev.w3.org/csswg/css-images-3.

Example:

gradient1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Gradient example 1</title>
    <meta charset=UTF-8>
    <style>
      body {
        background: repeating-radial-gradient(red, blue 20px, red 40px) fixed;
      }
      
      section {
        position: absolute;
        width: 500px;
        height: 500px;
        background: linear-gradient(90deg, black, transparent, white);
        border-radius: 100px;
      }

      article {
        width: 30px;
        height: 30px;
        background: repeating-linear-gradient(-45deg, red, red 5px, white 5px, white 10px);
        border-radius: 15px;
      }
    </style>
  </head>
  <body>
    <main>
      <section>
        <article>
          
        </article>
      </section>
    </main>
  </body>
</html>

An excellent gradient generator can be found at www.colorzilla.com/gradient-editor.

4.2.8.2. Buttons

With CSS3 it’s very easy to create great looking buttons without using pictures. There are many button generators available on the Web that produce nice results, for instance:

www.cssbuttongenerator.com

dabuttonfactory.com

css3button.net

4.2.8.3. Transformation and animation

For examples of what can be done, take a look at the following:

www.marcofolio.net/css/css_animated_profile_cards.html

www.marcofolio.net/css/animated_wicked_css3_3d_bar_chart.html

3dtransforms.desandro.com

To find out which CSS properties can be animated, take a look at developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties.

Here is a simpler example:

animation1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Animation example 1</title>
    <meta charset=UTF-8>
    <style>
      body {
        background: repeating-radial-gradient(red, blue 20px, red 40px) fixed;
      }

      section {
        position:          absolute;
        width:             500px;
        height:            500px;
        background:        linear-gradient(90deg, black, transparent, white);
        animation:         sectionAnimation 5s infinite alternate;
        border-radius:     100px;
      }

      @keyframes sectionAnimation {
        from {
          left: 0;
          top:  0;
        }
        to {
          left: 500px;
          top:  100px;
        }
      }

      aside {
        position:          absolute;
        width:             30px;
        height:            30px;
        background:        repeating-linear-gradient(-45deg, red, red 5px, white 5px, white 10px);
        border-radius:     15px;
        animation:         asideAnimation 5s infinite alternate;
      }

        /* Standard syntax */
      @keyframes asideAnimation {
        0% {
          left: 500px;
          top:  0;
        }

        50% {
          left: 250px;
          top:  300px;
        }

        100% {
          left: 0px;
          top:  100px;
        }
      }
    </style>
  </head>
  <body>
    <main>
      <section></section>
      <aside></aside>
    </main>
  </body>
</html>
4.2.8.4. Fonts

websitesetup.org/web-safe-fonts-html-css

www.typewolf.com/google-fonts

www.dvginteractive.com/serif-vs-sans-serif-how-to-increase-your-websites-readability-by-more-than-50-2

4.2.8.4.1. @font-face

You can find lots of free fonts at www.google.com/fonts. Also read www.creativebloq.com/typography/free-web-fonts-1131610.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Using the @font-face rule</title>
    <meta charset=utf-8>
    <link href=http://fonts.googleapis.com/css?family=Shadows+Into+Light rel=stylesheet>
    <style>
      body {
        font-family: 'Shadows Into Light', cursive;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>Test header</h1>
    </main>
  </body>
</html>

4.2.9. Quiz

Take the w3schools quiz at www.w3schools.com/quiztest/quiztest.asp?qtest=CSS as a fun way to check you are as good as you think you are.

4.2.10. Tests

4.2.10.1. Computer Shop
ComputerShop

Create the following validated page:

The following information is not complete:

  1. body: no margin and padding, background from black to grey, font color white, black shadow of 2 pixels h, v and blur.

  2. header: height of 100 pixels.

  3. nav: 150 pixels wide.

  4. main: right padding of 10 pixels, overflow auto.

  5. footer: 20 pixels high, font size half normal.

  6. ul with no margin and padding.

  7. li with 10 pixels padding above and below.

  8. Hyperlinks with color gold and 1 pixel black shadow h, v and blur.

  9. Navigation hyperlinks with font size twice normal.

  10. h1 with color gold and font size three times normal.

  11. Definition term with color hex 22bb22, bottom border of 2 pixels blueviolet, top margin of 10 pixels and bottom margin of 5 pixels.

  12. Table data items padding of 20 pixels.

You can copy paste the following text:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Computer Shop
Buy

Contact us

Welcome to our shop.

We offer the following:

Desktops

A desktop computer is a personal computer in a form intended for regular use at a single location desk/table due to its size and power requirements (cf. http://en.wikipedia.org/wiki/Desktop\_computer Wikipedia).

Laptops

A laptop or a notebook is a portable personal computer with a clamshell form factor, suitable for mobile use. There was a difference between laptops and notebooks in the past, but nowadays it has gradually died away. Laptops are commonly used in a variety of settings, including at work, in education, and for personal multimedia.
A laptop combines the components and inputs of a desktop computer, including display, speakers, keyboard and pointing device (such as a touchpad or a trackpad) into a single device. Most modern-day laptops also have an integrated webcam and a microphone. A laptop can be powered either from a rechargeable battery, or by mains electricity via an AC adapter. Laptop is a diverse category of devices and other more specific terms, such as rugged notebook or convertible, refer to specialist types of laptops, which have been optimized for specific uses. Hardware specifications change significantly between different types, makes and models of laptops (cf. http://en.wikipedia.org/wiki/Laptop Wikipedia).

Device

Brand

Price

LTAM

299.99

LTAM

349.99

2015 LTAM T0IF2
4.2.10.1.1. Solution
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Computer Shop</title>
    <meta charset=utf-8>
    <style>
      body {
        margin:      0;
        padding:     0;
        background:  linear-gradient(to bottom right, black, grey) fixed;
        color:       white;
        text-shadow: 2px 2px 2px black;
      }

      header {
        position: fixed;
        width:    100%;
        height:   100px;
      }

      nav {
        position: fixed;
        top:      100px;
        width:    150px;
      }

      main {
        position:      fixed;
        top:           100px;
        left:          150px;
        right:         0;
        bottom:        20px;
        text-align:    justify;
        padding-right: 10px;
        overflow:      auto;
      }

      footer {
        position:   fixed;
        bottom:     0;
        width:      100%;
        height:     20px;
        text-align: center;
        font-size:  0.5em;
      }

      ul {
        margin:  0;
        padding: 0;
      }

      li {
        padding-top:    10px;
        padding-bottom: 10px;
      }

      a {
        text-decoration: none;
        color:           gold;
        text-shadow:     1px 1px 1px black;
      }

      nav a {
        font-size: 2em;
      }

      h1 {
        text-align: center;
        font-size:  3em;
        color:      gold;
      }

      dt {
        color:         #22bb22;
        border-bottom: 2px dashed blueviolet;
        margin-top:    10px;
        margin-bottom: 5px;
      }

      th {
        text-align: center;
      }

      td {
        padding: 20px;
      }
    </style>
  </head>
  <body>
    <header>
      <h1>Computer Shop</h1>
    </header>
    <nav>
      <ul>
        <li><a href=#>Buy</a></li>
        <li><a href=#>Contact us</a></li>
      </ul>
    </nav>
    <main>
      <h2>Welcome to our shop.</h2>
      We offer the following:
      <dl>
        <dt>Desktops</dt>
        <dd>A desktop computer is a personal computer in a form intended for regular use at a
          single location desk/table due to its size and power requirements (cf. <a
            href=http://en.wikipedia.org/wiki/Desktop_computer target=_blank>Wikipedia</a>).
        </dd>
        <dt>Laptops</dt>
        <dd>A laptop or a notebook is a portable personal computer with a clamshell form
          factor, suitable for mobile use. There was a difference between laptops and
          notebooks in the past, but nowadays it has gradually died away. Laptops are
          commonly used in a variety of settings, including at work, in education, and for
          personal multimedia.
          A laptop combines the components and inputs of a desktop computer, including display,
          speakers, keyboard and pointing device (such as a touchpad or a trackpad) into a
          single device. Most modern-day laptops also have an integrated webcam and a
          microphone. A laptop can be powered either from a rechargeable battery, or by mains
          electricity via an AC adapter. Laptop is a diverse category of devices and other more
          specific terms, such as rugged notebook or convertible, refer to specialist types of
          laptops, which have been optimized for specific uses. Hardware specifications change
          significantly between different types, makes and models of laptops (cf. <a
            href=http://en.wikipedia.org/wiki/Laptop target=_blank>Wikipedia</a>).
        </dd>
      </dl>
      <table>
        <tr>
          <th>Device</th>
          <th>Brand</th>
          <th>Price</th>
        </tr>
        <tr>
          <td><img src=1432254774_mycomputer.png alt=Comp1></td>
          <td>LTAM</td>
          <td>299.99 &euro;</td>
        </tr>
        <tr>
          <td><img src=1432254808_Computer2.png alt=Comp2></td>
          <td>LTAM</td>
          <td>349.99 &euro;</td>
        </tr>
      </table>
    </main>
    <footer>&copy; 2015 LTAM T0IF2</footer>
  </body>
</html>
4.2.10.2. Video Viewer
VideoViewer

Create the validated site exactly as shown:

It consists of 2 HTML files (index.html and viewer.html) and one CSS file.

The following information is not complete:

  1. Form data is sent to the file viewer.html.

  2. The form box has a black shadow of 3 pixels h, v and blur.

  3. The user name field is focused automatically when the page is loaded.

  4. html and body have no margin and padding and use the whole browser window width and height.

  5. body: repeating radial gradient from black to yellow 100px to white 200px, white text shadow of 2 pixels h, v and blur.

  6. nav: 40 pixels high, full width.

  7. main: display flex, full width. In index.html, there is no navigation, so main starts at the top. Therefore the login box is centered.

  8. footer: 15 pixels high, font size half normal.

  9. ul with 10 px horizontal and no vertical margin. No padding.

  10. Navigation links with 10 pixel padding, 2 pixel golden border.

  11. The form has automatic margin.The animation lasts 5 seconds and starts with 0 opacity.

  12. Form inputs have 0.5 opacity and white text shadow of 2 pixels h, v, and blur.

  13. The table takes 20% of the total width and has a margin of 5 pixels.

  14. Table headings have red text color and a font size 25% bigger than normal.

  15. The iframe takes 80% of the total width and also has a margin of 5 pixels.

You can copy paste the following text:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Login

Title
Module
Link

Windows 7 installation
SYSEX1
https://www.youtube.com/embed/NP3cPmC-08A

Computer Shop
HTSTA
https://www.youtube.com/embed/C99FqKlnD1s

T1IF Invaders
CLISS2
https://www.youtube.com/embed/c--I9podO0s

2015 WMOTU
4.2.10.2.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Video Viewer</title>
    <meta charset=utf-8>
    <link href=style.css rel=stylesheet>
    <style>
      main {
        top: 0;
      }
    </style>
  </head>
  <body>
    <main>
      <form action=viewer.html>
        <fieldset>
          <legend>Login</legend>
          <input placeholder="user name" required autofocus>
          <input type=password placeholder=password required>
          <button>Login</button>
        </fieldset>
      </form>
    </main>
    <footer>&copy; 2015 WMOTU</footer>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Video Viewer</title>
    <meta charset=utf-8>
    <link href=style.css rel=stylesheet>
  </head>
  <body>
    <nav>
      <ul>
        <li><a href=index.html>Log out</a></li>
        <li><a
          href="mailto:t0if2@ltam.lu?subject=Information%20request">Contact us</a></li>
      </ul>
    </nav>
    <main>
      <table>
        <thead>
          <tr>
            <th>Title</th>
            <th>Module</th>
            <th>Link</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>Windows 7 installation</td>
            <td>SYSEX1</td>
            <td><a href=https://www.youtube.com/embed/NP3cPmC-08A target=myFrame>View</a></td>
          </tr>
          <tr>
            <td>Computer Shop</td>
            <td>HTSTA</td>
            <td><a href=https://www.youtube.com/embed/C99FqKlnD1s target=myFrame>View</a></td>
          </tr>
          <tr>
            <td>T1IF Invaders</td>
            <td>CLISS2</td>
            <td><a href=https://www.youtube.com/embed/c--I9podO0s target=myFrame>View</a></td>
          </tr>
        </tbody>
      </table>
      <iframe name=myFrame></iframe>
    </main>
    <footer>&copy; 2015 WMOTU</footer>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}

body {
  background: repeating-radial-gradient(black, yellow 100px, white 200px) fixed;
  text-shadow: 2px 2px 2px white;
}

nav {
  position: fixed;
  top: 0;
  width: 100%;
  height: 40px;
}

main {
  position: fixed;
  top: 40px;
  width: 100%;
  bottom: 15px;
  display: flex;
  overflow: auto;
}

footer {
  position: fixed;
  bottom: 0;
  width: 100%;
  height: 15px;
  text-align: center;
  font-size: 0.5em;
}

nav > ul {
  list-style: none;
  margin: 10px 0;
  padding: 0;
}

nav > ul > li {
  display: inline;
}

nav > ul > li > a {
  padding: 10px;
  background-color: black;
  border: 2px inset gold;
  color: gold;
}

nav > ul > li > a:hover {
  background-color: blue;
}

a {
  text-decoration: none;
}

form {
  margin: auto;
  box-shadow: 3px 3px 3px black;
  animation: formAnim 5s;
}

@keyframes formAnim {
  from {
    opacity: 0;
  }
}

form input {
  opacity: 0.5;
  text-shadow: 2px 2px 2px white;
}

table {
  width: 20%;
  margin: 5px;
}

iframe {
  border: none;
  width: 80%;
  margin: 5px;
}

th {
  color: red;
  font-size: 1.25em;
}

4.3. JavaScript

4.3.1. Introduction to programming

In order to tell a computer what to do, we use a special language, called a "programming" language. Like "human" languages, this is a set of instructions that are translated into codes that have specific meaning to the computer.

More formally, according to en.wikipedia.org/wiki/Computer_programming:

Computer programming (often shortened to programming) is a process that leads from an original formulation of a computing problem to executable programs. It involves activities such as analysis, understanding, and generically solving such problems resulting in an algorithm, verification of requirements of the algorithm including its correctness and its resource consumption, implementation (commonly referred to as coding) of the algorithm in a target programming language. Source code is written in one or more programming languages (such as C, C++, C#, Java, Python, Smalltalk, JavaScript, etc.). The purpose of programming is to find a sequence of instructions that will automate performing a specific task or solve a given problem. The process of programming thus often requires expertise in many different subjects, including knowledge of the application domain, specialized algorithms and formal logic.

4.3.2. Getting started with JavaScript

JavaScript is the programming language used to interact with the Document Object Model (DOM) that the browser creates from HTML and CSS files. It thereby allows the programmatic control of the web page’s appearance and behavior on the client side. It is also increasingly used on the server side with Node.js. It is the foundation for the development of full fledged web applications.

javascript

JavaScript is based on the ECMAScript language specification, the latest version of which can be found at (cf. www.ecma-international.org/ecma-262). Browser support is excellent (cf. kangax.github.io/compat-table/es2016plus).

A number of very insightful articles on JavaScript can be found at javascript.crockford.com/javascript.html.

To get an in-depth overview of the latest developments in the real world application of JS, have a look at The state of JavaScript.

An excellent free online book on ECMAScript 5 can be found at exploringjs.com/es5/toc.html.

To find out what’s new in ECMAScript 6 take a close look at the following resources:

Before we get started, open the console of your browser.

Firefox Chrome Internet Explorer

Console

Shift+F2

Ctrl+Shift+J or I

F12

console1
You should always keep the console open, as any error messages will only be visible there.

4.3.3. Adding JavaScript to HTML documents

The HTML tag to include a script is, for obvious reasons, the <script> tag. The user has the possibility to disable JavaScript in his browser. There’s nothing we can do about this except detect it using the <noscript> tag and inform the user that our app won’t run without JavaScript. In Firefox, you can disable JavaScript by going to the page about:config and setting javascript.enabled to false. Now run the example and turn JavaScript back on and rerun the example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Detect whether JavaScript is disabled</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <noscript>Sorry, but this application requires JavaScript to run!</noscript>
    </main>
  </body>
</html>
4.3.3.1. External JavaScript

The most common approach is to create a separate file with the extension .js (the extension is not mandatory, but recommended) and include it in the HTML file. Here’s an example:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Using external JS</title>
    <meta charset=utf-8>
    <script src=js1.js></script>
  </head>
  <body></body>
</html>
1
2
3
"use strict"

alert('Hello world!') // Display a message box with the text 'Hello world!'.

The strict mode "use strict"; instruction turns on strict mode (cf. Strict mode), which we should always do in order to make our life easier and to write better quality code. You can put this instruction into the default JavaScript template of your IDE, so that you do not have to type it in every time (cf. NetBeans templates or PhpStorm templates). In modern JS we should work with Modules as much as possible as opposed to standard JS files.

The <script> tag can be placed into the document’s head or body. Remember that your browser processes an HTML document from top to bottom. So if the script is put into the head, it is loaded and executed before the HTML body exists. If the script tries to access HTML elements, it will fail, as shown in this example (remember, you need to have the console open to see the error):

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Using external JS the wrong way</title>
    <meta charset=utf-8>
    <script src=js2.js></script>
  </head>
  <body><main></main></body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"use strict"

// Save the first (and only) main element in a constant named main.
const main = document.getElementsByTagName('main')[0]
// Set the text color of the main element to gold.
main.style.color = 'gold'
// Set the background color of the main element to black.
main.style.backgroundColor = 'black'
// Set the text 'Hello world!' as content of the main element.
main.innerHTML = 'Hello world!'

Here is the error message you should see:

firebug1

One solution to this problem is to include the script at the end of the HTML document, but this is not recommended, as it is difficult to see which script gets included. A cleaner approach is to have the script loaded in the head of the document but put all instructions that should be executed immediately into a function (don’t worry, we’ll look at those in detail in JS functions, but for now you just need to know that a function is a series of instructions that get executed when we "call" the function using its name) which gets run when the browser has either finished loading and parsing the initial HTML document, without waiting for stylesheets, images, and subframes to finish loading or when the full document has been loaded and parsed. To achieve the former, we use the DOMContentLoaded (cf. developer.mozilla.org/en/docs/Web/Events/DOMContentLoaded) and for the latter the load event (cf. Events where we talk about events in detail):

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Using external JS the right way</title>
    <meta charset=utf-8>
    <script src=js3.js></script>
  </head>
  <body><main></main></body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
"use strict"

// We declare but do not execute the function.
const init = () => {
  const main = document.querySelector('main') // Get the main element.
  main.style.color = 'gold' // Set text color to gold.
  main.style.backgroundColor = 'black' // Set background color to black.
  main.innerHTML = 'Hello world!' // Set main content.
}

/* Only after the browser has finished loading and parsing the initial HTML document, without
 waiting for stylesheets, images, and subframes or the whole document,
 meaning that the DOM is available, will the init function be called.
 There are 2 events we can use and 2 ways to do this. The first one is the preferred one, as it
 allows to install several listeners for the same event. The second one is shorter. */
window.addEventListener('DOMContentLoaded', init)
//window.addEventListener('load', init)
//window.onload = init
4.3.3.1.1. async and defer

The async attribute is only for external scripts (and should only be used if the src attribute is present).

Note: There are several ways an external script can be executed:

  • If async is present: The script is executed asynchronously with the rest of the page (the script will be executed while the page continues the parsing).

  • If async is not present and defer is present: The script is executed when the page has finished parsing.

  • If neither async or defer is present: The script is fetched and executed immediately, before the browser continues parsing the page.

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Using external JS the right way</title>
    <meta charset=utf-8>
    <script src=js3a.js defer></script>
  </head>
  <body><main></main></body>
</html>
1
2
3
4
5
6
"use strict"

const main = document.querySelector('main') // Get the main element.
main.style.color = 'gold' // Set text color to gold.
main.style.backgroundColor = 'black' // Set background color to black.
main.innerHTML = 'Hello world!' // Set main content.
4.3.3.2. Embedded JavaScript

If we have only a small script that we want to use in our document, we can embed it directly using the <script> tag, like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Using embedded JS</title>
  <meta charset=utf-8>
  <script type=module>
    alert('Hello world!')
  </script>
</head>
<body></body>
</html>

We can embed JavaScript in the head and/or in the body part of the document.

4.3.3.3. Inline JavaScript

If we need to execute only one or very few instructions, for instance when a link is clicked, we can use JavaScript inline as an event handler (cf. Events).

Here’s a simple illustration using a clickable link:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Using inline JS</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <a href=# onclick="alert('Hello world!')">
        If you click me I'll greet the world!</a>
    </main>
  </body>
</html>
4.3.3.4. Combinations

In the real world, you will often use combinations of some or all of the three methods, for instance an external script that defines functions, which are called via embedded or inline script.

Here is an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Combining external and inline JS</title>
    <meta charset=utf-8>
    <script src=js6.js></script> <!-- include an external script -->
  </head>
  <body>
    <main>
      <a href=# onclick=modifyMain()>If you click me I'll change the world!</a>
    </main>
  </body>
</html>
1
2
3
4
5
6
7
8
"use strict"

const modifyMain = () => {
  const main = document.getElementsByTagName('main')[0]
  main.style.color = 'gold'
  main.style.backgroundColor = 'black'
  main.innerHTML = 'Hello world!'
}

4.3.4. Modules

The import statement is used to import functions, objects, or primitives which are defined in and exported by an external module, script, or the like.

You might have to change a flag in your browser in order to enable this feature (cf. caniuse.com/#search=export and jakearchibald.com/2017/es-modules-in-browsers).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <meta name=viewport content="width=device-width, initial-scale=1">
  <title>Module 1</title>
  <script src=main.mjs type=module></script>
</head>
<body>
  <main>
    <button id=button>Click me</button>
  </main>
</body>
</html>
1
2
3
4
5
6
7
8
import {printText1, printText2} from './functions.mjs'

printText1('dsadsadsadsadsadsa')
printText2('dsadsadsadsadsadsa')

document.querySelector('#button').addEventListener('click', evt => {
    console.dir(evt)
})
1
2
3
4
5
6
7
8
9
const printText1 = text => {
    console.log(`Here's the text: ${text}`)
}

const printText2 = text => {
    alert(`Here's the text: ${text}`)
}

export {printText1, printText2}

4.3.5. Comments

In order to make our scripts easier to understand, it is a good idea to add comments that explain what the purpose of a piece of code is if it is not obvious. Comments should not repeat the code or explain trivialities. JavaScript provides single line comments using // and multiline comments using /* */. You have already seen examples of both in External JavaScript.

4.3.6. Semicolons

Most ECMAScript statements and declarations must be terminated with a semicolon. Such semicolons may always appear explicitly in the source text. For convenience, however, such semicolons may be omitted from the source text in certain situations. These situations are described by saying that semicolons are automatically inserted into the source code token stream in those situations.

4.3.7. Basic input and output

4.3.7.1. console.log

The console object provides access to the browser’s debugging console, which we use to detect errors in our programs. For the details see developer.mozilla.org/en-US/docs/Web/API/console and developer.mozilla.org/en-US/docs/Tools/Web_Console.

Here is a simple example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Console usage example</title>
    <meta charset=utf-8>
    <script src=console1.js type=module></script>
  </head>
  <body>
  </body>
</html>
1
2
3
4
5
6
7
8
const s = '%cIt\'s a %cbeautiful %cday!' // '\' is used to avoid the second ' being interpreted.
console.log(s, "color: blue", "color: red", "color: blue")
const a = [1, 2, 3, 4]
console.log(a)
console.dir(a) // Display object details.
for (let i = 0; i < 4; i++) console.count('Number of times loop body executed: ')
console.warn('Wrong user input')
console.error('Severe program error')
4.3.7.2. document.write

document.write writes a given text verbatim into the document at the current position. Example: students.btsi.lu/evegi144/WAD/JS/document_write1.html

1
2
3
document.write('<table><tr><th>Club</th><th>Score</th></tr>')
document.write('<tr><td>Bayern München</td><td>17</td></tr>')
document.write('<tr><td>FC Liverpool</td><td>15</td></tr></table>')
4.3.7.3. alert

In some of the previous examples we have already used the alert method. This method is part of the global window object, which we’ll discuss in Browser Object Model (BOM). Normally we would have to write window.alert to use this method, but the browser considers window as the default object, thereby allowing us to simply write the method name. alert takes the text, quoted in "" or '', as parameter and displays it in a dialog box, which will block the screen until the user confirms the message.

4.3.7.4. confirm

The confirm method, just like alert, displays a dialog box with a specified message, but with an OK and a Cancel button, so that the user has a choice. The method returns true if the user clicked OK and false if the user clicked Cancel. Example:

1
2
3
4
if (confirm('Do you really want to format your hard drive?') === true)
  console.log('OK, as you wish!')
else
  console.log('Pweh!')
4.3.7.5. prompt

prompt is used to get input from the user. The first parameter specifies the text to display. The second parameter is optional and can be used to display a default value. Take a look at the example and experiment with it. Note the \' in the prompt message. This is used to tell the browser that the apostrophe does not terminate the string but is to be displayed as such:

1
2
3
const input = prompt('Congratulations, you\'re the master of the universe. ' +
  'Please enter your name:', 'Donald Duck')
if (input) console.log(`Well done ${input}`)

4.3.8. Constants

If we want to use a specific immutable value throughout our script, we can declare it as a constant using const. Constants have block scope, i.e. they are only visible within the block that they are defined. If we define a constant in the main part of our script it will be visible everywhere. The advantage of using the constant compared to using the value directly is the ease with which we can change it.

For instance, if we decide that the player in our latest space shooter should have 5 lives instead of 3, we simply change the value of our constant. Without constant we would have to find all the occurrences of the value in our script and replace them individually, risking to accidentally change other values.

1
2
const LIVES = 3
console.log(LIVES)

4.3.9. Variables

A program spends most of its time manipulating some kind of data, e.g. integer and decimal (or floating point) numbers, text (called string) or other, potentially more complex, data types. The information we need, for internal calculations or for display to the user, has to be stored somehow. To store data, we use variables. A variable is simply a place in computer memory where some information is stored.

To work with a variable we first need to declare it using the let or var keyword. The former has block scope whereas the latter has function scope. For an illustration of the difference, see let vs var. For a more detailed explanation with examples see developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/let. After declaration the variable value is undefined. We can store some information in it using the = operator. Let’s take a look at an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// We want to recalculate the health of our spaceship after it has been hit by an enemy bullet.
// We declare the variable for the initial health level and assign it a value of 100.
let health // Declare a variable with block scope. Default value undefined.
alert(`Current health: ${health}`) //
health = 100 // We assign the value 100 to the variable.
alert(`Current health: ${health}`)
/* We declare a constant to store the damage caused by an enemy bullet hit. A constant cannot
be changed. */
const SHOT_IMPACT = 20

// *** Boom, we have been hit ***
// Now we calculate the new health value
health = health - SHOT_IMPACT
alert(`We've been hit! Current health: ${health}`)
4.3.9.1. Variable names
  • Variable names can contain letters, digits, underscores, and dollar signs.

  • Variable names must begin with a letter.

  • Variable names can also begin with $ and _.

  • Variable names are case sensitive (y and Y are different variables).

  • Reserved words (like JavaScript keywords) cannot be used as variable names.

It is recommended to use camelCase, e.g. firstFraction instead of first_fraction or firstfraction etc. Please read the quick overview of JavaScript code conventions at www.w3schools.com/js/js_conventions.asp.

4.3.10. Data types

JavaScript is a dynamically typed language, meaning that we can store any type of data in a variable or pass it as an argument to a function. This is in contrast to traditional programming languages such as C, C++, Java etc. Nevertheless, we need to be aware of the main data types that we will use. In our programs we can use the typeof operator to determine the current type of a variable, as in this example:

1
2
3
4
5
6
7
8
let x = 7 // integer number
console.log(`Current type of x: ${typeof x}`)
x = 5.6e-2 // decimal number
console.log(`Current type of x: ${typeof x}`)
x = 'abc sdfsd' // string
console.log(`Current type of x: ${typeof x}`)
x = false // boolean
console.log('Current type of x: ' + typeof x)
4.3.10.1. Strings

A string is simply text, i.e. a sequence of one or more characters. Strings are always embedded within simple (') or double (") quotes. We can combine several strings using the + operator, as shown in the examples below. Strings have a number of properties and methods that make working with them much easier. Study www.w3schools.com/jsref/jsref_obj_string.asp for a full reference with many examples.

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const s1 = 'This is a string'
/* Now we write the combination of two strings, which is a new string, into the body.
 Note that we use the + operator to concatenate, i.e. to combine, three strings. */
document.body.innerHTML = `<p>Content of constant s1: ${s1}</p>`

let firstName = 'Donald', lastName = 'Duck'
document.body.innerHTML += `<p>Hello ${firstName} ${lastName}, how are you today?</p>`
firstName = 'Dagobert'; // We change the content of this variable.
document.body.innerHTML += `<p>Hello ${firstName} ${lastName}, how are you today?</p>`
document.body.innerHTML += `<p>Hello ${firstName} ${lastName.toUpperCase()}, how are you today?</p>`
4.3.10.1.1. Template literals
A template literal is a new kind of string literal that can span multiple lines and interpolate expressions (include their results).
1
2
3
const firstName = prompt('Please enter your first name')
const lastName = prompt('Please enter your last name')
alert(`Hello ${firstName} ${lastName}, how are you?`)
The literal itself is delimited by backticks (`), the interpolated expressions inside the literal are delimited by ${ and }. Template literals always produce strings.

There’s much more to template literals, for the details please check the link above.

4.3.10.2. Numbers

JavaScript does not distinguish between integers and decimals. Numbers are always stored as double precision floating point numbers, following the international IEEE 754 standard. This format stores numbers in 64 bits, where the number (the fraction) is stored in bits 0 to 51, the exponent in bits 52 to 62, and the sign in bit 63 (cf. www.w3schools.com/js/js_numbers.asp). This has practical implications, some of which are problematic. It leads for instance to mathematical imprecision, which you can confirm by typing 0.1 + 0.2 in the console. To avoid this problem, we can use decimal.js. This problem is however common in almost all programming languages and not limited to JS. For details on the problem in different programming languages see 0.30000000000000004.com.

Numbers have properties and methods that make working with them much easier. Study www.w3schools.com/jsref/jsref_obj_number.asp for a full reference with many examples.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// We declare 4 variables and initialize them with 2 integer and 2 decimal numbers.
const i1 = 5000, i2 = 7345, d1 = 32.456787, d2 = -2.3e5 // exponential notation -> 2.3 * 10^5
/* We display their values as well as the results of simple operations.
   Note that the + operator automatically converts the value after the + into a string if
   the value in front of the + is a string.
   On the other hand, if there is nothing in front of the + operator and it is followed by a
   string, then this string will be converted to a number, if possible, otherwise to NaN
   (not a number).
 */
console.log(`i1: ${i1} i2: ${i2} d1: ${d1} d2: ${d2}`)
/* Note that we need to put the calculations into parentheses, otherwise i1 + i2 would
 give the string '50007345'. Try it!
  */
console.log('i1 + i2: ' + (i1 + i2))
console.log(`i1 - i2: ${i1 - i2}`)
console.log('i1 / i2: ' + (i1 / i2))
console.log(`i1 * i2: ${i1 * i2}`)
console.log('d1 + d2: ' + (d1 + d2))
console.log(`d1 - d2: ${d1 - d2}`)
console.log('d1 / d2: ' + (d1 / d2))
console.log(`d1 * d2: ${d1 * d2}`)

let input1 = prompt('Please enter your first number: ')
let input2 = prompt('Please enter your second number: ')
console.log(`input1: ${input1} input2: ${input2}`)
console.log(`input1 + input2: ${input1 + input2}`)
console.log(`input1 - input2: ${input1 - input2}`)
console.log(`input1 / input2: ${input1 / input2}`)
console.log(`input1 * input2: ${input1 * input2}`)
console.log('Oops! Something went wrong. Can you spot the problem?')
console.log('Apparently the + operator treats our numbers as strings!')
console.log("Let's fix it:")
input1 = parseFloat(input1) // Convert string to floating point number.
input2 = parseFloat(input2) // Ditto.
console.log(`input1: ${input1} input2: ${input2}`)
console.log(`input1 + input2: ${input1 + input2}`)
console.log(`input1 - input2: ${input1 - input2}`)
console.log(`input1 / input2: ${input1 / input2}`)
console.log(`input1 * input2: ${input1 * input2}`)

console.log('Problem:')
// Given the limited precision of floating point numbers, we need to be careful with decimals.
let i = 0
// The toFixed() method converts a number into a string with a specified number of decimals.
console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
i = i + 0.2
console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
i = i + 0.2
console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
i = i + 0.2
console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
i = i + 0.2
console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
i = i + 0.2
console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
i = i + 0.2
console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
i = i + 0.2
console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
i = i + 0.2
console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
i = i + 0.2
console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
i = i + 0.2
console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
console.log('The same with a loop:')
// This is a for loop, which we'll discuss later.
for (let i = 0; i < 2; i += .2) console.log(`i: ${i} i.toFixed(1): ${i.toFixed(1)}`)
4.3.10.3. Booleans

A boolean value is either true or false (cf. www.w3schools.com/js/js_booleans.asp). This data type is used in conditions and comparisons. Example:

1
2
3
4
const flag = true, x = 7, y = 34.6
console.log(flag)
console.log(`x: ${x} y: ${y} => x is bigger than y: ${x > y}`)
console.log(`x - y + 20 is negative: ${(x - y + 20) < 0}`)
4.3.10.4. Conversions

JavaScript provides several options to convert between data types. To convert strings into numbers, we can put the + operator in front of the string. If the string cannot be fully converted into a number, the result will be NaN, meaning not a number. To transform user input into a number, it is usually preferable to use parseInt or parseFloat. These functions take a string as input and return an integer or a decimal (float, shortcut for floating point number). The advantage of these functions is that even if there are non-numerical characters in the string, as long as there is at least one digit at the beginning of the string, they will return a number and simply ignore the other characters. To convert anything into a string, we can use toString, which is a method that every object has by default. Let’s look at a couple of examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const a1 = '66.76', a2 = '5' // Declare and define 2 string variables.
console.log(a1 + a2) // Concatenation of 2 strings.
console.log(+a1 + +a2) // Conversion to numbers.
const s1 = prompt('Please enter your first number: ') // string 1
const s2 = prompt('Please enter your second number: ') // string 2
console.log(s1 + s2) // Concatenation of 2 strings.
console.log(parseInt(s1) + parseInt(s2)) // Conversion to integers.
console.log(parseFloat(s1) + parseInt(s2)) // Conversion to 1 float + 1 int.
console.log(parseInt(s1) + parseFloat(s2)) // Conversion to 1 int + 1 float.
console.log(parseFloat(s1) + parseFloat(s2)) // Conversion to floats.
const x1 = 123, x2 = 456 // Declare and define 2 number variables.
console.log(x1 + x2) // Addition of 2 numbers.
console.log(x1.toString() + x2) // Conversion to strings.
console.log(x1 + x2.toString()) // Conversion to strings.
console.log(x1.toString() + x2.toString()) // Conversion to strings.
const b1 = false, b2 = true // Declare and define 2 booleans.
console.log(b1 + b2) // Addition of 2 booleans (0 + 1 -> 1).
console.log(b1.toString() + b2) // Conversion to strings.
console.log(b1 + b2.toString()) // Conversion to strings.
console.log(b1.toString() + b2.toString()) // Conversion to strings.
4.3.10.5. Dates

JavaScript provides a very useful Date object to handle time and date information. Study

www.w3schools.com/js/js_dates.asp

www.w3schools.com/jsref/jsref_obj_date.asp

developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date

4.3.10.6. Math

The Math object offers a number of useful methods. Study www.w3schools.com/js/js_math.asp and www.w3schools.com/jsref/jsref_obj_math.asp.

4.3.10.7. Regular expressions

Regular expressions provide a very powerful means to search for patterns in strings. Study www.w3schools.com/js/js_regexp.asp and www.w3schools.com/js/js_regexp.asp.

4.3.11. Operators

Study the JavaScript operator documentation on w3schools: www.w3schools.com/js/js_operators.asp and www.w3schools.com/js/js_comparisons.asp.

4.3.11.1. eval

eval takes a string argument and interprets it as JavaScript code.

1
console.log(eval('if (5 < 7) true; else false'))
For security and performance reasons usage of eval should be avoided (see developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval).

4.3.12. Conditional statements

4.3.12.1. if

Oftentimes the behavior of our program depends on a condition. For example, if we have developed a social network and we want to congratulate the user on his birthday, we first need to verify the condition of the user’s birthday being true. Let’s look at a couple of examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// We ask the user a very tough question and store his answer.
const correctAnswer = 7
const userAnswer = parseInt(prompt('3 + 4 = '))
// Now we check whether his answer is correct or not and react accordingly.
if (userAnswer === correctAnswer) document.body.innerHTML += 'Well done!<br>'
else document.body.innerHTML += 'Please check your answer.<br>'

/* Let's assume the user's birthday is 1.2.1983.
 First we need to get the current date.
 Then we need to check whether day and month correspond
 to the user's birthday.
 If that's the case, then we will congratulate him, otherwise
 we'll tell him how far away his birthday is.
 */
// getMonth() returns a number between 0 and 11, so we need to subtract 1!
const userMonth = 1 // February, January would be 0.
const userDay = 1 // User's month and day of birth.
const date = new Date() // Get the current date.
if (date.getMonth() === userMonth && date.getDate() === userDay)
  alert('Happy birthday!')
/* The else part is optional. In this case we don't need it, as we only want
 to do something if the condition is true.
 */

// Now we create a little script that displays the name of the current month.
const month = date.getMonth() // We reuse the date variable created above.
if (month === 0) document.body.innerHTML += 'January<br>'
else if (month === 1) document.body.innerHTML += 'February<br>'
else if (month === 2) document.body.innerHTML += 'March<br>'
else if (month === 3) document.body.innerHTML += 'April<br>'
else if (month === 4) document.body.innerHTML += 'May<br>'
else if (month === 5) document.body.innerHTML += 'June<br>'
else if (month === 6) document.body.innerHTML += 'July<br>'
else if (month === 7) document.body.innerHTML += 'August<br>'
else if (month === 8) document.body.innerHTML += 'September<br>'
else if (month === 9) document.body.innerHTML += 'October<br>'
else if (month === 10) document.body.innerHTML += 'November<br>'
else if (month === 11) document.body.innerHTML += 'December<br>'
4.3.12.2. switch

Another instruction that can be used if there are many alternatives to choose from is switch. A detailed explanation can be found at www.w3schools.com/js/js_switch.asp. Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const date = new Date() // Get the current date.
const month = date.getMonth() // We reuse the date variable created above.
switch (month) {
  case 0:
    document.body.innerHTML += 'January<br>'
    break
  /* If we leave this out, the following instructions will be executed,
   even though month is 0 and not 1.
   */
  case 1:
    document.body.innerHTML += 'February<br>'
    break
  case 2:
    document.body.innerHTML += 'March<br>'
    break
  case 3:
    document.body.innerHTML += 'April<br>'
    break
  case 4:
    document.body.innerHTML += 'May<br>'
    break
  case 5:
    document.body.innerHTML += 'June<br>'
    break
  case 6:
    document.body.innerHTML += 'July<br>'
    break
  case 7:
    document.body.innerHTML += 'August<br>'
    break
  case 8:
    document.body.innerHTML += 'September<br>'
    break
  case 9:
    document.body.innerHTML += 'October<br>'
    break
  case 10:
    document.body.innerHTML += 'November<br>'
    break
  case 11:
    document.body.innerHTML += 'December<br>'
    break
  default:
    document.body.innerHTML += 'Invalid month!<br>'
}

4.3.13. Loops

Loops are used to execute a block of, i.e. one or several, statements, several times. JavaScript provides the for, while, for in and do while loops. Each loop consists of a head and a body part.

4.3.13.1. for

The for loop head specifies the start value, a condition and what should be done after each iteration of the body. This type of loop is the preferred choice if we know the number of iterations in advance. We already saw an example in Numbers. Let’s study a couple more examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// A very basic loop.
for (var i = 0; i < 4; i++) {
  // Variable i is defined for the whole function, in this case the whole script as there
  // is no function.
  document.body.innerHTML += `i: ${i}<br>`
}

/*
 Let's calculate the sum of all numbers between a and b, except those that
 can be divided by 5.
 */
let a = 2, b = 123, sum = 0
for (i = a; i <= b; i++) { // i was defined using var -> function scope
  if (i % 5 !== 0) sum += i
}
console.log('sum: ' + sum)

console.log('Here is an illustration of what block scope means.')
// A very basic loop.
for (let j = 0; j < 4; j++) { // Variable j is only defined for this block.
  document.body.innerHTML += `i: ${i}<br>`
}

/*
 Let's calculate the sum of all numbers between a and b, except those that
 can be divided by 5.
 */
sum = 0
for (j = a; j <= b; j++) { // j was defined using let -> block scope
  // To solve this we would have to define j for this block using let j = a;
  if (j % 5 !== 0) {
    sum += j
  }
}
console.log(`sum: ${sum}`)
4.3.13.2. while

The while loop head only specifies the condition that needs to be true for the loop body to be executed. The condition can be any boolean expression, i.e. we do not necessarily need a counter variable. Let’s look at an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const result = 2 + 2
let input, count = 1 // Define variables.
/* As long as the user does not enter the correct answer, we loop.
 Given that prompt returns a string, we need to convert it to an integer.
 */
while ((input = parseInt(prompt('2 + 2 = '))) !== result) {
  document.body.innerHTML += 'Wrong answer, please try again.<br>'
  count++ // Increase the attempt counter.
}
console.log(`Correct, it took you ${count} attempt(s).`)
4.3.13.3. for in

for in loops through the enumerable properties of an object and includes the enumerable properties of the prototype chain (cf. Objects and classes).

1
2
3
4
5
6
7
8
9
const car = {
  color: 'black',
  weight: 1500,
  length: 4.5
}

for (const prop in car) {
  console.log(`Property name: ${prop} property value: ${car[prop]}`)
}

You should not use for in to loop through an array, as the following example inspired by "Effective JavaScript" p. 132 shows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const grades = [23, 45, 56, 43, 32, 29]
let total = 0
for (const grade in grades) {
  total += grade
}
let average = total / grades.length
console.log(`Average: ${average}`) // 2057.5 -> wrong, but why?

// Let's add a debugging instruction to our loop, so that we can see what happens:
total = 0
for (const grade in grades) {
  total += grade
  console.log(`${grade} ${total}`)
}

// Now we see that for in loops through the KEYS of the array, not the VALUES.
// The correct approach is to use a for loop:
total = 0
for (let i = 0; i < grades.length; i++) {
  total += grades[i]
  console.log(`${grades[i]} ${total}`)
}
average = total / grades.length
console.log(`Average: ${average}`) // 38 -> correct
4.3.13.4. for of
4.3.13.5. do while

This is identical to the while loop, except that the body of do while is always executed at least once, as the condition is only checked after the execution instead of before.

4.3.14. Jumps and exceptions

Jump statements instruct the JavaScript interpreter to jump to a different source code location.

4.3.14.1. Labeled statements

A JavaScript statement can be labeled by writing the label followed by a colon.

4.3.14.2. break

Used alone, break causes the innermost enclosing loop or switch statement to exit immediately. If followed by a label, it terminates the enclosing statement that has the specified label. Take a close look at these examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const matrix = [
  [1, 3, 5, 7, 9], // sum 25
  [2, 2, 'a', 2, 2], // sum 8
  [3, 4, 5, 6, 7], // sum 25
  [9, 8, 7, 6, 5], // sum 35
  [4, 4, 4, 4, 4] // 20
]
let sum = 0, error = false
outerloop: for (let i = 0; i < matrix.length; i++)
  for (let j = 0; j < matrix[0].length; j++) {
    if (typeof matrix[i][j] !== 'number') {
      error = true
      break outerloop
    }
    sum += matrix[i][j]
  }
console.log(`Sum: ${sum} error: ${error}`)

sum = 0
error = false
for (let i = 0; i < matrix.length; i++)
  for (let j = 0; j < matrix[0].length; j++) {
    if (typeof matrix[i][j] !== 'number') {
      error = true
      break
    }
    sum += matrix[i][j]
  }
console.log(`Sum: ${sum} error: ${error}`)
4.3.14.3. continue

continue is similar to break, except that it does not exit the loop but restarts it at the next iteration, i.e. it terminates only the current iteration, not the whole loop. Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const matrix = [
  [1, 3, 5, 7, 9], // sum 25
  [2, 2, 'a', 2, 2], // sum 8
  [3, 4, 5, 6, 7], // sum 25
  [9, 8, 7, 6, 5], // sum 35
  [4, 4, 4, 4, 4] // 20
]
let sum = 0, error = false
outerloop: for (let i = 0; i < matrix.length; i++)
  for (let j = 0; j < matrix[0].length; j++) {
    if (typeof matrix[i][j] !== 'number') {
      error = true
      continue outerloop
    }
    sum += matrix[i][j]
  }
console.log(`Sum: ${sum} error: ${error}`)
4.3.14.4. Exception handling

Often our programs rely on the user to provide reasonable input. But what happens if the user provides invalid input? For instance, we have a function that calculates the product of all given parameters, assuming they are all numbers. If one of the parameters is a string, our script might not perform as expected. That’s not very professional and chances are, our users won’t like it. A better way is to throw an exception using the throw instruction and catch it using try catch finally. In the try block we put the code that is at risk of breaking, for instance on wrong user input. In the catch block we put the code that should be executed if something went wrong in the try block, i.e. an exception occurred. The finally block is optional and will always be executed as the final block. JavaScript has a built-in Error object (cf. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error), that serves to signal an error. Let’s study an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const product = (...args) => {
  let prod = 1
  for (let i = 0; i < args.length; i++) {
    if (typeof args[i] !== 'number')
      throw new Error('All arguments must be numbers!')
    prod *= args[i]
  }
  return prod
}

try {
  console.log(product(2, 34, 5)) // OK
  console.log(product(2, 'abc', 5)) // Wrong arg -> trouble.
}
catch (error) {
  console.error(error)
}
finally {
  console.log('Job done')
}
console.log('sadsadadsadsa')

4.3.15. Functions

A function is a block of code that is executed by "calling" the function. Functions are of fundamental importance in software development, as they permit to reuse code, i.e. we solve a problem once and can then use the solution as often as we want without having to reinvent the wheel.

4.3.15.1. Normal functions

A function is defined using the function keyword, followed by parentheses (), followed by curly brackets {}. A function can take one or several parameters, which represent information that is given to the function by the caller.

Parameters of primitive types are passed by value, whereas objects are passed by reference.

Within the curly brackets we find the body of the function, i.e. the code that is executed when the function is called. A function can return an object using the return keyword. Please note that return does 2 things:

  • Returns the given object, or nothing if no object is provided.

  • Terminates the function. This means that any instructions following return will not be executed.

Normal functions have a built-in arguments object, which contains an array of the arguments used when the function was called.

Also note that a normal function can be called before its declaration. This is called hoisting.

Let’s study a couple of examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// A simple sum function
function sum(a, b) {
  return a + b
}
const a = 34, b = 67.89
console.log(`The sum of ${a} and ${b} is ${sum(a, b)}.`)

/* We define a function without parameters that adds a paragraph and returns
 nothing.
 */
function sayHelloWorld() {
  const r = Math.floor(Math.random() * 256) // Generate random red component.
  const g = Math.floor(Math.random() * 256) // Random green component.
  const b = Math.floor(Math.random() * 256) // Random blue component.
  const p = document.createElement('p') // Create new <p> element.
  p.style.color = 'blue' // Set text and background colors.
  p.style.backgroundColor = 'rgb(' + r + ', ' + g + ', ' + b + ')'
  p.innerHTML = 'Hello world!' // Set content.
  const main = document.querySelector('main') // Get main element.
  main.appendChild(p) // Append the new <p> element to <main>.
}

// Now we call the function, i.e. we reuse the code to display the text 5 times.
for (let i = 1; i <= 5; i++) sayHelloWorld()

// Here we call the factorial function even before it's defined -> hoisting.
console.log(`The factorial of 7 is ${factorial(7)}.`)

/* The factorial function takes one parameter, a positive integer, and returns
 the number's factorial. Remember the formula: n! = n * (n - 1) * ... * 2.
 */
function factorial(x) {
  // If x is not a number, we return false.
  if (typeof x !== 'number') return false
  x = x.toFixed(0) // Make sure x is an integer.
  let fact = 1 // In this variable we store the current result.
  for (let i = x; i >= 2; i--) fact *= i // Loop to calculate factorial.
  return fact // Return the result.
}

/* We can also define a function that works with a variable number of
 parameters, a so called variadic function.
 */
function printArgs() {
  for (let i = 0; i < arguments.length; i++)
    console.log(`Argument ${i}: ${arguments[i]} of type ${typeof arguments[i]}`)
  console.dir(arguments)
}

printArgs('Hi', 5.6, 5, false)

// Optional named parameters
// cf. http://exploringjs.com/es6/ch_core-features.html#sec_param-defaults-core-feature
// The second parameter is optional and has a default value.
function optionalArgs(arg1, arg2 = 'whatever') {
  console.log(`${arg1} ${arg2}`)
}

optionalArgs('arg1')
optionalArgs('arg1', 'arg2')

function printFromTo(values, {start=0, end=-1, step=1} = {}) {
  console.log(`Start: ${start}`)
  console.log(`Step: ${step}`)

  if (step === 0)
    console.dir(new Error('Infinite loops are not a good idea...'))
  else for (let i = start; i < end; i += step) console.log(values[i])
}

printFromTo([1,2,3,4,5,6])
printFromTo([1,2,3,4,5,6], {start: 1, end: 6, step: 0})
printFromTo([1,2,3,4,5,6], {start: 1, end: 6, step: 2})
4.3.15.2. Arrow functions
An arrow function expression has a shorter syntax than a function expression and does not bind its own this, arguments, super, or new.target. These function expressions are best suited for non-method functions, and they cannot be used as constructors.

An arrow function is created using . It means that the value in the parentheses, if any, is passed as parameter to the block of code that the arrow is pointing to. An arrow function is defined like a variable.

Contrary to normal functions, which are read at compile-time, arrow functions are read at run-time. Therefore they must be defined in the code before they can be called.

Also contrary to a function, an arrow function does not have an arguments object (cf. point 18.a in www.ecma-international.org/ecma-262/6.0/#sec-functiondeclarationinstantiation). Instead we can use so called rest parameters using the …​ operator, which can also be used with normal functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// A simple sum function
const sum = (a, b) => a + b

const a = 34, b = 67.89
console.log(`The sum of ${a} and ${b} is ${sum(a, b)}.`)

/* We define a function without parameters that adds a paragraph and returns
 nothing.
 */
const sayHelloWorld = () => {
  const r = Math.floor(Math.random() * 256) // Generate random red component.
  const g = Math.floor(Math.random() * 256) // Random green component.
  const b = Math.floor(Math.random() * 256) // Random blue component.
  const p = document.createElement('p') // Create new <p> element.
  p.style.color = 'blue' // Set text and background colors.
  p.style.backgroundColor = 'rgb(' + r + ', ' + g + ', ' + b + ')'
  p.innerHTML = 'Hello world!' // Set content.
  const main = document.querySelector('main') // Get main element.
  main.appendChild(p) // Append the new <p> element to <main>.
}

// Now we call the function, i.e. we reuse the code to display the text 5 times.
for (let i = 1; i <= 5; i++) sayHelloWorld()

/* The factorial function takes one parameter, a positive integer, and returns
 the number's factorial. Remember the formula: n! = n * (n - 1) * ... * 2.
 */
const factorial = x => {
  // If x is not a number, we return false.
  if (typeof x !== 'number') return false
  x = x.toFixed(0) // Make sure x is an integer.
  let fact = 1 // In this variable we store the current result.
  for (let i = x; i >= 2; i--) fact *= i // Loop to calculate factorial.
  return fact // Return the result.
}

// A function expression can only be called after its definition.
console.log('The factorial of 7 is ' + factorial(7) + '.')

const printArgs = (...args) => {
  for (let i = 0; i < args.length; i++)
    console.log(`Argument ${i}: ${args[i]} of type ${typeof args[i]}`)
  console.dir(args)
}

printArgs('Hi', 5.6, 5, false)

// The second parameter is optional and has a default value.
const optionalArgs = (arg1, arg2 = 'whatever') => {
  console.log(`${arg1} ${arg2}`)
}

optionalArgs('arg1')
optionalArgs('arg1', 'arg2')


const printFromTo = (values, {start=0, end=-1, step=1} = {}) => {
  if (step === 0)
    console.dir(new Error('Infinite loops are not a good idea...'))
  else for (let i = start; i < end; i += step) console.log(values[i])
};

printFromTo([1,2,3,4,5,6])
printFromTo([1,2,3,4,5,6], {start: 1, end: 6, step: 0})
printFromTo([1,2,3,4,5,6], {start: 1, end: 6, step: 2})

A major difference between a normal function and an arrow function is that in the case of a normal function, the scope is local whereas in the case of an arrow function, the scope is the surrounding code.

Use non-arrow functions for methods that will be called using the object.method() syntax. Those are the functions that will receive a meaningful this value from their caller. Use arrow functions for everything else.

For a more in-depth illustration of why this is very important, study stackoverflow.com/questions/20279484/how-to-access-the-correct-this-inside-a-callback.

4.3.15.3. Variable scope

It is important to understand the concept of variable scope. A local variable, i.e. a variable that is declared inside the body of a function using the let or var keyword is called a local variable and only exists within the function body. A variable declared outside a function (with or without let or var) or a variable implicitly declared without let or var inside a function is a global variable, i.e. it exists even after the function execution has finished. Furthermore, if a global variable x is declared and inside a function we declare a local variable x, the local x will hide the global x, meaning that when we use x in the function, it will be the local and not the global one.

If you declare a variable without the var or let keyword, it will always be global, even if declared within a function! This can lead to errors that are hard to detect and should be avoided.

Let’s illustrate this with a few examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let x = 7 // Declare and initialize a global variable.

f1(4) // We can call the normal function before it is defined -> function hoisting.
console.log('x: ' + x) // x is the GLOBAL variable.

function f1(a) {
  let x = a / 2
  console.log(`x: ${x}`) // x is the LOCAL variable.
  // The global variable x is not usable here.
}

let f2 = () => {
  x++ // We modify the global variable x, which is a very bad idea.
}

console.log(`x: ${x}`) // x is the GLOBAL variable.
f2()
console.log(`x: ${x}`) // x is the GLOBAL variable.

let f3 = () => {
  let x = 23
  console.log(`x: ${x}`) // x is the LOCAL variable.
}

f3()
// x is GLOBAL, the local one from f3 does not exist outside of f3.
console.log(`x: ${x}`)

Note in the examples above that we called normal function f1 before its declaration. This behavior is possible because of function hoisting, i.e. a normal function is read at run-time, as mentioned above.

4.3.15.4. let vs var

This first example does not work as expected:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Lexical scoping 1</title>
  <meta charset=utf-8>
  <script type=module>
    const buttons = document.querySelectorAll('button')
    for (var i = 0; i < 3; i++) {
      buttons[i].addEventListener('click', () => {
        console.log(`Button ${i + 1} clicked`)
      })
    }
  </script>
</head>
<body>
  <main>
    <button>B1</button>
    <button>B2</button>
    <button>B3</button>
  </main>
</body>
</html>

The reason is that the scope of var is the entire enclosing function, so the value that will be used in the event handler is the last value of i, which is 3 in this case.

The second example solves this problem using let.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Lexical scoping 2</title>
  <meta charset=utf-8>
  <script type=module>
    const buttons = document.querySelectorAll('button')
    for (let i = 0; i < 3; i++) {
      buttons[i].addEventListener('click', () => {
        console.log(`Button ${i + 1} clicked`)
      })
    }
  </script>
</head>
<body>
  <main>
    <button>B1</button>
    <button>B2</button>
    <button>B3</button>
  </main>
</body>
</html>

let has block scope, which means that the value used in the event handler is the value of i at that point and time in the block, as one would intuitively expect.

4.3.15.5. Anonymous functions

We can pass a nameless function as a parameter to a function or assign it to a variable. This will be particularly useful when we deal with event handlers as we’ll see later on.

Here is an example of an anonymous function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// This function takes as parameter a function and a number.
function f1(f, x) {
  // We call the function x times, each time passing i as parameter.
  for (let i = 0; i < x; i++) f(i)
}

// We call f1 with an anonymous function and a number as parameters.
f1(function (x) {
  console.log(x)
}, 5)

// The same using an anonymous function expression.
f1(x => {
  console.log(x)
}, 5)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
<head>
  <title>x1</title>
  <meta charset=UTF-8>
</head>
<body>
<script type=module>
  let i = 7
  setInterval(() => {
    console.log(i--)
  }, 500)
</script>
</body>
</html>
4.3.15.7. Asynchronous programming

Mixtral-8x7b-32768 answer to the prompt "Explain asynchronous programming in the context of JS":

Asynchronous programming in JavaScript refers to the ability to run code that does not block the execution of other code. This is particularly important in web development, where certain tasks, such as fetching data from a server or reading files from disk, can take a long time to complete.

In synchronous programming, the program executes one line of code at a time, and does not move on to the next line until the current line has finished executing. This can be a problem if one of the lines of code takes a long time to complete, as it can cause the entire program to become unresponsive or slow.

Asynchronous programming in JavaScript allows us to write code that can continue executing while waiting for a long-running task to complete. This is achieved through the use of callbacks, promises, and async/await syntax.

Callbacks are functions that are passed as arguments to other functions, and are called when the long-running task has completed. Promises are objects that represent a value that may not be available yet, but will be resolved or rejected at some point in the future. Async/await is a syntax sugar for working with promises, allowing us to write asynchronous code that looks and behaves like synchronous code.

Here’s an example of asynchronous programming in JavaScript using promises:

fetch('https://api.example.com/data') // fetch data from server
  .then(response => response.json()) // parse JSON data
  .then(data => {
    // process data here
    console.log(data)
  })
  .catch(error => {
    // handle error here
    console.error(error)
  })

In this example, the fetch() function returns a promise that resolves to the response from the server. We then chain a then() method to parse the JSON data, and another then() method to process the data. If there is an error at any point in the chain, the promise is rejected and the catch() method is called.

By using promises, we can write asynchronous code that is easier to read and reason about, and that does not block the execution of other code."."" Asynchronous programming in JavaScript refers to the ability to run code that does not block the execution of other code. This is particularly important in web development, where certain tasks, such as fetching data from a server or reading files from disk, can take a long time to complete.

4.3.15.7.1. Promises
A Promise is an object representing the eventual completion or failure of an asynchronous operation.

developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

hackernoon.com/transforming-callbacks-into-promises-and-back-again-e274c7cf7293

jakearchibald.com/2017/await-vs-return-vs-return-await

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
const URL = 'https://students.btsi.lu'

const httpGETCB = (URL, cb) => {
    const req = new XMLHttpRequest()
    req.addEventListener('load', e => {
        cb(e.target.response)
    })
    req.addEventListener('error', e => {
        cb(new Error(e.target.statusText))
    })
    req.open('GET', URL)
    req.send()
}

console.log('We first call httpGETCB...')
httpGETCB(URL, result => console.dir(`We got the result from httpGetCB`)) //: ${result}`))

const httpGETPromise = URL => {
    return new Promise((resolve, reject) => {
        const req = new XMLHttpRequest()
        req.addEventListener('load', e => {
            resolve(e.target.response)
        })
        req.addEventListener('error', e => {
            reject(new Error(e.target.statusText))
        })
        req.open('GET', URL)
        req.send()
    })
}

console.log('We call httpGETPromise...')
httpGETPromise(URL).then(res => {
        //console.log(`Contents: ${res}`)
        console.log('We got the result from httpGETPromise')
    },
    err => {
        console.log(`Something went wrong: ${err}`)
    }
)

const myFunCB1 = (x, cb) => {
    cb(x + 1)
}
const myFunCB2 = (x, cb) => {
    cb(x * 2)
}
const myFunCB3 = (x, cb) => {
    cb(x / 3)
}
const myFunCB4 = (x, cb) => {
    cb(x - 4)
}

myFunCB1(98,
    r1 => myFunCB2(r1,
        r2 => myFunCB3(r2,
            r3 => myFunCB4(r3,
                r4 => console.log(`CB: ${r4}`)))))
                
const myFunPromise1 = x => {
    return new Promise(resolve => resolve(x + 1))
}

const myFunPromise2 = x => {
    return new Promise(resolve => resolve(x * 2))
}

const myFunPromise3 = x => {
    return new Promise(resolve => resolve(x / 3))
}

const myFunPromise4 = x => {
    return new Promise(resolve => resolve(x - 4))
}

myFunPromise1(98)
    .then(res1 => myFunPromise2(res1))
    .then(res2 => myFunPromise3(res2))
    .then(res3 => myFunPromise4(res3))
    .then(res4 => console.log(`Promise returned ${res4}`))

const r1 = await myFunPromise1(98)
const r2 = await myFunPromise2(r1)
const r3 = await myFunPromise3(r2)
const r4 = await myFunPromise4(r3)
console.log(`await returned ${r4}`)

const timerFun = async (x, resolve, reject) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (x > 5) resolve('All good')
            else reject('Not good')
        }, 2000)
    })
}

const res =  await timerFun(3)
console.log(res)
4.3.15.7.2. async and await
Async/await makes asynchronous code look and behave a little more like synchronous code. This is where all its power lies.
Remember, the await keyword is only valid inside async functions. If you use it outside of an async function’s body, you will get a SyntaxError.

hackernoon.com/6-reasons-why-javascripts-async-await-blows-promises-away-tutorial-c7ec10518dd9

exploringjs.com/es2016-es2017/ch_async-functions.html

developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

ponyfoo.com/articles/understanding-javascript-async-await

stackoverflow.com/questions/42964102/syntax-for-async-arrow-function

medium.com/front-end-hacking/async-await-is-not-about-making-asynchronous-code-synchronous-ba5937a0c11e

exploringjs.com/es2016-es2017/ch_async-functions.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const httpGET = URL => {
  return new Promise((resolve, reject) => {
    const req = new XMLHttpRequest()
    req.addEventListener('load', e => {
      resolve(e.target.response)
    })
    req.addEventListener('error', e => {
      reject(new Error(e.target.statusText))
    })
    req.open('GET', URL)
    req.send()
  })
}

const fun1 = async () => {
  console.log('x1')
  await httpGET('https://students.btsi.lu').then(res => {
      console.log(`Contents: ${res}`)
    },
    err => {
      console.log(`Something went wrong: ${err}`)
    }
  )
  console.log('x2')
}

await fun1()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const resolveAfter2Seconds = x => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const rand = Math.random()
      rand < 0.5 ? resolve(x) : reject(x)
    }, 2000)
  })
}

(async () => {
  try {
    console.log('x1')
    console.log(await resolveAfter2Seconds(10))
    console.log(await resolveAfter2Seconds(15))
    console.log('x2')
  } catch (e) {
    console.error(e)
  }
})()

4.3.16. Debugging

Debugging is a methodical process of finding and reducing the number of bugs, or defects, in a computer program.

The short video at youtu.be/0zWiq8FB3Xg shows how this works in practice using the following HTML document:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Debugging example</title>
  <meta charset=utf-8>
  <script type=module>
    function f1() {
      // A very basic loop.
      for (let i = 0; i < 4; i++) {
        document.body.innerHTML += 'i: ' + i + '<br>'
      }
    }

    function f2() {
      // A very basic loop.
      for (let i = 0; i > 4; i++) {
        document.body.innerHTML += 'i: ' + i + '<br>'
      }
    }

    f1()
    f2()
  </script>
</head>
<body>
</body>
</html>

4.3.17. Arrays

Arrays are ordered value collections. Each value or element has its position, which is called index. The index of the first element is 0 and the position of the last element is the length of the array minus 1. The index is a 32 bit number, so the maximum number of elements in an array is 2^32 - 1. Arrays are dynamic and we do not have to specify any initial size.

4.3.17.1. Creating and iterating

We create an array either as an array literal, which is the recommended way, or using the Array constructor. The latter method is conducive to errors, as explained in the example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const a1 = [] // Create an empty array.
const a2 = [1, 4, 2, 5, 3] // Create an array with 5 numbers.
const a3 = ['Donald', 45, false] // Create an array with 3 elements of different types.
console.log(`Length of a1: ${a1.length}`)
console.log(`Length of a2: ${a2.length}`)
console.log(`Length of a3: ${a3.length}`)
console.log('Elements of a1:')
for (let i = 0; i < a1.length; i++) console.log(a1[i])
console.log('Elements of a2:')
/* This is a dangerous way of looping through an array, as all enumerable properties
 of the array and its prototype object will be returned and the order is not guaranteed
 by the ECMAScript specification.
 */
for (let i in a2) console.log(a2[i])
console.log('Elements of a3:')
/* If the order of the returned elements is not important, we can use the in operator by
 adding a test to exclude inherited enumerable properties.
 */
for (let i in a3) {
  if (!a3.hasOwnProperty(i)) continue
  console.log(a3[i])
}

console.log('Elements of a3:')
// The most concise method is for of
for (const elem of a3) console.log(elem)

const a4 = new Array() // Create an empty array.
/* You need to be careful when creating arrays with new. If there is only one parameter, it
 indicates the number of elements. If there are several parameters, these will be the
 elements of the array. This adds unneeded complexity and encourages errors.
 */
const a5 = new Array(8) // Create an array with no elements and length 8.
// Create an array with 3 elements of different types.
const a6 = new Array('Donald', 'Duck', 45, true)
console.log(`Length of a4: ${a4.length}`)
console.log(`Length of a5: ${a5.length}`)
console.log(`Length of a6: ${a6.length}`)
console.log('Elements of a4:')
for (const elem of a4) console.log(elem)
console.log('Elements of a5 using a normal for:')
for (let i = 0; i < a5.length; i++) console.log(a5[i])
// Note the different behavior: for/in simply skips undefined elements.
console.log('Elements of a5 using for/in:')
for (let i in a5) console.log(a5[i])
console.log('Elements of a6:')
for (const elem of a6) console.log(elem)
To know whether an array contains a specific element use Array.includes().

It is important to note that arrays are objects. Therefore the in operator will use all enumerable properties of the object itself and its prototype, as described in Testing properties. This will include methods that have been added to the prototype unless they are not enumerable. Furthermore, the ECMAScript specification does not fix the order in which the in operator returns elements. If element order is important, we should use a standard for loop to iterate through an array. If order does not matter, but we cannot guarantee that the array prototype is not polluted with enumerable properties, we should include a test to filter these, as shown in the examples above.

The preferred and most concise method to iterate over arrays is for of.
4.3.17.2. Adding and deleting
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const a = []
a[5] = 'Donald'
/* The array has only one element, at index 5, but the length of the array is 6!
   This is called a sparse array. */
console.log(`Contents of array a: ${a} length: ${a.length}`)
a[1] = false
console.log(`Contents of array a: ${a} length: ${a.length}`)
a.push('Duck') // Add an element at the end.
console.log(`Contents of array a: ${a} length: ${a.length}`)
a[a.length] = 23e4 // Another way of adding an element at the end.
console.log(`Contents of array a: ${a} length: ${a.length}`)
delete a[a.length - 1] // Delete the last element. This does not alter the array length!
console.log(`Contents of array a: ${a} length: ${a.length}`)
a.pop() // Remove the last element and return it. This does adjust the length.
console.log(`Contents of array a: ${a} length: ${a.length}`)
console.log('Just removed ' + a.pop())
console.log(`Contents of array a: ${a} length: ${a.length}`)
4.3.17.3. Multidimensional arrays

An array element can be another array. So we can easily create multidimensional arrays like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const init = () => {
  const MATRIXLENGTH = 10 // 10 by 10
// Create an empty 10 x 10 matrix.
  const matrix = new Array(MATRIXLENGTH)
  for (let i = 0; i < MATRIXLENGTH; i++) matrix[i] = new Array(MATRIXLENGTH)
// Fill the matrix with random integers from [0, 99].
  for (let i = 0; i < MATRIXLENGTH; i++)
    for (let j = 0; j < MATRIXLENGTH; j++) matrix[i][j] = Math.floor(100 * Math.random())
// Display the matrix as a table.
  let table = '<table><caption>Random matrix</caption>'
  for (let i = 0; i < MATRIXLENGTH; i++) {
    table += '<tr>'
    for (let j = 0; j < MATRIXLENGTH; j++)
      table += `<td>${matrix[i][j]}</td>`
    table += '</tr>'
  }
  document.body.innerHTML += `${table}</table>`
}

addEventListener('load', init)
4.3.17.4. Methods

We have already seen includes, push and pop methods. A complete reference with examples can be found at www.w3schools.com/jsref/jsref_obj_array.asp.

4.3.17.5. Turning arrays into parameters

Using the spread operator …​ we can turn arrays into parameters. Why would we want to do this? Let’s assume for instance, we want to determine the highest value in an array of numbers. Math.max() does not work on arrays. Using the spread operator, this is no problem: students.btsi.lu/evegi144/WAD/JS/array_spread.html

1
2
3
const arr = []
for (let i = 0; i < 100; i++) arr.push(Math.random())
console.log(`The biggest number is ${Math.max(...arr)}`)
4.3.17.6. Flattening arrays

4.3.18. Objects and classes

In JavaScript, anything that is not a string, number, boolean, null or undefined is an object. Objects are collections of properties (cf. www.ecma-international.org/ecma-262/#sec-object-type). They allow us to regroup data and functions that belong together under one name and to reuse them. One purpose is the representation of real world objects. A good introduction can be found at developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects.

JS comes with a number of standard built-in objects, details of which can be found at developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects.

4.3.18.1. Prototypes
If you have traditional object-oriented programming experience, e.g. with C++ or Java, it is imperative to understand that JS is not class- but prototype-based. To get a good understanding of what the difference is and why it matters study You Don’t Know JS.

Most objects are derived from another object. This other object is called prototype in JavaScript. Object is the basic object in JavaScript from which normally all other objects inherit, directly or indirectly. To determine the prototype of an object, we use Object.getPrototypeOf(), where we pass the object as parameter.

4.3.18.2. Object creation

There are three ways to create objects.

4.3.18.2.1. as literals

From "JavaScript The Definitive Guide" p. 117: "An object literal is a comma-separated list of colon-separated name:value pairs, enclosed within curly braces." this refers to the object itself, whose property we want to use. Object literals are often used to avoid declaring a large number of global variables and functions by creating a namespace. This helps to document that a set of properties belong together and serve a common purpose. When we create an object literal, its prototype is Object.prototype. WMOTU Invaders object-oriented provides a sample application.

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const myMerc = {
  brand: 'Mercedes',
  price: 100000,
  addVAT() {
    this.price = Math.ceil(this.price * 1.15)
  }
}
const myBMW = {
  brand: 'BMW',
  price: 60000,
  addVAT() {
    this.price = Math.ceil(this.price * 1.15)
  }
}

document.body.innerHTML += `Mercedes price without VAT: ${myMerc.price.toLocaleString()}<br>`
myMerc.addVAT()
document.body.innerHTML += `Mercedes price with VAT: ${myMerc.price.toLocaleString()}<br>`
document.body.innerHTML += `BMW price without VAT: ${myBMW.price.toLocaleString()}<br>`

const empty = {} // We create an empty object the REFERENCE to which is saved in this
// constant.
// We save the REFERENCE to object empty in constant myDog. This means that empty and myDog
// point to the same object!
const myDog = empty
myDog.name = 'Idefix' // We give the myDog, which is the same as the empty, object a name
myDog.bark = () => { // and a function.
  document.body.innerHTML += 'Wouf wouf!<br>'
}

document.body.innerHTML += `myDog's name is ${myDog.name}<br>`
myDog.bark()
document.body.innerHTML += `Objects myDog and empty are identical: ${myDog === empty}<br>`
document.body.innerHTML += `empty's name is ${empty.name}<br>`
empty.bark()

const doggy = Object.create(myDog)
doggy.bark()
myDog.name = 'Toto'
document.body.innerHTML += `empty's name is ${empty.name}<br>`
document.body.innerHTML += `myDog's name is ${myDog.name}<br>`
document.body.innerHTML += `doggy's name is ${doggy.name}<br>`
console.log('Prototype of empty: ')
console.dir(Object.getPrototypeOf(empty))
console.log('Prototype of myDog: ')
console.dir(Object.getPrototypeOf(myDog))
console.log('Prototype of doggy: ')
console.dir(Object.getPrototypeOf(doggy))
4.3.18.2.2. from a constructor with new

new must be followed by a constructor, which is a function that initializes the newly created object. As noted above, this refers to the object itself, whose property we want to use. The advantage of this approach compared to the previous one is that we can create as many objects as we like using the same constructor. When we use a constructor, the prototype is the prototype property of the constructor. Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Car(brand, price) {
  this.brand = brand
  this.price = price
  this.addVAT = function () {
    this.price = Math.ceil(this.price * 1.15)
  }
}

const myMerc = new Car('Mercedes', 100000), myBMW = new Car('BMW', 60000)

document.body.innerHTML += `Mercedes price without VAT: ${myMerc.price.toLocaleString()}<br>`
myMerc.addVAT()
document.body.innerHTML += `Mercedes price with VAT: ${myMerc.price.toLocaleString()}<br>`
document.body.innerHTML += `BMW price without VAT: ${myBMW.price.toLocaleString()}<br>`
myMerc.price = 80000 // This only changes the price of myMerc, but not myBMW.
document.body.innerHTML += `Mercedes price without VAT: ${myMerc.price.toLocaleString()}<br>`
document.body.innerHTML += `BMW price without VAT: ${myBMW.price.toLocaleString()}<br>`
document.body.innerHTML += `Prototype of myMerc: ${Object.getPrototypeOf(myMerc)}<br>`
console.log('Prototype of myMerc: ')
console.dir(Object.getPrototypeOf(myMerc))
document.body.innerHTML += `Prototype of myBMW: ${Object.getPrototypeOf(myBMW)}<br>`
console.log('Prototype of myBMW: ')
console.dir(Object.getPrototypeOf(myBMW))

Or using the syntactic sugar introduced in ECMAScript 6: students.btsi.lu/evegi144/WAD/JS/objects_new2.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Car {
  constructor(brand, price) {
    this.brand = brand
    this.price = price
  }

  addVAT() {
    this.price = Math.ceil(this.price * 1.15)
  }
}

const myMerc = new Car('Mercedes', 100000), myBMW = new Car('BMW', 60000)

document.body.innerHTML += `Mercedes price without VAT: ${myMerc.price.toLocaleString()}<br>`
myMerc.addVAT()
document.body.innerHTML += `Mercedes price with VAT: ${myMerc.price.toLocaleString()}<br>`
document.body.innerHTML += `BMW price without VAT: ${myBMW.price.toLocaleString()}<br>`
myMerc.price = 80000 // This only changes the price of myMerc, but not myBMW.
document.body.innerHTML += `Mercedes price without VAT: ${myMerc.price.toLocaleString()}<br>`
document.body.innerHTML += `BMW price without VAT: ${myBMW.price.toLocaleString()}<br>`
document.body.innerHTML += `Prototype of myMerc: ${Object.getPrototypeOf(myMerc)}<br>`
console.log('Prototype of myMerc: ')
console.dir(Object.getPrototypeOf(myMerc))
document.body.innerHTML += `Prototype of myBMW: ${Object.getPrototypeOf(myBMW)}<br>`
console.log('Prototype of myBMW: ')
console.dir(Object.getPrototypeOf(myBMW))
4.3.18.2.3. from a prototype with Object.create()

Almost all JavaScript objects inherit the properties of another object, their prototype. Object.create is a method that allows us to create a new object with a given prototype.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const myMerc = Object.create({
  brand: 'Mercedes',
  price: 100000,
  addVAT() {
    this.price = Math.ceil(this.price * 1.15)
  }
})

const myBMW = Object.create({
  brand: 'BMW',
  price: 60000,
  addVAT() {
    this.price = Math.ceil(this.price * 1.15)
  }
})

document.body.innerHTML += `Mercedes price without VAT: ${myMerc.price.toLocaleString()}<br>`
myMerc.addVAT()
document.body.innerHTML += `Mercedes price with VAT: ${myMerc.price.toLocaleString()}<br>`
document.body.innerHTML += `BMW price without VAT: ${myBMW.price.toLocaleString()}<br>`
myMerc.price = 80000 // This only changes the price of myMerc, but not myBMW.
document.body.innerHTML += `Mercedes price without VAT: ${myMerc.price.toLocaleString()}<br>`
document.body.innerHTML += `BMW price without VAT: ${myBMW.price.toLocaleString()}<br>`
document.body.innerHTML += 'The prototype of myBMW has the following properties:<br>'
for (let o in Object.getPrototypeOf(myBMW)) document.body.innerHTML += `${o}<br>`
document.body.innerHTML += `Prototype of myBMW: ${Object.getPrototypeOf(myBMW)}<br>`
console.log('Prototype of myBMW: ')
console.dir(Object.getPrototypeOf(myBMW))

It is important to understand the difference between a constructor and a prototype. The prototype of an object contains all the properties that are common to all objects that have this prototype. They exist only once in the browser memory. Thus any changes to any of these properties are automatically reflected in all the objects that inherit from this prototype! Take a close look at the following examples:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
const animal = {
  speciesName: '',
  sayHello() {
    document.body.innerHTML += `Hi, I am a ${this.speciesName}<br>`
  }
}

// Object animal is the prototype of objects myDog and myCat.
const myDog = Object.create(animal), myCat = Object.create(animal)
animal.sayHello()
myDog.sayHello()
myCat.sayHello()
// If we change a property of the prototype, it will affect all objects with this prototype.
animal.speciesName = 'dog'
animal.sayHello()
myDog.sayHello()
myCat.sayHello()

// Now let's use constructors.
// Each object constructed with this constructor will have its own name and bark properties.
function Dog(name) {
  this.name = name
  this.bark = () => {
    document.body.innerHTML += `${this.name} says wouf wouf!<br>`
  }
}

function Cat(name) {
  this.name = name
  this.miaow = function () {
    document.body.innerHTML += `${this.name} says mmmmiiiiaaaaoooowwww!<br>`
  }
}

class Ape {
  constructor(name) {
    this.name = name
  }

  scream() {
    document.body.innerHTML += `${this.name} screams Ouaaaaaaaahhhhhhhh!<br>`
  }

  crackNut() {
    document.body.innerHTML += `${this.name} cracked a nut.<br>`
  }
}

class Gorilla extends Ape {
  constructor(name) {
    super(name)
  }

  scream() {
    document.body.innerHTML += `${this.name} screams I am BOSS!!!<br>`
  }
}

const Toto = new Gorilla('Toto')
Toto.scream()
Toto.crackNut()
Ape.prototype.crackNut = function () {
  document.body.innerHTML += `${this.name} smashed a nut.<br>`
}
Toto.crackNut()

// Create 2 new dogs using the dog constructor.
let myDog1 = new Dog('Bello'), myDog2 = new Dog('Toro')
// Create 2 new cats using the cat constructor.
let myCat1 = new Cat('Kitty'), myCat2 = new Cat('Tiger')
myDog1.bark()
myDog2.bark()
myCat1.miaow()
myCat2.miaow()
myDog2.name = 'Idefix' // Change a property of a dog object.
myDog1.bark() // The change only affects the other dog object.
myDog2.bark()

// Now let's combine constructor and prototype.
// All dogs and cats will have the same species_name and sayHello properties
Dog.prototype = animal
Cat.prototype = animal
// Create 2 new dogs using the dog constructor.
myDog1 = new Dog('Bello')
myDog2 = new Dog('Toro')
// Create 2 new cats using the cat constructor.
myCat1 = new Cat('Kitty')
myCat2 = new Cat('Tiger')

// Assign prototype.
myDog1.sayHello()
myDog2.sayHello()
myCat1.sayHello()
myCat2.sayHello()
animal.speciesName = 'bird'
myDog1.sayHello()
myDog2.sayHello()
myCat1.sayHello()
myCat2.sayHello()
myDog1.bark()
myDog2.bark()
myCat1.miaow()
myCat2.miaow()
console.log('Prototype of myDog1:')
console.dir(Object.getPrototypeOf(myDog1))
console.log('Prototype of myDog2:')
console.dir(Object.getPrototypeOf(myDog2))
console.log('Prototype of myCat1:')
console.dir(Object.getPrototypeOf(myCat1))
console.log('Prototype of myCat2:')
console.dir(Object.getPrototypeOf(myCat2))
4.3.18.4. this

Mastery of the this keyword is a key requirement for understanding JS objects. You should therefore study the excellent explanation at developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this.

4.3.18.5. Getting and setting properties

To get or set properties of an object we can either use the dot (.) or the square bracket ([]) operators. There is an important difference between these two: when using the dot operator, the right-hand must be an identifier, i.e. the name of the property. This cannot be a programmatically generated dynamic value. When using the square bracket operator, the value between the brackets must be an expression that evaluates to a string or to something that can be converted to a string. This opens up endless possibilities, so let’s look at a few examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// The [] operator allows us to use names with spaces as property names:
const company = {
  'chief executive officer': 'Donald Duck'
}

//document.innerHTML += `${company.chief executive officer}<br>`; // Does not work!
document.innerHTML += `${company['chief executive officer']}<br>` // Works!


class Student {
  constructor(name) {
    this.name = name
    this.branches = {}
    // If function already defined, no need to define it again. This check is optional.
    if (!Student.prototype.addBranch)
      Student.prototype.addBranch = (name, average) => {
        this.branches[name] = average
        console.dir(this)
      }
    // If function already defined, no need to define it again. This check is optional.
    if (!Student.prototype.getTotalAverage)
      Student.prototype.getTotalAverage = () => {
        let avg = 0, count = 0
        for (let branch in this.branches) {
          avg += this.branches[branch]
          count++
        }
        if (count) return avg / count
        else return undefined
      }
  }
}

const st1 = new Student('Bill'), st2 = new Student('Bob')
document.body.innerHTML += `${st1.name} average: ${st1.getTotalAverage()}<br>`
st1.addBranch("INFOR", 49)
document.body.innerHTML += `${st1.name} average: ${st1.getTotalAverage()}<br>`
st1.addBranch("MATHE", 34)
document.body.innerHTML += `${st1.name} average: ${st1.getTotalAverage()}<br>`
st2.addBranch("INFOR", 57)
document.body.innerHTML += `${st2.name} average: ${st2.getTotalAverage()}<br>`
st2.addBranch("MATHE", 44)
document.body.innerHTML += `${st2.name} average: ${st2.getTotalAverage()}<br>`
4.3.18.6. Deleting properties

delete removes a property from an object. If you invoke delete on a prototype property, then all objects inheriting from this prototype lose this property.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Dog {
  constructor(name) {
    this.name = name
    this.bark = () => {
      document.body.innerHTML += `${this.name} says wouf wouf!<br>`
    }
  }
}

const myDog1 = new Dog('Bello')
myDog1.bark()
delete myDog1.name
myDog1.bark()
const myDog2 = new Dog('Idefix')
myDog2.bark()
4.3.18.7. Testing properties

We have different options to test whether an object has a given property:

  1. The in operator requires the name of a property as a string on its left and an object on its right side. It returns true if the object has an enumerable own or inherited property by that name (cf. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in).

  2. Object.keys(obj) returns an array of a given object’s own enumerable properties (cf. https://developer.mozilla .org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys).

  3. Object.getOwnPropertyNames(obj) returns an array of all properties (enumerable or not) found directly upon a given object (cf. https://developer.mozilla .org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames).

  4. The hasOwnProperty() of an object checks whether the object has a property of the given name. It does not consider inherited properties for which it returns false (cf. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object /hasOwnProperty).

  5. propertyIsEnumerable() returns a Boolean indicating whether the specified property is enumerable (cf. https://developer.mozilla .org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable).

The following examples illustrate the different approaches:

1
2
3
4
5
6
7
8
9
const proto1 = {
  a: 'a',
  b: 'b'
}

const obj1 = Object.create(proto1)
for (const prop in obj1) console.dir(prop)
console.dir(Object.keys(obj1))
console.dir(Object.getOwnPropertyNames(obj1))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Dog {
  constructor(name) {
    this.name = name
    this.bark = () => {
      document.write(`${this.name} says wouf wouf!<br>`)
    }
  }
}

const myDog1 = new Dog('Bello')
document.body.innerHTML += `toString is a property of myDog1: ${"toString" in myDog1}<br>`

let keys = Object.keys(myDog1) // Get the enumerable own properties of the object.
let output = ''
for (let i = 0; i < keys.length; i++) // Display the property/value pairs.
  output += `${keys[i]}: ${myDog1[keys[i]]}<br>`
document.body.innerHTML += `<p>${output}</p>`

const s = [1, 2, 3, 4, 5] // Create an array object.
keys = Object.keys(s) // Get the enumerable own properties of the object.
output = ''
for (let i = 0; i < keys.length; i++) // Display the property/value pairs.
  if (s.propertyIsEnumerable(keys[i])) output += `Enumerable ${keys[i]}: ${s[keys[i]]}<br>`
document.body.innerHTML += `<p>${output}</p>`

const props = Object.getOwnPropertyNames(s) // Get all own properties of the object.
output = ''
for (let i = 0; i < props.length; i++) {
  if (s.propertyIsEnumerable(props[i])) output += 'Enumerable '
  output += `${props[i]}: ${s[props[i]]}<br>`
}
document.body.innerHTML += `<p>${output}</p>`
4.3.18.8. Optional chaining
The optional chaining operator (?.) enables you to read the value of a property located deep within a chain of connected objects without having to check that each reference in the chain is valid. The ?. operator is like the . chaining operator, except that instead of causing an error if a reference is nullish (null or undefined), the expression short-circuits with a return value of undefined. When used with function calls, it returns undefined if the given function does not exist.
4.3.18.9. Object attributes

Every object has prototype, class and extensible attributes.

4.3.18.9.1. prototype

This attribute specifies the object’s parent, i.e. the object that it inherits from. As we’ve seen above, for object literals the prototype is Object.prototype, for objects created with new the prototype is the value of the constructor’s prototype property and for objects created with Object.create, the prototype is the first parameter passed to this method.

Every object has an isPrototypeOf method, which checks whether the object if the prototype of another object passed as parameter.

4.3.18.9.2. class

This internal read-only attribute tells us the type of the object. This attribute can take one of the following values: "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp" or "String".

4.3.18.9.3. extensible

This attribute determines whether new properties can be added to the object. Object.isExtensible() tells us whether an object is extensible or not. Object.preventExtensions allows us to make an object nonextensible.

Here is an example that illustrates the three object attributes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const animal = {
  species: 'animal',
  displaySpecies() {
    document.body.innerHTML += `I'm a ${this.species}<br>`
  }
}
const dog = Object.create(animal)
document.body.innerHTML += `Animal is the prototype of dog: ${animal.isPrototypeOf(dog)}<br>`
document.body.innerHTML += `The class of animal is:
  ${Object.prototype.toString.call(animal).slice(8, -1)}<br>`
console.dir(Object.getPrototypeOf(animal))
document.body.innerHTML += `The class of dog is:
  ${Object.getPrototypeOf(dog).toString().slice(8, -1)}<br>`
console.dir(Object.getPrototypeOf(dog))
document.body.innerHTML += `animal is extensible: ${Object.isExtensible(animal)}<br>`
Object.preventExtensions(animal)
document.body.innerHTML += `animal is extensible: ${Object.isExtensible(animal)}<br>`
document.body.innerHTML += `dog is extensible: ${Object.isExtensible(dog)}<br>`
4.3.18.10. Property attributes

Each object property has, in addition to its value, the following three attributes:

  1. configurable: true if and only if the type of this property may be changed and if the property may be deleted from the corresponding object.

  2. enumerable

  3. writable.

Using Object.defineProperty(obj, prop, descriptor) we can set these attributes (cf. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object /defineProperty).

For a more in-depth treatment, see 2ality.com/2019/11/object-property-attributes.html.

4.3.18.11. Closures
A closure is a special kind of object that combines two things: a function, and the environment in which that function was created. The environment consists of any local variables that were in-scope at the time that the closure was created.

Study the MDN article, it explains the subject in detail with good examples. Then take a look at bonsaiden.github.io/JavaScript-Garden.

4.3.18.12. ECMAScript 6 and beyond

ECMAScript 6 introduces more convenient syntax to work with objects and classes. Highly recommended introductions can be found at the following links:

scotch.io/tutorials/better-javascript-with-es6-pt-ii-a-deep-dive-into-classes

developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

exploringjs.com/es6/ch_classes.html

www.sitepoint.com/object-oriented-javascript-deep-dive-es6-classes

shiroyasha.io/oop-in-the-age-of-es6.html

github.com/getify/You-Dont-Know-JS/tree/1st-ed/this%20&%20object%20prototypes

www.ibm.com/developerworks/library/wa-ecmascript6-neward-p3

students.btsi.lu/evegi144/WAD/WAD.html#_call_apply_and_bind

ECMAScript 2022 has introduced further enhancements, in particular new class elements, cf. tc39.es/ecma262/multipage/#sec-intro.
Classes are always strict mode code. Calling methods with an undefined this will throw an error (cf. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this).
4.3.18.13. Private instance and prototype members

Before ECMAScript 2022, implementing private instances and prototypes could be done as shown below.

Then read one of the following:

css-tricks.com/implementing-private-variables-in-javascript

www.sitepoint.com/javascript-private-class-fields

medium.com/@weberino/you-can-create-truly-private-properties-in-js-without-es6-7d770f55fbc3

curiosity-driven.org/private-properties-in-javascript

Here are two examples of a simple web shop, one using public and the other one using private class attributes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Article {
  constructor(id, name, price) {
    this.id = id
    this.name = name
    this.price = price
  }
}

const shop = {
  articles: [],
  addArticle(article) {
    this.articles.push(article)
  },
  removeArticle(id) {
    for (let i = 0; i < this.articles.length; i++)
      if (this.articles[i].id === parseInt(id)) {
        this.articles.splice(i, 1)
        break
      }
  },
  display() {
    const ul = document.querySelector('ul')
    ul.innerHTML = ''
    for (const article of this.articles) {
      let li = document.createElement('li')
      li.id = article.id
      li.addEventListener('click', e => {
        this.removeArticle(e.target.id)
        this.display()
      })
      li.innerHTML = article.id + ' ' + article.name + ' ' + article.price + ' '
      ul.appendChild(li)
    }
  }
}

const a1 = new Article(1, 'art1', 132.45, '')
const a2 = new Article(2, 'art2', 12.5, '')
const a3 = new Article(3, 'art3', 13562.495, '')
shop.addArticle(a1)
shop.addArticle(a2)
shop.addArticle(a3)
shop.display()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class Article {
  constructor(pId, pName, pPrice) {
    let id = pId
    let name = pName
    let price = pPrice

    this.getId = () => {
      return id
    }

    this.setId = pId => {
      id = pId
    }

    this.getName = () => {
      return name
    }

    this.setName = pName => {
      name = pName
    }

    this.getPrice = () => {
      return price
    }

    this.setPrice = pPrice => {
      price = pPrice
    }
  }
}

class Shop {
  constructor() {
    const articles = []

    this.addArticle = article => {
      articles.push(article)
    }

    this.removeArticle = id => {
      for (let i = 0; i < articles.length; i++)
        if (articles[i].getId() === parseInt(id)) {
          articles.splice(i, 1)
          break
        }
    }

    this.display = () => {
      const ul = document.querySelector('ul')
      ul.innerHTML = ''
      for (const article of articles) {
        let li = document.createElement('li')
        li.id = article.getId()
        li.addEventListener('click', e => {
          this.removeArticle(e.target.id)
          this.display()
        })
        li.innerHTML = article.getId() + ' ' + article.getName() + ' '
          + article.getPrice() + ' '
        ul.appendChild(li)
      }
    }
  }
}

const shop = new Shop()
const a1 = new Article(1, 'art1', 132.45)
const a2 = new Article(2, 'art2', 12.5)
const a3 = new Article(3, 'art3', 13562.495)
shop.addArticle(a1)
shop.addArticle(a2)
shop.addArticle(a3)
shop.display()

Here is a more advanced example, illustrating how we can have both private prototype and private object members and inheritance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class Base {
  constructor() {
    let instance_day = "Tuesday" // Private instance property.

    this.getInstanceDay = () => { // Public instance property getter.
      return instance_day
    }

    this.setInstanceDay = day => { // Public instance property setter.
      instance_day = day
    }

    // Private prototype variable.
    let dayName = "Friday"

    // Private prototype methods
    const getPrototypeDayName = () => {
      return dayName
    }

    const setPrototypeDayName = day => {
      dayName = day
    }

    if (!Base.prototype.getDayName)
      Base.prototype.getDayName = () => {
        return getPrototypeDayName()
      }

    if (!Base.prototype.setDayName)
      Base.prototype.setDayName = day => {
        setPrototypeDayName(day)
      }
  }
}

class SubType extends Base {
  constructor() {
    super()
  }
}

const a = new Base(), b = new Base(), c = new SubType(), d = new SubType()
// Returns "Tuesday"
document.body.innerHTML += `${a.getInstanceDay()}<br>`

// Returns "Tuesday"
document.body.innerHTML += `${b.getInstanceDay()}<br>`

// Sets dayName to "Wednesday"
a.setInstanceDay("Wednesday")

// Returns "Wednesday"
document.body.innerHTML += `${a.getInstanceDay()}<br>`

// Returns "Tuesday"
document.body.innerHTML += `${b.getInstanceDay()}<br>`

document.body.innerHTML += `${a.getDayName()}<br>`
document.body.innerHTML += `${b.getDayName()}<br>`
a.setDayName('Saturday')
document.body.innerHTML += `${a.getDayName()}<br>`
document.body.innerHTML += `${b.getDayName()}<br>`

// Returns "Tuesday"
document.body.innerHTML += `${c.getInstanceDay()}<br>`

// Returns "Tuesday"
document.body.innerHTML += `${d.getInstanceDay()}<br>`

// Sets dayName to "Wednesday"
c.setInstanceDay("Wednesday")

// Returns "Wednesday"
document.body.innerHTML += `${c.getInstanceDay()}<br>`

// Returns "Tuesday"
document.body.innerHTML += `${d.getInstanceDay()}<br>`

document.body.innerHTML += `${c.getDayName()}<br>`
document.body.innerHTML += `${d.getDayName()}<br>`

Now let’s look how the same examples can be implementeed in ECMAScript 2022. Make sure to study developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_Classes#private_fields.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class Article {
  #id
  #name
  #price

  constructor(id, name, price) {
    this.#id = id
    this.#name = name
    this.#price = price
  }

  get id() {
    return this.#id
  }

  set id(id) {
    this.#id = id
  }

  get name() {
    return this.#name
  }

  set name(name) {
    this.#name = name
  }

  get price() {
    return this.#price
  }

  set price(price) {
    this.#price = price
  }
}

class Shop {
  #articles = []

  addArticle(article) {
    this.#articles.push(article)
  }

  removeArticle(id) {
    for (let i = 0; i < this.#articles.length; i++)
      if (this.#articles[i].getId() === parseInt(id)) {
        this.#articles.splice(i, 1)
        break
      }
  }

  display() {
    const ul = document.querySelector('ul')
    ul.innerHTML = ''
    for (const article of this.#articles) {
      let li = document.createElement('li')
      li.id = article.id
      li.addEventListener('click', e => {
        this.removeArticle(e.target.id)
        this.display()
      })
      li.innerHTML = article.id + ' ' + article.name + ' ' + article.price + ' '
      ul.appendChild(li)
    }
  }
}

const shop = new Shop()
const a1 = new Article(1, 'art1', 132.45)
const a2 = new Article(2, 'art2', 12.5)
const a3 = new Article(3, 'art3', 13562.495)
shop.addArticle(a1)
shop.addArticle(a2)
shop.addArticle(a3)
shop.display()

And here the more advanced example. Note that in order to access the private prototype properties we need to use the class and not the object:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Base {
  #instanceDay = "Tuesday" // Private instance property
  static #dayName = "Friday" // Private prototype property

  get instanceDay() { // Public instance property getter.
    return this.#instanceDay
  }

  set instanceDay(instanceDay) { // Public instance property setter.
    this.#instanceDay = instanceDay
  }

  static get dayName() { // Public prototype property getter.
    return this.#dayName
  }

  static set dayName(dayName) { // Public prototype property setter.
    this.#dayName = dayName
  }
}

class SubType extends Base {
  constructor() {
    super()
  }
}

const a = new Base(), b = new Base(), c = new SubType(), d = new SubType()
// Returns "Tuesday"
document.body.innerHTML += `${a.instanceDay}<br>`

// Returns "Tuesday"
document.body.innerHTML += `${b.instanceDay}<br>`

// Sets instanceDay to "Wednesday"
a.instanceDay = "Wednesday"

// Returns "Wednesday"
document.body.innerHTML += `${a.instanceDay}<br>`

// Returns "Tuesday"
document.body.innerHTML += `${b.instanceDay}<br>`

document.body.innerHTML += `${Base.dayName}<br>`
Base.dayName = 'Saturday'
document.body.innerHTML += `${Base.dayName}<br>`

// Returns "Tuesday"
document.body.innerHTML += `${c.instanceDay}<br>`

// Returns "Tuesday"
document.body.innerHTML += `${d.instanceDay}<br>`

// Sets instanceDay to "Wednesday"
c.instanceDay = "Wednesday"

// Returns "Wednesday"
document.body.innerHTML += `${c.instanceDay}<br>`

// Returns "Tuesday"
document.body.innerHTML += `${d.instanceDay}<br>`
4.3.18.14. call, apply and bind

Given that functions are objects in JavaScript, these three methods allow us to call a function as if it were a method of another object. Detailed descriptions of the Function object and examples can be found at developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function printProperties() {
  document.body.innerHTML += 'Here come the properties...<br>'
  for (let p in this) document.body.innerHTML += `Property ${p}<br>`
}

const o = {
  x: 1
}

printProperties()
printProperties.call(o)
4.3.18.15. Object

The global Object object has a number of properties. The details can be found at developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object. For instance, if we want to determine the number of an object’s own enumerable properties, we can use Object.keys(object).

4.3.19. Events

So far we have seen how to write scripts that execute sequentially, i.e. instructions are processed one after the other. In real life, however, our browser spends most of its time waiting for something to happen, e.g. a key or mouse button is pressed, a picture is loaded, a given timespan has expired, etc. This "something" is called an event in JavaScript. See the references below for the full list of available events.

For our scripts to be able to react to an event, we need to declare functions, called event listeners or handlers, that are invoked by the browser when a specific event occurs. Our handler will be automatically passed an Event object (cf. references).

www.w3schools.com/jsref/dom_obj_event.asp

developer.mozilla.org/en-US/docs/Web/Events

developer.mozilla.org/en-US/docs/Web/API/Event

eloquentjavascript.net/15_event.html

www.javascriptkit.com/script/script2/xeye.shtml

4.3.19.1. Registering event listeners

In order to tell the browser what it should do if an event occurs, we need to register an event listener. There are 3 ways to do this.

4.3.19.1.1. addEventListener

The recommended way is to use the addEventListener method of the object that will be the target of the event. The first parameter is a string containing the name of the event, the second one will be either an anonymous function or the name (without apostrophes) of the function that will handle the event. The optional third parameter allows to have the listener triggered already in the capture phase as described below. We can register several listeners for the same event using this method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Handling events using addEventListener</title>
    <meta charset=utf-8>
    <script src=event_listeners1.js type=module></script>
  </head>
  <body>
    <main>
      <button>Click me!</button>
    </main>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const clickHandler1 = () => {
  alert("I'm the first click event handler")
}

const clickHandler2 = (event, text) => { // This is the function that will handle the click
                                         // event.
  console.dir(event)
  alert(`I'm the second click event handler. You asked me to show this text: ${text}`)
}

const myButton = document.querySelector('button') // Get the button element from the DOM.
/* Register the first button click handler.
 Note that clickHandler1 is just a pointer to the function.
 This works because clickHandler1 takes no parameter except the event, which it receives
 by default. */
myButton.addEventListener('click', clickHandler1)
/* Register the button's second click event handler using an anonymous function.
 The function receives the event as a parameter by default.
 Our event handler takes a text to display as second parameter.
 To pass this to our event handler, we need to use an anonymous function.
 */

myButton.addEventListener('click', event => {
  clickHandler2(event, 'some random text')
})
4.3.19.1.2. Object property

Another option for event listener registration is the use of the corresponding object property. The name of this property is "on" followed by the event name, e.g. "onclick", "onload" etc. If a handler for this event has already been registered using this approach, it will be replaced:

1
2
3
4
5
6
const handleClick = event => { // This is the function that will handle the click event.
  alert('You clicked me!')
}

const myButton = document.querySelector('button') // Get the button element from the DOM.
myButton.onclick = handleClick // Register the click event handler as an object property.
4.3.19.1.3. HTML attribute

The third and least preferable registration method is the use of an HTML attribute. This way should be avoided as it violates the separation of content and behavior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Handling events using HTML attributes</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <button onclick="alert('You clicked me!');">Click me!</button>
    </main>
  </body>
</html>
4.3.19.1.4. Invocation order

Handlers registered via an object property or HTML attribute are always executed first. Handlers registered with addEventListener are called in the order in which they were registered.

4.3.19.2. Removing event listeners

To remove event listeners registered via object property or HTML attribute, in theory we could use the removeEventListener method. However, this can only be used for event listeners added via addEventListener and is not straightforward for anonymous event listeners.

An easy way to remove all event listeners is to assign the innerHTML of the container to itself or to assign outerHTML of the element to itself.

The following example shows how to remove all event listeners and also illustrates, that you can only have an event listener added via object property or HTML attribute, but not both at the same time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Removing event listeners</title>
    <meta charset=utf-8>
    <script src=event_listeners4.js type=module></script>
  </head>
  <body>
    <main>
      <button onclick = "alert('You clicked me!')">Click me!</button>
      <button>Remove event listeners</button>
    </main>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const clickHandler1 = () => {
  alert("I'm the first click event handler")
}

const clickHandler2 = (event, text) => { // This is the function that will handle the click
                                         // event.
  console.dir(event)
  alert(`I'm the second click event handler. You asked me to show this text: ${text}`)
}

const buttons = document.querySelectorAll('button') // Get button elements from the DOM.
/* Register the first button click handler.
 Note that clickHandler1 is just a pointer to the function.
 This works because clickHandler1 takes no parameter except the event, which it receives
 by default. */
buttons[0].addEventListener('click', clickHandler1)

/* Register the button's second click event handler using an anonymous function.
 The function receives the event as a parameter by default.
 Our event handler takes a text to display as second parameter.
 To pass this to our event handler, we need to use an anonymous function.
 */
buttons[0].addEventListener('click', event => {
  clickHandler2(event, 'some random text')
})

const handler = () => {
  console.log("I'm the third event handler.")
}
// Register the button's third event listener
buttons[0].addEventListener('click', handler)

// Register event listener via object property. Overwrites the one set via HTML attribute.
buttons[0].onclick = () => {
  alert("I'm the object property handler")
}

// This button removes the event listeners.
// Using removeEventListener we could not easily remove anonymous event listeners.
buttons[1].addEventListener('click', () => {
  /*buttons[0].removeEventListener('click', clickHandler1)
  buttons[0].removeEventListener('click', handler)
  alert('First and third event listeneers removed')*/
  const button = document.querySelector('button')
  // Cf. https://stackoverflow.com/questions/3106605/removing-an-anonymous-event-listener
  button.outerHTML = button.outerHTML
  document.querySelector('button').removeAttribute('onclick')
  console.log('Event listeners removed')
})
4.3.19.3. Event flow

The chart at www.w3.org/TR/DOM-Level-3-Events/#dom-event-architecture illustrates the three event phases: capture, target and bubble. If the target of an event is a standalone object (e.g. window), only the event handler on that object will be triggered. If the target object is a tree, for instance the document element, most, but not all, events bubble up the tree as illustrated in the chart. This avoids the need to register event handlers on lots of different elements. We can register a single event handler at the top of the tree, which can deal with the events triggered on its children. If we set the third parameter of addEventListener to true, events are captured before reaching the target object. As seen in the chart, this is the inverse process of bubbling. It provides an opportunity for the parent objects to analyze the event before it reaches the target. This is useful in certain situations, for instance for the implementation of drag and drop, as we’ll see in Drag and drop:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Event propagation with capturing demo</title>
  <meta charset=utf-8>
  <style>
      body {
          background-color: black;
      }

      main {
          background-color: gold;
      }

      p {
          background-color: green;
      }
  </style>
  <script type=module>
    /* Note that we use event CAPTURING in all cases here to illustrate the concept.
       We would normally leave out the third parameter of addEventListener.
     */
    const mouseHandler = event => {
      console.dir(this)
      console.log(`mouseHandler current event target: ${event.currentTarget}`)
    }

    window.addEventListener('click', mouseHandler, true)
    document.body.addEventListener('click', mouseHandler, true)
    document.getElementsByTagName('main')[0].addEventListener('click', mouseHandler, true)
    document.getElementsByTagName('p')[0].addEventListener('click', mouseHandler, true)
  </script>
</head>
<body>
<main>
  <p>This is a paragraph</p>
</main>
</body>
</html>

The following example is identical to the previous one, except that the third parameter of addEventListener is not set: students.btsi.lu/evegi144/WAD/JS/events2.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Event propagation without capturing demo</title>
  <meta charset=utf-8>
  <style>
      body {
          background-color: black;
      }

      main {
          background-color: gold;
      }

      p {
          background-color: green;
      }
  </style>
  <script type=module>
    // Note that we don't use event CAPTURING.
    const mouseHandler = event => {
      console.dir(this)
      console.log(`mouseHandler current event target: ${event.currentTarget}`)
    }

    window.addEventListener('click', mouseHandler)
    document.body.addEventListener('click', mouseHandler)
    document.getElementsByTagName('main')[0].addEventListener('click', mouseHandler)
    document.getElementsByTagName('p')[0].addEventListener('click', mouseHandler)
  </script>
</head>
<body>
<main>
  <p>This is a paragraph</p>
</main>
</body>
</html>
4.3.19.3.1. Preventing event propagation and/or default actions

In some cases we might want to prevent the default action for a given event to occur, for instance, in order to prevent a form from being submitted when the user clicks the submit button. For this purpose we can call the preventDefault method. We can also stop event propagation using stopPropagation. In most cases our event handlers will not return a value. However, if we want to prevent default action and stop propagation, we can return false in our event handler. This works only if the event handler has been registered as an object property or an HTML attribute. With addEventListenener we can call the preventDefault and/or stopPropagation methods:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Preventing event propagation and default action</title>
  <meta charset=utf-8>
  <script type=module>
    document.forms[2].addEventListener('submit', e => {
      e.preventDefault()
      //e.stopPropagation();
      console.log('Form 3 not submitted')
    })
  </script>
</head>
<body>
<main>
  <form action=f1.php onsubmit="console.log('Form 1 submitted')">
    <input name=i1>
    <input type=submit name=s1 value=Send>
  </form>
  <form action=f2.php onsubmit="console.log('Form 2 not submitted'); return false">
    <input name=i2>
    <input type=submit name=s2 value=Send>
  </form>
  <form action=f3.php>
    <input name=i3>
    <input type=submit name=s3 value=Send>
  </form>
</main>
</body>
</html>
4.3.19.4. Keyboard events

When you press a key down, a keydown event is triggered. If you keep the key pressed, keydown and keypress events will continue to be generated at regular time intervals until you release the key, in which case a keyup event will be triggered. The following program displays all the attributes of the key events:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Key event handler example 1</title>
  <meta charset=utf-8>
  <style>
      table {
          border-collapse: collapse;
          width:           100%;
          padding:         5px;
      }

      table, th, td {
          border:     1px solid red;
          text-align: center;
      }

      table caption {
          font-weight: bold;
          color:       blue;
      }
  </style>
  <script type=module>
    const key = event => {
      let table, i
      if (event.type === 'keydown') i = 0
      else i = 1
      table = document.getElementsByTagName('table')[i]
      table.querySelector('caption').innerHTML = event.type +
        ' ' + new Date().toTimeString()
      table.querySelector(`#altKey${i}`).innerHTML = event.altKey
      table.querySelector(`#char${i}`).innerHTML = event.char
      table.querySelector(`#charCode${i}`).innerHTML = event.charCode
      table.querySelector(`#code${i}`).innerHTML = event.code
      table.querySelector(`#ctrlKey${i}`).innerHTML = event.ctrlKey
      table.querySelector(`#isComposing${i}`).innerHTML = event.isComposing
      table.querySelector(`#key${i}`).innerHTML = event.key
      table.querySelector(`#keyCode${i}`).innerHTML = event.keyCode
      table.querySelector(`#locale${i}`).innerHTML = event.locale
      table.querySelector(`#location${i}`).innerHTML = event.location
      table.querySelector(`#metaKey${i}`).innerHTML = event.metaKey
      table.querySelector(`#repeat${i}`).innerHTML = event.repeat
      table.querySelector(`#shiftKey${i}`).innerHTML = event.shiftKey
      table.querySelector(`#which${i}`).innerHTML = event.which
      console.dir(event)
    }

    window.addEventListener('keydown', key)
    window.addEventListener('keypress', key)
    window.addEventListener('keyup', key)
  </script>
</head>
<body>
<main>
  <table>
    <caption></caption>
    <thead>
    <tr>
      <th>altKey</th>
      <th>char</th>
      <th>charCode</th>
      <th>code</th>
      <th>ctrlKey</th>
      <th>isComposing</th>
      <th>key</th>
      <th>keyCode</th>
      <th>locale</th>
      <th>location</th>
      <th>metaKey</th>
      <th>repeat</th>
      <th>shiftKey</th>
      <th>which</th>
    </tr>
    </thead>
    <tbody>
    <tr>
      <td id=altKey0></td>
      <td id=char0></td>
      <td id=charCode0></td>
      <td id=code0></td>
      <td id=ctrlKey0></td>
      <td id=isComposing0></td>
      <td id=key0></td>
      <td id=keyCode0></td>
      <td id=locale0></td>
      <td id=location0></td>
      <td id=metaKey0></td>
      <td id=repeat0></td>
      <td id=shiftKey0></td>
      <td id=which0></td>
    </tr>
    </tbody>
  </table>
  <table>
    <caption></caption>
    <thead>
    <tr>
      <th>altKey</th>
      <th>char</th>
      <th>charCode</th>
      <th>code</th>
      <th>ctrlKey</th>
      <th>isComposing</th>
      <th>key</th>
      <th>keyCode</th>
      <th>locale</th>
      <th>location</th>
      <th>metaKey</th>
      <th>repeat</th>
      <th>shiftKey</th>
      <th>which</th>
    </tr>
    </thead>
    <tbody>
    <tr>
      <td id=altKey1></td>
      <td id=char1></td>
      <td id=charCode1></td>
      <td id=code1></td>
      <td id=ctrlKey1></td>
      <td id=isComposing1></td>
      <td id=key1></td>
      <td id=keyCode1></td>
      <td id=locale1></td>
      <td id=location1></td>
      <td id=metaKey1></td>
      <td id=repeat1></td>
      <td id=shiftKey1></td>
      <td id=which1></td>
    </tr>
    </tbody>
  </table>
</main>
</body>
</html>

Here is the same program, but instead of hard coded HTML tables, they are generated using JavaScript. This shows how we can modify the DOM (cf. Document Object Model (DOM)). The result is 74 lines instead of 175:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Key event handler example 1 optimized</title>
  <meta charset=utf-8>
  <style>
      table {
          border-collapse: collapse;
          width:           100%;
          padding:         5px;
      }

      table, th, td {
          border:     1px solid red;
          text-align: center;
      }

      table caption {
          font-weight: bold;
          color:       blue;
      }
  </style>
  <script type=module>
    const keyAttrs = ['altKey', 'char', 'charCode', 'code', 'ctrlKey', 'isComposing',
      'key', 'keyCode', 'locale', 'location', 'metaKey', 'repeat', 'shiftKey',
      'which']

    const key = event => {
      let table, i
      if (event.type === 'keydown') i = 0
      else i = 1
      table = document.getElementsByTagName('table')[i]
      table.querySelector('caption').innerHTML =
        `${event.type} ${new Date().toTimeString()}`
      for (let j = 0; j < keyAttrs.length; j++)
        table.querySelector(`#${keyAttrs[j]}${i}`).innerHTML = event[keyAttrs[j]]
      console.dir(event)
    }

    let table, caption, thead, tbody, tr, th, td
    for (let i = 0; i < 2; i++) {
      table = document.createElement('table')
      caption = document.createElement('caption')
      thead = document.createElement('thead')
      tr = document.createElement('tr')
      tbody = document.createElement('tbody')
      table.appendChild(caption)
      table.appendChild(thead)
      thead.appendChild(tr)
      for (let j = 0; j < 14; j++) {
        th = document.createElement('th')
        th.innerHTML = keyAttrs[j]
        tr.appendChild(th)
      }
      table.appendChild(tbody)
      tr = document.createElement('tr')
      tbody.appendChild(tr)
      for (let j = 0; j < 14; j++) {
        td = document.createElement('td')
        td.id = keyAttrs[j] + i
        tr.appendChild(td)
      }
      document.querySelector('main').appendChild(table)
    }
    window.addEventListener('keydown', key)
    window.addEventListener('keyup', key)
  </script>
</head>
<body>
<main></main>
</body>
</html>

To determine the code for a specific key, take a look at the following links:

www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes

developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode

msdn.microsoft.com/en-us/library/windows/desktop/dd375731\%28v=vs.85\%29.aspx

4.3.19.5. unload and beforeunload

If we want to have some code executed when a user navigates away from our page, we can use the beforeunload (cf. developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload) or the unload (deprecated, cf. https://developer.mozilla .org/en-US/docs/Web/API/WindowEventHandlers/onunload) events. See Page lifecycle API.

Here is a simple example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Unload test</title>
  <meta charset=UTF-8>
</head>
<body>
<script type=module>
  const beforeunload = e => {
    e.preventDefault()
    console.dir(e)
    const req = new XMLHttpRequest()
    req.open('POST', 'beforeunload.php', false)
    req.send()
    const confirmationMessage = 'Are you sure you want to leave this shiny page?';
    (e || window.event).returnValue = confirmationMessage //Gecko + IE
    return confirmationMessage //Webkit, Safari, Chrome etc.
  }

  addEventListener('beforeunload', beforeunload)
</script>
</body>
</html>
1
2
3
<?php
  error_log('Before unload');
?>
4.3.19.6. error

When we include images in our HTML document, which cannot be found by the browser, the browser will display an ugly icon. In Firefox we can solve this issue by setting the alt attribute to the empty string. To solve the issue for all browsers, we can handle the error event, like so (test it in Chrome and Firefox to see the difference):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Handling the error event</title>
  <meta charset=utf-8>
  <script type=module>
    document.querySelectorAll('img')[1].addEventListener('error', e => {
      e.target.style.display = 'none'
    })
  </script>
</head>
<body>
<main>
  <img src=dontexist.png alt=''>
  <!-- http://stackoverflow.com/questions/8987428/image-placeholder -->
  <img src=dontexist.png alt=''>
</main>
</body>
</html>
4.3.19.7. Detect color scheme preference
1
2
3
4
5
6
// Set the initial style.
if (window.matchMedia("(prefers-color-scheme: dark)").matches) set_dark_theme()
else set_light_theme
// Listen for a change of the prefered style.
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",
() => switch_style())

4.3.20. Document Object Model (DOM)

The Document Object Model (DOM) is an API defined by the W3C to represent and interact with any HTML or XML document. The DOM is a model of an HTML or XML document that is loaded in a web browser. It represents a document as a tree of nodes, where each node represents a portion of the document, such as an element, a portion of text or a comment. The DOM is one of the most used APIs on the web because it allows code running in a web browser to access and interact with every node in the document. Nodes can be created, moved and changed. Event listeners can be added to nodes. Once a given event occurs all of its event listeners are triggered.

We have already looked at the DOM in several instances using the browser console or Firebug.

Here is what the DOM of a very basic HTML document looks like:

DOM

A basic introduction can be found at www.w3schools.com/js/js_htmldom.asp.

HTMLElement represents any HTML element. Specific elements are children of this object. For instance, a div element is represented via an HTMLDivElement object (cf. developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model).

HTMLDocument defines some specific properties that are often quite handy. document.body is the <body>, document.head the <head> and document.documentElement the root, i.e. <html> element of the document.

4.3.20.1. Creating new windows or tabs
4.3.20.2. Creating a new DOM document from a string
DOMParser can parse XML or HTML source stored in a string into a DOM Document.
4.3.20.3. Properties and methods of the HTML DOM Element Object

At www.w3schools.com/jsref/dom_obj_all.asp you can find a list of the properties and methods of the HTML DOM Element object with examples, which is very helpful.

4.3.20.4. Selecting DOM elements

Before we can change a DOM element in JavaScript, we need to select it, i.e. we need to get a pointer to the element. For this purpose, the document object allows us to select an element by id, name, tag, CSS class or selector.

4.3.20.4.1. document.getElementById

This method takes a string parameter specifying the ID of the element that we want.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang=en>
<head>
  <title>getElementById examples</title>
  <meta charset=utf-8>
  <script type=module>
    const myMain = document.getElementById('myMain') // Store the main element.
    console.dir(myMain) // Display the main element in the console.
    console.dir(document.getElementById('myHeader')) // Display the header.
  </script>
</head>
<body>
<main id=myMain>
  <header id=myHeader>Header</header>
</main>
</body>
</html>
4.3.20.4.2. document.getElementsByName

This method takes a string parameter specifying the name of the element that we want and returns a NodeList object, which we can access using indices, like arrays (cf. developer.mozilla.org/en-US/docs/Web/API/NodeList). Remember that the name attribute is used during form submission to send data to the server. Not every HTML element can have a name attribute.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang=en>
<head>
  <title>getElementsByName examples</title>
  <meta charset=utf-8>
  <script type=module>
    // Store ALL elements with name myForm in an array.
    const myForms = document.getElementsByName('myForm')
    console.log(`There are ${myForms.length} form elements.<br>`)
    // Display the first (and only) form element in the console.
    console.dir(myForms[0])
    const myInputs = document.getElementsByName('gender')
    console.log(`There are {myInputs.length} radio input elements.<br>`)
    // Display the first (and only) header element in the console.
    for (let i = 0; i < myInputs.length; i++) console.dir(myInputs[i])
  </script>
</head>
<body>
<main> <!--Note that the main element cannot have a name attribute.-->
  <form name=myForm>
    <input type=radio name=gender>male<br>
    <input type=radio name=gender>female
  </form>
</main>
</body>
</html>
4.3.20.4.3. document.getElementsByTagName

This method works like the previous one, except that the parameter is the HTML tag for which we want to select all elements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang=en>
<head>
  <title>getElementsByTagName examples</title>
  <meta charset=utf-8>
  <script type=module>
    // Store ALL main elements in an array.
    const myMains = document.getElementsByTagName('form')
    console.log(`There are ${myMains.length} main elements.<br>`)
    // Store ALL form elements in an array.
    const myForms = document.getElementsByTagName('form')
    console.log(`There are ${myForms.length} form elements.<br>`)
    // Display the first (and only) form element in the console.
    console.dir(myForms[0])
    const myInputs = document.getElementsByTagName('input')
    console.log(`There are ${myInputs.length} input elements.<br>`)
    // Display the first (and only) header element in the console.
    for (let i = 0; i < myInputs.length; i++) console.dir(myInputs[i])
  </script>
</head>
<body>
<main>
  <form>
    <input type=radio name=gender>male<br>
    <input type=radio name=gender>female
  </form>
</main>
</body>
</html>
4.3.20.4.4. document.getElementsByClassName

This method works like the previous two, except that the parameter is the CSS class for which we want to select all elements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang=en>
<head>
  <title>getElementsByClassName examples</title>
  <meta charset=utf-8>
  <style>
      .magic {
          background-color: lightgreen;
      }
  </style>
  <script type=module>
    // Store ALL elements of the magic class in an array.
    const myMagics = document.getElementsByClassName('magic')
    console.log(`There are ${myMagics.length} magic elements.<br>`)
    for (let i = 0; i < myMagics.length; i++) console.dir(myMagics[i])
  </script>
</head>
<body>
<main>
  <form>
    <input type=radio><span class=magic>male</span><br>
    <input type=radio>female
  </form>
</main>
</body>
</html>
4.3.20.4.5. document.querySelector and document.querySelectorAll

These methods work like the previous three, except that the parameter is the CSS selector for which we want to select all elements (cf. www.w3.org/TR/selectors-api). querySelector returns the first element that matches the given CSS selector, whereas querySelectorAll returns a NodeList object with all the matching elements. These are the most powerful selectors available. We can use the whole gamut of CSS selectors described in www.w3schools.com/cssref/css_selectors.asp.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang=en>
<head>
  <title>querySelectorAll examples</title>
  <meta charset=utf-8>
  <style>
      .magic {
          background-color: lightgreen;
      }
  </style>
  <script type=module>
    // Store the FIRST element of the magic class.
    const myFirstMagic = document.querySelector('.magic')
    console.dir(myFirstMagic)
    // Store all magic articles in an array.
    const myMagicArticles = document.querySelectorAll('.magic article')
    console.log(`There are ${myMagicArticles.length} magic articles.<br>`)
    for (let i = 0; i < myMagicArticles.length; i++) console.dir(myMagicArticles[i])
  </script>
</head>
<body>
<header><h1>Header</h1></header>
<main>
  <section>
    <h2>Section 1 header</h2>
    <article>
      <h3>Section 1 article 1 header</h3>
      Section 1 article 1
    </article>
    <article>
      <h3>Section 1 article 2 header</h3>
      Section 1 article 2
    </article>
  </section>
  <section class=magic>
    <h2>Section 2 header</h2>
    <article>
      <h3>Section 2 article 1 header</h3>
      Section 2 article 1
    </article>
    <article>
      <h3>Section 2 article 2 header</h3>
      Section 2 article 2
    </article>
  </section>
</main>
</body>
</html>
4.3.20.4.6. Direct access via the document object

We can directly access the following HTML objects (and object collections):

  • document.anchors

  • document.body

  • document.documentElement

  • document.embeds

  • document.forms

  • document.head

  • document.images

  • document.links

  • document.scripts

  • document.title

4.3.20.5. Traversing the DOM

Depending on what we want to do, we can traverse the DOM as a node or as an element tree.

4.3.20.5.1. Node trees

In the HTML DOM, everything is a node:

  • The document itself is a document node.

  • All HTML elements are element nodes.

  • All HTML attributes are attribute nodes.

  • Texts inside HTML elements are text nodes.

  • Comments are comment nodes.

The link above provides extensive information on the NodeList object’s methods and properties. In particular, we can determine a node’s type via the nodeType property (cf. www.w3schools.com/jsref/prop_node_nodetype.asp).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Illustrates DOM traversal using nodes</title>
  <meta charset=utf-8>
  <script type=module>
    // We traverse the DOM tree recursively, using all Node elements.
    const traverseDOM = e => {
      for (let child = e.firstChild; child; child = child.nextSibling) {
        console.log(`Node name: ${child.nodeName} value: ${child.nodeValue}`)
        if (child.nodeType === 1) traverseDOM(child)
      }
    }

    traverseDOM(document.querySelector('main'))
  </script>
</head>
<body>
<main>
  <header><h1>This is the main header</h1></header>
  <section>
    <header><h2>Section 1 header</h2></header>
    <article>
      <header><h3>Section 1 article 1 header</h3></header>
    </article>
    <article>
      <header><h3>Section 1 article 2 header</h3></header>
    </article>
  </section>
  <section>
    <header><h2>Section 2 header</h2></header>
    <article>
      <header><h3>Section 2 article 1 header</h3></header>
    </article>
    <article>
      <header><h3>Section 2 article 2 header</h3></header>
    </article>
  </section>
</main>
</body>
</html>
4.3.20.5.2. Element trees
The Element interface represents an object within a DOM document.

Text and comment nodes are not treated as objects in this context and are ignored. This API allows us therefore to traverse the DOM element tree, without bothering with text and comments.

The children property of an element is particularly useful, as it contains an array of all of its HTML element children.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Illustrates DOM traversal using elements</title>
  <meta charset=utf-8>
  <script type=module>
    // We traverse the DOM tree recursively, using only HTML elements.
    const traverseDOM = e => {
      for (let i = 0; i < e.children.length; i++) {
        console.log(e.children[i].nodeName)
        traverseDOM(e.children[i])
      }
    }

    traverseDOM(document.querySelector('main'))
  </script>
</head>
<body>
<main>
  <header><h1>This is the main header</h1></header>
  <section>
    <header><h2>Section 1 header</h2></header>
    <article>
      <header><h3>Section 1 article 1 header</h3></header>
    </article>
    <article>
      <header><h3>Section 1 article 2 header</h3></header>
    </article>
  </section>
  <section>
    <header><h2>Section 2 header</h2></header>
    <article>
      <header><h3>Section 2 article 1 header</h3></header>
    </article>
    <article>
      <header><h3>Section 2 article 2 header</h3></header>
    </article>
  </section>
</main>
</body>
</html>
4.3.20.6. Getting and setting attributes
4.3.20.6.1. as element properties

The attributes of HTML elements are available as properties of the corresponding HTMLElement in JavaScript. However, whereas HTML attributes are not case sensitive, JavaScript properties use camel case (cf. en.wikipedia.org/wiki/CamelCase). For instance the usemap attribute of an img element can be accessed via the useMap property. There are two exceptions: given that some attribute names are reserved words in JavaScript, the property name has an html prefix, for instance the for attribute can be accessed via the htmlFor property. The exception to the exception is the class attribute, which can be accessed via the className property.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Illustrates access of HTML element attributes using
    element properties</title>
  <meta charset=utf-8>
  <script type=module>
    const toggleImage = () => {
      let image = document.querySelector('img') // Get the image element.
      console.log(image.src) // Display the complete URL.
      // Extract the image filename from the complete URL.
      let imageName = image.src.substring(image.src.lastIndexOf('/') + 1)
      // Given that the image is in the same directory as this document,
      // we do not need to specify the complete URL, just the image filename.
      if (imageName === 'camaro256x256.png') {
        image.src = 'ferrari256x256.png'
        image.title = 'https://www.iconfinder.com/icons/67532/'
          + 'car_ferrari_red_small_car_sports_car_icon#size=256'
      } else {
        image.src = 'camaro256x256.png'
        image.title = 'https://www.iconfinder.com/icons/67528/'
          + 'camaro_car_sports_car_icon#size=256'
      }
    }

    document.querySelector('button').addEventListener('click', toggleImage)
  </script>
</head>
<body>
<button>Toggle image</button>
<br>
<img src=camaro256x256.png alt=Car
     title=https://www.iconfinder.com/icons/67528/camaro_car_sports_car_icon#size=256>
</body>
</html>

Here is an example of how to scroll to the end of an element’s content using JS. Note that the height of the element needs to be set to something smaller than the height taken by the content, otherwise this won’t work.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Scroll example 1</title>
  <meta charset=utf-8>
  <style>
      main {
          position:         fixed;
          top:              0;
          bottom:           0;
          left:             0;
          right:            0;
          background-color: greenyellow;
          overflow:         auto;
      }
  </style>
  <script type=module>
    const generateRandomText = (length, lineWidth) => {
      let string = ""
      const charset = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

      for (let i = 1; i <= length; i++) {
        string += charset.charAt(Math.floor(Math.random() * charset.length))
        if (i % lineWidth === 0) string += '<br>'
      }
      return string
    }

    const main = document.querySelector('main')
    main.innerHTML = generateRandomText(10000, 60)
    main.scrollTop = main.scrollHeight
  </script>
</head>
<body>
<main></main>
</body>
</html>
4.3.20.6.2. using getAttribute and setAttribute

Instead of using element properties, we can use two HTMLElement methods, one to get and one to set the value of an attribute. Note that attribute values are treated as strings, i.e. getAttribute always returns and setAttribute takes a string. The attribute names used are the standard HTML ones, not the camel case versions used with element properties. These methods can also be used with non standard attributes as well as attributes of XML documents.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Illustrates access of HTML element attributes using methods</title>
  <meta charset=utf-8>
  <script>
    "use strict"

    const toggleImage = () => {
      const image = document.querySelector('img') // Get the image element.
      console.log(image.src) // Display the complete URL.
      // Extract the image filename from the complete URL.
      const imageName =
        image.getAttribute('src').substring(image.getAttribute('src').lastIndexOf('/')
          + 1)
      // Given that the image is in the same directory as this document,
      // we do not need to specify the complete URL, just the image filename.
      if (imageName === 'camaro256x256.png') {
        image.setAttribute('src', 'ferrari256x256.png')
        image.setAttribute('title', 'https://www.iconfinder.com/icons/67532/'
          + 'car_ferrari_red_small_car_sports_car_icon#size=256')
      } else {
        image.setAttribute('src', 'camaro256x256.png')
        image.setAttribute('title', 'https://www.iconfinder.com/icons/67528/'
          + 'camaro_car_sports_car_icon#size=256')
      }
    }

    const toggleTitle = () => {
      const image = document.querySelector('img') // Get the image element.
      const imageName =
        image.getAttribute('src').substring(image.getAttribute('src').lastIndexOf('/')
          + 1)
      // If the image has a title attribute, we remove it.
      if (image.hasAttribute('title')) image.removeAttribute('title')
      // Otherwise, we need to determine the correct title to add.
      else if (imageName === 'camaro256x256.png')
        image.setAttribute('title', 'https://www.iconfinder.com/icons/67528/'
          + 'camaro_car_sports_car_icon#size=256')
      else
        image.setAttribute('title', 'https://www.iconfinder.com/icons/67532/'
          + 'car_ferrari_red_small_car_sports_car_icon#size=256')
    }
  </script>
</head>
<body>
<button onclick='toggleImage()'>Toggle image</button>
<button onclick='toggleTitle()'>Toggle image title attribute</button>
<br>
<img src=camaro256x256.png alt=Car
     title=https://www.iconfinder.com/icons/67528/camaro_car_sports_car_icon#size=256>
</body>
</html>
4.3.20.6.3. using dataset attributes

On occasion we might want to add our own attributes to HTML elements in order to store specific information. In order to be HTML5-compliant, we need to prefix our attribute names with data-. Our attribute names may not contain capital letters A to Z or semicolons and may not start with xml (cf. developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#attr-data-*). A nice example can be found at www.w3schools.com/tags/tryit.asp?filename=tryhtml5_global_data. We can access dataset attributes using methods or using the dataset property. In the latter case, attribute names are mapped to camel case property names. See also developer.mozilla.org/en-US/docs/Web/API/HTMLElement.dataset.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Illustrates access of HTML element dataset attributes</title>
  <!-- Based on http://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_global_data-->
  <meta charset=utf-8>
  <style>
      li {
          width:  65px;
          cursor: pointer;
      }
  </style>
  <script>
    'use strict'

    const showDetails1 = animal => {
      const animalType = animal.getAttribute("data-animal-type")
      alert(`The ${animal.innerHTML} is a ${animalType}.`)
    }

    const showDetails2 = animal => {
      const animalType = animal.dataset.animalType
      alert(`The ${animal.innerHTML} is a ${animalType}.`)
    }

  </script>
</head>
<body>
<h1>Species</h1>

<p>Click on a species to see what type it is:</p>
<ul>
  <li onclick="showDetails1(this)" id=owl data-animal-type=bird>Owl</li>
  <li onclick="showDetails2(this)" id=salmon data-animal-type=fish>Salmon</li>
  <li onclick="showDetails1(this)" id=tarantula data-animal-type=spider>Tarantula</li>
</ul>
</body>
</html>
4.3.20.6.4. as Attr objects

From p. 378 of the 6th edition of "JavaScript The Definitive Guide":

The Node type defines an attributes property. This property is null for any nodes that are not Element objects. For Element objects, attributes is a read-only array-like object that represents all the attributes of the element.

The Attr object has name and value properties representing the name and value of the attribute.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Illustrates access of HTML element dataset attributes</title>
  <!-- Based on http://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_global_data-->
  <meta charset=utf-8>
  <script type=module>
    const children = document.getElementsByTagName('ul')[0].childNodes
    for (let i = 0; i < children.length; i++)
      if (children[i].attributes)
        for (let j = 0; j < children[i].attributes.length; j++)
          console.log(`Name: ${children[i].attributes[j].name} value: `
            + children[i].attributes[j].value)
  </script>
</head>
<body>
<h1>Species</h1>
<ul>
  <li id=owl data-animal-type=bird>Owl</li>
  <li id=salmon data-animal-type=fish>Salmon</li>
  <li id=tarantula data-animal-type=spider>Tarantula</li>
</ul>
</body>
</html>
4.3.20.6.5. Manipulating an element’s classes

element.classList is a highly useful property to manipulate an element’s classes. See developer.mozilla.org/en-US/docs/Web/API/Element/classList for the details.

4.3.20.7. Element content

We can view the content of an element as HTML or plain text.

4.3.20.7.1. as HTML

The innerHTML property of an Element object contains the content as an HTML string. outerHTML contains the content, including the opening and closing tag of the element. With insertAdjacentHTML we can insert HTML before or after the beginning or before or after the end of a given element (cf. developer.mozilla.org/en-US/docs/Web/API/Element.insertAdjacentHTML).

insertAdjacentHTML() parses the specified text as HTML or XML and inserts the resulting nodes into the DOM tree at a specified position. It does not reparse the element it is being used on and thus it does not corrupt the existing elements inside the element. This, and avoiding the extra step of serialization make it much faster than direct innerHTML manipulation.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Element content as HTML</title>
  <meta charset=utf-8>
  <script type=module>
    const toggleInnerHTML = () => {
      // First we need to know whether the outer element is a <main> or a <div>.
      const el = document.querySelector('main') ? 'main' : 'div'
      if (document.querySelector(el).innerHTML !== '')
        document.querySelector(el).innerHTML = ''
      else document.querySelector(el).innerHTML =
        '<section><h1>Content header</h1>Content</section>'
    }

    const toggleOuterHTML = () => {
      let el, el_replace
      if (document.querySelector('main')) {
        el = 'main' // We have a <main>
        el_replace = 'div' // and want to replace it with a <div>.
      } else {
        el = 'div' // We have a <div>
        el_replace = 'main' // and want to replace it with a <main>
      }
      document.querySelector(el).outerHTML = `<${el_replace}>` +
        `<section><h1>Content header</h1>Content</section></${el_replace}>`
    }

    const addBeforeBegin = () => {
      document.querySelector('header').insertAdjacentHTML('beforebegin',
        '<h1>Header</h1>')
    }

    const addAfterBegin = () => {
      document.querySelector('header').insertAdjacentHTML('afterbegin',
        '<h1>Header</h1>')
    }

    const addBeforeEnd = () => {
      document.querySelector('header').insertAdjacentHTML('beforeend',
        '<h1>Header</h1>')
    }

    const addAfterEnd = () => {
      document.querySelector('header').insertAdjacentHTML('afterend',
        '<h1>Header</h1>')
    }

    const handlers = [toggleInnerHTML, toggleOuterHTML, addBeforeBegin,
      addAfterBegin, addBeforeEnd, addAfterEnd]
    for (let i = 0; i < handlers.length; i++)
      document.getElementsByTagName('button')[i].addEventListener('click',
        handlers[i])
  </script>
</head>
<body>
<button>Toggle innerHTML</button>
<button>Toggle outerHTML</button>
<button>Header add before begin</button>
<button>Header add after begin</button>
<button>Header add before end</button>
<button>Header add after end</button>
<header>Header content</header>
<main></main>
</body>
</html>
4.3.20.7.2. as plain text

The textContent property of an Element object contains the content as plain text.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Element content as plain text</title>
  <meta charset=utf-8>
  <script type=module>
    const changeText = () => document.querySelector('main').textContent =
      prompt('Please enter the new text')

    // Register button event handler.
    document.getElementsByTagName('button')[0].addEventListener('click', changeText)
  </script>
</head>
<body>
<button>Change text</button>
<main>Text</main>
</body>
</html>
4.3.20.8. Managing nodes

We create a new element with createElement and a new text node with createTextNode. There are other creation methods available, as detailed in developer.mozilla.org/en-US/docs/Web/API/Document.

With appendChild we add a node as the last child of the given node. With insertBefore we insert the new node (first parameter) before a given node (second parameter). If the second parameter is null, the method behaves like appendChild.

removeChild is invoked on the parent node and given the child node that is to be removed as parameter. replaceChild is also invoked on the parent node. It takes the new node as first and the node to be replaced as second parameter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Node management example</title>
  <meta charset=utf-8>
  <script type=module>
    const button = document.createElement('button')
    button.textContent = 'Click to kill me!'
    // With a normal function, we have 3 options:
    button.addEventListener('click', function (e) {
      // this.parentElement.removeChild(this); // Does not work with arrow function.
      e.target.parentElement.removeChild(e.target)
      // This should be avoided as button may not exist in the event handler.
      // button.parentElement.removeChild(button);
    })
    document.body.appendChild(button)
  </script>
</head>
<body>
</body>
</html>
4.3.20.8.1. Document fragments

DocumentFragments are DOM Nodes. They are never part of the main DOM tree. The usual use case is to create the document fragment, append elements to the document fragment and then append the document fragment to the DOM tree. In the DOM tree, the document fragment is replaced by all its children.

Since the document fragment is in memory and not part of the main DOM tree, appending children to it does not cause page reflow (computation of element’s position and geometry). Consequently, using document fragments often results in better performance.

4.3.20.10. Manipulating CSS

www.w3.org/wiki/Dynamic_style_-_manipulating_CSS_with_JavaScript

www.w3schools.com/js/js_htmldom_css.asp

www.w3schools.com/jsref/dom_obj_style.asp

developer.mozilla.org/en-US/docs/Web/API/Window.getComputedStyle

developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration

We can easily manipulate CSS via JavaScript, which opens up some interesting applications, for instance moving HTML objects or changing colors dynamically.

In order to access internal or external stylesheets, we can use document.styleSheets, which gives us an array with all stylesheets used by the document.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang=en>
<head>
  <title>DOM CSS manipulation example 1</title>
  <meta charset=UTF-8>
  <style>
      body {
          background-color: black;
      }
  </style>
  <script type=module>
    const switchToWhite = () => {
      document.styleSheets[0].cssRules[0].style.backgroundColor = "white"
      /* Or we can set the inline style of the body:
      document.body.style.backgroundColor = "white";
      */
    }

    document.querySelector('button').addEventListener('click', switchToWhite)
  </script>
</head>
<body>
<button>Set background color to white</button>
</body>
</html>

In order to access the inline styling of a particular element, we can use the style attribute of that element. For instance, if we want to change the color of the second p element in our document, we could write:

1
document.querySelectorAll('p')[1].style.color = "#F0F"
Gorilla1

Let’s look at a more dynamic example, where we move a gorilla:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html lang=en>
<head>
  <title>DOM CSS manipulation example 2</title>
  <meta charset=UTF-8>
  <script>
    'use strict'

    let gorilla

    const init = () => {
      gorilla = document.querySelector('img')
      gorilla.style.cssText = "position: relative; top: 0; left: 0"
    }

    const moveRight = () => {
      const x = parseInt(gorilla.style.left)
      gorilla.style.left = x + 10 + 'px'
    }

    const moveLeft = () => {
      const x = parseInt(gorilla.style.left)
      gorilla.style.left = x - 10 + 'px'
    }

    const moveUp = () => {
      const y = parseInt(gorilla.style.top)
      gorilla.style.top = y - 10 + 'px'
    }

    const moveDown = () => {
      const y = parseInt(gorilla.style.top)
      gorilla.style.top = y + 10 + 'px'
    }

    addEventListener('load', init)
  </script>
</head>
<body>
<button onclick=moveLeft()><</button>
<button onclick=moveRight()>></button>
<button onclick=moveUp()>^</button>
<button onclick=moveDown()>v</button>
<img src=gorilla236x256.png>
</body>
</html>
4.3.20.10.1. getComputedStyle
The Window.getComputedStyle() method gives the values of all the CSS properties of an element after applying the active stylesheets and resolving any basic computation those values may contain.

This method is particularly useful if we want to query an element’s CSS value that we have not set programmatically.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
<head>
  <title>DOM CSS manipulation example 3</title>
  <meta charset=UTF-8>
  <script type=module>
    alert(getComputedStyle(document.querySelector('body')).getPropertyValue('color'))
  </script>
</head>
<body>
</body>
</html>
4.3.20.10.2. Inserting a new style sheet
To create a new stylesheet, insert a <style> or <link> element into the document.
4.3.20.11. Inserting JS dynamically
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>Insert JS</title>
</head>
<body>
<script type=module>
  const script = document.createElement('script')
  script.innerHTML = 'alert("Test");'
  document.body.appendChild(script)
</script>
</body>
</html>
4.3.20.13. Observing DOM mutations

developer.mozilla.org/en-US/docs/Web/API/MutationObserver

hacks.mozilla.org/2012/05/dom-mutationobserver-reacting-to-dom-changes-without-killing-browser-performance

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// select the target node
const target = document.querySelector('body')

// create an observer instance
const observer = new MutationObserver(mutations => {
  for (const mutation of mutations) console.dir(mutation)
})

// configuration of the observer:
const config = {
  attributes: true, childList: true, characterData: true, subtree: true,
  attributeOldValue: true, characterDataOldValue: true
}

// pass in the target node, as well as the observer options
observer.observe(target, config)

document.querySelector('button').addEventListener('click', () => {
  observer.disconnect()
})

4.3.21. Browser Object Model (BOM)

window is the global object put at our disposal by the browser. It is of central importance and described in detail in developer.mozilla.org/en-US/docs/Web/API/Window. In Basic input and output we have already met some useful window methods.

4.3.21.1. Timers

We can measure time, accurate to one microsecond, using the Performance.now method (cf. developer.mozilla.org/en-US/docs/Web/API/Performance.now()).

We can choose between three timer methods.

4.3.21.1.1. setTimeout

setTimeout runs a given function (first parameter) after a specified number of milliseconds (second parameter) and returns the ID of the timeout, which can be used with clearTimeout if we change our mind and do not want the timer to execute. setTimeout is ideal if we just want to execute a function once after a given delay, as in the following example. Note that we need to use an anonymous function if we want to pass parameters to the function that is to be called by the timer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang=en>
<head>
  <title>setTimeout example 1</title>
  <meta charset=utf-8>
</head>
<body>
<main>
  <script type=module>
    const displayAlert = msg => alert(msg)

    setTimeout(() => displayAlert('Test'), 2000)
  </script>
</main>
</body>
</html>

We can, however, also repeat the function call by invoking setTimeout within the function that is to be executed repeatedly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang=en>
<head>
  <title>setTimeout example 2</title>
  <meta charset=utf-8>
</head>
<body>
<main>
  <script type=module>
    const countDown = x => {
      console.log(`Current counter: ${x}`)
      if (x > 0) setTimeout(() => countDown(x - 1), 1000)
    }

    countDown(10)
  </script>
</main>
</body>
</html>
4.3.21.1.2. setInterval

setInterval is identical to setTimeout except that the given function gets invoked repeatedly until clearInterval is called with the timer ID.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang=en>
<head>
  <title>setInterval example</title>
  <meta charset=utf-8>
  <script type=module>
    let counter = 10, timer // Initialise.

    const countDown = () => {
      console.log(`Current counter: ${counter--}`) // Display and decrement.
      if (counter < 0) clearInterval(timer) // Stop timer.
    }

    timer = setInterval(countDown, 1000) // Start timer.
  </script>
</head>
<body>
<main>
</main>
</body>
</html>
RadialGradiantAnim1

This is an example of how we can create a background color animation using DOM CSS manipulation inside a timer function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Radial gradient animation</title>
  <meta charset=utf-8>
  <script type=module>
    /*
     We create a radial gradient with an origin and a target.
     We set random origin colors. Target color are created via 255 - origin color.
     The red, green and blue components of the origin are animated upwards or downwards.
     The RGB components of the target are animated in opposite directions.
     */
    const RGBColorsOrigin = [Math.floor(Math.random() * 256), Math.floor(Math.random() *
        256),
        Math.floor(Math.random() * 256)],
      RGBColorsTarget = [255 - RGBColorsOrigin[0], 255 - RGBColorsOrigin[1], 255 -
      RGBColorsOrigin[2]]
    const RGBDirections = [Math.random() >= 0.5 ? 1 : -1, Math.random() >= 0.5 ? 1 : -1,
      Math.random() >= 0.5 ? 1 : -1], RGBDirectionsOrigin = [-RGBDirections[0],
      -RGBDirections[1], -RGBDirections[2]]

    const changeBackground = () => {
      for (let i = 0; i <= 2; i++) {
        if (RGBColorsOrigin[i] >= 255 && RGBDirectionsOrigin[i] === 1 || RGBColorsOrigin[i]
          <= 0 && RGBDirectionsOrigin[i] === -1)
          RGBDirectionsOrigin[i] = -RGBDirectionsOrigin[i]
        RGBColorsOrigin[i] += RGBDirectionsOrigin[i] * Math.floor(Math.random() * 2 + 0.5)
        if (RGBColorsTarget[i] >= 254 && RGBDirections[i] === 1 || RGBColorsTarget[i] <= 1 &&
          RGBDirections[i] === -1) RGBDirections[i] = -RGBDirections[i]
        RGBColorsTarget[i] += RGBDirections[i] * Math.floor(Math.random() * 2 + 0.5)
      }
      const originColor = `rgb(${RGBColorsOrigin[0]}, ${RGBColorsOrigin[1]}, ` +
          `${RGBColorsOrigin[2]})`,
        targetColor = `, rgb(${RGBColorsTarget[0]}, ${RGBColorsTarget[1]}, ` +
          `${RGBColorsTarget[2]})) no-repeat fixed`
      document.querySelector('body').style.background =
        `radial-gradient(${originColor}${targetColor}`
    }

    setInterval(changeBackground, 100)
  </script>
</head>
<body>

</body>
</html>
4.3.21.1.3. requestAnimationFrame
You should call this method whenever you’re ready to update your animation onscreen. This will request that your animation function be called before the browser performs the next repaint. The number of callbacks is usually 60 times per second, but will generally match the display refresh rate in most web browsers as per W3C recommendation. The callback rate may be reduced to a lower rate when running in background tabs.

developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame

www.w3.org/TR/animation-timing/\#requestAnimationFrame

requestAnimationFrame1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang=en>
<head>
  <!-- https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame-->
  <title>requestAnimationFrame example</title>
  <meta charset=utf-8>
  <style>
    div {
      position: absolute;
      left: 10px;
      padding: 50px;
      background: crimson;
      color: white
    }
  </style>
  <script type=module>
    let requestId = 0, animationStartTime

    const animate = time => {
      document.getElementById("animated").style.left =
        `${(time - animationStartTime) % 2000 / 4}px`
      requestId = window.requestAnimationFrame(animate)
    }

    const start = () => {
      if (!requestId) {
        animationStartTime = window.performance.now()
        requestId = window.requestAnimationFrame(animate)
      }
    }

    const stop = () => {
      if (requestId)
        window.cancelAnimationFrame(requestId)
      requestId = 0
    }

    const buttons = document.querySelectorAll('button')
    buttons[0].addEventListener('click', start)
    buttons[1].addEventListener('click', stop)
  </script>
</head>
<body>
  <button>Click me to start!</button>
  <button>Click me to stop!</button>
  <div id=animated>Hello there.</div>
</body>
</html>
4.3.21.2. The location object

This object allows us to redirect the browser to another page. See www.w3schools.com/jsref/obj_location.asp and developer.mozilla.org/en-US/docs/Web/API/Window.location.

4.3.21.3. The navigator object

Here is an example that checks once per second whether the browser is online:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>On/offline detector</title>
  <script type=module>
    let onlineStatus = true
    const checkOnlinestatus = () => {
      console.log(`Currently: ${onlineStatus}`)
      if (navigator.onLine !== onlineStatus) {
        onlineStatus = navigator.onLine
        alert(`Online status changed to ${onlineStatus}`)
      }
    }

    setInterval(checkOnlinestatus, 1000)
  </script>
</head>
<body>
</body>
</html>

4.3.22. Strict mode

The strict mode, according to ECMA, "provides enhanced error checking and program security". To enter this mode just put 'use strict' or "use strict"; at the top of any script. See developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode and www.ecma-international.org/ecma-262/#sec-strict-mode-of-ecmascript.

To find out whether we are currently in strict mode, we can use this one liner from page 167 of the 6th edition of "JavaScript The Definitive Guide":

1
const strict = (function() { return !this; } ())

This works because in non-strict mode, the invocation context of a function is the global object, therefore !this will be false. In strict mode, the invocation context is undefined, thus !this will be true.

In Firefox, you can set strict mode to default by changing javascript.options.strict to true in about:config.

4.3.26. AJAX

Asynchronous JavaScript And XML (AJAX) is a term used to describe the programming practice of using HTML, CSS, JavaScript, the Document Object Model (DOM), and the XMLHttpRequest object together to build complex Web pages that update their content without reloading the entire Web page. This makes the application faster and more responsive to user actions.

Study the gentle introduction to AJAX at www.w3schools.com/xml/ajax_intro.asp.

4.3.26.1. XMLHttpRequest

The key object that enables AJAX is XMLHttpRequest (cf. developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest), which allows us to take control of HTTP communication with a server, whereas normally this is handled in the background by the browser.

The required steps are the following:

  1. Create an XMLHttpRequest object.

  2. Prepare the request using open and register the event handler to handle the response.

  3. Send the request to the server using send.

  4. When the server sends a response, an event gets triggered and our event handler reads the data and takes the required action, for instance update some parts of our web without a page reload.

It’s as easy as this:

AJAX1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Very basic AJAX example</title>
  <meta charset=utf-8>
  <script type=module>
    const displayResponse = e =>
      document.querySelector('main').innerHTML = e.target.response

    const AJAX = () => {
      const req = new XMLHttpRequest() // Create new request.
      req.open('POST', 'AJAX1.php') // Specify the method and script to be used.
      // Add the event listener for successful request completion.
      req.addEventListener('load', displayResponse)
      req.send() // Send the request to the server.
    }

    document.querySelector('button').addEventListener('click', AJAX)
  </script>
</head>
<body>
<main>
  <button>Click me and something wonderful will happen without page reload!</button>
</main>
</body>
</html>
1
2
3
<?php
  echo '<h1>Hello world!</h1>';
?> 
4.3.26.1.1. open(method, url, async, user, password)

For our purposes the relevant methods are POST and GET(cf. Forms). The second parameter represents the location of the server script that will receive the request. The third parameter is optional and true by default, which specifies an asynchronous request, i.e. our script will not block to wait for the response. Instead, when the server sends a response, an event will be triggered to which our script can react. If this parameter is set to false, our script will block and wait for the server response. Parameters four and five are only needed to access password protected resources.

4.3.26.1.2. send(data)

send sends the request to the server. Any data that we want to send is given as argument. The following types can be used: ArrayBuffer, ArrayBufferView, Blob, Document, DOMString and FormData. To send binary data we should use Blob (cf. developer.mozilla.org/en-US/docs/Web/API/Blob) or ArrayBufferView (cf. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) objects.

The FormData object (cf. developer.mozilla.org/en-US/docs/Web/API/FormData) allows us to programmatically send form data to the server just like the user can when sending an HTML form. To use it we simply create a new FormData object and then add name/value pairs to it using the append(name, value) method. The name parameter is a string whereas the value parameter can be a string or a Blob or a File. In the latter two cases, a third optional parameter specifies the filename to be reported to the server. For Blob objects the default filename is "blob". We can also use FormData to submit an HTML form asynchronously (cf. developer.mozilla.org/en-US/docs/Web/Guide/Using_FormData_Objects).

4.3.26.1.3. abort

This method aborts the request if it has already been sent.

4.3.26.1.4. Events

The following events are relevant for the XMLHttpRequest object:

loadstart

triggered on request start

progress

triggered periodically during request execution

abort

triggered on request abortion

error

triggered if a request error occurs

load

triggered on successful request completion

timeout

triggered on request timeout

loadend

triggered after load, abort or error have been dispatched

We need to register a handler that takes care of the load event. As we have seen in Events, all event handlers automatically receive an Event object when invoked. The target attribute of the event corresponds to our XMLHttpRequest object, given that we registered the event handler on this object. The status attribute contains the HTTP response code (cf. developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes) and statusText the status in text form. If this code has the value 200, the request has succeeded and we can use the response data. A specific response header can be queried using getResponseHeader(header) or we can retrieve a string with all response headers using getAllResponseHeaders. Note that cookie headers are automatically filtered out. The XMLHttpRequest object has three properties to contain the response data. response contains the response in the format specified by the responseType property. responseText has the response in text format and responseXML as a Document object in parsed XML format, which can then be traversed as described in Traversing the DOM, if applicable.

Let’s look at a couple more examples:

AJAX2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Illustrates the programmatic creation and submission of a form without
    page reload</title>
  <meta charset=utf-8>
  <script type=module>
    // The event handler reads the data from the server and displays it.
    const displayData = e => {
      console.log(e)
      const p = document.createElement('p') // Create new paragraph element
      // and set its content to server response data.
      p.innerHTML = e.target.response + '<br>Response headers:'
      p.innerHTML += '<pre>' + e.target.getAllResponseHeaders() + '</pre>'
      // Append the paragraph to the main element.
      document.querySelector('main').appendChild(p)
    }

    // This event handler gets triggered when the HTML document has finished loading.
    // Here we create a form, fill it with data and send it to the server.
    let data = new FormData(), req = new XMLHttpRequest()
    // Fill in the form.
    data.append('first_name', 'Mickey')
    data.append('last_name', 'Mouse')
    // Set the event listener to be triggered upon successful request completion.
    req.addEventListener('load', displayData)
    // Open the HTTP connection to the server script using POST method.
    req.open('POST', 'AJAX2.php')
    req.send(data) // Send the form to the server.

    // When the user clicks on submit, we submit the form asynchronously.
    document.forms[0].addEventListener('submit', e => {
      e.preventDefault() // Prevent default form submission
      // Create a new form and a new XMLHttpRequest.
      data = new FormData(document.forms[0]), req = new XMLHttpRequest()
      req.addEventListener('load', displayData)
      // Open the HTTP connection to the server script using POST method.
      req.open('POST', 'AJAX2.php')
      req.send(data) // Send the form to the server.
    })
  </script>
</head>
<body>
<main>
  <form method=post>
    <label>First name<input name=first_name required></label><br>
    <label>Last name<input name=last_name required></label><br>
    <button>Log in</button>
  </form>
</main>
</body>
</html>
1
2
3
4
<?php
  if (isset($_POST['first_name']) && isset($_POST['last_name']))
    echo 'Hello ' . $_POST['first_name'] . ' ' . $_POST['last_name'];
?> 
AJAX3
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Illustrates reading comma separated files and displaying them</title>
  <meta charset=utf-8>
  <style>
      select {
          position: fixed;
          top:      0;
          left:     0;
      }

      table {
          border-collapse: collapse;
          position:        relative;
          left:            100px;
      }

      th, td {
          border:     1px solid red;
          padding:    5px;
          text-align: left;
      }

      aside {
          position:      absolute;
          width:         30px;
          height:        30px;
          background:    repeating-linear-gradient(-45deg, red, red 5px, white 5px, white 10px);
          border-radius: 15px;
          animation:     asideAnimation 5s infinite alternate;
      }

      @keyframes asideAnimation {
          0% {
              left: 500px;
              top:  0;
          }

          50% {
              left: 250px;
              top:  300px;
          }

          100% {
              left: 0px;
              top:  100px;
          }
      }
  </style>
  <script type=module>
    const displayData = () => {
      const index = document.querySelector('select').selectedIndex
      const req = new XMLHttpRequest()
      req.open('POST', `testdata${index + 1}.csv`)
      req.addEventListener('load', e => {
        const oldTable = document.querySelector('table')
        const newTable = document.createElement('table')
        if (oldTable) document.querySelector('main').replaceChild(newTable, oldTable)
        else document.querySelector('main').appendChild(newTable)
        const lines = e.target.responseText.split('\n') // Explode rows.
        let line, tableRow, cellTag, cell
        for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
          line = lines[lineIdx].split('|') // Explode data fields.
          tableRow = document.createElement('tr')
          if (lineIdx === 0) cellTag = 'th' // head cell
          else cellTag = 'td' // body cell
          for (let cellIdx = 0; cellIdx < line.length; cellIdx++) {
            cell = document.createElement(cellTag)
            cell.textContent = line[cellIdx]
            tableRow.appendChild(cell)
          }
          newTable.appendChild(tableRow)
        }
        console.log(`Response type: ${e.target.responseType}`)
        console.log('e.target.response:')
        console.log(e.target.response)
        console.log('e.target.responseText:')
        console.log(e.target.responseText)
        console.log('e.target.responseXML:')
        console.log(e.target.responseXML)
      })
      req.send()
    }

    document.querySelector('select').addEventListener('change', displayData)
    displayData() // Make sure the first file gets displayed at startup.
  </script>
</head>
<body>
<main>
  <select>
    <option>Data set 1</option>
    <option>Data set 2</option>
    <option>Data set 3</option>
  </select>
  <aside></aside>
</main>
</body>
</html>

We can easily use AJAX to upload files to a server. If we want to monitor the upload progress, all we need to do is assign an event handler to the upload property of our XMLHttpRequest. This handler will automatically receive a ProgressEvent (cf. developer.mozilla.org/en-US/docs/Web/API/ProgressEvent) object as argument. Using the three properties lengthComputable, loaded and total we can monitor the upload progress.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!DOCTYPE html>
<html lang=en>
<head>
  <title>File upload to server</title>
  <meta charset=utf-8>
  <style>
      section {
          width:      500px;
          resize:     both;
          overflow:   auto;
          border:     2px groove darkorange;
          text-align: center;
      }
  </style>
  <script type=module>
    // Adapted from the 2nd edition of "HTML5 for Masterminds" p. 396-398.
    let dataBox

    const init = () => {
      dataBox = document.querySelector('section')
      dataBox.addEventListener('dragenter', e => e.preventDefault())
      dataBox.addEventListener('dragover', e => e.preventDefault())
      dataBox.addEventListener('drop', dropped)
    }

    const dropped = e => {
      e.preventDefault()
      const files = e.dataTransfer.files
      if (files.length) {
        let list = ''
        for (let f = 0; f < files.length; f++) {
          list += `<div>File: ${files[f].name}`
          list += '<br><span><progress value=0 max=100>0%</progress></span>'
          list += '</div>'
        }
        dataBox.innerHTML = list
        let count = 0
        const upload = () => {
          const myfile = files[count]
          const data = new FormData()
          data.append('file', myfile)
          const url = 'AJAX4.php'
          const request = new XMLHttpRequest()
          const xmlupload = request.upload
          xmlupload.addEventListener('progress', e => {
            if (e.lengthComputable) {
              let child = count + 1
              const per = parseInt(e.loaded / e.total * 100)
              const progressBar =
                dataBox.querySelector(`div:nth-child(${child}) > span > progress`)
              progressBar.value = per
              progressBar.innerHTML = `${per}%`
            }
          })
          request.addEventListener('load', () => {
            const child = count + 1
            const elem = dataBox.querySelector(`div:nth-child(${child}) > span`)
            elem.innerHTML = 'done!'
            count++
            if (count < files.length) upload()
          })
          request.open('POST', url)
          request.send(data)
        }
        upload()
      }
    }

    addEventListener('load', init)
  </script>
</head>
<body>
<main>
  <section>
    <p>Drop files here</p>
  </section>
</main>
</body>
</html>
1
2
3
4
5
<?PHP
  // Only allow authenticated users to upload files to your server!
error_log(print_r($_FILES, true));
  move_uploaded_file($_FILES['file']['tmp_name'], 'upload/'.$_FILES['file']['name']);
?>
4.3.26.1.5. setRequestHeader(header, value)

In some cases we need to specify specific HTTP headers to give the server additional information with regards to the data we want to send and/or receive. setRequestHeader sets the value of the HTTP request header. If used, it must be called after open but before send. If this method is called several times with the same header, the values are merged into one single request header. The official header list can be found at www.iana.org/assignments/message-headers/message-headers.xml#perm-headers and the official value list for the Content-Encoding header, which is the most often used one for our purposes, can be found at www.iana.org/assignments/media-types/media-types.xhtml.

4.3.26.2. Cross-origin requests
The same-origin policy restricts how a document or script loaded from one origin can interact with a resource from another origin.

Cross-Origin Resource Sharing (CORS) is one way to get around these restrictions. The details can be found at developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS.

In its simplest form, to give everyone access, we can just add the following at the top of our PHP script:

1
header('Access-Control-Allow-Origin: *');

Another possibility is to use JSONP (JSON with padding) as explained in en.wikipedia.org/wiki/JSONP. For a practical application example, study WMOTU Invaders.

[TIP] If you want to get around the CORS problem for your local development, i.e. for accessing locally stored files, see www.thepolyglotdeveloper.com/2014/08/bypass-cors-errors-testing-apis-locally.

4.3.26.3. Fetch

The Fetch API provides a JavaScript interface for accessing and manipulating parts of the HTTP pipeline, such as requests and responses. It also provides a global fetch() method that provides an easy, logical way to fetch resources asynchronously across the network.

This kind of functionality was previously achieved using XMLHttpRequest. Fetch provides a better alternative that can be easily used by other technologies such as Service Workers. Fetch also provides a single logical place to define other HTTP-related concepts such as CORS and extensions to HTTP.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>Fetch demo</title>
  <script type=module>
    fetch('../camaro256x256.png').then(response => {
      if (response.ok)
        response.blob().then(blob =>
          document.querySelector('img').src = URL.createObjectURL(blob))
      else console.log('Network response was not ok.')
    }).catch(error =>
      console.log(`There has been a problem with your fetch operation: ${error.message}`))

    // Old fashioned alternative
    const req = new XMLHttpRequest()
    req.open('GET', '../ferrari256x256.png')
    // cf. http://stackoverflow.com/questions/20035615/using-raw-image-data-from-ajax-request-for-data-uri
    req.responseType = 'arraybuffer'
    req.addEventListener('load', e => {
      const blob = new Blob([e.target.response])
      document.querySelectorAll('img')[1].src = URL.createObjectURL(blob)
    })
    req.send()
  </script>
</head>
<body>
<img>
<img>
</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>Open new window in JS</title>
  <script type=module>
    // Note that most browsers prevent scripts from openeing new windows/tabs if they were not triggered by the user.
    // And even then the user needs to unblock them.
    document.querySelector('button').addEventListener('click', () => {
      alert('You might need to unblock popups!')
      const win1 = window.open('https://apess.lu')
      const win2 = window.open()
      if (win2) fetch('fetch1.html').then(res => {
        if (res.ok) {
          res.text().then(text => {
            win2.document.open()
            win2.document.write(text)
            win2.document.close()
          })
        }
      })
    })
  </script>
</head>
<body>
<button>Open new window</button>
</body>
</html>

Here is an example application to ease the monitoring of the PHP error log file:

4.3.26.3.1. index.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>PHP log monitor</title>
  <style>
      main {
          position: fixed;
          top:      40px;
          bottom:   0;
          left:     0;
          right:    0;
          overflow: auto;
      }
  </style>
  <script type=module>
    const displayLogFile = async () => {
      try {
        const logFile = await fetch('https://students.btsi.lu/evegi144/.log/error.log')
        if (logFile && logFile.ok) {
          const main = document.querySelector('main')
          const text = await logFile.text()
          const lines = text.split('\n')
          main.innerHTML = ""
          for (const line of lines) main.innerHTML += line + '<br>'
          main.scrollTop = main.scrollHeight
        }
      } catch (err) {
        console.log(err)
      }
    }

    const deleteLogFile = async () => {
      try {
        await fetch('PHPLogMonitor.php')
        displayLogFile()
      } catch (err) {
        console.log(err)
      }
    }

    const main = async () => {
      try {
        await displayLogFile()
        addEventListener('focus', displayLogFile)
        document.querySelector('#reload').addEventListener('click', displayLogFile)
        document.querySelector('#delete').addEventListener('click', deleteLogFile)
      } catch (err) {
        console.log(err)
      }
    }

  await main()
  </script>
</head>
<body>
<header>
  <button id=reload>Reload</button>
  <button id=delete>Delete</button>
</header>
<main></main>
</body>
</html>
4.3.26.3.2. PHPLogMonitor.php
1
2
3
<?php
  fclose(fopen('/var/www/html/evegi144/.log/error.log','w'));
?>

Study the following links for a detailed demonstration and explanation of the differences between fetch and XMLHttpRequest:

jakearchibald.com/2015/thats-so-fetch

developers.google.com/web/updates/2015/03/introduction-to-fetch

4.3.27. JSON

JavaScript Object Notation (JSON) is a data serialization format often used to exchange data, including complex objects, between server and client. Objects are converted into a JSON string, which is sent to/from the server from/to the client.

JSON is based on a subset of JavaScript (cf. json.org, www.w3schools.com/js/js_json_intro.asp and en.wikipedia.org/wiki/JSON). Data consists of name/value pairs separated by commas and embedded within {}. [] are used for arrays.

Key names and strings need to be enclosed in double quotes.

Use jsonlint.com to verify that a given string is valid JSON.

If you use a version of Firefox that is older than version 53, you should turn the JSON viewer on as described in www.ghacks.net/2017/01/12/firefox-53-json-viewer-on-by-default.

In JavaScript, we use JSON.stringify to encode and JSON.parse to decode a JSON string.

You can play around with JSON at www.tutorialspoint.com/online_json_editor.htm.

Example:

1
2
3
4
5
const myObj = {firstName: "Donald", lastName: "Duck"}
// Convert JS object to JSON string: '{"firstName":"Donald","lastName":"Duck"}'
console.log(JSON.stringify(myObj))
// -> Convert string back to JS object: {firstName: "Donald", lastName: "Duck"}
try {JSON.parse(JSON.stringify(myObj))} catch(e) {console.log(`Error: ${e}`)}

In PHP, we should first tell the browser to expect to receive JSON by sending the header application/json. We use json_encode to encode a PHP object into a JSON string and json_decode to decode a JSON string into a PHP object. Note that the latter can be given a second parameter in order to have returned objects converted into associative arrays.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Simple JSON data retrieval via AJAX and PHP</title>
  <meta charset=utf-8>
  <script type=module>
    let dataJSON

    const displayData = data => {
      const output = document.querySelector('p')
      console.dir(data)
      const keys = Object.keys(data) // Get the keys of the object.
      for (let i = 0; i < keys.length; i++) { // Display the key/value pairs.
        output.innerHTML += `${keys[i]}: ${data[keys[i]]}<br>`
      }
    }

    // Old approach
    const req = new XMLHttpRequest()
    req.open('POST', 'JSON1.php')
    req.addEventListener('load', e => {
      displayData(JSON.parse(e.target.response))
    })
    req.send()

    // New approach
    fetch('JSON1.php', {
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      method: "POST"
    }).then(response => response.json()).then(data => {
      displayData(data)
    }).catch(error =>
      console.log(`There has been a problem with the fetch operation: ${error.message}`))
  </script>
</head>
<body>
<main><p></p></main>
</body>
</html>
1
2
3
4
5
<?php
  header("Content-Type: application/json; charset=UTF-8");
  $arr = array("INFOR" => 53, "MATHE" => 45);
  echo json_encode($arr); // Send the array as a JSON string to the client browser.
?> 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Sending JavaScript array and object to PHP and back</title>
  <meta charset=utf-8>
  <script type=module>
    const displayData = data => {
      const output = document.querySelector('p')
      const keys = Object.keys(data) // Get the keys of the object.
      for (let i = 0; i < keys.length; i++) { // Display the key/value pairs.
        output.innerHTML += `${keys[i]}: ${data[keys[i]]}<br>`
      }
    }

    // Old approach
    const req1 = new XMLHttpRequest(), req2 = new XMLHttpRequest()
    const arr = ['s1', 's2'] // A simple array.
    const obj = {width: 15, name: 'abc'} // A simple object.
    req1.open('POST', 'JSON2a.php')
    req1.addEventListener('load', e => displayData(JSON.parse(e.target.response)))
    req1.send(JSON.stringify(arr)) // Send array as JSON string to the server script.
    req2.open('POST', 'JSON2b.php')
    req2.addEventListener('load', e => displayData(JSON.parse(e.target.response)))
    req2.send(JSON.stringify(obj)) // Send object as JSON string to server script.

    // New approach
    const headers = {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    }
    fetch('JSON2a.php', {
      headers: headers,
      method: "POST",
      body: JSON.stringify(arr)
    }).then(response => response.json()).then(data => displayData(data)).catch(
      error =>
        console.log(`There has been a problem with the fetch operation: ${error.message}`))
    fetch('JSON2b.php', {
      headers: headers,
      method: "POST",
      body: JSON.stringify(obj)
    }).then(response => response.json()).then(data => displayData(data)).catch(
      error =>
        console.log(`There has been a problem with the fetch operation: ${error.message}`))
  </script>
</head>
<body>
<main><p></p></main>
</body>
</html>

In order to read JSON data sent by the client, we use php://input. See stackoverflow.com/questions/8893574/php-php-input-vs-post.

1
2
3
4
5
6
7
8
9
<?php
  header("Content-Type: application/json; charset=UTF-8");
  // http://stackoverflow.com/questions/8599595/send-json-data-from-javascript-to-php
  $arr = json_decode(file_get_contents('php://input')); // Decode the JSON string.
  if ($arr) {
    $arr[0] = 'OK'; // Modify it.
    echo json_encode($arr); // Send the array as a JSON string to the client browser.
  }
?> 
1
2
3
4
5
6
7
8
9
<?php
  header("Content-Type: application/json; charset=UTF-8");
  // http://stackoverflow.com/questions/8599595/send-json-data-from-javascript-to-php
  $obj = json_decode(file_get_contents('php://input')); // Decode the JSON string
  if ($obj) {
    $obj->name = 'OK'; // Modify it.
    echo json_encode($obj); // Send the object as a JSON string to the client browser.
  }
?> 

Here is a simple example of reading JSON data from a text file and displaying it in an HTML table:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[
  {
    "brand": "BMW",
    "model": "120d",
    "colour": "black",
    "consumption": 4.5
  },
  {
    "brand": "Audi",
    "model": "A5",
    "colour": "green",
    "consumption": 6.7
  }
]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Reading JSON data from a text file</title>
  <meta charset=utf-8>
  <script type=module>
    const displayData = e => {
      const data = JSON.parse(e.target.response)
      const output =
        `<table><tr><th>brand</th><th>model</th><th>colour</th><th>consumption</th>
            </tr><tr><td>${data[0]['brand']}</td><td>${data[0]['model']}</td>
            <td>${data[0]['colour']}</td><td>${data[0]['consumption']}</td></tr>
            <tr><td>${data[1]['brand']}</td><td>${data[1]['model']}</td>
            <td>${data[1]['colour']}</td><td>${data[1]['consumption']}</td></tr></table>`
      console.dir(data)
      document.querySelector('p').innerHTML = output
    }

    const req = new XMLHttpRequest()
    req.open('POST', 'cars.json')
    req.addEventListener('load', displayData)
    req.send()
  </script>
</head>
<body>
<main><p></p>
</main>
</body>
</html>

Here is an example of exchanging more complex objects between client and server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Sending complex JS objects to PHP and back</title>
  <meta charset=utf-8>
  <script type=module>
    const displayData = e => {
      const dataJSON = JSON.parse(e.target.response)
      console.dir(dataJSON)
    }
    const req = new XMLHttpRequest()
    const obj1 = {elems: [1, 2, 3, 4, 5], name: 'abc'}
    const obj2 = {elems: [6, 7, 8, 9, 10], name: 'def'}
    const arr = [obj1, obj2]
    req.open('POST', 'JSON4.php')
    req.addEventListener('load', displayData)
    console.log(JSON.stringify(arr))
    req.send(JSON.stringify(arr)) // Send array as JSON string to the server script.
  </script>
</head>
<body>
<main><p></p></main>
</body>
</html>
1
2
3
4
5
6
7
8
9
<?php
  header("Content-Type: application/json; charset=UTF-8");
  // http://stackoverflow.com/questions/8599595/send-json-data-from-javascript-to-php
  $obj = json_decode(file_get_contents('php://input')); // Decode the JSON string.
  if ($obj) {
    $obj[0]->name = 'OK'; // Modify it.
    echo json_encode($obj); // Send the object as a JSON string to the client browser.
  }
?> 
Note that functions cannot be stored directly in JSON. There are ways around this but they should be used only if really needed and with great care, see stackoverflow.com/questions/36517173/how-to-store-a-javascript-function-in-json.
4.3.27.1. JSON Lines

jsonlines.org

4.3.28. Application Programming Interfaces (API)

platform.html5.org

html5demos.com

To find out which HTML5 APIs your browser supports, use html5test.com.

If you want to develop your own JSON API, take a look at jsonapi.org.

4.3.28.1. File system

The File API allows our app to represent and access file objects. The file System API provides file system access.

www.w3.org/TR/FileAPI

developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API

fs.spec.whatwg.org

www.html5rocks.com/en/tutorials/file/dndfiles

web.dev/read-files

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>File API demo 1</title>
  <script type=module>
    const handleFile = e => {
      const file = e.target.files[0]
      const reader = new FileReader()
      const display = e => document.querySelector('section').innerHTML = e.target.result
      reader.addEventListener('load', display)
      reader.readAsBinaryString(file)
      document.querySelector('span').innerHTML = `${file.size} bytes`
    }

    document.querySelector('input').addEventListener('change', handleFile)
  </script>
</head>
<body>
<header>
  <input type=file>
  File size: <span></span>
</header>
<section></section>
</body>
</html>

Here’s the same example but displaying images: students.btsi.lu/evegi144/WAD/JS/file2.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>File API demo 2</title>
  <script type=module>
    const handleFile = e => {
      const file = e.target.files[0]
      const reader = new FileReader()
      const display = e => {
        const img = document.createElement('img')
        img.src = e.target.result
        document.body.appendChild(img)
      }
      reader.addEventListener('load', display)
      reader.readAsDataURL(file)
      document.querySelector('span').innerHTML = `${file.size} bytes`
    }

    document.querySelector('input').addEventListener('change', handleFile)
  </script>
</head>
<body>
<header>
  <input type=file>
  File size: <span></span>
</header>
</body>
</html>
If you need to access file meta data, have a look at jdataview.github.io/jDataView.
4.3.28.2. Drag and drop

To make any object drag and droppable it needs to have its position attribute set to absolute or fixed. We can then use Peter-Paul Koch’s dragDrop object (cf. www.quirksmode.org/js/dragdrop.html), which does not use the drag and drop API but implements a solution based on the classic mousemove and mouseup events. It works very well across browsers. A stripped-down version is used in the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Drag and drop example 1</title>
  <meta charset=UTF-8>
  <style>
      .draggable {
          position:         fixed;
          cursor:           move;
          background-color: lightgreen;
      }
  </style>
  <script type=module> // Based on http://www.quirksmode.org/js/dragdrop.html
  const dragDrop = {
    initialMouseX: undefined,
    initialMouseY: undefined,
    startX: undefined,
    startY: undefined,
    draggedObject: undefined,
    initElement: element => {
      if (typeof element == 'string') element = document.getElementById(element)
      element.onmousedown = dragDrop.startDragMouse
      element.className += ' draggable'
    },
    startDragMouse: e => {
      dragDrop.startDrag(e.target)
      var evt = e || window.event
      dragDrop.initialMouseX = evt.clientX
      dragDrop.initialMouseY = evt.clientY
      document.addEventListener('mousemove', dragDrop.dragMouse)
      document.addEventListener('mouseup', dragDrop.releaseElement)
      return false
    },
    startDrag: obj => {
      if (dragDrop.draggedObject) dragDrop.releaseElement()
      dragDrop.startX = obj.offsetLeft
      dragDrop.startY = obj.offsetTop
      dragDrop.draggedObject = obj
      obj.className += ' dragged'
    },
    dragMouse: e => {
      var evt = e || window.event
      var dX = evt.clientX - dragDrop.initialMouseX
      var dY = evt.clientY - dragDrop.initialMouseY
      dragDrop.setPosition(dX, dY)
      return false
    },
    setPosition: (dx, dy) => {
      dragDrop.draggedObject.style.left = `${dragDrop.startX + dx}px`
      dragDrop.draggedObject.style.top = `${dragDrop.startY + dy}px`
      console.log(`${dx} ${dy}`)
    },
    releaseElement: () => {
      document.removeEventListener('mousemove', dragDrop.dragMouse)
      document.removeEventListener('mouseup', dragDrop.releaseElement)
      dragDrop.draggedObject.className =
        dragDrop.draggedObject.className.replace(/dragged/, '')
      dragDrop.draggedObject = null
    }
  }

  dragDrop.initElement('art1')
  dragDrop.initElement('art2')
  </script>
</head>
<body>
<main>
  <article id=art1>
    This is a draggable article
  </article>
  <article id=art2>
    This is another draggable article
  </article>
</main>
</body>
</html>

The following two examples show a Window class to create a draggable and resizable window:

window1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Window {
  constructor(id, width, height, top, left, bgColor, resizable) {
    this.elem = document.createElement('aside')
    this.elem.style.position = 'absolute'
    this.elem.style.width = width >= 0 ? width + 'px' : '100px'
    this.elem.style.height = height >= 0 ? height + 'px' : '100px'
    this.elem.style.minWidth = '30px'
    this.elem.style.minHeight = '30px'
    this.elem.style.border = '2px solid black'
    this.elem.style.resize = 'both'
    this.elem.style.overflow = 'auto'
    this.elem.style.cursor = 'move'
    this.elem.id = id
    this.dragging = false
    // cf. http://stackoverflow.com/questions/18942402/unable-to-remove-an-bound-event-listener
    this.mousemoveListener = undefined
    this.initialMouseX = undefined
    this.initialMouseY = undefined
    this.initialElemX = undefined
    this.initialElemY = undefined
    this.startDrag = e => {
      if (this.dragging) return
      this.dragging = true
      this.initialMouseX = e.clientX
      this.initialMouseY = e.clientY
      this.initialElemX = this.elem.offsetLeft
      this.initialElemY = this.elem.offsetTop
      this.elem.addEventListener('mousemove', this.mousemoveListener)
    }
    this.drag = e => {
      this.elem.style.left = (this.initialElemX + e.clientX - this.initialMouseX) + 'px'
      this.elem.style.top = (this.initialElemY + e.clientY - this.initialMouseY) + 'px'
    }
    this.stopDrag = e => {
      this.dragging = false
      this.elem.removeEventListener('mousemove', this.mousemoveListener)
    }
    const btn = document.createElement('button')
    btn.innerHTML = 'X'
    btn.addEventListener('click', e => {
      document.body.removeChild(e.target.parentElement)
    })
    btn.style.position = 'absolute'
    btn.style.right = '0'
    btn.style.top = '0'
    this.elem.appendChild(btn)
    this.elem.addEventListener('mousedown', this.startDrag.bind(this))
    this.elem.addEventListener('mouseup', this.stopDrag.bind(this))
    document.body.appendChild(this.elem)
    this.mousemoveListener = this.drag
  }
}

new Window('w1', 300, 200, 10, 10, 'green', true)
window2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Window class demo 2</title>
    <meta charset=utf-8>
    <style>
      #w1, #w2, #w3 {
        position:         absolute;
        overflow:         auto;
        width:            300px;
        height:           200px;
        min-width:        30px;
        min-height:       30px;
        background-color: green;
        resize:           both;
        border:           2px solid black;
      }
    </style>
    <script src=window2.js type=module></script>
  </head>
  <body></body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Window {
  constructor(id) {
    this.elem = document.createElement('aside')
    this.elem.id = id
    this.dragging = false
    // cf. http://stackoverflow.com/questions/18942402/unable-to-remove-an-bound-event-listener
    this.mousemoveListener = undefined
    this.mouseUpListener = undefined
    this.initialMouseX = undefined
    this.initialMouseY = undefined
    this.initialElemX = undefined
    this.initialElemY = undefined
    this.nav = undefined
    this.startDrag = e => {
      if (this.dragging) return
      this.dragging = true
      this.initialMouseX = e.clientX
      this.initialMouseY = e.clientY
      this.initialElemX = this.elem.offsetLeft
      this.initialElemY = this.elem.offsetTop
      addEventListener('mousemove', this.mousemoveListener)
      addEventListener('mouseup', this.mouseupListener)
    }
    this.drag = e => {
      let x = this.initialElemX + e.clientX - this.initialMouseX
      let y = this.initialElemY + e.clientY - this.initialMouseY
      const elWidth = this.elem.getBoundingClientRect().width
      const elHeight = this.elem.getBoundingClientRect().height
      if (x < 0) x = 0
      else if ((x + elWidth) > innerWidth) x = innerWidth - elWidth
      if (y < 0) y = 0
      else if ((y + elHeight) > innerHeight) y = innerHeight - elHeight
      this.elem.style.left = `${x}px`
      this.elem.style.top = `${y}px`
    }
    this.stopDrag = e => {
      this.dragging = false
      removeEventListener('mousemove', this.mousemoveListener)
      removeEventListener('mouseup', this.mouseupListener)
    }
    const nav = document.createElement('nav')
    nav.style.backgroundColor = 'lightblue'
    nav.style.height = '25px'
    nav.style.cursor = 'move'
    this.nav = nav
    nav.addEventListener('mousedown', this.startDrag)
    const btn = document.createElement('button')
    btn.innerHTML = 'X'
    btn.addEventListener('click', e =>
      document.body.removeChild(e.target.parentElement.parentElement))
    btn.style.position = 'absolute'
    btn.style.right = '0'
    btn.style.top = '0'
    nav.appendChild(btn)
    this.elem.appendChild(nav)
    document.body.appendChild(this.elem)
    this.mousemoveListener = this.drag
    this.mouseupListener = this.stopDrag
  }
}

const win1 = new Window('w1')
const win2 = new Window('w2')
const win3 = new Window('w3')

If we want to use the HTML5 drag and drop API instead of this object, we need to set the draggable attribute to true. See developer.mozilla.org/en-US/docs/DragDrop/Drag_and_Drop and developers.whatwg.org/dnd.html#dnd for an in-depth explanation of drag and drop.

4.3.28.3. Web Workers
This script will use 100% of the processing power of a 4 core CPU.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Web Workers Demo 1</title>
  <meta charset=utf-8>
  <script type=module>
    // http://stackoverflow.com/questions/11871452/can-web-workers-utilize-100-of-a-
    // multi-core-cpu
    //window.URL = window.URL || window.webkitURL;
    const blob = new Blob(["while(true){}"], {type: 'text/javascript'})
    const code = window.URL.createObjectURL(blob)
    new Worker(code)
    new Worker(code)
    new Worker(code)
    new Worker(code)
  </script>
</head>
<body>
</body>
</html>
4.3.28.4. Server-Sent Events

This API allows the opening of an HTTP connection for receiving push notifications from a server in the form of DOM events. The specification can be found at html.spec.whatwg.org/#server-sent-events. Good descriptions and examples can be found at:

www.html5rocks.com/en/tutorials/eventsource/basics

html5doctor.com/server-sent-events

www.sitepoint.com/server-sent-events

developer.mozilla.org/en-US/docs/Server-sent_events/Using_server-sent_events

Here is a simple example that sends the current server time to the client every second. Take a look at the networking tab of the browser console. The communication takes place without new HTTP requests, as the existing one is kept alive. This is more efficient than using AJAX polling on the client side, i.e. each client checking with the server every second to see whether any new data has arrived.

When using sessions, we must call session_write_close, otherwise the session object will be locked and no other script can use it, given that our SSE-server runs an endless loop.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Server-Sent Events Demo</title>
  <meta charset=utf-8>
  <script type=module>
    const source = new EventSource('SSE_server.php')
    source.addEventListener('message', e =>
      document.querySelector('main').innerHTML = `${e.data}<br>`)
    source.addEventListener('open', e =>
      document.querySelector('main').innerHTML += 'Connection opened<br>')
    source.addEventListener('error', e =>
      document.querySelector('main').innerHTML += 'Error<br>')
  </script>
</head>
<body>
<main>
</main>
</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
  // From http://stackoverflow.com/questions/9070995/
  // html5-server-sent-events-prototyping-ambiguous-error-and-repeated-polling
  header('Content-Type: text/event-stream');
  header('Cache-Control: no-cache');

  function sendMsg($id, $msg) {
    //echo "id: $id" . PHP_EOL;
    echo "data: $msg" . PHP_EOL;
    echo PHP_EOL;
    ob_flush();
    flush();
  }

  date_default_timezone_set('Europe/Luxembourg');
  while (true) {
    $serverTime = time();
    $msg        = 'Server time: ' . date("h:i:s", time());
    sendMsg($serverTime, $msg);
    //session_write_close(); // Only needed if we work with sessions in this script.
    sleep(1);
  }
?>

Nicolas Detombes has developed a chat app that nicely illustrates how SSE can be used:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php
  // Created by Nicolas Detombes
  require_once 'chat_database.php';
?>
<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <title>Chat</title>
  <style>
      body {
          font-family: Arial, sans-serif;
      }

      textarea {
          width:       700px;
          height:      200px;
          border:      3px solid #cccccc;
          padding:     5px;
          font-family: Arial, sans-serif;
          resize:      none;
      }

      input {
          border:      3px solid #cccccc;
          padding:     5px;
          font-family: Arial, sans-serif;
      }
  </style>
</head>
<body>
<h2>Chat</h2>
<textarea disabled></textarea>
<form method='post'><br>
  <input name='comment' id='comment' required>
  <input type='submit' value='Send'>
</form>
<script type=module>
  function init() {
    const source = new EventSource('chat_load.php')
    source.addEventListener('message', function (e) {
      //get the 20 first chars of the last line in the textarea
      const textarea = document.querySelector('textarea')
      const content = textarea.value
      const lastLine = content.substr(content.lastIndexOf("[ ID ]"), 20)
      //console.log(e.data.substr(0, 20));
      //console.log(lastLine);
      if (e.data.substr(0, 20) !== lastLine) {//if first 10 chars do not match (id's)
        textarea.value += e.data + '\n'
        //auto-scroll down textarea
        textarea.scrollTop = textarea.scrollHeight - textarea.clientHeight
      }
    })

    document.forms[0].addEventListener('submit', function (e) {
      e.preventDefault()
      const data = new FormData(document.forms[0]), req = new XMLHttpRequest()
      req.addEventListener('load', displayData)
      req.open('POST', 'chat_comment.php')
      req.send(data)
      document.getElementById('comment').value = ''
    })
  }

  function displayData(e) {
    document.querySelector('textarea').innerHTML += e.target.response + '\n'
  }

  init()
</script>
<?php
  echo '<script>document.querySelector("textarea").value = "";</script>';
  //load the last 5 posts
  $posts = Database::listPost();
  foreach ($posts as $post) {
    $data = '[ ID ] ' . $post[0] . ' [ TIME ] ' . $post[1] . ' [ CONTENT ] ' . $post[2] .
      '\n';
    echo '<script>document.querySelector("textarea").value += "' . $data . '";</script>';
  }
?>
</body>
</html>
1
2
3
4
5
6
7
8
<?php
  require_once 'chat_database.php';

  if (isset($_POST['comment'])) {
    //shouldn't be needed since comment input has required set to yes
    Database::post($_POST['comment']);
  }
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php

  require_once 'chat_credentials.php';

  class Database {

    private static $DB_HOST;
    private static $DB_USER;
    private static $DB_PASSWORD;
    private static $DB_NAME;
    private static $DB_TABLE_POST = 'tblPost';

    static function set_credentials($db_host, $db_user, $db_password, $db_name) {
      self::$DB_HOST     = $db_host;
      self::$DB_USER     = $db_user;
      self::$DB_PASSWORD = $db_password;
      self::$DB_NAME     = $db_name;
    }

    static function connect() {
      $dbc = @mysqli_connect(self::$DB_HOST, self::$DB_USER,
        self::$DB_PASSWORD, self::$DB_NAME) or
        die('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
      mysqli_set_charset($dbc, "utf8");
      return $dbc;
    }

    static function post($dtPost) {
      $dbc   = self::connect();
      $query = 'INSERT INTO ' . self::$DB_TABLE_POST . ' (dtPost, fiUser, fiGroup) VALUES (?,
        1, 1)';
      $stmt  = $dbc->prepare($query);
      if (!$stmt)
        trigger_error('Wrong SQL: ' . $query . ' Error: ' . $dbc->error, E_USER_ERROR);
      $stmt->bind_param('s', $dtPost);
      $stmt->execute();
      $stmt->close();
      $dbc->close();
    }

    static function listPost() {
      //select last 5 records WHERE [id] > (SELECT MAX([id]) - 5 FROM [MyTable]) NOT ORDER BY
      // idPost ASC limit 5
      $idPost     = '';
      $tTimestamp = '';
      $Post       = '';
      $dbc        = self::connect();
      $query      = 'SELECT idPost, dtTimestamp, dtPost FROM ' . self::$DB_TABLE_POST .
        ' WHERE idPost > (SELECT MAX(idPost) - 5 FROM ' . self::$DB_TABLE_POST . ')';
      $stmt       = $dbc->prepare($query);
      $stmt->execute();
      $stmt->store_result();
      for ($i = 0; $i < $stmt->num_rows; $i++) {
        $stmt->bind_result($idPost, $tTimestamp, $Post);
        $stmt->fetch();
        $result[] = array($idPost, $tTimestamp, $Post);
      }
      $stmt->close();
      $dbc->close();
      return $result;
    }

    static function lastPost() {
      $idPost     = '';
      $tTimestamp = '';
      $Post       = '';
      $dbc        = self::connect();
      $query      = 'SELECT idPost, dtTimestamp, dtPost FROM ' . self::$DB_TABLE_POST .
        ' WHERE idPost = (SELECT MAX(idPost)  FROM ' . self::$DB_TABLE_POST . ')';
      $stmt       = $dbc->prepare($query);
      $stmt->execute();
      $stmt->store_result();
      $stmt->bind_result($idPost, $tTimestamp, $Post);
      $stmt->fetch();
      $result = array($idPost, $tTimestamp, $Post);
      $stmt->close();
      $dbc->close();
      return $result;
    }
  }
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
  require_once 'chat_database.php';

  header('Content-Type: text/event-stream');
  header('Cache-Control: no-cache');

  function sync($data) {
    echo 'data: [ ID ] ' . $data[0] . ' [ TIME ] ' . $data[1] . ' [ CONTENT ] ' . $data[2] .
      PHP_EOL;
    echo PHP_EOL;
    ob_flush();
    flush();
  }

  while (true) {
    sync(Database::lastPost());
    sleep(1);
  }
?>
4.3.28.5. Canvas

Here is a pacman trying to catch the mouse cursor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Pacman</title>
  <meta charset=UTF-8>
  <style>
    body {
      background-color: black;
      overflow: hidden;
      cursor: url("mouseWithCheese64x64.cur"), auto;
    }
  </style>
  <script type=module>
    const canvas = document.createElement("canvas")

    const sizeCanvas = () => {
      canvas.width = window.innerWidth
      canvas.height = window.innerHeight
    }
    sizeCanvas()
    document.querySelector("body").appendChild(canvas)
    const context = canvas.getContext('2d')
    const pacman = new Image()
    pacman.src = "pacman128x128.png"
    pacman.alt = "pacman128x128.png"

    const TO_RADIANS = Math.PI / 180, TIMEOUT = 20
    let mouseX = 0, mouseY = 0, currX = 100, currY = 100, timerID = null

    const drawRotatedImage = (image, x, y, angle) => {
      // Save the current co-ordinate system before we play with it.
      context.save()

      // Move to the middle of where we want to draw our image.
      context.translate(x, y)

      // Rotate around that point, converting our angle from degrees to radians.
      context.rotate(angle * TO_RADIANS)

      // Draw it up and to the left by half the width and height of the image.
      context.drawImage(image, -(image.width / 2), -(image.height / 2))

      // Restore the co-ords to what they were when we began.
      context.restore()
    }

    const getMouseXY = e => {
      mouseX = e.pageX
      mouseY = e.pageY
    }

    const animate = () => {
      const dX = currX - mouseX, dY = currY - mouseY
      const a = Math.floor(dX / 20), b = Math.floor(dY / 20)
      currX -= a
      currY -= b
      if ((Math.abs(a) >= 1) || (Math.abs(b) >= 1)) {
        context.clearRect(0, 0, canvas.width, canvas.height)
        drawRotatedImage(pacman, currX, currY, (Math.atan2(dY, dX) - Math.PI) * 180 / Math.PI)
      }
    }

    const toggle = () => {
      if (timerID === null) timerID = setInterval(animate, TIMEOUT)
      else {
        clearInterval(timerID)
        timerID = null
      }
    }

    addEventListener('mousemove', getMouseXY)
    addEventListener('click', toggle)
    addEventListener('resize', sizeCanvas)
    timerID = setInterval(animate, TIMEOUT)
  </script>
</head>
<body>
</body>
</html>

Here is a skeleton for a pong game:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Pong</title>
    <meta charset=utf-8>
    <script src=pong.js type=module></script>
  </head>
  <body>
    <canvas width=1000 height=780></canvas>
    <button>Stop</button>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const BAT_WIDTH = 10, BAT_HEIGHT = 100
let timer, posLX, posLY, posRX, posRY, posBallX, posBallY, ballDX, ballDY, posbatLX,
  posbatLY, posbatRX, posbatRY, radius, canvas, ctx, running = true

const init = () => {
  canvas = document.querySelector('canvas')
  ctx = canvas.getContext("2d")
  posLX = 2
  posLY = 340
  posRX = 988
  posRY = 340
  posBallX = 500
  posBallY = 360
  ballDX = 3
  ballDY = 3
  radius = 20
  posbatLX = 2
  posbatLY = 340
  posbatRX = 988
  posbatRY = 340
  document.querySelector('button').addEventListener('click', toggleAnimation)
  requestAnimationFrame(gameLoop)
}

const gameLoop = () => {
  ctx.fillStyle = "#000000"
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  ctx.fillStyle = "#FFFFFF"
  ctx.fillRect(posLX, posLY, 10, 100)
  ctx.fillRect(posRX, posRY, 10, 100)
  ctx.beginPath()
  ctx.arc(posBallX, posBallY, radius, 0, 2 * Math.PI)
  ctx.fill()
  // Check for collision with canvas borders.
  if (((posBallX + radius) >= canvas.width) || ((posBallX - radius) <= 0)) ballDX = -ballDX
  if (((posBallY + radius) >= canvas.height) || ((posBallY - radius) <= 0)) ballDY = -ballDY
  ctx.fillRect(posbatLX, posbatLY, BAT_WIDTH, BAT_HEIGHT)
  ctx.fillRect(posbatRX, posbatRY, BAT_WIDTH, BAT_HEIGHT)
  if ((posBallX - radius) <= (posbatLX + BAT_WIDTH) &&
    (BAT_HEIGHT + posbatLY >= (posBallY - radius)) && (posbatLY <= (posBallY + radius)))
    ballDX = Math.abs(ballDX)
  if ((posBallX + radius) >= posbatRX && (BAT_HEIGHT + posbatRY >= (posBallY - radius))
    && (posbatRY <= (posBallY + radius))) ballDX = -Math.abs(ballDX)
  posBallX += ballDX
  posBallY += ballDY
  if (running) requestAnimationFrame(gameLoop)
}

const toggleAnimation = () => {
  running = !running
  if (running) {
    requestAnimationFrame(gameLoop)
    document.querySelector('button').innerHTML = 'Stop'
  } else document.querySelector('button').innerHTML = 'Resume'
}

init()

And here is a random maze generator class:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Canvas 2D maze demo</title>
  <meta charset=utf-8>
  <script type=module>
    /**
     * Construct a maze cell.
     * @param x {number} horizontal position in the maze (0 <= x < maze.width)
     * @param y {number} vertical position in the maze (0 <= y < maze.height)
     * @param topWall {boolean} does the cell have a top wall?
     * @param rightWall {boolean} does the cell have a right wall?
     * @param bottomWall {boolean} does the cell have a bottom wall?
     * @param leftWall {boolean} does the cell have a left wall?
     * @constructor
     */
    class Cell {
      constructor(x, y, topWall, rightWall, bottomWall, leftWall) {
        this.x = x
        this.y = y
        this.topWall = topWall
        this.rightWall = rightWall
        this.bottomWall = bottomWall
        this.leftWall = leftWall
        this.visited = false
      }
    }

    /**
     * Generate a maze using dept-first search with backtracking.
     * cf. http://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
     * @param width {number} number of cells in a row
     * @param height {number} number of cells in a column
     * @param cellSize {number} side length of a cell in pixels
     * @param wallThickness {number} wall thickness in pixels
     * @constructor
     */
    class Maze {
      constructor(width, height, cellSize, wallThickness) {
        this.width = width
        this.height = height
        this.cellSize = cellSize
        this.wallThickness = wallThickness
        this.numCells = width * height
        this.cells = [] // Arranged in x, y order, i.e. first column, then row index.

        this.getUnvisitedNeighbors = (x, y) => {
          const unvisitedNeighbors = []
          if (x >= 1 && !this.cells[x - 1][y].visited)
            unvisitedNeighbors.push(this.cells[x - 1][y]) // left neighbour
          if (x < (this.width - 1) && !this.cells[x + 1][y].visited)
            unvisitedNeighbors.push(this.cells[x + 1][y]) // right
          if (y >= 1 && !this.cells[x][y - 1].visited)
            unvisitedNeighbors.push(this.cells[x][y - 1]) // top
          if (y < (this.height - 1) && !this.cells[x][y + 1].visited)
            unvisitedNeighbors.push(this.cells[x][y + 1]) // bottom
          return unvisitedNeighbors
        }

        this.draw2D = () => {
          const canvas = document.querySelector('canvas')
          const ctx = canvas.getContext('2d')
          ctx.fillStyle = 'black'
          ctx.fillRect(0, 0, canvas.width, canvas.height)
          ctx.fillStyle = 'red'
          let cell
          const cs = this.cellSize, wt = this.wallThickness
          for (let i = 0; i < this.width; i++)
            for (let j = 0; j < this.height; j++) {
              cell = this.cells[i][j]
              if (cell.topWall) ctx.fillRect(cell.x * cs - wt / 2, cell.y * cs - wt / 2,
                cs + wt, wt)
              if (cell.rightWall) ctx.fillRect(cell.x * cs + cs - wt / 2, cell.y * cs - wt /
                2, wt, cs + wt)
              if (cell.bottomWall) ctx.fillRect(cell.x * cs - wt / 2, cell.y * cs + cs - wt
                / 2, cs + wt, wt)
              if (cell.leftWall) ctx.fillRect(cell.x * cs - wt / 2, cell.y * cs - wt / 2,
                wt, cs + wt)
            }
          // Draw maze border.
          ctx.lineWidth = 2 * wt
          ctx.strokeStyle = 'red'
          ctx.strokeRect(0, 0, canvas.width, canvas.height)
          // Draw exit.
          ctx.strokeStyle = 'green'
          ctx.beginPath()
          ctx.moveTo(0, 0)
          ctx.lineTo(cs, 0)
          ctx.stroke()
          // Draw entry.
          ctx.lineWidth = wt
          ctx.strokeStyle = 'pink'
          ctx.beginPath()
          ctx.moveTo(this.width * cs, (this.height - 1) * cs)
          ctx.lineTo(this.width * cs, this.height * cs)
          ctx.stroke()
        }

        // Initialize maze with all walls present.
        for (let i = 0; i < width; i++) {
          this.cells[i] = []
          for (let j = 0; j < height; j++)
            this.cells[i][j] = new Cell(i, j, true, true, true, true)
        }
        // We start at a random place and mark it as visited.
        let currentCell = new Cell(Math.floor(Math.random() * width),
          Math.floor(Math.random() * height), true, true, true, true)
        currentCell.visited = true
        let numCellsVisited = 1, unvisitedNeighbors, chosenNeighbor
        const stack = []
        // While there are unvisited cells.
        while (numCellsVisited <= this.numCells) {
          // If the current cell has any neighbors which have not been visited.
          unvisitedNeighbors = this.getUnvisitedNeighbors(currentCell.x, currentCell.y)
          if (unvisitedNeighbors.length > 0) {
            // Choose randomly one of the unvisited neighbors.
            chosenNeighbor =
              unvisitedNeighbors[Math.floor(Math.random() * unvisitedNeighbors.length)]
            // Push the current cell to the stack.
            stack.push(currentCell)
            // Remove the wall between the current cell and the chosen cell.
            if (chosenNeighbor.x < currentCell.x) { // Neighbor is left from current cell.
              currentCell.leftWall = false
              chosenNeighbor.rightWall = false
            } else if (chosenNeighbor.x > currentCell.x) {
              // Neighbor is right from current cell.
              currentCell.rightWall = false
              chosenNeighbor.leftWall = false
            } else if (chosenNeighbor.y < currentCell.y) {
              // Neighbor is above current cell.
              currentCell.topWall = false
              chosenNeighbor.bottomWall = false
            } else if (chosenNeighbor.y > currentCell.y) {
              // Neighbor is below current cell.
              currentCell.bottomWall = false
              chosenNeighbor.topWall = false
            }
            // Make the chosen cell the current cell and mark it as visited.
            currentCell = chosenNeighbor
            currentCell.visited = true
            numCellsVisited++
          }
          // Else if stack is not empty.
          else if (stack.length > 0) {
            // Pop a cell from the stack and make it the current cell.
            currentCell = stack.pop()
          } else {
            // Pick random unvisited cell, make it current and mark it as visited.
            const unvisitedCells = []
            for (let i = 0; i < this.width; i++)
              for (let j = 0; j < this.height; j++)
                if (!this.cells[i][j].visited) unvisitedCells.push(this.cells[i][j])
            currentCell = unvisitedCells[Math.floor(Math.random() * unvisitedCells.length)]
            currentCell.visited = true
            numCellsVisited++
          }
        }
        this.cells[0][0].topWall = false // Open up exit.
        this.cells[this.width - 1][this.height - 1].rightWall = false // Open up entry.
        this.draw2D()
      }
    }

    new Maze(20, 20, 40, 3)
  </script>
</head>
<body>
<main>
  <canvas width=1000 height=1000></canvas>
</main>
</body>
</html>
4.3.28.6. Web Sockets

So far we’ve used HTTP POST or GET requests to send data to the server, who responded with new HTML. This is a very inefficient and limited approach. If we want real-time two way communication between the server and a potentially large number of clients, we should take advantage of the new JavaScript WebSocket API that is available in Firefox and Chrome. By establishing a bidirectional communication channel between the server and each client, we can for instance implement real-time chat.

The easiest way to get started can be found at websocketd.com. The recommended way is to use WebSockets with Node.js.

4.3.28.6.1. Client

On the client side, we need to implement something like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const socket = new WebSocket("wss://foxi.ltam.lu:35000")
socket.addEventListener('open', opened)
socket.addEventListener('message', received)
socket.addEventListener('close', closed)
socket.addEventListener('error', error)

const opened = () => {

}

const received = event => {
  console.log("Received: ", event.data)
}

const closed = () => {

}

const error = event => {
  console.error("Error: ", event.data)
}

const send = event => {
  socket.send("Hello world!")
}

opened will be called once the web socket connection is ready. received will be called when a message from the server has been received.

4.3.28.6.2. Server

On the server side, we can use WebSockets with PHP but WebSockets with Node.js is highly recommended.

4.3.28.7. WebGL
WebGL (Web Graphics Library) is a JavaScript API for rendering interactive 3D graphics and 2D graphics within any compatible web browser without the use of plug-ins.

Relevant web pages:

registry.khronos.org/webgl/specs/latest

www.khronos.org/webgl/wiki/Main_Page

developer.cdn.mozilla.net/media/uploads/demos/a/z/azakai/3baf4ad7e600cbda06ec46efec5ec3b8/bananabread_1373485124_demo_package/index.html

spacejack.github.io

hexgl.bkcore.com/play

htmlchess.sourceforge.net/demo/example.html

www.chromeexperiments.com/webgl

playwebgl.com

4.3.28.7.1. Three.js

Given the complexity of direct WebGL programming, we’ll start by using the Three.js JavaScript library, which can be downloaded from github.com/mrdoob/three.js and greatly simplifies the development of 3D web apps.

The best way to get started is to study the documentation at threejs.org/docs/#manual/introduction/Creating-a-scene and threejs.org/manual.

There’s a nice online editor that you can use to create and export geometries, objects and scenes.

Let’s start with a very simple example and walk it through step by step:

Threejs1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!DOCTYPE html>
<html lang=en>
<head>
  <title>My first Three.js app</title>
  <meta charset=utf-8>
  <script src=three.js></script>
  <script type=module>
    let scene, camera, renderer, cube, spotlight, animation = true

    // This is our animation loop that will ideally be executed 60 times per second.
    const render = () => {
      if (animation) requestAnimationFrame(render)
      cube.rotation.x += 0.1
      cube.rotation.y += 0.1
      renderer.render(scene, camera)
    }

    // Stop/restart the animation.
    const toggleAnimation = () => {
      animation = !animation
      if (animation) render()
    }

    const canvas = document.querySelector('canvas')
    scene = new THREE.Scene()
    camera = new THREE.PerspectiveCamera(50, canvas.width / canvas.height, 0.1, 1000)
    renderer = new THREE.WebGLRenderer()
    renderer.setSize(canvas.width, canvas.height)
    document.body.appendChild(renderer.domElement)
    let geometry = new THREE.BoxGeometry(1, 1.8, 0.3)
    let material = new THREE.MeshBasicMaterial({color: 'green'})
    cube = new THREE.Mesh(geometry, material)
    cube.castShadow = true
    scene.add(cube)
    geometry = new THREE.PlaneGeometry(5, 5)
    material = new THREE.MeshBasicMaterial({color: 'red'})
    const plane = new THREE.Mesh(geometry, material)
    plane.receiveShadow = true
    plane.rotation.x = -0.5 * Math.PI
    plane.position.y = -1
    scene.add(plane)
    camera.position.y = 4
    camera.position.z = 5
    camera.lookAt(scene.position)
    spotlight = new THREE.AmbientLight('blue')
    scene.add(spotlight)
    render()
    document.querySelector('button').addEventListener('click', toggleAnimation)
  </script>
</head>
<body>
  <canvas width=500 height=500></canvas>
  <button>Stop/start</button>
</body>
</html>

As explained in the Three.js introductory example, we need a scene, a camera and a renderer to display the scene using the camera. The renderer needs the canvas element, which is where the whole scene will be displayed.

Our script consists of 3 functions:

  1. An initialization function that creates the scene, the camera and the renderer. It then adds them to the DOM and starts the rendering. This function is executed only once, after the document has been loaded.

  2. The rendering function, which calls itself using requestAnimationFrame (cf. developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame). Here we perform the animation, but only if the animation is supposed to be running as indicated by the boolean global variable. In this simple example we rotate our cube around the x and y axes.

  3. toggleAnimation simply toggles a boolean global variable, which indicates whether the animation is currently meant to be running or stopped. In the former case, the render function gets executed.

Here’s a slightly more evolved example:

Threejs2
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
<!DOCTYPE html>
<html lang=en>
<head>
  <title>My second Three.js app</title>
  <meta charset=utf-8>
  <style>
      body {
          margin:   0;
          padding:  0;
          overflow: hidden;
      }

      button {
          position: fixed;
          top:      0;
          left:     0;
      }

      #stats {
          position: fixed;
          top:      20px;
          left:     0;
      }
  </style>
  <script src=three.js></script>
  <script src=stats.min.js></script>
  <script type=module>
    let scene, camera, renderer, geometry, material, mesh, animation = true, stats,
      stepX = 0.1, stepY = 0.1, stepZ = 0.1, directionX = 1, directionY = 1, directionZ = 1

    // This is our animation loop that will ideally be executed 60 times per second.
    const render = () => {
      if (animation) requestAnimationFrame(render)
      mesh.rotation.x += 0.01
      mesh.rotation.y += 0.02
      mesh.rotation.z += 0.01
      if (mesh.position.x > 100) directionX = -1
      else if (mesh.position.x < -100) directionX = 1
      if (mesh.position.y > 50) directionY = -1
      else if (mesh.position.y < -50) directionY = 1
      if (mesh.position.z > 50) directionZ = -1
      else if (mesh.position.z < -50) directionZ = 1
      mesh.position.x += stepX * Math.random() * directionX
      mesh.position.y += stepY * Math.random() * directionY
      mesh.position.z += stepZ * Math.random() * directionZ
      stats.update()
      renderer.render(scene, camera)
    }

    const resize = () => {
      camera.aspect = window.innerWidth / window.innerHeight
      camera.updateProjectionMatrix()
      renderer.setSize(window.innerWidth, window.innerHeight)
    }

    // Stop/restart the animation.
    const toggleAnimation = () => {
      animation = !animation
      if (animation) render()
    }

    stats = new Stats()
    document.getElementById('stats').appendChild(stats.domElement)
    scene = new THREE.Scene()
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000)
    camera.position.x = 0
    camera.position.y = 70
    camera.position.z = 30
    camera.lookAt(scene.position)

    // Add a texture to our sphere.
    const mapUrl = "LTAM256x256.png"
    const map = THREE.ImageUtils.loadTexture(mapUrl)
    geometry = new THREE.SphereGeometry(20, 50, 50)
    material = new THREE.MeshLambertMaterial({color: 0xff00ff, map: map})
    mesh = new THREE.Mesh(geometry, material)
    mesh.position.y = 20
    mesh.castShadow = true
    scene.add(mesh)

    const spotLight = new THREE.SpotLight(0xffffff)
    spotLight.position.set(-40, 100, 50)
    spotLight.castShadow = true
    scene.add(spotLight)
    renderer = new THREE.WebGLRenderer()
    renderer.setSize(window.innerWidth, window.innerHeight)
    renderer.setClearColor(0x000000)
    renderer.shadowMapEnabled = true

    // When the window gets resized, we need to remove the old canvas
    // and add a new one with the correct size.
    const childNodes = document.body.childNodes
    let n = childNodes.length
    while (n > 0) {
      if (childNodes[n - 1].tagName && childNodes[n - 1].tagName === 'CANVAS') {
        document.body.removeChild(childNodes[n - 1])
      }
      n--
    }
    document.body.appendChild(renderer.domElement)
    render()
    window.addEventListener('resize', resize)
    document.querySelector('button').addEventListener('click', toggleAnimation)
  </script>
</head>
<body>
<div id=stats></div>
<button>Stop/start</button>
</body>
</html>

Instead of declaring a canvas element, we’ll let the renderer take care of that. We’ll let the scene take up the whole browser window width and height. We can even switch to full screen mode using F11. Finally we use Mr. Doob’s performance monitor, available from github.com/mrdoob/stats.js.

As a further evolutionary step, we can add user controls (cf. code.google.com/p/dat-gui), mouse interaction and a funny background plane:

cover4
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Three.js Skeleton</title>
  <meta charset=utf-8>
  <style>
      body {
          margin:   0;
          padding:  0;
          overflow: hidden;
      }

      button {
          position: fixed;
          top:      0;
          left:     0;
      }

      #stats {
          position: fixed;
          top:      20px;
          left:     0;
      }
  </style>
  <script src=three.js></script>
  <script src=OrbitControls.js></script>
  <script src=stats.min.js></script>
  <script src=dat.gui.min.js></script>
  <script type=module>
    let scene, camera, renderer, geometry, material, mesh, animation = true, stats,
      projector = new THREE.Projector(),
      directionX = 1, directionY = 1, directionZ = 1, controls, cameraControl

    const animate = () => {
      // note: three.js includes requestAnimationFrame shim
      if (animation) requestAnimationFrame(animate)
      stats.update()
      mesh.rotation.x += 0.01
      mesh.rotation.y += 0.02
      mesh.rotation.z += 0.01
      if (mesh.position.x > 100) directionX = -1
      else if (mesh.position.x < -100) directionX = 1
      if (mesh.position.y > 50) directionY = -1
      else if (mesh.position.y < -50) directionY = 1
      if (mesh.position.z > 50) directionZ = -1
      else if (mesh.position.z < -50) directionZ = 1
      mesh.position.x += Math.random() * controls.stepX * directionX
      mesh.position.y += Math.random() * controls.stepY * directionY
      mesh.position.z += Math.random() * controls.stepZ * directionZ
      cameraControl.update()
      renderer.render(scene, camera)
    }

    const resize = () => {
      camera.aspect = window.innerWidth / window.innerHeight
      camera.updateProjectionMatrix()
      renderer.setSize(window.innerWidth, window.innerHeight)
    }

    const toggleAnimation = () => {
      animation = !animation
      if (animation) animate()
    }

    const onDocumentMouseDown = event => {
      event.preventDefault()
      const vector = new THREE.Vector3((event.clientX / window.innerWidth) * 2 - 1,
        -(event.clientY / window.innerHeight) * 2 + 1, 0.5)
      projector.unprojectVector(vector, camera)
      const raycaster = new THREE.Raycaster(camera.position,
        vector.sub(camera.position).normalize())
      const intersects = raycaster.intersectObjects([mesh])
      if (intersects.length > 0) {
        const r = Math.floor(Math.random() * 256)
        const g = Math.floor(Math.random() * 256)
        const b = Math.floor(Math.random() * 256)
        intersects[0].object.material.color =
          new THREE.Color("rgb(" + r + ", " + g + ", " + b + ")")
      }
    }

    stats = new Stats()
    document.getElementById('stats').appendChild(stats.domElement)
    controls = {
      stepX: 0.1,
      stepY: 0.1,
      stepZ: 0.1
    }

    const gui = new dat.GUI()
    gui.add(controls, 'stepX', 0, 3)
    gui.add(controls, 'stepY', 0, 3)
    gui.add(controls, 'stepZ', 0, 3)
    scene = new THREE.Scene()

    camera = new THREE.PerspectiveCamera(75, window.innerWidth /
      window.innerHeight, 1, 10000)
    camera.position.x = 0
    camera.position.y = 70
    camera.position.z = 30
    camera.lookAt(scene.position)
    cameraControl = new THREE.OrbitControls(camera)
    let mapUrl = "LAM256x256.png"
    let map = THREE.ImageUtils.loadTexture(mapUrl)
    geometry = new THREE.BoxGeometry(20, 20, 20)
    material = new THREE.MeshLambertMaterial({color: 0xff00ff, map: map})
    mesh = new THREE.Mesh(geometry, material)
    mesh.position.y = 20
    mesh.castShadow = true
    scene.add(mesh)

    mapUrl = "checker_large.gif"
    map = THREE.ImageUtils.loadTexture(mapUrl)
    map.wrapS = map.wrapT = THREE.RepeatWrapping
    map.repeat.set(8, 8)
    const color = 0xffffff
    const ambient = 0x888888
    // Put in a ground plane to show off the lighting
    geometry = new THREE.PlaneGeometry(200, 200, 50, 50)
    const chessboard = new THREE.Mesh(geometry,
      new THREE.MeshPhongMaterial({
        color: color,
        ambient: ambient, map: map, side: THREE.DoubleSide,
        opacity: 0.5, transparent: true
      }))
    chessboard.rotation.x = -Math.PI / 2
    chessboard.position.y = -4.02
    scene.add(chessboard)

    const spotLight = new THREE.SpotLight(0xffffff)
    spotLight.position.set(-40, 100, 50)
    spotLight.castShadow = true
    scene.add(spotLight)
    const hemisphereLight = new THREE.HemisphereLight(0xdd00dd, 0x00aa00, 0.3)
    scene.add(hemisphereLight)
    renderer = new THREE.WebGLRenderer()
    renderer.setSize(window.innerWidth, window.innerHeight)
    renderer.setClearColor(0x000000)
    renderer.shadowMapEnabled = true
    /*const childNodes = document.body.childNodes;
     let n = childNodes.length;
     while (n > 0) {
     if (childNodes[n - 1].tagName && childNodes[n - 1].tagName === 'CANVAS') {
     document.body.removeChild(childNodes[n - 1]);
     }
     n--;
     }*/
    document.body.appendChild(renderer.domElement)
    animate()
    window.addEventListener('resize', resize)
    document.addEventListener('mousedown', onDocumentMouseDown)
    document.querySelector('button').addEventListener('click', toggleAnimation)
  </script>
</head>
<body>
<div id=stats></div>
<button>Stop/start</button>
</body>
</html>

Loading a Collada model

threejscollada1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Load Collada model with Three.js</title>
  <meta charset=utf-8>
  <script src=three.js></script>
  <script src=ColladaLoader.js></script>
  <script type=module>
    const scene = new THREE.Scene()
    const camera = new THREE.PerspectiveCamera(45,
      window.innerWidth / window.innerHeight, 0.1, 1000)
    const webGLRenderer = new THREE.WebGLRenderer()
    webGLRenderer.setClearColorHex(0xcccccc, 1.0)
    webGLRenderer.setSize(window.innerWidth, window.innerHeight)
    webGLRenderer.shadowMapEnabled = true

    // position and point the camera to the center of the scene
    camera.position.x = 15
    camera.position.y = 15
    camera.position.z = 15
    camera.lookAt(new THREE.Vector3(0, 2, 0))

    // add spotlight for the shadows
    const spotLight = new THREE.SpotLight(0xffffff)
    spotLight.position.set(150, 150, 150)
    spotLight.intensity = 2
    scene.add(spotLight)

    document.querySelector('main').appendChild(webGLRenderer.domElement)

    const loader = new THREE.ColladaLoader()
    loader.options.convertUpAxis = true
    let mesh
    loader.load("Boat2.dae", result => {
      console.dir(result)
      mesh = result.scene
      mesh.position.set(0, -1, 0)
      //mesh.scale.set(1, 1, 1);
      scene.add(mesh)
    })

    const render = () => {
      requestAnimationFrame(render)
      webGLRenderer.render(scene, camera)
    }

    render()
  </script>
</head>
<body>
<main></main>
</body>
</html>

Physijs

FPS

Let’s have some real fun and create a first person shooter (FPS) from scratch.

threejs3
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>FPS</title>
    <meta charset=utf-8>
    <link rel=stylesheet href=style.css>
    <script src=three.js></script>
    <script src=FirstPersonControls.js></script>
    <script src=stats.min.js></script>
    <script src=dat.gui.min.js></script>
    <script src=main.js type=module></script>
  </head>
  <body>
    <aside id=stats></aside>
    <button>Stop/start</button>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
const game = {}
game.aspectRatio = window.innerWidth / window.innerHeight
game.scene = new THREE.Scene()
game.camera = new THREE.PerspectiveCamera(60, game.aspectRatio, 1, 10000)
game.cameraControls = new THREE.FirstPersonControls(game.camera)
game.renderer = new THREE.WebGLRenderer()
game.projector = new THREE.Projector()
game.stats = new Stats()
game.animation = true
game.clock = new THREE.Clock()
game.map = [ // 1  2  3  4  5  6  7  8  9
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], // 0
  [1, 0, 0, 0, 0, 0, 0, 1, 0, 1], // 1
  [1, 1, 0, 0, 2, 0, 0, 0, 0, 1], // 2
  [1, 0, 0, 0, 0, 2, 0, 0, 0, 1], // 3
  [1, 0, 0, 2, 0, 0, 2, 0, 0, 1], // 4
  [1, 0, 0, 0, 2, 0, 0, 0, 1, 1], // 5
  [1, 0, 1, 0, 0, 0, 0, 1, 1, 1], // 6
  [1, 0, 1, 0, 0, 1, 0, 0, 0, 1], // 7
  [1, 0, 1, 0, 1, 0, 0, 0, 0, 1], // 8
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]  // 9
]
game.mapW = game.map.length
game.mapH = game.map[0].length
game.UNITSIZE = 50
game.WALLHEIGHT = game.UNITSIZE / 3
game.MOVESPEED = 100
game.LOOKSPEED = 0.075
game.init = () => {
  document.getElementById('stats').appendChild(game.stats.domElement)
  game.cameraControls.movementSpeed = game.MOVESPEED
  game.cameraControls.lookSpeed = game.LOOKSPEED
  game.cameraControls.lookVertical = false
  game.cameraControls.noFly = true
  game.camera.position.y = game.UNITSIZE * .2
  game.scene.add(game.camera)
  const geometry = new THREE.BoxGeometry(game.UNITSIZE * game.mapW, 1,
    game.UNITSIZE * game.mapW)
  const material = new THREE.MeshLambertMaterial(
    {map: THREE.ImageUtils.loadTexture('images/texture-91829_1920.jpg')})
  const floor = new THREE.Mesh(geometry, material)
  game.scene.add(floor)
  const cube = new THREE.BoxGeometry(game.UNITSIZE, game.WALLHEIGHT, game.UNITSIZE)
  const materials = [
    new THREE.MeshLambertMaterial({map: THREE.ImageUtils.loadTexture('images/wall-1.jpg')}),
    new THREE.MeshLambertMaterial({map: THREE.ImageUtils.loadTexture('images/wall-2.jpg')}),
    new THREE.MeshLambertMaterial({color: 0xFBEBCD})
  ]
  for (let i = 0; i < game.mapW; i++) {
    for (let j = 0, m = game.map[i].length; j < m; j++) {
      if (game.map[i][j]) {
        const wall = new THREE.Mesh(cube, materials[game.map[i][j] - 1])
        wall.position.x = (.5 + i - game.mapW / 2) * game.UNITSIZE
        wall.position.y = game.WALLHEIGHT / 2
        wall.position.z = (.5 + j - game.mapW / 2) * game.UNITSIZE
        game.scene.add(wall)
      }
    }
  }

  const directionalLight1 = new THREE.DirectionalLight(0xF7EFBE, 0.7)
  directionalLight1.position.set(.5, 1, .5)
  game.scene.add(directionalLight1)
  const directionalLight2 = new THREE.DirectionalLight(0xF7EFBE, 0.5)
  directionalLight2.position.set(-0.5, -1, -0.5)
  game.scene.add(directionalLight2)
  game.scene.fog = new THREE.FogExp2(0xa6a1aF, 0.0005)
  game.renderer.setSize(window.innerWidth, window.innerHeight)
  game.renderer.setClearColor(0x2222ff)
  game.renderer.shadowMapEnabled = true
  document.body.appendChild(game.renderer.domElement)
  game.animate()
}

game.animate = () => {
  if (game.animation) requestAnimationFrame(game.animate)
  game.stats.update()
  game.cameraControls.update(game.clock.getDelta())
  game.renderer.render(game.scene, game.camera)
}

game.toggleAnimation = () => {
  game.animation = !game.animation
  if (game.animation) game.animate()
}

game.resize = () => {
  game.camera.aspect = window.innerWidth / window.innerHeight
  game.camera.updateProjectionMatrix()
  game.renderer.setSize(window.innerWidth, window.innerHeight)
}

window.addEventListener('resize', game.resize)
document.querySelector('button').addEventListener('click', game.toggleAnimation)
game.init()

carvisualizer.plus360degrees.com/threejs

hexgl.bkcore.com

portableapps.com/apps/graphics_pictures/blender_portable

github.com/tparisi/Vizi

www.peter-strohm.de/webgl/index.php

www.khronos.org/webgl/wiki/User_Contributions

4.3.28.7.2. GLAM

GLAM (GL And Markup) is a declarative language for 3D web content (cf. tparisi.github.io/glam).

4.3.28.7.3. Cesium

This is a JavaScript library for creating 3D globes and 2D maps (cf. cesiumjs.org).

Cesium1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html lang=en>
<head>
  <title>A first Cesium experiment</title>
  <meta charset=utf-8>
  <style>
      @import url(Build/Cesium/Widgets/widgets.css);

      html, body, #cesiumContainer {
          width:    100%;
          height:   100%;
          margin:   0;
          padding:  0;
          overflow: hidden;
      }
  </style>
  <script src=Build/Cesium/Cesium.js></script>
  <script type=module>
    const viewer = new Cesium.Viewer('cesiumContainer', {
      imageryProvider: new Cesium.ArcGisMapServerImageryProvider({
        url:
          'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer'
      }),
      baseLayerPicker: false
    })
  </script>

</head>
<body>
<div id=cesiumContainer></div>
</body>
</html>
4.3.28.7.4. Direct WebGL programming

All WebGL drawing happens inside the HTML canvas element (cf. developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement.getContext). Before using the WebGL API we need a WebGLRenderingContext object (cf. developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext), which manages the whole 3D drawing process. To get it we call the getContext method of the canvas element and pass webgl as parameter to get the 3D context. If we pass 2d we get a 2D context. If we wanted to ensure compatibility with older browsers, we would have to check experimental-webgl, webkit-3d and moz-webgl as parameters to get a 3D context, but we will assume that the user uses an up to date browser.

WebGL methods correspond to OpenGL methods documented at www.khronos.org/opengles/sdk/docs/man.

Useful WebGLRenderingContext methods:

  • createShader(type) creates an empty shader object of the given type (VERTEX_SHADER or FRAGMENT_SHADER) and returns it’s reference.

  • createProgram() creates an empty program object and returns it’s reference.

  • clearColor(red, green, blue, alpha) sets color for drawing area. Values from 0 to 1, alpha === 1 → opaque, alpha === 0 → fully transparent.

  • shaderSource(shader, source) replaces the source code in a shader object.

  • compileShader(shader) compiles the shader object.

  • attachShader(program, shader) attaches a shader to a program.

  • linkProgram(program) links a program.

  • useProgram(program) installs the program as part of the current rendering state.

  • clearColor(red, green, blue, alpha) specifies clear values for the color buffers.

  • clear(buffer) clears the buffer(s) specified. It takes a single argument. If several buffers are to be cleared it is the bitwise OR of several values from GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT and GL_STENCIL_BUFFER_BIT.

  • drawArrays(mode, first, count) renders primitives from array data. The first parameter specifies what kind of primitives to render. Choices are GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN and GL_TRIANGLES.

  • getAttribLocation(program, name) returns the location of an attribute variable.

  • vertexAttrib3f(index, v0, v1, c2) specifies the value of a generic vertex attribute. Similar methods end with 1f, 2f or 4f. From www.khronos.org/opengles/sdk/docs/man:

These commands can be used to specify one, two, three, or all four components of the generic vertex attribute specified by index. A 1 in the name of the command indicates that only one value is passed, and it will be used to modify the first component of the generic vertex attribute. The second and third components will be set to 0, and the fourth component will be set to 1. Similarly, a 2 in the name of the command indicates that values are provided for the first two components, the third component will be set to 0, and the fourth component will be set to 1. A 3 in the name of the command indicates that values are provided for the first three components and the fourth component will be set to 1, whereas a 4 in the name indicates that values are provided for all four components.

  • getUniformLocation(program, name) returns the location of a uniform variable.

  • uniform[1, 2, 3, 4]f(index, v0 [, v1, v2, v3]) specifies the values of a uniform variable.

  • createBuffer() creates a buffer object.

  • deleteBuffer(buffer) deletes a buffer object.

  • bindBuffer(target, buffer) binds a buffer object telling WebGL what type of data it contains. target must be GL_ARRAY_BUFFER or GL_ELEMENT_ARRAY_BUFFER.

  • bufferData(target, data, usage) creates and initializes a buffer object’s data store. target must be GL_ARRAY_BUFFER or GL_ELEMENT_ARRAY_BUFFER. usage is one of GL_STREAM_DRAW, GL_STATIC_DRAW, or GL_DYNAMIC_DRAW.

  • vertexAttribPointer(location, size, type, normalized, stride, offset)

WebGL uses typed arrays for maximum performance:

Array Bytes

Int8Array

1

Uint8Array

1

Int16Array

2

Uint16Array

2

Int32Array

4

Uint32Array

4

Float32Array

4

Float64Array

8

To create a typed array, we call the constructor.

Typed arrays have the following methods, properties and constants:

get(index)

set(index, value)

set(array, offset)

length

BYTES_PER_ELEMENT

Let’s look at two simple examples:

WebGLdirect1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html lang=en>
<head>
  <title>WebGL Example 1</title>
  <meta charset=utf-8>
  <script type=module>
    let vertexShaderSource, fragmentShaderSource

    const handleShader = (e, isFragment) => {
      if (isFragment) fragmentShaderSource = e.target.response
      else vertexShaderSource = e.target.response
      if (vertexShaderSource && fragmentShaderSource) runShaders()
    }

    const init = () => {
      const req1 = new XMLHttpRequest(), req2 = new XMLHttpRequest()
      req1.open('POST', 'vertex_shader1.js')
      req1.addEventListener('load', e => {
        handleShader(e, false)
      })
      req1.send()
      req2.open('GET', 'fragment_shader1.js')
      req2.addEventListener('load', e => {
        handleShader(e, true)
      })
      req2.send()
    }

    const runShaders = () => {
      const canvas = document.querySelector('canvas')
      let gl
      try {
        gl = canvas.getContext('webgl')
      } catch (e) {
        alert('Your browser does not seem to support WebGL')
      }
      if (gl) {
        const vertexShader = gl.createShader(gl.VERTEX_SHADER)
        const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
        const program = gl.createProgram()
        gl.shaderSource(vertexShader, vertexShaderSource)
        gl.shaderSource(fragmentShader, fragmentShaderSource)
        gl.compileShader(vertexShader)
        gl.compileShader(fragmentShader)
        gl.attachShader(program, vertexShader)
        gl.attachShader(program, fragmentShader)
        gl.linkProgram(program)
        gl.useProgram(program)
        //gl.program = program;
        gl.clearColor(0, 0, 0, 1)
        /*gl.enable(gl.DEPTH_TEST);
        gl.depthFunc(gl.LEQUAL);*/
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
        gl.drawArrays(gl.POINTS, 0, 1)
      }
    }

    init()
  </script>
</head>
<body>
<main>
  <canvas width=640 height=480>No canvas support.</canvas>
</main>
</body>
</html>
1
2
3
4
void main() {
  gl_Position = vec4(0.5, 0.0, 0.0, 1.0);
  gl_PointSize = 10.0;
}
1
2
3
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
WebGLdirect2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<!DOCTYPE html>
<html lang=en>
<head>
  <title>WebGL Example 1</title>
  <meta charset=utf-8>
  <script type=module>
    const VSHADER_FILE = 'vertex_shader2.js', HSHADER_FILE = 'fragment_shader2.js'
    let vertexShaderSource, fragmentShaderSource

    const handleShader = (e, isFragment) => {
      if (isFragment) fragmentShaderSource = e.target.response
      else vertexShaderSource = e.target.response
      if (vertexShaderSource && fragmentShaderSource) runShaders()
    }

    const init = () => {
      const req1 = new XMLHttpRequest(), req2 = new XMLHttpRequest()
      req1.open('POST', VSHADER_FILE)
      req1.addEventListener('load', e => {
        handleShader(e, false)
      })
      req1.send()
      req2.open('POST', HSHADER_FILE)
      req2.addEventListener('load', e => {
        handleShader(e, true)
      })
      req2.send()
    }

    const runShaders = () => {
      const canvas = document.querySelector('canvas')
      let gl
      try {
        gl = canvas.getContext('webgl')
      } catch (e) {
        alert('Your browser does not seem to support WebGL')
      }
      if (gl) {
        const vertexShader = gl.createShader(gl.VERTEX_SHADER)
        const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
        const program = gl.createProgram()
        gl.shaderSource(vertexShader, vertexShaderSource)
        gl.shaderSource(fragmentShader, fragmentShaderSource)
        gl.compileShader(vertexShader)
        gl.compileShader(fragmentShader)
        gl.attachShader(program, vertexShader)
        gl.attachShader(program, fragmentShader)
        gl.linkProgram(program)
        gl.useProgram(program)
        //gl.program = program;
        const a_Position = gl.getAttribLocation(program, 'a_Position')
        gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
        const a_PointSize = gl.getAttribLocation(program, 'a_PointSize')
        gl.vertexAttrib1f(a_PointSize, 5.0)
        gl.clearColor(0, 0, 0, 1)
        /*gl.enable(gl.DEPTH_TEST);
         gl.depthFunc(gl.LEQUAL);*/
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
        gl.drawArrays(gl.POINTS, 0, 1)
      }
    }

    init()
  </script>
</head>
<body>
<main>
  <canvas width=640 height=480>No canvas support.</canvas>
</main>
</body>
</html>
1
2
3
4
5
6
7
attribute vec4 a_Position;
attribute float a_PointSize;

void main() {
  gl_Position = a_Position;
  gl_PointSize = a_PointSize;
}
1
2
3
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
4.3.28.7.5. Blender

According to www.blender.org:

Blender is a free and open source 3D animation suite. It supports the entirety of the 3D pipeline—modeling, rigging, animation, simulation, rendering, compositing and motion tracking, even video editing and game creation.

Blender is an extremely powerful application. Unfortunately, it’s user interface is not necessarily the easiest one to master.

Bforartists is a fork of the popluar open source 3d software Blender. The primary goal of the Bforartists fork is to deliver a better graphical UI and a better usability.
4.3.28.8. Page Visibility
The Page Visibility specification defines a means for site developers to programmatically determine the current visibility of a document and be notified of visibility changes.

This API is very useful for instance in the case of WMOTU Invaders, where we do not want the aliens to continue moving and shooting in the background whilst we are not playing the game! Take a look at WMOTU Invaders object-oriented to see how it’s done. Further details and examples can be found at developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API.

4.3.28.9. WebAudio

Here is a simple example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Web Audio Example</title>
    <meta charset=utf-8>
    <script src=webaudio1.js type=module></script>
  </head>
  <body>
    <main>
      <input type=button value=Play>
    </main>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
let context = null

const init = () => {
  try {
    window.AudioContext = window.AudioContext || window.webkitAudioContext
    context = new AudioContext()
  } catch (e) {
    window.alert('Web Audio API is not supported in this browser.')
  }

  let mybuffer = null
  const button = document.querySelector('input')
  button.addEventListener('click', () => {
    play(mybuffer)
  })

  if (context) {
    const url = 'gunshot.wav'
    const request = new XMLHttpRequest()
    request.open("GET", url, true)
    request.responseType = "arraybuffer"
    request.addEventListener('load', () => {
      context.decodeAudioData(request.response, buffer => {
        mybuffer = buffer
      })
      button.disabled = false
    })
    request.send()
  }
}

const play = buffer => {
  var sourceNode = context.createBufferSource()
  sourceNode.buffer = buffer
  sourceNode.connect(context.destination)
  sourceNode.start(0)
}

init()
4.3.28.12. IndexedDB

javascript.info/indexeddb

medium.com/@AndyHaskell2013/build-a-basic-web-app-with-indexeddb-8ab4f83f8bda

developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API

github.com/jakearchibald/idb

flaviocopes.com/indexeddb

medium.com/dev-channel/offline-storage-for-progressive-web-apps-70d52695513c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>IndexedDB example 1</title>
  <!--<script src=indexeddb1.js></script>-->
  <script type=module>
    import {openDB, deleteDB, wrap, unwrap} from 'https://unpkg.com/idb?module'

    const init = async () => {
      // https://github.com/jakearchibald/idb

      if (!indexedDB) {
        console.warn('IndexedDB not supported')
        return
      }

      const dbName = 'TestDB'
      const storeName = 'store1'
      const version = 1 //versions start at 1

      const db = await openDB(dbName, version, {
        upgrade(db, oldVersion, newVersion, transaction) {
          console.log('upgrade called')
          const store = db.createObjectStore(storeName)
        },
        blocked() {
          console.log('blocked')
        },
        blocking() {
          console.log('blocking')
        }
      })

      const handleFile = async e => {
        const files = e.target.files
        console.dir(files)
        const isValidType = type => {
          const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif',
            'image/bmp']
          for (const validType of validTypes) if (type === validType) return true
          return false
        }

        const readAndDisplay = async () => {
          let tx = db.transaction(storeName, 'readwrite')
          let store = await tx.objectStore(storeName)
          const pics = await store.getAll()
          await tx.done
          console.dir(pics)
          for (const pic of pics) {
            let image = new Image()
            let buffer = Uint8Array.from(pic)
            let blob = new Blob([buffer])
            let img = document.createElement('img')
            img.src = URL.createObjectURL(blob)
            document.querySelector('section').appendChild(img)
          }
        }

        let filesLeft = files.length
        for (const file of files) {
          if (isValidType(file.type)) {
            console.log('Valid type: ' + file.type)
            let fr = new FileReader()
            fr.addEventListener('load', async e => {
              const array = Array.from(new Uint8Array(e.target.result))
              let tx = db.transaction(storeName, 'readwrite')
              let store = await tx.objectStore(storeName)
              await store.put(array, file.name)
              await tx.done
              filesLeft--
              if (filesLeft === 0) await readAndDisplay()
            })
            fr.readAsArrayBuffer(file)
          } else alert('Only the following file types are supported: jpeg/jpg, png, gif' +
            ' and bmp')
        }
      }

      document.querySelector('input').addEventListener('change', handleFile)
    }

    init()
  </script>
</head>
<body>
<header>
  <input type=file multiple>
  File size: <span></span>
</header>
<section></section>
</body>
</html>
4.3.28.14. Service Worker

Install the Service Worker Detector extension in your browser.

developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API

github.com/w3c/ServiceWorker/blob/master/explainer.md

github.com/jakearchibald/simple-serviceworker-tutorial

jakearchibald.github.io/isserviceworkerready/resources.html

w3c.github.io/ServiceWorker

blog.sessionstack.com/how-javascript-works-service-workers-their-life-cycle-and-use-cases-52b19ad98b58

serviceworke.rs

jakearchibald.github.io/isserviceworkerready

developers.google.com/web/fundamentals/primers/service-workers

developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook

github.com/GoogleChrome/samples/tree/gh-pages/service-worker

developers.google.com/web/tools/workbox

gist.github.com/Rich-Harris/fd6c3c73e6e707e312d7c5d7d0f3b2f9

alligator.io/js/service-workers

medium.com/dev-channel/two-http-headers-related-to-service-workers-you-never-may-have-heard-of-c8862f76cc60

medium.com/@boopathi/service-workers-gotchas-44bec65eab3f

flaviocopes.com/service-workers

techbeacon.com/app-dev-testing/how-use-service-workers-progressive-web-apps

hacks.mozilla.org/2016/03/debugging-service-workers-and-push-with-firefox-devtools

codelabs.developers.google.com/codelabs/debugging-service-workers

www.html5rocks.com/en/tutorials/service-worker/introduction

developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil

medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c

blog.bitsrc.io/understanding-service-workers-and-caching-strategies-a6c1e1cbde03

developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading

www.trysmudford.com/blog/offline-first-thoughts

medium.com/web-on-the-edge/offline-posts-with-progressive-web-apps-fc2dc4ad895

a.kabachnik.info/offline-post-requests-via-service-worker-and-indexeddb.html

stackoverflow.com/questions/35270702/can-service-workers-cache-post-requests

stackoverflow.com/questions/38193221/how-to-use-a-service-worker-with-basic-authentication-ntlm-negotiate

wicg.github.io/cookie-store/explainer.html

www.trysmudford.com/blog/offline-first-thoughts

www.chromestatus.com/feature/5682567464353792

www.developpez.net/forums/d1590248/javascript/general-javascript/services-worker-l-authentification-http

stackoverflow.com/questions/23256389/html5-offline-authentication

blog.formpl.us/how-to-handle-post-put-requests-in-offline-applications-using-service-workers-indexedb-and-da7d0798a9ab

www.netguru.com/codestories/how-to-share-session-cookie-or-state-between-pwa-in-standalone-mode-and-safari-on-ios

stackoverflow.com/questions/39724902/handle-authentication-in-a-service-worker-for-a-react-app

frontendmasters.com/courses/service-workers

filipbech.github.io/2017/02/service-worker-and-caching-from-other-origins

For a great free and in-depth course on ServiceWorker see www.udacity.com/course/offline-web-applications\-\-ud899.

To list and unregister service workers in Firefox, got to about:debugging#workers (cf. love2dev.com/blog/how-to-uninstall-a-service-worker). In Chrome, use chrome://serviceworker-internals and use chrome://inspect/#service-workers to inspect.

To reload the page bypassing the service worker(s) hold the shift key when reloading.

To cache assets that are only available after login see stackoverflow.com/questions/40666079/service-worker-login-page.

Here’s an example:

4.3.28.14.1. index.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>First service worker example</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
    <link rel=stylesheet href=index.css>
    <script src=app.js type=module></script>
  </head>
  <body>
    <h1>My first Service Worker!</h1>
  </body>
</html>
4.3.28.14.2. index.css
1
2
3
body {
  background-color: #ffa9e8;
}
4.3.28.14.3. app.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if (navigator.serviceWorker)
  navigator.serviceWorker.register('serviceWorker.js').then(reg => {
    // Was not loaded via an existing SW so up to date.
    if (!navigator.serviceWorker.controller) return
    if (reg.waiting) console.log('There\'s an updated worker waiting')
    //if (reg.installing)
    console.log(`Service worker registered`)
  }, err => {
    console.log('Service worker registration failed!', err)
  })
4.3.28.14.4. serviceWorker.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const cacheName = 'Testv1'

self.addEventListener('install', event => {
  self.skipWaiting()
  /*event.waitUntil(
    caches.open(cacheName.then(cache => {
      return cache.addAll([
        './pics/'
      ]);
    })
  );*/
})

self.addEventListener('activate', event => {
  event.waitUntil(
    clients.claim().then(() => {
      caches.keys().then(keyList => {
        return Promise.all(keyList.map(key => {
          if (key !== cacheName) return caches.delete(key)
        }))
      })
    }))
})

self.addEventListener('fetch', event => {
  const corsRequest = new Request(event.request.url, {mode: 'cors'})
  console.log(corsRequest)
  event.respondWith(
    caches.match(corsRequest).then(res => {
      return res || fetch(corsRequest).then(resp => {
        let responseClone = resp.clone()
        caches.open(cacheName).then(cache => {
          cache.put(corsRequest, responseClone)
        })

        return resp
      })
    }).catch(() => {
      return new Response('There\'s a problem...')
    })
  )
})
4.3.28.19. Web Messaging

4.3.29. Tools

4.3.29.1. Dealing with old browsers: graceful degradation, polyfills and transpilers

Older browsers don’t support the latest HTML, CSS and JS syntax and features as you can see at kangax.github.io/compat-table/es2016plus and html5please.com. If we want to support old browsers and still use the latest features, we can use polyfills and/or transpilers as described in hackernoon.com/polyfills-everything-you-ever-wanted-to-know-or-maybe-a-bit-less-7c8de164e423. Note that this works to a certain degree but is far from perfect.

www.htmlgoodies.com/beyond/javascript/js-ref/the-2017-guide-to-polyfills.html

www.w3.org/community/webed/wiki/Optimizing_content_for_different_browsers:_the_RIGHT_way

outdatedbrowser.com

browser-update.org

developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent

There are ways to change the user agent string that our browser sends to the server: www.howtogeek.com/113439/how-to-change-your-browsers-user-agent-without-installing-any-extensions

4.3.29.2. Minimizers, optimizers, obfuscators, deobfuscation, compressors and beautifiers

Obfuscation means making the code unreadable for human beings whilst preserving its function. An excellent survey of obfuscation techniques can be found in www.cse.psu.edu/~szhu/papers/malware.pdf. A good overview of minification resources can be found at developers.google.com/speed/docs/insights/MinifyResources#overview.

A list of JS performance improvement techniques can be found at kongaraju.blogspot.lu/2016/05/101-javascript-performance-improvement.html.

4.3.29.2.2. Deobfuscation

Obfuscated code that uses some form of encryption usually starts with eval and can be deobfuscated by replacing eval with for instance alert or console.log. Here is a simple deobfuscator for encrypted JavaScript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<html>
<head>
  <title>JavaScript Decoder</title>
  <meta charset=UTF-8>
  <script type=module>
    const DecodeJS = () => {
      const str = document.getElementById("inputTA").value
      if (str.slice(0, 4) === 'eval') {
        eval(`let value = String${str.slice(4)}`)
        document.getElementById("outputTA").value = value
      } else {
        const str1 = '\x3c\x61\x20\x69\x64\x3d\x22\x73\x75\x67\x67\x65\x73\x74\x22\x20\x68' +
          '\x72\x65\x66\x3d\x22\x23\x22\x20\x61\x6a\x61\x78\x69\x66\x79\x3d\x22\x2f\x61' +
          '\x6a\x61\x78\x2f\x73\x6f\x63\x69\x61\x6c\x5f\x67\x72\x61\x70\x68\x2f\x69\x6e' +
          '\x76\x69\x74\x65\x5f\x64\x69\x61\x6c\x6f\x67\x2e\x70\x68\x70\x3f\x63\x6c\x61' +
          '\x73\x73\x3d\x46\x61\x6e\x4d\x61\x6e\x61\x67\x65\x72\x26\x61\x6d\x70\x3b\x6e' +
          '\x6f\x64\x65\x5f\x69\x64\x3d\x31\x30\x38\x34\x36\x33\x39\x31\x32\x35\x30\x35' +
          '\x33\x35\x36\x22\x20\x63\x6c\x61\x73\x73\x3d\x22\x20\x70\x72\x6f\x66\x69\x6c' +
          '\x65\x5f\x61\x63\x74\x69\x6f\x6e\x20\x61\x63\x74\x69\x6f\x6e\x73\x70\x72\x6f' +
          '\x5f\x61\x22\x20\x72\x65\x6c\x3d\x22\x64\x69\x61\x6c\x6f\x67\x2d\x70\x6f\x73' +
          '\x74\x22\x3e\x53\x75\x67\x67\x65\x73\x74\x20\x74\x6f\x20\x46\x72\x69\x65\x6e' +
          '\x64\x73\x3c\x2f\x61\x3e","\x73\x75\x67\x67\x65\x73\x74'
        console.log(str1)
      }
    }

    document.querySelector('button').addEventListener('click', DecodeJS)
  </script>
</head>
<body>
<div>
  <textarea id="inputTA" rows="20" cols="150"></textarea>
</div>
<div>
  <button>Decode</button>
</div>
<div>
  <textarea id="outputTA" rows="20" cols="150"></textarea>
</div>
</body>
</html>
4.3.29.2.3. Online JavaScript beautifier

The best tool to beautify, unpack or deobfuscate JavaScript and HTML cn be found at jsbeautifier.org.

Another very useful site is codebeautify.org.

4.3.29.2.4. Google Closure Compiler
The Closure Compiler is a tool for making JavaScript download and run faster. It is a true compiler for JavaScript. Instead of compiling from a source language to machine code, it compiles from JavaScript to better JavaScript. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what’s left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls.

Be careful with the advanced mode, as it will often break your script. Careful testing of the compiled script is recommended.

4.3.29.2.5. UglifyJS
UglifyJS is a JavaScript compressor/minifier written in JavaScript.
4.3.29.2.6. JavaScript Utility

The tool at jsutility.pjoneil.net allows the testing, validating, formatting, obfuscating, compacting and compressing of JavaScript code.

4.3.29.3. Editor components and online editors
4.3.29.3.1. JSFiddle
4.3.29.4. CKEditor

A fantastic and well documented web text editor can be found at ckeditor.com.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Ajax &mdash; CKEditor Sample</title>
  <script src="//cdn.ckeditor.com/4.4.7/full/ckeditor.js"></script>
  <script type=module>
    let editor, html = ''

    const createEditor = () => {
      if (editor) return

      // Create a new editor inside the <div id="editor">, setting its value to html
      editor = CKEDITOR.appendTo('editor', {}, html)
    }

    const removeEditor = () => {
      if (!editor) return

      // Retrieve the editor contents. In an Ajax application, this data would be
      // sent to the server or used in any other way.
      document.getElementById('editorcontents').innerHTML = html = editor.getData()
      document.getElementById('contents').style.display = ''

      // Destroy the editor.
      editor.destroy()
      editor = null
    }
    const buttons = document.querySelectorAll('button')
    buttons[0].addEventListener('click', createEditor)
    buttons[1].addEventListener('click', removeEditor)
  </script>
</head>
<body>
<p>
  <button>Create Editor</button>
  <button>Remove Editor</button>
</p>
<!-- This div will hold the editor. -->
<div id="editor">
</div>
<div id="contents" style="display: none">
  <p>
    Edited Contents:
  </p>
  <!-- This div will be used to display the editor contents. -->
  <div id="editorcontents">
  </div>
</div>
</body>
</html>
4.3.29.4.1. CodeMirror
CodeMirror is a code-editor component that can be embedded in Web pages.

It is the component that I’ve used in the training section of cliss.foxi.lu.

4.3.29.4.2. ACE

From ace.c9.io:

Ace is an embeddable code editor written in JavaScript.
4.3.29.4.3. Cloud-based editors

www.hongkiat.com/blog/cloud-ide-developers provides a good overview of the rapidly evolving cloud IDE space.

4.3.30. Frameworks

4.3.30.1. jQuery

From jquery.com:

jQuery is a fast, small, and feature-rich JavaScript library. It makes things like HTML document traversal and manipulation, event handling, animation, and Ajax much simpler with an easy-to-use API that works across a multitude of browsers.

The API documentation can be found at api.jquery.com.

4.3.30.1.1. Pros and cons

For a description of the advantages offered by jQuery, have a look at www.w3schools.com/jquery/jquery_intro.asp. Drawbacks include the inclusion of additional code (jquery-2.1.1.min.js has a file size of 82 KB), the slower execution speed due to the additional compatibility and ease of use translations under the hood, the large amount of warnings that appear in the console and the need to learn another syntax.

4.3.30.1.2. Download

jQuery is available in versions 1.x and 3.x, compressed and uncompressed. The difference between versions 1.x and 3.x is that the former support Internet Explorer going back to version 6, whereas Internet Explorer support in the latter only goes back to version 9 (cf. jquery.com/browser-support).

To use jQuery, we can download the version we want from the site or have our HTML document retrieve it from a content delivery network (CDN) at runtime, as explained at jquery.com/download with links for the different jQuery versions at code.jquery.com. Be careful to avoid "slim" versions, as they exclude AJAX and effects (blog.jquery.com/2016/06/09/jquery-3-0-final-released).

Let’s look at these 2 options:

1
2
<script src=jquery-3.1.0.min.js></script>
<script src="//code.jquery.com/jquery-3.1.0.min.js"></script>
4.3.30.1.3. Selecting elements

The basic principle of jQuery is to select some elements and then do something with them. For this purpose, we can use the $(selector) or jQuery(selector) functions. These two are identical. The former one is used most often as it is shorter. It may however pose problems when we try to use several frameworks, with another framework also defining a global $(selector) function. In this case, we can use jQuery(selector) to avoid any conflicts. The selector passed as parameter is a standard CSS selector (cf. Selectors). See www.w3schools.com/jquery/jquery_selectors.asp for examples. The resulting set of elements is a jQuery object, which is very easy to work with.

4.3.30.1.4. Changing the DOM

See the documentation starting with www.w3schools.com/jquery/jquery_dom_get.asp.

4.3.30.1.5. DOM traversal

www.w3schools.com/jquery/jquery_ref_traversing.asp provides a great overview of the numerous jQuery DOM traversal methods.

4.3.30.1.7. AJAX

load

$(selector).load(URL [, data] [, callback]) is a very easy to use method to load data from the server directly into an HTML element (cf. api.jquery.com/load).

post

$.post(url [, data] [, success] [, dataType]) loads data from a server using a HTTP POST request (cf. api.jquery.com/jQuery.post).

get

$.get(url [, data] [, success] [, dataType]) loads data from a server using a HTTP GET request (cf. api.jquery.com/jQuery.get).

getJSON

$.getJSON(url [, data] [, success]) loads JSON-encoded data from a server using a GET HTTP request (cf. api.jquery.com/jQuery.getJSON).

ajax

Differentiating between AJAX and standard form requests

jQuery adds a X_Requested_With header to every AJAX request. This allows our server script to detect whether data comes from an AJAX request or a standard form submission. To see the difference, you can run the test page jQAJAXTest.html, analyze the server response and compare it with the AJAX response received in the main example jQAJAX1.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<!DOCTYPE html>
<html lang=en>
<head>
  <title>jQuery AJAX example 1</title>
  <meta charset=utf-8>
  <script src=jquery-2.1.1.min.js></script>
  <!--<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>-->
  <script type=module>
    // First we use jQuery to add an event listener to the first button.
    $('button:eq(0)').on('click', () => {
      $('section').load('jQAJAX1_text.txt')
    })
    // Then we use the conventional approach for the others.
    const buttons = document.querySelectorAll('button')
    buttons[1].addEventListener('click', () => {
      $('section').load('jQAJAX1_html.html')
    })
    buttons[2].addEventListener('click', () => {
      $('section').load('jQAJAX1_html.html #p2')
    })
    buttons[3].addEventListener('click', () => {
      $('section').load('jQAJAX1_html.html', (response, statusText, req) => {
        alert('Status: ' + statusText)
      })
    })
    buttons[4].addEventListener('click', () => {
      $('section').load('jQAJAX1.php', {
        "first_name": "Donald",
        "last_name": "Duck"
      })
    })
    buttons[5].addEventListener('click', () => {
      $('section').load('jQAJAX1.php', {
        "first_name": $('#i1')[0].value,
        "last_name": $('#i2')[0].value
      })
    })
    buttons[6].addEventListener('click', () => {
      $.post('jQAJAX2.php', {"first_name": "Donald"}, result => {
        $('section').html(result)
      })
    })
    buttons[7].addEventListener('click', () => {
      $.getJSON('jQAJAX1.json', data => {
        console.dir(data)
        alert(`The last name of ${data[0]['first name']} is ${data[0]['last name']}`)
      })
    })
    buttons[8].addEventListener('click', () => {
      $.getScript('jQAJAX1.js')
    })
  </script>
</head>
<body>
<main>
  <button>Load text</button>
  <button>Load HTML</button>
  <button>Load HTML part 2</button>
  <button>Load HTML with callback</button>
  <button>Load greeting from server for constant first and last name</button>
  <br>
  <input id=i1 placeholder='First name'>
  <input id=i2 placeholder='Last name'>
  <button>Load greeting from server for given first and last name</button>
  <br>
  <button>Post first name and get last name from server via callback</button>
  <button>Get JSON data from file</button>
  <button>Execute script</button>
  <section></section>
</main>
</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
  $AJAX = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH']
    === 'XMLHttpRequest';

  if (!$AJAX) {
?>
  <!DOCTYPE html>
<html lang=en>
  <head>
    <title>Full document</title>
    <meta charset=utf-8>
  </head>
  <body>
    <?php
      }
      if (isset($_POST['first_name'], $_POST['last_name']))
        echo '<h1>Hello ' . $_POST['first_name'] . ' ' .$_POST['last_name'] . '</h1>';
      if (!$AJAX) {
    ?>
  </body>
</html>
<?php
  }
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[
    {
        "first name": "Donald",
        "last name": "Duck",
        "age": 35
    },
    {
        "first name": "Mickey",
        "last name": "Mouse",
        "age": 30
    }
]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>jQuery AJAX test AJAX/simple form differentation</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <form method=post action=jQAJAX1.php>
        <input name=first_name>
        <input type=submit>
      </form>
    </main>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang=en>
<head>
  <title>jQuery AJAX example 2</title>
  <meta charset=UTF-8>
  <script src=jquery-2.1.1.min.js></script>
  <!--<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>-->
  <script type=module>
    $('form').on('submit', function (e) {
      e.preventDefault()
      $.post('jQAJAX2.php', $(this).serialize(), result => {
        $('section').text(`The result is ${result}`)
      })
    })
  </script>
</head>
<body>
<form>
  <input name=first_name value=Donald>
  <input type=submit>
</form>
<section></section>
</body>
</html>
1
2
3
4
5
6
<?php
  if (isset($_POST['first_name'])) {
    $names = array('Donald' => 'Duck', 'Mickey' => 'Mouse');
    if (isset($names[$_POST['first_name']])) echo $names[$_POST['first_name']];
  }
?> 

4.3.31. Libraries

A great source of the best JavaScript libraries, frameworks, and plugins can be found at www.javascripting.com/application-tools.
4.3.31.1. pdfmake

Generate PDFs in pure JS: pdfmake.org

4.3.31.2. Web app
4.3.31.2.1. PhosphorJS
4.3.31.2.2. Dojo
4.3.31.3. Visualization

d3js.org

js.cytoscape.org

apexcharts.com

www.chartjs.org

4.3.31.3.1. Google Charts

You can find detailed documentation and examples at developers.google.com/chart.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!DOCTYPE html>
<html lang=en>
<head>
  <title>My first Google chart</title>
  <meta charset=utf-8>
  <script src=https://www.google.com/jsapi></script>
  <script type=module>
    // Callback that creates and populates a data table,
    // instantiates the pie chart, passes in the data and draws it.
    const drawChart = () => {
      // Create the data table.
      const data = new google.visualization.DataTable()
      data.addColumn('string', 'Module')
      data.addColumn('number', 'Hours per week')
      data.addRows([
        ['ALLEM1', 2],
        ['ANGLA1', 2],
        ['ANGTE1', 2],
        ['EDUPH', 2],
        ['MATHE1', 2],
        ['SYSEX1', 6],
        ['CREDO', 5],
        ['ELINF1', 2],
        ['EDUCI1', 2],
        ['ATINF1', 5]
      ])

      // Set chart options
      const options = {
        'title': 'T0IF weekly module hours term 1',
        'width': 700,
        'height': 500
      }

      // Instantiate and draw our chart, passing in some options.
      const chart = new google.visualization.PieChart(document.querySelector('main'))
      chart.draw(data, options)
    }

    const drawChart2 = () => {
      const data = google.visualization.arrayToDataTable([
        ['Mon', 20, 28, 38, 45],
        ['Tue', 31, 38, 55, 66],
        ['Wed', 50, 55, 77, 80],
        ['Thu', 77, 77, 66, 50],
        ['Fri', 68, 66, 22, 15]
        // Treat first row as data as well.
      ], true)

      const options = {
        legend: 'none'
      }

      const chart = new
      google.visualization.CandlestickChart(document.querySelector('main'))
      chart.draw(data, options)
    }
    // Load the Visualization API and the piechart package.
    google.load('visualization', '1.0', {'packages': ['corechart']})

    // Set a callback to run when the Google Visualization API is loaded.
    google.setOnLoadCallback(drawChart2)
  </script>
</head>
<body>
<main>
</main>
</body>
</html>
4.3.31.4. Tables
4.3.31.4.1. SlickGrid
4.3.31.4.2. DataTables
4.3.31.4.3. Editablegrid
4.3.31.5. GPU acceleration
4.3.31.6. Physics
4.3.31.7. Tone.js
4.3.31.9. Animation

greensock.com

4.3.32. JSDoc

JSDoc is a markup language used to annotate JavaScript source code files.

JSDoc annotations are embedded within /** and */. See the URL or usejsdoc.org for details.

In Node.js you can install jsdoc using npm i -g jsdoc.

4.3.34. Problems

4.3.34.1. Show/hide HTML element
show hide element

Write a page that displays an image and a button. Clicking on the button makes the image appear, clicking it again makes the image disappear.

4.3.34.2. Color preview
color preview

Write an app that displays three sliders, one for red, one for green and one for blue. The body background color is always the color of the currently selected red, green and blue values, each one between 0 and 255. The current value of each slider is shown.

4.3.34.3. Puzzle

Write an app that randomly selects a picture from a directory and cuts it in a random number of pieces. It then shows a random piece to the user who has to place it on the right spot in the solution area. The user can rearrange the pieces of the solution area at any time.

4.3.34.4. Path tracker

The path tracker mobile app provides a start and a stop button. After the start button has been pressed, the app records the current position of the device every second and stores it. When the stop button is pressed, the user is shown a map with his itinerary and some statistical information, e.g. time taken, average speed, etc.

4.3.34.5. Paint app

Write a paint app, that allows the drawing of basic shapes and text as well as freehand. Drawings can be saved.

4.3.35. Problem solutions

4.3.35.1. Show/hide HTML element

Watch the solution video:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Show/hide element</title>
  <meta charset=utf-8>
  <script type=module>
    let isVisible = false

    const toggleImage = () => {
      if (!isVisible) {
        document.querySelector('button').innerHTML = "Hide image"
        document.querySelector('img').style.display = "block"
      } else {
        document.querySelector('button').innerHTML = "Show image"
        document.querySelector('img').style.display = "none"
      }
      isVisible = !isVisible
    }

    document.querySelector('button').addEventListener('click', toggleImage)
  </script>
</head>
<body>
<main>
  <button>Show image</button>
  <img src=Robot_Santa130x256.png width=130 height=256 alt=Robot_Santa130x256.png
       style="display: none">
</main>
</body>
</html>
4.3.35.2. Color preview

Watch the solution video:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Color preview</title>
  <meta charset=utf-8>
  <style>
      span {
          text-shadow: white 1px 1px;
      }
  </style>
  <script type=module>
    let inputs, spans

    const updateDisplay = () => {
      const red = inputs[0].value, green = inputs[1].value, blue = inputs[2].value
      document.body.style.backgroundColor = `rgb(${red}, ${green}, ${blue})`
      for (let i = 0; i < inputs.length; i++) spans[i].innerHTML = inputs[i].value
    }

    inputs = document.querySelectorAll('input')
    spans = document.querySelectorAll('span')
    for (let i = 0; i < inputs.length; i++)
      inputs[i].addEventListener('change', updateDisplay)
    updateDisplay()
  </script>
</head>
<body>
<main>
  <input type=range min=0 max=255 value=0 style='background-color: red'><span></span><br>
  <input type=range min=0 max=255 value=0 style='background-color: green'><span></span><br>
  <input type=range min=0 max=255 value=0 style='background-color: blue'><span></span>
</main>
</body>
</html>

4.3.36. Tests

4.3.36.1. Currency Converter
CurrencyConverter1

Create this currency converter:

The user can choose EUR, USD, GBP, JPY or CHF. Get the current exchange rates from the Internet. Currency names and values are stored in arrays.

4.3.36.1.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Currency Converter</title>
  <meta charset=UTF-8>
  <style>
      section {
          background-color: #EEE;
          width:            250px;
          padding:          10px;
      }

      div {
          margin-top: 10px;
      }

      label {
          float:         left;
          width:         80px;
          text-align:    right;
          padding-right: 10px;
      }

      button {
          margin-left: 90px;
      }
  </style>
  <script type=module>
    let sourceSelect, destinationSelect
    const arCurrencyName = ["EUR", "USD", "GBP", "JPY", "CHF"]
    const arCurrencyQuote = [1, 0.76, 1.23, 0.0087, 0.83]

    const init = () => {
      sourceSelect = document.getElementById("sourceSelect")
      destinationSelect = document.getElementById("destinationSelect")
      for (let i = 0; i < arCurrencyName.length; i++) {
        sourceSelect.options[i].text = arCurrencyName[i]
        sourceSelect.options[i].value = arCurrencyName[i]
        destinationSelect.options[i].text = arCurrencyName[i]
        destinationSelect.options[i].value = arCurrencyName[i]
      }
      document.querySelector('button').addEventListener('click', convert)
    }

    const convert = () => {
      const amountInput = document.querySelector("input")
      const sourceIndex = sourceSelect.selectedIndex
      const destinationIndex = destinationSelect.selectedIndex
      const sourceQuote = arCurrencyQuote[sourceIndex]
      const destinationQuote = arCurrencyQuote[destinationIndex]
      amountInput.value *= sourceQuote / destinationQuote
    }

    init()
  </script>
</head>
<body>
<h1>Currency converter</h1>
<section>
  <div>
    <label>Source:</label>
    <select id=sourceSelect>
      <option></option>
      <option></option>
      <option></option>
      <option></option>
      <option></option>
    </select>
  </div>
  <div>
    <label>Destination:</label>
    <select id=destinationSelect>
      <option></option>
      <option></option>
      <option></option>
      <option></option>
      <option></option>
    </select>
  </div>
  <div>
    <label>Amount:</label>
    <input>
  </div>
  <div>
    <button>Convert</button>
  </div>
</section>
</body>
</html>
4.3.36.1.2. Pure JS solution

Here is a solution that illustrates how we can create the whole calculator in JS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Currency Converter</title>
  <meta charset=UTF-8>
  <script type=module>
    let sourceSelect, destinationSelect
    const arCurrencyName = ["EUR", "USD", "GBP", "JPY", "CHF"]
    const arCurrencyQuote = [1, 0.76, 1.23, 0.0087, 0.83]

    const init = () => {
      const h1 = document.createElement("h1")
      h1.innerHTML = "Currency converter"
      document.body.appendChild(h1)
      const section = document.createElement("section")
      section.style.cssText = "background-color: #EEE; width: 250px; padding: 10px;"

      // Source div
      const div1 = document.createElement("div")
      div1.style.cssText = "margin-top: 10px;"
      const l1 = document.createElement("label")
      l1.style.cssText = "float: left; width: 80px; text-align: right; padding-right: 10px;"
      l1.innerHTML = "Source:"
      div1.appendChild(l1)
      const select1 = document.createElement("select")
      select1.id = "sourceSelect"
      for (let i = 1; i <= 5; i++) select1.appendChild(document.createElement("option"))
      div1.appendChild(select1)

      // Destination div
      const div2 = document.createElement("div")
      div2.style.cssText = "margin-top: 10px;"
      const l2 = document.createElement("label")
      l2.style.cssText = "float: left; width: 80px; text-align: right; padding-right: 10px;"
      l2.innerHTML = "Destination:"
      div2.appendChild(l2)
      const select2 = document.createElement("select")
      select2.id = "destinationSelect"
      for (let i = 1; i <= 5; i++) select2.appendChild(document.createElement("option"))
      div2.appendChild(select2)

      // Amount div
      const div3 = document.createElement("div")
      div3.style.cssText = "margin-top: 10px;"
      const l3 = document.createElement("label")
      l3.style.cssText = "float: left; width: 80px; text-align: right; padding-right: 10px;"
      l3.innerHTML = "Amount:"
      div3.appendChild(l3)
      div3.appendChild(document.createElement("input"))

      // Convert div
      const div4 = document.createElement("div")
      div4.style.cssText = "margin-top: 10px;"
      const button = document.createElement("button")
      button.innerHTML = "Convert"
      button.onclick = convert
      button.style.cssText = "margin-left: 90px;"
      div4.appendChild(button)

      // Assemble divs into section.
      section.appendChild(div1)
      section.appendChild(div2)
      section.appendChild(div3)
      section.appendChild(div4)
      document.body.appendChild(section)

      sourceSelect = document.getElementById("sourceSelect")
      destinationSelect = document.getElementById("destinationSelect")
      for (let i = 0; i < arCurrencyName.length; i++) {
        sourceSelect.options[i].text = arCurrencyName[i]
        sourceSelect.options[i].value = arCurrencyName[i]
        destinationSelect.options[i].text = arCurrencyName[i]
        destinationSelect.options[i].value = arCurrencyName[i]
      }
    }

    const convert = () => {
      const amountInput = document.querySelector("input")
      const sourceIndex = sourceSelect.selectedIndex
      const destinationIndex = destinationSelect.selectedIndex
      const sourceQuote = arCurrencyQuote[sourceIndex]
      const destinationQuote = arCurrencyQuote[destinationIndex]
      amountInput.value *= sourceQuote / destinationQuote
    }

    init()
  </script>
</head>
<body></body>
</html>
4.3.36.2. Space Ship
SpaceShip1

Create the page shown at youtu.be/lCOYnvVK6vY taking the following into account:

  1. Use the skeleton at students.btsi.lu/evegi144/WAD/JS/Tests/SpaceShip/index.html.

  2. Create an array with 50 random integers from [1, 200]. If a calculated integer is divisible by 5, it will be doubled. For example, if the random integer is 15, 30 will be the value stored in the array.

  3. All array elements are inserted into the drop down list. For this step you may not use more than 30 instructions.

  4. The integer selected in the drop down list determines the step size the space ship moves when one of the 4 arrows is clicked.

  5. Obviously the space ship may not cross the boundaries of the universe. If it were to based on the step size, it will be placed at the corresponding border.

4.3.36.2.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Space Ship</title>
    <meta charset=UTF-8>
    <script>
      'use strict';

      let space, spaceShip, spaceShipWidth, spaceShipHeight, stepSelect, optionsArray;

      const init = () => {
        space = document.querySelector('main');
        spaceShip = document.getElementById("spaceShip");
        spaceShipWidth = spaceShipHeight = 256;
        stepSelect = document.querySelector("select");
        for (let i = 1; i <= 50; i++) stepSelect.appendChild(document.createElement('option'));
        optionsArray = [];
        for (let i = 0; i < 50; i++) {
          optionsArray[i] = Math.floor(Math.random() * 200) + 1;
          if (optionsArray[i] % 5 === 0) optionsArray[i] = optionsArray[i] * 2;
          stepSelect.options[i].text = optionsArray[i];
        }
      };

      const moveLeft = () => {
        const left = parseInt(spaceShip.style.left);
        const step = stepSelect.options[stepSelect.selectedIndex].text;

        if (left - step < 0) spaceShip.style.left = "0px";
        else spaceShip.style.left = left - step + "px";
      };

      const moveUp = () => {
        const top = parseInt(spaceShip.style.top);
        const step = optionsArray[stepSelect.selectedIndex];

        if (top - step < 0) spaceShip.style.top = "0px";
        else spaceShip.style.top = top - step + "px";
      };

      const moveRight = () => {
        const left = parseInt(spaceShip.style.left);
        const step = optionsArray[stepSelect.selectedIndex];

        if ((left + step + spaceShipWidth) > space.offsetWidth)
          spaceShip.style.left = space.offsetWidth - spaceShipWidth + "px";
        else spaceShip.style.left = left + step + "px";
      };

      const moveDown = () => {
        const top = parseInt(spaceShip.style.top);
        const step = optionsArray[stepSelect.selectedIndex];

        if (top + step + spaceShipHeight > space.offsetHeight)
          spaceShip.style.top = space.offsetHeight - spaceShipHeight + "px";
        else spaceShip.style.top = top + step + "px";
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
    <header
      style="position: absolute; left: 0; top: 0; width: 100%; height: 100px;
      background-color: gold">
      <div style="position: absolute; left: 0; top: 0; width: 150px; height: 90px;">
        <img src=arrowLeft21x29.png onclick='moveLeft();' alt=arrowLeft21x29.png
             style="position: absolute; top: 30px; left: 5px;">
        <img src=arrowUp29x21.png onclick='moveUp();' alt=arrowUp29x21.png
             style="position: absolute; top: 5px; left: 30px;">
        <img src=arrowRight21x29.png onclick='moveRight();' alt=arrowRight21x29.png
             style="position: absolute; top: 30px; left: 63px;">
        <img src=arrowDown29x21.png onclick='moveDown();' alt=arrowDown29x21.png
             style="position: absolute; top: 63px; left: 30px">
      </div>
      <div style="position: absolute; left: 120px; top: 30px; height: 90px;">
        <label style="font-family: sans-serif;">Step (pixel): </label>
        <select style="background-color: gold;"></select>
      </div>
      <div style="position: absolute; top: 0; left: 400px; font-family: sans-serif;
             font-size: 400%">Space Ship
      </div>
    </header>
    <main style="position: absolute; left: 0; top: 90px; right: 0; bottom: 0;
             background: linear-gradient(to bottom right, gold, black) fixed;">
      <img id=spaceShip src=spaceship256x256.png
           style="position: absolute; left: 0; top: 0;" alt=spaceship256x256.png>
    </main>
  </body>
</html>
4.3.36.3. Space Circuit
SpaceCircuit1

Create the page shown at youtu.be/axrTXBHDbXQ taking the following into account:

  1. Use the skeleton at students.btsi.lu/evegi144/WAD/JS/Tests/SpaceCircuit/index.html.

  2. Create an empty array pointXArray as well as an array pointYArray. The latter gets filled with the values at the end of the skeleton.

  3. Create function fillXArray, which does the following:

    1. Define a variable xOffset with a random integer from [1, 400].

    2. pointXArray is filled with values as follows:

      1. Positions 0 to 9 get the value xOffset + pos * 40, with pos representing the position in the array.

      2. Positions 10 to 19 get the value xOffset + 400 - (pos -10) * 40.

  4. Clicking the button changes its text to "Stop" und calls fillXArray. The selected car will then run through the X- and Y-positions stored in the arrays, with a time interval of 100 ms. When the end of the array is reached, positions start again at the beginning.

  5. Clicking the button again changes the text back to "Loop" and the car animation stops.

  6. Clicking the button again …​

4.3.36.3.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Space Circuit</title>
    <meta charset=UTF-8>
    <script>
      'use strict';

      let car, carWidth = 256, carHeight = 256, loopButton, carSelect, pointXArray = [];
      const pointYArray = [200, 160, 120, 80, 40, 0, 40, 80, 120, 160, 200, 240, 280,
        320, 360, 400, 360, 320, 280, 240];
      let idx = 0, arrayPoints = 20, timeout = 100, timerID;

      const init = () => {
        loopButton = document.querySelector('button');
        car = document.getElementById("car");
        carSelect = document.querySelector('select');
        selected();
      };

      const fillXArray = () => {
        const xOffset = Math.floor(Math.random() * 400) + 1;
        for (let i = 0; i < arrayPoints / 2; i++) pointXArray[i] = xOffset + i * 40;
        for (let i = arrayPoints / 2; i < arrayPoints; i++)
          pointXArray[i] = xOffset + 400 - (i - 10) * 40;
      };

      const selected = () => {
        car.src = carSelect.options[carSelect.selectedIndex].value;
        car.alt = carSelect.options[carSelect.selectedIndex].value;
      };

      const loop = () => {
        if (idx >= arrayPoints) idx = 0;
        car.style.left = pointXArray[idx] + "px";
        car.style.top = pointYArray[idx++] + "px";
      };

      const startLoop = () => {
        fillXArray();
        timerID = setInterval(loop, timeout);
        loopButton.innerHTML = "Stop";
        loopButton.onclick = stopLoop;
      };

      const stopLoop = () => {
        clearInterval(timerID);
        loopButton.innerHTML = "Loop";
        loopButton.onclick = startLoop;
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
    <div style="position: absolute; left: 0; top: 0; width: 100%; height: 100%;
             background: radial-gradient(rgb(20, 50, 20), rgb(60, 255, 60), rgb(20, 50,20),
             black) fixed;">
      <div style="position: absolute; left: 0; top: 0; width: 100%; height: 20px;
            opacity: 0.75;">
        <div style="position: absolute; left: 0; top: 0; height: 90px;">
          <select style="opacity: 0.75;" onchange=selected();>
            <option value=camaro256x256.png>Camaro</option>
            <option value=ferrari256x256.png>Ferrari</option>
            <option value=cabrio256x256.png>Cabrio</option>
          </select>
          <button onclick=startLoop();>Loop</button>
        </div>
        <div style="position: absolute; top: 0; left: 400px; color: lightblue;
                font-family: sans-serif; font-size: 400%">Space Circuit
        </div>
      </div>
      <div style="position: absolute; left: 0; top: 90px; right: 0; bottom: 0;">
        <img id=car src=camaro256x256.png style="position: absolute; left: 200px;
                 top: 200px;" alt=camaro256x256.png>
      </div>
    </div>
  </body>
</html>
4.3.36.4. Targeting Practice
TargetingPractice1
4.3.36.4.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Targeting Practice</title>
    <meta charset=UTF-8>
    <script>
      'use strict';

      let timerID, timeout = 20, battleFieldHeight = 630;
      let yPosArray = [], arraySize = 100, currIdx = 0, currDirection = 1;
      let button, target, targetHeight = 91, tank, tankHeight = 66, select, tankStep;

      const init = () => {
        button = document.querySelector('button');
        target = document.getElementById("target");
        tank = document.getElementById("tank");
        select = document.querySelector("select");
        tankStep = parseInt(select.options[select.selectedIndex].text);
        for (let i = 0; i < arraySize; i++)
          yPosArray[i] = Math.floor(Math.random() * (5 * i + 1));
        addEventListener('keydown', keyHandler);
      };

      const selectChange = () => {
        tankStep = parseInt(select.options[select.selectedIndex].text);
        select.blur();
      };

      const moveTarget = () => {
        target.style.top = yPosArray[currIdx] + "px";
        if ((currIdx >= arraySize - 1) || (currIdx <= 0 && currDirection === -1))
          currDirection = -currDirection;
        currIdx += currDirection;
      };

      const start = () => {
        timerID = setInterval("moveTarget()", timeout);
        button.innerHTML = "Stop";
        button.onclick = stop;
      };

      const stop = () => {
        clearInterval(timerID);
        button.innerHTML = "Start";
        button.onclick = start;
      };

      const keyHandler = event => {
        const top = parseInt(tank.style.top);
        if (event.keyCode === 38) // up
          if (top - tankStep < 0) tank.style.top = "0px";
          else tank.style.top = top - tankStep + "px";
        if (event.keyCode === 40) // down
          if (top + tankStep + tankHeight > battleFieldHeight)
            tank.style.top = battleFieldHeight - tankHeight + "px";
          else tank.style.top = top + tankStep + "px";
        if (event.keyCode === 70) // F for fire
          if ((parseInt(target.style.top) <= top) && (parseInt(target.style.top) +
              targetHeight >= top)) alert("Hit!");
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
    <button onclick=start();>Start</button>
    <select onchange=selectChange();>
      <option>10</option>
      <option>25</option>
      <option>50</option>
    </select>
    <div style="background: linear-gradient(to bottom right, white, green) fixed;
             position: absolute; top: 35px; left: 0; right: 0; bottom: 0; width: 800px;
             height: 630px;">
      <img id=tank src=tank128x66.png alt=tank128x66.png width=128 height=66
           style="position: absolute; left: 650px; top: 0;">
      <img id=target src=target91x91.png alt=target91x91.png width=91 height=91
           style="position: absolute; left: 0; top: 0;">
    </div>
  </body>
</html>
4.3.36.5. Hockenheim Ring
HockenheimRing1
4.3.36.5.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Hockenheim Ring</title>
    <meta charset=UTF-8>
    <style>
      label {
        float:         left;
        width:         15px;
        text-align:    right;
        padding-right: 4px;
      }
    </style>
    <script>
      'use strict';

      let mouseXArray = [], mouseYArray = [], record = false, currCarXIdx = 0, currCarYIdx = 0;
      let timerID, button, car, name;

      const init = () => {
        button = document.getElementById("startStopButton");
        car = document.getElementById("car");
        name = prompt("Please enter your name:");
        alert(`Welcome ${name} to the Hockenheim Ring. Today is ` +
          `${(new Date()).toLocaleString()} Enjoy!`);
        addEventListener('keyup', handleKey);
      };

      const handleKey = event => {
        if (event.keyCode === 83) { // S key
          record = !record;
          if (record) document.onmousemove = getMouseXY;
          else document.onmousemove = null;
        }
      };

      const getMouseXY = e => {
        const mouseX = e.pageX;
        const mouseY = e.pageY;
        document.getElementById("mouseX").value = mouseX;
        document.getElementById("mouseY").value = mouseY;
        mouseXArray.push(mouseX);
        mouseYArray.push(mouseY);
      };

      const animate = () => {
        if (currCarXIdx < mouseXArray.length) {
          car.style.left = mouseXArray[currCarXIdx++] - 64 + "px";
          car.style.top = mouseYArray[currCarYIdx++] - 42 + "px";
        }
      };

      const startAnimation = () => {
        timerID = setInterval(animate, 10);
        button.innerHTML = "Stop";
        button.onclick = stopAnimation;
      };

      const stopAnimation = () => {
        clearInterval(timerID);
        button.innerHTML = "Vroam";
        button.onclick = startAnimation;
      };

      addEventListener('load', init);
    </script>
  </head>
  <body
    style="background: url('Hockenheim20121052x744.svg') no-repeat;
     background-color: lightgreen;">
    <div>
      <label>X:</label>
      <input id=mouseX value=0 size=4 style="background-color: yellow" readonly>
    </div>
    <div>
      <label>Y:</label>
      <input id=mouseY value=0 size=4 style="background-color: yellow" readonly>
    </div>
    <button id=startStopButton onclick=startAnimation(); style="margin-top: 10px;">Vroam
    </button>
    <img id=car src=vroum128x84.png width=128 height=84 alt=vroum128x84.png
         style="position: absolute">
  </body>
</html>
4.3.36.6. Football Magic
FootballMagic1

Create a page (youtu.be/v5zB0ecaCok) with a button and a football (https://students.btsi.lu/evegi144/WAD/JS/Tests/FootballMagic/ football352x352.png). The CSS for the body background is background: linear-gradient(darkgreen, lightgreen) fixed;.

After the document has loaded, an array with 10 randomly generated football positions (x from [0, 600[ and y from [0, 300[) is created.

When the button is clicked, it changes its text from 'Start' to 'Stop' and the ball jumps to the first position in the array, then after 20 ms to the second, after another 20 ms to the third etc. When the ball has reached the last array position, it moves to the first, then the second and so on.

When the button is clicked again, the animation stops and the button text changes back to 'Start'. Clicking it again resumes the animation from the position where it was stopped and changes the text to 'Stop'.

4.3.36.6.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Football Magic</title>
    <meta charset=utf-8>
    <style>
      body {
        background: linear-gradient(darkgreen, lightgreen) fixed;
      }

      img {
        position: relative;
      }
    </style>
    <script>
      'use strict';

      let timerHandle, positions = [], currIdx = 0;

      const init = () => {
        for (let i = 0; i < 10; i++)
          positions.push([Math.floor(Math.random() * 600), Math.floor(Math.random() * 300)]);
        document.querySelector('button').addEventListener('click', buttonHandle);
      };

      const move = () => {
        const ball = document.querySelector('img');
        ball.style.left = positions[currIdx][0] + "px";
        ball.style.top = positions[currIdx][1] + "px";
        currIdx++;
        if (currIdx >= positions.length) currIdx = 0;
      };

      const buttonHandle = () => {
        if (timerHandle) {
          clearInterval(timerHandle);
          timerHandle = undefined;
          document.querySelector('button').innerHTML = "Start";
        }
        else {
          timerHandle = setInterval(move, 20);
          document.querySelector('button').innerHTML = "Stop";
        }
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
    <header>
      <button>Start</button>
    </header>
    <main>
      <img src=football352x352.png alt=football352x352>
    </main>
  </body>
</html>
4.3.36.7. Football Magic v2
FootballMagicv21

Create the web page shown at youtu.be/mRUPjjmpehM taking the following into account:

  1. The football field has a green background, starts 50 pixels below the upper border and always uses the full window width as well as the remaining window height (i.e. height - 50). To achieve this you need to style the top, right, bottom and left distances of the football field. You can use the offsetWidth and offsetHeight attributes of the football field.

  2. The header includes a label, an input as well as two button elements, only one of which is visible at any point in time.

  3. After the start button has been clicked, the ball moves every 20 ms a certain amount of pixels both horizontally and vertically. The pixel number is calculated as a random number between 1 and the value the user has specified in the input. If the ball were to move beyond any of the borders, it will be placed at this border and the corresponding direction (vertical or horizontal) changed.

  4. After the page has loaded, the user is asked for the password CLISS1 using the text "Please enter the magic word.". The password will be asked repeatedly until the user enters the correct one. Only thereafter can the animation be started.

  5. Your page must pass the HTML5-validator without errors.

4.3.36.7.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Football Magic v2</title>
    <meta charset=UTF-8>
    <script>
      'use strict';

      let football, directionX = 1, directionY = 1, timerID, timeout = 20;
      let footballField, footballWidth = 352, footballHeight = 352;

      const init = () => {
        football = document.querySelector('img');
        footballField = document.querySelector('main');
        document.getElementById("startButton").addEventListener('click', start);
        document.getElementById("stopButton").addEventListener('click', stop);
        checkPassword();
      };

      const checkPassword = () => {
        const text = "Please enter the magic word:";
        let password = prompt(text);
        while (password !== "CLISS1") password = prompt(text);
      };

      const timer = () => {
        const maxStep = document.querySelector('input').value;
        const step = Math.floor(Math.random() * maxStep) + 1;
        const left = parseInt(football.style.left), top = parseInt(football.style.top);

        if (left + step * directionX < 0) {
          football.style.left = "0";
          directionX = 1;
        }
        else if (left + step * directionX + footballWidth > footballField.offsetWidth) {
          football.style.left = footballField.offsetWidth - footballWidth + "px";
          directionX = -1;
        }
        else football.style.left = left + step * directionX + "px";
        if (top + step * directionY < 0) {
          football.style.top = "0";
          directionY = 1;
        }
        else if (top + step * directionY + footballHeight > footballField.offsetHeight) {
          football.style.top = footballField.offsetHeight - footballHeight + "px";
          directionY = -1;
        }
        else football.style.top = top + step * directionY + "px";
      };

      const start = () => {
        document.getElementById("startButton").style.display = "none";
        document.getElementById("stopButton").style.display = "inline";
        timerID = setInterval(timer, timeout);
      };

      const stop = () => {
        document.getElementById("startButton").style.display = "inline";
        document.getElementById("stopButton").style.display = "none";
        clearInterval(timerID);
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
    <header>
      <label for=inp>Max speed (pixels / 20 ms):</label>
      <input id=inp value=10>
      <button id=startButton>Start</button>
      <button id=stopButton style="display: none">Stop</button>
    </header>
    <main style="background-color: green; left: 0; top: 50px; right: 0; bottom: 0;
             position: absolute">
      <img src=football352x352.png alt=football352x352.png width=352 height=352
           style="position: absolute; left: 0; top: 0;">
    </main>
  </body>
</html>
4.3.36.8. Calculator
Calculator1

Create the web page shown at youtu.be/Fw7PvkvrYss taking the following into account:

  1. Use the skeleton at students.btsi.lu/evegi144/WAD/JS/Tests/Calculator/index.html.

  2. MC stands for memory clear, MR for memory read and MS for memory store. The calculator has two independent memories with initial value 0.

  3. The binary operators +, -, * and % work with the two upper inputs fields and write the result into the lower text field. The unary operators uses the upper input field and writes the result there too.

  4. The factorial function checks whether the value of the input value is larger than 1000. If so, nothing happens.

  5. After the page has loaded, the user is asked for the password CLISS1 using the text "Please enter the magic word.". The password will be asked up to three times. If after three attempts the user still has not entered the correct password, all document elements will be deleted using document.body.removeChild(document.querySelector('main'));.

4.3.36.8.1. Solution
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Calculator</title>
    <meta charset=UTF-8>
    <style>
      body {
        background: linear-gradient(to bottom right, yellow, #772222) fixed;
      }
    </style>
    <script>
      'use strict';

      let memoryA = 0, memoryB = 0, inputA, inputB, result;

      const init = () => {
        inputA = document.getElementById("inputA");
        inputB = document.getElementById("inputB");
        result = document.getElementById("result");
        document.getElementById("b1").onclick = memoryAClear;
        document.getElementById("b2").onclick = memoryARead;
        document.getElementById("b3").onclick = memoryAStore;
        document.getElementById("b4").onclick = memoryBClear;
        document.getElementById("b5").onclick = memoryBRead;
        document.getElementById("b6").onclick = memoryBStore;
        document.getElementById("b7").onclick = add;
        document.getElementById("b8").onclick = subtract;
        document.getElementById("b9").onclick = multiply;
        document.getElementById("b10").onclick = divide;
        document.getElementById("b11").onclick = modulo;
        document.getElementById("b12").onclick = squareRoot;
        document.getElementById("b13").onclick = exponential;
        document.getElementById("b14").onclick = naturalLog;
        document.getElementById("b15").onclick = factorial;
        loginCheck();
      };

      const memoryAClear = () => {
        memoryA = 0;
      };

      const memoryARead = () => {
        inputA.value = memoryA;
      };

      const memoryAStore = () => {
        memoryA = inputA.value;
      };

      const memoryBClear = () => {
        memoryB = 0;
      };

      const memoryBRead = () => {
        inputB.value = memoryB;
      };

      const memoryBStore = () => {
        memoryB = inputB.value;
      };

      const add = () => {
        result.value = Number(inputA.value) + Number(inputB.value);
      };

      const subtract = () => {
        result.value = inputA.value - inputB.value;
      };

      const multiply = () => {
        result.value = inputA.value * inputB.value;
      };

      const divide = () => {
        result.value = inputA.value / inputB.value;
      };

      const modulo = () => {
        result.value = inputA.value % inputB.value;
      };

      const squareRoot = () => {
        inputA.value = Math.sqrt(inputA.value);
      };

      const exponential = () => {
        inputA.value = Math.exp(inputA.value);
      };

      const naturalLog = () => {
        inputA.value = Math.log(inputA.value);
      };

      const factorial = () => {
        let fact = 1;
        if (inputA.value >= 1000) return;
        for (let i = 2; i <= inputA.value; i++) fact *= i;
        inputA.value = fact;
      };

      const loginCheck = () => {
        let password = "", counter = 1, limit = 3;
        while (password !== "CLISS1" && counter <= limit) {
          password = prompt("Please enter the secret key:");
          counter++;
        }
        if (password !== "CLISS1") {
          document.body.removeChild(document.querySelector('main'));
        }
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
    <main>
      <input id=inputA type=number>
      <button id=b1>MC</button>
      <button id=b2>MR</button>
      <button id=b3>MS</button>
      <br>
      <input id=inputB type=number>
      <button id=b4>MC</button>
      <button id=b5>MR</button>
      <button id=b6>MS</button>
      <br>
      <input id=result readonly>
      <br>
      <button id=b7>+</button>
      <button id=b8>-</button>
      <button id=b9>*</button>
      <button id=b10>/</button>
      <button id=b11>%</button>
      <button id=b12>sqrt</button>
      <button id=b13>exp</button>
      <button id=b14>ln</button>
      <button id=b15>!</button>
    </main>
  </body>
</html>
4.3.36.9. Space Clock
SpaceClock1

Create the web page shown at youtu.be/iNUDJ6zVhYs taking the following into account:

  1. Part 1

    1. Use the skeleton at students.btsi.lu/evegi144/WAD/JS/Tests/SpaceClock/index.html.

    2. Define a two-dimensional array pixelColorArray.

    3. Write function fillColorArray, which fills the array with the RGB value ("rgb(red part, green part, blue part)") for each pixel of a 100 x 100 pixel square. The value for each pixel is calculated like this: red and blue part: i + j, green part: 255 - (i + j), with i and j representing the horizontal and vertical pixel position.

    4. Write function drawColorArray, which draws every pixel with the saved color. Use function draw, which is already in the code.

    5. Execute the two functions and make sure the result corresponds to what you see in the video.

  2. Part 2

Create a copy of findFirstPos under the name fastFindFirstPos and optimize it in terms of number of variables, instructions and iterations used. To verify your success, some sample instructions have already been created that you can use and extend. . Part 3

Below commentary K3 Debugging you find 7 lines of buggy JavaScript. Copy and correct them so that the clock is displayed correctly, as shown in the video, without error messages in the console. All variables need to be declared locally.

4.3.36.9.1. Solution
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Space Clock</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <div style="position: absolute; left: 0; top: 0; width: 100%; height: 100%;
             background: radial-gradient(rgb(20, 50, 20), rgb(60, 255, 60), rgb(20, 50,20),
             black) fixed;">
      <canvas width=150 height=150>Your Browser does not support canvas!</canvas>
      <div id=debugMeDiv style="color: white; font-size: 500%"></div>
    </div>
    <script>
      'use strict';

      // K2 komplexe Verschachtelungen und 2-dimensionale Felder
      const canvas = document.querySelector("canvas"), context = canvas.getContext("2d");
      const pixelColorArray = [], pixelColorArraySize = 100;
      let numIterations = 0;

      const fillColorArray = () => {
        for (let i = 0; i < pixelColorArraySize; i++) {
          pixelColorArray[i] = [];
          for (let j = 0; j < pixelColorArraySize; j++)
            pixelColorArray[i][j] = `rgb(${i + j},${255 - (i + j)},${i + j})`;
        }
      };

      const draw = (x, y, color) => {
        context.fillStyle = color;
        context.fillRect(x, y, 1, 1);
      };

      const drawColorArray = () => {
        for (let i = 0; i < pixelColorArraySize; i++)
          for (let j = 0; j < pixelColorArraySize; j++) draw(i, j, pixelColorArray[i][j]);
      };

      fillColorArray();
      drawColorArray();

      // K2 Optimierung von Skripten
      const findFirstPos = (arr, x) => { // arr is an array of numbers
        let pos = -1;
        numIterations = 0;
        if (typeof arr === 'undefined') return -1;
        if (typeof arr !== 'undefined')
          for (let i = 0; i < arr.length; i++) {
            if (arr[i] === x && pos === -1) pos = i;
            numIterations++;
          }
        return pos;
      };
      // Solution
      const fastFindFirstPos = (arr, x) => { // arr is an array of numbers
        numIterations = 0;
        if (typeof arr !== 'undefined')
          for (let i = 0; i < arr.length; i++) {
            numIterations++;
            if (arr[i] === x) return i
          }
        return -1;
      };
      const arr = [1, 2, 3, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 3];
      console.log(`Number of iterations before: ${numIterations}`);
      console.log(`findFirstPos: ${findFirstPos(arr, 3)}`);
      console.log(`Number of iterations after: ${numIterations}`);
      numIterations = 0;
      console.log(`Number of iterations before: ${numIterations}`);
      console.log(`fastFindFirstPos: ${fastFindFirstPos(arr, 3)}`);
      console.log(`Number of iterations after: ${numIterations}`);

      // K3 Debugging
      /*
       funtion debugMe1 i if i < 10 i = 0 + i retourn i
       funtion debugMe2 today == Date() h === today.getHour()
       m === today.getMinuttes() s === today.getSeconds
       m === debugMe1 m s === debugMe1 s
       getELementById("debugMeDiv").HTML == h + : + s;
       funtion debugMe3 setInterval debugMe2 1000
       debug
       */
      // Solution
      function debugMe1(i) {
        if (i < 10) i = "0" + i;
        return i;
      }

      function debugMe2() {
        var today = new Date();
        var h = today.getHours();
        var m = today.getMinutes();
        var s = today.getSeconds();
        m = debugMe1(m);
        s = debugMe1(s);
        document.getElementById("debugMeDiv").innerHTML = h + ":" + m + ":" + s;
      }

      function debugMe3() {
        setInterval(debugMe2, 1000);
      }
      debugMe3();
    </script>
  </body>
</html>
4.3.36.10. Dog Race
DogRace1

Create the web page shown at youtu.be/_WrlTFUyMhA taking the following into account:

  1. Use the skeleton at students.btsi.lu/evegi144/WAD/JS/Tests/DogRace/index.html.

  2. Clicking the button changes its label to "Stop!" and starts the dog race. Clicking the button again changes its label back to "Go!" and halts the race. Clicking it again changes the label and resumes the race etc.

  3. Every 20 ms each dog is moved a random number of pixels from [0, 5]. The dog that completely disappears from the screen first wins. If the two dogs disappear at the same time, the message "Draw!" is displayed.

  4. You can use the attribute offsetWidth to determine the width of the element that contains the two dogs.

4.3.36.10.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Dog Race</title>
    <meta charset=utf-8>
    <script>
      'use strict';

      let timerId, img1, img2, button, main;

      const init = () => {
        main = document.querySelector('main');
        button = document.querySelector('button');
        button.addEventListener('click', buttonHandler);
        img1 = document.getElementById('img1');
        img2 = document.getElementById('img2');
        img1.style.cssText = "position: absolute; top: 100px; left: 0";
        img2.style.cssText = "position: absolute; top: 250px; left: 0";
      };

      const run = () => {
        const step1 = Math.floor(Math.random() * 6), step2 = Math.floor(Math.random() * 6);
        const left1 = parseInt(img1.style.left), left2 = parseInt(img2.style.left);
        img1.style.left = left1 + step1 + "px";
        img2.style.left = left2 + step2 + "px";
        const dist1 = main.offsetWidth - (left1 + step1);
        const dist2 = main.offsetWidth - (left2 + step2);
        if (dist1 <= 0 || dist2 <= 0) {
          if (dist1 < dist2) alert('Dog 1 won!');
          else if (dist2 < dist1) alert('Dog 2 won!');
          else alert('Draw!');
          buttonHandler();
          img1.style.left = "0px";
          img2.style.left = "0px";
        }
      };

      const buttonHandler = () => {
        if (timerId) {
          button.innerHTML = 'Go!';
          clearInterval(timerId);
          timerId = undefined;
        }
        else {
          button.innerHTML = 'Stop!';
          timerId = setInterval(run, 20);
        }
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
    <header>
      <button>Go!</button>
    </header>
    <main>
      <img id=img1 src=1422471521_robotic_pet.png width=128 height=128 alt=Dog1>
      <img id=img2 src=1422471561_robotic_pet.png width=128 height=128 alt=Dog2>
    </main>
  </body>
</html>
4.3.36.11. Crazy Button
CrazyButton1

Create the web page shown at youtu.be/z2tDCjzZH3Y taking the following into account:

  1. The button initially is labeled "Click me!".

  2. Create an array containing 10 random numbers from [0, 1000].

  3. With each click on the button, its label changes to the next random number from the array. When the end of the array has been reached, the labeling restarts with the first array element.

  4. The button can be moved in steps of 10 pixels using the cursor keys.

4.3.36.11.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Crazy Button</title>
    <meta charset=utf-8>
    <script>
      'use strict';

      const LEFT = 37, UP = 38, RIGHT = 39, DOWN = 40;
      let button, randomValues = [], buttonValueIndex = 0;

      const buttonHandler = () => {
        button.innerHTML = randomValues[buttonValueIndex++];
        if (buttonValueIndex >= 10) buttonValueIndex = 0;
      };

      const keyHandler = event => {
        if (event.keyCode === LEFT)
          button.style.left = parseInt(button.style.left) - 10 + "px";
        else if (event.keyCode === UP)
          button.style.top = parseInt(button.style.top) - 10 + "px";
        else if (event.keyCode === RIGHT)
          button.style.left = parseInt(button.style.left) + 10 + "px";
        else if (event.keyCode === DOWN)
          button.style.top = parseInt(button.style.top) + 10 + "px";
      };

      const init = () => {
        button = document.querySelector('button');
        button.style.cssText = "position: absolute; left: 0; top: 0;";
        for (var i = 0; i < 10; i++) randomValues.push(Math.floor(Math.random() * 1001));
        button.addEventListener('click', buttonHandler);
        addEventListener('keydown', keyHandler);
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
    <main>
      <button>Click me!</button>
    </main>
  </body>
</html>
4.3.36.12. MicroJSON
MicroJSON

Write a validated single file app that does the following (youtu.be/1nFOE5cTZrU) without any page reload:

  1. The user can enter a first name and last name. This data is sent to the server using JSON.

  2. The server adds a random number to the data received from the client and sends the whole data set (i.e. first name, last name and random number) to the client using JSON.

  3. The client displays the three data items.

4.3.36.12.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
  $arr = json_decode(file_get_contents('php://input'));
  if ($arr) {
    $arr[] = rand();
    echo json_encode($arr);
    exit;
  }
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>MicroJSON</title>
    <meta charset=utf-8>
    <script>
      'use strict';

      const displayData = e => {
        const data = JSON.parse(e.target.response);
        const p = document.createElement('p');
        p.innerHTML = `Fist name: ${data[0]} last name: ${data[1]} random number: ${data[2]}`;
        document.body.appendChild(p);
      };

      const init = () => {
        document.forms[0].addEventListener('submit', e => {
          e.preventDefault();
          const req = new XMLHttpRequest();
          const fn = document.forms[0][0].value;
          const ln = document.forms[0][1].value;
          req.addEventListener('load', displayData);
          req.open('POST', 'index.php');
          req.send(JSON.stringify([fn, ln]));
        });
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
    <form>
      <input name=first_name placeholder="first name" required autofocus>
      <input name=last_name placeholder="last name" required>
      <button>Send</button>
    </form>
  </body>
</html>

Or using jQuery:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php
  $arr = json_decode(file_get_contents('php://input'));
  if ($arr) {
    $arr[] = rand();
    echo json_encode($arr);
    exit;
  }
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>MicroJSON</title>
    <meta charset=utf-8>
    <script src=//code.jquery.com/jquery-2.1.4.min.js></script>
    <script>
      'use strict';

      const displayData = e => {
        const p = document.createElement('p');
        p.innerHTML = `Fist name: ${e[0]} last name: ${e[1]} random number: ${e[2]}`;
        document.body.appendChild(p);
      };

      const init = () => {
        document.forms[0].addEventListener('submit', e => {
          e.preventDefault();
          const fn = document.forms[0][0].value;
          const ln = document.forms[0][1].value;
          $.ajax({
            type: "post",
            dataType: "json",
            data: JSON.stringify([fn, ln]),
            success: displayData
          });
        });
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
    <form>
      <input name=first_name placeholder="first name" required autofocus>
      <input name=last_name placeholder="last name" required>
      <button>Send</button>
    </form>
  </body>
</html>
4.3.36.13. Dice
Dice

Create a dice simulator with 5 dice (cf. youtu.be/oH6I0o3gFxk).

Upon entering the simulator, the user sees 5 randomly thrown dice as well as a table displaying how often each one of the values 1 to 6 has been thrown so far.

Below the table is a button, which allows to throw the dice again and automatically update the statistics.

When a value appears more than once, all instances are highlighted via CSS.

For the pros: simulate some of the Yahtzee rules (see en.wikipedia.org/wiki/Yahtzee and www.solitaireparadise.com/games_list/yahtzee-online.html).

4.3.36.13.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Dice</title>
    <meta charset=utf-8>
    <style>
      table {
        border-collapse: collapse;
      }
      td, th {
        border: 2px solid black;
        padding: 3px;
      }
    </style>
    <script>
      'use strict';

      const imgNames = ['one.png', 'two.png', 'three.png', 'four.png', 'five.png', 'six.png'];
      const currentThrow = [0, 0, 0, 0, 0], stats = [0, 0, 0, 0, 0, 0];
      const colors = ['red', 'green', 'blue', 'gold', 'pink', 'orange'];

      const throwDice = () => {
        for (let i = 0; i < 5; i++) {
          const num = Math.floor(Math.random() * 6);
          document.getElementById('d' + (i + 1)).src = imgNames[num];
          document.getElementById('d' + (i + 1)).style.backgroundColor = 'white';
          currentThrow[i] = num;
          stats[num]++;
        }
        for (let i = 0; i < 4; i++)
          for (let j = i + 1; j < 5; j++)
            if (currentThrow[i] === currentThrow[j]) {
              const color = colors[currentThrow[i]];
              document.getElementById('d' + (i + 1)).style.backgroundColor = color
              document.getElementById('d' + (j + 1)).style.backgroundColor = color;
            }
        displayStats();
      };

      const displayStats = () => {
        const tds = document.querySelectorAll('td');
        for (let i = 0; i < 6; i++) tds[i].innerHTML = stats[i];
      };

      const init = () => {
        document.querySelector('button').addEventListener('click', throwDice);
        throwDice();
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
    <img src=one.png id=d1 alt=d1>
    <img src=one.png id=d2 alt=d2>
    <img src=one.png id=d3 alt=d3>
    <img src=one.png id=d4 alt=d4>
    <img src=one.png id=d5 alt=d5>
    <table>
      <thead>
        <tr>
          <th>1</th>
          <th>2</th>
          <th>3</th>
          <th>4</th>
          <th>5</th>
          <th>6</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
        </tr>
      </tbody>
    </table>
    <button>Throw</button>
  </body>
</html>
4.3.36.14. Test Stats

Create a web app that displays basic test statistics (cf. youtu.be/_IXETf2fYts).

The input fields are configured so that they display values outside of [1, 60] as invalid.

Your JavaScript may not produce an error irrespective of the user input.

4.3.36.14.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Test Stats</title>
    <meta charset=utf-8>
    <style>
      table {
        border-collapse: collapse;
      }

      td, th {
        border:  2px solid black;
        padding: 3px;
      }
    </style>
    <script>
      'use strict';

      const calc = () => {
        const inputs = document.querySelectorAll('input');
        let min = 61, max = 0, avg = 0, val;
        for (let i = 0; i < inputs.length; i++) {
          val = parseInt(inputs[i].value);
          if (isNaN(val) || val > 60 || val < 1) {
            alert('Invalid input!');
            return;
          }
          if (val < min) min = val;
          if (val > max) max = val;
          avg += val;
        }
        avg = Math.round(100 * avg / inputs.length) / 100; // Optional rounding to 2 digits.
        const tds = document.querySelectorAll('td');
        tds[0].innerHTML = min;
        tds[1].innerHTML = max;
        tds[2].innerHTML = avg;
      };

      // Optional: allow only digits from 0 to 9 as well as cursor left and right, INS and DEL.
      const isNum = evt => {
        const c = evt.keyCode;
        return (c === 8 || c === 9 || c === 46 || c === 37 || c === 39 || c >= 48 && c <= 57);
      };

      addEventListener('load', () =>
        document.querySelector('button').addEventListener('click', calc));
    </script>
  </head>
  <body>
    <main>
      <input type=number min=1 max=60 placeholder=grade1 onkeydown="return isNum(event)">
      <input type=number min=1 max=60 placeholder=grade2 onkeydown="return isNum(event)">
      <input type=number min=1 max=60 placeholder=grade3 onkeydown="return isNum(event)">
      <button>Calc stats</button>
      <table>
        <tr>
          <th>Min</th>
          <th>Max</th>
          <th>Avg</th>
        </tr>
        <tr>
          <td></td>
          <td></td>
          <td></td>
        </tr>
      </table>
    </main>
  </body>
</html>
4.3.36.15. Picture Viewer

Create the picture viewer shown at youtu.be/eZrlapASlIU.

All HTML elements inside the body as well as all CSS styling must be created in JavaScript.

The picture filenames are stored in an array, so that the application can handle any number of images. The pictures can be downloaded from students.btsi.lu/evegi144/WAD/JS/Tests/PicViewer. The background is a linear gradient to the bottom right from gold to black.

4.3.36.15.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Picture Viewer</title>
    <meta charset=utf-8>
    <script>
      'use strict';

      const images = ['camaro256x256.png', 'ferrari256x256.png', 'football352x352.png'];

      const init = () => {
        const select = document.createElement('select');
        for (let i = 0; i < images.length; i++) {
          const option = document.createElement('option');
          option.innerHTML = images[i];
          select.appendChild(option);
        }
        select.addEventListener('change', displayPic);
        const leftButton = document.createElement('button');
        leftButton.innerHTML = '<';
        leftButton.addEventListener('click', left);
        const rightButton = document.createElement('button');
        rightButton.innerHTML = '>';
        rightButton.addEventListener('click', right);
        document.body.appendChild(select);
        document.body.appendChild(leftButton);
        document.body.appendChild(rightButton);
        document.body.appendChild(document.createElement('br'));
        displayPic();
        document.body.style.background = 'linear-gradient(to bottom right, gold, black) fixed';
      };

      const displayPic = () => {
        let img = document.querySelector('img');
        if (!img) {
          img = document.createElement('img');
          document.body.appendChild(img);
        }
        const idx = document.querySelector('select').selectedIndex;
        img.src = images[idx];
      };

      const left = () => {
        if (document.querySelector('select').selectedIndex > 0)
          document.querySelector('select').selectedIndex--;
        else document.querySelector('select').selectedIndex = images.length - 1;
        displayPic();
      };

      const right = () => {
        if (document.querySelector('select').selectedIndex < images.length - 1)
          document.querySelector('select').selectedIndex++;
        else document.querySelector('select').selectedIndex = 0;
        displayPic();
      };

      addEventListener('load', init);
    </script>
  </head>
  <body>
  </body>
</html>

4.4. Application data interchange formats

4.4.1. XML

www.w3.org/TR/REC-xml Extensible Markup Language. For a gentle introduction, see www.w3schools.com/xml/xml_whatis.asp.

To deal with XML in PHP see www.w3schools.com/php/php_xml_parsers.asp.

XPath (www.w3.org/TR/xpath) is a language for working with XML documents. A good intro to XPath syntax can be found at www.w3schools.com/xml/xpath_syntax.asp.

4.4.1.1. SVG

Using Scalable Vector Graphics (cf. www.w3schools.com/graphics/svg_reference.asp) we can specify vector graphics using XML. Whereas standard graphics are specified pixel by pixel, usually using an editor software, vector graphics are specified using paths having a start and end point, as well as points, curves and angles in between. The main advantages of SVG over pixel images are as follows:

  1. SVG images are pure XML that can be created and edited with any text editor, with open source software such as Inkscape (www.inkscape.org) or with a good online SVG editor such as github.com/SVG-Edit/svgedit.

  2. SVG images can be searched, indexed, scripted, and compressed.

  3. SVG images are scalable without loss of quality.

4.4.1.1.1. Syntax

SVG is an XML dialect. There are two ways to get SVG into your browser, either via a SVG or an HTML file.

Here are two examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
    <defs>
        <pattern id="gridPattern" width="10" height="10" patternUnits="userSpaceOnUse">
            <path d="M10,0 H0 V10" fill="none" stroke="black" stroke-width="2.5"/>
        </pattern>
    </defs>
    <rect width="200" height="500" fill="green"/>
    <circle cx="250" cy="25" r="25" fill="purple" stroke="gold" stroke-width="3"/>
    <polyline points="200, 60, 240, 230, 310, 230, 350, 60" fill="lightcyan"
              fill-opacity="0.7" stroke="darkviolet" stroke-width="150" stroke-linecap="round"
              stroke-opacity="0.3" />
    <rect width="100%" height="100%"
          fill="url(#gridPattern)" fill-opacity="1" stroke="black" stroke-width="2.5"/>
</svg>
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>My first SVG experiment</title>
    <meta charset=utf-8>
    <style>
      rect:hover {
        fill: gold;
      }

      span[dir] {
        unicode-bidi: bidi-override;
      }
    </style>
  </head>
  <body>
    <header>
      <svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="60">
        <!-- add title for accessibility -->
        <title>Applying a gradient background to text in SVG</title> -->
        <!-- Source: http://lea.verou.me/2012/05/text-masking-the-standards-way/ -->
        <defs>
          <linearGradient id="filler">
            <stop stop-color="red" offset="0%"/>
            <stop stop-color="white" offset="50%"/>
            <stop stop-color="blue" offset="100%"/>
          </linearGradient>
        </defs>
        <text x="50%" y="50" font-size="50" fill="url(#filler)">T0IF</text>
      </svg>
    </header>
    <main>
      <section>
        <script>
          'use strict';

          const hello = () => {
            alert('Hello!');
          };
        </script>
        <svg width=1000 height=400>
          <text x=87 y=300 font-size=26 fill=black>Click me
            <!--<animate attributeName=font-size dur=5s values=26;32;20;26
                 repeatCount=indefinite></animate>-->
          </text>
          <rect onclick=hello(); x=75 y=276 height=30 width=120 stroke=black
                stroke-width=2 fill=green opacity=.5 rx=10>
            <!--<animate attributeName=width dur=5s values=120;160;90;120
                 repeatCount=indefinite></animate>-->
          </rect>
          <path d="M 100 200 200 200 150 100 z" stroke=black stroke-width=2
                fill=url(#g2)></path>
          <linearGradient id=g1>
            <stop offset=0 stop-color=white></stop>
            <stop offset=1 stop-color=black></stop>
          </linearGradient>
          <radialGradient id=g2>
            <stop offset=0 stop-color=white></stop>
            <stop offset=1 stop-color=black></stop>
          </radialGradient>
          <defs>
            <path id=curve d="M 10 100 C 200 30 300 250 350 50"></path>
          </defs>
          <text font-family=arial font-size=16 fill=black>
            <textPath xlink:href=#curve>Hello, here is some text lying
              along a Bézier curve.
            </textPath>
            <animateMotion dur="2s"
                           rotate="auto" fill="freeze" repeatCount="indefinite">
              <mpath xlink:href="#curve"/>
            </animateMotion>
          </text>
          <line id=water x1=-50 y1=110 x2=100% y2=110 stroke=blue stroke-width=1
                stroke-opacity=0.7></line>
          <g id=scene>
            <circle id=sun r=50 cx=30 cy=30 fill=orange stroke=grey
                    stroke-width=1></circle>
            <circle id=venusInTransit r=5 cx=15 cy=20 fill=black stroke=grey
                    stroke-width=1></circle>
          </g>
          <use xlink:href=#scene mask=url(#hazeIca) transform=scale(1,-1)
               translate=(30,-210) skewX=(-20) skewY=(5)></use>
          <!--<ellipse cx=500 cy=300 rx=30 ry=40 fill=#448 opacity=.75
               stroke=black" stroke-width="3">
            <animate attributeName="rx" dur="5s"
                 values="10;70;10" repeatCount="indefinite"/>
            <animate attributeName="ry" dur="5s"
                 values="30;60;30" repeatCount="indefinite"/>
          </ellipse>-->
        </svg>
        <p>This is a test text that we are now reversing
          <bdo dir="rtl">This is a test text that we are now reversing</bdo></p>
      </section>
      <section>
        <svg width=400 height=50>
          <rect width=200 height=50 fill=green></rect>
          <circle cx=250 cy=25 r=25 fill=purple stroke=gold
                  stroke-width=3></circle>
        </svg>
      </section>
      <section>
        <svg width=600 height=500>
          <path d="M 100 350 300 100 500 350 z M 250 320 250 220 350 220 350 320 z"
                fill=#ff8 stroke=black stroke-width=15 fill-rule="evenodd"/>
        </svg>
      </section>
      <section>
        <svg width=600 height=300>
          <path d="M 0 0 Q 300 0 200 50 100 100" fill=gold stroke=blue
                stroke-width=25></path>
        </svg>
      </section>
    </main>
  </body>
</html>
To understand SVG scaling, study css-tricks.com/scale-svg. The easiest approach is to make sure your SVG has a viewBox attribute and is embedded within an <img>.
To embed HTML into SVG use foreignObject as shown in stackoverflow.com/questions/4176146/svg-based-text-input-field and jsfiddle.net/leaverou/qwg3r.

Here is a simple example of embedding an external SVG into an <img>:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang=en>
<head>
    <meta charset=UTF-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <title>SVG example 1a</title>
    <style>
        img {
            width: 20%
        }
    </style>
</head>
<body>
    <img src=../../WMOTULabv1/logo.svg>
</body>
</html>
If you want to modify an embedded external SVG, see dahlström.net/svg/html/get-embedded-svg-document-script.html.
Study this excellent tutorial.
4.4.1.1.2. Responsive CSS
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>SVG2</title>
    <style>
      header svg text {
        font-weight: 900; font-size: 3em; fill: blue;
      }
    </style>
  </head>
  <body>
    <header>
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 100">
        <text x="0" y="40">I'm really responsive!</text>
      </svg>
    </header>
  </body>
</html>
The syntax <foo/> opens and immediately closes the foo element if it is a MathML or SVG element (i.e. not an HTML element). Attributes are tokenized the same way they are tokenized in HTML, so you can omit quotes in the same situations where you can omit quotes in HTML (i.e. when the attribute value is not the empty string and does not contain whitespace, ", ', `, <, =, or >).
The two above features do not combine well due to the reuse of legacy-compatible HTML tokenization. If you omit quotes on the last attribute value, you must have a space before the closing slash. <circle fill=green /> is OK but <circle fill=red/> is not.
4.4.1.2. RSS
RSS is a Web content syndication format. Its name is an acronym for Really Simple Syndication. RSS is a dialect of XML. All RSS files must conform to the XML 1.0 specification, as published on the World Wide Web Consortium (W3C) website.

www.w3schools.com/xml/xml_rss.asp provides a good introduction.

Open a RSS feed, for instance www.ghacks.net/feed, in your browser and look at the page source.

At the top level, we have an rss element with a mandatory version attribute. The latest version is 2.0. By simply checking for the presence of this element, we can easily determine whether it’s a RSS feed or not.

The top level element contains a channel element. The channel element must contain the following elements:

  • title

  • link

  • description

It can contain a large number of optional elements, which are listed and explained on the specification page shown above. The most important optional element for our purposes is item.

www.w3schools.com/php/php_ajax_rss_reader.asp shows a sample RSS reader using PHP and AJAX.

4.4.1.3. Atom

The official standard specification can be found at tools.ietf.org/html/rfc4287.

Take a look at a sample Atom feed.

The main element in an atom feed is named feed. By simply checking for the presence of this element, we can easily determine whether it’s an Atom feed or not.

The top level element contains zero or more entry elements. The entry element usually contains at least the following elements:

  • title

  • link

  • content

4.4.1.4. OPML

Outline Processor Markup Language or OPML is a format for storing outlines in XML 1.0 and to exchange information between outliners and Internet services that can be browsed or controlled through an outliner. The specification can be found at dev.opml.org/spec2.html.

4.4.1.5. MathML

www.w3.org/Math The formal specification can be found at www.w3.org/TR/MathML2. The Mozilla Developer Network (MDN) at developer.mozilla.org/en-US/docs/Web/MathML is a good starting point for MathML, in particular the element reference (developer.mozilla.org/en-US/docs/Web/MathML/Element). A nice text to MathML converter can be found at www.mathmlcentral.com/Tools/ToMathML.jsp.

Here’s a very basic MathML example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>MathML example</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <math>
        <mrow>
          <mi>cos</mi>
          <mo>(</mo>
          <msup>
            <mi>x</mi>
            <mn>3</mn>
          </msup>
          <mo>)</mo>
        </mrow>
        <mfrac bevelled="true">
          <mfrac>
            <mi> a</mi>
            <mi> b</mi>
          </mfrac>
          <mfrac>
            <mi> c</mi>
            <mi> d</mi>
          </mfrac>
        </mfrac>
      </math>
    </main>
  </body>
</html>

4.5. Web Application Programming Interfaces

See en.wikipedia.org/wiki/Application_programming_interface#Web_APIs for a detailed explanation of the term.

Here is an open-source API for generating random user data: randomuser.me.

Here is a noncomprehensive list of Web APIs:

docs.api.tfl.lu

data.public.lu

www.programmableweb.com/apis/directory

github.com/toddmotto/public-apis

gearside.com/public-json-feeds

any-api.com

shkspr.mobi/blog/2014/04/wanted-simple-apis-without-authentication

fixer.io

www.faroo.com/hp/api/api.html

speckyboy.com/18-free-mobile-apis-developers-should-consider

4.5.1. Facebook

Go to developers.facebook.com and register as a Facebook developer. Then create a FB app id for your application.

4.5.2. Yahoo Query Language (YQL)

This service allows us to access Internet data with SQL-like commands (cf. developer.yahoo.com/yql).

4.5.3. Finance

4.5.3.1. Quandl

Quandl (www.quandl.com) is a data platform covering over 10 million datasets from 500 sources accessible via a simple API (docs.quandl.com).

Here is an example illustrating the retrieval of stock data from Quandl and candlestick charting with Google Charts:

quandl1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<!DOCTYPE html>
<html lang=en>
<head>
  <title>Using Quandl and Google Charts</title>
  <meta charset=utf-8>
  <script src=https://www.google.com/jsapi></script>
  <script type=module>
    let result
    google.load('visualization', '1.0', {'packages': ['corechart']})

    const drawChart = () => {
      /* From Quandle we get open, high, low, close.
      GoogleCharts requires low, open, close, high.
      We therefore need to rearrange the data.
      */
      const dat = result.dataset.data.reverse()
      // We need the oldest data first (left to right).
      let low, open, close, high
      for (let i = 0; i < dat.length; i++) {
        low = dat[i][3]
        open = dat[i][1]
        close = dat[i][4]
        high = dat[i][2]
        dat[i].pop()
        dat[i][1] = low
        dat[i][2] = open
        dat[i][3] = close
        dat[i][4] = high
      }
      const data = google.visualization.arrayToDataTable(dat.slice(-20), true)

      const options = {
        legend: 'none',
        /*height: 600,
        width: 1000,*/
        hAxis: {slantedTextAngle: 90}/*,
                chartArea: {left: 50, top: 20, width: 950, height: 400}*/
      }

      const chart = new
      google.visualization.CandlestickChart(document.querySelector('main'))
      chart.draw(data, options)
    }

    const URL = "https://www.quandl.com/api/v3/datasets/GOOG/NASDAQ_MSFT.json"
    const req = new XMLHttpRequest()
    req.open('GET', URL)
    req.addEventListener('load', e => {
      result = JSON.parse(e.target.response)
      console.log(result)
      drawChart()
    })
    req.send()
  </script>
</head>
<body>
  <main>
  </main>
</body>
</html>
4.5.3.2. Federal Reserve Bank of St. Louis

FRED is the place for economic research on the US (research.stlouisfed.org). The FRED API (api.stlouisfed.org/docs/fred) allows us programmatic access to the whole DB.

FRED1
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>Federal Reserve Bank of St. Louis JSON data retrieval</title>
    <style>
      html, body {
        width: 100%;
        height: 100%;
        margin: 0;
        padding: 0;
        overflow: hidden;
      }

      body {
        display: flex;
        flex-direction: column;
        flex:auto;
      }

      nav {
        background-color: lightgrey;
        padding: 5px;
      }

      main {
        overflow: auto;
        flex: auto;
      }

      footer {
        min-height: 18px;
        text-align: center;
        background-color: lightgrey;
        font-size: 50%;
      }
      table {
        border: 1px solid black;
        border-collapse: collapse;
      }

      th, td {
        border: 1px solid black;
        padding: 3px;
      }
    </style>
    <script type=module>
      const AJAXFunctionCall = (functionName, parameter = '', callback) => {
        const req = new XMLHttpRequest()
        const data = new FormData()
        data.append('function', functionName)
        data.append('parameter', parameter)
        req.open('POST', 'FRED_functions.php')
        if (callback) req.addEventListener('load', callback)
        req.send(data)
      }

      const displayReleases = e => {
        const releases = JSON.parse(e.target.response).releases
        const select = document.querySelector('select')
        for (let i = 0; i < releases.length; i++) {
          const opt = document.createElement('option')
          opt.value = releases[i].id
          opt.innerHTML = releases[i].name
          select.appendChild(opt)
        }
        AJAXFunctionCall('get_series', select.value, displaySeries)
      }

      const displaySeries = e => {
        const series = JSON.parse(e.target.response).seriess
        const select = document.querySelectorAll('select')[1]
        select.innerHTML = ''
        for (let i = 0; i < series.length; i++) {
          const opt = document.createElement('option')
          opt.value = series[i].id
          opt.innerHTML = series[i].title
          select.appendChild(opt)
        }
      }

      const displayObservations = e => {
        const obs = JSON.parse(e.target.response).observations
        const table = document.createElement('table')
        let s = '<table><tr><th>Date</th><th>Value</th></tr><tr>'
        for (let i = 0; i < obs.length; i++) {
          s += `<tr><td>${obs[i].date}</td><td>${obs[i].value}</td></tr>`
        }
        table.innerHTML = `${s}</table>`
        const oldTable = document.querySelector('table')
        if (oldTable) document.querySelector('main').replaceChild(table, oldTable)
        else document.querySelector('main').appendChild(table)
      }

      const releaseChange = e => {
        AJAXFunctionCall('get_series', document.querySelector('select').value,
          displaySeries)
      }

      const seriesChange = e => {
        AJAXFunctionCall('get_observations', document.querySelectorAll('select')[1].value,
          displayObservations)
      }

      const init = () => {
        AJAXFunctionCall('get_releases', '', displayReleases)
        document.querySelector('select').addEventListener('change', releaseChange)
        document.querySelectorAll('select')[1].addEventListener('change', seriesChange)
      }

      init()
    </script>
  </head>
  <body>
    <nav>
      <select></select>
      <select></select>
    </nav>
    <main></main>
    <footer>This product uses the FRED® API but is not endorsed or certified by the Federal
      Reserve Bank of St. Louis.</footer>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
  require_once('key.php');

  function get_releases() {
    global $key;
    $URL = 'https://api.stlouisfed.org/fred/releases?api_key=' . $key . '&file_type=json';
    echo file_get_contents($URL);
  }

  function get_series($release_id) {
    global $key;
    $URL = 'https://api.stlouisfed.org/fred/release/series?api_key=' . $key .
      '&file_type=json&release_id=' . $release_id;
    echo file_get_contents($URL);
  }

  function get_observations($series_id) {
    global $key;
    $URL = 'https://api.stlouisfed.org/fred/series/observations?api_key=' . $key .
      '&file_type=json&series_id=' . $series_id;
    echo file_get_contents($URL);
  }

  if (isset($_POST['function']))
    if ($_POST['function'] === 'get_releases') get_releases();
    elseif ($_POST['function'] === 'get_series' && isset($_POST['parameter']))
      get_series($_POST['parameter']);
    elseif ($_POST['function'] === 'get_observations' && isset($_POST['parameter']))
      get_observations($_POST['parameter']);
?>
4.5.3.3. World Bank
4.5.3.4. Eurostat
4.5.3.5. Yahoo! Finance

This API allows us to download current and historical price and other information, charts and RSS news feeds for financial instruments.

4.5.3.6. IEX
IEX is a fair, simple and transparent stock exchange dedicated to investor protection.

Their API can be found at iextrading.com/developer.

4.5.4. Shodan

4.6. Security

Study CIA Vault 7!

www.eff.org

www.ghacks.net/2020/09/01/mozilla-there-is-a-high-probability-that-your-browsing-history-can-be-used-to-identify-you

globalprivacycontrol.org

4.6.2. Cross-Site Request Forgery (CSRF)

We need to send a token with our form:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
$token = password_hash(random_int(1, 999999999), PASSWORD_DEFAULT);
$_SESSION['token'] = $token;
?>

<form method=post>
  <input type=hidden name=token value=<?php echo $token; ?>>
</form>

<?php
  if (isset($_SESSION['token'], $_POST['token']) && $_POST['token'] == $_SESSION['token'])
    //valid
?>

www.acunetix.com/websitesecurity/csrf-attacks

www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet

stackoverflow.com/questions/20504846/why-is-it-common-to-put-csrf-prevention-tokens-in-cookies

github.com/pillarjs/understanding-csrf

stackoverflow.com/questions/20504846/why-is-it-common-to-put-csrf-prevention-tokens-in-cookies

4.6.3. Fiddler

FiddlerSyntaxView1

4.7. Mobile and desktop application development

codeburst.io/react-native-vs-nativescript-vs-xamarin-vs-flutter-vs-ionic-vs-cordova-vs-pwas-196810954450

flutter.dev

4.7.1. Mobile

Here is a good article on how to solve the hover problem on mobile devices: www.javascriptkit.com/dhtmltutors/sticky-hover-issue-solutions.shtml

dzone.com/refcardz/html5-mobile-development

en.wikipedia.org/wiki/Mobile_application_development

mobiledetect.net

4.7.1.1. Capacitor
4.7.1.3. NativeScript
4.7.1.4. MIT App Inventor 2
4.7.1.5. Titanium
4.7.1.5.1. Ubuntu
umask 0022
npm install cordova -g --user root
cordova create myApp
4.7.1.7. Framework 7
4.7.1.8. Sencha

4.7.2. Desktop

4.7.2.1. Electron
4.7.2.2. NW.js

4.8. Other

4.8.1. Accessibility

Study accessibility principles then automatically detect accessibility issues on web pages using open-indy.github.io/Koa11y.

4.8.2. Search engine and social network optimization, eCommerce

moz.com/beginners-guide-to-seo

www.willcoombe.com/on-page-optimisation

www.google.com/webmasters/tools/home?hl=en

search.google.com/test/mobile-friendly

en.wikipedia.org/wiki/Search_engine_optimization

varvy.com

www.quicksprout.com

www.makemyownwebpage.com/search-engine-optimization

ogp.me

www.outerboxdesign.com/search-marketing/search-engine-optimization/seo-trends-2018

schema.org

searchengineland.com

www.robotstxt.org/meta.html

www.sitemaps.org

potomacfund.com/seo-for-financial-advisors

You should markup your HTML with microdata, see www.keithcirkel.co.uk/schema-org-the-new-generation-of-seo.

4.8.2.1. Search engine optimization
  1. Crawling and indexing

    Search engines crawl and index links in web pages using robots called "crawlers" or "spiders" to reach the billions of interconnected documents on the web. They then store selected information from them in their databases.

  2. Providing answers

    When a user performs an online search, the engine scours its databases for this information and ranks the results according to their relevance and popularity (cf. moz.com/search-ranking-factors).

4.8.2.2. Know your customer

A key step in creating a successful eCommerce site is to analyse your market audience and create a list of the top keywords that are relevant for your business.

4.8.4. Performance

5. Server side programming

For a very high level overview, see roadmap.sh/backend.

5.1. Installing and configuring the tools

5.1.1. Introduction

Before we can develop our own web applications, we need access to a web server. The web server is the machine and software that delivers the web page to our users worldwide who are eager to use our apps:

Internet

Our web apps will consist of different parts. The content will be structured, or marked up, using HyperText Markup Language (HTML) and styled using Cascading Style Sheets (CSS). The behavior of our app will be programmed using JavaScript. All modern browsers understand HTML, CSS and JavaScript. In order to store data in a database, we use a web server that understands a programming language such as PHP or JavaScript. From PHP or JS it is easy to access a database, such as MySQL, which lives on the server. A relational database can be controlled using Structured Query Language (SQL).

It is essential to distinguish between code that is executed on the client side (HTML, CSS and JavaScript) and code that runs on the server side (for instance PHP, JavaScript or SQL). A browser understands JS but not PHP or SQL. This is not a problem as the server side code gets executed on the server and the results (HTML, CSS and JavaScript) are sent to the client browser for execution.

There are a number of different web servers available. A quick overview can be found at en.wikipedia.org/wiki/Comparison_of_web_server_software.

In order to develop your own web apps at home efficiently, you should install your own web server, either directly on your home PC or in a virtual machine. This will increase your understanding of the different parts that are involved in the web app life cycle. In order to execute a PHP script named, for instance, index.php in the main directory of your home web server, you need to run it in your browser using localhost/index.php. If you try to run it by double clicking on it in the file manager, your browser will access it using the file protocol, like so: /C:/Apache24/htdocs/index.php. Given that your browser does not understand PHP, this will not work. The PHP code needs to be processed by the PHP module. The processing needs to be triggered by the Apache web server using the HTTP protocol, which is the language that Apache speaks. The resulting output should be HTML (possibly including CSS and JavaScript), which is then sent by Apache to your browser.

You can download each component individually from httpd.apache.org, php.net and www.mysql.com. You then need to install and configure each component, which is a time-consuming and non-trivial task. A quicker and less error-prone approach is to download and install a preconfigured package that contains the Apache web server, PHP, MySQL and other useful tools ready to use. One such package is XAMPP, which is available for all the main operating systems.

5.1.2. Windows

5.1.2.1. Apache, PHP and MySQL/MariaDB

First we head over to the XAMPP home page and download and install the latest version. If you want to learn more about the idea behind XAMPP, this page is the best starting point. MariaDB is equivalent to MySQL for all intents and purposes. If you are interested in the differences, see mariadb.com/kb/en/the-mariadb-library/mariadb-vs-mysql-features.

Portable installation

If you want to have a portable web server on your USB stick or external hard drive that you can use anywhere, you can download a portable version of XAMPP. There are also portable alternatives to XAMPP.

Next, we start the XAMPP control panel:

Start XAMPP control panel

We make sure that Apache and MySQL are started:

Services started

Finally, we verify that the installation was successful. The status page confirms that everything’s running fine:

XAMPP status

For Windows users: in order to avoid having to run the XAMPP control panel after each system restart, we can install Apache and MySQL as a service so that they start automatically. First we need to open an elevated command prompt (i.e. run a command prompt as system administrator):

Run command prompt as admin

Depending on our user account control settings (type uac in the start menu) Windows will ask for confirmation:

Confirm

Now we need to execute the following commands:

Install Apache and MySQL as a service on Windows

To make sure that the services have been installed correctly, let’s check:

Open Windows services
Apache service is installed correctly
MySQL service is installed correctly

If we get an error message saying that another process is using port 80, we need to check which process this is and kill it. Open an elevated command prompt and run netstat -anb|more:

Who is listening on port 80?

The Apache web server, PHP and MySQL all have their own configuration files.

Apache

In the folder xampp\apache\conf we find the file httpd.conf. This is the main Apache configuration file. Open it in a text editor and take a look. For now we won’t change it.

In the folder xampp\php we find the file php.ini. This is the main PHP configuration file. Open it in a text editor and search for the setting short_open_tag. Read the explanation carefully and make sure that the setting is set to off. Next search for the setting error_reporting. Read the explanation carefully and make sure that the setting is set to E_ALL (and nothing else). Explain to someone else, why we want this particular setting.

In order to send emails from a Windows system, we could install a mail server. Here we’ll limit ourselves to installing a fake sendmail program that sends email via an SMTP server that we specify in php.ini. Download the zip file and unpack it for instance into C:\sendmail. In php.ini you need to set SMTP to point to an SMTP server, e.g. smtp.restena.lu. Then you need to specify smtp_port. The standard value is 25. Finally, you need to specify sendmail_path. If you have installed fake sendmail in C:\sendmail, the value would be "C:\sendmail\sendmail.exe -t". You also need to modify sendmail.ini by changing smtp_server to your SMTP server and smtp_ssl to none.

In order for the changes to take effect, we need to restart Apache. You can do this from the XAMPP Control Panel or Windows Services (run services.msc).

MySQL

In the folder mysql\bin we find a sample config file my.ini. We won’t change any MySQL config files manually.

At the moment, our MySQL installation is not secured, i.e. user root has no password. In order to avoid unauthorized access to our database, we set a root password as follows (using the command prompt):

cd \xampp\mysql\bin
mysql -u root
alter user 'root'@'localhost' identified with mysql_native_password by 'T2IF!secret';
exit
mysql -u root
mysql -u root -p
Set MySQL root password

Our database is still not secure, as we need to remove the empty user names, otherwise anyone can connect:

Remove users with no password
phpMyAdmin

The main config file is config.inc.php in the folder xampp\phpMyAdmin. Open it in a text editor and make the changes required to obtain the following settings:

/* Authentication type and info */
$cfg['Servers'][$i]['auth_type'] = 'cookie';
$cfg['Servers'][$i]['user'] = 'root';
$cfg['Servers'][$i]['password'] = '';
$cfg['Servers'][$i]['extension'] = 'mysqli';
$cfg['Servers'][$i]['AllowNoPassword'] = false;

Let’s take a look with localhost/phpmyadmin. Enter the MySQL root credentials. As you can see, phpMyAdmin provides a convenient user interface to manage our databases. You might find it more comfortable than the MySQL command line and you can access it from anywhere. If you get a 'The secret passphrase in configuration (blowfish_secret) is too short.' message you need to increase the size if the `$cfg['blowfish_secret']`string.

In order to be able to run the main XAMPP tools without having to specify the path or to enter the correct directory, we can add the required paths to the global PATH environment variable:

XAMPP19 XAMPP20 XAMPP21 XAMPP22 XAMPP23

Now you can run mysql or htpasswd from anywhere using the command prompt.

5.1.3. Ubuntu

5.1.3.1. VirtualBox

If you want to install Ubuntu with Apache in a virtual machine:

  1. Create a new VM and add a rule for NAT port forwarding setting the host and guest port to 22. You can leave the IP fields empty.

  2. Download Ubuntu Server from ubuntu.com/#download and install it.

  3. Enable root login via sudo passwd root.

  4. Enable port forwarding in VirtualBox for your virtual machine by opening ports 22, 80 and 443.

  5. Enable the firewall using ufw enable. Then add rules to allow SSH, HTTP and HTTPS connections via ufw allow 22/tcp, ufw allow 80/tcp and ufw allow 443/tcp.

  6. Install the SSH server using apt install openssh-server, then follow linuxconfig.org/allow-ssh-root-login-on-ubuntu-20-04-focal-fossa-linux.

  7. You should now be able to connect via SSH from your host to the server using ssh root@localhost.

  8. Run apt update&&apt upgrade&&apt autoremove. You might want to save this command in an executable script to make future updates easier. To do so run nano update, paste the command, press Ctrl+X and then Y. Now make the file exectuable using chmod u+x update.

  9. Run apt install apache2 apache2-doc mysql-server php-fpm php-mysql

  10. Then run a2enmod proxy_fcgi, a2enconf phpx.y-fpm (replace x.y by the PHP version) and systemctl restart apache2.

  11. If you get an AH00558: apache2: Could not reliably determine the server’s fully qualified domain name error, see www.digitalocean.com/community/tutorials/apache-configuration-error-ah00558-could-not-reliably-determine-the-server-s-fully-qualified-domain-name.

  12. Visit localhost with your browser. It should display the "Apache2 Ubuntu Default Page". At localhost/manual you can access the HTTP server documentation.

Check Apache status using /etc/init.d/apache2 status, service apache2 status and journalctl -xe.

5.1.3.2. Apache multi-processing modules (MPM)

Check which MPM is currently running using apachectl -V|grep -i mpm.

If you still have the prefork MPM running and would like to switch, for instance in order to use HTTP2, see www.linode.com/community/questions/17027/http2-and-apache-prefork-mpm. Note that PHP directives will need to be moved to PHP-FPM user configuration files as described in the linked document.

5.1.3.3. MySQL

If you want to install the latest version of MySQL you might need to add the latest repository to your system’s package sources list as described in dev.mysql.com/doc/mysql-apt-repo-quick-guide/en/#apt-repo-fresh-install and www.tecmint.com/install-mysql-8-in-ubuntu.

Improve MySQL security by running mysql_secure_installation. Remove all anonymous users as well as the test DB and disable remote root login. Then study www.acunetix.com/websitesecurity/securing-mysql-server-ubuntu-16-04-lts and www.digitalocean.com/community/tutorials/how-to-secure-mysql-and-mariadb-databases-in-a-linux-vps.
To disable MYSQL root login without password use ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'ANY_PASSWORD';.
To change the current user’s password, use SET PASSWORD = 'auth_string', cf. dev.mysql.com/doc/refman/8.0/en/set-password.html.
5.1.3.4. Advanced configuration

If you have your own domain, use Certbot to get a free certificate.

Use certbot --apache to create, certbot --apache --expand to expand and certbot renew to renew all certificates. Alternatively, for test purposes, you can create a self-signed certificate as shown in www.digitalocean.com/community/tutorials/how-to-create-a-self-signed-ssl-certificate-for-apache-in-ubuntu-20-04.

SSLProtocol All -SSLv2 -SSLv3
SSLHonorCipherOrder on
SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA !RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS"
a2enmod headers

Add the following line to default-ssl.conf in the <VirtualHost default:443> directive:

Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"

Make sure that the file headers.load is in the mods-enabled folder. If it isn’t, copy it from mods-available. Add your site to hstspreload.org if you intend to use HTTPS over the long term.

Test your server security.

To set umask permanently, add umask 0027 to /etc/profile or change the following in /etc/pam.d/common-session (cf. serverfault.com/questions/231717/how-to-get-full-control-of-umask-pam-permissions):

session optional pam_umask.so umask=0027

If you get the error apache2: Could not reliably determine the server’s fully qualified domain name, using 127.0.1.1 when restarting Apache, edit your /etc/hosts file and make sure it contains 127.0.0.1 localhost servername.domain.com servername (cf. source).

5.1.3.4.1. .htaccess

In order to be able to use .htaccess files:

  1. The Apache rewrite module needs to be enabled, if it isn’t already:

    a2enmod rewrite
    apache2ctl restart
  2. The AllowOverride All directive needs to be in your Apache config file (usually in /etc/apache2) for the directory tree where your access file is located. See httpd.apache.org/docs/2.4/howto/auth.html. If you are the webmaster you should use directives in the Apache configuration files instead of .htaccess files.

To disable directory listing, see www.simplified.guide/apache/disable-directory-listing.

5.1.3.6. Security
5.1.3.6.1. mod_security
ModSecurity is an open source, cross platform web application firewall (WAF) engine for Apache, IIS and Nginx.

www.modsecurity.org/documentation.html

5.1.3.6.2. mod_evasive
mod_evasive is an evasive maneuvers module for Apache to provide evasive action in the event of an HTTP DoS or DDoS attack or brute force attack.

blog.rapid7.com/2017/04/09/how-to-configure-modevasive-with-apache-on-ubuntu-linux

github.com/jzdziarski/mod_evasive

5.1.3.6.3. Fail2ban
Fail2Ban scans log files like /var/log/auth.log and bans IP addresses conducting too many failed login attempts. It does this by updating system firewall rules to reject new connections from those IP addresses, for a configurable amount of time.

linuxhandbook.com/fail2ban-basic

github.com/fail2ban/fail2ban

pipo.blog/articles/20210915-fail2ban-apache-dos

help.ubuntu.com/community/Fail2ban

Alternative to stopping fail2ban:

From the Linux command prompt type: service fail2ban stop

To start fail2ban: service fail2ban start

To reload fail2ban if you have a banned IP: service fail2ban restart Restarting will clear the ban.

To prevent fail2ban from banning IPs on the local network or other places: Modify /etc/fail2ban/jail.conf look for the line:

#ignoreip 127.0.0.1 192.168.1.24/24 …​.

uncomment it by removing the # and then change the IP addresses. To have fail2ban ignore network 192.168.20.0 (255.255.255.0), add 192.168.20.0/24 to the above line. You can add as many networks as you like. Just leave a space.

Just a note, if you have a VPN or a tunnel, you should add its network too. I’ve had the tunnel banned!

You can see if fail2ban has banned an IP by checking /var/log/fail2ban.log. It will indicate banned and unbanned IP addresses.

To unban an IP address, use fail2ban-client set YOURJAILNAMEHERE unbanip IPADDRESSHERE

The hard part is finding the right jail: Use iptables -L -n to find the rule name then use fail2ban-client status to get the actual jail names. The rule name and jail name may not be the same but it should be clear which one is related to which (source).

To analyse fail2ban log files, fail2ban-analyse can be useful. You need to add nolookup`to line 88 in `fail2ban_analyse_wrapper.sh in order to avoid the use of ipinfo.
5.1.3.9. Reverse proxy

Enable mod_proxy_wstunnel via a2enmod proxy_wstunnel. You might have to enable a few additional modules, see stackoverflow.com/questions/23931987/apache-proxy-no-protocol-handler-was-valid.

Insert the following into your virtual host config, replacing 9000 with your chosen port number:

ProxyRequests Off
ProxyPreserveHost On
SSLProxyEngine on
ProxyPass /wss/ wss://localhost:9000/
ProxyPassReverse /wss/ wss://localhost:9000/
ProxyPass / https://localhost:9000/
ProxyPassReverse / https://localhost:9000/

This will however prevent the web server from getting the correct client IP address, as the IP will be the one of the machine that the reverse proxy is running on. See 2bits.com/articles/correct-client-ip-address-reverse-proxy-or-content-delivery-network-cdn.html for solutions.

Or create a reverse proxy in Node as explained in onezerology.wordpress.com/2016/06/14/run-apache-with-node-js-reverse-proxy/.

The ! directive is useful in situations where you don’t want to reverse-proxy a subdirectory, see httpd.apache.org/docs/2.4/mod/mod_proxy.html.

5.1.3.10. Caching

To prevent the browser from caching a specific resource, we can append a ? followed by a random number as described in www.willmaster.com/library/web-content-prep/preventing-browser-cache.php.

For a more general explanation of web caching see www.mnot.net/cache_docs.

5.1.3.11. Analyze Apache logs

GoAccess is an open source real-time web log analyzer and interactive viewer that runs in a *nix terminal or in the browser.

Create a web accessible directory, e.g. /var/www/html/.goaccess.

Add the following to your site configuration file (e.g. /etc/apache2/sites-enabled/000-default-le-ssl.conf):

<Directory "var/www/html/.goaccess">
AuthType Basic
AuthName "Restricted Files"
AuthUserFile /var/www/html/.goaccess/.htpasswd
Require valid-user
</Directory>

Create a new .htpasswd file for user root in /var/www/html/.goaccess:

htpasswd -c .htpasswd root

As shown in goaccess.io/download#distro, run:

wget -O - https://deb.goaccess.io/gnugpg.key | gpg --dearmor | sudo tee /usr/share/keyrings/goaccess.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/goaccess.gpg] https://deb.goaccess.io/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/goaccess.list
apt update
apt install goaccess

Create /root/updateGoAccess.bat with this content (cf. goaccess.io/man#options):

zcat /var/log/apache2/access.log.*.gz| goaccess /var/log/apache2/access.log /var/log/apache2/access.log.1 -o /var/www/html/.goaccess/report.html --log-format=COMBINED --restore --persist -

Run crontab -e and add * 3 * * * /root/updateGoAccess.sh to run this script every day at 3am.

5.1.4. Database management tools

Adminer

A fast alternative to phpMyAdmin is Adminer. It is a single file and highly recommended.

You can easily install it like so:
apt install adminer
a2enmod adminer
apachectl restart
phpMyAdmin

phpMyAdmin is the traditional MySQL web management tool.

If phpMyAdmin does not work after you’ve upgraded the PHP version disable the old PHP version and enable the new one. For instance, after upgrading from PHP 7.1 to 7.2 use (cf. www.howtoforge.com/community/threads/phpmyadmin-missing-mysqli-extension.78307):

a2dismod php7.1
a2enmod php7.2
DbGate

Free, open source, cross database and cross platform DB client: dbgate.org

DBeaver

A great portable multi-platform DB tool is DBeaver.

5.2. PHP

5.2.1. Introduction

PHP is a widely-used open source general-purpose scripting language that is especially suited for web development and can be embedded into HTML.

To get an idea of how widely used PHP still is, see kinsta.com/blog/is-php-dead.

A PHP script can either work without output to the browser, or, in the usual case, send HTML, CSS and JavaScript to the browser. In any case, the client never gets to see the underlying PHP code.

What do we need PHP for in the context of web application development?

  1. To access a server database. For instance, if we want to build the next Amazon or YouTube, we need a database where we store the data of our gazillion users and products.

  2. To access data on the Internet. Remember that for security reasons the browser is severely restricted in terms of accessing content from other sites (same-origin policy (cf. Cross-origin requests)). In PHP, we have no limitations, unless our host provider has put some in place (which many of them do), in which case we switch to a host provider that does not or host our own server.

  3. To provide services that can be accessed from anywhere (see Web Application Programming Interfaces).

  4. To run code that we do not want the client to see and/or manipulate.

A good way to start learning PHP is to use www.phpschool.io. The latest version of PHP is version 8. To see what’s new read www.php.net/manual/en/migration80.php and for major changes introduced in version 7 see www.tutorialspoint.com/php7.

A PHP script has a file name ending with .php.

5.2.2. Hello world

1
2
3
<?php
  echo 'Hello world';
?>

echo is a PHP construct that outputs all parameters. This script is parsed by the Apache PHP module and sends the string Hello world to the browser. This is of course not a valid HTML5 page (run the validator), but still, the browser will display the text.

There’s also a short form of echo:

1
<?= 'Hello world'?>
Unlike JavaScript, PHP requires instructions to be terminated with a semicolon at the end of each statement (cf. www.php.net/manual/en/language.basic-syntax.instruction-separation.php).

If we want to create a valid HTML5 page, we can mix HTML and PHP:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Hello World</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <?php
      echo 'Hello world!';
    ?>
  </body>
</html>

Take a look at the source in your browser. What do you see? What happened to the PHP part? How do you explain your observation?

From now on, the HTML part will not be shown, unless it is relevant.

5.2.3. Logging

In your home directory on students.btsi.lu you have a directory named .log where the PHP interpreter saves errors generated during the execution of your PHP scripts in a file called error.log. It’s a good idea to monitor this file. To do this, log in to students.btsi.lu with Putty and run the command tail -f /www/<class>/<username>/.log/error.log.

Use the PHP function error_log to log an error message to the log file.

It’s a good idea to set the error level in your script to E_ALL (cf. php.net/manual/en/errorfunc.constants.php), irrespective of the default setting on the server. This can be done using the error_reporting function.

Let’s run an erroneous script and see what happens to our log file:

1
2
3
<?php
  eccho 'abc';
?> 

If you want a more comfortable way to monitor your PHP error log:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>PHP log monitor</title>
  <style>
    main {
      position: fixed;
      top: 40px;
      bottom: 0;
      left: 0;
      right: 0;
      overflow: auto;
    }
  </style>
  <script type=module>
    const displayLogFile = async () => {
      try {
        const logFile = await fetch('error.log')
        if (logFile && logFile.ok) {
          const main = document.querySelector('main')
          const text = await logFile.text()
          const lines = text.split('\n')
          main.innerHTML = ""
          for (const line of lines) main.innerHTML += line + '<br>'
          main.scrollTop = main.scrollHeight
        }
      } catch (err) {
        console.log(err)
      }
    }

    const deleteLogFile = async () => {
      try {
        await fetch('PHPLogMonitor.php')
        displayLogFile()
      } catch (err) {
        console.log(err)
      }
    }

    const main = async () => {
      try {
        await displayLogFile()
        addEventListener('focus', displayLogFile)
        document.querySelector('#reload').addEventListener('click', displayLogFile)
        document.querySelector('#delete').addEventListener('click', deleteLogFile)
      } catch (err) {
        console.log(err)
      }
    }

    main()
  </script>
</head>
<body>
  <header>
    <button id=reload>Reload</button>
    <button id=delete>Delete</button>
  </header>
  <main></main>
</body>
</html>
1
2
3
<?php
  fclose(fopen('error.log','w'));
?>

5.2.4. Variables and data types

1
2
3
4
5
6
7
<?php
  $name = 'Asterix';
  echo "My name is $name<br>";
  echo 'My name is $name<br>';
  unset($name);
  echo "My name is $name<br>";
?>

This is a simple example of a variable declaration and usage. A variable is used to temporarily store data under a given name.

Variable names in PHP are case-sensitive and always start with $ followed by a letter or an underscore, then any combination of letters, numbers and the underscore character (cf. php.net/manual/en/language.variables.basics.php).

unset is used to destroy a variable.

Strings are embedded in "" or '' in PHP. There is however a significant difference between the two. Can you see it in the example above?

PHP will not perform variable substitution inside single-quoted strings and won’t even replace most escape characters (except \). In double-quoted strings, PHP will replace variables with their current value.

Take a look at the output of the following script. What happened?

1
2
3
4
5
6
<?php
  $fruit = 'apple';
  echo "I've bought 5 $fruits.<br>";
  echo "I've bought 5 ${fruit}s.<br>";
  echo "I've bought 5 {$fruit}s.";
?>

By adding s we have changed the variable name and the PHP interpreter now looks for a variable named $fruits, which does not exist (line 3). We can use braces to tell PHP where the variable name ends (cf. lines 4 and 5).

PHP has nine data types: string, integer, float, boolean, array, object, callable, resource and NULL. PHP considers 0, 0.0, "", "0" and NULL to be equivalent to FALSE and everything else to be TRUE.
A variable’s type is determined by the context in which the variable is used. That is to say, if a string value is assigned to variable $var, $var becomes a string. If an integer value is then assigned to $var, it becomes an integer.

With settype we can change the type of a variable explicitly.

To check the type and value of an expression, use the var_dump function. To get a human-readable representation of a type for debugging, use the gettype function. To check for a certain type use is_int, is_array etc. functions.

5.2.4.1. String concatenation

Strings can be concatenated using the . operator.

1
2
3
4
5
6
7
<?php
  $phrase = "The glass is half full";
  echo 'Quote of the day: "' . $phrase . '"<br>';

  // Alternatively, we can escape the "".
  echo "Quote of the day: \"$phrase\"";
?>

5.2.5. Heredoc

To deal with long strings, we can use heredoc syntax. It allows us to define our own string delimiter so that we do not have to use quotes. The string delimiter, EOT in our example, needs to be in the first column on a line by itself. Spaces or tabs are not allowed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
  $string = "Hello world!";
  echo <<<EOT
    <!DOCTYPE html>
    <html lang=en>
      <head>
        <title>$string</title>
        <meta charset=UTF-8>
      </head>
      <body>
        $string
      </body>
    </html>
EOT;
?>

However, the same can be achieved without the heredoc hassle given that in PHP strings can span multiple lines. Here is the same example without heredoc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
  $string = "Hello world!";
  echo "<!DOCTYPE html>
    <html lang=en>
      <head>
        <title>$string</title>
        <meta charset=UTF-8>
      </head>
      <body>
        $string
      </body>
    </html>";
?>

5.2.6. Constants

Constants are like variables, except that once they are defined, they cannot be undefined or changed. Their name does not start with a $. A constant is case-sensitive by default. By convention, constant identifiers are always uppercase. They are defined using const or the define function:

1
2
3
4
5
6
7
<?php
  const SECONDSPERDAY = 86400;
  define("DAYOFTHEWEEK", 'Sunday');
  echo SecondsPerDay . '<br>';
  echo SECONDSPERDAY . '<br>';
  echo DAYOFTHEWEEK;
?>

The define() function can be given a third parameter to turn off case-sensitivity:

1
2
3
4
<?php
  define("SECONDSPERDAY", 86400, true);
  echo SecondsPerDay;
?>

Constants are automatically global across the entire script, unlike variables.

5.2.7. Comments

# or // tell PHP to ignore the rest of the line. For multi line comments, use /* */.

1
2
3
4
5
6
7
8
<?php
  echo 'Comments are useful'; // This is a comment.
  # This is a comment too.
  /* This is a comment
  that stretches over
  several lines.
  */
?>

If we want to enable automatic documentation generation, we can use PHPDoc.

5.2.8. Operators

Detailed information and examples related to operators can be found at php.net/manual/en/language.operators.php and www.w3schools.com/php/php_operators.asp.

Pay particular attention to the equal operator.

A single = is used to assign the right value to the left operand.

== and === are comparison operators. The former may perform an implicit type conversion whereas the latter will not.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
  var_dump($x); // $x is not defined
  echo '<br>';
  $x = 24; // Assignment -> variable $x now has the value 24
  var_dump($x);
  echo '<br>';
  if ("2" === 2) echo '"2" === 2'; // Check type and value without implicit conversion.
  else echo '"2" !== 2';
  echo '<br>';
  if ("2" == 2) echo '"2" == 2'; // Check value, if necessary after implicit type conversion.
  else echo '"2" != 2';
?>

When combining several operators, it is helpful to know their precedence.

5.2.8.1. @ operator

If we put this operator in front of an expression in PHP, any error messages that might be generated by that expression will be ignored (cf. php.net/manual/en/language.operators.errorcontrol.php).

5.2.9. Conditional statements

5.2.9.1. if else

We can take decisions based on conditions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
  $password = "abc";
  if ($password === "secret") echo "You may proceed!<br>";
  else echo "No entry!<br>";

  $mark = rand(-100, 100); // Generate a random number from [-100, 100].
  echo "Mark: $mark<br>";
  if ($mark < 1 || $mark > 60) echo "Something went wrong!"; // || -> logical or
  else if ($mark === 60) echo "Excellent!!";
  else if ($mark >= 50) echo "Very good!";
  else if ($mark >= 40) echo "Good";
  else if ($mark >= 30) echo "Pass";
  else if ($mark >= 20) echo "Fail";
  else if ($mark >= 10) echo "Bad";
  else echo "Very bad!";
?>
5.2.9.2. Ternary operator

Like JavaScript, PHP provides a shortcut version of if else:

1
2
3
4
5
6
7
<?php
  echo (time() % 2 === 0 ? "Even" : "Uneven") . " number of seconds.";

  // The previous line is functionally equivalent to the following:
  if (time() % 2 === 0) echo "Even number of seconds.";
  else echo "Uneven number of seconds.";
?>

5.2.10. Loops

5.2.10.1. for

The for loop consists of three parts: the declaration, the condition and the action. The declaration sets the variable $i to 1. The condition checks whether $i is less than 10. As long as the condition is true, we increment $i by 1 after every iteration.

1
2
3
4
<?php
  for ($i = 1; $i < 10; $i++) echo "Number $i<br>";
  echo "Variable $i now has the value $i.";
?>
5.2.10.2. while
We need to verify that a loop terminates, i.e. that there will be a time, where the condition won’t be true anymore. Otherwise, we end up with an infinite loop, meaning our program will never terminate!
1
2
3
4
5
6
7
8
9
<?php
  $num1 = rand(1, 100); # Generate a random number from [1, 100].
  $num2 = rand(1, 100); # Generate a random number from [1, 100].
  while ($num1 < 90 && $num2 > 10) { # Loop as long as the condition is true.
    echo "Life goes on<br>";
    $num1 = rand(1, 100); # Generate a random number from [1, 100].
    $num2 = rand(1, 100); # Generate a random number from [1, 100].
  }
?>
5.2.10.3. break and continue

break exits the loop immediately. continue skips the rest of the current iteration and goes on to the next.

1
2
3
4
5
6
7
<?php
  for ($i = 1; $i < 10; $i++) {
    if ($i % 2 === 0) continue;
    if ($i > 8) break;
    echo "$i ";
  }
?>
5.2.10.4. Nested loops
1
2
3
4
5
6
<?php
  for ($i = 1; $i < 10; $i++) {
    for ($j = 1; $j < 10; $j++) echo "$i ";
    echo "<br>";
  }
?>

5.2.11. Arrays

An array in PHP is a container. You can use it to store values and/or variables inside, which are called elements. Each element has:

  1. a key, which can be an integer or a string.

  2. a value, which can be any data type or a variable.

We can create an array using the array function or using the [] operator. We can also add elements to an array using [].

The function print_r is very helpful to visualize the contents of an array. To optimize the readability of the output, we can set the function’s second parameter to TRUE and embed it in a pre tag.

1
2
3
4
5
6
<?php
  $car_brands = ["Audi", "BMW", "Renault", "Nissan"]; // Create an array with 4 elements.
  echo '<pre>' . print_r($car_brands, true) . '</pre>';
  $car_brands[] = "Hyundai";
  echo '<pre>' . print_r($car_brands, true) . '</pre>';
?>

The function count (or sizeof, which is just an alias of count) tells us the number of elements in an array.

in_array returns TRUE if a given element is in the array, FALSE otherwise. Note that if we set the third parameter to TRUE, it will check both the type and value of the element.

The elements of an array variable are variables too, so we can delete them using unset. However, this will create an empty space in our array, although the length will be correct. To completely remove an array element, we can use array_splice.

If we want to assign the elements of an array to individual variables, the list command comes in handy.

PHP standard operators can be used with arrays as explained at secure.php.net/manual/en/language.operators.array.php.

PHP offers a number of useful functions to change the internal pointer of an array using reset, current, each, end, next and prev. See php.net/manual/en/function.reset.php for usage examples.

To get the full list of PHP array functions, see php.net/manual/en/ref.array.php.

Take your time to study the following examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
  $car_brands = ["Audi", "BMW", "Renault", "Nissan"]; // Create an array with 4 elements.
  echo "Number of array elements: " . count($car_brands) . ".<br>";
  echo '<pre>' . print_r($car_brands, true) . '</pre>';

  if (in_array("Audi", $car_brands)) echo 'Audi is part of our brands.';
  else echo 'Audi is not part of our brands.';
  echo "<br>";

  if (in_array("Mercedes", $car_brands)) echo 'Mercedes is part of our brands.';
  else echo 'Mercedes is not part of our brands.';
  echo "<br>";

  $car_brands[] = "Hyundai";
  echo "Number of array elements: " . count($car_brands) . ".<br>";
  echo '<pre>' . print_r($car_brands, true) . '</pre>';

  unset($car_brands[3]); // Delete the fourth element in the array.
  echo "Number of array elements: " . count($car_brands) . ".<br>";
  echo '<pre>' . print_r($car_brands, true) . '</pre>';

  $car_brands[3] = "Nissan";
  echo $car_brands[3] . ' ** ' . $car_brands[4] . '<br>';
  echo '<pre>' . print_r($car_brands, true) . '</pre>';

  array_splice($car_brands, 3, 1);
  echo "Number of array elements: " . count($car_brands) . ".<br>";
  echo '<pre>' . print_r($car_brands, true) . '</pre>';

  $car_brands[] = "VW";
  echo "Number of array elements: " . count($car_brands) . ".<br>";
  echo '<pre>' . print_r($car_brands, true) . '</pre>';
  echo "The third brand is $car_brands[2].<br>";

  $schools[] = "LAM";
  echo '<pre>' . print_r($schools, true) . '</pre>';

  $numbers1 = array(1, 2, 3, 4);
  if (in_array('2', $numbers1, TRUE)) echo "'2' is in " . '$numbers1';
  else echo "'2' is not in " . '$numbers1';
  echo '<br>';
  if (in_array('2', $numbers1)) echo "'2' is in " . '$numbers1';
  else echo "'2' is not in " . '$numbers1';
  echo '<br>';

  $numbers2 = [2, 4, 8, 3, 1, 5, 6, 7];
    echo '<pre>' . print_r($numbers1 + $numbers2, true) . '</pre>';

  list($n1, $n2, $n3, $n4) = $numbers1;
  echo "<br>The result of the magic formula is " . ($n1 + $n2 * $n3 / $n4);
?>
5.2.11.1. Associative arrays

In addition to specifying array values, we can also specify our own keys as well as mix own keys with automatically generated ones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
  $car_brands = ["a" => "Audi", "b" => "BMW", "n" => "Nissan", "r" => "Renault"];
  echo '<pre>' . print_r($car_brands, true) . '</pre>';
  echo "I like {$car_brands['b']}.";
  $car_brands['v'] = 'VW';
  echo '<pre>' . print_r($car_brands, true) . '</pre>';
  $car_brands[] = 'Tesla';
  echo '<pre>' . print_r($car_brands, true) . '</pre>';
  unset($car_brands["n"]);
  echo '<pre>' . print_r($car_brands, true) . '</pre>';
  echo count($car_brands);
?>
5.2.11.2. Looping through arrays with foreach

foreach allows us to iterate through an array or object (we’ll get to objects later on):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
  $car_brands = ["a" => "Audi", "b" => "BMW", "n" => "Nissan", "r" => "Renault"];
  foreach ($car_brands as $brand) echo "$brand<br>"; // Ignore the keys and use only values.
  foreach ($car_brands as $key => $val) echo "$key = $val<br>"; // Here we use both.
  #We can also use foreach with a standard array.
  $arr = [random_int(1, 100), random_int(1, 100), random_int(1, 100), random_int(1, 100)];
  $sum = 0;
  foreach ($arr as $elem) $sum += $elem;
  echo "The sum of all array elements is $sum.<br>";
  foreach ($arr as $key => $val) echo "$key = $val<br>";
?>

Of course, we can use a standard for loop to do exactly the same:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
  $car_brands = ["a" => "Audi", "b" => "BMW", "n" => "Nissan", "r" => "Renault"];
  var_dump(array_keys($car_brands));
  for ($i = 0; $i < count($car_brands); $i++)
    echo "{$car_brands[array_keys($car_brands)[$i]]}<br>";
  for ($i = 0; $i < count($car_brands); $i++) {
    $key = array_keys($car_brands)[$i];
    echo "$key = {$car_brands[$key]}<br>";
  }

  $arr = [random_int(1, 100), random_int(1, 100), random_int(1, 100), random_int(1, 100)];
  $sum = 0;
  for ($i = 0; $i < count($arr); $i++) $sum += $arr[$i];
  echo "The sum of all array elements is $sum.<br>";
  for ($i = 0; $i < count($arr); $i++) {
    $key = array_keys($arr)[$i];
    echo "$key = {$arr[$key]}<br>";
  }
?>
5.2.11.3. Multidimensional arrays

Array elements can be arrays, so we can create multidimensional arrays.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
  // Two-dimensional array
  $matrix = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]];
  echo '<pre>' . print_r($matrix, true) . '</pre>';
  echo '<table border=1>';
  foreach ($matrix as $row) {
    echo '<tr>';
    foreach ($row as $elem) echo "<td>$elem</td>";
    echo '</tr>';
  }
  echo '</table>';

  // Three-dimensional mixed associative/non-associative array
  $grades = ["WSERS1" => ["Asterix" => [56, 55, 57], "Obelix" => [34, 24, 41]],
             "WSERS2" => ["Asterix" => [58, 59, 60], "Obelix" => [39, 43, 48]]];
  echo '<pre>' . print_r($grades, true) . '</pre>';
  echo '<pre>' . print_r($grades['WSERS1'], true) . '</pre>';
  echo '<pre>' . print_r($grades['WSERS1']['Asterix'], true) . '</pre>';
  echo '<pre>' . print_r($grades['WSERS1']['Asterix'][0], true) . '</pre>';
  foreach ($grades as $subject => $students) {
    echo "Subject: $subject<br>";
    echo '<pre>' . print_r($students, true) . '</pre>';
    foreach ($students as $student => $student_grades) {
      echo "$student's grades:";
      foreach ($student_grades as $grade) echo " $grade";
      echo "<br>";
    }
  }
?>

5.2.12. Functions

Functions are a key element of effective code reuse and maintainable software development. You may not redefine PHP’s built-in functions.

5.2.12.1. Type declarations
Type declarations allow functions to require that parameters are of a certain type at call time. If the given value is of the incorrect type, then an error is generated. To specify a type declaration, the type name should be added before the parameter name. The declaration can be made to accept NULL values if the default value of the parameter is set to NULL.

Type declarations are not compulsory, but they help to clarify the purpose of your functions and to avoid mistakes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
  function factorial(int $n): void {
    $res = 1;
    for ($i = $n; $i > 1; $i--) $res *= $i;
    echo "$n! -> $res<br>";
  }

  try {
    factorial(5);
    factorial(-5);
    factorial(50);
    factorial('asd');
  } catch (Error $e) {
    echo 'Caught error: ', $e->getMessage();
  }
?>

A function can return a value using the return keyword.

The return statement terminates the function meaning any following instructions will never get executed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
  function factorial(int $n): float {
    $res = 1;
    for ($i = $n; $i > 1; $i--) $res *= $i;
    return $res;
  }

  try {
    echo "5! -> " . factorial(5) . "<br>";
    echo "-5! -> " . factorial(-5) . "<br>";
    echo "50! -> " . factorial(50);
  } catch (Error $e) {
    echo 'Caught error: ', $e->getMessage();
  }
?>

A function can of course also return HTML. Let’s create a function that returns an HTML table filled with the data from a one- or two-dimensional array:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Function example</title>
    <meta charset=UTF-8>
    <style>
      table {
        border: 1px solid black;
        border-collapse: collapse;
      }

      td {
        border: 1px solid black;
        border-collapse: collapse;
        padding: 5px;
      }
    </style>
  </head>
  <body>
    <?php
      // Can handle 1- and 2-dimensional arrays.
      function get_table(array $data): string {
        $output = '<table>';
        foreach ($data as $row) {
          $output .= "<tr>";
          if (is_array($row))
            foreach ($row as $cell) $output .= "<td>$cell</td>";
          else $output .= "<td>$row</td>";
          $output .= "</tr>";
        }
        $output .= '</table>';
        return $output;
      }

      try {
        echo 'Table 1<br>';
        echo get_table([[1, 2, 3], [1, 4, 'a']]);
        echo 'Table 2<br>';
        echo get_table([1, 2, 3, [1, 4, 'a']]);
      } catch (Error $e) {
        echo 'Caught error: ', $e->getMessage(), "\n";
      }
    ?>
  </body>
</html>
5.2.12.2. Variable scope
5.2.12.2.1. global

Variables declared outside of functions and classes are global, i.e. they are available everywhere in the script except within functions and classes. Inside functions we can use the global keyword to access global variables.

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
  $name = "Asterix";

  function test(): void {
    echo $name;
  }

  echo $name . "<br>";
  test(); // This produces an error, as $name is global and not visible inside function test.
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
  $name = "Asterix";

  function test(): void {
    global $name;
    $name .= " and Obelix<br>";
    echo $name;
  }

  echo $name . "<br>";
  test(); // This is OK.
  echo $name . "<br>";
?>
5.2.12.2.2. static

A static variable exists only in a local function scope, but it does not lose its value when program execution leaves this scope.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
  function test(): void {
    static $x = 0;
    echo $x++ . '<br>';
  }

  test();
  test();
  test();
?>
5.2.12.3. Default values for function parameters

We can specify default values for parameters in case the caller does not pass them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
  function createParagraph(string $text, bool $strong = false): string {
    $s1 = $strong ? "<strong>" : "";
    $s2 = $strong ? "</strong>" : "";
    return "<p>$s1$text$s2</p>";
  }

  try {
    echo createParagraph("This is normal text.");
    echo createParagraph("This is important text.", true);
  } catch(Error $e) {
    echo 'Caught error: ', $e->getMessage(), "\n";
  }
?>
When using default arguments, any defaults should be on the right side of any non-default arguments.
5.2.12.4. Passing values by reference

If we want a function to be able to change the value of a parameter, so that the changed value is available to the caller, we pass the parameter by reference by prepending a & to the parameter name in the function prototype:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
  function double(int &$int) {
    $int *= 2;
  }

  $x = 7;
  echo $x;
  double($x);
  echo $x;

  function doubleArray(array &$arr) {
    for ($i = 0; $i < count($arr); $i++) $arr[$i] *= 2;
  }

  try {
    $arr = array(1, 2, 3);
    echo '<pre>' . print_r($arr, true) . '</pre>';
    doubleArray($arr);
    echo '<pre>' . print_r($arr, true) . '</pre>';
  } catch(Error $e) {
    echo 'Caught error: ', $e->getMessage(), "\n";
  }
?>
5.2.12.5. Using a variable number of parameters
argument lists may include the …​ token to denote that the function accepts a variable number of arguments. The arguments will be passed into the given variable as an array

We can also write functions that accept a variable number of parameters using func_num_args, func_get_args or func_get_arg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
  function test1(...$args) {
    echo "Number of arguments: " . $num = count($args) . "<br>";
    for ($i = 0; $i < $num; $i++) echo "Argument $i: " . $args[$i] . "<br>";
    echo '<pre>' . print_r($args, true) . '</pre>';
  }

  function test2() {
    echo "Number of arguments: " . $num = func_num_args() . "<br>";
    for ($i = 0; $i < $num; $i++) echo "Argument $i: " . func_get_arg($i) . "<br>";
    echo '<pre>' . print_r(func_get_args(), true) . '</pre>';
  }

  test1();
  test1(1, "abc", 5.67);
  test2();
  test2(1, "abc", 5.67);
?>
5.2.12.6. Anonymous functions

Anonymous functions, also known as closures, can be used for callbacks and other purposes.

1
2
3
4
5
6
7
8
<?php
  $numbers = [7, 5, 1, 3, 4, 8, 6, 2];
  usort($numbers, function ($x, $y) {
    if ($x < $y) return -1;
    else return 1;
  });
  echo '<pre>' . print_r($numbers, true) . '</pre>';
?>
5.2.12.7. Recursive functions

The factorial problem can be solved very elegantly using a recursive function call:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
  function factorial(int $n): float {
    if ($n <= 0) return 1;
    return $n * factorial($n - 1);
  }

  try {
    echo "5! -> " . factorial(5) . "<br>";
    echo "-5! -> " . factorial(-5) . "<br>";
    echo "50! -> " . factorial(50) . "<br>";
  } catch(Error $e) {
    echo 'Caught error: ', $e->getMessage(), "\n";
  }
?>

5.2.13. Including external scripts

For major projects PHP files can become very large and unwieldy. In these cases, splitting large files into separate entities and including them in the main script can render our project much more manageable. Let’s look at an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>Include demo 1</title>
  </head>
  <body>
    <?php
      include 'includeheader.html';
      include 'includemain.html';
      include 'includefooter.html';
    ?>
  </body>
</html>
1
2
3
<header>
  <h1>Include demo 1</h1>
</header>
1
2
3
<footer>
  &copy; 2017 LAM T2IF1
</footer>

PHP provides four keywords to include external scripts:

include

include_once

require

require_once

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
  echo "Let's include...<br>";
  include 'includeme.php';
  include 'includeme.php';
  echo "Let's include_once...<br>";
  include_once 'includeme.php';
  echo "Let's require...<br>";
  require 'includeme.php';
  require 'includeme.php';
  echo "Let's require_once...<br>";
  require_once 'includeme.php';
?>
1
2
3
<?php
  echo "I've been included!<br>";
?>

What is the difference between include and require? Let’s take a look:

1
2
3
4
5
6
7
<?php
  echo 'Check 1<br>';
  include_once 'include3mega.php';
  echo 'Check 2<br>';
  require_once 'include3mega.php';
  echo 'Check 3';
?>

5.2.14. Superglobals

There are nine superglobal arrays that we can use anywhere in our scripts.

5.2.14.1. $_GET

This array contains all variables sent via a HTTP GET request, either directly through the URL or via an HTML form (cf. GET). Look at the invoking URL in the following example!

This can be useful, for instance, to create a simple page navigation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>GET hyperlink navigation example</title>
  </head>
  <body>
    <h1>GET hyperlink navigation example</h1>
    <?php
      echo '<pre>' . print_r($_GET, true) . '</pre>';
      if (isset($_GET['login'])) echo 'Please log in <a href=get2.php>Home</a>';
      elseif (isset($_GET['register'])) echo 'Please register <a href=get2.php>Home</a>';
      else echo '<a href=?login>Login</a> <a href=?register>Register</a>';
    ?>
  </body>
</html>
5.2.14.2. $_POST

This array contains all variables sent via a HTTP POST request, either through an HTML form or directly through an HTTP request (cf. POST).

5.2.14.3. $_FILES

Contains all variables sent via a HTTP POST file upload.

Allowing anyone to upload files to a server represents a major security risk, as the uploaded file can be executed by everyone who has access to the directory. Only allow file upload for authenticated users and only for those file types that you want.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>File Upload</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <form action=upload1.php method=post enctype=multipart/form-data>
      <label for=file>Filename:</label>
      <input type=file name=file id=file><br>
      <input type=submit value=Upload>
    </form>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
  exit; //For security reasons, script may not run further.
  echo '<pre>' . print_r($_FILES, true) . '</pre>';
  if (isset($_FILES["file"]) && $_FILES["file"]["size"] > 0) {
    if ($_FILES["file"]["error"] > 0) {
      echo "Error: " . $_FILES["file"]["error"] . "<br>";
    }
    else {
      move_uploaded_file($_FILES["file"]["tmp_name"], 'upload/' . $_FILES["file"]["name"]);
      echo <<<EOT
        <p>
          Type:  {$_FILES["file"]["type"]}<br>
          Size:  {$_FILES["file"]["size"]} / 1024 kB<br>
          Temporarily stored in: {$_FILES["file"]["tmp_name"]}<br>
          Moved to: {$_FILES["file"]["name"]}
        </p>
        <button onclick=location.href='upload.php'>Upload another file</button>
EOT;
    }
  }
  else header('Location: https://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']) .
    '/upload.php');
?>

For an excellent more in-depth example, study w3schools.com/php/php_file_upload.asp.

Contains all variables sent via HTTP cookies. See Cookies and sessions.

5.2.14.5. $_REQUEST

Contains all variables sent via HTTP GET, HTTP POST and HTTP cookies unless there are variables with the same name, in which case some are overwritten (cf. php.net/manual/en/ini.core.php#ini.request-order). It is preferable to use $_GET, $_POST or $_COOKIE directly instead of $_REQUEST.

5.2.14.6. $_SESSION

Contains all variables stored in a user’s session (server-side data store). See Cookies and sessions.

5.2.14.7. $_SERVER

Contains all variables set by the web server or other sources that relate directly to the execution of the script.

1
2
3
<?php
  echo '<pre>' . print_r($_SERVER, true) . '</pre>';
?>
5.2.14.8. $_ENV

Contains all variables passed to the current script via the environment, e.g. when the script has been called from a shell instead of via the web server.

1
2
3
<?php
  echo '<pre>' . print_r($_ENV, true) . '</pre>';
?> 
5.2.14.9. $GLOBALS
1
2
3
4
<?php
  $x = 27;
  echo '<pre>' . print_r($GLOBALS, true) . '</pre>';
?> 

5.2.15. Forms

HTML forms are the main mechanism used to transfer data from the client to the server. The HTTP protocol provides two methods to send form data to the server: POST and GET.

5.2.15.1. POST

The POST method transmits form data within the HTTP request. It is the recommended method to transmit data to the server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Forms1</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <form method=post action=forms1.php>
      <label for=name>Please enter your name:</label>
      <input id=name name=x1>
      <input type=submit value=Submit>
    </form>
  </body>
</html>
1
2
3
4
<?php
  if (isset($_POST['x1']) && strlen($_POST['x1'])) echo "Hello {$_POST['x1']}";
  else echo "Alarm! Unknown intruder!";
?>
5.2.15.2. GET

The GET method transmits data within the URL, i.e. visible to the world! Take a look at the URL after submitting the form:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Forms2</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <a href=forms2.php?name=Otto>Click</a>
    <form action=forms2.php>
      <label for=name>Please enter your name:</label>
      <input id=name name=name>
      <input type=submit value=Submit>
    </form>
  </body>
</html>
1
2
3
4
<?php
  if (isset($_GET['name']) && strlen($_GET['name'])) echo "Hello {$_GET['name']}";
  else echo "Alarm! Unknown intruder!";
?>

This is about as insecure as it gets. It should only be used to request information from a server, for instance from Yahoo: students.btsi.lu/evegi144/WAD/PHP/get1.php

1
2
3
<?php
  header('Location: http://finance.yahoo.com/q?s=EURUSD=X');
?>
5.2.15.3. Form validation

We should not simply accept any input the user submits using a form. In order to avoid an attack on our application and to prevent invalid data from entering our database, we need to perform an in-depth check. To check whether a value has been entered in a text box, use strlen (cf. strlen) instead of empty. If the user enters 0, empty will return true:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Form Validation</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <form method=post>
      Number of dogs:
      <input type=text name=dogs>
      <input type=submit value=Submit>
    </form>
    <?php
      if (isset($_POST['dogs'])) {
        if (empty($_POST['dogs'])) echo "Dog number empty!<br>";
        if (!strlen($_POST['dogs'])) echo "Missing dog number!";
      }
    ?>
  </body>
</html>
5.2.15.3.1. Allow only alphanumeric input

In order to allow only alphanumeric characters to be submitted in a form element we can use ctype_alnum:

if (!ctype_alnum($username)) die('Invalid characters in Username');
5.2.15.3.2. Number validation

If we want to accept decimal, hexadecimal, binary, octal and exponential notation numbers, is_numeric is the way to go. If we only want integers larger than or equal to 0, we use ctype-digit. To accept negative and positive integers, we can convert the string to an integer using intval and then back to a string and check whether we get the original string. The same procedure works for decimals, where we convert the string to a float using floatval.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Number Validation</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <form method=post>
      <label>Number of dogs:</label>
      <input name=dogs required>
      <input type=submit value=Submit>
    </form>
    <?php
      if (isset($_POST['dogs'])) {
        $x = $_POST['dogs'];
        if (!is_numeric($x)) echo "You did not enter a valid numeric string.<br>";
        if (!ctype_digit($x))
          echo "You did not enter an integer larger than or equal to zero.<br>";
        if (strval(intval($x)) !== $x) echo "You did not enter an integer.<br>";
        if (strval(floatval($x)) !== $x) echo "You did not enter a float.<br>";
      }
    ?>
  </body>
</html>
5.2.15.3.3. Email validation

We can use a simplistic regular expression (cf. "Head First PHP & MySQL" first edition p. 600):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Email Validation</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <form method=post>
      <label>E-mail:</label>
      <input type=email name=email required>
      <input type=submit value=Submit>
    </form>
    <?php
      if (isset($_POST['email'])) {
        if (preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\._\-&!?=#]*@/ ', $_POST['email']))
          echo "Valid email";
        else echo "Invalid email";
      }
    ?>
  </body>
</html>

The safest way of handling email validation is probably to use Carl Henderson’s is_valid_email_address function followed by a call to checkdnsrr.

5.2.15.3.4. Drop-down lists
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Form Validation</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <form method=post>
      <select name=selection>
        <option value=1>Option 1</option>
        <option value=2>Option 2</option>
        <option value=3>Option 3</option>
        <option value=4>Option 4</option>
      </select>
      <button>Submit</button>
    </form>
    <?php
      if (isset($_POST['selection']))
        echo 'Option number ' . $_POST['selection'] . ' selected';
    ?>
  </body>
</html>

If we allow multiple selections, we need to add [] to the name of the selection so that the values are available as an array in PHP.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Form Validation</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <form method=post>
      <select multiple name=selection[]>
        <option value=1>Option 1</option>
        <option value=2>Option 2</option>
        <option value=3>Option 3</option>
        <option value=4>Option 4</option>
      </select>
      <button>Submit</button>
    </form>
    <?php
      if (isset($_POST['selection']))
        echo '<pre>' . print_r($_POST['selection'], true) . '</pre>';
    ?>
  </body>
</html>
5.2.15.3.5. Multiple choice forms
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Form Validation</title>
    <meta charset=UTF-8>
    <style>
      form {
        width:        150px;
        margin-left:  auto;
        margin-right: auto;
      }

      input {
        margin: 3px;
      }
    </style>
  </head>
  <body>
    <form method=post>
      <input name=name placeholder=Name required>
      <input type=email name=email placeholder=E-mail required><br>
      <input type=checkbox name=colours[] value=white checked>White<br>
      <input type=checkbox name=colours[] value=black>Black<br>
      <input type=checkbox name=colours[] value=red>Red
      <input type=submit name=submit>
    </form>
    <?php
      if (isset($_POST['name'])) echo $_POST['name'];
      if (isset($_POST['colours'])) {
        echo "<br><hr>";
        echo '<pre>' . print_r($_POST['colours'], true) . '</pre>';
      }
    ?>
  </body>
</html>
5.2.15.3.6. Preventing form hijacking

What runs on the client side is out of our control. Thus we cannot prevent a malicious user from trying to hijack our application in order to inflict damage. What we can do is to make our scripts as safe as possible in order to deal with attacks. How can our forms be attacked? Let’s take the previous example of the multiple choice form. The way we have implemented it so far, an attacker can simply write and run a script such as the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Form Validation</title>
    <meta charset=UTF-8>
    <style>
      form {
        width:        150px;
        margin-left:  auto;
        margin-right: auto;
      }

      input {
        margin: 3px;
      }
    </style>
  </head>
  <body>
    <?php $terror = '<script>alert("Your app has been infiltrated. '
      . 'Wire $10m to my account, or else...")</script>' ?>
    <form method=post action=formvalidation4.php>
      <input name=name placeholder=Name required>
      <input type=email name=email placeholder=E-mail required><br>
      <input type=checkbox name=colours[] value='<?php echo $terror; ?>'
             checked>White<br>
      <input type=checkbox name=colours[] value='<?php echo $terror; ?>'>Black<br>
      <input type=checkbox name=colours[] value='<?php echo $terror; ?>'>Red
      <input type=submit name=submit>
    </form>
  </body>
</html>

This is a harmless example, but a real attacker would obviously submit code in the value field that would try to hijack our server and/or database or perform some other mischief.

5.2.15.4. Using arrays to submit large volumes of data to the server

If we want to send large quantities of data to the server via a form, arrays can be very useful:

forms arrays1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Submit tabular data to the server with arrays</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <?php
        echo '<pre>' . print_r($_POST, true) . '</pre>';
        if (isset($_POST['submit'])) {
          $ids = $_POST['ids'];
          $brands = $_POST['brands'];
          $models = $_POST['models'];
          $prices = $_POST['prices'];
        }
        else {
          $ids = [1, 2, 3];
          $brands = ['LAM', 'LAM', 'LAM'];
          $models = ['Mega1', 'Mega2', 'Mega3'];
          $prices = [123.45, 234.56, 345.67];
        }

      ?>
      <form method=post>
        <table>
          <tr>
            <th>Id</th>
            <th>Brand</th>
            <th>Model</th>
            <th>Price</th>
          </tr>
          <tr>
            <td><input name=ids[] value=<?php echo $ids[0]; ?>></td>
            <td><input name=brands[] value=<?php echo $brands[0]; ?>></td>
            <td><input name=models[] value=<?php echo $models[0]; ?>></td>
            <td><input name=prices[] value=<?php echo $prices[0]; ?>></td>
          </tr>
          <tr>
            <td><input name=ids[] value=<?php echo $ids[1]; ?>></td>
            <td><input name=brands[] value=<?php echo $brands[1]; ?>></td>
            <td><input name=models[] value=<?php echo $models[1]; ?>></td>
            <td><input name=prices[] value=<?php echo $prices[1]; ?>></td>
          </tr>
          <tr>
            <td><input name=ids[] value=<?php echo $ids[2]; ?>></td>
            <td><input name=brands[] value=<?php echo $brands[2]; ?>></td>
            <td><input name=models[] value=<?php echo $models[2]; ?>></td>
            <td><input name=prices[] value=<?php echo $prices[2]; ?>></td>
          </tr>
        </table>
        <button name=submit>Submit</button>
      </form>
    </main>
  </body>
</html>
5.2.15.5. Using form fields to transfer data between pages

The safer way to transfer data between pages is to use sessions, as session data is stored on the server and therefore cannot be manipulated by the user. An easy, though not recommended, way to transfer data between pages is using input fields. In addition to simple data structures such as numbers or text, we can also transfer multidimensional arrays.

A simple application example is the creation of a list:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Simple list</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <header>
      <h1>Simple list</h1>
    </header>
    <main>
      <?php
        // To understand what happens, open your browser console, switch to the network tab
        // and look at the response to each POST request.
        $list = []; // Start with an empty list.

        if (isset($_POST['submit'])) { // Has the form been submitted?
          if (isset($_POST['list'])) $list = $_POST['list']; // Store previous list items.
          $list[] = $_POST['item']; // Add the new item.
        }
      ?>
      <form method=post>
        <?php
          $count = 0; // We need this as index for the list array in the input.
          foreach ($list as $item) { // Loop through the item list.
            // Add item as input so that it gets resubmitted with the form and not lost.
            echo "<input name=list[$count] value=$item><br>";
            $count++;
          }
        ?>
        Number of items: <?php echo $count; ?>
        <br><br>
        <label for=item>Item:</label>
        <input id=item name=item required>
        <input type=submit name=submit value=Add>
      </form>
    </main>
  </body>
</html>

Let’s say we want a page that allows us to create the name list for our sports club. Using input fields, we could do this as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Tennis club member list</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <header>
      <h1>Tennis club member list</h1>
    </header>
    <main>
      <?php
        // To understand what happens, open your browser console, switch to the network tab
        // and look at the response to each POST request.
        $names = [];

        if (isset($_POST['submit'])) {
          if (isset($_POST['existing_names'])) $names = $_POST['existing_names'];
          $names[] = [$_POST['first_name'], $_POST['last_name']];
        }
      ?>
      <form method=post>
        <?php
          $count = 0;
          foreach ($names as $name) {
            echo "<input name=existing_names[$count][0] value=$name[0]>";
            echo "<input name=existing_names[$count][1] value=$name[1]><br>";
            $count++;
          }
        ?>
        Number of members: <?php echo $count; ?>
        <br>
        First name: <input name=first_name required>
        Last name: <input name=last_name required>
        <input type=submit name=submit value=Add>
      </form>
    </main>
  </body>
</html>

If we want to transfer information between pages without the user seeing it (unless he/she uses the console or some other tool), we can hide the input fields.

5.2.16. Useful PHP functions

5.2.16.1. isset

Determine if a variable is set and not NULL. A good list of examples can be found by clicking on the section title.

5.2.16.2. strlen

Get the length of a string:

1
2
3
4
<?php
  $test_string = "WSERS rocks";
  echo "Length of string '$test_string': " . strlen($test_string);
?>
5.2.16.3. exit or die

These two perform exactly the same job, i.e. output a message and terminate the current script. Both are not really functions but language constructs.

5.2.16.4. header

Sends a raw HTTP header, often used to redirect the browser to another page. The function must be called before any actual output is sent, either by normal HTML tags, blank lines in a file, or from PHP. Alternatively, you can use ob_start and ob_flush to activate and use output buffering. In this case it does not matter where in your script you use header.

The client is not obliged to respect the location header. Therefore we need to make sure our script stops after the location header has been sent (cf. thedailywtf.com/articles/WellIntentioned-Destruction).
1
2
3
4
<?php
  header('Location: header2.php');
  exit;
?> 
1
2
3
<?php
  echo "This is header2.php.";
?>
5.2.16.5. mt_rand

Generates a random integer. Minimum and maximum can be specified.

5.2.16.6. eval

Evaluates a given string as PHP code.

Example:

1
2
3
4
5
6
7
<?php
  $x = 7;
  $y = 19;
  $op = '+';
  $str = $x . $op . $y;
  echo eval('return ' . $str. ';');
?>
5.2.16.7. dirname

Given a string containing the path of a file or directory, this function will return the parent directory’s path.

1
2
3
4
<?php
  echo $_SERVER['PHP_SELF'] . '<br>';
  echo dirname($_SERVER['PHP_SELF']);
?>
5.2.16.8. number_format

Excellent description and examples at the usual place.

5.2.16.9. strpos
5.2.16.10. implode
5.2.16.11. explode
5.2.16.12. filter_var

This function filters a variable with a specified filter.

1
2
3
4
5
6
7
<?php
  $email = 'gilles.everling@education.lu';
  var_dump(filter_var($email, FILTER_VALIDATE_EMAIL));
  $res = checkdnsrr(preg_replace('/^[a-zA-Z0-9][a-zA-Z0-9\._\-&!?=#]*@/ ', '', $email));
  if ($res) echo "<br>OK";
  else echo "<br>Not OK";
?>
5.2.16.13. preg_match
5.2.16.14. sprintf
5.2.16.16. list
5.2.16.17. ini_set
5.2.16.18. rawurlencode

Encode given string according to www.faqs.org/rfcs/rfc3986.html.

5.2.16.19. htmlspecialchars

Convert special characters to HTML entities, cf. secure.php.net/manual/en/faq.html.php.

5.2.16.20. htmlentities
5.2.16.21. html_entity_decode
5.2.16.22. serialize and unserialize

In order to store arrays or objects in a database without loss of type and structure we can use the serialize function. We can use unserialize to restore the saved structure. In the databse it is important to use a binary column type such as a BLOB in MySQL to store the object.

5.2.16.23. getdate

getdate returns an associative array containing the date information of the timestamp, or the current local time if no timestamp is given.

5.2.16.24. date

date(format) gives us a string with the current date formatted according to the given format string. We should call date_default_timezone_set to set the correct timezone beofre calling date. The list of supported timezones can be found at php.net/manual/en/timezones.php.

1
2
3
4
<?php
  date_default_timezone_set('Europe/Luxembourg');
  echo 'Current date: ' . date('d-m-Y H:i:s');
?>
5.2.16.25. nl2br

nl2br inserts HTML line breaks before all newlines in a string. This function is particularly useful for chat boxes, see Message board for a usage example.

5.2.16.26. Executing system commands

PHP offers several options to execute system commands:

For user input you should apply escapeshellarg before using any of the above functions.

5.2.16.27. Password hashing
Hashing algorithms such as MD5, SHA1 and SHA256 are designed to be very fast and efficient. With modern techniques and computer equipment, it has become trivial to "brute force" the output of these algorithms, in order to determine the original input.

Therefore, it is important to use the password_hash and password_verify functions, like so:

1
2
3
4
5
6
7
<?php
  $pw = '1!@23sa#GdTERWsd';
  $pw_hash = password_hash($pw, PASSWORD_DEFAULT);
  echo $pw_hash . '<br>';
  if (password_verify($pw, $pw_hash)) echo 'Password verified';
  else echo 'Password verification failed.';
?>

5.2.17. Regular expressions

Regular expressions can be used to replace text in, test for a pattern in or extract a substring from a string. They are very flexible and powerful and can be used in many programming languages, including JavaScript and PHP.

In PHP, they are implemented using the PCRE extension.

Remember that the pattern must be enclosed by delimiters. A delimiter can be any non-alphanumeric, non-backslash, non-whitespace character.

5.2.18. Cookies and sessions

HTTP is a stateless protocol, thus any data is forgotten when the page has been sent to the client and the connection is closed. To store client-specific data, we can use cookies, sessions, databases, files, etc. Here we’ll take a closer look at cookies and sessions.

5.2.18.1. Cookies

Cookies are bits of information that are stored locally in files by the client browser (cf. php.net/manual/en/function.setcookie.php and w3schools.com/php/func_http_setcookie.asp).

From php.net:

setcookie() defines a cookie to be sent along with the rest of the HTTP headers. Like other headers, cookies must be sent before any output from your script (this is a protocol restriction). This requires that you place calls to this function prior to any output, including <html> and <head> tags as well as any whitespace.

The next time the user points his browser to the same page, assuming that the cookie’s lifetime has not expired and the user has not deleted the cookie, which is easy to do using the browser options, the cookie will be automatically sent to the server. Thus, cookies are a mechanism to store data persistently on the client side, i.e. the information does not disappear on page reload or when the client browser is shut down.

As a simple example, let’s store a user name in the client’s browser for the duration of 3 days (3 days * 24 hours * 60 minutes * 60 seconds → 259200 seconds) that will only be sent for files that are located in the cookies folder or subfolders thereof:

1
2
3
4
5
6
<?php
  // Store the cookie named 'user' with value 'Asterix' for 3 days in the client's browser.
  setcookie('user10', 'Asterix', time() + 259200, dirname($_SERVER['PHP_SELF']) . '/cookies', '',
    true, true);
  header('Location: cookies/cookies2.php');
?>
1
2
3
<?php
  print_r($_COOKIE);
?> 

Now when we visit this page again, we can see that the cookie is indeed sent to the server:

cookie1

To delete a cookie before its expiration time, we can set its value to empty and its expiration time in the past:

1
2
3
4
5
6
<?php
  print $_COOKIE['user10'];
  setcookie('user10', '', 1, dirname($_SERVER['PHP_SELF']) . '/cookies', '',
    true, true);
  header('Location: cookies/cookies2.php');
?> 

Changes made to cookies are only visible the next time our script gets executed. Therefore, this won’t work as expected:

1
2
3
4
<?php
  setcookie('my_cookie', 'HYPER_COOKIE', time() + 10);
  print $_COOKIE['my_cookie'];
?> 

If we want to use them to store confidential data, we must use strong encryption. If we want to store the user’s password in encrypted form, so that nobody, including ourselves, can get the unencrypted password, we can proceed as follows:

1
2
3
4
<?php
  setcookie('password', password_hash('Asterix', PASSWORD_DEFAULT), time() + 10);
  header('Location: cookies/cookies2.php');
?>

If we want to store encrypted information that we can decrypt, we can use php.net/manual/en/function.mcrypt-encrypt.php.

You should not use spaces or other special characters in file or path names. If you absolutely have to, then use rawurlencode.
5.2.18.2. Sessions

If we do not want our data to be accessible outside of our server, we can use sessions. Sessions serve to maintain visitor-specific state between page requests. Here the description from secure.php.net/manual/en/intro.session.php:

A visitor accessing your web site is assigned a unique id, the so-called session id. This is either stored in a cookie on the user side or is propagated in the URL. The session support allows you to store data between requests in the $_SESSION superglobal array. When a visitor accesses your site, PHP will check automatically (if session.auto_start is set to 1) or on your request (explicitly through session_start() or implicitly through session_register()) whether a specific session id has been sent with the request. If this is the case, the prior saved environment is recreated.
To use cookie-based sessions, session_start() must be called BEFORE sending anything to the browser.

Let’s look at a very basic, though insecure, example:

1
2
3
4
5
6
<?php
  session_start();
  $_SESSION['first name'] = 'Donald';
  $_SESSION['last name'] = 'Duck';
  header('Location: session1a.php');
?>
1
2
3
4
5
<?php
  session_start();
  echo '<pre>' . print_r($_SESSION, true) . '</pre>';
  var_dump(session_get_cookie_params());
?>

The bouncer (cf. bouncer.php) used in the WMOTU Address Book illustrates how to implement safe session handling. There are a number of runtime configuration settings that we can use to control session handling (cf. php.net/manual/en/session.configuration.php):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
  // Protect from session fixation via session adoption.
  ini_set('session.use_strict_mode', true);
  # Prevent CSRF attacks
  ini_set('session.cookie_samesite', 'Strict');
  # Only send session id cookie over SSL.
  ini_set('session.cookie_secure', true);
  # Session IDs may only be passed via cookies, not appended to URL.
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_httponly', true);
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  if (!isset($_SERVER['HTTPS'])) {# If SSL is not active, activate it.
    header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']);
    exit;
  }
  if (!isset($_SESSION)) session_start(); // Start session.
  # After 30 seconds we'll generate a new session ID to prevent a session
  # fixation attack (cf. PHP cookbook p. 338).
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  if (!isset($_SESSION['user_id'])) {// No user logged in -> go to the login page.
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/index.php');
    exit;
  }
?>

The main page index.php (cf. index.php) saves the user id in $_SESSION['user_id'] if the user has provided a valid login. As long as the user is not logged in, the bouncer will always refer back to the login and sign up page. The bouncer is required in every file that executes any instructions by simple invocation in order to prevent any unauthorized access to our application. Our database (cf. database.php) class is one of the few exceptions, as the invocation of database.php by the client does essentially nothing.

To prevent a so-called session fixation attack (cf. en.wikipedia.org/wiki/Session_fixation) where a malicious user uses a session id to impersonate another user, we use the session_regenerate_id (cf. php.net/manual/en/function.session-regenerate-id.php) function, which will replace the current session id with a new one, but keep the current session information.

A session terminates when the user closes the browser. If we want the session to survive the closing of the browser, we need to change session.cookie_lifetime (cf. secure.php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime). When the user logs out, the session needs to terminate immediately. We achieve this as follows (php.net/manual/en/function.session-destroy.php):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
  if (!isset($_SESSION)) session_start(); # Start session if not done already.
  $_SESSION = []; # Empty session array.
  # If session cookie exists, kill it.
  if (session_id() != "" || isset($_COOKIE[session_name()])) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', 1, $params["path"],
      $params["domain"], $params["secure"], $params["httponly"]
    );
  }
  session_destroy(); # Kill session.
  echo '<pre>' . print_r($_SESSION, true) . '</pre>';
?>
To kill the session cookie in the browser, we must specify the same path that we used to create the session when we specified cookie_path, i.e. usually rawurlencode(dirname($_SERVER['PHP_SELF'])). Make sure to verify that the session cookie really gets deleted in the client browser.
Session security is of paramount importance. If not handled correctly, your users and your app will be exposed. Study www.phparch.com/2018/01/php-sessions-in-depth and secure.php.net/manual/en/features.session.security.management.php for excellent descriptions of the issues and solutions.
You can store objects in sessions if you remember to include the class definition before calling session_start in each file where you want to use an object retrieved from a session. See www.php.net/manual/en/function.serialize.php.

5.2.19. Files

5.2.19.1. Read file contents as a string

file_get_contents (cf. php.net/manual/en/function.file-get-contents.php) returns the content of the file as a string, including new line characters (\n) where appropriate. If we don’t want the new lines included, we can use the FILE_IGNORE_NEW_LINES option. We might also want to skip empty lines using FILE_SKIP_EMPTY_LINES. If the file couldn’t be opened, the function returns FALSE.

1
2
3
4
5
6
7
8
<?php
  $file_name = 'files1.txt';
  $file_string = file_get_contents($file_name, FILE_IGNORE_NEW_LINES |
    FILE_SKIP_EMPTY_LINES);
  if ($file_string) echo $file_string;
  else echo "Could not open $file_name";
?>
 
5.2.19.2. Read file contents as an array

file (cf. php.net/manual/en/function.file.php) returns the content of the file as an array. Each element of the array corresponds to a line in the file, with the newline still attached, unless we use the ignore option. If the file couldn’t be opened, the function returns FALSE.

1
2
3
4
5
6
7
<?php
  $file_name = 'files1.txt';
  $file_array = file($file_name, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  if ($file_array) echo '<pre>' . print_r($file_array, TRUE) . '</pre>';
  else echo "Could not open $file_name";
?>
 
5.2.19.3. Write to a file

Before writing to a new or existing file, we need to make sure the web server has write access to the directory (use chmod to change if necessary) and then get a handle to the file via fopen (cf. php.net/manual/en/function.fopen.php). Be careful to select the right mode.

Then we can write to the file using fwrite (cf. php.net/manual/en/function.fwrite.php).

1
2
3
4
5
6
7
<?php
  $file_name = 'files2.txt';
  // Remember to give the web server write access to the directory.
  $file = fopen($file_name, 'a+');
  if ($file) fwrite($file, 'This is a test string.');
  else echo '<pre>' . print_r(error_get_last(), TRUE) . '</pre>';
?> 
5.2.19.4. Delete a file

To delete a file, we use the unlink function (cf. php.net/manual/en/function.unlink.php).

5.2.19.5. Create a zip file

See www.php.net/manual/en/class.ziparchive.php. Remember to make sure that the web server has write access to the directory that you want to save the zip file in.

5.2.20. Classes and objects

Object-oriented programming allows us to align our programs much better with the real world. Instead of having variables and numerous functions seemingly independent of these variables but manipulating them, we can define things or objects that exist in the real world and model their characteristics and behavior, thereby linking data and functions.

5.2.20.1. Defining a class
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

  class Car {
    private $color;

    public function get_color() {
      return $this->color;
    }

    public function set_color($color) {
      $this->color = $color;
    }
  }

?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>First class example</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <?php
        $my_car = new Car();
        $my_car->set_color('black');
        echo $my_car->get_color();
        echo '<br>';
        var_dump($my_car);
        echo '<br>';
        print_r($my_car);
      ?>
    </main>
  </body>
</html>
5.2.20.2. Static properties and functions
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

  class Database {
    # This is a private static property, so we can use it without creating an object.
    private static $DB_HOST;

    public static function set_host($db_host) {
      self::$DB_HOST = $db_host;
      echo 'Host set to ' . self::$DB_HOST . '.';
    }
  }
?>

<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Second class example</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <?php
        Database::set_host('foxi.ltam.lu');
      ?>
    </main>
  </body>
</html>
5.2.20.3. Objects as parameters

5.2.23. PDF generation

ourcodeworld.com/articles/read/226/top-5-best-open-source-pdf-generation-libraries-for-php

blog.chapagain.com.np/php-easily-create-pdf-on-the-fly

5.2.23.1. TCPDF

Generate PDFs with PHP: www.tcpdf.org. This package is normally already installed as php-tcpdf.

5.2.23.2. FPDF
5.2.23.3. dompdf

Convert HTML to PDF using github.com/dompdf/dompdf.

5.2.25. WebSockets

Currently the Apache web server does not come with a web socket module. We can write our own web socket server in PHP, as shown here or use open source libraries.

We’ll use Open Swoole. See these video tutorials to get started. Here's an example WebSocket server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php

  use OpenSwoole\WebSocket\Server;
  use OpenSwoole\Http\Request;
  use OpenSwoole\WebSocket\Frame;
  use OpenSwoole\Table;

  $server = new Server("0.0.0.0", 9000);
  // https://stackoverflow.com/questions/74400372/php-swoole-how-to-make-variables-persistent-between-requests
  $timers = new OpenSwoole\Table(1024);
  $timers->column('timer_handle', OpenSwoole\Table::TYPE_INT, 8);
  $timers->create();

  $server->on("Start", function (Server $server) {
    echo "OpenSwoole WebSocket Server is started at http://127.0.0.1:9000\n";
  });

  $server->on('Open', function (Server $server, OpenSwoole\Http\Request $request) {
    // https://stackoverflow.com/questions/74572384/how-to-access-session-data-in-openswoole-websocket-server
    // Sessions do not really work in this context, you would need to implement your own
    // session or token system, e.g. https://www.youtube.com/watch?v=9qPnPGfAYdo.
    // session_id(trim($request->cookie['PHPSESSID']));
    // session_start();
    //var_dump($request);
    echo "connection open: {$request->fd}\n";
    // $_SESSION['user'] = 'test' . date('h:i:sa');
    // echo "Session contents: " . print_r($_SESSION, true);
    global $timers;
    $timers->set($request->fd, [$server->tick(1000, function () use ($server, $request) {
      if ($server->isEstablished($request->fd))
        $server->push($request->fd, json_encode(["hello", time()]));
    })]);
    foreach ($timers as $timer) var_dump($timer);
  });

  $server->on('Message', function (Server $server, Frame $frame) {
    echo "Server:";
    //foreach($server->connections as $conn) var_dump($conn);
    echo "received message: {$frame->data}\n";
    $server->push($frame->fd, json_encode(["You sent: ", $frame->data]));
  });

  $server->on('Close', function (Server $server, int $fd) {
    echo "connection close: {$fd}\n";
    global $timers;
    echo $timers->get($fd)['timer_handle'];
    $server->clearTimer($timers->get($fd)['timer_handle']);
    var_dump($timers->del($fd));
    //session_write_close();
  });

  $server->on('Disconnect', function (Server $server, int $fd) {
    echo "connection disconnect: {$fd}\n";
  });

  $server->start();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <meta name=viewport content="width=device-width, initial-scale=1">
  <title>Swoole WebSocket client 1</title>
  <script type=module>
    const DEBUG = true
    const input = document.querySelector('input')
    const button = document.querySelector('button')
    const section = document.querySelector('section')
    const wssURL = 'wss://students.btsi.lu/node/wss'
    const wss = new WebSocket(wssURL)
    wss.addEventListener('open', () => {
      if (DEBUG) console.log('WebSocket connection opened.')
      button.addEventListener('click', () => {
        wss.send(input.value)
      })
    })

    wss.addEventListener('message', e => {
      section.innerHTML += e.data
    })

    wss.addEventListener('close', () => {
      if (DEBUG) console.log('Disconnected...')
    })

    wss.addEventListener('error', () => {
      if (DEBUG) console.log('Error...')
    })
  </script>
</head>
<body>
  <input placeholder=message>
  <button>Send</button>
  <br>
  <section></section>
</body>
</html>

There are other libraries around, such as github.com/walkor/phpsocket.io or Ratchet (see github.com/ratchetphp/Ratchet/issues/489 on how to use it with HTTPS). Sample chat application.

But, as mentioned before, in order to really work with WebSockets you should consider using Node.js as it is much better suited for this purpose.

5.2.26. Web scraping

The following sources can give you a quick overview of what web scraping is and how it can be done:

en.wikipedia.org/wiki/Web_scraping

stackoverflow.com/questions/3577641/how-do-you-parse-and-process-html-xml-in-php

We will look at two approaches, one using PHP and one using JS.

5.2.26.1. using PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
  $URL = 'https://www.dne.lu';
  $html = file_get_contents($URL);
  $dom = new DOMDocument();
  $dom->loadHTML($html);
  echo '<pre>' . print_r($dom->childNodes, true) . '</pre>';
  foreach ($dom->childNodes as $child) var_dump($child);
  //echo $dom->saveHTML(); // In case we manipulate the DOM, we can then save the DOM as HTML
                           // and send it to the browser.
  /*$meteo_header = $dom->getElementById('meteo-header');
  echo '<pre>' . print_r($meteo_header, true) . '</pre>';*/
  /*$out = "<script>'use strict'; const meteoHeader =' " . $meteo_header . "';</script>";
  echo $out;
  error_log($out);*/
?>
5.2.26.2. using JS
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>Web scraper</title>
    <script>
      'use strict';

      const scrape = e => {
        const html = e.target.response;
        if (html == 'Invalid URL') console.log(html);
        else {
          const doc = new DOMParser().parseFromString(html, "text/html");
          console.dir(html);
          console.dir(doc.querySelector('.meteo-short'));
        }
      };

      const URL = 'https://www.rtl.lu';
      const req = new XMLHttpRequest();
      req.open('POST', 'web_scrape2.php');
      req.addEventListener('load', scrape);
      req.send(URL);
    </script>
  </head>
  <body>
  </body>
</html>
1
2
3
4
5
<?php
  $URL = file_get_contents('php://input');
  if (filter_var($URL, FILTER_VALIDATE_URL)) echo file_get_contents($URL);
  else echo 'Invalid URL';
?>

5.2.28. Tests

5.2.28.1. National Elections 2013
NationalElections20131

You have been tasked to develop an online election system (students.btsi.lu/evegi144/WAD/PHP/Tests/NationalElections2013) for the Luxembourg national elections 2013. Use students.btsi.lu/evegi144/WAD/PHP/Tests/NationalElections2013/index_skeleton.zip as skeleton.

Create a folder Tests/NationalElections2013 in your main Foxi folder and save your solution under the name index.php. Make sure that the group www-data has write access to the NationalElections2013 folder.

The array $parties stores the name of each party and $polls contains the number of votes for each party.

Write the function get_total_number_of_votes that returns the total number of votes. The script generates an HTML table with the votes and percentages for each party. Use number_format to format the percentage numbers.

5.2.28.1.1. Solution
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Elections</title>
    <meta charset=UTF-8>
    <style>
      label {
        float:         left;
        clear:         left;
        width:         60px;
        text-align:    right;
        padding-right: 10px;
        margin-top:    5px;
        display:       inline-block;
      }

      input {
        margin-top: 9px;
      }

      table {
        margin-top:       10px;
        border:           1px solid black;
        border-spacing:   0;
        background-color: green;
      }

      table th, td {
        border:  4px groove green;
        padding: 4px;
      }

      table th {
        color:      gold;
        text-align: left;
      }

      table tr:nth-of-type(even) {
        color: lightgreen;
      }

      thead {
        background-color: darkgreen;
      }

      tfoot {
        background-color: orange;
      }
    </style>
  </head>
  <body>
    <form>
      <div>
        <label for=1>déi Lénk</label>
        <input type=radio id=1 name=vote <?php if (isset($_GET['vote']) && $_GET['vote']==1)
          echo "checked";?> value=1>
      </div>
      <div>
        <label for=2>adr</label>
        <input type=radio id=2 name=vote <?php if (isset($_GET['vote']) && $_GET['vote']==2)
          echo "checked";?>  value=2>
      </div>
      <div>
        <label for=3>KPL</label>
        <input type=radio id=3 name=vote <?php if (isset($_GET['vote']) && $_GET['vote']==3)
          echo "checked";?>  value=3>
      </div>
      <div>
        <label for=4>DP</label>
        <input type=radio id=4 name=vote <?php if (isset($_GET['vote']) && $_GET['vote']==4)
          echo "checked";?>  value=4>
      </div>
      <div>
        <label for=5>Piraten</label>
        <input type=radio id=5 name=vote <?php if (isset($_GET['vote']) && $_GET['vote']==5)
          echo "checked";?>  value=5>
      </div>
      <div>
        <label for=6>déi gréng</label>
        <input type=radio id=6 name=vote <?php if (isset($_GET['vote']) && $_GET['vote']==6)
          echo "checked";?>  value=6>
      </div>
      <div>
        <label for=7>LSAP</label>
        <input type=radio id=7 name=vote <?php if (isset($_GET['vote']) && $_GET['vote']==7)
          echo "checked";?>  value=7>
      </div>
      <div>
        <label for=8>CSV</label>
        <input type=radio id=8 name=vote <?php if (isset($_GET['vote']) && $_GET['vote']==8)
          echo "checked";?>  value=8>
      </div>
      <div>
        <label for=9>PID</label>
        <input type=radio id=9 name=vote <?php if (isset($_GET['vote']) && $_GET['vote']==9)
          echo "checked";?>  value=9>
      </div>
      <input type=submit value=Vote>
    </form>
    <?php
      function get_polls() {
        $filename = "poll_result.txt";
        $contents = @file($filename);
        if (!$contents) $array = array(0, 0, 0, 0, 0, 0, 0, 0, 0);
        else $array = explode('x', $contents[0]);
        if (isset($_GET['vote'])) {
          $array[$_GET['vote'] - 1]++;
          $fp = fopen($filename, "w");
          fputs($fp, implode('x', $array));
          fclose($fp);
        }
        return $array;
      }

      function get_total_number_of_votes() {
        $sum = 0;
        global $polls;
        //foreach ($polls as $poll) $sum += $poll;
        for ($i = 0; $i < count($polls); $i++) $sum += $polls[$i];
        return $sum;
      }

      $parties = array('déi Lénk', 'adr', 'KPL', 'DP', 'Piraten', 'déi gréng', 'LSAP', 'CSV',
        'PID');
      $polls = get_polls();
      $total = get_total_number_of_votes();
      echo "<table><thead><tr><th>Party</th><th>#</th><th>%</th></tr></thead>";
      for ($i = 0; $i < count($parties); $i++)
        echo "<tr><td>$parties[$i]</td><td>$polls[$i]</td><td>"
          . ($total > 0 ? 100 * number_format($polls[$i] / $total, 2) : "0") .
          "</td></tr>";
      echo "<tfoot><tr><td>Total</td><td>$total</td><td>100</td></tr></tfoot></table>";
    ?>
  </body>
</html>

The skeleton provides the HTML frame with embedded CSS and the form. The CSS part hints already at the elements that we’ll need to generate the poll result table. Note the rule table tr:nth-of-type(even) on line 38, which creates the color alternation between even and odd lines.

The form illustrates how we can implement mutually exclusive radio buttons and remember the last selection. For this to work, all radio inputs need to have the same name, but different value attributes. We use PHP to check for each radio input whether it was checked when the form was submitted. If it was, we’ll check it again. The only modification required in the first 104 lines of the skeleton is in line 89, where we need to add `type=submit to turn the input into a submit button. Otherwise the user has no way to send his vote to the server.

It is important to realize that the get_polls function should not be called more than once, given that it increases the number of votes of the chosen party during each call.

The function get_total_number_of_votes is superfluous, as PHP provides the function php.net/manual/en/function.array-sum.php[`array_sum^]. We’ll still develop it, for pedagogical reasons. In fact we’ve done this already in a previous exercise. The problem statement did not specify whether the function should take a parameter or not, although the skeleton suggests in line 114 that the function should take no parameters. In this case, we need to work with the global variable $polls. As we’ve seen in Variable scope, we need to use the keyword global to make a global variable visible inside a function. Alternatively, we can pass $polls as parameter to our function.

Now we can generate the HTML table.

5.2.28.2. WMOTU Speed Calc
WMOTUSpeedCalc1

Develop the web site (www.youtube.com/watch?v=Dm-mPRNqYQc[^) for WMOTU Speed Calc. Create a folder Tests/WMOTUSpeedCalc in your main Foxi folder and save your solution under the name index.php. Implement the following:

  1. The whole app consists of a single script index.php.

  2. Login with cookie-based sessions. User data (user name, password) are stored statically in an array. You need 2 users with name/password user/user and admin/admin. The session cookie is only sent via SSL. Session IDs are regenerated every 20 seconds.

  3. Logout.

  4. The page contains a short description of the company.

  5. Everyone can enter a text into a form. After submission the number of words will be displayed. The form provides a check box. If it is selected, the first letter of every word in the text will be capitalized. Example: "WSERS1 is great" → "WSERS1 Is Great".

  6. Everyone can send a contact request. Form submission will send an email to your teacher. The subject and from fields must have sensible values.

  7. For a logged in user the screen background color is green. For an admin user, the background color is gold. For everyone else the background color is the default value, i.e. white.

  8. The administrator sees a list of all registered users.

  9. A logged in user can perform the following calculations:

    1. Convert an integer to binary.

    2. Calculate the volume of a room given its width, length and height.

    3. Calculate the annual cost of the user’s power consumption based on the average operation time, which can be provided on a daily, weekly or monthly basis.

5.2.28.2.1. Solution
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
<?php
  # SESSION HANDLING
  # We need to handle the session related stuff before we send anything (HTML, CSS,
  #  JavaScript) to the browser, as the session cookie is sent in the HTTP header,
  # which needs to be sent before the HTTP body containing the web page content.
  ini_set('session.cookie_secure', true); # Only send session id cookie over SSL.
  ini_set('session.use_only_cookies', true); # Pass session IDs only via cookies, not URL.
  ini_set('session.cookie_path', dirname(htmlspecialchars($_SERVER['PHP_SELF'])));
  if (!isset($_SERVER['HTTPS'])) # If SSL is not active, activate it.
    header('Location: https://' . $_SERVER['HTTP_HOST'] .
      htmlspecialchars($_SERVER['PHP_SELF']));
  if (!isset($_SESSION)) session_start(); # If we are not already in a session, start one.
  # After 20 seconds we'll generate a new session ID to prevent a session
  # fixation attack (cf. PHP cookbook p. 338).
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 20)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }

  # GLOBAL VARIABLE DECLARATION
  $users = array(array('user', 'user'), array('admin', 'admin')); # Static user array.
  $text = $word_count = $number = $volume = $cost = ''; # Global vars for form processing.

  # LOGOUT PROCESSING
  # If the user has clicked the logout button, we delete the session array, expire the
  # session cookie, destroy the session and reload the page.
  if (isset($_POST['logout'])) {
    if (!isset($_SESSION)) session_start();
    $_SESSION = array();
    if (session_id() != "" || isset($_COOKIE[session_name()])) setcookie(session_name(),
      '', time() - 2592000, '/');
    session_destroy();
    header('Location: https://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']));
  }

  # LOGIN PROCESSING
  elseif (isset($_POST['username']) && isset($_POST['password'])) {
    if ($_POST['username'] === $users[0][0] && $_POST['password'] === $users[0][1])
      $_SESSION['user_id'] = 0;
    elseif ($_POST['username'] === $users[1][0] && $_POST['password'] === $users[1][1])
      $_SESSION['user_id'] = 1;
  }

  # TEXT FORM PROCESSING
  elseif (isset($_POST['textBox'])) {
    $text       = $_POST['textBox'];
    $word_count = str_word_count($text);
    if (isset($_POST['checkBox'])) $text = ucwords($text);
  }

  # CONTACT FORM PROCESSING
  elseif (isset($_POST['contactBox'])) {
    $to      = 'gilles.everling@education.lu';
    $subject = 'Contact request';
    $message = $_POST['contactBox'];
    $headers = 'From: T2IF2@ltam.lu';
    mail($to, $subject, $message, $headers);
  }

  # INTEGER TO BINARY CONVERSION FORM PROCESSING
  elseif (isset($_POST['number'])) $number = sprintf('%b', $_POST['number']);

  # ROOM VOLUME FORM PROCESSING
  elseif (isset($_POST['width']) && isset($_POST['length']) && isset($_POST['height']))
    $volume = $_POST['width'] * $_POST['length'] * $_POST['height'];

  # ANNUAL POWER COST FORM PROCESSING
  elseif (isset($_POST['operatingTime']) && isset($_POST['duration'])) {
    if ($_POST['duration'] === 'day') $cost = $_POST['operatingTime'] * 365;
    elseif ($_POST['duration'] === 'week') $cost = $_POST['operatingTime'] * 52;
    else $cost = $_POST['operatingTime'] * 12;
  }
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Speed Calc</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <!-- LOGIN INDEPENDENT STUFF -->
      <!-- LOGIN/LOGOUT FORM -->
      <form method=post>
        User name:
        <input type=text name=username required autofocus>
        Password:
        <input type=password name=password required>
        <input type=submit value=Login>
      </form>

      <!-- LOGOUT FORM -->
      <form method=post>
        <input type=submit name=logout value=Logout>
      </form>

      <!-- COMPANY DESCRIPTION -->
      <hr>
      <h1>WMOTU Speed Calc</h1>
      <p>
        From our offices serving 5 continents we deliver unmatched calculation speed at
        insane prices.
      </p>

      <!-- TEXT FORM -->
      <form method=post>
        <h2>Enter your text and see the magic</h2>
        <textarea name=textBox required><?php echo $text; ?></textarea>
        <input type=checkbox name=checkBox>
        <input type=submit value=Send>
      </form>
      Number of words: <?php echo $word_count; ?><br>

      <!-- CONTACT FORM -->
      <form method=post>
        <h2>Contact us</h2>
        <textarea name=contactBox required></textarea>
        <input type=submit value=Send>
      </form>

      <!-- LOGIN DEPENDENT STUFF -->
      <?php
        if (isset($_SESSION['user_id'])) { # If a user is logged in.
          if ($_SESSION['user_id'] === 0) $color = 'green'; # Normal user -> green bg.
          else { # Admin user gets a list of all users and golden bg.
            echo '<p>Registered users: ';
            for ($i = 0; $i < count($users); $i++) echo " {$users[$i][0]} ";
            echo '</p>';
            $color = 'gold';
          }
          echo "<script>document.getElementsByTagName('body')[0].style." .
            "backgroundColor = '$color'</script>"; # Set bg color based on login.
          echo <<<EOT
        <!-- INTEGER TO BINARY CONVERSION FORM -->
        <h2>Convert an integer to binary</h2>
        <form method=post>
          <input type=number name=number required>
          <input type=submit value="Convert to binary">
        </form>
        Binary equivalent: $number

        <!-- ROOM VOLUME CALCULATION FORM -->
        <h2>Calculate the volume of a room</h2>
        <form method=post>
          width: <input type=number name=width required>
          length: <input type=number name=length required>
          height: <input type=number name=height required>
          <input type=submit value="Calculate volume">
        </form>
        Volume: $volume

        <!-- ANNUAL POWER COST FORM -->
        <h2>Calculate your power cost</h2>
        <form method=post>
          Average operating time: <input type=number name=operatingTime required><br>
          <input type="radio" name=duration value=day>daily<br>
          <input type="radio" name=duration value=week>weekly<br>
          <input type="radio" name=duration value=month>monthly
          <input type=submit value="Calculate power cost">
        </form>
        Cost: $cost
EOT;
        }
      ?>
    </main>
  </body>
</html>
5.2.28.3. WMOTU Shop
WMOTUShop1

Create the web page shown at youtu.be/aWo78bwl0s8.

5.2.28.3.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
  $items = []; // All items will be stored in this array.

  if (isset($_POST['BUTTON_add'])) { // If the form has been submitted.
    if (isset($_POST['DATA_items'])) $items = $_POST['DATA_items']; // Existing items.
    if ($_POST['DATA_name'] !== '') { // Add new item.
      $items[] = ['name' => $_POST['DATA_name'],
        'price' => str_replace(',', '.', $_POST['DATA_price'])]; // Convert , to .
    }
  }
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=utf-8>
    <title>WMOTU Shop</title>
    <style>
      div.left {
        display: inline-block;
        width: 250px;
      }

      div.right {
        display: inline-block;
        width: 80px;
        text-align: right;
      }

      input[type=text] {
        width: 100%;
      }
    </style>
  </head>
  <body>
    <h1>Shopping list</h1>
    <form method=POST>
      <?php // Add the existing items as hidden inputs so they'll be included in the form.
        $total = 0;
        $i = 0;
        foreach ($items as $item) {
          echo "<input type=hidden name=DATA_items[$i][name] value=" . $item['name'] . ">";
          echo "<input type=hidden name=DATA_items[$i][price] value=" . $item['price'] . ">";
          echo '<div class=left>' . $item['name'] . '</div>';
          echo '<div class=right>' . number_format($item['price'], 2) . ' &euro;</div><br>';
          $total += $item['price'];
          $i++;
        }
      ?>
      <div class=left style="border-top: 1px #000 solid; font-weight: bold;">Total</div>
      <div class=right style="border-top: 1px #000 solid; font-weight: bold;">
        <?php echo number_format($total, 2) ?> &euro;</div>
      <br><br><br>
      <div class=left>Name</div>
      <div class=right>Price &euro;</div>
      <br>
      <div class=left><input name=DATA_name autofocus></div>
      <div class=right><input name=DATA_price></div>
      <br>
      <button name=BUTTON_add>Add</button>
    </form>
  </body>
</html>
5.2.28.4. Quiz and Shopping Center
QuizandShoppingCenter1

Create a folder Tests/QaSC in your main Foxi folder and save your solution under the name index.php.

The initial page shows a navigation bar with two links, one to the quiz and one to the shop. The main part shows a welcome message.

The quiz link calls the same script. It’s navigation bar shows only the link to the shop. The main part shows a heading, an empty line and then the problem to be solved, which consists of 2 random integers between 0 and 20 and an operator, which is either +, - or *. You may find the eval function (cf. eval) helpful. Beneath is an input and a submit button. After the user has submitted an answer, the text "You’ve answered " followed by the number of correctly answered questions followed by "question" followed by an "s" if several questions have been answered correctly followed by " correctly out of " followed by the total number of questions answered is displayed.

The shop link calls the same script. It’s navigation bar shows only the link to the quiz. The main part shows a heading, then the listing of all the items added so far with their name and price and the total price. Beneath are 2 labels and 2 inputs for the name and price of a new item to be added to the shopping list and the submit button.

5.2.28.4.1. Solution
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=utf-8>
    <title>Quiz and Shopping Center</title>
    <style>
      div.left {
        display: inline-block;
        width:   250px;
      }

      div.right {
        display:    inline-block;
        width:      80px;
        text-align: right;
      }

      input[type=text] {
        width: 100%;
      }
    </style>
  </head>
  <body>
    <nav>
      <?php
        if (!isset($_REQUEST['quiz'])) echo '<a href=?quiz>Quiz</a> ';
        if (!isset($_REQUEST['shop'])) echo '<a href=?shop>Shop</a>';
      ?>
    </nav>
    <main>
      <?php
        if (!isset($_REQUEST['quiz']) && !isset($_REQUEST['shop'])) echo '<h1>Welcome to the Quiz and Shopping Center</h1>';
        elseif (isset($_REQUEST['quiz'])) {
          echo '<h1>Quiz</h1>';
          $a   = rand(0, 20);
          $b   = rand(0, 20);
          $ops = ['+', '-', '*'];
          $op  = $ops[rand(0, 2)];
          $str = $a . $op . $b;
          eval('$correct_answer = ' . $str . ';');
          $correct_count = 0;
          $total_count   = 0;
          if (isset($_POST['answer'])) {
            $correct_count = $_POST['correct_count'];
            $total_count   = $_POST['total_count'] + 1;
            if ($_POST['answer'] === $_POST['correct_answer']) $correct_count++;
            echo "You've answered $correct_count question" .
              (($correct_count < 2) ? '' : 's') . " correctly out of
            $total_count.";
          }
          echo '<br>' . $str;
          ?>
          <form method=POST>
            <?php
              echo "<input type=hidden name=correct_answer value=$correct_answer>";
              echo "<input type=hidden name=correct_count value=$correct_count>";
              echo "<input type=hidden name=total_count value=$total_count>";
            ?>
            <input type=number name=answer autofocus>
            <input type=submit name=quiz value=Check>
          </form>
        <?php
        }
        if (isset($_REQUEST['shop'])) {
          $items = []; // All items will be stored in this array.
          if (isset($_POST['shop'])) { // If the form has been submitted.
            if (isset($_POST['items'])) {$items = $_POST['items']; // Existing items.
            error_log(print_r($_POST['items'], TRUE));}
            if ($_POST['name'] !== '') // Add new item.
              $items[] = ['name'  => $_POST['name'],
                          'price' => str_replace(',', '.', $_POST['price'])]; // Convert , to .
          }
          ?>
          <h1>Shopping list</h1>
          <form method=POST>
            <?php // Add existing items as hidden inputs so they'll be included in the form.
              $total = 0;
              $i     = 0;
              foreach ($items as $item) {
                echo "<input type=hidden name=items[$i][name] value=" . $item['name'] . ">";
                echo "<input type=hidden name=items[$i][price] value=" . $item['price'] . ">";
                echo '<div class=left>' . $item['name'] . '</div>';
                echo '<div class=right>' . number_format($item['price'], 2) . ' &euro;</div>
                <br>';
                $total += $item['price'];
                $i++;
              }
            ?>
            <div class=left style="border-top: 1px #000 solid; font-weight: bold;">
              Total
            </div>
            <div class=right
                 style="border-top: 1px #000 solid; font-weight: bold;">
              <?php echo number_format($total, 2) ?> &euro;</div>
            <br><br><br>
            <div class=left>Name</div>
            <div class=right>Price &euro;</div>
            <br>
            <div class=left><input name=name autofocus></div>
            <div class=right><input name=price></div>
            <br>
            <button name=shop>Add</button>
          </form>
        <?php
        }
      ?>
    </main>
  </body>
</html>
5.2.28.5. Roll The Dice
RollTheDice1

Develop the following app using only HTML, CSS and PHP: youtu.be/Vbwny2QUi4Y

5.2.28.5.1. Solution

This solution uses hidden fields just to illustrate their usage. A better approach would be to use sessions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Roll The Dice</title>
    <meta charset=utf-8>
    <style>
      #div1 {
        display: inline-block;
        width: 150px;
      }

      #div2 {
        display: inline-block;
        width: 350px;
      }

      select {
        width: 100%;
      }
    </style>
  </head>
  <body>
    <main>
      <form method=POST>
        <?php
          $throws = [];
          $stats = [0, 0, 0, 0, 0, 0];
          if (isset($_POST['throw'])) {
            if (isset($_POST['throws'])) $throws = explode(',', $_POST['throws']);
            $rnd = rand(1, 6);
            $throws[] = $rnd;
            foreach ($throws as $throw) $stats[$throw - 1]++;
            echo "<input type=hidden name=throws value=" . implode(',', $throws) . ">";
          }
        ?>
        <div id=div1>
          Results<br>
          <select size=6>
            <?php
              foreach ($throws as $throw) echo "<option>$throw</option>";
            ?>
          </select>
        </div>
        <div id=div2>
          Statistics<br>
          <select size=6>
            <?php
              $sum = array_sum($stats);
              for ($i = 0; $i < count($stats); $i++)
                echo "<option>" . ($i + 1) . " has been thrown {$stats[$i]}
                times" . ($sum > 0 ? sprintf(', i.e. %1.2f', 100 * $stats[$i] / array_sum
                    ($stats)) . ' %' : '') . "</option > "; // Avoid division by zero!
            ?>
          </select>
        </div>
        <br>
        <button name=throw>Throw</button>
      </form>
    </main>
  </body>
</html>
5.2.28.6. Message board
MessageBoard1

Develop a simple message board, as shown in youtu.be/1KoDEvFF7q0, using only HTML, CSS and PHP. You may use sessions.

Messages are stored in a 2-dimensional array, the first element of each message being the timestamp and the second the message text.

Define and use the two functions displayMessage($msg) and displayMessages(). The former displays a given message with its timestamp. The latter displays all messages, with the newest at the top.

Use colors lavender, lightskyblue and magenta.

5.2.28.6.1. Solution

Watch the solution video at youtu.be/hAsZs4amUkA.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php
  session_start(); // We need to start a session in order to be able to use $_SESSION.
  date_default_timezone_set('Europe/Luxembourg'); // Set default timezone.

  /* If the messages array does not yet exist or if the clear button has been pressed,
     we need to initialize the messages array with an empty array. */
  if (!isset($_SESSION['messages']) || isset($_POST['clear'])) $_SESSION['messages'] = [];
  if (isset($_POST['msg'])) // If a message text has been sent, store it with date and time.
    $_SESSION['messages'][] = [date('d-m-Y H:i:s'), $_POST['msg']];

  /**
   * Display a single message consisting of timestamp and text.
   * @param $msg message array consisting of timestamp and message text
   */
  function displayMessage($msg)
  {
    echo "<div><span>$msg[0]: </span>" . nl2br($msg[1]) . "</div>";
  }

  /**
   * Display all messages, newest first.
   */
  function displayMessages()
  {
    for ($i = count($_SESSION['messages']) - 1; $i >= 0; $i--)
      displayMessage($_SESSION['messages'][$i]);
  }

?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Message Board</title>
    <meta charset=utf-8>
    <style>
      div:nth-of-type(odd) {
        background-color: lavender;
      }

      div:nth-of-type(even) {
        background-color: lightskyblue;
      }

      span {
        color: magenta;
      }
    </style>
  </head>
  <body>
    <main>
      <form method=post>
        <textarea name=msg autofocus required></textarea>
        <button>Send</button>
      </form>
      <hr>
      <form method=post>
        <button name=clear>Clear messages</button>
      </form>
      <?php
        displayMessages(); // Display all messages.
      ?>
    </main>
  </body>
</html>
5.2.28.7. Book Shop
BookShop1

Develop the single file app exactly as shown at youtu.be/Er7XZmGWNkg using only HTML, CSS and PHP and taking the following into account:

  1. Use the skeleton at students.btsi.lu/evegi144/WAD/PHP/Tests/BookShop.

  2. The book titles, authors and prices are all stored in a single multidimensional array.

  3. Write a function get_total, which takes a single parameter from which it computes and returns the total cost of all items currently in the shopping cart.

  4. Write a function get_table, which takes a single parameter from which it generates and returns the HTML table with the contents of the shopping cart.

5.2.28.7.1. Solution
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<?php
  session_start();
  $books = [['JavaScript - The Definitive Guide', 'David Flanagan', 49.99],
    ['HTML5 for Masterminds', 'J.D. Gauchat', 39.95],
    ['PHP and MySQL Web Development', 'Luke Welling and Laura Thomson', 59.99],
    ['PHP in a Nutshell', 'Paul Hudson', 29.95],
    ['Advanced PHP Programming', 'George Schlossnagle', 49.99]];
  if (!isset($_SESSION['cart_items']) || isset($_POST['empty']))
    $_SESSION['cart_items'] = [0, 0, 0, 0, 0];
  if (isset($_POST['selected_book'])) $_SESSION['cart_items'][$_POST['selected_book']]++;

  function get_total($cart)
  {
    global $books;
    $total = 0;
    for ($i = 0; $i < count($cart); $i++) $total += $books[$i][2] * $cart[$i];
    return $total;
  }

  function get_table($cart)
  {
    global $books;
    $table = '<table>';
    for ($i = 0; $i < count($cart); $i++) {
      $table .= "<tr><td>$cart[$i]</td><td>" . $books[$i][0] . "</td><td>$" . $books[$i][2] *
        $cart[$i] .
        "</td></tr>";
    }
    $table .= "<tr><th colspan=3>Number of articles: " . array_sum($cart) . " Total: $" .
      get_total($cart) . '</th></tr>';
    $table .= '</table>';
    return $table;
  }

?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Book Shop</title>
    <meta charset=utf-8>
    <style>
      body {
        background: linear-gradient(darkgreen, lightgreen) fixed;
      }

      #div1 {
        float: left;
      }

      #div2 {
        display: inline-block;
        width: 350px;
      }

      select {
        width: 100%;
        vertical-align: top;
        background-color: khaki;
      }

      table {
        border: 2px solid blue;
      }

      td, th {
        border: 1px inset blue;
        padding: 5px;
      }

      button {
        background-color: lightblue;
      }

      h1 {
        text-shadow: 2px 2px 1px gold;
      }
    </style>
  </head>
  <body>
    <header>
      <h3>
        <a
        <?php
          if (isset($_GET['credits'])) echo 'href=?book_shop>Book Shop';
          else echo 'href=?credits>Credits';
        ?>
        </a>
      </h3>
      <hr>
    </header>
    <main>
      <?php
        if (isset($_GET['credits'])) {
          ?>
          <h1>Web Server Side Scripting is the greatest thing on Earth</h1>
        <?php
        } else {
          ?>
          <div id=div1>
            <form method=post>
              <select name=selected_book>
                <?php
                  for ($i = 0; $i < count($books); $i++) {
                    echo "<option value=" . $i . ">" . $books[$i][0] . " - $" .
                      $books[$i][2] . "</option>";
                  }
                ?>
              </select>
              <br>
              <button>Add</button>
            </form>
            <form method=post>
              <button name=empty>Empty cart</button>
            </form>
          </div>
          <div id=div2>
            <?php
              echo get_table($_SESSION['cart_items']);
            ?>
          </div>
        <?php
        }
      ?>
    </main>
  </body>
</html>
5.2.28.8. TicTacToe
TicTacToe1

Develop the single file app exactly as shown at youtu.be/pZaja-g-o2U using only HTML, CSS and PHP and taking the following into account:

  1. Use the skeleton at students.btsi.lu/evegi144/WAD/PHP/Tests/TicTacToe.

  2. The play field is stored in a simple array.

  3. Each button contains one of the three images (image1, image2 and image3), but these img tags are generated by a function get_img, which takes the play field position as parameter (i.e. an integer from [0, 8]) and returns the complete img tag.

  4. The two players play in alternation. The current player is indicated in the header.

  5. If a player clicks an occupied field, nothing happens.

  6. Pressing the "New game" button clears the play field and asks player 1 to make a move.

5.2.28.8.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?php
  session_start();
  define('NONE', 0);
  define('PLAYER_ONE', 1);
  define('PLAYER_TWO', 2);
  if (!isset($_SESSION['field']) || isset($_POST['new_game'])) {
    $_SESSION['field'] = [NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE];
    $_SESSION['player1'] = true;
    $_SESSION['h1'] = "Player 1, your turn";
  } elseif (isset($_POST['b'])) {
    if ($_SESSION['field'][$_POST['b'] - 1] === NONE) {
      $_SESSION['field'][$_POST['b'] - 1] = $_SESSION['player1'] ? PLAYER_ONE : PLAYER_TWO;
      $_SESSION['player1'] = !$_SESSION['player1'];
      $_SESSION['h1'] = $_SESSION['player1'] ? "Player 1, your turn" : "Player 2, your turn";
    }
  }

  function get_img($i) {
    if ($_SESSION['field'][$i - 1] === NONE)
      return "<img src=empty100x100.png width=100 height=100 alt=empty>";
    elseif ($_SESSION['field'][$i - 1] === PLAYER_ONE)
      return "<img src=X100x100.png width=100 height=100 alt=X>";
    else return "<img src=O100x100.png width=100 height=100 alt=O>";
  }

?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>TicTacToe</title>
    <meta charset=utf-8>
    <style>
      section {
        display: table;
      }

      article {
        display: table-row;
      }

      input {
        display: table-cell;
      }
    </style>
  </head>
  <body>
    <main>
      <h1><?php echo $_SESSION['h1']; ?></h1>
      <form method=post>
        <section>
          <article>
            <button name=b value=1><?php echo get_img(1); ?></button>
            <button name=b value=2><?php echo get_img(2); ?></button>
            <button name=b value=3><?php echo get_img(3); ?></button>
          </article>
          <article>
            <button name=b value=4><?php echo get_img(4); ?></button>
            <button name=b value=5><?php echo get_img(5); ?></button>
            <button name=b value=6><?php echo get_img(6); ?></button>
          </article>
          <article>
            <button name=b value=7><?php echo get_img(7); ?></button>
            <button name=b value=8><?php echo get_img(8); ?></button>
            <button name=b value=9><?php echo get_img(9); ?></button>
          </article>
        </section>
        <button name=new_game>New game</button>
      </form>
    </main>
  </body>
</html>
5.2.28.9. TicTacToe Pro
TicTacToePro1

Enhance your TicTacToe app as shown at youtu.be/fvKEeKhjCDI.

5.2.28.9.1. Solution
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
<?php
  session_start();
  define('NONE', 0);
  define('PLAYER_ONE', 1);
  define('PLAYER_TWO', 2);
  define('DRAW', 3);
  if (!isset($_SESSION['field']) || isset($_POST['new_game'])) {
    $_SESSION['field'] = [NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE];
    $_SESSION['player1'] = true;
    $_SESSION['num_empty_fields'] = 9;
    $_SESSION['status'] = NONE;
    $_SESSION['h1'] = "Player 1, your turn";
  } elseif (isset($_POST['b']) && isset($_SESSION['status']) && $_SESSION['status'] === NONE) {
    if ($_SESSION['field'][$_POST['b'] - 1] === NONE) {
      $_SESSION['field'][$_POST['b'] - 1] = $_SESSION['player1'] ? PLAYER_ONE : PLAYER_TWO;
      $_SESSION['player1'] = !$_SESSION['player1'];
      $_SESSION['num_empty_fields']--;
    }
    $_SESSION['status'] = real_game_over();
    if ($_SESSION['status'] === NONE)
      $_SESSION['h1'] = $_SESSION['player1'] ? "Player 1, your turn" : "Player 2, your turn";
    elseif ($_SESSION['status'] === PLAYER_ONE)
      $_SESSION['h1'] = "Congratulations player 1, you won!";
    elseif ($_SESSION['status'] === PLAYER_TWO)
      $_SESSION['h1'] = "Congratulations player 2, you won!";
    else $_SESSION['h1'] = "Draw";
  }

  function game_over() {
    $count = 0;
    foreach ($_SESSION['field'] as $field)
      if ($field !== NONE) $count++;
    if ($count === 9) return true;
    else return false;
  }

  function real_game_over() {
    $winners = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8],
      [2, 4, 6]];
    foreach ($winners as $winner) {
      if ($_SESSION['field'][$winner[0]] === PLAYER_ONE && $_SESSION['field'][$winner[1]] ===
        PLAYER_ONE && $_SESSION['field'][$winner[2]] === PLAYER_ONE
      ) return PLAYER_ONE;
      elseif ($_SESSION['field'][$winner[0]] === PLAYER_TWO && $_SESSION['field'][$winner[1]]
        === PLAYER_TWO && $_SESSION['field'][$winner[2]] === PLAYER_TWO
      ) return PLAYER_TWO;
    }
    if (game_over()) return DRAW;
    else return NONE;
  }

  function get_img($i) {
    if ($_SESSION['field'][$i - 1] === NONE)
      return "<img src=empty100x100.png width=100 height=100 alt=empty>";
    elseif ($_SESSION['field'][$i - 1] === PLAYER_ONE)
      return "<img src=X100x100.png width=100 height=100 alt=X>";
    else return "<img src=O100x100.png width=100 height=100 alt=O>";
  }

?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>TicTacToe Pro</title>
    <meta charset=utf-8>
    <style>
      section {
        display: table;
      }

      article {
        display: table-row;
      }

      input {
        display: table-cell;
      }
    </style>
  </head>
  <body>
    <main>
      <h1><?php echo $_SESSION['h1']; ?></h1>
      <form method=post>
        <section>
          <article>
            <button name=b value=1><?php echo get_img(1); ?></button>
            <button name=b value=2><?php echo get_img(2); ?></button>
            <button name=b value=3><?php echo get_img(3); ?></button>
          </article>
          <article>
            <button name=b value=4><?php echo get_img(4); ?></button>
            <button name=b value=5><?php echo get_img(5); ?></button>
            <button name=b value=6><?php echo get_img(6); ?></button>
          </article>
          <article>
            <button name=b value=7><?php echo get_img(7); ?></button>
            <button name=b value=8><?php echo get_img(8); ?></button>
            <button name=b value=9><?php echo get_img(9); ?></button>
          </article>
        </section>
        <button name=new_game>New game</button>
      </form>
    </main>
  </body>
</html>
5.2.28.10. WSERS1 Shop
WSERS1Shop1

Develop the single file app exactly as shown at youtu.be/zmsARb06Ths using only HTML, CSS and PHP and taking the following into account:

  1. The header shows a link to the shop if the home page is displayed, otherwise a link to the home page.

  2. The home page allows the user to enter his/her name, which is then displayed embedded in a welcome message.

  3. The shop requires the following:

    1. A two-dimensional array, in which you store a number of items as arrays. Each item has a name and a price.

    2. Function get_table, which takes a two-dimensional item array as parameter and returns a string containing the HTML table. This table includes the names and prices from the array as well as a random quantity calculated for each article from [1, 10].

5.2.28.10.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WSERS1 Shop</title>
    <meta charset=utf-8>
    <style>
      table, th, td {
        border: 1px solid black;
        border-collapse: collapse;
        padding: 5px;
      }
    </style>
  </head>
  <body>
    <header>
      <?php
        if (!isset($_GET['shop'])) echo '<a href=?shop>Shop</a>';
        else echo '<a href=?home>Home</a>';
      ?>
    </header>
    <main>
      <?php
        if (!isset($_GET['shop']))
          if (!isset($_POST['name'])) {
            echo "<form method=post>
                    <input placeholder=Name name=name>
                    <button>Submit</button>
                  </form>";
          } else echo '<h1>Welcome, ' . $_POST['name'] . ', to the WSERS1 shop</h1>';
        else {
          $articles = [['Desktop', 599.99], ['Mobile', 299.99], ['USB cam', 19.99]];

          function get_table($arr) {
            $table = '<table><tr><th>Article</th><th>Price</th><th>Quantity</th></tr>';
            foreach ($arr as $article)
              $table .= "<tr><td>{$article[0]}</td><td>{$article[1]}</td><td>" . rand(1, 10)
                . "</td></tr>";
            return $table . '</table>';
          }

          echo get_table($articles);
        }
      ?>
    </main>
  </body>
</html>
5.2.28.11. WMOTU Madhouse
WMOTUMadhouse1

Develop the single file app exactly as shown at youtu.be/VWQTI6QXCk0 using only HTML, CSS and PHP and taking the following into account:

  1. The header shows a link to the address book if the latter is not displayed.

  2. The header shows a link to the calculator if the latter is not displayed.

  3. The header shows a link to the home content if the latter is not displayed.

  4. The address book and the calculator can be displayed at the same time, in which case the address book is always displayed first.

  5. The date in the home content is shown using Luxembourg as timezone.

  6. The address table is generated from a two-dimensional array.

  7. The calculator calculates the sum of all integers from [a, b] using a function that receives a and b as parameters and returns the sum.

5.2.28.12. Operating Systems
OperatingSystems1

Write a single PHP file that performs exactly as shown at youtu.be/Dn5er8tfSKQ using only HTML, CSS and PHP.

Your HTML should be valid. The border color is greenyellow.

The script recognizes two user/password combinations: dummy1/d1pw and admin/admin. Any other combination returns to the login display.

Your PHP script defines and uses a function get_table, which takes a two-dimensional associative array as parameter. It returns the HTML table as a string.

For the admin user, the current date and time is shown below the table.

5.2.28.12.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Operating Systems</title>
    <meta charset=utf-8>
    <style>
      table, th, td {
        border:     2px greenyellow double;
        text-align: center;
      }
    </style>
  </head>
  <body>
    <main>
      <?php
        $user = false;
        if (isset($_POST['user_name'], $_POST['password'])) {
          if ($_POST['user_name'] === 'dummy1' && $_POST['password'] === 'd1pw')
            $user = 'dummy1';
          elseif ($_POST['user_name'] === 'admin' && $_POST['password'] === 'admin')
            $user = 'admin';
        }
        if ($user) {
          function get_table($OSs) {
            $output = "<table>";
            foreach ($OSs as $OS => $versions) {
              $output .= "<tr><th>$OS</th>";
              foreach ($versions as $version) $output .= "<td>$version</td>";
              $output .= '</tr>';
            }
            return $output . '</table>';
          }

          $OSs = ['Windows' => ['7', '8', '8.1', '10'],
                  'Linux'   => ['Ubuntu', 'openSUSE', 'Mint']];
          echo "<h1>Welcome $user</h1>" . get_table($OSs);
          if ($user === 'admin') {
            date_default_timezone_set('Europe/Luxembourg');
            echo '<br>Current date: ' . date('d-m-Y H:i:s');
          }
        }
        else {
          ?>
          <form method=post>
            <input placeholder='user name' name=user_name required>
            <input type=password placeholder=password name=password required>
            <button>Log in</button>
          </form>
        <?php
        }
      ?>
    </main>
  </body>
</html>
5.2.28.13. Quiz
Quiz1

Write a single PHP file that performs exactly as shown at youtu.be/eMXcLq6mdHM using only HTML, CSS and PHP.

Your HTML should be valid.

Your PHP script defines and uses the following:

  1. $questions = [['The capital of China is: ', 0], ['The value of 5^2 is:', 4]];

    The first element is the question, the second the index of the correct answer.

  2. $answers = [['Beijing', 'Nanjing', 'Luoyang', "Chang'an"], ['19', '23', '45', '67', '25']];

    These are all answers shown in the dropdown list.

  3. Function checkAnswer($idx, $answer), which returns true if the correct answer for the question with index $idx is $answer, otherwise false.

5.2.28.13.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Quiz</title>
    <meta charset="UTF-8">
  </head>
  <body>
    <?php
      $questions = [['The capital of China is: ', 0], ['The value of 5^2 is:', 4]];
      $answers = [['Beijing', 'Nanjing', 'Luoyang', "Chang'an"],
        ['19', '23', '45', '67', '25']];
      if (isset($_POST['questions_answered'], $_POST['correct_answers'], $_POST['answer'])) {
        $questions_answered = $_POST['questions_answered'];
        $correct_answers = $_POST['correct_answers'];
        $answer = $_POST['answer'];

        function checkAnswer($idx, $answer) {
          global $answers, $questions;
          if ($answers[$idx][$questions[$idx][1]] === $answer) return true;
          return false;
        }

        if (checkAnswer($questions_answered, $answer)) $correct_answers++;
      } else {
        $questions_answered = -1;
        $correct_answers = 0;
      }

      $questions_answered++;
      if ($questions_answered === 2)
        echo <<<EOT
          <h3>You scored $correct_answers out of $questions_answered.</h3>
           <form>
            <button>New game</button>
           </form>
EOT;
      else {
        echo <<<EOT
          <h3>Question </h3>
          <form method=post>
            <input type=hidden name=questions_answered value="$questions_answered">
            <input type=hidden name=correct_answers value="$correct_answers">
            {$questions[$questions_answered][0]}
            <select name=answer>
EOT;
        for ($i = 0; $i < count($answers[$questions_answered]); $i++)
          echo '<option value="' . $answers[$questions_answered][$i] . '">' .
            "{$answers[$questions_answered][$i]}</option>";
        echo "</select><button>Send</button></form>";
      }
    ?>
  </body>
</html>
5.2.28.14. Login

Create the app exactly as shown in youtu.be/lkEaFVX9NEo.

  1. Create a file functions.php. In this file:

    1. Write the PHP function check_login, which takes two parameters, the user name and the password entered by the user (valid user name/password combinations are a1/a1 and admin/admin). In this function:

      1. If the two parameters are not defined (for instance, the function was called with only one or without any parameters), return 0;

      2. Define the array $valid_logins, which contains two arrays, the first one with the values 'a1', 'a1' and 1, the second one with the values 'admin', 'admin' and 2.

      3. Run through $valid_logins and check whether the given user name and password correspond to the first two elements of one of the arrays. If this is the case, return the third value of the matching array (i.e. 1 or 2).

      4. If no matching login was found, return 0.

    2. Write the PHP function generate_welcome, which takes one parameter, a number. In this function:

      1. If the parameter is not defined or its value is less than 1 or greater than 2 return "error".

      2. Define the associative array $outputs, which contains 2 elements: the first one has key 1 and value "Welcome user a1", the second one has key 2 and as value the HTML code to generate a heading with background color gold and the text "Welcome master of the universe!".

      3. Select the output corresponding to the parameter value from $outputs and return it.

  2. Create the file index.php:

    1. The title is "Test 1: Login".

    2. The generated HTML document must obviously be valid HTML5.

    3. Include the functions file.

    4. Create, but not necessarily display, a form with two inputs, one for the user name and one for the password, as well as a login button.

    5. Check whether the login button has been pressed and the values in the two inputs correspond to a valid login (you must use function check_login for this purpose).

    6. If the check is positive, send the result of generate_welcome called with the result from check_login to the browser. Then send a new line with a link to log out.

    7. If the previous check is not positive, display the login form created in point 4.

5.2.28.14.1. Solution

functions.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
  function check_login($un, $pw) {
    if (!isset($un, $pw)) return 0;
    $valid_logins = [['a1', 'a1', 1], ['admin', 'admin', 2]];
    foreach ($valid_logins as $valid_login)
      if ($un === $valid_login[0] && $pw === $valid_login[1]) return $valid_login[2];
    return 0;
  }

  function generate_welcome($num) {
    if (!isset($num) || $num < 1 || $num > 2) return 'error';
    $outputs = [1 => "Welcome user a1",
      2 => "<h1 style='background-color: gold'>Welcome master of the  universe!</h1>"];
    return $outputs[$num];
  }

?>

index.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>Login</title>
  </head>
  <body>
    <?php
      require_once 'functions.php';
      if (isset($_POST['login']) && check_login($_POST['un'], $_POST['pw']) > 0) {
        echo generate_welcome(check_login($_POST['un'], $_POST['pw']));
        echo '<br><a href="">Logout</a>';
      } else echo "<form method=post><input name=un placeholder='user name' required>" .
          "<input type=password name=pw placeholder=password required>" .
          "<button name=login>Login</button></form>";
    ?>
  </body>
</html>

index.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>Gradiator</title>
  </head>
  <body>
    <form method=post>
      <input type=radio name=action value=radial checked>Radial gradient<br>
      <input type=radio name=action value=linear>Linear gradient<br>
      Number of stops: <input type=range name=stops min=2 max=10 value=6 step=1><br>
      <button name=submit>Submit</button>
    </form>
    <?php
      require_once 'function.php';
      if (isset($_POST['submit'])) echo get_gradiant_JS($_POST['action'], $_POST['stops']);
    ?>
  </body>
</html>
5.2.28.15. Computer Shop

Create the single file app exactly as shown in youtu.be/dtIWHlG8joU.

  1. The title is "Computer Shop".

  2. The generated HTML document must obviously be valid HTML5.

  3. If the user has selected something from the drop down list, the text "Congratulations, you bought " followed by the the article name followed by a "." followed, in the next line, by the "Return" button are displayed.

  4. Otherwise, the form with the selection drop down list is displayed. To do this:

    1. Create one 2-dimensional array with the three keys "PC", "Laptop" and "Tablet" and the associated values "Mega PC1" and "Mega PC2" for the first key, "Mega Lap1" and "Mega Lap2" for the second as well as "Mega Tab1" and "Mega Tab2" for the third.

    2. The keys are used to generate the option groups. The values are used as the option text.

    3. Do not forget the "Buy" button.

5.2.28.15.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Computer Shop</title>
  </head>
  <body>
    <?php
      if (isset($_POST['selection'])) {
        echo "Congratulations, you bought {$_POST['selection']}.";
        echo "<br><a href=>Return</a>";
      } else {
        echo "<form method=post><select name=selection>";
        $opts = ['PC'     => ['Mega PC1', 'Mega PC2'],
                 'Laptop' => ['Mega Lap1', 'Mega Lap2'],
                 'Tablet' => ['Mega Tab1', 'Mega Tab2']];
        foreach ($opts as $opt_key => $opt_values) {
          echo "<optgroup label=$opt_key>";
          foreach ($opt_values as $opt) echo "<option>$opt</option>";
          echo "</optgroup>";
        }
        echo "</select><button>Buy</button></form>";
      }
    ?>
  </body>
</html>
5.2.28.16. Simple Calculator

In this test, you will create the single file app exactly as shown in www.youtube.com/watch?v=zTiS1Rnh9is.

  1. The title is "Simple Calculator".

  2. The generated HTML document must obviously be valid HTML5 and your PHP script may not generate any messages in your PHP log file.

  3. Create the following PHP array:

    $arr = ['bg-color' => ['black', 'green', 'blue', 'red'],
            'color'    => ['white', 'brown', 'cyan', 'maroon']];
  4. Set the document background and font colors by selecting randomly from the corresponding arrays in $arr.

  5. Create the PHP function calc($x, $y, $op), which does the following:

    1. Check if $x and $y are numbers and if $op is one of '+', '-', '*' or '/'. If not, return the empty string.

    2. Return the result of $x $op $y (use the eval function and check out the example in the book). Example, if $x is 3, $y is 4 and $op is '-', the function returns -1.

  6. The two inputs in the form allow only numbers to be entered and require input.

  7. If the form has been submitted, the text 'The result of ' followed by $x $op $y followed by ' is ' followed by the result from the calc function for the submitted parameters is displayed above the form.

5.2.28.16.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
  $arr = ['bg-color' => ['black', 'green', 'blue', 'red'],
          'color'    => ['white', 'brown', 'cyan', 'maroon']];
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>Simple Calculator</title>
    <style>
      body {
        background-color: <?php echo $arr['bg-color'][rand(0, 3)]?>;
        color: <?php echo $arr['color'][rand(0, 3)]?>;
      }
    </style>
  </head>
  <body>
    <?php
      //echo '<script>document.body.style.backgroundColor="' . $arr['bg-color'][rand(0, 3)] .
      //  '";document.body.style.color="' . $arr['color'][rand(0, 3)] . '";</script>';

      function calc($x, $y, $op) {
        if (!is_numeric($x) || !is_numeric($y) || !in_array($op, ['+', '-', '*', '/']))
          return '';
        return eval('return ' . $x . $op . $y . ';');
      }

      if (isset($_POST['x'])) echo "The result of " . $_POST['x'] . " " . $_POST['op'] .
        " " . $_POST['y'] . " is " . calc($_POST['x'], $_POST['y'], $_POST['op']);
    ?>
    <form method=post>
      x: <input type=number name=x title=x required>
      <select name=op title=operator>
        <option value=+>+</option>
        <option value=->-</option>
        <option value=*>*</option>
        <option value='/'>/</option>
      </select>
      y: <input type=number name=y title=y required>
      <button>Calc</button>
    </form>
  </body>
</html>

5.3. MySQL

5.3.1. When and where do we need a database?

For a comparison of MySQL with some other database management systems, see db-engines.com/en/system/CouchDB%3BMongoDB%3BMySQL.

For a detailed comparison of MySQL with MariaDB, see hackr.io/blog/mariadb-vs-mysql.

5.3.2. Structured data

By structured data we mean data that has a defined format and maximum size. Traditionally these have been for instance numbers, dates or strings. Increasingly we see machine-generated structured data as described in www.dummies.com/programming/big-data/engineering/structured-data-in-a-big-data-environment.

5.3.2.1. Personal data
'Personal data’ means any information relating to an identified or identifiable natural person (‘data subject’); an identifiable natural person is one who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number, location data, an online identifier or to one or more factors specific to the physical, physiological, genetic, mental, economic, cultural or social identity of that natural person;

The main legal base for storing data in the EU is the General Data Protection Regulation (GDPR) which is explained by Wikipedia. The European Commission provides another great resource. In Luxembourg, the Commission Nationale pour la Protection des Données (CNPD) provides detailed information.

You are obliged to inform your visitors about their privacy rights. To do this, link them to www.knowyourprivacyrights.org.

5.3.4. Introduction to MySQL

According to www.mysql.com/products, MySQL is the world’s most popular open source database. We will use it to store our web app’s data.

If we add a database to the picture we discussed in section Installing and configuring the tools, we get the following:

internetDB

MySQL is a program that runs on the server and specializes in storing, retrieving and manipulating large amounts of data in a structured and highly efficient way.

SQL stands for Structured Query Language. It is the standard language that has been used for decades to manage databases.

Two very good online SQL learning resources can be found at www.tutorialspoint.com/sql/sql-overview.htm and sqlzoo.net.
Remember to use some of the excellent cheat sheets on the web, for instance www.cheatography.com/guslong/cheat-sheets/essential-mysql or www.cheatography.com/davechild/cheat-sheets/mysql.

Data is organized in databases. Each database consists of one or more tables. A table contains data for a specific purpose, for instance addresses. A table is organized in rows and columns. Each column represents a specific data field, e.g. last name. Rows contain data. If we have 10 addresses stored in our address table, there’ll be 10 rows, each row containing the address of one person. For each row we can access a specific field, e.g. street.

Here is a sample table for the top 10 clubs in the German Bundesliga (not up to date!):

Rank Club Points

1

Bayern München

23

2

Dortmund

22

3

Leverkusen

22

4

Berlin

15

5

Schalke 04

14

6

M’Gladbach

13

7

Hannover 96

13

8

Bremen

12

9

Stuttgart

11

10

Hoffenheim

10

5.3.5. Data types

For each column, we need to specify the type of data we want to store. For instance, in the sample table above, the first and third columns need to be numbers, whereas the second is used for text. A detailed overview of the data types available in MySQL can be found at dev.mysql.com/doc/refman/5.7/en/data-types.html.

The main ones that we’ll use are listed in the following table:

Data type Meaning Example

VARCHAR

string up to specified length (max. 65535)

VARCHAR(50)

INT

integer

INT

DECIMAL

decimal with given precision

DECIMAL(5, 2)

TIMESTAMP

date and time

TIMESTAMP

DATETIME

date and time

DATETIME

DATE

date

DATE

TEXT

string up to different sizes

TEXT

BLOB

binary large object

BLOB

ENUM

string from a given list

ENUM('Donald Duck', 'Asterix', 'Mickey Mouse')

SET

string object with zero or more values from a list of permitted values

SET('BMW', 'Nissan')

BOOL

boolean

BOOL

JSON

JSON

JSON

A string is a sequence of bytes or characters, enclosed within either single quote (') or double quote (") characters. Examples:

'a string'

"another string"

Quoted strings placed next to each other are concatenated to a single string. The following lines are equivalent:

'a string'

'a' ' ' 'string'

TIMESTAMP and DATETIME values are in the format 'YYYY-MM-DD HH:MM:SS', for instance '2014-08-11 15:04:00'. The supported range is '1970-01-01 00:00:01.000000' to '2038-01-19 03:14:07.999999' for TIMESTAMP and '1000-01-01 00:00:00' to '9999-12-31 23:59:59' for DATETIME. For a TIMESTAMP and (starting from MySQL 5.6.5) DATETIME column we can specify DEFAULT CURRENT_TIMESTAMP, in which case the current timestamp is automatically set for inserted rows that specify no value for the column. We can also set ON UPDATE CURRENT_TIMESTAMP to have the date and time automatically updated when the value of any other column in the row is changed (cf. dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html).

When inserting SET values into a table, there should be no spaces between values, i.e. use 'val1,val2,val3' instead of 'val1, val2, val3'.

5.3.6. Case sensitivity

SQL keywords, function names, column and index names are not case sensitive. Database, table and view names however are represented using directories and files in the underlying filesystem. Whether they are case sensitive or not depends therefore on the operating system.

5.3.7. Comments

MySQL supports three comment styles which are very well illustrated at dev.mysql.com/doc/refman/5.7/en/comments.html.

1
2
3
4
5
6
# This comment goes until the end of the line.
-- So does this comment.
SELECT dtFirstName, dtLastName /* inline comment */ FROM tblT1;
/* Multiline
comment
 */

5.3.8. Naming conventions

Item Example(s)

Database

dbShop

Table

tblUser, tblLoggedInUser

Primary key

idUser

Data field

dtName

Foreign key

fiUser referencing tblUser.idUser

Table names start with the prefix tbl and are always singular, i.e. tblUsers should be avoided.

5.3.9. Character sets and collations

A character set is a set of symbols and encodings. A collation is a set of rules for comparing characters in a character set. Let’s make the distinction clear with an example of an imaginary character set.

Suppose that we have an alphabet with four letters: A, B, a, b. We give each letter a number: A = 0, B = 1, a = 2, b = 3. The letter A is a symbol, the number 0 is the encoding for A, and the combination of all four letters and their encodings is a character set.

Suppose that we want to compare two string values, A and B. The simplest way to do this is to look at the encodings: 0 for A and 1 for B. Because 0 is less than 1, we say A is less than B. What we’ve just done is apply a collation to our character set. The collation is a set of rules (only one rule in this case): “compare the encodings.” We call this simplest of all possible collations a binary collation.

But what if we want to say that the lowercase and uppercase letters are equivalent? Then we would have at least two rules: (1) treat the lowercase letters a and b as equivalent to A and B; (2) then compare the encodings. We call this a case-insensitive collation. It is a little more complex than a binary collation.

In real life, most character sets have many characters: not just A and B but whole alphabets, sometimes multiple alphabets or eastern writing systems with thousands of characters, along with many special symbols and punctuation marks. Also in real life, most collations have many rules, not just for whether to distinguish lettercase, but also for whether to distinguish accents (an “accent” is a mark attached to a character as in German Ö), and for multiple-character mappings (such as the rule that Ö = OE in one of the two German collations).

For our purposes we always use utf8mb4 as character set and utf8mb4_bin as collation type (cf. www.eversql.com/mysql-utf8-vs-utf8mb4-whats-the-difference-between-utf8-and-utf8mb4).

There are default settings for character sets and collations at four levels: server, database, table, and column. Usually we do not specify the character set and collation at column level, which means that the table settings will be used.

5.3.11. Creating and dropping databases and tables

The detailed MySQL syntax to create and drop a database and a table can be found here:

Here is a simple application example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Drop database evegi144_dbDemo1, if it already exists.
DROP DATABASE IF EXISTS evegi144_dbDemo1;

# Create new DB named evegi144_dbDemo1.
CREATE DATABASE evegi144_dbDemo1
  # Avoid problems with é, ö etc.
  DEFAULT CHARSET utf8mb4
  # Use case sensitivity.
  DEFAULT COLLATE utf8mb4_bin;

# If we want to create a user
CREATE USER evegi144_dbDemo1@localhost IDENTIFIED BY 'xxx';
# If you want to change the password validation configuration, see
# https://stackoverflow.com/questions/43094726/your-password-does-not-satisfy-the-current-policy-requirements

# If we want to give another user access to the DB.
GRANT ALL PRIVILEGES ON evegi144_dbDemo1.* TO 'evegi144'@'localhost';
FLUSH PRIVILEGES;

# First select the DB that we want to use.
USE evegi144_dbDemo1;

# Drop table tblT1 if it already exists.
DROP TABLE IF EXISTS tblT1;

# Create table tblT1 with 8 columns.
CREATE TABLE tblT1 (
  idT1                INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  dtFirstName         VARCHAR(20) NOT NULL,
  dtLastName          VARCHAR(20) NOT NULL,
  dtBirthDate         DATE,
  dtCreationTimestamp TIMESTAMP                DEFAULT CURRENT_TIMESTAMP,
  dtAverage           DECIMAL(4, 2),
  dtNumberOfGrades    INT UNSIGNED,
  dtModules           SET('HTSTA', 'CLISS1', 'CLISS2', 'WSERS1', 'WSERS2', 'WEBAP1',
                          'WEBAP2')
)
  ENGINE = INNODB # We use INNODB as the DB engine.
  DEFAULT CHARSET utf8mb4 # Same as above
  DEFAULT COLLATE utf8mb4_bin;

# Insert a row of data into the table.
INSERT INTO tblT1 (dtFirstName, dtLastName, dtBirthDate, dtAverage, dtNumberOfGrades,
                   dtModules)
VALUES
  ('Mickey', 'Mouse', '1927-07-03', 23.45, 15, 'HTSTA,CLISS1,CLISS2'),
  ('Donald', 'Duck', '1949-02-04', 33.55, 12, 'WSERS1,WSERS2');

We use the INNODB storage engine. From dev.mysql.com/doc/refman/5.7/en/storage-engines.html:

Storage engines are MySQL components that handle the SQL operations for different table types. InnoDB is the most general-purpose storage engine, and Oracle recommends using it for tables except for specialized use cases.

We set the default character set to be UTF8 in order to avoid any problems with storing é, ö etc. We also set collation to utf8_bin in order to have case sensitivity. If we skip this, the DB will treat the strings home, Home and HOME as identical.

In a production environment, you should create one or several specific DB users for your app that have only the minimum privileges required. Given that you cannot do this on Foxi, we won’t go into detail here, but take a look at dev.mysql.com/doc/refman/5.7/en/create-user.html and dev.mysql.com/doc/refman/5.7/en/grant.html.

There are several ways to create and work with a database.

5.3.11.1. Via PhpStorm

The recommended approach is to write a SQL file that creates the DB using SQL statements. This allows us to easily review the database structure that we have created, make modifications and recreate the database.

If we use PhpStorm, we can configure a data source when we create a new file with extension .sql:

createDB1

To configure the PhpStorm DB connection, see PhpStorm DB setup.

If you cannot see the database window, you can open it like this:

createDB5

We can run our SQL script to create the DB and tables:

createDB6

Don’t forget to click DBSynchronize to update the DB display.

We can also add a table using the PhpStorm wizard:

createDBTable1
createDBTable2
5.3.11.2. Via the MySQL command line

If you have the option to log into your web server (as you do with Foxi) you can create and manage your database via the MySQL command line. Before you do so, it is important to set the system variables with respect to the character set and collation correctly, otherwise you are asking for trouble. Here’s why:

createDB7a

As system administrator you could set these settings in the MySQL configuration file, like so (cf. stackoverflow.com/questions/3513773/change-mysql-default-character-set-to-utf-$):

[mysqld]
collation-server = utf8mb4_bin
init-connect='SET NAMES utf8mb4'
character-set-server = utf8mb4

But often you won’t have admin rights on the host machine, so you’ll need to run the following script after logging in to MySQL and before creating or manipulating any structures or data (cf. dev.mysql.com/doc/refman/5.7/en/server-system-variables.html and dev.mysql.com/doc/refman/5.7/en/using-system-variables.html):

1
2
3
4
5
6
7
8
SET character_set_client=utf8mb4;
SET character_set_connection=utf8mb4;
SET character_set_database=utf8mb4;
SET character_set_results=utf8mb4;
SET character_set_server=utf8mb4;
SET collation_connection=utf8mb4_bin;
SET collation_database=utf8mb4_bin;
SET collation_server=utf8mb4_bin;
createDB7c

Now you can work on your structures and data. Here is an example:

createDB7

If you have created a SQL file you can then simply execute it:

mysql1

Note that before you can work with a database, you need to select it as the active database using the use command.

5.3.11.3. Via phpMyAdmin
createDB8
mysql2
mysql3

5.3.12. Work flow in PHP

DBflow

5.3.13. Connecting to and disconnecting from a database via PHP

In Installing and configuring the tools we saw how to install and configure MySQL. Now we’ll see how we can connect to an existing database (DB) via PHP (cf. php.net/manual/en/mysqli.construct.php). We use the MySQL Improved Extension (mysqli) to access a MySQL DB. A great summary of the MySQLi extension can be found at php.net/manual/en/mysqli.summary.php.

First we define the constants that we need to connect to the database and put them into a file that we can include:

1
2
3
4
5
6
<?php
  const DB_HOST = 'localhost';
  const DB_USER = 'your_user_name';
  const DB_PW   = 'your_password';
  const DB_NAME = 'your_database';
?>

Note that this is safe, given that PHP code gets executed on the server and in this case only an empty document is sent to the browser as you can easily verify by clicking the link.

Now we will connect to our DB using PHP.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
  require_once 'db_credentials.php'; // Import connection details.
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME); // Connect to DB.
  // Stop script with error message if connection fails. Don't do this in production version!
  if ($dbc->connect_error) die("Database connection failed: $dbc->connect_error");
  $dbc->set_charset("utf8mb4");
  echo '<pre>' . print_r($dbc, true) . '</pre>';
  echo '<strong>mysqli class methods:</strong>';
  echo '<pre>' . print_r(get_class_methods('mysqli'), true) . '</pre>';
  $dbc->close(); // Close the DB connection.
?>

$dbc is a mysqli object, containing all the data fields and methods we need to work with our DB (cf. www.php.net/manual/en/class.mysqli.php). It is essential to become familiar with a number of these fields and methods, which we’ll use in many of our DB operations. In particular:

  • connect_errno contains the error code for the last connect call.

  • connect_error contains a string description of the last connect error.

  • errno contains the error code for the last function call.

  • error contains a string description of the last error.

  • close closes a previously opened DB connection.

  • prepare prepares an SQL statement for execution. The parameter markers must be bound to application variables using the bind_param method of the stmt class before executing the statement.

    There are four possible data types:

    Character Data type

    i

    integer

    d

    double

    s

    string

    b

    blob (cf. php.net/manual/en/mysqli-stmt.send-long-data.php)

    The resulting data can be bound to specific variables and then retrieved using bind_result and fetch or retrieved using the result object with get_result and then using one of the fetch functions described below.

    We’ll look at practical examples in the following sections.

  • query performs a query on the DB. Returns FALSE on failure. For successful SELECT, SHOW, DESCRIBE or EXPLAIN queries it returns a mysqli_result object, for other successful queries TRUE.

  • escape_string escapes special characters in a string for use in an SQL statement, taking into account the current charset of the connection. However, _ and % are not escaped, although they have special meaning in LIKE clauses. It is therefore safer to use prepared statements, which will be our preferred approach in the following sections.

  • set_charset sets the default client character set. We use utf8mb4.

5.3.14. SQL injection

We need to distinguish between queries that use data provided by the user and those that use internal data. In the first case we need to sanitize user input or use prepared statements to avoid hijacking of our database, which is a major threat to our web app (cf. en.wikipedia.org/wiki/SQL_injection).

Why? Let’s look at an example provided by Robin Nixon in his book "PHP, MySQL, JavaScript, & CSS" on page 249:

Suppose you have a simple piece of code to verify a user, and it looks like this:

1
2
3
$user  = $_POST['user'];
$password  = $_POST['password'];
$query = "SELECT * FROM tblUser WHERE dtUser='$user' AND dtPassword='$password'";

At first glance, you might think this code is perfectly fine. But what if someone enters the following for $user (and doesn’t enter anything for $password)?

admin' #

Let’s look at the string that would be sent to MySQL:

SELECT * FROM tblUser WHERE dtUser='admin' #' AND dtPassword=''

Do you see the problem? In MySQL, the # symbol represents the start of a comment. Therefore, the user will be logged in as admin (assuming there is a user admin), without having to enter a password.

What about the case in which your application code removes a user from the database? The code might look something like this:

$user  = $_POST['user'];
$password  = $_POST['password'];
$query = "DELETE FROM tblUser WHERE dtUser='$user' AND dtPassword='$password'";

Again, this looks quite normal at first glance, but what if someone entered the following for $user?

anything' OR 1=1 #

MySQL would interpret this as the following:

DELETE FROM tblUser WHERE dtUser='anything' OR 1=1 #' AND dtPassword=''

Ouch - that SQL query will always be true, and therefore you’ve lost all your users table data!

I highly recommend studying this and this.

Let’s look at a working example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# First select the DB that we want to use.
USE evegi144_dbDemo1;

# Drop table tblHijack if it already exists.
DROP TABLE IF EXISTS tblHijack1;

# Create table tblHijack1.
CREATE TABLE tblHijack1 (
  idHijack1  INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  dtUserName VARCHAR(20),
  dtPassword VARCHAR(20)
)
  ENGINE = INNODB
  DEFAULT CHARSET utf8mb4
  DEFAULT COLLATE utf8mb4_bin;

# Insert a row of data into the table.
INSERT INTO tblHijack1 (dtUserName, dtPassword) VALUES
  ('Mickey', '123'), ('Donald', 'abc');
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
  if (!isset($_POST['user'], $_POST['pw'])) {
    echo '<form method=post><input placeholder=user name=user required>
                            <input placeholder=password name=pw required>
                            <button name=login>Login</button>
                            <button name=delete>Delete</button></form>';
  } else {
    require_once 'db_credentials.php';
    $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME) or
    die('Connect Error (' . $dbc->connect_errno . ') ' . $dbc->connect_error);
    $dbc->set_charset('utf8mb4');

    $query = "INSERT INTO tblHijack1 (dtUserName, dtPassword) VALUES
            ('Mickey', '123'), ('Donald', 'abc')";
    echo $query . '<br>';
    $result = $dbc->query($query) or die($dbc->errno . ':' . $dbc->error);

    $query = "SELECT * FROM tblHijack1";
    echo $query . '<br>';
    $result = $dbc->query($query) or die($dbc->errno . ':' . $dbc->error);
    $rows = $result->fetch_all();
    echo '<pre>' . print_r($rows, true) . '</pre>';

    if (isset($_POST['login'])) {
      // User name and password provided by user
      $user_name = $_POST['user']; // "Donald'#"
      $password  = $_POST['pw']; // ''
      $query     = "SELECT * FROM tblHijack1 WHERE dtUserName = '$user_name' AND dtPassword
                  = '$password'";
      echo $query . '<br>';
      $result = $dbc->query($query) or die($dbc->errno . ':' . $dbc->error);
      echo 'Number of records returned: ' . $result->num_rows . '<br>';
      if ($result->num_rows >= 1) echo "You're logged in!";
    } elseif (isset($_POST['delete'])) {
      $user_name = $_POST['user']; // "' or 1=1#"
      $password  = $_POST['pw']; // ''
      $query     = "DELETE FROM tblHijack1 WHERE dtUserName = '$user_name' AND dtPassword = "
        . "'$password'";
      echo $query . '<br>';
      $dbc->query($query) or die($dbc->errno . ':' . $dbc->error);

      $query = "SELECT * FROM tblHijack1";
      echo $query . '<br>';
      $result = $dbc->query($query) or die($dbc->errno . ':' . $dbc->error);
      $rows = $result->fetch_all();
      echo '<pre>' . print_r($rows, true) . '</pre>';
    }

    $dbc->close(); # Close the DB connection.
  }
?> 

Check out www.hacksplaining.com/exercises/sql-injection for an excellent tutorial.

To sanitize user input, we can call the escape_string function mentioned above, like this:

$user  = $dbc->escape_string($_POST['user']);

This approach suffers from several major security problems:

  1. This does not escape _ and %.

  2. Numeric user provided data is not enclosed in quotation marks, in which case escaping won’t prevent SQL injection.

  3. Storing escaped data in the DB, reading it back and then storing it again will remove the escaping.

If we also want to remove HTML and PHP tags and slashes, we can write a function that we call for every input:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
  function sanitize_string($dbc, $string) {
    $string = strip_tags(trim($string)); # Remove HTML and PHP tags from string.
    /* If slashes have been added automatically, remove them.
       Starting from PHP 5.4 this is not necessary anymore
       (cf. http://php.net/manual/en/function.get-magic-quotes-gpc.php)
    if (get_magic_quotes_gpc()) $string = stripslashes($string); */
    $result = $dbc->escape_string($string); # Escape special characters.
    return $result;
  }
?>

To avoid the problem of questionable user input we could force the user to enter only alphanumeric characters using ctype_alnum:

if (!ctype_alnum($username)) die('Invalid characters in Username');

The preferred way is to use prepared statements (cf. en.wikipedia.org/wiki/Prepared_statement):

Prepared statements are resilient against SQL injection, because parameter values, which are transmitted later using a different protocol, need not be correctly escaped.

Prepared statements separate query structure from data. Given that user data has no influence on query structure, this is the safest approach. A good article on the subject can be found at websitebeaver.com/prepared-statements-in-php-mysqli-to-prevent-sql-injection.

5.3.15. Retrieving data

In SQL we retrieve data from our DB using the SELECT statement. A straightforward explanation can be found at www.w3schools.com/sql/sql_select.asp. The gory details are at dev.mysql.com/doc/refman/5.7/en/select.html. We can specify columns for which we want to retrieve data or use * to indicate that we want data for all columns.

We can retrieve data for all rows or only for those rows that fulfill conditions. The conditions are specified using the WHERE clause. We can even use regular expressions, which are explained in detail at dev.mysql.com/doc/refman/5.7/en/regexp.html.

Let’s look at a few SQL query examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
SELECT * FROM evegi144_dbDemo1.tblT1; # Select all columns from the tblT1 table of the demo1 DB.
USE evegi144_dbDemo1; # Select the demo1 DB as default.
SELECT dtLastName FROM tblT1; # Select the last name from table tblT1 of the currently used DB.
# Get all first names where last name is Gates or Torvalds
SELECT dtFirstName FROM tblT1 WHERE dtLastName = 'Gates' OR dtLastName = 'Torvalds';
# Get all first names where last name is ate.
SELECT dtFirstName FROM tblT1 WHERE dtLastName LIKE 'ate';
# Get all first names where last name contains the word ate.
SELECT dtFirstName FROM tblT1 WHERE dtLastName LIKE '%ate%';
# Get all first names where last name starts with G and ends with s.
SELECT dtFirstName FROM tblT1 WHERE dtLastName REGEXP '^G.*s$';
# Get all data ordered by last name in descending order.
SELECT * FROM tblT1 ORDER BY dtLastName DESC;

How do we execute SQL queries in PHP? We can use the object-oriented or the procedural approach. In the former we work with objects, such as a DB connection or result set and use a property or call a method (function) of this object using , for instance $result→num_rows or $dbc→query($query). The procedural approach requires that we call the corresponding mysqli function and provide the variable to be used. Thus we need to write mysqli_num_rows($result) or mysqli_query($dbc, $query).

5.3.15.1. With internal data

With internal data we do not need to take special precautions to avoid DB hijacking although the safest approach would be to always use prepared statements, given that we cannot be sure that the data was not processed in some unexpected way.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
  require_once 'db_credentials.php'; # Include the DB connection credentials.
  /* Create a new DB connection. We need to store the connection as we will need it for all DB
    operations. If something goes wrong, $dbc will be null and we display the
    connection error number as well as the error text and terminate the script. */
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME);
  if ($dbc->connect_error) die("Database connection failed: $dbc->connect_error");
  $dbc->set_charset('utf8mb4'); # Specify the character set to be used for our queries.
  $query  = 'SELECT * FROM tblT1'; # Our query

  /* Execute the SQL query. To do this we need to provide the query.
     If something goes wrong, we display the error message and terminate the script.
     If the query executed correctly, we get a result object which we store in $result. */
  $result = $dbc->query($query); # Store the result object.
  if (!$result) die("Wrong SQL: $query Error: $dbc->error");

  # Take a look at the result object.
  echo '<pre>' . print_r($result, true) . '</pre>';

  # Display the number of records returned by the query.
  echo '<br>Number of rows: ' . $result->num_rows;

  /* Retrieve each record one by one. fetch_assoc returns the next record until we
     have reached the last one. We append each record to the $all_rows array. */
  while ($row = $result->fetch_assoc()) $rows[] = $row;

  # Let's take a look at the result array.
  echo '<pre>' . print_r($rows, true) . '</pre>';

  /* Since PHP 5.4 the mysqli result object supports iteration (cf.
     https://secure.php.net/manual/en/class.mysqli-result.php) which makes our life easier: */
  foreach ($result as $row) echo '<pre>' . print_r($row, true) . '</pre>';

  if ($result->data_seek(0)) // Set the result pointer back to the beginning.
    while ($row = $result->fetch_array())
      echo '<pre>' . print_r($row, true) . '</pre>';

  echo '<pre>' . print_r($dbc, true) . '</pre>';
  $result->free(); # Free the memory used by the result object.
  $dbc->close(); # Close the DB connection.
?>

First we establish a DB connection and set the character set. Then we define our query and execute it using the query method. This method returns FALSE if something went wrong, otherwise TRUE or the result object of the query, depending on the type of query. The result object has several interesting properties and methods.

For our purposes, the following property and methods are particularly useful:

Property/method Description

num_rows

Number of rows returned by query.

fetch_row

Fetch current result row as a numeric array.

fetch_assoc

Fetch current result row as an associative array.

fetch_array

Fetch current result row as an associative, a numeric array, or both.

fetch_all

Fetch all result rows as an associative array, a numeric array, or both.

fetch_object

Fetch current result row as a PHP object.

Since PHP 5.4 the mysqli result object supports iteration which makes our life easier (see code above).
The result set has an internal pointer representing the current row. Every call to a fetch method increases this internal pointer. Thus, every fetch will return the next row until the end of the result set has been reached. If we want to retrieve a different row, we can use the data_seek method to move the pointer.

To finish the job we free the memory of the result object using its free method and we close the DB connection.

fetch_array returns an array that corresponds to the fetched row or NULL if there are no more rows for the result set.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
  require_once 'db_credentials.php';
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    $dbc->connect_errno . ') ' . $dbc->connect_error);
  $dbc->set_charset('utf8mb4');

  $query = 'SELECT * FROM tblT1';
  $result   = $dbc->query($query);
  if (!$result) die("Wrong SQL: $query Error: $dbc->error");
  while ($row = $result->fetch_array(MYSQLI_ASSOC)) $rows[] = $row;
  echo '<pre>' . print_r($rows, true) . '</pre>';
  $result->free();
  $dbc->close();
?>

fetch_all returns an array of associative or numeric arrays holding result rows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
  require_once 'db_credentials.php';
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    $dbc->connect_errno . ') ' . $dbc->connect_error);
  $dbc->set_charset('utf8mb4');

  $query = 'SELECT * FROM tblT1';
  $result   = $dbc->query($query);
  if (!$result) die("Wrong SQL: $query Error: $dbc->error");
  $rows = $result->fetch_all();
  echo '<pre>' . print_r($rows, true) . '</pre>';
  $result->free();
  $dbc->close();
?>
5.3.15.2. With user provided data

If some of the data used to query the DB is user provided, we either need to sanitize it ourselves or use prepared statements.

5.3.15.2.1. Using escape_string

All we have to do is run the user provided inputs through escape_string, like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
  require_once 'db_credentials.php'; # Include constants for host, user, pw and DB.
  # Create a new DB connection.
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    $dbc->connect_errno . ') ' . $dbc->connect_error);
  $dbc->set_charset('utf8mb4'); # Specify the character set to be used for our queries.

  $last_name = "Duck";
  $last_name = $dbc->escape_string($last_name);
  $query     = "SELECT dtFirstName, dtLastName FROM tblT1 WHERE dtLastName = '$last_name'"; # query
  $result = $dbc->query($query) or die("Wrong SQL: $query Error: $dbc->error");
  echo 'Number of rows ' . $result->num_rows . '<br>'; # Display the number of rows.
  # Loop through the result object and display each first name.
  while ($row = $result->fetch_array()) printf("%s %s<br>", $row[0], $row[1]);
  $dbc->close(); # Close the DB connection.
?>

Keep in mind that escaping is not fully secure.

5.3.15.2.2. Using prepared statements
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
  require_once 'db_credentials.php';
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    $dbc->connect_errno . ') ' . $dbc->connect_error);
  $dbc->set_charset('utf8mb4');

  $query = 'SELECT dtFirstName, dtLastName FROM tblT1 WHERE dtLastName = ?'; # our query
  $stmt  = $dbc->prepare($query); # Prepare query and store statement object in $stmt.
  if (!$stmt) die('Wrong SQL: ' . $query . ' Error: ' . $dbc->error); # If query preparation failed, display error and stop script.
  echo '<pre>' . print_r($stmt, true) . '</pre>';

  $last_name = 'Gates'; # Save the last name that we want to use in our query in a variable.
  # Bind the parameter to our query as a string.
  $stmt->bind_param('s', $last_name) or die('Bind failure: ' . $stmt->error);
  $stmt->execute() or die($stmt->error);
  $result = $stmt->get_result();
  if ($result) {
    echo 'Result object properties:<br>';
    echo '<pre>' . print_r($result, true) . '</pre>';
    echo 'Result object methods:<br>';
    //echo '<pre>' . print_r(get_class_vars(get_class($result)), true) . '</pre>';
    echo '<pre>' . print_r(get_class_methods(get_class($result)), true) . '</pre>';
    echo 'Number of rows: ' . $result->num_rows;
    echo '<pre>' . print_r($result->fetch_all(), true) . '</pre>';
  }
  $stmt->close();
  $dbc->close();
?>

prepare returns a prepared statement object or FALSE if something went wrong. See www.php.net/manual/en/class.mysqli-stmt.php.

store_result returns a buffered result object or FALSE if something went wrong. To see what this object looks like, go to php.net/manual/en/class.mysqli-result.php. Once we have a result object, we can for instance figure out how many rows have been retrieved.

With bind_result we can bind columns in the result set to variables. To free the resources used for our query, we close the prepared statement and the DB connection.

get_result returns a result set from a prepared statement.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
  require_once 'db_credentials.php';
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    $dbc->connect_errno . ') ' . $dbc->connect_error);
  $dbc->set_charset('utf8mb4');

  $query = 'SELECT dtFirstName, dtLastName FROM tblT1 WHERE dtLastName = ?'; # our query
  $stmt  = $dbc->prepare($query); # Prepare query and store statement object in $stmt.
  if (!$stmt) die('Wrong SQL: ' . $query . ' Error: ' . $dbc->error); # If query preparation failed, display error and stop script.
  echo '<pre>' . print_r($stmt, true) . '</pre>';

  $last_name = 'Gates'; # Save the last name that we want to use in our query in a variable.
  # Bind the parameter to our query as a string.
  $stmt->bind_param('s', $last_name) or die('Bind failure: ' . $stmt->error);
  $stmt->execute() or die($stmt->error);
  $result = $stmt->get_result();
  if ($result)
    while ($row = $result->fetch_array(MYSQLI_NUM))
      printf("%s %s<br>", $row[0], $row[1]);
  $stmt->close();
  $dbc->close();
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
  require_once 'db_credentials.php'; # Include constants for host, user, pw and DB.
  # Create a new DB connection.
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    $dbc->connect_errno . ') ' . $dbc->connect_error);
  $dbc->set_charset('utf8mb4'); # Specify the character set to be used for our queries.

  $query = 'SELECT dtFirstName, dtLastName FROM tblT1 WHERE dtLastName = ?'; # our query
  $stmt  = $dbc->prepare($query); # Prepare query and store statement object in $stmt.
  # If query preparation failed, display error and stop script.
  if (!$stmt) die("Wrong SQL: $query Error: $dbc->error");
  echo '<pre>' . print_r($stmt, true) . '</pre>';

  $last_name = 'Gates'; # Save the last name that we want to use in our query in a variable.
  # Bind the parameter to our query as a string.
  $stmt->bind_param('s', $last_name) or die('Bind failure: ' . $stmt->error);
  $stmt->execute() or die($stmt->error); # Execute query and stop if failure.
  $stmt->store_result() or die($stmt->error); # Store the result object.
  echo 'Number of rows ' . $stmt->num_rows . '<br>'; # Display the number of rows.
  $stmt->bind_result($result[0], $result[1]) or die($stmt->error); # Bind the query result to variables.
  # Loop through the result object and display each first name.
  while ($stmt->fetch()) printf("%s %s<br>", $result[0], $result[1]);
  $stmt->close();
  $dbc->close(); # Close the DB connection.
?>

5.3.16. Inserting data

In SQL we insert data into our DB using the INSERT statement. A straightforward explanation can be found at www.w3schools.com/sql/sql_insert.asp. The gory details are at dev.mysql.com/doc/refman/5.7/en/insert.html.

5.3.16.2. Preparing strings for database insertion

First we should in most cases remove any whitespace from the beginning and end of any input provided by the user using the trim function. The following characters can cause problems when inserting data into a database, as they may be interpreted as control characters by the database: "", '', \, NULL. We need to tell MySQL that we mean these characters literally and not as control characters. To do this, we add a backslash in front of them.

Once our insertion statement has been executed, we can easily get the id of the inserted record (cf. php.net/manual/en/mysqli.insert-id.php):

The mysqli_insert_id function returns the ID generated by a query on a table with a column having the AUTO_INCREMENT attribute. If the last query wasn’t an INSERT or UPDATE statement or if the modified table does not have a column with the AUTO_INCREMENT attribute, this function will return zero.

In the case of prepared statements, we use the insert_id field of the statement (cf. examples below).

5.3.16.3. With internal data
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
  //exit; // to avoid misuse
  require_once 'db_credentials.php';
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME) or
  die('Connect Error (' . $dbc->connect_errno . ') ' . $dbc->connect_error);
  $dbc->set_charset('utf8mb4');

  $first_name = $dbc->escape_string('Bill');
  $last_name  = $dbc->escape_string('Gates');
  $query      = "INSERT INTO tblT1 (dtFirstName, dtLastName)
                  VALUES ('$first_name', '$last_name')";
  $dbc->query($query) or die('Error inserting into DB: ' . $dbc->error);
  echo "Id of inserted record: " . $dbc->insert_id . '<br>';

  $query = 'SELECT dtFirstName, dtLastName FROM tblT1 WHERE idT1=' . $dbc->insert_id;
  echo $query . '<br>';
  $result = $dbc->query($query) or die('Error inserting into DB: ' . $dbc->error);
  $row = $result->fetch_assoc();
  print_r($row);
  $dbc->close(); # Close the DB connection.
?>
5.3.16.4. With user provided data

The simplest approach is to use escape_string on all user provided data, as illustrated above. The safer approach is to use prepared statements.

For illustration of how vulnerable insert queries can be to SQL injection, take a look at amolnaik4.blogspot.lu/2012/02/sql-injection-in-insert-query.html.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
  //exit; // to avoid misuse
  require_once 'db_credentials.php';
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME);
  if ($dbc->connect_error) die('Database connection failed: ' . $dbc->connect_error);
  $dbc->set_charset('utf8mb4');

  $first_name = 'Bill';
  $last_name  = 'Gates';
  $query      = 'INSERT INTO tblT1 (dtFirstName, dtLastName) VALUES (?, ?)';
  $stmt = $dbc->prepare($query);
  if (!$stmt) die("Wrong SQL: $query Error: $dbc->error");
  $stmt->bind_param('ss', $first_name, $last_name) or die('Bind failure: ' . $stmt->error);
  $stmt->execute() or die('Error inserting into DB: ' . $stmt->error);
  echo "Id of inserted record: " . $stmt->insert_id . '<br>';

  $first_name = 'Linus';
  $last_name  = 'Torvalds';
  $stmt->bind_param('ss', $first_name, $last_name) or die('Bind failure: ' . $stmt->error);
  $stmt->execute() or die('Error inserting into DB: ' . $stmt->error);
  echo "Id of inserted record: " . $stmt->insert_id;

  $stmt->close();
  $dbc->close();
?>
5.3.16.5. Storing objects, arrays and other complex data types

If we want to store more complex PHP data types in a MySQL table, we can use the JSON data type (cf. JSON) or alternatively the serialize and unserialize functions. Serialized data should be stored in a MySQL BLOB.

5.3.17. Deleting data

In SQL we delete data from our DB using the DELETE statement. A straightforward explanation can be found at www.w3schools.com/sql/sql_delete.asp. The gory details are at dev.mysql.com/doc/refman/5.7/en/delete.html.

Deleting all the data in a table is very straightforward in MySQL:

1
2
# Delete all data in table tblT1 of DB demo1. The table structure remains.
DELETE FROM demo1.tblT1;

We can filter which data gets deleted using the WHERE clause, just as for SELECT queries:

1
DELETE FROM demo1.tblT1 WHERE dtFirstName = 'Bill';
5.3.17.1. With internal data
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
  require_once 'db_credentials.php';
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME);
  if ($dbc->connect_error) die("Database connection failed: $dbc->connect_error");
  $dbc->set_charset('utf8mb4');

  $query = 'DELETE FROM tblT1 WHERE dtLastName = "Gates"';
  $result = $dbc->query($query);
  if (!$result) die("Wrong SQL: $query Error: $dbc->error");
  $dbc->close(); // Close DB.
?>
 
5.3.17.2. With user provided data

The simplest approach is to use escape_string on all user provided data, as illustrated above. The preferred approach is to use prepared statements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
  require_once 'db_credentials.php';
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME);
  if ($dbc->connect_error) die("Database connection failed: $dbc->connect_error");
  $dbc->set_charset('utf8mb4');

  $query = 'DELETE FROM tblT1 WHERE dtLastName = ?';
  $stmt = $dbc->prepare($query);
  if (!$stmt) die("Wrong SQL: $query Error: $dbc->error");
  $last_name = 'Gates';
  $stmt->bind_param('s', $last_name) or die('Bind failure: ' . $stmt->error);
  $stmt->execute() or die($stmt->error);
  $stmt->close(); // Close prepared statement.
  $dbc->close(); // Close DB connection.
?>
 

5.3.18. Updating data

In SQL we update data in our DB using the UPDATE statement. A straightforward explanation can be found at www.w3schools.com/sql/sql_update.asp. The gory details are at dev.mysql.com/doc/refman/5.7/en/update.html.

A simple example:

1
UPDATE demo1.tblT1 SET dtFirstName = 'James', dtLastName = 'Bond' WHERE dtLastName = 'Gates';
5.3.18.1. With internal data
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
  require_once 'db_credentials.php';
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME);
  if ($dbc->connect_error) die('Database connection failed: ' . $dbc->connect_error);
  $dbc->set_charset('utf8mb4');

  $query = 'UPDATE tblT1 SET dtFirstName = "James", dtLastName = "Bond" WHERE ' .
    'dtLastName = "Gates"';
  $result = $dbc->query($query);
  if (!$result) die("Wrong SQL: $query Error: $dbc->error");
  $dbc->close();
?>
5.3.18.2. With user provided data

The simplest approach is to use escape_string on all user provided data, as illustrated above. The preferred approach is to use prepared statements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
  require_once 'db_credentials.php';
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME);
  if ($dbc->connect_error) die('Database connection failed: ' . $dbc->connect_error);
  $dbc->set_charset('utf8mb4');

  $query = 'UPDATE tblT1 SET dtLastName = ? WHERE dtLastName = ?';
  $stmt = $dbc->prepare($query);
  if (!$stmt) die("Wrong SQL: $query Error: $dbc->error");
  $new_last_name = 'Duck';
  $old_last_name = 'Bond';
  $stmt->bind_param('ss', $new_last_name, $old_last_name) or
    die('Bind failure: ' . $stmt->error);
  $stmt->execute() or die($stmt->error);
  $stmt->close();
  $dbc->close();
?>
 

5.3.19. Altering tables

In SQL we can change the structure of a table using the ALTER TABLE statement. A straightforward explanation can be found at www.w3schools.com/sql/sql_alter.asp. The gory details are at dev.mysql.com/doc/refman/5.7/en/alter-table.html.

A simple example:

1
ALTER TABLE demo1.tblT1 ADD dtBirthDate date;

5.3.20. DB normalization

Normalization is the process of organizing our DB efficiently in order to avoid anomalies that will prevent our application from working correctly (cf. en.wikipedia.org/wiki/Database_normalization).

Study the following excellent explanations:

5.3.21. Creating a safe registration and login

5.3.21.1. Rudimentary version

We need the following:

  1. A DB table to store our users.

  2. A PHP class that provides login and registration functionality for our user table.

  3. A login and registration script.

5.3.21.2. User table

As recommended at php.net/manual/en/password.constants.php, we use a VARCHAR(255) to store the password hash.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
DROP TABLE IF EXISTS tblUser;

CREATE TABLE tblUser (
  idUser         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  dtUserName     VARCHAR(32)  NOT NULL UNIQUE,
  dtPasswordHash VARCHAR(255) NOT NULL
)
  ENGINE = INNODB
  DEFAULT CHARSET utf8mb4
  DEFAULT COLLATE utf8mb4_bin;
5.3.21.3. DB class
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
  require_once 'login1_db_credentials.php';

  class Database {
    private static $DB_HOST;
    private static $DB_USER;
    private static $DB_PASSWORD;
    private static $DB_NAME;
    private static $DB_USER_TABLE = 'tblUser';

    static function set_credentials($db_host, $db_user, $db_password, $db_name) {
      self::$DB_HOST     = $db_host;
      self::$DB_USER     = $db_user;
      self::$DB_PASSWORD = $db_password;
      self::$DB_NAME     = $db_name;
    }

    static function connect() {
      $dbc = new mysqli(self::$DB_HOST, self::$DB_USER, self::$DB_PASSWORD, self::$DB_NAME);
      if ($dbc->connect_error) trigger_error('Database connection failed: ' .
        $dbc->connect_error, E_USER_ERROR);
      $dbc->set_charset("utf8mb4");
      return $dbc;
    }

    # Returns user id or FALSE.
    static function login($user_name, $password) {
      $dbc = self::connect();
      $query = 'SELECT idUser, dtPasswordHash FROM ' . self::$DB_USER_TABLE .
        ' WHERE dtUserName = ?';
      $stmt = $dbc->prepare($query) or trigger_error('Wrong SQL: ' . $query .
        ' Error: ' .  $dbc->error, E_USER_ERROR);
      $stmt->bind_param('s', $user_name);
      if ($stmt->execute()) {
        $result = $stmt->get_result();
        if ($result && $result->num_rows === 1) {
          $row = $result->fetch_assoc();
          if ($row && password_verify($password, $row['dtPasswordHash'])) return $row['idUser'];
        }
      } else trigger_error('Wrong SQL: ' . $query .
        ' Error: ' .  $dbc->error, E_USER_ERROR);
      return false;
    }

    # Returns FALSE if user could not be created, otherwise user id.
    static function create_user($user_name, $password) {
      $dbc = self::connect();
      $query = 'INSERT INTO ' . self::$DB_USER_TABLE . ' (dtUserName, dtPasswordHash)' .
        ' VALUES(?, ?)';
      $stmt = $dbc->prepare($query) or trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $pw_hash = password_hash($password, PASSWORD_DEFAULT);
      $stmt->bind_param('ss', $user_name, $pw_hash);
      if ($stmt->execute()) return $stmt->insert_id;
      else return false;
    }
  }
?>
5.3.21.4. Login and registration script
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?php
  // Protect from session fixation via session adoption.
  ini_set('session.use_strict_mode', true);
  # Only send session id cookie over SSL.
  ini_set('session.cookie_secure', true);
  # Session IDs may only be passed via cookies, not appended to URL.
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_httponly', true);
  // Set the path for the cookie to the current directory in order to prevent it from
  // being available to scripts in other directories.
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  if (!isset($_SERVER['HTTPS'])) {// If SSL is not active, activate it.
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']));
    exit;
  }
  // If no session is started yet, we'll start one.
  if (!isset($_SESSION)) session_start();
  // After 30 seconds we'll generate a new session ID to prevent a session fixation
  // attack (cf. PHP cookbook p. 338).
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  // Include the database class needed to access the database.
  require_once 'loginDB1.php';
  // If a user is already logged in, let him through to the main page.
  if (isset($_SESSION['user_id'])) {
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/logged_in1.php');
    exit;
  }
  // Else, if the user has submitted his login details, we need to check them.
  elseif (isset($_POST['login'])) {
    if (isset($_POST['username'], $_POST['password'])) {
      $result = Database::login($_POST['username'], $_POST['password']);
      // If a user with this login exists, we load the main page.
      if ($result) {
        $_SESSION['user_id'] = $result;
        $_SESSION['user_name'] = $_POST['username'];
        header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
          dirname($_SERVER['PHP_SELF']) . '/logged_in1.php');
        exit;
      }
    }
  }
  // Else, if the user has signed up for a new account, we need to check if
  // such a user already exists.
  elseif (isset($_POST['register'])) {
    if (isset($_POST['username'], $_POST['pw1'], $_POST['pw2']) && $_POST['pw1'] ===
      $_POST['pw2']
    )
      if (Database::create_user($_POST['username'], $_POST['pw1']))
        echo "<script>alert('Registration succeeded, please log in!');</script>";
      else echo "<script>alert('Registration failed!');</script>";
  }
?>

<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Login and registration</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <form method=post>
        <input name=username placeholder="User name" required>
        <input name=password type=password placeholder=Password required>
        <button name=login>Log in</button>
      </form>
      <form method=post>
        <input name=username placeholder="User name" required>
        <input name=pw1 type=password placeholder=Password required>
        <input name=pw2 type=password placeholder="Repeat password" required>
        <button name=register>Register</button>
      </form>
    </main>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
  // Protect from session fixation via session adoption.
  ini_set('session.use_strict_mode', true);
  # Only send session id cookie over SSL.
  ini_set('session.cookie_secure', true);
  # Session IDs may only be passed via cookies, not appended to URL.
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_httponly', true);
  // Set the path for the cookie to the current directory in order to prevent it from
  // being available to scripts in other directories.
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  if (!isset($_SERVER['HTTPS'])) // If SSL is not active, activate it.
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']));
  // If no session is started yet, we'll start one.
  if (!isset($_SESSION)) session_start();
  // After 30 seconds we'll generate a new session ID to prevent a session fixation
  // attack (cf. PHP cookbook p. 338).
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  // If no user is logged in, send him to the login and registration page.
  if (isset($_POST['logout'])) {
    $_SESSION = [];
    if (session_id() != "" || isset($_COOKIE[session_name()])) setcookie(session_name(),
      '', 1, rawurlencode(dirname($_SERVER['PHP_SELF'])));
    session_destroy();
  }
  if (!isset($_SESSION['user_id']))
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/login1.php');
?>

<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Login and registration</title>
    <meta charset=utf-8>
  </head>
  <body>
    <main>
      <h1>Welcome <?php if (isset($_SESSION['user_name'])) echo $_SESSION['user_name']; ?></h1>
      <form method=post>
        <button name=logout>Log out</button>
      </form>
    </main>
  </body>
</html>

5.3.22. Procedural use of mysqli

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
  require_once 'db_credentials.php'; // Import connection details.
  // Connect to DB. Stop script with error message if connection fails. Don't do this in
  // production version!
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or
    die('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, "utf8mb4"); // Set the default character set.
  echo '<pre>' . print_r($dbc, true) . '</pre>';
  echo '<strong>mysqli class methods:</strong>';
  echo '<pre>' . print_r(get_class_methods('mysqli'), true) . '</pre>';
  mysqli_close($dbc); // Close the DB connection.
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
  require_once 'db_credentials.php'; // Include the DB connection credentials.

  // Connect to the DB. We need to store the connection as we will need it for all DB
  // operations. If something goes wrong, $dbc will be null and we display the
  // connection error number as well as the error text and terminate the script.
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    mysqli_connect_errno() . ') ' . mysqli_connect_error());

  // Don't forget to set the character set, otherwise you are looking for trouble!
  mysqli_set_charset($dbc, 'utf8mb4');

  $query = 'SELECT * FROM tblT1'; // Store our SQL query in a varibale.

  // Execute the SQL query. To do this we need to provide the DB connection and the query.
  // If something goes wrong, we display the error message and terminate the script.
  // If the query executed correctly, we get a result object which we store in $result.
  $result = mysqli_query($dbc, $query) or die("Wrong SQL: $query Error: " .
    mysqli_error($dbc));

  // This is just to take a look at the result object.
  echo '<pre>' . print_r($result, true) . '</pre>';

  // Display the number of records returned by the query.
  echo '<br>Number of rows: ' . mysqli_num_rows($result);

  // Retrieve each record one by one. mysqli_fetch_assoc returns the next record until we
  // have reached the last one. We append each record to the $all_rows array.
  while ($row = mysqli_fetch_assoc($result)) $all_rows[] = $row;

  // Let's take a look at the result array.
  echo '<pre>' . print_r($all_rows, true) . '</pre>';

  // Since PHP 5.4 the mysqli result object supports iteration (cf.
  // https://secure.php.net/manual/en/class.mysqli-result.php) which makes our life easier.
  // Note however, that $row will only be an associative, not a numerically indexed array.
  foreach ($result as $row) echo '<pre>' . print_r($row, true) . '</pre>';

  if (mysqli_data_seek($result, 0)) // Set the result pointer back to the beginning.
    while ($row = mysqli_fetch_array($result))
      echo '<pre>' . print_r($row, true) . '</pre>';

  mysqli_free_result($result); # Free the memory used by the result object.
  mysqli_close($dbc); # Close the Db connection.
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $query = 'SELECT * FROM tblT1';
  $result   = mysqli_query($dbc, $query);
  if (!$result) die("Wrong SQL: $query Error: " . mysqli_error($dbc));
  while ($row = mysqli_fetch_array($result, MYSQLI_ASSOC)) $rows[] = $row;
  echo '<pre>' . print_r($rows, true) . '</pre>';
  mysqli_free_result($result);
  mysqli_close($dbc);
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $query = 'SELECT * FROM tblT1';
  $result   = mysqli_query($dbc, $query);
  if (!$result) die("Wrong SQL: $query Error: " . mysqli_error($dbc));
  $rows = mysqli_fetch_all($result);
  echo '<pre>' . print_r($rows, true) . '</pre>';
  mysqli_free_result($result);
  mysqli_close($dbc);
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $last_name = "Duck";
  $last_name = mysqli_escape_string($dbc, $last_name);
  $query = "SELECT dtFirstName, dtLastName FROM tblT1 WHERE dtLastName = '$last_name'";
  $result = mysqli_query($dbc, $query) or die('Wrong SQL: ' . $query . ' Error: ' .
    mysqli_error($dbc));
  echo 'Number of rows ' . mysqli_num_rows($result) . '<br>'; # Display the number of rows.
  # Loop through the result object and display each first name.
  while ($row = mysqli_fetch_array($result)) printf("%s %s<br>", $row[0], $row[1]);
  mysqli_close($dbc);
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $query = 'SELECT dtFirstName, dtLastName FROM tblT1 WHERE dtLastName = ?'; # our query
  $stmt  = mysqli_prepare($dbc, $query);
  if (!$stmt) die('Wrong SQL: ' . $query . ' Error: ' . mysqli_error($dbc));
  echo '<pre>' . print_r($stmt, true) . '</pre>';

  $last_name = 'Gates'; # Save the last name that we want to use in our query in a variable.
  # Bind the parameter to our query as a string.
  mysqli_stmt_bind_param($stmt, 's', $last_name) or die('Bind failure: ' .
    mysqli_error($dbc));
  mysqli_stmt_execute($stmt) or die('Error reading from DB.');
  $result = mysqli_stmt_get_result($stmt);
  if ($result) echo '<pre>' . print_r(mysqli_fetch_all($result), true) .  '</pre>';
  mysqli_stmt_close($stmt);
  mysqli_close($dbc);
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $query = 'SELECT dtFirstName, dtLastName FROM tblT1 WHERE dtLastName = ?'; # our query
  $stmt  = mysqli_prepare($dbc, $query);
  if (!$stmt) die('Wrong SQL: ' . $query . ' Error: ' . mysqli_error($dbc));
  echo '<pre>' . print_r($stmt, true) . '</pre>';

  $last_name = 'Gates'; # Save the last name that we want to use in our query in a variable.
  # Bind the parameter to our query as a string.
  mysqli_stmt_bind_param($stmt, 's', $last_name) or die('Bind failure: ' .
    mysqli_error($dbc));
  mysqli_stmt_execute($stmt) or die('Error reading from DB.');
  $result = mysqli_stmt_get_result($stmt);
  while ($row = mysqli_fetch_array($result, MYSQLI_NUM))
    printf("%s %s<br>", $row[0], $row[1]);
  mysqli_stmt_close($stmt);
  mysqli_close($dbc);
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or die('Connect Error (' .
    mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $query = 'SELECT dtFirstName, dtLastName FROM tblT1 WHERE dtLastName = ?';
  $stmt  = mysqli_prepare($dbc, $query);
  if (!$stmt) die('Wrong SQL: ' . $query . ' Error: ' . mysqli_error($dbc));
  echo '<pre>' . print_r($stmt, true) . '</pre>';

  $last_name = 'Gates';
  mysqli_stmt_bind_param($stmt, 's', $last_name) or die('Bind failure: ' .
    mysqli_error($dbc));;
  mysqli_stmt_execute($stmt) or die('Error reading from DB.');
  //$result = mysqli_stmt_get_result($stmt);
  $result_set = mysqli_stmt_store_result($stmt) or die('Error storing result.');
  echo 'Number of rows ' . mysqli_stmt_num_rows($stmt) . '<br>'; # Display the number of rows
  mysqli_stmt_bind_result($stmt, $row[0], $row[1]);
  while (mysqli_stmt_fetch($stmt)) printf("%s %s<br>", $row[0], $row[1]);
  mysqli_stmt_close($stmt);
  mysqli_close($dbc);
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
  exit; // to avoid misuse
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or
    die('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $first_name = 'Bill';
  $last_name  = 'Gates';
  $query      = "INSERT INTO tblT1 (dtFirstName, dtLastName)
          VALUES ('$first_name', '$last_name')";
  mysqli_query($dbc, $query) or die('Error inserting into DB: ' . mysqli_error($dbc));
  echo "Id of inserted record: " . mysqli_insert_id($dbc);
  mysqli_close($dbc); # Close the DB connection.
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
  exit; // to avoid misuse
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or
    die('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $first_name = 'Bill';
  $last_name  = 'Gates';
  $query      = 'INSERT INTO tblT1 (dtFirstName, dtLastName) VALUES (?, ?)';
  $stmt = mysqli_prepare($dbc, $query);
  if (!$stmt) die("Wrong SQL: $query Error: mysqli_error($dbc)");
  mysqli_stmt_bind_param($stmt, 'ss', $first_name, $last_name) or
    die('Bind failure: ' . mysqli_error($dbc));
  mysqli_stmt_execute($stmt) or die('Error inserting into DB.');
  echo "Id of inserted record: " . mysqli_stmt_insert_id($stmt);

  $first_name = 'Linus';
  $last_name  = 'Torvalds';
  mysqli_stmt_bind_param($stmt, 'ss', $first_name, $last_name);
  mysqli_stmt_execute($stmt) or die('Error inserting into DB.');
  echo "Id of inserted record: " . mysqli_stmt_insert_id($stmt);

  mysqli_stmt_close($stmt);
  mysqli_close($dbc);
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or
    die('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $query = 'DELETE FROM tblT1 WHERE dtLastName = "Gates"';
  $result = mysqli_query($dbc, $query);
  if (!$result) die("Wrong SQL: $query Error: mysqli_error($dbc)");
  mysqli_close($dbc);
?>
 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or
    die('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $query = 'DELETE FROM tblT1 WHERE dtLastName = ?';
  $stmt = mysqli_prepare($dbc, $query);
  if (!$stmt) die("Wrong SQL: $query Error: mysqli_error($dbc)");
  $last_name = 'Gates';
  mysqli_stmt_bind_param($stmt, 's', $last_name);
  mysqli_stmt_execute($stmt) or die('Error deleting from DB.');
  mysqli_stmt_close($stmt);
  mysqli_close($dbc);
?>
 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or
    die('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $query = 'UPDATE tblT1 SET dtFirstName = "James", dtLastName = "Bond" WHERE ' .
    'dtLastName = "Gates"';
  $result = mysqli_query($dbc, $query);
  if (!$result) die("Wrong SQL: $query Error: " . mysqli_error($dbc));
  mysqli_close($dbc);
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
  require_once 'db_credentials.php';
  $dbc = mysqli_connect(DB_HOST, DB_USER, DB_PW, DB_NAME) or
    die('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
  mysqli_set_charset($dbc, 'utf8mb4');

  $query = 'UPDATE tblT1 SET dtLastName = ? WHERE dtLastName = ?';
  $stmt = mysqli_prepare($dbc, $query);
  if (!$stmt) die("Wrong SQL: $query Error: " . mysqli_error($dbc));
  $new_last_name = 'Duck';
  $old_last_name = 'Bond';
  mysqli_stmt_bind_param($stmt, 'ss', $new_last_name, $old_last_name) or
    die('Bind failure: ' . mysqli_error($dbc));;
  mysqli_stmt_execute($stmt) or die('Error deleting from DB.');
  mysqli_stmt_close($stmt);
  mysqli_close($dbc);
?>
 

5.3.23. PHP Data Objects (PDO)

The PHP Data Objects (PDO) extension defines a lightweight, consistent interface for accessing databases in PHP. PDO provides a data-access abstraction layer, which means that, regardless of which database you’re using, you use the same functions to issue queries and fetch data.

The details of the PDO class can be found at php.net/manual/en/class.pdo.php.

5.3.23.1. Connection
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
  require_once 'db_credentials.php';
  $dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4';
  $options = [
    // https://stackoverflow.com/questions/5741187/sql-injection-that-gets-around-mysql-real-escape-string
    // turn off emulation mode for "real" prepared statements
    PDO::ATTR_EMULATE_PREPARES   => false,
    // turn on errors in the form of exceptions
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    // make the default fetch be an associative array
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
  ];
  try {
    $pdo = new PDO($dsn, DB_USER, DB_PW, $options);
    foreach ($pdo->query('SELECT * FROM tblT1') as $row) {
      echo '<pre>' . print_r($row, true) . '</pre>';
    }
  } catch (PDOException $e) {
    error_log($e->getMessage());
    exit('Something weird happened'); //something a user can understand
  }
?>
It is essential to catch the exception thrown by the PDO constructor otherwise your user name and password are at risk as explained in php.net/manual/en/pdo.connections.php.
5.3.23.2. Statement execution

To execute an SQL statement, we can choose between exec and query or use a prepared statement.

query executes an SQL statement in a single function call, returning the result set (if any) returned by the statement as a PDOStatement object.

exec also executes an SQL statement in a single function call but returns the number of rows affected by the statement. It does not return results from a SELECT statement.

For query and exec you need to escape the data inside the query using quote if there’s any chance it comes from outside, e.g. a user.

For a statement that you need to issue multiple times, prepare a PDOStatement object with prepare and issue the statement with execute.

You are strongly recommended to use prepare to prepare SQL statements with bound parameters instead of using quote to interpolate user input into an SQL statement. Prepared statements with bound parameters are not only more portable, more convenient, immune to SQL injection, but are often much faster to execute than interpolated queries, as both the server and client side can cache a compiled form of the query.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
  require_once 'db_credentials.php';
  $dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4';
  $options = [
    // https://stackoverflow.com/questions/5741187/sql-injection-that-gets-around-mysql-real-escape-string
    // turn off emulation mode for "real" prepared statements
    PDO::ATTR_EMULATE_PREPARES   => false,
    // turn on errors in the form of exceptions
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    // make the default fetch be an associative array
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
  ];
  try {
    $pdo = new PDO($dsn, DB_USER, DB_PW, $options);
    $sql = 'SELECT dtFirstName, dtLastName FROM tblT1 WHERE dtNumberOfGrades >= ?';
    $sth = $pdo->prepare($sql);
    $sth->execute([12]);
    echo 'Result of query 1:<br>';
    $result = $sth->fetchAll();
    echo '<pre>' . print_r($result, true) . '</pre>';

    // We can also use named parameters:
    $sql = 'SELECT * FROM tblT1 WHERE dtNumberOfGrades >= :numOfGrades AND FIND_IN_SET(:modules, dtModules)';
    $sth = $pdo->prepare($sql);
    $sth->execute([':numOfGrades' => 15, ':modules' => 'HTSTA']);
    echo 'Result of query 2:<br>';
    $result = $sth->fetchAll();
    echo '<pre>' . print_r($result, true) . '</pre>';
  }
  catch (PDOException $e) {
    error_log($e->getMessage());
    exit('Something weird happened'); //something a user can understand
  }
?>

The predefined PDO constants provided by the MySQL driver can be found at php.net/manual/en/ref.pdo-mysql.php.

We can also use MySQLi and PDO in Open Swoole:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
require_once '../../MySQL/db_credentials.php';
// Enable the hook for MySQL: PDO/MySQLi
co::set(['hook_flags' => OpenSwoole\Runtime::HOOK_TCP]);

// Setup a coroutine context
co::run(function () {

    // Execute a query inside a coroutine
    go(function () {
        $dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4';
        $options = [
            // https://stackoverflow.com/questions/5741187/sql-injection-that-gets-around-mysql-real-escape-string
            // turn off emulation mode for "real" prepared statements
            PDO::ATTR_EMULATE_PREPARES => false,
            // turn on errors in the form of exceptions
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            // make the default fetch be an associative array
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ];
        $pdo = new PDO($dsn, DB_USER, DB_PW, $options);
        // Already setup the $pdo connection before...

        $statement = $pdo->prepare("SELECT * FROM evegi144_dbDemo1.tblUser");
        $statement->execute([]);
        $data = $statement->fetchAll();

        // Process $data result...
        print_r($data);
    });
});

5.3.25. Stored procedures

www.php.net/manual/en/mysqli.quickstart.stored-procedures.php

dev.mysql.com/doc/refman/8.0/en/stored-programs-defining.html

www.techbeamers.com/mysql-declare-variable

stackoverflow.com/questions/9845171/run-a-query-in-a-mysql-stored-procedure-if-a-condition-is-true

stackoverflow.com/questions/11951570/mysql-create-user-with-a-variable

stackoverflow.com/questions/273437/how-do-you-debug-mysql-stored-procedures

You should also use prepared statements in stored procedures (cf. www.tutorialspoint.com/How-can-we-use-prepared-statements-in-a-stored-procedure).

Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
DROP PROCEDURE IF EXISTS createUsers;
DELIMITER // ;
CREATE PROCEDURE createUsers()
BEGIN
    DECLARE users VARCHAR(100);
    DECLARE user VARCHAR(10);
    DECLARE userLocalhost VARCHAR(20);
    DECLARE userStar VARCHAR(10);
    SET users = "user1,user2,user3,";
    WHILE (LOCATE(',', users) > 0) DO
        SET user = LEFT(users, 8);
        SET users = SUBSTRING(users, LOCATE(',', users) + 1);
        SET userLocalhost = CONCAT(user, '@localhost');
        SET userStar = CONCAT(user, '.*');
        SELECT user;
        SET @query = CONCAT('CREATE USER ', userLocalhost, " IDENTIFIED BY 'superpassword'");
        SELECT @query;
        PREPARE stmt FROM @query; EXECUTE stmt; DEALLOCATE PREPARE stmt;
        SET @query = CONCAT('CREATE DATABASE ', user, ' DEFAULT CHARSET utf8 DEFAULT collate utf8_bin');
        SELECT @query;
        PREPARE stmt FROM @query; EXECUTE stmt; DEALLOCATE PREPARE stmt;
        set @query = CONCAT('GRANT ALL PRIVILEGES ON ', userStar, ' TO ', userLocalhost);
        SELECT @query;
        PREPARE stmt FROM @query; EXECUTE stmt; DEALLOCATE PREPARE stmt;
    END WHILE;
    FLUSH PRIVILEGES;
END //
DELIMITER ; //
CALL createUsers();

5.3.26. Security

5.3.27. Tests

5.3.27.1. WMOTU Shop DB
WMOTUShopDB1

WMOTU Shop DB provides the following functionality (cf. youtu.be/fpzwKXdBnpc):

  1. The user sees all the articles in the database. For each article, the name, description and price are displayed.

  2. The user can add a new and delete or edit an existing article.

  3. The database is created using a MySQL script, which is located in a protected place, so that the user cannot access it.

  4. The user cannot hijack our DB with malignant input.

  5. Special characters such as ' and " are handled correctly.

5.3.27.1.1. Solution

The solution can be found at students.btsi.lu/evegi144/WAD/MySQL/Tests/WMOTUShopDB. To protect the protected directory, we proceed as described in Security.

createDB.sql

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
USE evegi144;

DROP TABLE IF EXISTS WMOTUShopArticle;

CREATE TABLE WMOTUShopArticle (
  idArticle     INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  dtName        VARCHAR(40) NOT NULL,
  dtDescription VARCHAR(40) NOT NULL,
  dtPrice       DEC         NOT NULL
)
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;

INSERT INTO WMOTUShopArticle (dtName, dtDescription, dtPrice)
VALUES ('Car', 'World\'s fastest car', 50000), ('TV', 'Experience 4K resolution', 1000);

database.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<?php
  require_once 'db_credentials.php';

  class Database {
    private static $DB_HOST;
    private static $DB_USER;
    private static $DB_PASSWORD;
    private static $DB_NAME;
    private static $DB_TABLE = 'WMOTUShopArticle';

    static function set_credentials($db_host, $db_user, $db_password, $db_name) {
      self::$DB_HOST     = $db_host;
      self::$DB_USER     = $db_user;
      self::$DB_PASSWORD = $db_password;
      self::$DB_NAME     = $db_name;
    }

    static private function connect() {
      $dbc = new mysqli(self::$DB_HOST, self::$DB_USER, self::$DB_PASSWORD,
        self::$DB_NAME);
      if ($dbc->connect_error) trigger_error('Database connection failed: ' .
      $dbc->connect_error, E_USER_ERROR);
      $dbc->set_charset("utf8mb4");
      return $dbc;
    }

    static function get_articles() {
      $dbc    = self::connect();
      $result = FALSE;
      $query  = 'SELECT * FROM ' . self::$DB_TABLE;
      $res    = $dbc->query($query);
      if (!$res) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
      $dbc->error, E_USER_ERROR);
      while ($dat = $res->fetch_assoc()) $result[] = $dat;
      $dbc->close();
      return $result;
    }

    static function get_article($id) {
      $dbc    = self::connect();
      $query  = 'SELECT * FROM ' . self::$DB_TABLE . " WHERE idArticle = $id";
      $res    = $dbc->query($query);
      if (!$res) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
      $dbc->error, E_USER_ERROR);
      $result = $res->fetch_assoc();
      $dbc->close();
      return $result;
    }

    static function delete_article($id) {
      $dbc    = self::connect();
      $query  = 'DELETE FROM ' . self::$DB_TABLE . " WHERE idArticle = $id";
      $result = $dbc->query($query);
      if (!$result) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
      $dbc->error, E_USER_ERROR);
      $dbc->close();
    }

    static function update_article($id, $name, $description, $price) {
      $dbc    = self::connect();
      $query  = 'UPDATE ' . self::$DB_TABLE . ' SET dtName = ?, dtDescription = ?, ' .
        "dtPrice = ? WHERE idArticle = $id";
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
      $dbc->error, E_USER_ERROR);
      $stmt->bind_param('ssi', $name, $description, $price);
      $result = $stmt->execute();
      $dbc->close();
      return $result;
    }

    static function add_article($name, $description, $price) {
      $dbc    = self::connect();
      $query  = 'INSERT INTO ' . self::$DB_TABLE .
        ' (dtName, dtDescription, dtPrice) VALUES (?, ?, ?)';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('ssi', $name, $description, $price);
      $result = $stmt->execute();
      $dbc->close();
      return $result;
    }
  }
?>

index.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
  require_once 'protected/database.php';
  if (isset($_GET['op']) && isset($_GET['id'])) {
    if ($_GET['op'] === 'del') Database::delete_article($_GET['id']);
    elseif ($_GET['op'] === 'edit') $edit_id = $_GET['id'];
  }
  elseif (isset($_POST['update']))
    Database::update_article($_POST['id'], $_POST['name'],
      $_POST['description'], $_POST['price']);
  elseif (isset($_POST['add']))
    Database::add_article($_POST['name'], $_POST['description'], $_POST['price']);
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Shop DB</title>
    <meta charset=UTF-8>
  </head>
  <body>
    <main>
      <?php
        if (isset($edit_id)) {
          $article = Database::get_article($edit_id);
          ?>
          <form method=post action=index.php>
            <input type=hidden name=id value=<?php echo $edit_id ?>>
            Name: <input name=name autofocus value='<?php echo
            htmlspecialchars($article['name'], ENT_QUOTES, 'UTF-8'); ?>'><br>
            Description: <input name=description value='<?php echo
            htmlspecialchars($article['description'], ENT_QUOTES, 'UTF-8'); ?>'><br>
            Price: <input type=number name=price value='<?php echo
            htmlspecialchars($article['price'], ENT_QUOTES, 'UTF-8'); ?>'>
            <input type=submit value=Update name=update>
          </form>
        <?php
        }
        else {
          $articles = Database::get_articles();
          $output   = '<table><thead><tr><th>Article</th><th>Description</th>' .
            '<th>Price</th><th></th><th></th></tr></thead>';
          if ($articles)
            foreach ($articles as $article) {
              $output .= '<tr><td>' . $article['dtName'] . '</td><td>' .
                $article['dtDescription'] . '</td><td>' . $article['dtPrice'] .
                '</td><td><a href="index.php?op=del&id=' . $article['idArticle'] .
                '">delete</a></td><td><a href="index.php?op=edit&id=' .
                $article['idArticle'] . '">edit</a></td></tr>';
            }
          echo $output . '</table>';
          ?>
          <form method=post action=index.php>
            Name: <input name=name autofocus><br>
            Description: <input name=description><br>
            Price: <input type=number name=price><br>
            <input type=submit value=Add name=add>
          </form>
        <?php
        }
      ?>
    </main>
  </body>
</html>
5.3.27.2. Perfume Shop
PerfumeShop

Develop a perfume shop with the following features:

  1. A user DB table to store the user name and encrypted password. There are 2 users (dummy1, dummy1) and (dummy2, dummy2).

  2. A perfume DB table to store the perfumes available for sale with the following names and prices (€):

    • Eau de SYSEX 99.99

    • Eau de HTSTA 129.99

    • Eau de CLISS 179.99

    • Eau de WSERS 299.99

    • Eau de WEBAP 499.99

  3. A purchase DB table, storing for each purchase a link to the user, a link to the perfume and the quantity.

  4. A login (no sign up).

  5. Every user has his own shopping cart, which displays the articles bought so far and gives the total price. For each article there may not be more than one entry in the shopping cart, e.g. if the user buys 1 Eau de WSERS and later on buys another 3, then the shopping cart will show 4 Eau de WSERS at €299.99 each for a total of €1199.96.

  6. The user can add and remove individual perfumes from his shopping cart. For instance, using the previous example, he can remove 2 Eau de WSERS, which leaves 2 for a total of €599.98.

  7. Each user has a logout button.

  8. Obviously, if the user logs out and then logs in again, the shopping cart still shows all purchases made.

5.3.27.2.1. Solution

createDB.sql

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
DROP TABLE IF EXISTS tblPerfumeShopPurchase;
DROP TABLE IF EXISTS tblPerfumeShopPerfume;
DROP TABLE IF EXISTS tblPerfumeShopUser;

CREATE TABLE tblPerfumeShopUser (
  idUser     INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  dtName     VARCHAR(32)  NOT NULL UNIQUE,
  dtPassword VARCHAR(255) NOT NULL
)
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;

CREATE TABLE tblPerfumeShopPerfume (
  idPerfume INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  dtName    VARCHAR(32)   NOT NULL UNIQUE,
  dtPrice   DECIMAL(5, 2) NOT NULL
)
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;

INSERT INTO tblPerfumeShopPerfume (dtName, dtPrice) VALUES ('Eau de SYSEX', 99.99),
  ('Eau de HTSTA', 129.99), ('Eau de CLISS', 179.99), ('Eau de WSERS', 299.99),
  ('Eau de WEBAP', 499.99);

CREATE TABLE tblPerfumeShopPurchase (
  idPurchase INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  fiUser     INT UNSIGNED NOT NULL,
  fiPerfume  INT UNSIGNED NOT NULL,
  FOREIGN KEY (fiUser) REFERENCES tblPerfumeShopUser (idUser)
    ON DELETE CASCADE
    ON UPDATE CASCADE,
  FOREIGN KEY (fiPerfume) REFERENCES tblPerfumeShopPerfume (idPerfume)
    ON DELETE CASCADE
    ON UPDATE CASCADE,
  dtQuantity INT UNSIGNED NOT NULL
)
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;

database.php

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
<?php
  require_once 'db_credentials.php';

  class Database {
    private static $DB_HOST;
    private static $DB_USER;
    private static $DB_PASSWORD;
    private static $DB_NAME;

    static function set_credentials($db_host, $db_user, $db_password, $db_name) {
      self::$DB_HOST     = $db_host;
      self::$DB_USER     = $db_user;
      self::$DB_PASSWORD = $db_password;
      self::$DB_NAME     = $db_name;
    }

    static function connect() {
      $dbc = @new mysqli(self::$DB_HOST, self::$DB_USER, self::$DB_PASSWORD, self::$DB_NAME);
      if ($dbc->connect_error) error_log('Database connection failed: ' . $dbc->connect_error);
      $dbc->set_charset("utf8mb4");
      return $dbc;
    }

    # Returns user id or FALSE.
    static function login($user_name, $password) {
      $dbc   = self::connect();
      $query = 'SELECT idUser, dtPassword FROM tblPerfumeShopUser WHERE dtName = "' .
        $dbc->real_escape_string(($user_name)) . '"';
      $result = $dbc->query($query) or trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $res = false;
      if ($result && $result->num_rows === 1) {
        $arr = $result->fetch_assoc();
        if (password_verify($password, $arr['dtPassword'])) $res = $arr['idUser'];
        $result->free();
      }
      $dbc->close();
      return $res;
    }

    # Returns FALSE if user could not be created, otherwise user id.
    static function create_user($user_name, $password) {
      $dbc   = self::connect();
      $query = 'INSERT INTO tblPerfumeShopUser(dtName, dtPassword) VALUES("' .
        $dbc->real_escape_string($user_name) . '", "' .
        password_hash($password, PASSWORD_DEFAULT) . '")';
      $result = $dbc->query($query) or trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $dbc->close();
      return $result;
    }

    # Inserts a purchase, returns nothing.
    static function purchase($user_id, $perfume_id) {
      $dbc   = self::connect();
      $query = "INSERT INTO tblPerfumeShopPurchase (fiUser, fiPerfume, dtQuantity) VALUES
      ($user_id, $perfume_id, 1)";
      $result = $dbc->query($query) or die('Error inserting into DB.' . $dbc->error);
    }

    # Removes a purchase, returns nothing.
    static function remove($user_id, $perfume_id) {
      $dbc   = self::connect();
      $query = "SELECT idPurchase FROM tblPerfumeShopPurchase WHERE fiUser = $user_id AND
        fiPerfume = $perfume_id";
      $result = $dbc->query($query) or die('Error inserting into DB.' . $dbc->error);
      if ($result->num_rows > 0) {
        $purchase_id = $result->fetch_row()[0];
        $query       = "DELETE FROM tblPerfumeShopPurchase WHERE idPurchase = $purchase_id";
        $result = $dbc->query($query) or die('Error inserting into DB.' . $dbc->error);
      }
      $dbc->close();
    }

    # Returns all perfumes.
    static function get_perfumes() {
      $dbc   = self::connect();
      $query = "SELECT * FROM tblPerfumeShopPerfume";
      $result = $dbc->query($query) or die('Error inserting into DB.' . $dbc->error);
      $perfumes = [];
      while ($perfume = $result->fetch_assoc()) $perfumes[] = $perfume;
      $result->free();
      $dbc->close();
      return $perfumes;
    }

    # Returns purchases for given user or empty array.
    static function get_purchases($user_id) {
      $dbc   = self::connect();
      $query = "SELECT idPerfume, dtPrice, dtName, SUM(dtQuantity) as dtQuantity FROM
        tblPerfumeShopPerfume, tblPerfumeShopPurchase WHERE fiUser =  $user_id AND
        fiPerfume = idPerfume GROUP BY idPerfume";
      $result = $dbc->query($query) or die('Error inserting into DB.' . $dbc->error);
      $purchases = [];
      while ($purchase = $result->fetch_assoc()) $purchases[] = $purchase;
      $result->free();
      $dbc->close();
      return $purchases;
    }
  }

  /*Database::create_user('dummy1', 'dummy1');
  Database::create_user('dummy2', 'dummy2');*/
?>

index.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
  ini_set('session.cookie_secure', true);
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  if (!isset($_SERVER['HTTPS']))
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']));
  if (!isset($_SESSION)) session_start();
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  if (isset($_SESSION['user_id'])) header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
    dirname($_SERVER['PHP_SELF']) . '/main.php');
  elseif (isset($_POST['username'], $_POST['password'])) {
    require_once 'database.php';
    $result = Database::login($_POST['username'], $_POST['password']);
    if ($result) {
      $_SESSION['user_id']   = $result;
      $_SESSION['user_name'] = $_POST['username'];
      header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
        dirname($_SERVER['PHP_SELF']) . '/main.php');
    }
  }
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Perfume Shop</title>
    <meta charset=utf-8>
  </head>
  <body>
    <form method=post>
      <input name=username placeholder="User name" required autofocus>
      <input type=password name=password placeholder=Password required>
      <button>Log in</button>
    </form>
  </body>
</html>

logout.php

1
2
3
4
5
6
7
8
9
<?php
  if (!isset($_SESSION)) session_start();
  $_SESSION = [];
  if (session_id() != "" || isset($_COOKIE[session_name()])) setcookie(session_name(),
    '', 1, '/');
  session_destroy();
  header('Location: https://' . $_SERVER['HTTP_HOST'] .
    dirname($_SERVER['PHP_SELF']) . '/index.php');
?>

main.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
  ini_set('session.cookie_secure', true);
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  if (!isset($_SERVER['HTTPS']))
    header('Location: https://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']));
  if (!isset($_SESSION)) session_start();
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  if (!isset($_SESSION['user_id'])) header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/index.php');
  if (!isset($_SESSION['quantities'])) $_SESSION['quantities'] = [0, 0, 0, 0, 0];
  require_once 'database.php';
  $perfumes = Database::get_perfumes();
  if (count($_POST) > 0)
    if (isset($_POST["a"])) Database::purchase($_SESSION['user_id'], $_POST['a']);
    elseif (isset($_POST["r"])) Database::remove($_SESSION['user_id'], $_POST['r']);
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Perfume Shop</title>
    <meta charset=utf-8>
    <style>
      table {
        border-collapse: collapse;
      }

      th, td {
        border: 3px solid black;
        padding: 5px;
      }
    </style>
  </head>
  <body>
    <button onclick="location='logout.php'">Log out</button>
    <?php
      $out = '<h1>Our products</h1><form method=post><table><tr><td>Article</td><td>Price</td>'
        . '<td></td></tr>';
      foreach ($perfumes as $perfume)
        $out .= "<tr><td>{$perfume['dtName']}</td><td>€{$perfume['dtPrice']}</td>
          <td><button name=a value={$perfume['idPerfume']}>buy</button></td>";
      $out .= '</table></form>';
      echo $out;
      $purchases = Database::get_purchases($_SESSION['user_id']);
      if (count($purchases) > 0) {
        $total = 0;
        $out = '<h1>Your shopping cart</h1><form method=post><table><tr><td>Article</td>'
          . '<td>Quantity</td><td>Total price</td><td></td></tr>';
        foreach ($purchases as $purchase) {
          $total += $purchase['dtQuantity'] * $purchase['dtPrice'];
          $out .= "<tr><td>{$purchase['dtName']}</td><td>{$purchase['dtQuantity']}</td><td>€" .
            $purchase['dtQuantity'] * $purchase['dtPrice'] . "</td><td><button
            name=r value={$purchase['idPerfume']}>remove</button></td></tr>";
        }
        $out .= "<tr><td colspan=2>Total</td><td colspan=2>€$total</td></tr></table></form>";
        echo $out;
      }
    ?>
  </body>
</html>
5.3.27.3. MicroQuack

Create a simple Internet message board with the following features:

  • A user needs to register first with a user name and password. Users are stored in a user table, which you create via a SQL script. Your app needs to be protected against SQL injection attempts. You may not store plaintext passwords in your DB.

  • A logged-in user sees a list of all messages together with their timestamp and author. Messages are stored in a message table, which you create via a SQL script.

  • A logged-in user can write a new message or edit or delete one of his previous messages.

5.3.27.3.1. Solution

createDB.sql

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
USE evegi144;

DROP TABLE IF EXISTS tblMessage;
DROP TABLE IF EXISTS tblUser;
CREATE TABLE tblUser (
  idUser     INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  dtUserName VARCHAR(50)  NOT NULL UNIQUE,
  dtPassword VARCHAR(255) NOT NULL
)
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;

CREATE TABLE tblMessage (
  idMessage   INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  dtMessage   TEXT         NOT NULL,
  dtTimestamp TIMESTAMP                DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  fiUser      INT UNSIGNED NOT NULL,
  FOREIGN KEY (fiUser) REFERENCES tblUser (idUser)
    ON DELETE CASCADE
    ON UPDATE CASCADE
)
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;

main.php

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<?php
  ini_set('session.cookie_secure', true);
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  if (!isset($_SERVER['HTTPS']))
    header('Location: https://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']));
  if (!isset($_SESSION)) session_start();
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  require_once 'db_credentials.php';
  $dbc = new mysqli(DB_HOST, DB_USER, DB_PW, DB_NAME) or
    die("Connect error: $dbc->connect_error");
  $dbc->set_charset('utf8mb4');
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>MicroQuack</title>
  </head>
  <body>
    <?php
      if (!isset($_SESSION['user_id'])) {
        if (isset($_POST['login'])) {
          $un = $dbc->real_escape_string($_POST['user']);
          $query = "SELECT * FROM tblUser WHERE dtUserName = '$un'";
          $result = $dbc->query($query) or die('SQL error: ' . $dbc->error);
          if ($result->num_rows !== 1) echo 'Invalid login!';
          else {
            $row = $result->fetch_assoc();
            if (password_verify($_POST['password'], $row['dtPassword'])) {
              $_SESSION['user_id'] = $row['idUser'];
              header('Location: index.php');
            } else echo 'Wrong password!';
          }
        } elseif (isset($_POST['register'])) {
          $un = $dbc->real_escape_string($_POST['user']);
          $query = "SELECT * FROM tblUser WHERE dtUserName = '$un'";
          $result = $dbc->query($query) or die('SQL error: ' . $dbc->error);
          if ($result->num_rows > 0) echo 'User name is already taken!';
          $pw = password_hash($_POST['password'], PASSWORD_DEFAULT);
          $query = "INSERT INTO tblUser(dtUserName, dtPassword) VALUES('$un', '$pw')";
          $dbc->query($query) or die('SQL error: ' . $dbc->error);
        }
        echo <<<EOT
          <form method=post>
            <input placeholder=user name=user required> 
            <input type=password placeholder=password name=password required>
            <button name=login>Login</button>
            <button name=register>Register</button>
          </form>
EOT;
      } elseif (isset($_POST['logout'])) {
        $_SESSION = [];
        if (session_id() != "" || isset($_COOKIE[session_name()]))
          setcookie(session_name(), '', 1, rawurlencode(dirname($_SERVER['PHP_SELF'])));
        session_destroy();
        header('Location: index.php');
      } elseif (isset($_POST['save'])) {
        $msg = $dbc->real_escape_string($_POST['message']);
        $query = "INSERT INTO tblMessage(dtMessage, fiUser) VALUES('$msg', 
          {$_SESSION['user_id']})";
        $dbc->query($query) or die('SQL error: ' . $dbc->error);
        header('Location: index.php');
      } elseif (isset($_POST['delete'])) {
        $id = $_POST['delete'];
        $query = "DELETE FROM tblMessage WHERE idMessage=$id";
        $dbc->query($query) or die('SQL error: ' . $dbc->error);
        header('Location: index.php');
      } elseif (isset($_POST['edit'])) {
        $id = $_POST['edit'];
        $query = "SELECT dtMessage FROM tblMessage WHERE idMessage=$id";
        $result = $dbc->query($query) or die('SQL error: ' . $dbc->error);
        $msg = $result->fetch_assoc();
        echo <<<EOT
          <form method=post>
            <textarea name=message required>{$msg['dtMessage']}</textarea> 
            <button name=update value=$id>Update</button>
          </form>
EOT;
      } elseif (isset($_POST['update'])) {
        $id = $_POST['update'];
        $msg = $dbc->real_escape_string($_POST['message']);
        $query = "UPDATE tblMessage SET dtMessage='$msg' WHERE idMessage=$id";
        $dbc->query($query) or die('SQL error: ' . $dbc->error);
        header('Location: index.php');
      } else {
        echo <<<EOT
        <form method=post> 
            <button name=logout>Logout</button>
        </form>
EOT;
        $query = 'SELECT * FROM tblMessage';
        $result = $dbc->query($query) or die('SQL error: ' . $dbc->error);
        if ($result->num_rows > 0) {
          $msg_table = '<table><tr><th>Id</th><th>Message</th><th>User</th><th>Timestamp</th>';
          $msg_table .= '<th></th><th></th></tr>';
          while ($msg = $result->fetch_assoc()) {
            $query = "SELECT dtUserName FROM tblUser WHERE idUser = {$msg['fiUser']}";
            $res = $dbc->query($query) or die('SQL error: ' . $dbc->error);
            $user = $res->fetch_assoc();
            //$messages[] = $msg;
            $msg_table .= "<tr><td>{$msg['idMessage']}</td>";
            $msg_table .= "<td><textarea readonly>{$msg['dtMessage']}</textarea></td>";
            $msg_table .= "<td>{$user['dtUserName']}</td>";
            $msg_table .= "<td>{$msg['dtTimestamp']}</td>";
            $msg_table .= '<td>';
            if ($_SESSION['user_id'] === $msg['fiUser'])
              $msg_table .= '<form method=post><button name=delete value=' .
                $msg['idMessage'] . '>delete</button></form>';
            $msg_table .= '</td><td>';
            if ($_SESSION['user_id'] === $msg['fiUser'])
              $msg_table .= '<form method=post><button name=edit value=' .
                $msg['idMessage'] . '>edit</button></form>';
            $msg_table .= '</td></tr>';
          }
          echo $msg_table . '</table>';
        }
        echo <<<EOT
        <form method=post>
            <textarea name=message required></textarea> 
            <button name=save>Save</button>
        </form>
EOT;
      }
    ?>
  </body>
</html>

5.4. Node.js

From nodejs.org:

Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js' package ecosystem, npm, is the largest ecosystem of open source libraries in the world.

You can play with Node right in your browser using npm.runkit.com.

Note that even though Node is single threaded, you can still have race conditions as is the case with JavaScript too, see medium.com/@slavik57/async-race-conditions-in-javascript-526f6ed80665.

5.4.1. Installation

Verify that the NODE_PATH environment variable is set, otherwise Node might not find modules:

find /etc -type f -exec grep -F NODE_PATH {} +

If it is not set, create the file nodejs.sh in /etc/profile.d with this content:

NODE_PATH=/usr/lib/nodejs:/usr/lib/node_modules:/usr/share/javascript
export NODE_PATH

If required uncomment the respective line in the nodejs.sh file. Then you need to login again in order for the variable to be set.

5.4.2. NPM

NPM is the Node package manager.

Create a directory for your project. Then, inside this directory, run npm init.

Read www.tutorialspoint.com/understanding-the-npm-scripts-in-node-js and use npm start to run your project.

All the commands can be found at docs.npmjs.com/cli-documentation.

Make sure that /usr/lib/node_modules and its subfolders are readable and accessible by everyone. In order to achieve this, run umask 022 before running npm installation.

The current NPM version installs modules as belonging to user root but older versions used user nobody (cf. stackoverflow.com/questions/44633419/no-access-permission-error-with-npm-global-install-on-docker-image).

To install a package globally, use -g. It is however recommended to install packages locally.

To upgrade NPM, run:

npm i -g npm@latest

To see a list of all outdated packages:

npm outdated -g ---depth=0

To update all global packages the documentation claims you can use:

npm up -g

However, this does not work. See gist.github.com/othiym23/4ac31155da23962afd0e for a solution.

To determine where global modules are installed:

npm root -g
Here is further info on where npm installs modules. To change the global installation directory, see here.

To determine the version of a globally installed module:

npm list -g <module>

To uninstall npm use npm uninstall npm -g (cf. askubuntu.com/questions/873729/how-to-uninstall-corrupted-npm).

If you get gyp WARN EACCES user "root" does not have permission to access the dev dir or something similar when trying to install a module using NPM version <7, check github.com/GoogleChrome/puppeteer/issues/375. This may help:

npm i -g <module> --unsafe-perm=true

The reason why this is not a problem anymore with NPM version 7 or higher is:

When npm is run as root, scripts are always run with the effective uid and gid of the working directory owner.

Whereas before it was:

If npm was invoked with root privileges, then it will change the uid to the user account or uid specified by the user config, which defaults to nobody. Set the unsafe-perm flag to run scripts with root privileges.

5.4.4. Parcel web application bundler

5.4.5. Introduction

Start your journey at nodejs.dev/en/learn.

There are a number of tutorials that you might find useful, such as www.youtube.com/watch?v=TlB_eWDSMt4, The Art of Node, NodeSchool, ultra fast apps using Node.js or learnyounode.

freeCodeCamp also have a good introductory tutorial.

Next you should study the documentation at Node.js and some of the free books found at JSbooks.

You can also take an online course and/or certification, see for example digitaldefynd.com/best-nodejs-courses-class-certification-online.

For a more detailed explanation of the Node runtime, see jscomplete.com/learn/node-beyond-basics/learning-node-runtime.

Avoid running Node apps as root: syskall.com/dont-run-node-dot-js-as-root.

For debugging you can dump an object at unlimited depth using console.log(util.inspect(result, false, null));.

Synchronous functions and methods tie up the executing process until they return. A single call to a synchronous function might return in a few microseconds or milliseconds, however in high-traffic websites, these calls add up and reduce the performance of the app. Avoid their use in production.

Although Node and many modules provide synchronous and asynchronous versions of their functions, always use the asynchronous version in production. The only time when a synchronous function can be justified is upon initial startup.

A fantastic site to find open source Node projects is bestofjs.org.

In terms of organizing your project files, check gist.github.com/tracker1/59f2c13044315f88bee9.
5.4.5.1. Streams

Streams are a key feature of Node.js and very well explained in medium.freecodecamp.org/node-js-streams-everything-you-need-to-know-c9141306be93.

Here is an example of a transform stream that reads an HTML file and replaces the string {replace} with Godzilla.

5.4.5.1.1. streams1.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import fs from 'fs'
const rs = fs.createReadStream('streamtest.html')
const ws = fs.createWriteStream('streamtestout.html')
import stream from 'stream'

const transform = new stream.Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().replace(/{replace}/g, 'Godzilla'))
    callback()
  }
})

rs.on('error', err => rs.end('Could not read file!'))
rs.pipe(transform).pipe(ws)
5.4.5.1.2. streamtest.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>Hello {replace}</title>
  </head>
  <body>
    <h1>Hello {replace}</h1>
  </body>
</html>
5.4.5.1.3. streamtestout.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>Hello Godzilla</title>
  </head>
  <body>
    <h1>Good morning Godzilla</h1>
  </body>
</html>
5.4.5.2. Hello world web server

The following is a very basic example of an HTTP web server that reads an HTML file from the server and sends it as a response to each client request.

Remember that ports 0-1024 are designated as well-known ports and reserved for privileged services. So please choose a port number larger than 1024 otherwise you’ll need root permission to listen on that port.
5.4.5.2.1. helloworld_web_server.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// curl -k https://localhost:9000
import http from 'http' // include HTTP module
import fs from 'fs' // include File System module
const port = 9000 // Port our server will be listening on. Must be opened on firewall.

const server = http.createServer((req, res) => {
  const rs = fs.createReadStream('helloworld.html') // Read page to be sent to browser.
  rs.on('error', err => res.end('Could not read file!'))
  rs.pipe(res, {
    end: false
  }) // Send it as part of the server response.
  rs.on('end', () => { // Send some additional info to the client.
    res.write('That\'s all folks...')
    res.end() // Do not forget to end the response.
  })
  /*res.writeHead(200)
  console.log(req.headers)
  res.end('hello world\n')*/
})

server.on('listening', () => {
  console.log(`Listening on port ${port}...`)
})

server.on('connection', () => {
  console.log('New connection...')
})

server.on('close', () => {
  console.log('Connection closed...')
})

server.listen(port, err => {
})

Here is the same example using HTTPS. Note that it needs to be run with root permissions, preferably using sudo, to access the private key.

5.4.5.2.2. helloworld_web_server_HTTPS.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// curl -k https://localhost:9000
import https from 'https' // include HTTPS module
import fs from 'fs' // include File System module
const port = 9000 // Port our server will be listening on. Must be opened on firewall.
const options = { // Paths to our private key and certificate.
  key: fs.readFileSync('/etc/letsencrypt/live/students.btsi.lu/privkey.pem'),
  cert: fs.readFileSync('/etc/letsencrypt/live/students.btsi.lu/cert.pem')
}

const server = https.createServer(options, (req, res) => {
  const rs = fs.createReadStream('helloworld.html') // Read page to be sent to browser.
  rs.on('error', err => res.end('Could not read file!'))
  rs.pipe(res, {
    end: false
  }) // Send it as part of the server response.
  rs.on('end', () => { // Send some additional info to the client.
    res.write('That\'s all folks...')
    res.end() // Do not forget to end the response.
  })
  /*res.writeHead(200)
  console.log(req.headers)
  res.end('hello world\n')*/
})

server.on('listening', () => {
  console.log(`Listening on port ${port}...`)
})

server.on('connection', () => {
  console.log('New connection...')
})

server.on('close', () => {
  console.log('Connection closed...')
})

server.listen(port, err => { //http://syskall.com/dont-run-node-dot-js-as-root/
    //let uid = parseInt(process.env.SUDO_UID)
    //if (uid) process.setuid(uid) // Set our server's uid to that user.
    process.setuid(1012) // Set our server's uid to that user.
    console.log('Server\'s UID is now ' + process.getuid())
  }
)
5.4.5.3. AJAX and JSON

Let’s see how we can exchange data via JSON between client and server:

5.4.5.3.1. HTTPServer1.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import fs from 'fs'
import http from 'http' // Include the HTTP module.
const server = http.createServer() // Create our HTTP server which is an event emitter.
const port = 9000 // Port number our HTTP server will be listening on.

// Add an event listener for the request event.
server.on('request', (req, res) => {
  const {method, url, headers} = req // All headers are represented in lower-case only.
  console.log(method)
  console.log(url)
  console.dir(headers)
  console.log(req.headers['content-type'])
  res.on('error', err => {
    console.error(err)
  })
  if (req.method === 'POST') {
    if (req.headers['content-type'] === 'application/json') {
      let data = ''
      req.on('error', err => {
        console.error(err.stack)
      }).on('data', chunk => {
        data += chunk
      }).on('end', () => {
        if (data) {
          try {
            data = JSON.parse(data)
          } catch (e) {
            console.error(e)
          }
        }
        console.dir(data)
        let login = false
        if (data.un && data.un === 'a1' && data.pw && data.pw === 'a1') login = true
        res.end(JSON.stringify({login: login}))
      })
    }
  } else if (req.method === 'GET') {
    console.dir(req.headers)
    let fileName = 'HTTPClient1.html'
    if (req.url === '/HTTPClient1.css') fileName = 'HTTPClient1.css'
    const rs = fs.createReadStream(fileName)
    rs.on('error', err => res.end('Could not read file!'))
    rs.pipe(res)
  }
})

server.listen(port) // Let server listen for HTTP requests.
server.on('error', err => {
  console.error('Something went wrong...')
})
5.4.5.3.2. HTTPClient1.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>HTTP Client 1</title>
  <link href=node/HTTPClient1.css rel=stylesheet>
  <script type=module>
    const button = document.querySelector('button')
    button.addEventListener('click', () => {
      const inputs = document.querySelectorAll('input')
      if (inputs[0].value && inputs[1].value) {
        fetch('', {
          headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
          },
          method: "POST",
          //credentials: 'same-origin', // https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
          body: JSON.stringify({un: inputs[0].value, pw: inputs[1].value})
        }).then(response => response.json()).then(data => {
          console.dir(data)
        }).catch(error => {
          console.log(`There has been a problem with the fetch operation: ${error.message}`)
        })
      }
    })
  </script>
</head>
<body>
  <input name=user placeholder=username required>
  <input type=password name=pw placeholder=password required>
  <button>Login</button>
</body>
</html>
5.4.5.5. Modules

www.geeksforgeeks.org/import-and-export-in-node-js

www.freecodecamp.org/news/node-module-exports-explained-with-javascript-export-function-examples

medium.freecodecamp.org/requiring-modules-in-node-js-everything-you-need-to-know-e7fbd119be8

hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive

ECMAScript modules are the official standard format to package JavaScript code for reuse. Modules are defined using a variety of import and export statements. Node.js fully supports ECMAScript modules as they are currently specified and provides interoperability between them and its original module format, CommonJS.

Our ECMAScript modules should have extension .mjs.

Note that you may not use {} to import default exports, i.e. use

import defaultExport from "module-name"

and not

import {defaultExport} from "module-name"
For a very detailed look at ECMAScript modules, see 2ality.com/2022/01/esm-specifiers.html.
If you have to, you can mix import and require in the same file.
Globally installed Node modules cannot easily be imported as ECMAScript modules, as the NODE_PATH environment variable is not used.
5.4.5.7. Asynchronous programming using promises and async/await
5.4.5.7.1. async1.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const fun1 = ms => setTimeout(() => console.log("x0"), ms)
fun1(1000)

const fun2 = ms => new Promise((resolve, reject) => setTimeout(() => resolve("x1"), ms))
fun2(3000).then(value => {
    console.log(value)
    console.log("x2")
})
console.log("x3")

const res = await(fun2(5000))
console.log(`Result from await: ${res}`)
5.4.5.7.2. async2.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import https from 'https'

// https://apidog.com/articles/call-rest-api-node-js/#step-4-an-alternative-approach
const getData = f => {
    let data = ''
    const request = https.get('https://students.btsi.lu', response => {
        response.setEncoding('utf8')
        response.on('data', chunk => {
            data += chunk
        })

        response.on('end', () => {
            f(data)
        })
    })

    request.on('error', (error) => {
        f(error)
    })

    request.end()
}

console.log('x1')
getData(console.log)
console.log('x2')

5.4.6. WebSockets

Visualising WebSockets traffic in Firefox

WebSocketAPI

WebSocket protocol

WebSocket protocol name registry

Overview of the main WebSocket packages for Node

Here is a great introductory article.

samsaffron.com/archive/2015/12/29/websockets-caution-required

www.terlici.com/2015/11/26/realtime-node-expressjs-with-websockets.html

banksco.de/p/state-of-realtime-web-2016.html

deepstream.io/tutorials/concepts/what-is-deepstream

blog.sessionstack.com/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path-584e6b8e3bf7

hackernoon.com/implementing-a-websocket-server-with-node-js-d9b78ec5ffa8 provides a deep dive into the details of WebSockets and how to implement them yourself in Node.

blog.securityevaluators.com/websockets-not-bound-by-cors-does-this-mean-2e7819374acc

www.freecodecamp.org/news/how-to-secure-your-websocket-connections-d0be0996c556

ably.com/topic/websockets-vs-http

Here’s a useful tool to test your WebSocket server.
5.4.6.1. ws
ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and server implementation.

In order to use WebSocket over TLS we need to create a HTTPS server first and use it to create the WSS server. This is so because the HTTPS server takes care of the connection encryption, which takes place before the upgrade request to the WebSocket protocol is received from the client (cf. www.giacomovacca.com/2015/02/websockets-over-nodejs-from-plain-to.html). The example at github.com/websockets/ws/blob/master/examples/ssl.js shows how it can be done.

To have ws reconnect automatically, take a look at github.com/joewalnes/reconnecting-websocket or www.npmjs.com/package/reconnecting-websocket.

Let’s take a look at a basic chat app:

5.4.6.1.1. wsServer1.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const DEBUG = true
const port = 9000
import WebSocket, { WebSocketServer } from 'ws'
const wss = new WebSocketServer({port: port})

wss.on('error', err => {
  console.dir(err)
})

wss.on('connection', (socket, req) => {
  console.log('Client connected...')

  socket.on('error', err => {
    console.dir(err)
  })

  socket.on('message', data => {
    if (DEBUG) console.log(data)
    socket.send(`You said: ${data}<br>`)
  })

  socket.on('close', () => {
    console.log('Socket closed')
  })
})

wss.on('listening', () => {
  console.log('Listening...')
})
5.4.6.1.2. wsClient1.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>ws Client 1</title>
  <script type=module>
    const DEBUG = true
    const input = document.querySelector('input')
    const button = document.querySelector('button')
    const section = document.querySelector('section')
    const wssURL = 'wss://students.btsi.lu/node/wss'
    const wss = new WebSocket(wssURL)
    wss.addEventListener('open', () => {
      if (DEBUG) console.log('WebSocket connection opened.')
      button.addEventListener('click', () => {
        wss.send(input.value)
      })
    })

    wss.addEventListener('message', e => {
      section.innerHTML += e.data
    })

    wss.addEventListener('close', () => {
      if (DEBUG) console.log('Disconnected...')
    })

    wss.addEventListener('error', () => {
      if (DEBUG) console.log('Error...')
    })
  </script>
</head>
<body>
<input placeholder=message>
<button>Send</button>
<br>
<section></section>
</body>
</html>

Here’s the server using HTTPS:

5.4.6.1.3. wsServer1a.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const DEBUG = true
const port = 9000
import fs from 'fs'
import https from 'https'
const server = https.createServer({
  key: fs.readFileSync('/etc/letsencrypt/live/students.btsi.lu/privkey.pem'),
  cert: fs.readFileSync('/etc/letsencrypt/live/students.btsi.lu/cert.pem')
})
import WebSocket, { WebSocketServer } from 'ws'
const wss = new WebSocketServer({server})

wss.on('error', err => {
  console.dir(err)
})

wss.on('connection', (socket, req) => {
  console.log('Client connected...')

  socket.on('error', err => {
    console.dir(err)
  })

  socket.on('message', data => {
    if (DEBUG) console.log(data)
    socket.send(`You said: ${data}<br>`)
  })

  socket.on('close', () => {
    console.log('Socket closed')
  })
})

wss.on('listening', () => {
  console.log('Listening...')
})

server.listen(port)

Here’s a WS server that understands the command getRandomNums with a given number and will reply with as many random numbers as requested. Note that the communication between client and server is handled using JSON:

5.4.6.1.4. wsServer2.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const DEBUG = true
const port = 9000
import WebSocket, { WebSocketServer } from 'ws'
const wss = new WebSocketServer({port: port})

wss.on('error', err => {
  console.dir(err)
})

wss.on('connection', (socket, req) => {
  console.log('Client connected...')

  // Generic send function taking a message string and an options object.
  const send = (msg, options) => {
    const obj = {}
    Object.assign(obj, {msg: msg}, options)
    socket.send(JSON.stringify(obj))
    if (DEBUG) console.log('WS sent:')
    if (DEBUG) console.log(obj)
  }

  socket.on('error', err => {
    console.dir(err)
  })

  socket.on('message', data => {
    try { // Remember that JSON.parse will fail if data is not valid JSON.
      data = JSON.parse(data)
      if (DEBUG) console.dir(data)
      if (data && data.command && data.command === 'getRandomNums' && data.num) {
        const nums = []
        for (let i = 0; i < data.num; i++) nums.push(Math.random())
        send('randomNums', {nums: nums})
      } else if (data && data.command && data.command === 'getFunnyText') {
        send('funnyText', {text: 'This is so funny!'})
      }
    } catch (error) {
      console.error(error)
    }
  })

  socket.on('close', () => {
    console.log('Socket closed')
  })
})

wss.on('listening', () => {
  console.log('Listening...')
})
5.4.6.1.5. wsClient2.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>ws Client 2</title>
  <script type=module>
    const DEBUG = true
    const wssURL = 'wss://students.btsi.lu/node/wss'
    const wss = new WebSocket(wssURL)
    const messageCallbacks = []

    // Generic function to register a callback for a given message.
    const registerMessageCB = (msg, cb) => {
      messageCallbacks.push({msg: msg, cb: cb})
    }

    const displayNums = nums => {
      // Cf. https://exploringjs.com/es6/ch_for-of.html#sec_pitfall-for-of-iterability
      for (const num of Array.from(nums)) console.log(num)
    }

    const displayFunnyText = text => {
      console.log(text.text)
    }

    const execute = (command, options) => {
      let obj = {}
      // Build an object with the given fields.
      Object.assign(obj, {command: command}, options)
      wss.send(JSON.stringify(obj))
      if (DEBUG) {
        console.log('WS sent:')
        console.dir(obj)
      }
    }

    wss.addEventListener('open', () => {
      if (DEBUG) console.log('WebSocket connection opened.')
      // Tell the server to send us 5 random numbers asynchronously.
      execute('getRandomNums', {num: 5})
      execute('getFunnyText')
    })

    wss.addEventListener('message', e => {
      try { // Remember that JSON.parse will fail if data is not valid JSON.
        const data = JSON.parse(e.data)
        if (DEBUG) {
          console.log('WS client received the following message:')
          console.dir(data)
        }
        if (!data.msg) return
        // Let's see if we have one or more callbacks for this message.
        for (const cb of messageCallbacks) if (cb.msg === data.msg) cb.cb(data)
      } catch (error) {
        console.error(error)
      }
    })

    wss.addEventListener('close', () => {
      if (DEBUG) console.log('Disconnected...')
    })

    wss.addEventListener('error', () => {
      if (DEBUG) console.log('Error...')
    })

    // If the server sends a 'randomNums' msg we want 'displayNums' to be called.
    registerMessageCB('randomNums', displayNums)
    registerMessageCB('funnyText', displayFunnyText)
  </script>
</head>
<body></body>
</html>

Now let’s look at WS communication using promises. The server remains unchanged. On the client side however, we need to promisify our execute function. To do this we need to store the resolve function of the promise as the resolve function will only be called after the answer from the server has been received (see stackoverflow.com/questions/26150232/resolve-javascript-promise-outside-function-scope):

5.4.6.1.6. wsClient3.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>ws Client 3</title>
  <script type=module>
    const DEBUG = true
    const wssURL = 'wss://students.btsi.lu/node/wss'
    const wss = new WebSocket(wssURL)
    const promises = []

    wss.addEventListener('open', () => {
      if (DEBUG) console.log('WebSocket connection opened.')

      const displayNums = nums => {
        for (const num of nums) console.log(num)
      }

      const displayFunnyText = text => {
        console.log(text.text)
      }

      // Send a command with options to the server and wait for a given reply msg.
      const execute = (command, options, msg) => {
        return new Promise((resolve, reject) => {
          promises.push({msg: msg, resolve: resolve})
          if (DEBUG) console.log('Promise added')
          let obj = {}
          Object.assign(obj, {command: command}, options)
          wss.send(JSON.stringify(obj)) // Send command to server.
          if (DEBUG) {
            console.log('WS sent:')
            console.dir(obj)
          }
        })
      }

      wss.addEventListener('message', e => {
        try { // Remember that JSON.parse will fail if data is not valid JSON.
          const data = JSON.parse(e.data)
          if (DEBUG) {
            console.log('WS client received the following message:')
            console.dir(data)
          }
          if (!data.msg) return
          // Let's see if we have one or more promises for this message.
          for (const [index, promise] of promises.entries()) if (promise.msg === data.msg) {
            console.log('x1')
            promise.resolve(data) // Fulfil promise.
            console.log('x2')
            promises.splice(index, 1) // Delete promise from waiting list.
          }
        } catch (error) {
          console.error(error)
        }
      })

      wss.addEventListener('close', () => {
        if (DEBUG) console.log('Disconnected...')
      })

      wss.addEventListener('error', () => {
        if (DEBUG) console.log('Error...')
      })

      // Tell server to send us 5 random numbers and wait for result.
      execute('getRandomNums', {num: 5}, 'randomNums').then(data => {
        displayNums(data.nums)
      })
      execute('getFunnyText', {}, 'funnyText').then(data => {
        displayFunnyText(data.text)
      })
      if (DEBUG) console.log('Done!')
    })
  </script>
</head>
<body></body>
</html>

We can also use async and await, like so:

5.4.6.1.7. wsClient4.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<!DOCTYPE html>
<html lang=en>
<head>
    <meta charset=UTF-8>
    <title>ws Client 4</title>
    <script type=module>
        const DEBUG = true
        const wssURL = 'wss://students.btsi.lu/node/wss'
        const wss = new WebSocket(wssURL)
        const promises = []

        wss.addEventListener('open', () => {
            if (DEBUG) console.log('WebSocket connection opened.')

            const displayNums = nums => {
                for (const num of nums) console.log(num)
            }

            const displayFunnyText = text => {
                console.log(text.text)
            }

            // Send a command with options to the server and wait for a given reply msg.
            const execute = (command, options, msg) => {
                return new Promise((resolve, reject) => {
                    promises.push({msg: msg, resolve: resolve})
                    if (DEBUG) console.log('Promise added')
                    let obj = {}
                    Object.assign(obj, {command: command}, options)
                    wss.send(JSON.stringify(obj)) // Send command to server.
                    if (DEBUG) {
                        console.log('WS sent:')
                        console.dir(obj)
                    }
                })
            }

            wss.addEventListener('message', e => {
                try { // Remember that JSON.parse will fail if data is not valid JSON.
                    const data = JSON.parse(e.data)
                    if (DEBUG) {
                        console.log('WS client received the following message:')
                        console.dir(data)
                    }
                    if (!data.msg) return
                    // Let's see if we have one or more promises for this message.
                    for (const [index, promise] of promises.entries()) if (promise.msg === data.msg) {
                        promise.resolve(data) // Fulfil the promise.
                        promises.splice(index, 1) // Delete the promise from the waiting list.
                    }
                } catch (error) {
                    console.error(error)
                }
            })

            wss.addEventListener('close', () => {
                if (DEBUG) console.log('Disconnected...')
            })

            wss.addEventListener('error', () => {
                if (DEBUG) console.log('Error...')
            });

            // Tell the server to send us 5 random numbers and wait for the result.
            (async () => {
                const obj = await execute('getRandomNums', {num: 5}, 'randomNums')
                displayNums(obj.nums)
                const obj = await execute('getFunnyText', {}, 'funnyText')
                displayFunnyText(obj.text)
                if (DEBUG) console.log('Done!')
            })()
        })
    </script>
</head>
<body></body>
</html>
5.4.6.1.8. Displaying images received from the server

Displaying images from the server can be done as follows, assuming that data contains the binary data obtained from the server:

1
2
3
4
5
6
7
const image = new Image()
const buffer = Uint8Array.from(data)
const blob = new Blob([buffer])
const URL = window.URL || window.webkitURL
// https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
image.src = URL.createObjectURL(blob)
document.querySelector('body').appendChild(image)
5.4.6.1.9. Sending binary data embedded in JSON to the server

Blob, ArrayBuffer, File etc. are not part of JSON. So the tricky part is to read the image file as ArrayBuffer and convert it into an array that can then be stringified. On the server side we then need to turn the array into a buffer which we can write to disc.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>ws Image Client 1</title>
  <script type=module>
    const DEBUG = true
    const input = document.querySelector('input')
    const section = document.querySelector('section')
    const wssURL = 'wss://students.btsi.lu/node/wss'
    const wss = new WebSocket(wssURL)
    wss.addEventListener('open', e => {
      if (DEBUG) console.log('WebSocket connection opened.')
      input.addEventListener('change', e => {
        const files = e.target.files
        if (DEBUG) console.dir(files)
        const isValidType = type => {
          const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif',
            'image/bmp']
          for (const validType of validTypes) if (type === validType) return true
          return false
        }
        for (const file of files) {
          if (isValidType(file.type)) {
            if (DEBUG) console.log('Valid type: ' + file.type)
            let fr = new FileReader()
            fr.onload = e => {
              const array = Array.from(new Uint8Array(e.target.result))
              wss.send(JSON.stringify({filename: file.name, data: array}))
            }
            fr.readAsArrayBuffer(file)
          } else alert('Only the following file types are supported: jpeg/jpg, png, gif and' +
            ' bmp')
        }
      })
    })

    wss.addEventListener('message', e => {
      section.innerHTML += e.data
    })

    wss.addEventListener('close', () => {
      if (DEBUG) console.log('Disconnected...')
    })

    wss.addEventListener('error', () => {
      if (DEBUG) console.log('Error...')
    })
  </script>
</head>
<body>
  <input type=file multiple>
  <section></section>
</body>
</html>
5.4.6.1.10. wsImageServer1.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const DEBUG = true
const port = 9000
import WebSocket, { WebSocketServer } from 'ws'
const wss = new WebSocketServer({port: port})
import fs from 'fs'

wss.on('error', err => {
  console.dir(err)
})

wss.on('connection', (socket, req) => {
  if (DEBUG) console.log('Client connected...')

  // Generic send function taking a message string and an options object.
  const send = (msg, options) => {
    const obj = {}
    Object.assign(obj, {msg: msg}, options)
    socket.send(JSON.stringify(obj))
    if (DEBUG) console.log('WS sent:')
    if (DEBUG) console.log(obj)
  }

  socket.on('error', err => {
    console.dir(err)
  })

  socket.on('message', async data => {
    try { // Remember that JSON.parse will fail if data is not valid JSON.
      data = JSON.parse(data)
      if (DEBUG) console.dir(data)
      fs.writeFile(data.filename, Buffer.from(data.data), err => {
        if (err) console.dir(err)
      })
      send(data.filename + ' successfully saved.')
    } catch(error) {
      console.error(error)
    }
  })

  socket.on('close', () => {
    if (DEBUG) console.log('Socket closed')
  })
})

wss.on('listening', () => {
  if (DEBUG) console.log('Listening...')
})
5.4.6.2. uws

uws (www.npmjs.com/package/uws) offers the same functionality as ws but is much faster. See hackernoon.com/%C2%B5ws-as-your-next-websocket-library-d34209686357 for an illustration of the difference it can make.

5.4.6.3. Socket.io
github.com/socketio/socket.io or www.npmjs.com/package/socket.io enables real-time bidirectional event-based communication. It works on every platform, browser or device, focusing equally on reliability and speed.
  1. In order for the client to access the socket.io.js file, you need to specify the correct path to it. If you installed Socket.io locally using npm it will be /socket.io/socket.io.js.

  2. If you want to access the client as a normal HTML file without having to specify the socket port, you need to specify the domain and port in the Socket.io constructor call:

    1
    2
    3
    4
    
    <script src=socket.io.js></script>
    <script>
        const socket = io('https://everling.lu:9000');
    </script>
    
  3. To turn debugging on on the client enter localStorage.debug = '*'; in the console. To turn it off, set the value to ''. To log only certain debug messages, just specify what you want, e.g. localStorage.debug = 'engine.io-client:polling, engine.ioclient:socket'; (cf. "Socket.IO Cookbook").

  4. To turn on debugging on the server either start the server using DEBUG=* node server or set the DEBUG environment variable. using export DEBUG=* and start the server. To stop server logging, set the variable to null.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const io = require('socket.io')
const https = require('https') // include HTTPS module
const fs = require('fs') // include File System module
const port = 9000 // Port our server will be listening on. Must be opened on firewall.
const options = { // Paths to our private key and certificate.
  key: fs.readFileSync('/etc/letsencrypt/live/students.btsi.lu/privkey.pem'),
  cert: fs.readFileSync('/etc/letsencrypt/live/students.btsi.lu/cert.pem')
}
const server = https.createServer(options, (req, res) => {
  res.writeHead(200)
  console.log(req.headers)
  res.end('hello world\n')
})

server.on('listening', () => {
  console.log(`Listening on port ${port}...`)
})

server.on('connection', () => {
  console.log('New connection...')
})

server.on('close', () => {
  console.log('Connection closed...')
})

server.listen(port)
const socket_server = io(server)
socket_server.on('connection', socket => {
  socket.emit('news', {hello: 'world'})
  socket.on('my other event', data => {
    console.log(data)
  })
})
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>Socket.io client 1</title>
    <script src=socket.io.js></script> <!-- File needs to be in the same directory as this
    file. Alternatively, use src=https://everling.lu:9000/socket.io/socket.io.js if Socket.io
    was installed locally in which case the Socket.io server will serve it automatically at
    this link. -->
    <script type=module>
      const socket = io('https://students.btsi.lu/node')
      socket.on('news', data => {
        console.log(data)
        socket.emit('my other event', {my: 'data'})
      })
    </script>
  </head>
  <body>
  </body>
</html>
5.4.6.3.2. Using Socket.io behind a reverse proxy

5.4.7. Express.js

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
5.4.7.1. Getting started

Walk through expressjs.com/en/starter/installing.html and the following pages for an easy start.

5.4.7.2. Form handling
5.4.7.3. HTTPFormClient1.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>HTTP Form Client 1</title>
    <link href=node/HTTPClient1.css rel=stylesheet>
  </head>
  <body>
    <form method=post>
      <input name=user placeholder=username required>
      <input type=password name=pw placeholder=password required>
      <button>Login</button>
    </form>
  </body>
</html>
5.4.7.4. HTTPFormServer1.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import fs from 'fs'
import express from 'express'
const app = express()
const port = 9000 // Port number that our HTTP server will be listening on.

app.use(express.urlencoded({extended: true}))
app.post('', (req, res) => {
  console.dir(req.body)
  const {method, url, headers} = req // All headers are represented in lower-case only.
  console.log(method)
  console.log(url)
  console.log(req.headers['content-type'])
  if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
    if (req.body) {
      console.dir(req.body)
      let login = false
      if (req.body.user && req.body.pw) {
        if (req.body.user === 'a1' && req.body.pw === 'a1') login = true
        res.send(login ? '<h1>logged in</h1>' : '<h1>invalid credentials</h1>')
      } else res.send('<h1>Something went wrong!</h1>')
    } else
      res.send('<h1>Empty request body...</h1>')
  } else
    res.send('<h1>Nothing to see here...</h1>')
})

app.get('/HTTPClient1.css', (req, res) => {
  console.log(req.url)
  res.on('error', err => {
    console.error(err)
  })
  try {
    const rs = fs.createReadStream('HTTPClient1.css')
    rs.on('error', err => res.end('Could not read file!'))
    rs.pipe(res)
  } catch (e) {
    res.send('<h1>Could not read file!</h1>')
  }
})

app.get('', (req, res) => {
  console.log(req.url)
  res.on('error', err => {
    console.error(err)
  })
  try {
    const rs = fs.createReadStream('HTTPFormClient1.html')
    rs.on('error', err => res.end('Could not read file!'))
    rs.pipe(res)
  } catch (e) {
    res.send('<h1>Could not read file!</h1>')
  }
})

app.listen(port) // Let the server listen for HTTP requests.

We can make our life easier by creating a folder where we put all the files that we want to be publicly accessible and then create a static route in Express as explained in expressjs.com/en/starter/static-files.html.

5.4.7.5. HTTPFormClient2.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>HTTP Form Client 1</title>
    <link href=node/HTTPFormClient2.css rel=stylesheet>
  </head>
  <body>
    <form method=post>
      <input name=user placeholder=username required>
      <input type=password name=pw placeholder=password required>
      <button>Login</button>
    </form>
  </body>
</html>
5.4.7.6. HTTPFormClient2.css
1
2
3
body {
  background-color: orange;
}
5.4.7.7. HTTPFormServer2.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import fs from 'fs'
import express from 'express'
const app = express()
const port = 9000 // Port number that our HTTP server will be listening on.

app.use(express.static('./public'))
app.use(express.urlencoded({extended: true}))
app.post('/', (req, res) => {
  const {method, url, headers} = req // All headers are represented in lower-case only.
  console.log(method)
  console.log(url)
  console.log(req.headers['content-type'])
  if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
    if (req.body) {
      console.dir(req.body)
      let login = false
      if (req.body.user && req.body.pw) {
        if (req.body.user === 'a1' && req.body.pw === 'a1') login = true
        res.end(login ? '<h1>logged in</h1>' : '<h1>invalid credentials</h1>')
      } else res.end('<h1>Something went wrong!</h1>')
    } else
      res.end('<h1>Empty request body...</h1>')
  } else
    res.end('<h1>Nothing to see here...</h1>')
})

app.get('/', (req, res) => {
  console.log(req.url)
  res.on('error', err => {
    console.error(err)
  })
  try {
    const rs = fs.createReadStream('HTTPFormClient2.html')
    rs.on('error', err => res.end('Could not read file!'))
    rs.pipe(res)
  } catch (e) {
    res.end('<h1>Could not read file!</h1>')
  }
})

app.listen(port) // Let the server listen for HTTP requests.

Here is a slightly more elaborated example where we give the user 3 login attempts after which his IP address will be blocked for 5 minutes:

5.4.7.8. HTTPFormServer3.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import fs from 'fs'
import express from 'express'
const app = express()
const port = 9000 // The port number that our HTTP server will be listening on.
const blackListedIPs = {}, failedAttemptIPs = {}

const blacklist = (req, res, next) => {
  const IP = req.headers['x-forwarded-for']
  if (blackListedIPs[IP]) {
    if (blackListedIPs[IP] < Date.now()) {
      delete blackListedIPs[IP]
      delete failedAttemptIPs[IP]
    } else {
      console.log(`${IP} is blacklisted`)
      res.end('<h1>You have been blacklisted!</h1>')
    }
  }
  next()
}

app.use(express.static('./public'))
app.use(express.urlencoded({extended: true}))
app.use(blacklist)
app.post('/', (req, res) => {
  console.dir(req.headers['content-type'])
  if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
    if (req.body) {
      if (req.body.user && req.body.pw) {
        if (req.body.user === 'a1' && req.body.pw === 'a1')
          res.end('<h1>logged in</h1>')
        else {
          const IP = req.headers['x-forwarded-for']
          if (failedAttemptIPs[IP]) failedAttemptIPs[IP]++
          else failedAttemptIPs[IP] = 1
          if (failedAttemptIPs[IP] === 3) blackListedIPs[IP] = Date.now() + 300000
          res.end(`<h1>Invalid credentials.<br>You have ${3- failedAttemptIPs[IP]} attempts left</h1>`)
        }
      } else res.end('<h1>Something went wrong!</h1>')
    } else
      res.end('<h1>Empty request body...</h1>')
  } else
    res.end('<h1>Nothing to see here...</h1>')
})

app.get('/', (req, res) => {
  console.log(req.url)
  res.on('error', err => {
    console.error(err)
  })
  try {
    const rs = fs.createReadStream('HTTPFormClient2.html')
    rs.on('error', err => res.end('Could not read file!'))
    rs.pipe(res)
  } catch (e) {
    res.end('<h1>Could not read file!</h1>')
  }
})

app.listen(port) // Let the server listen for HTTP requests.

Here is a more elegant solution to the same problem:

5.4.7.9. BlackList.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class BlackList {
  constructor() {
    const failedAttempts = {}
    const blackList = {}

    this.failedAttempt = ip => {
      if (typeof ip !== 'string' && !(ip instanceof String)) ip = toString(ip)
      if (!failedAttempts[ip]) {
        failedAttempts[ip] = 1
        return 2
      } else {
        failedAttempts[ip]++
        if (failedAttempts[ip] === 3) {
          blackList[ip] = Date.now() + 300000
          return 0
        }
        return 1
      }
    }

    this.isBlackListed = ip => {
      if (typeof ip !== 'string' && !(ip instanceof String)) ip = toString(ip)
      if (blackList[ip])
        if (blackList[ip] >= Date.now()) return true
        else {
          delete failedAttempts[ip]
          delete blackList[ip]
        }
      return false
    }
  }
}

export default BlackList
5.4.7.10. HTTPFormServer4.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import fs from 'fs'
import express from 'express'
import BlackList from './BlackList.mjs'
const app = express()
const port = 9000 // Port number that our HTTP server will be listening on.
const blackList = new BlackList()

const blacklist = (req, res, next) => {
  const ip = req.headers['x-forwarded-for']
  if (blackList.isBlackListed(ip)) {
    console.log(`${ip} is blacklisted`)
    res.send('<h1>You have been blacklisted!</h1>')
  }
  next()
}

app.use(express.static('public'))
app.use(express.urlencoded({extended: true}))
app.use(blacklist)
app.post('/', (req, res) => {
  console.dir(req.headers['content-type'])
  if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
    if (req.body) {
      console.dir(req.body)
      if (req.body.user && req.body.pw) {
        if (req.body.user === 'a1' && req.body.pw === 'a1')
          res.send('<h1>logged in</h1>')
        else {
          const failures = blackList.failedAttempt(req.headers['x-forwarded-for'])
          res.end(`<h1>Invalid credentials.<br>You have ${failures} attempts left</h1>`)
        }
      } else res.send('<h1>Something went wrong!</h1>')
    } else
      res.send('<h1>Empty request body...</h1>')
  } else
    res.send('<h1>Nothing to see here...</h1>')
})

app.get('/', (req, res) => {
  console.log(req.url)
  res.on('error', err => {
    console.error(err)
  })
  try {
    const rs = fs.createReadStream('HTTPFormClient2.html')
    rs.on('error', err => res.end('Could not read file!'))
    rs.pipe(res)
  } catch (e) {
    res.send('<h1>Could not read file!</h1>')
  }
})

app.listen(port) // Let the server listen for HTTP requests.

The AJAX and JSON example from above can be implemented and enhanced using Express like so:

5.4.7.11. HTTPServer2.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import fs from 'fs'
import express from 'express'
const app = express()
const port = 9000 // The port number that our HTTP server will be listening on.

const logger = (req, res, next) => {
  const d = new Date()
  let s = `${d.getDate()} ${d.getMonth()} ${d.getFullYear()} ${d.getHours()}`
  s += ` ${d.getMinutes()} ${d.getSeconds()}`
  console.log(s)
  next()
}

//app.use(logger)
app.post('/', (req, res) => {
  console.dir(req)
  const {method, url, headers} = req // All headers are represented in lower-case only.
  console.log(method)
  console.log(url)
  console.dir(headers)
  console.log(req.headers['content-type'])
  if (req.headers['content-type'] === 'application/json') {
    let data = ''
    req.on('error', err => {
      console.error(err.stack)
    }).on('data', chunk => {
      data += chunk
    }).on('end', () => {
      if (data) {
        try {
          data = JSON.parse(data)
        } catch (e) {
          console.error(e)
        }
      }
      console.dir(data)
      res.on('error', err => {
        console.error(err)
      });
      let login = false
      if (data.un === 'a1' && data.pw === 'a1') login = true
      res.end(JSON.stringify({login: login}))
    })
  }
})
/*app.get('/*', (req, res) => {
  console.log(req.url)
  res.on('error', err => {
    console.error(err)
  })
  try {
    if (req.url === '/HTTPClient1.css') {
      const rs = fs.createReadStream('HTTPClient1.css')
      rs.on('error', err => res.end('Could not read file!'))
      rs.pipe(res)
    } else {
      const rs = fs.createReadStream('HTTPClient1.html')
      rs.on('error', err => res.end('Could not read file!'))
      rs.pipe(res)
    }
  } catch (e) {
    res.send('<h1>Could not read file!</h1>')
  }
})*/

app.get('/HTTPClient1.css', (req, res) => {
  console.log(req.url)
  res.on('error', err => {
    console.error(err)
  })
  try {
    const rs = fs.createReadStream('HTTPClient1.css')
    rs.on('error', err => res.end('Could not read file!'))
    rs.pipe(res)
  } catch (e) {
    res.send('<h1>Could not read file!</h1>')
  }
})

app.get('/', (req, res) => {
  console.log(req.url)
  res.on('error', err => {
    console.error(err)
  })
  try {
    const rs = fs.createReadStream('HTTPClient1.html')
    rs.on('error', err => res.end('Could not read file!'))
    rs.pipe(res)
  } catch (e) {
    res.send('<h1>Could not read file!</h1>')
  }
})

app.listen(port) // Let the server listen for HTTP requests.

If our server is running behind a reverse proxy, we can use app.set('trust proxy', true); to get the client IP address (cf. stackoverflow.com/questions/46295635/how-to-get-ip-address-in-node-js-express and expressjs.com/en/guide/behind-proxies.html).

5.4.7.12. Sending files to the server

To make our life easy we can use formidable. Also see www.w3schools.com/nodejs/nodejs_uploadfiles.asp.

fileServer.mjs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import fs from 'fs'
import formidable from 'formidable'
import express from 'express'

const app = express()
const port = 9000

app.get('', (req, res) => {
    res.on('error', err => {
        console.error(err)
    })
    try {
        let filenameToServe = 'fileClient.html'
        const rs = fs.createReadStream(filenameToServe)
        rs.on('error', err => res.end('Could not read file!'))
        rs.pipe(res)
    } catch (e) {
        res.end('<h1>Could not read file!</h1>')
    }
})

app.post('', (req, res) => {
    const form = formidable({multiples: true, uploadDir: 'uploads'})
    form.parse(req, (err, fields, files) => {
        if (err) {
            console.error(err)
            return
        }
        console.dir(files.modelFiles)
        const handleFile = file => {
            const oldPath = file.filepath
            const newPath = './uploads/' + file.originalFilename
            try {
                fs.renameSync(oldPath, newPath)
            } catch (error) {
                console.error('The following error occured: ' + error)
            }
        } // multiple files
        if (Array.isArray(files.modelFiles))
            for (const file of files.modelFiles) handleFile(file)
        else handleFile(files.modelFiles) // single file
        res.json({fields, files})
    })
})

app.listen(port)

fileClient.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
  <meta charset=utf-8>
  <meta name=viewport content='width=device-width, initial-scale=1'>
  <title>File client</title>
</head>
<body>
<form enctype=multipart/form-data method=post>
  <input type=file name=modelFiles multiple>
  <button>Send</button>
</form>
</body>
</html>
5.4.7.14. Creating a combined HTTP and WebSocket server

Using the wonderful express-ws package, we can easily create a combined HTTP and ws server:

5.4.7.14.1. expressWSServer1.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import fs from 'fs'
import http from 'http'
import express from 'express'
import expressWs from 'express-ws'

const port = 9000; // The port number that our HTTP server will be listening on.
const app = express()
const server = http.createServer(app)
const wsAPI = expressWs(app, server)

//app.set('trust proxy', true) // https://expressjs.com/en/guide/behind-proxies.html

app.post('', (req, res) => {
  if (req.headers['content-type'] === 'application/json') {
    let data = ''
    req.on('error', err => {
      console.error(err.stack)
    }).on('data', chunk => {
      data += chunk
    }).on('end', () => {
      if (data) {
        try {
          data = JSON.parse(data)
        } catch (e) {
          console.error(e)
        }
      }
      console.dir(data)
      res.on('error', err => {
        console.error(err)
      })
      let login = false
      if (data.un === 'a1' && data.pw === 'a1') login = true
      res.end(JSON.stringify({login: login}))
      console.log('*** POST ***')
    })
  }
})

app.get('', (req, res) => {
  res.on('error', err => {
    console.error(err)
  })
  try {
    console.log('*** GET ***')
    const rs = fs.createReadStream('expressWSClient1.html')
    rs.on('error', err => res.end('Could not read file!'))
    rs.pipe(res)
  } catch (e) {
    res.send('<h1>Could not read file!</h1>')
  }
})

const wsServer = wsAPI.getWss()
wsServer.on('connection', (socket, req) => {
  console.log('New connection')
  socket.on('close', () => {
    console.log('Websocket closed')
  })
})
wsServer.on('listening', () => {
  console.log('Listening')
})

app.ws('', (ws, req) => {
  ws.on('error', err => {
    console.error(err)
  })
  ws.on('message', msg => {
    console.log(msg)
    console.log('*** WS ***')
    ws.send(msg)
    console.log('Connected clients:')
    console.log(wsServer.clients.size)
  })
})

server.listen(port)
5.4.7.14.2. expressWSClient1.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>Express WS Client 1</title>
  <script type=module>
    const wssURL = 'wss://students.btsi.lu/node/wss'
    const buttons = document.querySelectorAll('button')
    const inputs = document.querySelectorAll('input')
    const section = document.querySelector('section')
    buttons[0].addEventListener('click', () => {
      if (inputs[0].value && inputs[1].value) {
        fetch('', {
          headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
          },
          method: "POST",
          credentials: 'same-origin', // https://github.com/expressjs/session/issues/374
          body: JSON.stringify({un: inputs[0].value, pw: inputs[1].value})
        }).then(response => response.json()).then(data => {
          console.dir(data)
        }).catch(error => {
          console.log(`There has been a problem with the fetch operation: ${error.message}`)
        })
        /*const req1 = new XMLHttpRequest()
        req1.open('POST', '')
        req1.addEventListener('load', e => {
          console.dir(JSON.parse(e.target.response))
        })
        req1.send(JSON.stringify({un: inputs[0].value, pw: inputs[1].value}))*/
      }
    })
    const wss = new WebSocket(wssURL)
    wss.addEventListener('open', () => {
      console.log('WebSocket connection opened.')
      buttons[1].addEventListener('click', () => {
        wss.send(inputs[2].value)
      })
    })

    wss.addEventListener('message', e => {
      section.innerHTML += e.data
    })

    wss.addEventListener('close', () => {
      console.log('Disconnected...')
    })

    wss.addEventListener('error', () => {
      console.log('Error...')
    })
  </script>
</head>
<body>
<h1>HTTP</h1>
<input name=user placeholder=username required>
<input type=password name=pw placeholder=password required>
<button>Login</button>
<h1>WS</h1>
<input name=msg placeholder=message required>
<button>Send</button>
<section></section>
</body>
</html>

5.4.8. MySQL

We can use the mysql2 package to access a MySQL/MariaDB database. See DB.js for a concrete application example.

codeforgeek.com/nodejs-mysql-tutorial

www.sitepoint.com/using-node-mysql-javascript-client

sidorares.github.io/node-mysql2/docs#using-connection-pools

Here is a simple example:

5.4.8.1. createNodeDBTable1.sql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
USE evegi144;

DROP TABLE IF EXISTS tblNodeUser;

CREATE TABLE tblNodeUser
(
    idNodeUser     INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    dtUserName     VARCHAR(32)  NOT NULL UNIQUE,
    dtPasswordHash VARCHAR(255) NOT NULL
)
    ENGINE = INNODB
    DEFAULT CHARSET utf8mb4
    DEFAULT COLLATE utf8mb4_bin;
5.4.8.2. mysql_options.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const mysqlSessionOpts = {
  // https://stackoverflow.com/questions/73317991/connection-refused-when-connecting-to-mysql-using-nodejs-mysql2
  host: '127.0.0.1',// Host name for database connection.
  port: 3306,// Port number for database connection.
  user: 'xxx',// Database user.
  password: 'xxx',// Password for the above database user.
  database: 'xxx',// Database name.
  checkExpirationInterval: 90000,// How frequently expired sessions will be cleared; milliseconds.
  expiration: 86400000,// The maximum age of a valid session; 24 hours in milliseconds.
  createDatabaseTable: true
}

const DBConnectOpts = {
  host: '127.0.0.1',
  port: 3306,
  charset: 'utf8_bin',
  user: 'xxx',
  password: 'xxx',
  database: 'evegi144'
}

export {mysqlSessionOpts as default, DBConnectOpts}
5.4.8.3. DB1.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import mysql from 'mysql2'
import {DBConnectOpts} from './private/mysql_options.mjs'

class DB {
    constructor() {
//    const conn = mysql.createConnection(DBConnectOpts)
        const pool = mysql.createPool(DBConnectOpts)

        this.getUserData = (userName, cb) => {
            pool.execute('SELECT * FROM tblNodeUser WHERE dtUserName=?', [`${userName}`],
                (err, rows, fields) => {
                    if (cb) cb(err, rows)
                })
        }

        this.registerUser = (userName, passwordHash, cb) => {
            pool.execute('INSERT INTO tblNodeUser(dtUserName, dtPasswordHash) VALUES (?, ?)',
                [`${userName}`, `${passwordHash}`], (err, rows, fields) => {
                    if (cb) cb(err, rows)
                })
        }
    }
}

export default new DB()
5.4.8.4. mysql1.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import bcrypt from 'bcryptjs'
import DB from './DB1.mjs'

const userName = 'a1'
const password = 'a1'

DB.getUserData(userName, (err, rows) => {
    if (err) throw err
    if (rows.length === 1) {
        bcrypt.compare(password, rows[0].dtPasswordHash).then(res => {
            if (res) console.log('You are now logged in.')
            else console.log('Invalid credentials')
            process.exit()
        })
    } else { // Must be 0 as > 1 not possible due to uniqueness in DB.
        bcrypt.hash(password, 10).then(hash => {
            DB.registerUser(userName, hash, (err, rows) => {
                if (err) throw err
                console.log('New user inserted')
                process.exit()
            })
        })
    }
})

The same example using mysql2/promise and async/await:

5.4.8.5. DB2.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import mysql from 'mysql2/promise'
import {DBConnectOpts} from './private/mysql_options.mjs'

class DB {
    constructor() {
        const pool = mysql.createPool(DBConnectOpts)

        this.getUserData = async userName => {
            try {
                return await pool.execute('SELECT * FROM tblNodeUser WHERE dtUserName=?', [`${userName}`])
            } catch (err) {
                console.log(err)
            }
        }

        this.registerUser = async (userName, passwordHash) => {
            try {
                return await pool.execute(`INSERT INTO tblNodeUser(dtUserName, dtPasswordHash)
                                           VALUES (?, ?)`, [`${userName}`, `${passwordHash}`])
            } catch (err) {
                console.log(err)
            }

        }
    }
}

export default new DB()
5.4.8.6. mysql2.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import bcrypt from 'bcryptjs'
import DB from './DB2.mjs'

const userName = 'a1'
const password = 'a1'

try {
    const data = await DB.getUserData(userName)
    console.dir(data)
    const rows = data[0]
    let loggedin = false
    if (rows.length === 1) {
        loggedin = await bcrypt.compare(password, rows[0].dtPasswordHash)
        if (loggedin) console.log('You are now logged in.')
        else console.log('Invalid credentials')
    } else { // Must be 0 as > 1 not possible due to uniqueness in DB.
        const hash = await bcrypt.hash(password, 10)
        await DB.registerUser(userName, hash)
        console.log('New user inserted')
    }
    console.log(`Final loggedin value: ${loggedin}`)
} catch (err) {
    console.log(err)
}
process.exit()

5.4.9. MongoDB

MongoDB, a NoSQL DB, “is an open-source document database that provides high performance, high availability, and automatic scaling”. MongoDB is fully programmable in JS. The MongoDB manual should be your starting place.

Take the free courses at MongoDB University to get up to speed quickly and easily.

5.4.9.1. Installation

Follow the official instructions.

If you get an 'mongodb-org-mongos : Depends: libssl1.1 (>= 1.1.1) but it is not installable' error during installation on Ubuntu 22.04, see askubuntu.com/questions/1403619/mongodb-install-fails-on-ubuntu-22-04-depends-on-libssl1-1-but-it-is-not-insta
5.4.9.2. Admin

Shutting down the server:

mongo admin --port 27001 --eval 'db.shutdownServer()'

Restarting the server with mongod.conf:

mongod -f mongod.conf
5.4.9.3. Configuration

www.mongodb.com/docs/manual/reference/configuration-options/#std-label-configuration-options

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use admin
db.createUser(
  {
    user: "myUserAdmin",
    pwd: passwordPrompt(), // or cleartext password
    roles: [
      { role: "root", db: "admin" }
    ]
  }
)

Then set

security:
  authorization: enabled

in /etc/mongod.conf.

After restarting mongod you need to connect as mongosh -u <user> -p.

5.4.9.4. MongoDB Shell (mongosh)

www.mongodb.com/docs/mongodb-shell

blog.forestadmin.com/mongodb-cheat-sheet

stackoverflow.com/questions/35265277/how-to-rename-a-user-in-mongodb

www.statology.org/mongodb-list-fields

www.statology.org/mongodb-check-if-field-exists

use <db> Switches to a DB or creates it if it does not yet exist.

www.mongodb.com/docs/manual/core/authentication

www.mongodb.com/docs/manual/tutorial/create-users

Before creating new users, we should make sure they’ll be able to change their password and custom data. Therefore we create a role with the appropriate privileges, as shown in www.mongodb.com/docs/manual/tutorial/change-own-password-and-custom-data:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use admin
db.createRole(
   { role: "changeOwnPasswordCustomDataRole",
     privileges: [
        {
          resource: { db: "", collection: ""},
          actions: [ "changeOwnPassword", "changeOwnCustomData" ]
        }
     ],
     roles: []
   }
)

Now we can create a user for the currently selected DB (for an existing user we can use db.grantRolesToUser():

1
2
3
4
5
6
7
db.createUser(
   {
     user:"user123",
     pwd: passwordPrompt(),  // or cleartext password
     roles:[ "readWrite", { role:"changeOwnPasswordCustomDataRole", db:"admin" } ]
   }
)

A user can change his or her password and/or custom data using db.updateUser():

1
2
3
4
5
6
7
8
use <userDB>
db.updateUser(
   "user123",
   {
      pwd: passwordPrompt(),  // or cleartext password
      customData: { title: "Senior Manager" }
   }
)
5.4.9.5. With Node

www.mongodb.com/docs/drivers/node/current/fundamentals/connection/connect/#std-label-node-connect-to-mongodb

www.mongodb.com/docs/manual/reference/connection-string

mongodb.github.io/node-mongodb-native

5.4.9.5.1. mongo_options.mjs
1
2
3
const adminConnectionURL = 'mongodb://<adminUserName>:<adminPassword>@localhost:27017/?authSource=admin'

export default adminConnectionURL
5.4.9.5.2. mongo1.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {MongoClient} from 'mongodb'
import adminConnectionURL from './private/mongo_options.mjs'

try {
  const dbName = 'admin'
  const client = new MongoClient(adminConnectionURL)
  await client.connect()
  console.log('Connected successfully to server')
  const db = client.db(dbName)
  const users = ['schbo549', 'schbo568', 'tacbr509', 'evegi144', 'sougi846', 'crujo453', 'hofkr186', 'krema914', 'abare205', 'schsa682']
  const PW = 'topSecret'

  for (const user of users) {
    await db.addUser(user, PW, {
      roles: [{
        role: 'readWrite',
        db: user
      }, {role: 'changeOwnPasswordCustomDataRole', db: 'admin'}]
    })
  }
  await client.close()
} catch (err) {
  console.error(err)
}
5.4.9.5.3. mongo2.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {MongoClient} from 'mongodb'
import adminConnectionURL from './private/mongo_options.mjs'

try {
  const dbName = 'evegi144'
  const client = new MongoClient(adminConnectionURL)
  await client.connect()
  console.log('Connected successfully to server')
  const db = client.db(dbName)
  const htmlTags = db.collection('htmlTags')
  const docs = [
    {name: 'html', description: 'html tag'},
    {name: 'head', description: 'head tag'},
    {name: 'body', description: 'body tag'}
  ]

  const result = await htmlTags.insertMany(docs, {ordered: true})

  const cursor = htmlTags.find({}, {sort: {name: 1}})
  if (await htmlTags.countDocuments() === 0) console.log("No documents found!")
  else {
    console.log(`Here are the ${await htmlTags.countDocuments()} documents:`)
    await cursor.forEach(console.log)
  }
  await client.close()
} catch (err) {
  console.error(err)
}
5.4.9.7. Compass
MongoDB Compass is a powerful GUI for querying, aggregating, and analyzing your MongoDB data in a visual environment.

www.mongodb.com/docs/compass/current/install

5.4.10. Handling other databases

We can use an SQL or a NoSQL database.

5.4.10.1. CouchDB

couchdb.apache.org

5.4.10.2. PostgreSQL

PostgreSQL on Ubuntu

PostgreSQL docs

PostgreSQL and NodeJS

blog.risingstack.com/node-js-database-tutorial

Edit /etc/postgresql/9.6/main/pg_hba.conf to look like this:

# Database administrative login by Unix domain socket
local   all             postgres                                peer

# TYPE  DATABASE        USER            ADDRESS                 METHOD

# "local" is for Unix domain socket connections only
local   all             all                                     md5 (1)
# IPv4 local connections:
host    all             all             127.0.0.1/32            md5
# IPv6 local connections:
host    all             all             ::1/128                 md5
# Allow replication connections from localhost, by a user with the
# replication privilege.
#local   replication     postgres                                peer
#host    replication     postgres        127.0.0.1/32            md5
#host    replication     postgres        ::1/128                 md5
host    all             all             0.0.0.0/0               reject (2)
1 Changed from peer to md5.
2 Added for security so that connections from other hosts get rejected.

Unquoted names are case-insensitive and explicitly lower-cased. Quoted names are case-sensitive.

5.4.10.2.1. psql
su - postgres
psql dbname user
psql -d dbname -U user
\du #list users
\l #list databases
\q exit
\password user #set user password
CREATE ROLE <user> LOGIN CREATEDB PASSWORD <pw>
5.4.10.2.2. pgAdmin
5.4.10.2.3. MassiveJS

MassiveJS is a PostgreSQL-specific data access tool.

5.4.10.3. Redis

Start Redis command line interface using redis-cli. List all keys via keys . Get the type of a key via type <key>. Depending on the type use get <key> or smembers <key>`or `hgetall <key>. Delete keys using pattern: redis-cli --scan --pattern 'Product::*' | xargs redis-cli DEL. Delete all keys in all databases using flushall.

redis.io

redis.io/topics/persistence

redis.js.org

www.cheatography.com/tasjaevan/cheat-sheets/redis

lzone.de/cheat-sheet/Redis

matt.sh/thinking-in-redis-part-one

5.4.11. Sessions

5.4.11.1. Security

Study Security and web.dev/samesite-cookies-explained before proceeding.

5.4.11.2. With HTTP(s)

Using express-session and express-mysql-session or express-mongodb-session we can add sessions to our earlier AJAX and JSON example:

5.4.11.2.1. mysql_options.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const mysqlSessionOpts = {
  host: 'localhost',// Host name for database connection.
  port: 3306,// Port number for database connection.
  user: 'xxx',// Database user.
  password: 'xxx',// Password for the above database user.
  database: 'xxx',// Database name.
  checkExpirationInterval: 90000,// How frequently expired sessions will be cleared; milliseconds.
  expiration: 86400000,// The maximum age of a valid session; 24 hours in milliseconds.
  createDatabaseTable: true
}

const DBConnectOpts = {
  host: 'localhost',
  port: 3306,
  charset: 'utf8_bin',
  user: 'xxx',
  password: 'xxx',
  database: 'xxx'
}

export {mysqlSessionOpts as default, DBConnectOpts}
5.4.11.2.2. session1.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import fs from 'fs'
import express from 'express'
import session from 'express-session'
import expressMysqlSession from 'express-mysql-session'
import mysqlSessionOpts from './private/mysql_options.mjs'

const port = 9000
const app = express()
const MySQLStore = expressMysqlSession(session)
const sessionStore = new MySQLStore(mysqlSessionOpts)
const sessionOpts = {
  cookie: {
    path: '/', secure: false, httpOnly: true, sameSite: true /*,
         maxAge: new Date(Date.now() + 300000)*/ /* 5 min */
  },
  secret: 'my-secret',
  resave: false,
  saveUninitialized: true,
  name: 'test1.sid',
  key: 'session_cookie_name',
  store: sessionStore,
  proxy: true
}
const sess = session(sessionOpts)

const logger = (req, res, next) => {
  const d = new Date()
  let s = `${d.getDate()} ${d.getMonth()} ${d.getFullYear()} ${d.getHours()}`
  s += ` ${d.getMinutes()} ${d.getSeconds()}`
  console.log(s)
  console.dir(req.session)
  next()
}

//app.set('trust proxy', true) // https://expressjs.com/en/guide/behind-proxies.html
app.use(sess)
app.use(logger)
app.post('', (req, res) => {
  const {method, url, headers} = req // All headers are represented in lower-case only.
  console.log(req.headers['content-type'])
  if (req.headers['content-type'] === 'application/json') {
    let data = ''
    req.on('error', err => {
      console.error(err.stack)
    }).on('data', chunk => {
      data += chunk
    }).on('end', () => {
      if (data) {
        try {
          data = JSON.parse(data)
        } catch (e) {
          console.error(e)
        }
      }
      console.dir(data)
      res.on('error', err => {
        console.error(err)
      })
      let login = false
      if (data.un === 'a1' && data.pw === 'a1') login = true
      req.session.loggedin = login
      req.session.save()
      console.dir(req.session)
      res.end(JSON.stringify({login: login}))
    })
  }
})
app.get('', (req, res) => {
  res.on('error', err => {
    console.error(err)
  })
  try {
    let filenameToServe = 'session1.html'
    if (req.session && req.session.loggedin) filenameToServe = 'session1_loggedin.html'
    const rs = fs.createReadStream(filenameToServe)
    rs.on('error', err => res.end('Could not read file!'))
    rs.pipe(res)
  } catch (e) {
    res.end('<h1>Could not read file!</h1>')
  }
})

app.listen(port) // Let the server listen for HTTP requests.
5.4.11.2.3. session1.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>Session Client 1</title>
  <script type=module>
    const button = document.querySelector('button')
    button.addEventListener('click', () => {
      const inputs = document.querySelectorAll('input')
      if (inputs[0].value && inputs[1].value) {
        fetch('', {
          headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
          },
          method: "POST",
          body: JSON.stringify({un: inputs[0].value, pw: inputs[1].value})
        }).then(response => response.json()).then(data => {
          window.location = '/node'
        }).catch(error => {
          console.log(`There has been a problem with the fetch operation: ${error.message}`)
        })
      }
    })
  </script>
</head>
<body>
<input name=user placeholder=username required>
<input type=password name=pw placeholder=password required>
<button>Login</button>
</body>
</html>
5.4.11.2.4. session1_loggedin.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>Session Client 1 loggedin</title>
  <script type=module>
      const button = document.querySelector('button')
      button.addEventListener('click', () => {
          fetch('', {
              headers: {
                  'Accept': 'application/json',
                  'Content-Type': 'application/json'
              },
              method: "POST",
              body: JSON.stringify({logout: 'logout'})
          }).then(response => response.json()).then(data => {
              window.location = '/node'
          }).catch(error => {
              console.log(`There has been a problem with the fetch operation: ${error.message}`)
          })
      })
  </script>
</head>
<body>
  <button>Logout</button>
</body>
</html>
5.4.11.3. With HTTP(s) and WebSocket

We can also use sessions for both HTTP and WebSocket communication.

Please note that due to a still incomplete session exchange between ws and express-session, we need to parse the session for every WebSocket connection, as otherwise the ws session would not be updated after session data changes performed in the HTTP server:
5.4.11.3.1. HTTPWSServer1.mjs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import fs from 'fs'
import http from 'http'
import WebSocket, {WebSocketServer} from 'ws'
import express from 'express'
import session from 'express-session'
import expressMysqlSession from 'express-mysql-session'
import mysqlSessionOpts from './private/mysql_options.mjs'

const app = express()
const MySQLStore = expressMysqlSession(session)
const port = 9000 // Port number that our HTTP server will be listening on.
const sessionStore = new MySQLStore(mysqlSessionOpts)
const sessionOpts = {
    cookie: {
        path: '/',
        domain: 'students.btsi.lu',
        secure: false,
        httpOnly: true,
        sameSite: true,
        expires: new Date(Date.now() + 86400000)/*,
         maxAge: new Date(Date.now() + 300000)*/ /* 5 min */
    },
    secret: 'my-secret',
    resave: false,
    saveUninitialized: true,
    name: 'test1.sid',
    key: 'session_cookie_name',
    store: sessionStore,
    proxy: true
}
const sess = session(sessionOpts)
let globalSession

//app.set('trust proxy', true) // https://expressjs.com/en/guide/behind-proxies.html
app.use(sess)
app.post('', (req, res) => {
    if (req.headers['content-type'] === 'application/json') {
        let data = ''
        req.on('error', err => {
            console.error(err.stack)
        }).on('data', chunk => {
            data += chunk
        }).on('end', () => {
            if (data) {
                try {
                    data = JSON.parse(data)
                } catch (e) {
                    console.error(e)
                }
            }
            console.dir(data)
            res.on('error', err => {
                console.error(err)
            })
            let login = false
            if (data.un === 'a1' && data.pw === 'a1') login = true
            req.session.loggedin = login
            globalSession = req.session
            res.json(JSON.stringify({login: login}))
            console.log('*** POST ***')
            console.log(req.sessionID)
            console.dir(req.session)
            console.log()
        })
    }
})

app.get('', (req, res) => {
    res.on('error', err => {
        console.error(err)
    })
    try {
        if (req.session.a) req.session.a++
        else req.session.a = 1
        globalSession = req.session
        console.log('*** GET ***')
        console.log(req.sessionID)
        console.dir(req.session)
        console.log()
        let fileName = 'HTTPWSClient1.html'
        if (req.url === '/HTTPClient1.css') fileName = 'HTTPClient1.css'
        const rs = fs.createReadStream(fileName)
        rs.on('error', err => res.end('Could not read file!'))
        rs.pipe(res)
    } catch (e) {
        res.send('<h1>Could not read file!</h1>')
    }
})

const server = http.createServer(app)
const wss = new WebSocketServer({server})

wss.on('error', err => {
    console.dir(err)
})

/*wss.broadcast = function broadcast(msg){
  wss.clients.forEach(function each(client) {
    client.send(msg)
  })
}*/

wss.on('connection', (socket, req) => {
        console.log('WS client connected...')

        socket.on('error', err => {
            console.dir(err)
        })

        socket.on('message', async data => {
            req.session = globalSession
            req.session.test = Math.random()
            req.session.save() // We need to call this in WebSocket connections.
            console.log(data)
            console.log('*** WS ***')
            console.log(req.sessionID)
            console.dir(req.session)
            console.log()
            socket.send(`You said: ${data}<br>`)
        })

        socket.on('close', () => {
            console.log('Socket closed')
        })
    }
)

server.listen(port) // Let the server listen for HTTP requests.
5.4.11.3.2. HTTPWSClient1.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>HTTP Client 1</title>
  <link rel=stylesheet href=node/HTTPClient1.css>
  <script type=module>
      const wssURL = 'wss://students.btsi.lu/node/wss'
      const buttons = document.querySelectorAll('button')
      const inputs = document.querySelectorAll('input')
      const section = document.querySelector('section')
      buttons[0].addEventListener('click', () => {
          if (inputs[0].value && inputs[1].value) {
              fetch('', {
                  headers: {
                      'Accept': 'application/json',
                      'Content-Type': 'application/json'
                  },
                  method: "POST",
                  credentials: 'same-origin', // https://github.com/expressjs/session/issues/374
                  body: JSON.stringify({un: inputs[0].value, pw: inputs[1].value})
              }).then(response => response.json()).then(data => {
                  console.dir(data)
              }).catch(error => {
                  console.log(`There has been a problem with the fetch operation: ${error.message}`)
              });
              /*const req1 = new XMLHttpRequest();
      req1.open('POST', '');
      req1.addEventListener('load', e => {
      console.dir(JSON.parse(e.target.response));
      });
      req1.send(JSON.stringify({un: inputs[0].value, pw: inputs[1].value}));*/
          }
      });
      const wss = new WebSocket(wssURL)
      wss.addEventListener('open', () => {
          console.log('WebSocket connection opened.')
          buttons[1].addEventListener('click', () => {
              wss.send(inputs[2].value)
          })
      })

      wss.addEventListener('message', e => {
          section.innerHTML += e.data
      })

      wss.addEventListener('close', () => {
          console.log('Disconnected...')
      })

      wss.addEventListener('error', () => {
          console.log('Error...')
      })
  </script>
</head>
<body>
  <h1>HTTP</h1>
  <input name=user placeholder=username required>
  <input type=password name=pw placeholder=password required>
  <button>Login</button>
  <h1>WS</h1>
  <input name=msg placeholder=message required>
  <button>Send</button>
  <section></section>
</body>
</html>

We can once again make our life much easier by using the express-ws package, like so:

5.4.11.3.3. expressWSServer2.mjs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
import fs from 'fs'
import http from 'http'
import express from 'express'
import expressWs from 'express-ws'
import session from 'express-session';
import expressMysqlSession from 'express-mysql-session'
import mysqlSessionOpts from './private/mysql_options.mjs'

const port = 9000; // The port number that our HTTP server will be listening on.
const app = express()
const server = http.createServer(app)
expressWs(app, server)
const MySQLStore = expressMysqlSession(session)
const sessionStore = new MySQLStore(mysqlSessionOpts);
const sessionOpts = {
    cookie: {
        path: '/',
        domain: 'students.btsi.lu',
        secure: false,
        httpOnly: true,
        sameSite: true,
        expires: new Date(Date.now() + 86400000)/*,
         maxAge: new Date(Date.now() + 300000)*/ /* 5 min */
    },
    secret: 'my-secret',
    resave: false,
    saveUninitialized: true,
    name: 'test1.sid',
    key: 'session_cookie_name',
    store: sessionStore,
    proxy: true
}
const sess = session(sessionOpts)
let globalSession

//app.set('trust proxy', true); // https://expressjs.com/en/guide/behind-proxies.html
app.use(sess)

app.post('/', (req, res) => {
    if (req.headers['content-type'] === 'application/json') {
        let data = ''
        req.on('error', err => {
            console.error(err.stack)
        }).on('data', chunk => {
            data += chunk
        }).on('end', () => {
            if (data) {
                try {
                    data = JSON.parse(data)
                } catch (e) {
                    console.error(e)
                }
            }
            console.dir(data)
            res.on('error', err => {
                console.error(err)
            })
            let login = false
            if (data.un === 'a1' && data.pw === 'a1') login = true
            req.session.loggedin = login
            globalSession = req.session
            res.json(JSON.stringify({login: login}))
            console.log('*** POST ***')
            console.log(req.sessionID)
            console.dir(req.session)
            console.log()
        })
    }
})

app.get('/', (req, res) => {
    res.on('error', err => {
        console.error(err)
    })
    try {
        if (req.session.a) req.session.a++
        else req.session.a = 1
        globalSession = req.session
        console.log('*** GET ***')
        console.log(req.sessionID)
        console.dir(req.session)
        console.log()
        const rs = fs.createReadStream('expressWSClient2.html')
        rs.on('error', err => res.end('Could not read file!'))
        rs.pipe(res)
    } catch (e) {
        res.send('<h1>Could not read file!</h1>')
    }
})

app.ws('/', (ws, req) => {
    ws.on('message', msg => {
        console.log(msg)
        req.session = globalSession
        if (req.session.a) req.session.a++
        else req.session.a = 1
        req.session.save()
        console.log('*** WS ***')
        console.log(req.sessionID)
        console.dir(req.session)
        console.log()
        ws.send(`You said: ${msg}<br>`)
    })
    console.log('socket', req.session)
})

server.listen(port)

The client remains unchanged.

www.npmjs.com/package/express-session

www.npmjs.com/package/express-mysql-session

lockmedown.com/securing-node-js-managing-sessions-express-js

www.packtpub.com/books/content/using-socketio-and-express-together

github.com/expressjs/session/issues/208

botleg.com/stories/using-your-sessions-with-socketio

mono.software/2014/08/25/Sharing-sessions-between-SocketIO-and-Express-using-Redis

github.com/jfromaniello/passport.socketio

www.npmjs.com/package/socket.io-mysql-session

socket.io-express-session

www.npmjs.com/package/express-socket.io-session

stackoverflow.com/questions/23494016/socket-io-and-express-4-sessions

stackoverflow.com/questions/24290699/socket-io-1-0-5-how-to-save-session-variables/24380110#24380110

www.codementor.io/tips/0217388244/sharing-passport-js-sessions-with-both-express-and-socket-io

www.packtpub.com/mapt/book/Web%20Development/9781783984480/2/ch02lvl1sec26/Using+Socket.IO+and+Express+together

stackoverflow.com/questions/4753957/socket-io-authentication

mono.software/2014/08/25/Sharing-sessions-between-SocketIO-and-Express-using-Redis

www.packtpub.com/mapt/book/Web%20Development/9781785284922/7/ch07lvl1sec29/Authentication

security.stackexchange.com/questions/115695/when-should-server-side-sessions-be-used-instead-of-client-side-sessions

phillbarber.blogspot.lu/2014/02/client-side-vs-server-side-session.html

stackoverflow.com/questions/9071969/using-express-and-node-how-to-maintain-a-session-across-subdomains-hostheaders/14627464#14627464

lockmedown.com/securing-node-js-managing-sessions-express-js

www.duringthedrive.com/2017/11/25/sesh-express-sessions-cookies-and-json-web-tokens

5.4.12. Registration and login

5.4.12.1. Using AJAX
5.4.12.1.1. reglogin.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <meta name=viewport content="width=device-width, initial-scale=1">
  <title>Reglogin Client 1</title>
  <script type=module>
    const buttons = document.querySelectorAll('button')
    const fetcher = (type, un, pw) => {
      fetch('', {
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        method: "POST",
        body: JSON.stringify({type: type, un: un, pw: pw})
      }).then(response => response.json()).then(data => {
        console.dir(data)
        window.location = '/node'
      }).catch(error => {
        console.log(`There has been a problem with the fetch operation: ${error.message}`)
      })
    }
    buttons[0].addEventListener('click', () => {
      const inputs = document.querySelectorAll('input')
      if (inputs[0].value && inputs[1].value) fetcher('login', inputs[0].value, inputs[1].value)
    })
    buttons[1].addEventListener('click', () => {
      const inputs = document.querySelectorAll('input')
      if (inputs[0].value && inputs[1].value) fetcher('register', inputs[0].value, inputs[1].value)
    })
  </script>
</head>
<body>
  <input name=user placeholder=username required>
  <input type=password name=pw placeholder=password required>
  <button>Login</button>
  <button>Register</button>
</body>
</html>
5.4.12.1.2. reglogin_loggedin.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>Reglogin Client 1 logged in</title>
  <script type=module>
    const button = document.querySelector('button')
    button.addEventListener('click', () => {
      const inputs = document.querySelectorAll('input')
      fetch('', {
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        method: "POST",
        body: JSON.stringify({type: 'logout'})
      }).then(response => response.json()).then(data => {
        console.dir(data)
        window.location = '/node'
      }).catch(error => {
        console.log(`There has been a problem with the fetch operation: ${error.message}`)
      })
    })
  </script>
</head>
<body>
<button>Logout</button>
</body>
</html>
5.4.12.1.3. DB2.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import mysql from 'mysql2/promise'
import {DBConnectOpts} from './private/mysql_options.mjs'

class DB {
    constructor() {
        const pool = mysql.createPool(DBConnectOpts)

        this.getUserData = async userName => {
            try {
                return await pool.execute('SELECT * FROM tblNodeUser WHERE dtUserName=?', [`${userName}`])
            } catch (err) {
                console.log(err)
            }
        }

        this.registerUser = async (userName, passwordHash) => {
            try {
                return await pool.execute(`INSERT INTO tblNodeUser(dtUserName, dtPasswordHash)
                                           VALUES (?, ?)`, [`${userName}`, `${passwordHash}`])
            } catch (err) {
                console.log(err)
            }

        }
    }
}

export default new DB()
5.4.12.1.4. reglogin.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import fs from 'fs'
import express from 'express'
import bcrypt from 'bcryptjs'
import DB from './DB2.mjs'
import session from 'express-session'
import expressMysqlSession from 'express-mysql-session'
import mysqlSessionOpts from './private/mysql_options.mjs'

const port = 9000; // The port number that our HTTP server will be listening on.
const app = express()
const MySQLStore = expressMysqlSession(session)
const sessionStore = new MySQLStore(mysqlSessionOpts)
const sessionOpts = {
    cookie: {
        path: '/', secure: false, httpOnly: true, sameSite: true /*,
         maxAge: new Date(Date.now() + 300000)*/ /* 5 min */
    },
    secret: 'my-secret',
    resave: false,
    saveUninitialized: true,
    name: 'test1.sid',
    key: 'session_cookie_name',
    store: sessionStore,
    proxy: true
}
const sess = session(sessionOpts)

app.set('trust proxy', true) // https://expressjs.com/en/guide/behind-proxies.html
app.use(sess)
app.post('', (req, res) => {
    if (req.headers['content-type'] === 'application/json') {
        let data = ''
        req.on('error', err => {
            console.error(err.stack)
        }).on('data', chunk => {
            data += chunk
        }).on('end', async () => {
            if (data) {
                try {
                    data = JSON.parse(data)
                    console.dir(data)
                    res.on('error', err => {
                        console.error(err)
                    })
                    if (data.type) {
                        req.session.loggedin = false
                        if (data.type === 'login') {
                            if (data.un && data.pw) {
                                const userData = await DB.getUserData(data.un)
                                const rows = userData[0]
                                let loggedin = false
                                if (rows.length === 1) {
                                    loggedin = await bcrypt.compare(data.pw, rows[0].dtPasswordHash)
                                    req.session.loggedin = loggedin
                                } else console.log('Invalid credentials')
                            }
                        } else if (data.type === 'register') {
                            if (data.un && data.pw) {
                                const userData = await DB.getUserData(data.un)
                                const rows = userData[0]
                                if (rows.length < 1) {
                                    const hash = await bcrypt.hash(data.pw, 10)
                                    await DB.registerUser(data.un, hash)
                                    req.session.loggedin = true
                                    console.log('New user inserted and logged in')
                                }
                            }
                        }
                        res.end(JSON.stringify({login: req.session.loggedin}))
                        req.session.save()
                    }
                } catch (e) {
                    console.error(e)
                }
            }
        })
    }
})
app.get('', (req, res) => {
    res.on('error', err => {
        console.error(err)
    })
    try {
        let filenameToServe = 'reglogin.html'
        if (req.session && req.session.loggedin) filenameToServe = 'reglogin_loggedin.html'
        const rs = fs.createReadStream(filenameToServe)
        rs.on('error', err => res.end('Could not read file!'))
        rs.pipe(res)
    } catch (e) {
        res.end('<h1>Could not read file!</h1>')
    }
})

app.listen(port) // Let server listen for HTTP requests.
5.4.12.2. Using WebSockets
5.4.12.2.1. regloginClient.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <meta name=viewport content="width=device-width, initial-scale=1">
  <title>Reglogin Client 2</title>
  <script type=module>
    import ReconnectingWebSocket
      from 'https://cdn.jsdelivr.net/npm/reconnecting-websocket/dist/reconnecting-websocket.mjs'

    const wsURL = 'wss://students.btsi.lu/node/wss'
    const ws = new ReconnectingWebSocket(wsURL)
    ws.token = null
    ws.execute = (command, options = {}) => {
      let obj = {}
      Object.assign(obj, {command: command, token: ws.token}, options)
      ws.send(JSON.stringify(obj))
    }

    ws.addEventListener('error', () => {
      console.log('Error...')
    })

    ws.addEventListener('open', () => {
      console.log('WebSocket connection opened.')
    })

    ws.addEventListener('message', e => {
        try {
          const data = JSON.parse(e.data)
          if ('token' in data) ws.token = data.token
          if ('loggedin' in data) ws.loggedin = data.loggedin
          if (ws.loggedin) displayLoggedInSection()
          else displayLoginSection()
          console.log(e.data)
        } catch (error) {
          console.error(error)
        }
      }
    )

    ws.addEventListener('close', () => {
      console.log('Disconnected...')
    })

    const displayLoginSection = () => {
      const input1 = document.createElement('input')
      input1.name = 'user'
      input1.placeholder = 'username'
      input1.required = true
      const input2 = document.createElement('input')
      input2.type = 'password'
      input2.name = 'pw'
      input2.placeholder = 'password'
      input2.required = true
      const button1 = document.createElement('button')
      button1.innerText = 'Login'
      button1.addEventListener('click', () => {
        const inputs = document.querySelectorAll('input')
        if (inputs[0].value && inputs[1].value) {
          ws.execute('login', {un: inputs[0].value, pw: inputs[1].value})
        }
      })
      const button2 = document.createElement('button')
      button2.innerText = 'Register'
      button2.addEventListener('click', () => {
        const inputs = document.querySelectorAll('input')
        if (inputs[0].value && inputs[1].value) {
          ws.execute('register', {un: inputs[0].value, pw: inputs[1].value})
        }
      })
      const section = document.querySelector('section')
      section.innerHTML = ''
      section.appendChild(input1)
      section.appendChild(input2)
      section.appendChild(button1)
      section.appendChild(button2)
    }

    const displayLoggedInSection = () => {
      const button = document.createElement('button')
      button.innerText = 'Logout'
      button.addEventListener('click', () => ws.execute('logout'))
      document.querySelector('section').innerHTML = ''
      document.querySelector('section').appendChild(button)
    }
  </script>
</head>
<body>
<noscript><h1>This app requires JavaScript!</h1></noscript>
<section></section>
</body>
</html>
5.4.12.2.2. regloginServer.mjs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import fs from 'fs'
import http from 'http'
import express from 'express'
import expressWs from 'express-ws'
import crypto from 'crypto'
import bcrypt from 'bcryptjs'
import DB from './DB2.mjs'
import session from 'express-session'
import expressMysqlSession from 'express-mysql-session'
import mysqlSessionOpts from './private/mysql_options.mjs'

const port = 9000 // The port number that our HTTP server will be listening on.
const app = express()
const MySQLStore = expressMysqlSession(session)
const sessionStore = new MySQLStore(mysqlSessionOpts)
const sessionOpts = {
    cookie: {
        path: '/', secure: false, httpOnly: true, sameSite: true /*,
         maxAge: new Date(Date.now() + 300000)*/ /* 5 min */
    },
    secret: 'my-secret',
    resave: false,
    saveUninitialized: true,
    name: 'test1.sid',
    key: 'session_cookie_name',
    store: sessionStore,
    proxy: true
}
const sess = session(sessionOpts)
const server = http.createServer(app)
const wsAPI = expressWs(app, server)
const wsServer = wsAPI.getWss()

app.disable('x-powered-by')
app.set('trust proxy', true) // https://expressjs.com/en/guide/behind-proxies.html
app.use(sess)

app.get('', (req, res) => {
    res.on('error', err => {
        console.error(err)
    })
    try {
        let filenameToServe = 'regloginClient.html'
        console.dir(req.session)
        const rs = fs.createReadStream(filenameToServe)
        rs.on('error', err => res.end('Could not read file!'))
        rs.pipe(res)
    } catch (e) {
        res.end('<h1>Could not read file!</h1>')
    }
})

app.ws('', (ws, req) => {
    console.log('New connection')
    if (!req.session.loggedin) {
        req.session.loggedin = false
        req.session.save()
    }
    ws.on('error', err => {
        console.error(err)
    })
    ws.on('close', () => {
        console.log('WebSocket closed')
    })
    ws.on('message', async msg => {
        console.log(msg)
        console.log('*** WS ***')
        console.log('Connected clients:')
        console.log(wsServer.clients.size)
        try {
            const data = JSON.parse(msg)
            if (data?.token === ws.token) { // Ignore msg if invalid token.
                console.log(data?.command)
                if (data?.command === 'login') {
                    if (data.un && data.pw) {
                        const userData = await DB.getUserData(data.un)
                        const rows = userData[0]
                        if (rows.length === 1) {
                            let loggedin = await bcrypt.compare(data.pw, rows[0].dtPasswordHash)
                            req.session.loggedin = loggedin
                            if (loggedin) {
                                req.session.un = data.un
                                req.session.pw = data.pw
                            }
                        } else console.log('Invalid credentials')
                    }
                } else if (data?.command === 'register') {
                    if (data.un && data.pw) {
                        const userData = await DB.getUserData(data.un)
                        const rows = userData[0]
                        if (rows.length < 1) {
                            const hash = await bcrypt.hash(data.pw, 10)
                            await DB.registerUser(data.un, hash)
                            req.session.loggedin = true
                            req.session.un = data.un
                            req.session.pw = data.pw
                            console.log('New user inserted and logged in')
                        }
                    }
                } else if (data?.command === 'logout') {
                    req.session.loggedin = false
                    delete req.session.un
                    delete req.session.pw
                    console.log('logging out...')
                } else console.log('Unknown command!')
                req.session.save()
                ws.send(JSON.stringify({loggedin: req.session.loggedin}))
            }
        } catch (e) {
            console.error(e)
        }
    })

// Generate and send token to client. This token needs to be received with every client msg.
    crypto.randomBytes(5, (err, buffer) => {
        if (err) throw err
        ws.token = buffer.toString('hex')
        ws.send(JSON.stringify({
            token: ws.token,
            loggedin: req.session.loggedin
        }))
    })
})

server.listen(port) // Let server listen for HTTP requests.

5.4.15. Other useful APIs and packages

5.4.15.5. PDF
5.4.15.5.1. Display
5.4.15.5.2. Generation

PDFKit

pdfmake.org

5.4.15.7. Email

www.sitepoint.com/sending-email-using-node-js

emailjs.org

5.4.15.7.1. SMTP

nodemailer.com

www.mailgun.com

5.4.15.7.3. Working example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const nodemailer = require('nodemailer')
const imaps = require('imap-simple')
// Convert LF to CRLF to avoid bare newlines error, cf. https://cr.yp.to/docs/smtplf.html
const eol = require('eol')
const smtpConfig = {
  host: 'smtp.restena.lu',
  port: 587,
  secure: false, // upgrade later with STARTTLS
  auth: {
    user: 'xxxx',
    pass: 'xxxx'
  }
}
const imapConfig = {
  imap: {
    user: 'xxxx',
    password: 'xxxx',
    host: 'mail.restena.lu',
    port: 993,
    tls: true,
    authTimeout: 3000
  }
}
const transporter = nodemailer.createTransport(smtpConfig)
const message = {
  sender: 'gilles.everling@education.lu',
  from: 'gilles.everling@education.lu',
  /*replyTo: 'gilles.everling@education.lu',*/
  to: 'gilles.everling@education.lu',
  subject: 'Test',
  html: '<h1>Test</h1>'
}
imaps.connect(imapConfig).then(connection => {
  const message = eol.crlf(`Content-Type: text/html
To: gilles.everling@education.lu
From: gilles.everling@education.lu
Subject: Test

<h1>Test</h1>
`)
  connection.append(message, {mailbox: 'Inbox.Sent'})
  process.exit()
})
5.4.15.11. Others

www.npmjs.com/package/jsfiddle-downloader

download fiddles from jsFiddle.net

www.clinicjs.org

tools to help diagnose and pinpoint Node.js performance issues

www.polymer-project.org

libraries, tools, and standards for a better web

systeminformation.io

retrieve detailed hardware, system and OS information

5.4.16. apess.lu

5.4.16.1. mysql_options.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'use strict'

const mysqlSessionOpts = {
  host: 'localhost',// Host name for database connection.
  port: 3306,// Port number for database connection.
  user: 'xxx',// Database user.
  password: 'xxx',// Password for the above database user.
  database: 'xxx',// Database name.
  checkExpirationInterval: 90000,// How frequently expired sessions will be cleared; milliseconds.
  expiration: 86400000,// The maximum age of a valid session; 24 hours in milliseconds.
  createDatabaseTable: true
}

const DBConnectOpts = {
  host: 'localhost',
  port: 3306,
  charset: 'utf8_bin',
  user: 'xxx',
  password: 'xxx',
  database: 'xxx'
}

exports.mysqlSessionOpts = mysqlSessionOpts
exports.DBConnectOpts = DBConnectOpts
5.4.16.2. DB.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
'use strict'

class DB {
  constructor() {
    const mysql = require('mysql2')
    const asyncMysql = require('mysql2/promise')
    const DBConnectOpts = require('./mysql_options').DBConnectOpts
    const conn = mysql.createConnection(DBConnectOpts)
    let asyncConn = undefined

    this.destroy = () => {
      if (conn) conn.destroy()
    }

    this.updatePassword = (email, hash, cb) => {
      conn.execute(`UPDATE tblMember
                    SET dtPassword=?
                    WHERE dtEmail = ?`, [`${hash}`, `${email}`],
        (err, rows, fields) => {
          if (cb) cb(err, rows)
        })
    }

    this.test = (exceptionEmails, cb) => {
      console.log('Exceptions: ' + exceptionEmails)
      conn.execute(`SELECT COUNT(*)
                    FROM tblMember
                    WHERE dtEmail NOT IN (?)`,
        [`${exceptionEmails}`],
        (err, rows, fields) => {
          if (cb) cb(err, rows)
        })
    }

    this.deletePasswords = (exceptionEmails, cb) => {
      conn.execute(`UPDATE tblMember
                    SET dtPassword=''
                    WHERE dtEmail NOT IN (?)`, [`${exceptionEmails}`],
        (err, rows, fields) => {
          if (cb) cb(err, rows)
        })
    }

    this.updateMember = (email, newMemberData, cb) => {
      conn.execute(`UPDATE tblMember
                    SET dtFirstName=?,
                        dtLastName=?,
                        dtEmail=?,
                        dtStreet=?,
                        dtCity=?,
                        dtCountry=?,
                        dtPostalCode=?,
                        dtSchool=?,
                        dtFunction=?,
                        dtSubject=?,
                        dtBIC=?,
                        dtIBAN=?,
                        dtAP_V=?,
                        dtGetEmail=?,
                        dtLastUpdateTime=now()
                    WHERE dtEmail = ?`,
        [`${newMemberData.dtFirstName}`,
          `${newMemberData.dtLastName}`,
          `${newMemberData.dtEmail}`,
          `${newMemberData.dtStreet}`,
          `${newMemberData.dtCity}`,
          `${newMemberData.dtCountry}`,
          `${newMemberData.dtPostalCode}`,
          `${newMemberData.dtSchool}`,
          `${newMemberData.dtFunction}`,
          `${newMemberData.dtSubject}`,
          `${newMemberData.dtBIC}`,
          `${newMemberData.dtIBAN}`,
          `${newMemberData.dtAP_V}`,
          `${newMemberData.dtGetEmail}`,
          `${email}`],
        (err, rows, fields) => {
          if (!err) {
            conn.execute(`INSERT INTO tblMemberUpdateLog (dtFirstName, dtLastName,
                                                          dtEmail,
                                                          dtStreet, dtCity, dtCountry,
                                                          dtPostalCode, dtSchool,
                                                          dtFunction, dtSubject, dtBIC,
                                                          dtIBAN, dtAP_V, dtGetEmail,
                                                          fiMemberId)
                          VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
                                  ?, ?, ?)`,
              [`${newMemberData.dtFirstName}`,
                `${newMemberData.dtLastName}`,
                `${newMemberData.dtEmail}`,
                `${newMemberData.dtStreet}`,
                `${newMemberData.dtCity}`,
                `${newMemberData.dtCountry}`,
                `${newMemberData.dtPostalCode}`,
                `${newMemberData.dtSchool}`,
                `${newMemberData.dtFunction}`,
                `${newMemberData.dtSubject}`,
                `${newMemberData.dtBIC}`,
                `${newMemberData.dtIBAN}`,
                `${newMemberData.dtAP_V}`,
                `${newMemberData.dtGetEmail}`,
                `${newMemberData.idMember}`],
              (err, rows, fields) => {
                if (cb) cb(err, rows)
              })
          } else if (cb) cb(err, rows)
        })
    }

    this.loginUser = (email, cb) => {
      conn.execute('SELECT * FROM tblMember WHERE dtEmail=?', [`${email}`],
        (err, rows, fields) => {
          if (cb) cb(err, rows)
        })
    }

    this.updateLoginTimestamp = (email, cb) => {
      conn.execute('UPDATE tblMember SET dtLastLoginTime=now() WHERE dtEmail=?', [`${email}`],
        (err, rows, fields) => {
          if (cb) cb(err, rows)
        })
    }

    this.getMembers = cb => {
      conn.execute(`SELECT *
                    FROM tblMember
                    WHERE idMember NOT IN
                          (SELECT fiMember FROM tblMembershipDate)`, (err, rows, fields) => {
        if (cb) cb(err, rows)
      })
    }

    this.getPayingMembers = (cb, lastPaidSchoolYear) => {
      conn.execute(`SELECT *
                    FROM tblMember
                    WHERE /*(dtAdhesion < '2017-07-1' OR dtAdhesion IS
        NULL) AND*/ (dtLastPaidSchoolYear IS NULL OR dtLastPaidSchoolYear <>
                                                     ${lastPaidSchoolYear})
                      AND idMember NOT IN
                          (SELECT fiMember FROM tblMembershipDate)`, (err, rows, fields) => {
        if (cb) cb(err, rows)
      })
    }

    this.getSEPAErrorMembers = cb => {
      conn.execute(`SELECT *
                    FROM tblMember as m,
                         tblSEPAError20182019 as e
                    WHERE m.dtRef = e.fiRef`,
        (err, rows, fields) => {
          if (cb) cb(err, rows)
        })
    }

    this.insertIPUA = (IP, userAgent) => {
      if (IP && userAgent)
        conn.execute(`INSERT INTO tblIP (dtIP, dtUserAgent)
                      VALUES (?, ?)`, [IP, userAgent],
          (err, rows, fields) => {
            if (err) console.log(err)
          })
    }

    this.insertSEPAError = (ID, errorCode, cb) => {
      conn.execute(`INSERT INTO tblSEPAError20182019 (dtErrorCode, fiRef)
                    VALUES (?, ?)`, [errorCode, ID],
        (err, rows, fields) => {
          if (cb) cb(err, rows)
        })
    }

    const connect = async () => {
      if (!asyncConn) asyncConn = await asyncMysql.createConnection(DBConnectOpts)
    }

    this.getHighestdtRef = async () => {
      if (!asyncConn) await connect()
      return asyncConn.execute(`SELECT MAX(dtRef) /*AS maxRef*/
                                FROM tblMember`)
    }

    this.getMemberIdsWithoutdtRef = async () => {
      if (!asyncConn) await connect()
      return asyncConn.execute(`SELECT idMember
                                FROM tblMember
                                WHERE dtRef IS NULL`)
    }

    this.updateHighestdtRef = async (id, dtRef) => {
      if (!asyncConn) await connect()
      return asyncConn.execute('UPDATE tblMember SET dtRef=? WHERE idMember=?', [`${dtRef}`, `${id}`])
    }
  }
}

module.exports = new DB()
5.4.16.3. WebSocketServer.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
module.exports = class WebSocketServer {
  constructor(server, session) {
    const DEBUG = false
    const WebSocketServer = require('ws').Server
    const wss = new WebSocketServer({
      /* verifyClient: (info, done) => {
        if (DEBUG) console.log('Parsing session from request...');
        session(info.req, {}, () => {
          if (DEBUG) console.log('Session is parsed!');
          if (DEBUG) console.dir(info.req.session);
          // We can reject the connection by returning false to done(). For example,
          // reject here if user is unknown.
          done(true);
        });
      },*/
      server
    })

    wss.on('connection', (ws, req) => {
      //console.log(req.connection.remoteAddress);
      //console.dir(req);
      const fs = require('fs')
      const util = require('util')
      const crypto = require('crypto')
      const bcrypt = require('bcryptjs')
      const path = require('path')
      const DB = require(__dirname + '/DB')
      ws.mySend = (msg, options = {}) => {
        const obj = {}
        Object.assign(obj, {msg: msg}, options)
        ws.send(JSON.stringify(obj))
      }

      //ws.upgradeReq = req; // https://github.com/websockets/ws/pull/1099
      if (DEBUG) {
        console.log('************** socket request headers start')
        console.dir(req.headers)
        console.log('************** socket request headers end')
      }
      session(req, {}, () => {
        ws.session = req.session
        if (DEBUG) console.log('Session created')
        if (req.headers.host === 'apess.lu' || req.headers.host === 'www.apess.lu' ||
          req.headers.referer === 'https://apess.lu/' ||
          req.headers.referer === 'https://www.apess.lu/') {
          if (DEBUG) console.log('A user connected')
          crypto.randomBytes(5, (err, buffer) => {
            if (err) throw err
            ws.token = buffer.toString('hex')
            ws.mySend('token', {
              token: ws.token
            })
            if (DEBUG) console.log('Token sent')
            if (req.session.memberData) {
              const result = {
                success: true,
                error: '',
                memberData: req.session.memberData
              }
              ws.mySend('loggedin', result)
              if (DEBUG) console.log('loggedin sent')
            }
          })
        }
      })

      ws.on('error', error => {
        if (error) console.err(error)
      })

      ws.on('close', () => {
        //sessionStore.close();
        if (DEBUG) console.log('User disconnected')
        ws.token = null
      })

      ws.on('message', data => {
          data = JSON.parse(data)
          if (data && data.token && data.token === ws.token) {
            switch (data.command) {
              /*case 'register':
                if (data.email && data.password && /^.{6,50}$/.test(data.password)) {
                  bcrypt.hash(data.password, 10).then(hash => {
                    DB.updatePassword(data.email, hash, (err, rows) => {
                      if (err) throw err;
                    });
                    //fs.mkdir(`users/${data.user}`, 0o770, err => {
                    //  if (err) throw err;
                    //});
                    ws.mySend('registered', {
                      success: true,
                    });
                  }).catch(err => {
                    if (DEBUG) console.log(`bcrypt error: ${err}`);
                  });
                } else {
                  if (DEBUG) console.log('invalid registration data');
                  ws.mySend('registered', {
                    success: false,
                  });
                }
                break;*/
              case 'login':
                if (data.email && data.password) {
                  DB.loginUser(data.email, (err, rows) => {
                      if (err) throw err
                      const result = {
                        success: true,
                        error: '',
                        memberData: undefined
                      }
                      if (rows.length === 1) {
                        bcrypt.compare(data.password, rows[0].dtPassword).then(res => {
                          if (res) {
                            req.session.memberData = rows[0]
                            //req.session.memberData.IP = req.headers['x-forwarded-for'];
                            result.memberData = req.session.memberData
                            req.session.save()
                            if (DEBUG) console.log('-------------------------- Login session start')
                            if (DEBUG) console.dir(req.session)
                            if (DEBUG) console.log('-------------------------- Login session end')
                            DB.updateLoginTimestamp(data.email, (err, rows) => {
                              if (err) throw err
                            })
                          } else {
                            if (DEBUG) console.log('Wrong password')
                            result.success = false
                            result.error = 'Invalid login!'
                          }
                          ws.mySend('loggedin', result)
                        }).catch(err => {
                          if (DEBUG) console.log(`bcrypt error: ${err}`)
                        })
                      } else {
                        if (DEBUG) console.log('Invalid data received: ')
                        if (DEBUG) console.log(data.email + ' ' + data.password)
                        result.success = false
                        result.error = 'Invalid login!'
                        ws.mySend('loggedin', result)
                      }
                    }
                  )
                }
                break
              case 'logout':
                if (DEBUG) console.log('logout received...')
                delete req.session.memberData
                req.session.save()
                /*
                // https://github.com/expressjs/session/issues/425
                const tempSession = req.session;
                req.session.regenerate(err => {
                  if (err && DEBUG) console.log('Session could not be regenerated: ' + err);
                  Object.assign(req.session, tempSession);
                });*/
                /*req.session.regenerate(err => {
                  if (err && DEBUG) console.log('Session could not be regenerated: ' + err);
                });*/
                ws.mySend('loggedout')
                break
              case 'updateMember':
                if (data.email && data.email === req.session.memberData.dtEmail &&
                  data.newMemberData &&
                  /^.{1,50}$/.test(data.newMemberData.dtFirstName) &&
                  /^.{1,50}$/.test(data.newMemberData.dtLastName) &&
                  /^.{6,50}$/.test(data.newMemberData.dtEmail) &&
                  /^.{1,50}$/.test(data.newMemberData.dtStreet) &&
                  /^.{1,50}$/.test(data.newMemberData.dtCity) &&
                  /^[A-Z]-$/.test(data.newMemberData.dtCountry) &&
                  /^[0-9]{4,15}$/.test(data.newMemberData.dtPostalCode) &&
                  /^$|^.{1,50}$/.test(data.newMemberData.dtSchool) &&
                  /^$|^.{1,50}$/.test(data.newMemberData.dtFunction) &&
                  /^$|^.{1,50}$/.test(data.newMemberData.dtSubject) &&
                  /^$|^[A-Za-z]{2}[0-9 ]{18,22}$/.test(data.newMemberData.dtIBAN) &&
                  /^.{1,3}$/.test(data.newMemberData.dtAP_V) &&
                  /^[0-1]$/.test(data.newMemberData.dtGetEmail) &&
                  (!data.newPW || /^.{6,50}$/.test(data.newPW))
                ) {
                  DB.updateMember(data.email, data.newMemberData, (err, rows) => {
                      if (data.newPW) {
                        bcrypt.hash(data.newPW, 10).then(hash => {
                          DB.updatePassword(data.newMemberData.dtEmail, hash, (err, rows) => {
                            if (err) throw err
                          })
                        }).catch(err => {
                          if (DEBUG) console.log(`bcrypt error: ${err}`)
                        })
                      }
                      const result = {
                        success: err ? false : true,
                        error: err
                      }
                      ws.mySend('updatedMember', result)
                      if (err) console.dir(err)
                      else {
                        req.session.memberData = data.newMemberData // Update session member
                        // data
                        req.session.save()
                      }
                    }
                  )
                } else {
                  const result = {
                    success: false,
                    error: 'Invalid data'
                  }
                  ws.mySend('updatedMember', result)
                }
                break
              case 'getFile':
                if (req.session.memberData && fs.existsSync(data.path)) {
                  if (DEBUG) console.log(`getFile path: ${data.path}`)
                  fs.readFile(data.path, (err, data) => {
                    if (DEBUG && err) console.err(err)
                    ws.mySend('file', {
                      data: data
                    })
                  })
                } else if (DEBUG) console.log(`Cannot find ${data.path}`)
                break
              case 'test':
                if (DEBUG) console.log(data.msg)
                ws.mySend('test', {
                    msg: data.msg
                  }
                )
                break
              case 'getMembers':
                if (req.session.memberData)
                  DB.getMembers((err, rows) => {
                    if (err && DEBUG) console.log(err)
                    else ws.mySend('members', {rows: rows})
                  })
                break
              case 'getAssets':
                const assets = []
                for (const asset of data.assets) {
                  if (fs.existsSync(asset.file))
                    assets.push({
                      asset: asset.file, data: fs.readFileSync(asset.file,
                        asset.encoding === 'binary' ? '' : 'utf8')
                    })
                }
                ws.mySend('assets', {assets: assets})
                break
              case 'reloadMemberData':
                if (req.session.memberData)
                  DB.loginUser(req.session.memberData.dtEmail, (err, rows) => {
                    if (err) throw err
                    const result = {
                      success: true,
                      error: '',
                      memberData: undefined
                    }
                    if (rows.length === 1) {
                      req.session.memberData = rows[0]
                      result.memberData = req.session.memberData
                      req.session.save()
                    } else {
                      result.success = false
                      result.error = 'Invalid user!'
                    }
                    ws.mySend('reloadedMemberData', result)
                  })
                break
            }
          } else if (DEBUG) console.log('register: Intruder alarm: no token sent!')
        }
      )
    })

    wss.on('error', error => {
      if (error) console.err(error)
    })
  }
}
5.4.16.4. server.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
'use strict'

const DEBUG = false
const fs = require('fs')
const spdy = require('spdy')
const express = require('express')
const app = express()
const minify = require('express-minify')
const uglifyEs = require('uglify-es')
const helmet = require('helmet')
const DB = require(__dirname + '/DB')
const session = require('express-session')
const MySQLStore = require('express-mysql-session')(session)
const WebSocketServer = require(__dirname + '/WebSocketServer')
const port = 9002 // Port that our server will be listening on. It needs to be opened on
// the firewall.
const options = { // Paths to our private key and certificate.
  key: fs.readFileSync('/etc/letsencrypt/live/apess.lu/privkey.pem'),
  cert: fs.readFileSync('/etc/letsencrypt/live/apess.lu/cert.pem')
}
const mysqlSessionOpts = require(__dirname + '/mysql_options').mysqlSessionOpts
const sessionStore = new MySQLStore(mysqlSessionOpts)
const sessionOpts = {
  cookie: {
    path: '/', domain: 'apess.lu', secure: true, httpOnly: true/*, expires: new
     Date(Date.now() + 86400000)*//* 24
     hours,
         maxAge: new Date(Date.now() + 300000)*/ /* 5 min */
  },
  secret: 'my-secret',
  resave: false,
  saveUninitialized: true,
  name: 'test1.sid',
  key: 'session_cookie_name',
  store: sessionStore,
  secure: true,
  proxy: true,
  sameSite: true
}

process.setuid(1000) // Switch to webdev user as running with root is too dangerous http://syskall.com/dont-run-node-dot-js-as-root.
app.use(helmet())
const sess = session(sessionOpts)
// https://stackoverflow.com/questions/46295635/how-to-get-ip-address-in-node-js-express
app.disable('x-powered-by') //https://stackoverflow.com/questions/10717685/how-to-remove-x-powered-by-in-expressjs
app.set('trust proxy', true) // https://expressjs.com/en/guide/behind-proxies.html
app.use(sess)
app.use(minify({
  uglifyJsModule: uglifyEs
}))
app.use(express.static(__dirname + '/assets/public'))
// https://stackoverflow.com/questions/21170253/cannot-use-basic-authentication-while-serving-static-files-using-express/21170931
const isLoggedIn = (req, res, next) => {
  if (DEBUG) console.log('isLoggedIn1')
  if (DEBUG) console.dir(req.session)
  if (req.session.memberData) {// && req.headers['x-forwarded-for'] === req.session.memberData.IP)
    if (DEBUG) console.log('isLoggedIn2')
    if (DEBUG) console.dir(req.session)
    return next()
  } else res.redirect('/netageloggt.html') // Not logged in -> redirect to page so user can log in.
  //res.end();
}
app.use('/members', [isLoggedIn, express.static(__dirname + '/assets/members')])
app.get('/*', (req, res) => {
  /*console.dir(req.ip);
  console.dir(req.ips);*/
  if (req.headers.host === 'apess.lu' || req.headers.host === 'www.apess.lu' ||
    req.headers.referer === 'https://apess.lu/' ||
    req.headers.referer === 'https://www.apess.lu/') {
    if (DEBUG) console.log(req.url)
    const validPublicFilenames = ['/', '/index.html', '/index.css', '/index.babel.js', '/polyfill.min.js',
      '/core.min.js', '/reconnecting-websocket.min.js', '/WebSocketClient.babel.js', '/netageloggt.html']
    const pubIdx = validPublicFilenames.indexOf(req.url)
    if (pubIdx > -1 && pubIdx < 2) {
      DB.insertIPUA(req.headers['x-forwarded-for'], req.headers['user-agent'])
      res.sendFile(__dirname + '/index.html')
    } else if (pubIdx >= 2 && pubIdx < validPublicFilenames.length) {
      res.sendFile(__dirname + validPublicFilenames[pubIdx])
      if (DEBUG) console.log(__dirname + validPublicFilenames[pubIdx])
    } else res.end()
    if (DEBUG && req.cookie) console.log(req.cookie)
    if (DEBUG) console.log(req.sessionID)
    if (DEBUG) console.dir(req.session)
    if (req.session) {
      //req.session.userName = 'Bill';
      if (DEBUG) console.dir(req.session)
    } else console.log('app.get NO SESSION!!!')
  } else {
    res.end()
    console.log('Invalid host!')
  }
})

const server = spdy.createServer(options, app)

server.on('listening', () => {
  if (DEBUG) console.log(`Listening on port ${port}...`)
})

server.on('connection', () => {
  if (DEBUG) console.log('New connection...')
})

server.on('close', () => {
  sessionStore.close()
  if (DEBUG) console.log('Connection closed...')
})

server.listen(port, err => {
  if (err) console.log('Server error')
})

const WSS = new WebSocketServer(server, sess)
5.4.16.5. WebSocketClient.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// This is the WebSocket client controller class, which handles all WebSocket communication
// but does not touch the GUI.
class WebSocketClient {
  constructor(cb) { // Callback should only be called after server has sent token to client.
    const DEBUG = false
    const wsURL = 'wss://apess.lu/wss'
    const ws = new ReconnectingWebSocket(wsURL)
    const messageCallbacks = []
    ws.token = null
    ws.addEventListener('open', () => {
      if (DEBUG) console.log('WebSocket connection opened.')
    })

    ws.addEventListener('message', e => {
      const data = JSON.parse(e.data)
      if (!data.msg) return
      if (data.msg === 'token') {
        ws.token = data.token
        cb()
      } else for (const cb of messageCallbacks)
        if (cb.msg === data.msg) {
          cb.cb(data)
          break
        }
    })

    ws.addEventListener('close', () => {
      if (DEBUG) console.log('Disconnected...')
    })

    ws.addEventListener('error', () => {
      if (DEBUG) console.log('Error...')
    })

    const execute = (command, options = {}) => {
      let obj = {}
      Object.assign(obj, {command: command}, options)
      ws.send(JSON.stringify(obj))
    }

    this.registerMessageCB = (msg, cb) => {
      messageCallbacks.push({msg: msg, cb: cb})
    }

    this.deleteAllCBsForMessage = msg => {
      const filter = cb => {
        return cb.msg === msg
      }
      let idx = messageCallbacks.findIndex(filter)
      while (idx >= 0) {
        messageCallbacks.splice(idx, 1)
        idx = messageCallbacks.findIndex(filter)
      }
    }

    this.login = (email, pw) => {
      execute('login', {
        token: ws.token,
        email: email,
        password: pw
      })
    }

    this.register = (email, pw) => {
      if (/^.{6,50}$/.test(pw)) {
        execute('register', {
          token: ws.token,
          email: email,
          password: pw
        })
      } else alert('Invalid registration data!')
    }

    this.logout = () => {
      execute('logout', {
        token: ws.token
      })
    }

    this.updateMember = (email, newMemberData, newPW) => {
      execute('updateMember', {
        token: ws.token,
        email: email,
        newMemberData: newMemberData,
        newPW: newPW
      })
    }

    this.getFile = path => {
      //console.log(`Path: ${path}`);
      execute('getFile', {
        token: ws.token,
        path: path
      })
    }

    this.test = () => {
      execute('test', {token: ws.token, msg: 'Hello world!'})
    }

    this.getMembers = () => {
      execute('getMembers', {token: ws.token})
    }

    this.getAssets = assets => {
      if (Array.isArray(assets)) execute('getAssets', {token: ws.token, assets: assets})
    }

    this.reloadMemberData = () => {
      execute('reloadMemberData', {
        token: ws.token
      })
    }
  }
}
5.4.16.6. index.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
'use strict'

{
  const init = () => {
      const DEBUG = false
      const assetPaths = [
        /*{file: 'accueil.html', encoding: 'utf8'},
        {file: 'accueil.css', encoding: 'utf8'},*/
        {file: 'CHFEP2020.html', encoding: 'utf8'},
        {file: 'CHFEP2020.css', encoding: 'utf8'},
        {file: 'presentatioun.html', encoding: 'utf8'},
        {file: 'presentatioun.css', encoding: 'utf8'},
        {file: 'communiqueen.html', encoding: 'utf8'},
        {file: 'communiqueen.css', encoding: 'utf8'},
        {file: 'editiounen.html', encoding: 'utf8'},
        {file: 'editiounen.css', encoding: 'utf8'},
        {file: 'dossieren.html', encoding: 'utf8'},
        {file: 'dossieren.css', encoding: 'utf8'},
        {file: 'legislatioun.html', encoding: 'utf8'},
        {file: 'legislatioun.css', encoding: 'utf8'},
        {file: 'ressourcen.html', encoding: 'utf8'},
        {file: 'ressourcen.css', encoding: 'utf8'},
        {file: 'interna.html', encoding: 'utf8'},
        {file: 'interna.css', encoding: 'utf8'},
        {file: 'interna.js', encoding: 'utf8'},
        {file: 'login.html', encoding: 'utf8'},
        {file: 'login.css', encoding: 'utf8'},
        {file: 'profil.html', encoding: 'utf8'},
        {file: 'profil.css', encoding: 'utf8'},
        {file: 'admin.html', encoding: 'utf8'},
        {file: 'admin.css', encoding: 'utf8'}
        /*, {file: 'Apess Statuts 2009.pdf', encoding: 'binary'}*/
      ]
      const assets = []
      let wsClient = undefined
      let loggedin = false
      let memberData = undefined

      // Prevent reaction to touch.
      document.addEventListener("touchstart", () => {
      }, true)

      const getAsset = path => {
        for (const asset of assets) if (asset.asset === path) return asset.data
        return false
      }

      const toggleNavButton = id => {
        const buttons = document.querySelectorAll('header > nav > ul > li > button')
        for (const button of buttons) button.style.backgroundColor = ''
        const button = document.querySelector(`header > nav > ul > li > button#${id}`)
        button.style.backgroundColor = 'lightgreen'
      }

      const switchMainDisplay = subject => {
        toggleNavButton(subject)
        const sheets = document.querySelectorAll('style')
        for (const sheet of sheets) document.head.removeChild(sheet)
        const html = getAsset(`${subject}.html`)
        const css = getAsset(`${subject}.css`)
        const js = getAsset(`${subject}.js`)
        document.querySelector('main').innerHTML = ''
        if (html && css) {
          document.querySelector('main').innerHTML = html
          if (js) {
            const script = document.createElement('script')
            script.innerHTML = js
            document.body.querySelector('main').appendChild(script)
          }
          const style = document.createElement('style')
          style.id = `${subject}Style`
          style.innerHTML = css
          document.head.appendChild(style)
        }
        document.querySelector('main').scrollTo(0, 0) // Scroll to the top.
      }

      const displayLogin = () => {
        const html = getAsset('login.html')
        const css = getAsset('login.css')
        if (html && css) {
          const loginStyle = document.createElement('style')
          loginStyle.id = 'loginStyle'
          loginStyle.innerHTML = css
          document.head.appendChild(loginStyle)
          const oldBodyHTML = document.body.innerHTML
          document.body.innerHTML = html
          const buttons = document.querySelectorAll('button')
          const loginButton = buttons[0], cancelButton = buttons[1]
          loginButton.addEventListener('click', () => {
            wsClient.login(document.getElementsByName('email')[0].value,
              document.getElementsByName('password')[0].value)
            document.body.innerHTML = oldBodyHTML
            document.head.removeChild(loginStyle)
            //document.querySelector('button').addEventListener('click', displayLogin);
            displayMain()
          })
          cancelButton.addEventListener('click', () => {
            document.body.innerHTML = oldBodyHTML
            document.head.removeChild(loginStyle)
            displayMain()
          })
          const unInput = document.querySelector('input[name=email]')
          if (unInput) {
            unInput.focus()
            unInput.addEventListener('keyup', e => {
              if (e.code === 'Enter') {
                const input = document.querySelector('input[name=password]')
                if (input) input.focus()
              }
            })
          }
          const pwInput = document.querySelector('input[name=password]')
          if (pwInput)
            pwInput.addEventListener('keyup', e => {
              if (e.code === 'Enter' && document.querySelector('input[name=email]:valid')) {
                const button = document.querySelector('button[name=login]')
                if (button) button.click()
              }
            })
        }
      }

      const displayCHFEP2020 = () => {
        switchMainDisplay('CHFEP2020')
      }

      const displayPresentatioun = () => {
        switchMainDisplay('presentatioun')
      }

      const displayCommuniqueen = () => {
        switchMainDisplay('communiqueen')
        /*        PDFJS.getDocument('Communique_22.10.17.pdf').then(pdf => {
                  pdf.getPage(1).then(page => {
                    const scale = 1.5;
                    const viewport = page.getViewport(scale);
                    const canvas = document.querySelector('canvas');
                    const context = canvas.getContext('2d');
                    canvas.height = viewport.height;
                    canvas.width = viewport.width;

                    const renderContext = {
                      canvasContext: context,
                      viewport: viewport
                    };
                    page.render(renderContext);
                  });
                });*/
      }

      const displayEditiounen = () => {
        switchMainDisplay('editiounen')
      }

      const displayDossieren = () => {
        switchMainDisplay('dossieren')
      }

      const displayLegislatioun = () => {
        switchMainDisplay('legislatioun')
        /*PDFJS.getDocument('ApessStatuts.pdf').then(pdf => {
          pdf.getPage(1).then(page => {
            const scale = 1.5;
            const viewport = page.getViewport(scale);
            const canvas = document.querySelector('canvas');
            const context = canvas.getContext('2d');
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            const renderContext = {
              canvasContext: context,
              viewport: viewport
            };
            page.render(renderContext);
          });
        });*/
      }

      const displayRessourcen = () => {
        switchMainDisplay('ressourcen')
      }

      const foldMenuButton = () => {
        document.querySelector('#menuButton').classList.remove('active')
        document.querySelector('nav').classList.remove('active')
      }

      const toggleMenuButton = () => {
        document.querySelector('#menuButton').classList.toggle('active')
        document.querySelector('nav').classList.toggle('active')
      }

      const displayMain = () => {
        /*const getPic = path => {
          ws.mySend('getFile', {
            token: ws.token,
            path: path,
          });
        };*/
        /*
          else
            if (data.command === 'pic') {
              const buffer = Uint8Array.from(data.data.data);
              const blob = new Blob([buffer]);
              const image = new Image();
              const URL = window.URL || window.webkitURL;
              image.src = URL.createObjectURL(blob);
              document.querySelector('body').appendChild(image);
              URL.revokeObjectURL(image.src);
            }
    */
        /*document.head.appendChild(mainStyle);
        let HTML = `<img src=APESS_logo.jpg><h1>APESS</h1>`;
        document.body.innerHTML = HTML;*/
        const listeners = [/*displayAccueil,*/ displayCHFEP2020, displayPresentatioun,
          displayCommuniqueen, displayEditiounen, displayDossieren, displayLegislatioun,
          displayRessourcen, displayLogin]
        const buttons = document.querySelectorAll('header nav > ul > li > button')
        if (buttons.length === listeners.length)
          for (let i = 0; i < listeners.length; i++)
            buttons[i].onclick = function () {
              foldMenuButton()
              listeners[i]()
            }
        document.querySelector('#menuButton').addEventListener('click', toggleMenuButton)
      }

      const displayInterna = () => {
        switchMainDisplay('interna')
      }

      const displayProfil = () => {
        foldMenuButton()
        toggleNavButton('profil')
        const sheets = document.querySelectorAll('style')
        for (const sheet of sheets) document.head.removeChild(sheet)
        const html = getAsset('profil.html')
        const css = getAsset('profil.css')
        document.querySelector('main').innerHTML = ''
        if (html && css) {
          document.querySelector('main').innerHTML = html
          const style = document.createElement('style')
          style.id = `profilStyle`
          style.innerHTML = css
          document.head.appendChild(style)
          const firstNameInput = document.querySelector('input[name=firstName]')
          firstNameInput.value = memberData.dtFirstName
          const lastNameInput = document.querySelector('input[name=lastName]')
          lastNameInput.value = memberData.dtLastName
          const emailInput = document.querySelector('input[name=email]')
          emailInput.value = memberData.dtEmail
          const streetInput = document.querySelector('input[name=street]')
          streetInput.value = memberData.dtStreet
          const cityInput = document.querySelector('input[name=city]')
          cityInput.value = memberData.dtCity
          const countryInput = document.querySelector('input[name=country]')
          countryInput.value = memberData.dtCountry
          const postalCodeInput = document.querySelector('input[name=postalCode]')
          postalCodeInput.value = memberData.dtPostalCode
          const schoolInput = document.querySelector('input[name=school]')
          schoolInput.value = memberData.dtSchool
          const functionInput = document.querySelector('input[name=function]')
          functionInput.value = memberData.dtFunction
          const subjectInput = document.querySelector('input[name=subject]')
          subjectInput.value = memberData.dtSubject
          const adhesionInput = document.querySelector('input[name=adhesion]')
          let val = ''
          if (memberData.dtAdhesion) {
            const d1 = new Date(memberData.dtAdhesion)
            val = d1.getDate() + '.' + (d1.getMonth() + 1) + '.' + d1.getFullYear()
          }
          adhesionInput.value = val
          const lastPaymentDateInput = document.querySelector('input[name=lastPaymentDate]')
          val = ''
          if (memberData.dtLastPaymentDate) {
            const d2 = new Date(memberData.dtLastPaymentDate)
            val = d2.getDate() + '.' + (d2.getMonth() + 1) + '.' + d2.getFullYear()
          }
          lastPaymentDateInput.value = val
          const BICInput = document.querySelector('input[name=BIC]')
          BICInput.value = memberData.dtBIC
          const IBANInput = document.querySelector('input[name=IBAN]')
          IBANInput.value = memberData.dtIBAN
          const AP_VInput = document.querySelectorAll('input[name=AP_V]')
          if (memberData.dtAP_V === 'V') AP_VInput[1].checked = true
          else AP_VInput[0].checked = true
          //AP_VInput.value = memberData.dtAP_V;
          const getEmailInput = document.querySelector('input[name=getEmail]')
          getEmailInput.checked = memberData.dtGetEmail
          const pw1Input = document.querySelector('input[name=pw1]')
          const pw2Input = document.querySelector('input[name=pw2]')
          document.querySelector('button[name=save]').onclick = () => {
            // Check values client-side.
            if (firstNameInput.checkValidity() && lastNameInput.checkValidity()
              && emailInput.checkValidity() && streetInput.checkValidity()
              && cityInput.checkValidity() && countryInput.checkValidity()
              && postalCodeInput.checkValidity() && schoolInput.checkValidity()
              && functionInput.checkValidity() && subjectInput.checkValidity()
              && BICInput.checkValidity()
              && IBANInput.checkValidity()  /*&& AP_VInput.checkValidity()
              && getEmailInput.checkValidity() */ && pw1Input.checkValidity()
              && pw2Input.checkValidity()) {
              const newMemberData = memberData
              newMemberData.dtFirstName = firstNameInput.value
              newMemberData.dtLastName = lastNameInput.value
              /*newMemberData.dtEmail = emailInput.value;*/
              newMemberData.dtStreet = streetInput.value
              newMemberData.dtCity = cityInput.value
              newMemberData.dtCountry = countryInput.value
              newMemberData.dtPostalCode = postalCodeInput.value
              newMemberData.dtSchool = schoolInput.value
              newMemberData.dtFunction = functionInput.value
              newMemberData.dtSubject = subjectInput.value
              newMemberData.dtBIC = BICInput.value
              newMemberData.dtIBAN = IBANInput.value
              if (AP_VInput[0].checked) newMemberData.dtAP_V = 'APS'
              else newMemberData.dtAP_V = 'V'
              newMemberData.dtGetEmail = getEmailInput.checked ? 1 : 0
              let newPW = ''
              if (pw1Input.value || pw2Input.value)
                if (pw1Input.value && pw2Input.value && pw1Input.value === pw2Input.value)
                  newPW = pw1Input.value
                else {
                  alert("D'Passwierder stëmmen net iwwereneen")
                  return
                }
              wsClient.updateMember(memberData.dtEmail, newMemberData, newPW)
            } else alert('Iwwerpréift w.e.g. Är Daten.')
            // If OK send them to the server, then check them server-side.
          }
        }
      }

      const displayLoggedinScreen = () => {
        //wsClient.getMembers();
        /*let li = document.querySelector('#ressourcen').parentNode;
        li.parentNode.removeChild(li);*/
        // Remove login dialog if it is currently displayed.
        // If it is not displayed that means we were already logged in, so no need for work.
        let loginDialog = document.querySelector('#login')
        let loginDialogParent = null
        if (loginDialog) loginDialogParent = loginDialog.parentNode
        if (loginDialogParent && loginDialogParent.parentNode) {
          loginDialogParent.parentNode.removeChild(loginDialogParent)
          let li = document.createElement('li')
          let button = document.createElement('button')
          button.id = 'interna'
          button.innerText = 'Interna'
          li.appendChild(button)
          document.querySelector('ul').appendChild(li)
          li = document.createElement('li')
          button = document.createElement('button')
          button.id = 'profil'
          button.innerText = 'Profil'
          button.addEventListener('click', () => {
            wsClient.reloadMemberData()
          })
          li.appendChild(button)
          document.querySelector('ul').appendChild(li)
          li = document.createElement('li')
          button = document.createElement('button')
          button.id = 'logout'
          button.innerText = 'Ausloggen'
          button.addEventListener('click', e => {
            wsClient.logout()
          })
          li.appendChild(button)
          document.querySelector('ul').appendChild(li)

          const listeners = [/*displayAccueil,*/ displayCHFEP2020, displayPresentatioun,
            displayCommuniqueen, displayEditiounen, displayDossieren, displayLegislatioun,
            displayRessourcen, displayInterna]
          const buttons = document.querySelectorAll('header nav > ul > li > button')
          for (let i = 0; i < listeners.length; i++)
            buttons[i].onclick = () => {
              foldMenuButton()
              listeners[i]()
            }
          document.querySelector('#menuButton').addEventListener('click', toggleMenuButton)
        }
      }

      const displayMembers = members => {
        if (!members || !Array.isArray(members)) return
        const keys = Object.keys(members[0])
        const main = document.querySelector('main')
        const table = document.createElement('table')
        const caption = document.createElement('caption')
        caption.innerText = 'Members'
        table.appendChild(caption)
        const thead = document.createElement('thead')
        let tr = document.createElement('tr')
        for (const key of keys) {
          let th = document.createElement('th')
          th.innerText = key
          tr.appendChild(th)
        }
        thead.appendChild(tr)
        table.appendChild(thead)
        const tbody = document.createElement('tbody')
        for (const member of members) {
          let tr = document.createElement('tr')
          for (const key of keys) {
            let td = document.createElement('td')
            td.innerText = member[key]
            tr.appendChild(td)
          }
          tbody.appendChild(tr)
        }
        table.appendChild(tbody)
        const css = getAsset('admin.css')
        if (css) {
          const tableStyle = document.createElement('style')
          tableStyle.id = 'tableStyle'
          tableStyle.innerHTML = css
          document.head.appendChild(tableStyle)
        }
        main.innerHTML = ''
        main.appendChild(table)
      }

      wsClient = new WebSocketClient(() => {
        wsClient.registerMessageCB('loggedin', data => {
          if (data.success) {
            loggedin = true
            memberData = data.memberData
            displayLoggedinScreen()
            switchMainDisplay('interna')
            //switchMainDisplay('communiqueen');
            //switchMainDisplay('presentatioun');
            //switchMainDisplay('CHFEP2020');
          } else {
            window.alert('Är Emailadress an/oder Äert Passwuert si net korrekt!')
          }
        })
        wsClient.registerMessageCB('loggedout', data => {
          window.location = window.location //reload
        })
        wsClient.registerMessageCB('registered', data => {
          alert('Registered!')
        })
        wsClient.registerMessageCB('test', data => {
          if (DEBUG) console.log(data.msg)
        })
        wsClient.registerMessageCB('file', data => {
          console.dir(data.data.data)
          const showFile = blob => {
            // It is necessary to create a new blob object with mime-type explicitly set
            // otherwise only Chrome works like it should
            let newBlob = new Blob([blob], {type: "application/pdf"})

            // IE doesn't allow using a blob object directly as link href
            // instead it is necessary to use msSaveOrOpenBlob
            if (window.navigator && window.navigator.msSaveOrOpenBlob) {
              window.navigator.msSaveOrOpenBlob(newBlob)
              return
            }

            // For other browsers:
            // Create a link pointing to the ObjectURL containing the blob.
            const data = window.URL.createObjectURL(newBlob)
            let link = document.createElement('a')
            link.href = data
            link.download = "file.pdf"
            link.click()
            console.dir(link)
            setTimeout(() => {
              // For Firefox it is necessary to delay revoking the ObjectURL
              window.URL.revokeObjectURL(data)
            }, 100)
          }
          showFile(data.data.data)
        })
        wsClient.registerMessageCB('members', data => {
          displayMembers(data.rows)
        })
        wsClient.registerMessageCB('image', data => {
          const buffer = Uint8Array.from(data.data.data)
          //https://stackoverflow.com/questions/21002750/send-pdf-file-using-websocket-node-js
          const blob = new Blob([buffer], {type: 'image/jpeg'})
          const image = new Image()
          const URL = window.URL || window.webkitURL
          image.src = URL.createObjectURL(blob)
          document.querySelector('body').appendChild(image)
          URL.revokeObjectURL(image.src)
        })
        wsClient.registerMessageCB('assets', data => {
          for (const arr of data.assets) assets.push(arr)
          if (DEBUG) console.dir(assets)
          if (!loggedin) displayMain()
          switchMainDisplay('communiqueen')
          //switchMainDisplay('presentatioun');
          //switchMainDisplay('CHFEP2020');
        })
        wsClient.registerMessageCB('updatedMember', function (result) {
          if (result.error) {
            if (DEBUG) console.log(result.error)
            alert('Är Daten konnten net gespäichert ginn.')
          } else alert('Är Daten goufen gespäichert.')
        })
        wsClient.registerMessageCB('reloadedMemberData', result => {
          if (result.error) {
            if (DEBUG) console.log(result.error)
            window.location = window.location //reload
          } else {
            memberData = result.memberData
            displayProfil()
          }
        })
        wsClient.getAssets(assetPaths)
      })
    }


  window.addEventListener('load', init)
}
5.4.16.7. index.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<!DOCTYPE html>
<html lang=lu>
  <head>
    <meta charset=UTF-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <title>APESS</title>
    <link rel=canonical href=https://apess.lu>
    <link rel="apple-touch-icon" sizes="57x57" href="favicon/apple-icon-57x57.png">
    <link rel="apple-touch-icon" sizes="60x60" href="favicon/apple-icon-60x60.png">
    <link rel="apple-touch-icon" sizes="72x72" href="favicon/apple-icon-72x72.png">
    <link rel="apple-touch-icon" sizes="76x76" href="favicon/apple-icon-76x76.png">
    <link rel="apple-touch-icon" sizes="114x114" href="favicon/apple-icon-114x114.png">
    <link rel="apple-touch-icon" sizes="120x120" href="favicon/apple-icon-120x120.png">
    <link rel="apple-touch-icon" sizes="144x144" href="favicon/apple-icon-144x144.png">
    <link rel="apple-touch-icon" sizes="152x152" href="favicon/apple-icon-152x152.png">
    <link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-icon-180x180.png">
    <link rel="icon" type="image/png" sizes="192x192" href="favicon/android-icon-192x192.png">
    <link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="96x96" href="favicon/favicon-96x96.png">
    <link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16x16.png">
    <link rel="manifest" href="favicon/manifest.json">
    <meta name="msapplication-TileColor" content="#ffffff">
    <meta name="msapplication-TileImage" content="ms-icon-144x144.png">
    <meta name="theme-color" content="#ffffff">
    <!--<link href=https://fonts.googleapis.com/css?family=Shadows+Into+Light
    rel=stylesheet>-->
    <!--<link href=https://fonts.googleapis.com/css?family=Pacifico rel=stylesheet>-->
    <link href=index.css rel=stylesheet>
    <!--
    <script>
      //https://stackoverflow.com/questions/24861073/detect-if-any-kind-of-ie-msie
      var isIE = !!navigator.userAgent.match(/Trident/g) ||
        !!navigator.userAgent.match(/MSIE/g);
      if (isIE) {
        alert("Op Grond vum Asaz vu modernster Webtechnologie funktionéiert dëse Site net" +
          " optimal mam Microsoft Internet Explorer deen e groussen Deel vun deenen" +
        " Technologien net ënnerstëtzt. Mam Firefox, Chrome, Safari an anere Browseren dierft" +
        " et keng Problemer ginn.");
      }
    </script>-->
    <!--<script src=polyfill.min.js></script>-->
    <script src=core.min.js></script>
    <script src=reconnecting-websocket.min.js></script>
    <script src=WebSocketClient.babel.js></script>
    <!--<script src=pdf.min.js></script>-->
    <script src=index.babel.js></script>
  </head>
  <body>
    <noscript><h1>Dëse Site funktionéiert net ouni JavaScript!</h1></noscript>
    <header>
      <!--<img src=APESS_logo.jpg
           alt="Association des Professeurs de l'Enseignement Secondaire et Supérieur"
           title="Association des Professeurs de l'Enseignement Secondaire et Supérieur"
           width=171 height=79>-->
      <section id=header>
        <div>APESS</div>
        <div>Association des Professeurs de I'Enseignement Secondaire et Supérieur du
          Grand-Duché de Luxembourg, association sans but lucratif</div>
        <div><a href="&#109;&#x61;&#x69;&#108;&#116;&#111;&#58;&#x61;&#x70;&#101;&#115;&#115;&#x40;&#101;&#x64;&#117;&#99;&#x61;&#x74;&#x69;&#x6f;&#110;&#x2e;&#x6c;&#117;">&#97;&#112;&#x65;&#x73;&#115;&#x40;&#x65;&#x64;&#117;&#x63;&#x61;&#x74;&#x69;&#x6f;&#110;&#x2e;&#108;&#117;</a>
        </div>
      </section>
      <section id=menu><button id=menuButton></button><span>APESS</span></section>
      <nav>
        <ul>
          <!--<li>
            <button id=accueil>Accueil</button>
          </li>-->
          <li>
            <button id=presentatioun>Presentatioun</button>
          </li>
          <li>
            <button id=communiqueen>Communiquéen a Press</button>
          </li>
          <li>
            <button id=editiounen>Editiounen</button>
          </li>
          <li>
            <button id=dossieren>Dossieren</button>
          </li>
          <li>
            <button id=legislatioun>Legislatioun</button>
          </li>
          <li>
            <button id=ressourcen>Ressourcen</button>
          </li>
          <li>
            <button id=login>Aloggen</button>
          </li>
        </ul>
      </nav>
    </header>
    <main>
    </main>
  </body>
</html>
5.4.16.8. index.css
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
html { /* http://www.paulirish.com/2012/box-sizing-border-box-ftw */
  box-sizing: border-box;
}

*, *:before, *:after {
  box-sizing: inherit;
}

::-moz-focus-inner {
  border: 0; /* Remove dotted line around clicked button */
}

/*.email {
  unicode-bidi: bidi-override;
  direction:    rtl;
}*/

html, body {
  width:  100%;
  height: 100%;
  margin: 0;
}

body {
  display:   flex;
  flex-flow: column;
  overflow:  hidden;
  hyphens:   auto;
  font-family: Helvetica, sans-serif;
  /*  font-family: 'Pacifico', cursive;*/
}

main {
  margin:     5px;
  padding:    5px;
  /*padding-right: 15px;*/
  overflow:   auto;
  text-align: justify;
}

img {
  /*max-width:  100%;
  height: auto;*/
  /*max-height: 100%;
  width:      100%;
  height:     auto;*/
  margin:     auto;
}

h1 {
  text-align: left;
}

header > nav > ul {
  list-style: none;
  padding:    0;
  margin:     0;
  display:    flex;
  flex:       auto;
  flex-wrap:  wrap;
}

header > nav > ul > li {
  flex:       auto;
  text-align: center;
  padding:    2px;
}

input, button {
  font-family: inherit;
}

header > nav > ul > li > button {
  width:            100%;
  border:           none;
  background-color: rgb(240, 240, 240);
  padding:          2px;
  font-size:        1.3em;
  box-shadow:       4px 4px 4px #888888;
  font-family:      inherit;
}

header > nav > ul > li > button:focus, header nav > ul > li > button:active {
  border: none;
}

header > nav > ul > li > button:hover {
  background-color: grey;
}

@media all and (min-width: 571px) and (min-height: 401px) {
  #header {
    display: flex;
    /*font-family: 'Pacifico', cursive;*/
  }

  #header > div {
    flex:          auto;
    padding-right: 10px;
    padding-left:  10px;
  }

  #header > div {
    text-align: center;
    margin:     auto;
  }

  #header > div:first-child {
    font-size:   2em;
    color:       rgb(255, 50, 50);
    text-shadow: 5px 5px 5px grey;
  }

  header > nav {
    width: 100%;
  }

  header {
    /*min-height: 80px;*/
  }

  #menu {
    display: none;
  }
}

/*@media all and (max-width: 700px) {
  header > img {
    display: none;
  }
}*/

@media all and (max-width: 570px), (max-height: 400px) {
  #header {
    display: none;
  }

  #menu {
    /*font-family: 'Pacifico', cursive;*/
    /*font-size:   2em;*/
  }

  #menu > span {
    display:     inline-block;
    width:       calc(100vw - 160px);
    text-align:  center;
    color:       rgb(255, 50, 50);
    text-shadow: 5px 5px 5px grey;
    font-size:   1.5em;
  }

  #menuButton {
    /*display:         block;*/
    padding:          0 0.2em 0.2em 0.2em;
    background-color: rgb(240, 240, 240);
    text-decoration:  none;
    color:            #333;
    cursor:           pointer;
    font-size:        inherit;
    width:            10em;
    height:           3em;
  }

  #menuButton.active {
    background-color: #333;
    color:            #fff;
  }

  header {
    max-height: 100%;
  }

  header > nav {
    display: none;
  }

  header > nav.active {
    /* https://stackoverflow.com/questions/27992881/max-height-x-doesnt-work-on-chrome */
    /* vh -> https://css-tricks.com/viewport-sized-typography/ */
    max-height: calc(100vh - 48px); /* 48px is height of menu section */
    display:    block;
    overflow:   auto;
  }

  header > nav.active > ul {
    display:    block;
    max-height: calc(100vh - 27.2px);
  }

  header > nav.active > ul > li {
    display: block;
  }
}

/*@media all and (max-height: 404.2px) {
  body {
    display: block;
  }
  header {
    overflow: hidden;
  }

  header > nav.active {
    flex: auto;
    overflow: auto;
    min-height: 368px;
    height: 368px;
  }

  header > nav, header > nav > ul {
    overflow: auto;
  }

  main {
    display: none;
  }
}*/
5.4.16.9. dossieren.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<article><h1>Ofschafe vun de Proffecomitéen</h1>
D'<a href=http://legilux.public.lu/eli/etat/leg/loi/2004/06/25/n9/jo target=_blank>Gesetz
  vum 25.6.04</a> iwwert d'Organisatioun vun de Lycéeën an technesche Lycéeën gouf am
<a href=http://legilux.public.lu/eli/etat/leg/loi/2016/12/15/n1/jo target=_blank>Gesetz
  vum 15.12.16</a> geännert (cf. Säit 5 Artikelen 9 an 10 resp. Säit 7 Artikel 33 am ale
Gesetz).

Dat Gesetz ass säit dem 25.12.16 a Kraaft, legal gëtt et also keng Proffecomitéen méi.
</article>
<article>
  <a href=http://www.men.public.lu/fr/actualites/articles/communiques-conference-presse/2017/10/20-manuels-gratuits
     target=_blank><h1>Gratis Schoulbicher fir d'Lycéesschüler vun der Rentrée 2018-2019 un</a>
</article>
5.4.16.10. dossieren.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
main > article > h1 {
  text-align: center;
}

/*main > article > a {
  text-decoration: none;
}*/

main > article {
  background-color: rgb(240, 240, 240);
  padding:    5px;
  margin:     0 10px 20px;
  box-shadow: 10px 10px 10px black;
}

main > article > img {
  display: block;
}
5.4.16.11. editiounen.html
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
<section>
  <header>
    <h1>Cahiers pédagogiques</h1>
  </header>
  <div>
    <article>
      <img src=editiounen/cp/CP1.png width=230 height=155
           alt='Cahier pédagogique 1 - Homo Faber'>
      <h1>Cahier pédagogique 1 - Homo Faber</h1>
      6e édition revue et corrigée, 136 p.<br>
      ISBN 978-2-87979-146-3<br>
      prix : 11 €<br>
      emballage pour les librairies : cartons à 32 exemplaires
    </article>
    <article>
      <img src=editiounen/cp/CP7.jpg width=87 height=123
           alt='Cahier pédagogique 7 - Allemand VIIe–Ve'>
      <h1>Cahier pédagogique 7 - Allemand VII<sup>e</sup>–V<sup>e</sup></h1>
      Exercices (238 p.) et corrigés (142 p.)<br>
      ISBN 978-2-87979-070-1<br>
      prix : 33 €<br>
      emballage pour les librairies : cartons à 18 exemplaires
    </article>
    <article>
      <img src=editiounen/cp/CP8.png width=230 height=155
           alt='Cahier pédagogique 8 - Mathématiques Ve'>
      <h1>Cahier pédagogique 8 - Mathématiques V<sup>e</sup></h1>
      ISBN 2-87979-017-4<br>
      prix : 15 €
    </article>
    <article>
      <img src=editiounen/cp/CP12.jpg width=88 height=123
           alt='Cahier pédagogique 12 - Philosophie'>
      <h1>Cahier pédagogique 12 - Philosophie</h1>
      ISBN 978-2-87979-072-5<br>
      prix : 19 €<br>
      emballage pour les librairies : cartons à 32 exemplaires
    </article>
    <article>
      <img src=editiounen/cp/CP248.png width=230 height=155
           alt='Cahier pédagogique 248 - Mathématiques VIIe–Ve'>
      <h1>Cahier pédagogique 248 - Mathématiques VII<sup>e</sup>–V<sup>e</sup></h1>
      Exercices (128 p.) et corrigés (216 p.)<br>
      ISBN 978-2-87979-248-4<br>
      prix : 33 €<br>
      emballage pour les librairies : cartons à 19 exemplaires
    </article>
    <article>
      <img src=editiounen/cp/CP356.png width=230 height=155
           alt='Cahier pédagogique 356 - Grammaire française VIIe–IVe'>
      <h1>Cahier pédagogique 356 - Grammaire française VII<sup>e</sup>–IV<sup>e</sup></h1>
      Exercices (320 p.) et corrigés (104 p.)<br>
      ISBN 2-87979-018-2<br>
      prix : 33 €<br>
      emballage pour les librairies : cartons à 16 exemplaires
    </article>
  </div>
</section>
<section>
  <header>
    <h1>Ausbléck - Récré</h1>
    <h2>Annuaire culturel des professeurs luxembourgeois</h2>
  </header>
  <div>
    <article>
      <img src=editiounen/recre/Aus30.png width=211 height=299 alt='Ausbléck-Récré 30'>
      <h1>Ausbléck-Récré 30</h1>
      209 p., 2019<br>
      ISBN 978-2-87979-130-2<br>
      <a href=editiounen/recre/ExtraitAus30.pdf target=_blank>Table des matières et préface</a>
    </article>
    <article>
      <img src=editiounen/recre/Aus29.jpg width=211 height=300 alt='Ausbléck-Récré 29'>
      <h1>Ausbléck-Récré 29</h1>
      260 p., 2016<br>
      ISBN 978-2-87979-129-6<br>
      <a href=editiounen/recre/ExtraitAus29.pdf target=_blank>Table des matières et préface</a>
    </article>
    <article>
      <img src=editiounen/recre/Aus28.jpg width=212 height=300 alt='Ausbléck-Récré 28'>
      <h1>Ausbléck-Récré 28</h1>
      326 p., 2014<br>
      ISBN 978-2-87979-128-9<br>
      <a href=editiounen/recre/ExtraitAus28.pdf target=_blank>Table des matières et préface</a>
    </article>
    <article>
      <img src=editiounen/recre/Aus27.jpg width=214 height=300 alt='Ausbléck-Récré 27'>
      <h1>Ausbléck-Récré 27</h1>
      344 p., 2013<br>
      ISBN 978-2-87979-127-2<br>
      <a href=editiounen/recre/ExtraitAus27.pdf target=_blank>Table des matières et préface</a>
    </article>
    <article>
      <img src=editiounen/recre/Aus26.jpg width=211 height=300 alt='Ausbléck-Récré 26'>
      <h1>Ausbléck-Récré 26</h1>
      274 p., 2011<br>
      ISBN 978-2-87979-126-5<br>
      <a href=editiounen/recre/Aus26intro.pdf target=_blank>Table des matières et
        avant-propos</a>
    </article>
    <article>
      <img src=editiounen/recre/recres.png width=230 height=155 alt='Récré 1 – 25'>
      <h1>Récré 1 – 25</h1>
      1986–2010<br>
      Tous les numéros sont encore disponibles.
    </article>
  </div>
</section>
<section>
  <header>
    <h1>Collection</h1>
  </header>
  <div>
    <article>
      <img src=editiounen/collection/LaubUndNadel.jpg width=230 height=155
           alt=LaubUndNadel.jpg>
      <h1>Laub und Nadel</h1>
      Roland Harsch, 2012 (nouvelle édition), Collection 11<br>
      ISBN 978-2-87979-211-8
    </article>
    <article>
      <img src=editiounen/collection/DuTourDeFrantzAuTourDeGaul.jpg width=230 height=155
           alt=DuTourDeFrantzAuTourDeGaul.jpg>
      <h1>Du Tour de Frantz au Tour de Gaul</h1>
      François Guillaume, 2006 (nouvelle édition), Collection 14 (ancienne 13)<br>
      ISBN 2-87979-202-9
    </article>
    <article>
      <img src=editiounen/collection/PARODIESUndDAS.jpg width=230 height=155
           alt=PARODIESUndDAS.jpg>
      <h1>PARODIES und DAS</h1>
      Roland Harsch, 2004, Collection 14<br>
      ISBN 2-87979-214-2
    </article>
    <article>
      <img src=editiounen/collection/LettresÀSophie.jpg width=230 height=155
           alt=LettresÀSophie.jpg>
      <h1>Lettres à Sophie</h1>
      Claude Conter, 2002, Collection 12<br>
      ISBN 2-87979-212-6
    </article>
    <article>
      <img src=editiounen/collection/Chroniques1961-1997.jpg width=230 height=155
           alt=Chroniques1961-1997.jpg>
      <h1>Chroniques 1961–1997</h1>
      Nic Klecker, 1998, Collection 10<br>
      ISBN 2-87979-210-X
    </article>
    <article>
      <img src=editiounen/collection/SatirenUndGlossenII.jpg width=230 height=155
           alt=SatirenUndGlossenII.jpg>
      <h1>Satiren und Glossen II</h1>
      Henry Gelhausen, 1997, Collection 9<br>
      ISBN 2-87979-209-6
    </article>
    <article>
      <img src=editiounen/collection/Kalendarium.jpg width=230 height=155 alt=Kalendarium.jpg>
      <h1>Kalendarium</h1>
      Roland Harsch, 1997, Collection 8<br>
      ISBN 2-87979-208-8
    </article>
    <article>
      <img src=editiounen/collection/MusikalischeFederspiele.jpg width=230 height=155
           alt=MusikalischeFederspiele.jpg>
      <h1>Musikalische Federspiele</h1>
      Roland Harsch, Carlo Schmitz, 1996, Collection 7<br>
      ISBN 2-87979-207-X
    </article>
    <article>
      <img src=editiounen/collection/DerBürgerImStaatII.jpg width=230 height=155
           alt=DerBürgerImStaatII.jpg>
      <h1>Der Bürger im Staat II</h1>
      Marcel Engel, 1995, Collection 6<br>
      ISBN 2-87979-206-1
    </article>
    <article>
      <img src=editiounen/collection/SatirenUndGlossen.jpg width=230 height=155
           alt=SatirenUndGlossen.jpg>
      <h1>Satiren und Glossen</h1>
      Henry Gelhausen, 1994, Collection 5<br>
      ISBN 2-87979-205-0
    </article>
    <article>
      <img src=editiounen/collection/LesFleursOntFroid.jpg width=230 height=155
           alt=LesFleursOntFroid.jpg>
      <h1>Les fleurs ont froid</h1>
      Rosemarie Kieffer, 1993, Collection 4<br>
      ISBN 2-87979-204-5
    </article>
    <article>
      <img src=editiounen/collection/Chroniques1961-1997.jpg width=230 height=155
           alt=Chroniques1961-1997.jpg>
      <h1>Chroniques de J.-M- Durand</h1>
      Léon Thyes, 1991, Collection 3<br>
      ISBN 2-87979-203-7
    </article>
    <article>
      <img src=editiounen/collection/DieStreuwiese.jpg width=230 height=155
           alt=DieStreuwiese.jpg>
      <h1>Die Streuwiese</h1>
      Fernand Hoffmann, 1988, Collection 2<br>
      ISBN 2-87979-202-9
    </article>
    <article>
      <img src=editiounen/collection/DerBürgerImStaat.jpg width=230 height=155
           alt=DerBürgerImStaat.jpg>
      <h1>Der Bürger im Staat</h1>
      Marcel Engel, 1987, Collection 1<br>
      ISBN 2-87979-201-0
    </article>
  </div>
</section>
<footer>
  <h1>Kontakt</h1>
  <h2>Administrateur</h2>
  Pascal Zeihen
  Tel. 691 273 769
  <h2>Comité de rédaction</h2>
  Franck Colotte (President), Roland Harsch, Edouard Kayser, Paul Kintziger, Georges Milmeister
  <h1>Bestellungen</h1>
  <h2>Librairien (an alphabetescher Reiefolleg)</h2>
  <ul>
    <li><a href=http://www.ernster.com target=_blank>Ernster</a> Bartreng a Lëtzebuerg</li>
    <li><a href=http://www.libo.lu target=_blank>LIBO</a> Lëtzebuerg, Dikrech, Ettelbréck,
      Wolz a Gréiwemaacher
    </li>
    <li>Librairie A.B.C. À la Bouquinerie du Centre, 40 avenue de la Gare, L-4130 Esch/Alzette,
      Tel. 53 05 82
    </li>
    <li>Librairie Alinéa, 5 rue Beaumont, L-1219 Luxembourg, Tel. 22 67 87</li>
    <li>Librairie des Lycées, 30 av. Victor Hugo, L-1750 Luxembourg, Tel. 22 79 83</li>
    <li><a href=http://www.diderich.lu target=_blank>Librairie Diderich</a> Esch/Alzette</li>
    <li>Librairie um Fieldgen, 3 rue Glesener, L-1631 Luxembourg, Tel. 48 88 93</li>
    <li>Librairie Zimmer, 30-32 Grand-Rue, L-9240 Diekirch, Tel. 80 95 59</li>
    <li><a href=http://www.samkats.com target=_blank>Samkats</a> Iechternach</li>
  </ul>
  <h2>Direkt Bestellungen</h2>
  Eis Memberen profitéieren vu reduzéierte Präiser a bezuele keng Versandkäschten.
  <a href=mailto:editions@apess.lu?subject=Commande target=_blank>Kontaktéiert eis</a>.
</footer>
5.4.16.12. editiounen.css
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
main {
  /*overflow: hidden;*/
}

main > section > div {
  display:               grid;
  grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
  /*grid-template-rows:    1fr 5fr 5fr;*/
  /*align-items:           stretch;*/
  /*overflow:              auto;
  max-height:            100%; calc(100vh - 150px);*/
}

main > nav > ul {
  list-style: none;
  padding:    0;
  margin:     0;
  display:    flex;
  flex:       auto;
  flex-wrap:  wrap;
}

main > nav > ul > li {
  flex:       auto;
  text-align: center;
  padding:    2px;
}

main > nav > ul > li > button {
  width:            100%;
  border:           none;
  background-color: rgb(200, 200, 200);
  padding:          2px;
  font-size:        1.3em;
  box-shadow:       4px 4px 4px #888888;
  font-family:      inherit;
}

main > nav > ul > li > button:focus, header nav > ul > li > button:active {
  border: none;
}

main > nav > ul > li > button:hover {
  background-color: grey;
}

main > section > header {
  grid-column: span 5;
  grid-row: span 1;
  margin: 0;
}

main > section > div > article {
  background-color: rgb(240, 240, 240);
  padding:          5px;
  margin:           0 10px 20px;
  box-shadow:       10px 10px 10px black;
  display:          flex;
  flex-flow:        column;
  text-align:       center;
  float:            left;
}

main > section > div > article > img {
  margin: auto;
  /*max-height: 100%;
  max-width: 100%;*/
  width:  initial;
  height: initial;
}

main > section > div > article > h1 {
  text-align: center;
}

@media all and (max-width: 679px) {
  main > section > div {
    grid-template-columns: 1fr;
  }

  main > section > header {
    grid-column: span 1;
  }
}

@media all and (min-width: 680px) {
  main > section > div {
    grid-template-columns: 1fr 1fr;
  }

  main > section > header {
    grid-column: span 2;
  }
}

@media all and (min-width: 1006px) {
  main > section > div {
    grid-template-columns: 1fr 1fr 1fr;
  }

  main > section > header {
    grid-column: span 3;
  }
}

@media all and (min-width: 1338px) {
  main > section > div {
    grid-template-columns: 1fr 1fr 1fr 1fr;
  }

  main > section > header {
    grid-column: span 4;
  }
}

@media all and (min-width: 1662px) {
  main > section > div {
    grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
  }

  main > section > header {
    grid-column: span 5;
  }
}

@media all and (min-width: 571px) and (min-height: 401px) {
  main > nav {
    width: 100%;
  }

  #editiounenMenuButton {
    display: none;
  }
}

@media all and (max-width: 570px), (max-height: 400px) {
  #editiounenMenuButton {
    padding:          0 0.2em 0.2em 0.2em;
    background-color: rgb(240, 240, 240);
    text-decoration:  none;
    color:            #333;
    cursor:           pointer;
    font-size:        inherit;
    width:            10em;
    height:           3em;
  }

  #editiounenMenuButton.active {
    background-color: #333;
    color:            #fff;
  }

  main > nav {
    display: none;
  }

  main > nav.active {
    max-height: calc(100% - 48px);
    display:    block;
    overflow:   auto;
  }

  main > nav.active > ul {
    display:    block;
    max-height: calc(100% - 27.2px);
  }

  main > nav.active > ul > li {
    display: block;
  }
}
5.4.16.13. interna.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
document.getElementById('Delegéiert').addEventListener('click', () => {
  const table = document.querySelector('#Delegéiert table')
  if (!table) return
  if (table.style.display && table.style.display !== 'table') table.style.display = 'table'
  else table.style.display = 'none'
})

/*const OGFs = document.getElementsByClassName('OGF');
OGFs.forEach(OGF => {
  OGF.addEventListener
});*/
5.4.16.14. interna.html
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
<aside>
    Wann Dir eis wëllt e vertrauleche Mail schécken a wëllt sécher sinn, datt keen aneren e
    liese kann, da benotzt w.e.g. d'Mailadress  <a href="&#x6d;&#x61;&#x69;&#x6c;&#x74;&#111;&#x3a;&#x61;&#112;&#x65;&#x73;&#x73;&#64;&#x70;&#x72;&#x6f;&#116;&#111;&#110;&#x6d;&#x61;&#x69;&#x6c;&#x2e;&#99;&#x6f;&#109;">&#x61;&#x70;&#x65;&#x73;&#115;&#64;&#x70;&#x72;&#x6f;&#x74;&#x6f;&#110;&#109;&#x61;&#x69;&#108;&#46;&#99;&#x6f;&#109;</a>.
    Dir dierft Äre Mail
    dann awer net vun enger education.lu Adress schécken mä vun enger, déi net ënner
    d'Kontroll vum MENJE fält. Am séchersten geet dat, wann Dir Iech och e Gratiskont op
  <a href=https://protonmail.com target=_blank>protonmail.com</a> erstellt, dann ass
  d'komplett Kommunikatioun verschlësselt an och
    protonmail selwer kënnen Är Mailen net liesen.
</aside>
<a href='members/pdf/2019-10-24 IM organisation des épreuves orales en classe de première.pdf'
   target=_blank>
  <article><h1>Instruction ministérielle ES 2019-4 du 9 octobre 2019
    concernant l'organisation des épreuves orales en classe de première</h1></article>
</a>
<a href='members/pdf/2019-07-11 IM organisation scolaire VF signée.pdf'
   target=_blank>
  <article><h1>Instruction ministérielle du 11 juillet 2019
    concernant l'organisation scolaire des lycées</h1></article>
</a>
<section>
  <article class=noflex><h1>Legalitéit vun der Publikatioun vun de Memberen vun der Commission
    d’experts
    chargés d’examiner les sujets ou questions proposés aux différentes épreuves de l’examen de
    fin d’études secondaires de l’enseignement secondaire classique, sessions de l’année 2019
    3.6.19</h1>
    No enger Ufro vun engem Member hu mir der Commission nationale pour la protection des
    données (CNPD) folgend Fro gestallt:
    "Pouvez-vous nous confirmer, que la publication des noms des membres de la commission
    d’experts chargés d’examiner les sujets ou questions proposés aux différentes épreuves de
    l’examen de fin d’études secondaires de l’enseignement secondaire classique, sessions de
    l’année 2019 par arrêté ministériel
    (<a href=http://legilux.public.lu/eli/etat/adm/amin/2019/03/30/b1231/jo
        target=_blank>http://legilux.public.lu/eli/etat/adm/amin/2019/03/30/b1231/jo</a>)
    n'enfreint pas la loi relative à la protection des données ?"
    <br><br>
    Mir kruten déi heiten Äntwert:<br>
    "Après analyse, nous avons pu constater que les articles 3
    du règlement grand-ducal modifié du 31 juillet 2006 portant organisation de l’examen de fin
    d’études secondaires classiques et le règlement grand-ducal modifié du 31 juillet 2006
    portant organisation de l’examen de fin d’études secondaires générales prévoient que
    « l'examen a lieu devant des commissions nommées chaque année par le ministre ».
    <br><br>
    Dans ce contexte, il nous semble que la publication des noms et prénoms des membres de ces
    commissions peut être considérée comme nécessaire dans le cadre de la mission d’intérêt
    public poursuivie par le Ministre de l’Éducation nationale, de l’Enfance et de la Jeunesse,
    conformément à ces dispositions. Une telle publication répond par ailleurs à une finalité
    de transparence, alors que tout arrêté ministériel doit être publié au Journal officiel du
    Grand-Duché de Luxembourg.
    <br><br>
    Par conséquent, la publication des noms et prénoms des membres de la commission d’experts
    chargés d’examiner les sujets ou questions proposés aux différentes épreuves de l’examen de
    fin d’études secondaires de l’enseignement secondaire classique, sessions de l’année 2019,
    nous paraît répondre à la condition de licéité de l’article 6, paragraphe (1), lettre (e),
    du règlement général sur la protection des données (UE) 2016/679 (ou « RGPD »). Nous ne
    voyons par ailleurs a priori pas en quoi la publication des noms et prénoms des membres de
    ces commissions enfreindrait d’autres dispositions du RGPD."
  </article>
</section>
<section>
  <article><h1>AGO vum 14.3.19</h1>
    <h2><a href="members/pdf/APESS - Rapport de l'AGO du 14 mars 2019.pdf" target=_blank>
      Rapport
    </a></h2>
    <h2><a href='members/pdf/Assemblée générale ordinaire 2019.pdf' target=_blank>
      Presentatioun
    </a></h2>
    <h2><a href='members/pdf/APESS Rapport financier 2018.pdf' target=_blank>
      Finanzrapport 2018
    </a></h2>
  </article>
</section>
<section>
  <article><h1>Kommunikatioun zum Bewäertungssystem 22.11.18</h1>
    <h2><a href='members/pdf/Avis juridique Bewäertungssystem 23.11.18.pdf' target=_blank>
      Avis juridique
    </a></h2>
  </article>
</section>
<a href='members/pdf/2018-07-11 IM organisation scolaire VF_signée.pdf'
   target=_blank>
  <article><h1>Instruction ministérielle du 11 juillet 2018
    concernant l'organisation scolaire des lycées</h1></article>
</a>
<a>
  <article>
    <h1>Delegéiert</h1>
    <table>
      <tr>
        <th>Numm</th>
        <th>Schoul</th>
      </tr>
      <tr>
        <td>Marco BREYER</td>
        <td>AL</td>
      </tr>
      <tr>
        <td>Gilles EVERLING</td>
        <td>LAM</td>
      </tr>
      <tr>
        <td>Myriam WAGNER ép. ENGEL</td>
        <td>LAML</td>
      </tr>
      <tr>
        <td>Pascal ZEIHEN</td>
        <td>LCD</td>
      </tr>
      <tr>
        <td>Jacques DELLERÉ</td>
        <td>LCE</td>
      </tr>
      <tr>
        <td>Jean-Louis WEIS</td>
        <td>LGL</td>
      </tr>
      <tr>
        <td>Jean-Marc CIMA</td>
        <td>LJBM</td>
      </tr>
      <tr>
        <td>Emmanuel BOCK</td>
        <td>LN</td>
      </tr>
      <tr>
        <td>Fabien HENGEN</td>
        <td>LTB</td>
      </tr>
      <tr>
        <td>Lelio LOEWEN</td>
        <td>LTC</td>
      </tr>
      <tr>
        <td>Paulo DE SOUSA</td>
        <td>LTMA</td>
      </tr>
      <tr>
        <td>Guy RHEIN</td>
        <td>MLG</td>
      </tr>
    </table>
  </article>
</a>
<a href='members/pdf/Traitements_Fonctionnaires_Enseignement_version_2.1.20180703.pdf'
   target=_blank>
  <article><h1>Carrières et rémunérations applicables aux fonctionnaires (-stagiaires)
    engagés depuis la réforme du 1er octobre 2015</h1></article>
</a>
<a href='members/pdf/FICHE_OPERA_PLUS_FR-APESS.pdf' target=_blank>
  <article><h1>Raiffeisen Opera Plus Offer fir Memberen</h1></article>
</a>
<section>
  <article><h1>AGO an AG vum 17.5.18</h1>
    <h2><a href='members/pdf/Rapport_AG_17.5.18.pdf' target=_blank>
      Rapport
    </a></h2>
    <h2><a href='members/pdf/Assemblée_générale_ordinaire_2018.pdf' target=_blank>
      Presentatioun
    </a></h2>
    <h2><a href='members/pdf/APESS_Rapport_financier_2017.pdf' target=_blank>
      Finanzrapport 2017
    </a></h2>
    <h2><a href='members/pdf/APESS_Rapport_financier_2016.pdf' target=_blank>
      Finanzrapport 2016
    </a></h2>
    <h2>
      <a href='members/pdf/Homologatioun.pdf' target=_blank>
        Homologatioun vun der Statutenännerung</a>
    </h2>
  </article>
</section>
5.4.16.15. interna.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/*table {
  border-collapse: collapse;
  box-shadow:      2px 2px 2px #888888;
}

table, tr, th, td {
  border: 4px groove black;
}

th, td {
  padding: 5px;
}
*/

main > aside {
  background: linear-gradient(to bottom right, #550000, red);
  padding:    5px;
  margin:     0 10px 20px;
  box-shadow: 10px 10px 10px black;
  color: gold;
}

main > aside > a {
  color: lightblue;
}

main article > h1, main article h2 {
  text-align: center;
}

main article h2 {
  margin: 5px;
}

main a {
  text-decoration: none;
}

main article {
  background: linear-gradient(to bottom right, lightgray, gray);
  padding:    5px;
  margin:     0 10px 20px;
  box-shadow: 10px 10px 10px black;
  display:    flex;
  flex-flow:  column;
}

main article.noflex {
  display: block;
}

main article:hover {
  background: linear-gradient(to bottom right, darkslategray, lightgray);
}

main article > img {
  display:   block;
  max-width: 100%;
  height:    auto;
  /*max-width: initial;
  max-height: initial;*/
}

main article {
  display:     flex;
  align-items: center;
}

main article table, main article table tr, main article table th, main article table td {
  padding:     5px;
  color:       lightyellow;
  text-shadow: 2px 2px 2px black;
}

/*@media all and (min-width: 571px) and (min-height: 401px) {
  article {
    transition: 5s;
  }
}*/
5.4.16.16. legislatioun.html
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
<!--https://stackoverflow.com/questions/291813/recommended-way-to-embed-pdf-in-html?rq=1-->
<!--<h1>Statuten, Gesetzer, Reglementer an Instruktiounen</h1>-->
<section>
  <a href='pdf/Statuts approuvés le 17.5.18.pdf' target=_blank>
    <article>
      <div>APESS-Statuten</div>
      <img
        src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
        height=32></article>
  </a>
  <a href=http://www.mj.public.lu/legislation/asbl_fondations/2009_Loi_21_avril_1928.pdf
     target=_blank class=law>
    <article>
      <div>A.s.b.l.-Gesetz vum 21.4.1928</div>
      <img src=biller/if_law_298810.svg
           alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/loi/2002/08/02/n2/jo
     target=_blank class=law>
    <article>
      <div>Dateschutzgesetz vum 2.8.2</div>
      <img src=biller/if_law_298810.svg
           alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/code/education_nationale target=_blank>
    <article>
      <div>Code de l'éducation nationale
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/code/fonction_publique target=_blank>
    <article>
      <div>Code de la fonction publique
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/code/travail target=_blank>
    <article>
      <div>Code du travail</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a href=https://budget.public.lu/lb/budget2015/zukunftspak.html target=_blank>
    <article>
      <div>Zukunftspak</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a href=pdf/Recueil-de-legislation-stagiaires-2017.pdf target=_blank>
    <article>
      <div>Recueil de législation stagiaires 2017</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg
           width=32
           height=32></article>
  </a>
  <a href=http://www.men.public.lu/fr/legislation/education-nationale/enseignement-secondaire/index.html
     target=_blank>
    <article>
      <div>Legislatiounskollektioun fir de Secondaire vum MENJE</div>
      <img
        src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
        height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/loi/1980/06/10/n1/jo
     target=_blank class=law>
    <article>
      <div>Gesetz vum 10.6.7 iwwert d'Tâche vum Enseignant</div>
      <img
        src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
        height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/rgd/2007/07/24/n1/jo
     target=_blank class=RGD>
    <article>
      <div>RGD vum 24.7.7 iwwert d'Tâche vum Enseignant</div>
      <img
        src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
        height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/loi/2017/08/29/a789/jo
     target=_blank class=law>
    <article>
      <div>Gesetz vum 29.8.17 iwwert de Secondaire</div>
      <img
        src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
        height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/loi/2008/12/19/n19/jo
     target=_blank class=law>
    <article>
      <div>Gesetz vum 19.12.08 iwwert d'Reform vun der Beruffsausbildung</div>
      <img
        src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
        height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/rgd/2017/08/21/a803/jo
     target=_blank class=RGD>
    <article>
      <div>RGD vum 21.8.17 iwwert d'Evaluatioun an d'Promotioun am Secondaire</div>
      <img
        src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
        height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/rgd/2018/07/20/a735/jo
     target=_blank class=RGD>
    <article>
      <div>RGD vum 20.7.18 iwwert d'Evaluatioun an d'Promotioun am Secondaire</div>
      <img
        src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
        height=32></article>
  </a>
  <a href='pdf/IM 2018_03_23.pdf' target=_blank class=IM>
    <article>
      <div>Instruction ministérielle du 23 mars 2018 portant sur les mesures
        transitoires pour la décision de promotion à la fin de l'année scolaire
        2017/2018 dans le cadre de la réforme des classes inférieures et supérieures de
        l'enseignement secondaire général
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/rgd/2016/08/31/n1/jo
     target=_blank class=RGD>
    <article>
      <div>RGD vum 31.8.16 iwwert d'Evaluatioun an d'Promotioun an der Beruffsausbildung</div>
      <img src=biller/if_law_298810.svg
           alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a href=http://legilux.public.lu/eli/etat/leg/rgd/2016/09/06/n1/jo
     target=_blank class=RGD>
    <article>
      <div>RGD vum 6.9.16 betreffend d'Chargés d'éducation, Examen an d'Tâche vun den
        Enseignanten
      </div>
      <img src=biller/if_law_298810.svg
           alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a
    href='pdf/2018-06-22 IM Formation continue.pdf'
    target=_blank class=IM>
    <article>
      <div>Instruction ministérielle du 22 juin 2018 concernant la formation continue
        des enseignants fonctionnaires et employés de l'enseignement secondaire classique et
        de l'enseignement secondaire général
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a
    href='pdf/10 07 14 Instruction organisation scolaire.pdf' target=_blank class=IM>
    <article>
      <div>Instruction ministérielle du 17 juillet 2014 concernant l'organisation scolaire
        des lycées et lycées techniques
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a
    href=http://www.men.public.lu/fr/secondaire/personnel-ecoles/recrutement-enseignant-fonctionnaire
    target=_blank>
    <article>
      <div>Recrutement des enseignants-fonctionnaires</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a
    href=https://ssl.education.lu/ifen/stage-f-es target=_blank>
    <article>
      <div>Stage des fonctionnaires de l'enseignement secondaire, de la formation d'adultes et
        du Centre de logopédie
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a
    href=biller/PRP.png target=_blank>
    <article>
      <div>Postes à responsabilités particulières</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a
    href=http://legilux.public.lu/eli/etat/leg/loi/2016/07/23/n22/jo target=_blank class=law>
    <article>
      <div>Gesetz vum 23.7.16 iwwert d'Chargés d'éducation (cf.
        <object><a
          href=http://www.men.public.lu/fr/actualites/articles/communiques-conference-presse/2016/06/30-charges/index.html
          target=_blank>Artikel MENJE</a>)
        </object>
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a
    href="pdf/Applicabilité loi 15.12.2017 aux agents de l'Etat - Version finale.pdf"
    target=_blank>
    <article>
      <div>Applicabilité de la loi du 15 décembre 2017 aux agents de l’Etat
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
           height=32></article>
  </a>
  <a
    href=http://www.men.public.lu/fr/actualites/articles/communiques-conference-presse/2016/03/17-CAR
    target=_blank>
    <article>
      <div>Aménagements raisonnables</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32>
    </article>
  </a>
  <a
    href=http://www.men.public.lu/fr/actualites/articles/communiques-conference-presse/2015/12/10-medicaments-ecole
    target=_blank>
    <article>
      <div>Projet d'accueil individualisé (PAI)
        <object>(cf. <a
          href=http://www.guichet.public.lu/citoyens/fr/famille/parents/assistance-enfance/projet-accueil-individualise/pai-lettre-medecine-scolaire.pdf
          target=_blank>Aktualiséierung vun der Instruction de service PAI vum 15.10.15</a>)
        </object>
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32>
    </article>
  </a>
  <a
    href=http://legilux.public.lu/eli/etat/leg/rgd/2018/05/09/a425/jo
    target=_blank class=RGD>
    <article>
      <div>RGD vum 9.5.18 iwwert d'Disziplin an de Lycéeën</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32></article>
  </a>
  <a
    href=http://legilux.public.lu/eli/etat/leg/loi/2018/05/09/a373/jo target=_blank class=law>
    <article>
      <div>Gesetz vum 9.5.18 dat de Beamtestatut verännert</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32></article>
  </a>
  <a
    href=https://fonction-publique.public.lu/fr/plus/actualites/articles-actualites/2018/09/miseplacecetnouvelleorganisationtempstravail.html
    target=_blank class=law>
    <article>
      <div>Mise en place d'un compte épargne-temps et nouvelle organisation du temps de
        travail
        <object>(<a href=http://chd.lu/wps/portal/public/Accueil/TravailALaChambre/Recherche/RoleDesAffaires?action=doDocpaDetails&backto=/wps/portal/public/Accueil/Actualite&id=7171
                    target=_blank>Projet de loi</a>)</object>
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32></article>
  </a>
  <a
    href=http://legilux.public.lu/eli/etat/leg/rgd/2018/07/20/a677/jo
    target=_blank class=RGD>
    <article>
      <div>RGD vum 20.7.18 iwwert de Fonctionnement an d'Missiounen vum Collège des
        directeurs
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32></article>
  </a>
  <a
    href=http://legilux.public.lu/eli/etat/leg/rgd/2018/08/31/a815/jo
    target=_blank class=RGD>
    <article>
      <div>RGD vum 31.8.18 iwwert d'Grilles horaires am Secondaire classique</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32></article>
  </a>
  <a
    href=http://legilux.public.lu/eli/etat/leg/rgd/2018/08/31/a816/jo
    target=_blank class=RGD>
    <article>
      <div>RGD vum 31.8.18 iwwert d'Grilles horaires am Secondaire général</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32></article>
  </a>
  <a
    href=http://legilux.public.lu/eli/etat/leg/rgd/2018/08/31/a817/jo
    target=_blank class=RGD>
    <article>
      <div>RGD vum 31.8.18 iwwert d'Grilles horaires an der Beruffsausbildung</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32></article>
  </a>
  <a
    href=http://data.legilux.public.lu/eli/etat/leg/memorial/2013/108/fr/pdf
    target=_blank class=RGD>
    <article>
      <div>RGD vum 21.6.13 iwwert d'Reduktioun vun den Indemnitéiten vun de Kommissiounen
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32></article>
  </a>
  <a
    href=http://legilux.public.lu/eli/etat/leg/rgd/2018/10/19/a985/jo
    target=_blank class=RGD>
    <article>
      <div>RGD vum 19.10.18 iwwert d'Indemnitéiten vun de Kommissiounen
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32></article>
  </a>
  <a
    href=http://legilux.public.lu/eli/etat/leg/loi/2019/08/01/a563/jo
    target=_blank class=law>
    <article>
      <div>Gesetz vum 1.8.19 iwwert de Stage an d'Formation continue
        <object>(cf. <a
          href=https://chd.lu/wps/portal/public/Accueil/TravailALaChambre/Recherche/RoleDesAffaires?action=doDocpaDetails&id=7440
          target=_blank>kompletten Dossier vum Projet de loi 7440</a>)
        </object>
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32>
    </article>
  </a>
  <a
    href=http://legilux.public.lu/eli/etat/leg/rgd/2019/08/15/a571/jo
    target=_blank class=RGD>
    <article>
      <div>RGD vum 15.8.19 iwwert d'Evaluatioun an d'Promotioun an der
        Beruffsausbildung</div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32></article>
  </a>
  <a href='http://www.ja.etat.lu/35001-40000/37203.pdf' target=_blank class=judgement>
    <article>
      <div>Geriichtsuerteel vum 12.10.16 dat den RGD vum 25.8.15 iwwer d'Ofschlossexamen,
        d'Tâche vum Enseignant an d'Indemnitéiten annuléiert</div>
      <img
        src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
        height=32></article>
  </a>
  <a href='http://www.ja.etat.lu/35001-40000/37490.pdf' target=_blank class=judgement>
    <article>
      <div>Geriichtsuerteel vum 24.1.17 dat den RGD vum 19.10.15 iwwer d'Tâche vum
        Enseignant annuléiert</div>
      <img
        src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
        height=32></article>
  </a>
  <a
    href=https://justice.public.lu/fr/actualites/2019/02/arret-cour-administrative-tache-des-professeurs-zukunftspak.html
    target=_blank class=judgement>
    <article>
      <div>Pressecommuniqué vum Geriicht zum Uerteel vum 12.2.19 zur Formation continue an
        dem Coefficient correcteur
        <object>(cf. <a
          href=http://www.ja.etat.lu/40001-45000/40638CA.pdf
          target=_blank>Geriichtsuerteel vum 12.2.19</a>, <a
          href=http://www.ja.etat.lu/40001-45000/40638C.pdf
          target=_blank>Geriichtsuerteel vum 19.6.18</a>, <a
          href=http://www.ja.etat.lu/35001-40000/38823.pdf
          target=_blank>Geriichtsuerteel vum 8.12.17</a> an <a
          href=http://legilux.public.lu//eli/etat/leg/acc/2018/12/07/a1127/jo
          target=_blank>Unuerdnung vum Verfassungsgeriicht vum 7.12.18</a>)
        </object>
      </div>
      <img src=biller/if_law_298810.svg alt=if_law_298810.svg width=32 height=32>
    </article>
  </a>
  <a href='http://www.ja.etat.lu/35001-40000/39953.pdf' target=_blank class=judgement>
    <article>
      <div>Geriichtsuerteel vum 5.4.19 iwwert de Refus vum MENJE fir e Recalcul vum
        Traitement an den Indemnitéiten vir ze huelen op Grond vun der Annulatioun vum
        RGD vum 25.8.15
      </div>
      <img
        src=biller/if_law_298810.svg alt=if_law_298810.svg width=32
        height=32></article>
  </a>
</section>
<!--<h1>Geriichtsuerteeler</h1>-->
<section>
  <article>
    <p>Benotzte Faarwen: <span style='color: white; background-color: red'>Gesetzer</span>,
      <span style='color: white; background-color: orange'>Reglementer</span>,
      <span style='color: white; background-color: green'>ministeriell Instruktiounen</span>,
      <span style='color: white; background-color: blue'>Geriichtsuerteeler</span>,
      <span style='color: white; background-color: black'>Aneres</span>.</p>
    Déi éischt Plaz fir e spezifeschen Gesetzestext oder Reglement ze fannen ass de
    <a href=http://legilux.public.lu/search/A target=_blank>Legilux</a>. Hei e puer Hiweiser
    wéi ee schnell eppes fanne kann wann een ongeféier weess wat ee sicht:
    <ul>
      <li>Wann een den Titel vun engem Gesetz oder RGD kennt kann een deen an der
        Sichkëscht aginn.
      </li>
      <li>Wann een no engem RGD sicht an och weess a wéi engem Joer an eventuell Mount en
        ënnerschriwwe ginn ass, da kann een deen heite Link benotzen:
        <a href=http://legilux.public.lu/eli/etat/leg/rgd/2007/07 target=_blank>
          http://legilux.public.lu/eli/etat/leg/rgd/2007/07</a> woubäi
        <pre>2007</pre>
        duerch d'Joer an
        <pre>07</pre>
        duerch de Mount z'ersetze sinn. Wann ee just
        d'Joer kennt da léisst een den
        <pre>/07</pre>
        Deel ewech. Wann een souguer den Dag kennt da setzt een deen
        dobäi, z.B. <a href=http://legilux.public.lu/eli/etat/leg/rgd/2007/07/24
                       target=_blank>
          http://legilux.public.lu/eli/etat/leg/rgd/2007/07/24</a>.
      </li>
      <li>Wann een no engem Gesetz sicht ass d'Approche genau déi selwecht, et ersetzt een
        just
        <pre>rgd</pre>
        duerch
        <pre>loi</pre>
        , z.B.
        <a href=http://legilux.public.lu/eli/etat/leg/loi/2017/08/29 target=_blank>
          http://legilux.public.lu/eli/etat/leg/loi/2017/08/29</a>.
      </li>
    </ul>
    Geriichtsuerteeler fënnt een um Site vun der <a
    href=http://www.justice.public.lu/fr/jurisprudence/juridictions-administratives/index.php
    target=_blank>Justiz</a>.
    Gesetzesprojeten, Motiounen etc. fënnt een um Site vun der
    <a href=http://chd.lu/wps/portal/public/Accueil/TravailALaChambre/Recherche/RoleDesAffaires
       target=_blank>Chamber</a>. Bei der Sich no engem Affekot kann de
    <a href=https://www.barreau.lu/advancedsearch target=_blank>Barreau</a>
    ganz hëllefräich sinn.
  </article>
</section>
5.4.16.17. legislatioun.css
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
main {
  overflow: auto;
}

/* https://developer.mozilla.org/en-US/docs/Web/CSS/@supports */
@supports not (-ms-high-contrast: none) {
  /* not IE10+ CSS */
  main > section > a {
    text-decoration: none;
    background:      linear-gradient(to bottom right, lightgray, gray);
    box-shadow:      10px 10px 10px black;
    padding:         5px;
    margin:          0 10px 20px;
  }

  main > section > a:hover {
    background: linear-gradient(to bottom right, darkslategray, lightgray);
  }

  main > section > a.law {
    box-shadow:      10px 10px 10px red;
  }

  main > section > a.RGD {
    box-shadow:      10px 10px 10px orange;
  }

  main > section > a.IM {
    box-shadow:      10px 10px 10px green;
  }

  main > section > a.judgement {
    box-shadow:      10px 10px 10px blue;
  }

  main > section > a > article {
    display:    flex;
    min-height: 50px;
  }
}

/* https://stackoverflow.com/questions/43528940/how-to-detect-ie-and-edge-browsers-in-css */
@media screen and (-ms-high-contrast: none), (-ms-high-contrast: active) {
  /* IE10+ CSS */
  main > section > a {
    text-decoration: none;
  }

  main > section > a > article {
    background: linear-gradient(to bottom right, lightgray, gray);
    box-shadow: 10px 10px 10px black;
    padding:    5px;
    margin:     0 10px 20px;
    display:    flex;
    min-height: 50px;
  }

  main > section > a > article:hover {
    background: linear-gradient(to bottom right, darkslategray, lightgray);
  }
}

main > section > a > article > div {
  flex: auto;
}

main > section > a > article > img {
  min-width:       32px;
  max-width:       32px;
  justify-content: flex-end;
  margin-left:     10px;
}

main > section:first-of-type {
  display:               grid;
  grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
  grid-template-rows:    1fr 1fr;
  align-items:           stretch;
}

main > section pre {
  display: inline;
}

/*main > section:nth-child(2) > article a {
  text-decoration: none;
}*/

main > section:nth-of-type(2) > article {
  background-color: rgb(240, 240, 240);
  padding:          5px;
  margin:           0 10px 20px;
  box-shadow:       10px 10px 10px black;
}

@media all and (max-width: 679px) {
  main > section:first-child {
    grid-template-columns: 1fr;
  }
}

@media all and (min-width: 680px) {
  main > section:first-child {
    grid-template-columns: 1fr 1fr;
  }
}

@media all and (min-width: 1006px) {
  main > section:first-child {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

@media all and (min-width: 1338px) {
  main > section:first-child {
    grid-template-columns: 1fr 1fr 1fr 1fr;
  }
}

@media all and (min-width: 1662px) {
  main > section:first-child {
    grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
  }
}
5.4.16.18. login.html
1
2
3
4
<input type=email name=email placeholder=Emailadress required autofocus>
<input pattern=.{6,50} type=password name=password placeholder=Passwuert required>
<button name=login>Aloggen</button>
<button name=cancel>Annuléieren</button>
5.4.16.19. login.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
body > input {
  text-align: center;
}

body > input, body > button {
  font-size: 2em;
}

input:not([type=file]):valid {
  background-color: lawngreen;
}

input:not([type=file]):invalid {
  background-color: red;
}
5.4.16.20. member.html
1
2
3
4
5
6
7
<p>Dat éischt Schouljoer ass d'Memberschaft fir nei Memberen gratis. Duerno kascht et 60
  €/Joer.
  Doranner abegraff
  sinn och d'Servicer wéi Rechtsschutz an Haftflicht. Wann Dir wëllt Member
  ginn da schéckt w.e.g. en Email un
  <a
    href="&#109;&#x61;&#x69;&#108;&#116;&#111;&#58;&#x61;&#x70;&#101;&#115;&#115;&#x40;&#101;&#x64;&#117;&#99;&#x61;&#x74;&#x69;&#x6f;&#110;&#x2e;&#x6c;&#117;?subject=APESS%20Member%20ginn">&#97;&#112;&#x65;&#x73;&#115;&#x40;&#x65;&#x64;&#117;&#x63;&#x61;&#x74;&#x69;&#x6f;&#110;&#x2e;&#108;&#117;</a>.</p>
5.4.16.21. member.css
1
2
p {
}
5.4.16.22. presentatioun.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<h1>Association des Professeurs de I'Enseignement Secondaire et Supérieur du
  Grand-Duché de Luxembourg, association sans but lucratif</h1>
<h2>Adress</h2>
389, Route d'Arlon<br>
L-8011 Strassen

<h2>Exekutivcomité</h2>
<!--
<img srcset="fotoen/Exekutivcomite533x400.jpg 533w,
             fotoen/Exekutivcomite1000x750.jpg 1000w,
             fotoen/Exekutivcomite1440x1080.jpg 1440w,
             fotoen/Exekutivcomite2000x1500.jpg 2000w,
             fotoen/Exekutivcomite3000x2250.jpg 3000w,
             fotoen/Exekutivcomite3902x2927.jpg 3902w"
     sizes="(max-width: 1000px) 533px,
            (max-width: 1500px) 1000px,
            (max-width: 2000px) 1440px,
            (max-width: 2500px) 2000px,
            (max-width: 3500px) 3000px,
            3902px"
     src=fotoen/Exekutivcomite3902x2927.jpg alt=fotoen/Exekutivcomité3902x2927.jpg>
<br>Vu lénks no riets:-->
<table>
  <tr>
    <th>President</th>
    <td>Gilles Everling</td>
    <td>Professeur-ingénieur</td>
    <td>Lycée des Arts et Métiers</td>
    <td>Informatik</td>
  </tr>
  <tr>
    <th>Vizepresident</th>
    <td>Patrick Beil</td>
    <td>Professeur-ingénieur</td>
    <td>Lycée technique du Centre</td>
    <td>Mechanik</td>
  </tr>
  <tr>
    <th>Sekretär</th>
    <td>Jean-Marc Cima</td>
    <td>Professeur-ingénieur</td>
    <td>Lycée Josy Barthel Mamer</td>
    <td>Génie civil</td>
  </tr>
  <tr>
    <th>Tresorier</th>
    <td>Pascal Zeihen</td>
    <td>Professeur</td>
    <td>Lycée classique de Diekirch</td>
    <td>Mathematik</td>
  </tr>

  <!--  <tr>
      <th>President</th>
      <td>André Berns</td>
      <td>Professeur</td>
      <td>Lycée de Garçons de
        Luxembourg
      </td>
      <td>Mathematik</td>
    </tr>-->
</table>

<h2>Kontakt</h2>
<a
  href="&#109;&#x61;&#x69;&#108;&#116;&#111;&#58;&#x61;&#x70;&#101;&#115;&#115;&#x40;&#101;&#x64;&#117;&#99;&#x61;&#x74;&#x69;&#x6f;&#110;&#x2e;&#x6c;&#117;">&#97;&#112;&#x65;&#x73;&#115;&#x40;&#x65;&#x64;&#117;&#x63;&#x61;&#x74;&#x69;&#x6f;&#110;&#x2e;&#108;&#117;</a>

<h2>Member ginn</h2>
D'Memberschaft kascht 60 €/Schouljoer. Doranner abegraff sinn och déi ënnen beschriwwen
Servicer. Wann Dir wëllt Member ginn da schéckt w.e.g. en Email un
<a
  href="&#109;&#x61;&#x69;&#108;&#116;&#111;&#58;&#x61;&#x70;&#101;&#115;&#115;&#x40;&#101;&#x64;&#117;&#99;&#x61;&#x74;&#x69;&#x6f;&#110;&#x2e;&#x6c;&#117;?subject=APESS%20Member%20ginn">&#97;&#112;&#x65;&#x73;&#115;&#x40;&#x65;&#x64;&#117;&#x63;&#x61;&#x74;&#x69;&#x6f;&#110;&#x2e;&#108;&#117;</a>.

<h2>Kontosnummer</h2>
CCPLLULL LU53 1111 0220 7859 0000

<h1>Servicer</h1>
<h2>Rechtsschutz- an Haftflichtversécherung bei der Lalux</h2>
All APESS-Member huet eng Rechtsschutz- an Haftflichtversécherung bei der Lalux.
Déi verséchert Zommen sinn:
<ul>
  <li>Rechtsschutz: 4.800,00 €</li>
  <li>Haftpflicht: Dommages corporels et immatériels consécutifs: 11.143.195,52 €</li>
  <li>Haftpflicht: Dommages matériels et immatériels consécutifs: 718.200,00 €</li>
</ul>

<h2>Luxembourg Air Rescue</h2>
Als APESS-Member hutt Dir Urecht op eng ëm 15 repektiv 30 €/Joer reduzéiert Cotisatioun
bei der
<a href=https://www.lar.lu target=_blank>Luxembourg Air
  Rescue</a>. Fir eng Eenzelpersoun bedeit
dat 50 anstatt 65 €/Joer a fir eng Famill 85 anstatt 115 €/Joer.
Wann Dir wëllt vun dëser Offer profitéieren da wennt Iech w.e.g. un den Här Philippe JAAQUES
(Tel. 489 006 227, <a href=&#109;&#097;&#105;&#108;&#116;&#111;:&#112;&#104;&#105;&#108;&#105;&#112;&#112;&#101;&#046;&#106;&#097;&#097;&#113;&#117;&#101;&#115;&#064;&#108;&#097;&#114;&#046;&#108;&#117;>&#112;&#104;&#105;&#108;&#105;&#112;&#112;&#101;&#046;&#106;&#097;&#097;&#113;&#117;&#101;&#115;&#064;&#108;&#097;&#114;&#046;&#108;&#117;</a>
oder <a href=&#109;&#097;&#105;&#108;&#116;&#111;:&#105;&#110;&#102;&#111;&#064;&#108;&#097;&#114;&#046;&#108;&#117;>&#105;&#110;&#102;&#111;&#064;&#108;&#097;&#114;&#046;&#108;&#117;</a>).

<h2>Raiffeisen</h2>
Als APESS-Member kënnt Dir vu reduzéierten Tariffer bei der Raiffeisen profitéieren.
5.4.16.23. presentatioun.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
th, td {
  text-align:    left;
  padding-right: 10px;
}

img {
  /*display: block;*/
  max-width:  100%;
  /*width:      100%;*/
  height:     auto;
}
5.4.16.24. profil.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<section>
  <label>Virnumm:</label><input pattern=.{1,50} name=firstName required
                                title='1 bis 50 Zeechen' placeholder='1 bis 50 Zeechen'>
  <label>Familljennumm:</label><input pattern=.{1,50} name=lastName required
                                      title='1 bis 50 Zeechen' placeholder='1 bis 50 Zeechen'>
  <label>Email:</label><input type=email readonly name=email required>
  <label>Strooss:</label><input pattern=.{1,50} name=street required
                                title='Z.B. 5, rue du Village' placeholder='Z.B. 5, rue du Village'>
  <label>Stad:</label><input pattern=.{1,50} name=city required
                             title='1 bis 50 Zeechen' placeholder='1 bis 50 Zeechen'>
  <label>Land:</label><input pattern=[A-Z]- name=country title='Z.B. L-, D-, F- oder B-'
                             required placeholder='Z.B. L-, D-, F- oder B-'>
  <label>Postleetzuel:</label><input pattern=[0-9]{4,15} name=postalCode required
                                    title='4 bis 15 Zifferen' placeholder='4 bis 15 Zifferen'>
  <label>Schoul:</label><input placeholder='(RET -> pension., DIV -> divers/décharg.)'
                               pattern=.{1,50} name=school
                               title='(RET -> pension., DIV -> divers/décharg.)'>
  <label>Fach:</label><input pattern=.{1,50} name=subject
                             title='1 bis 50 Zeechen, z.B. FRA oder MAT'
                             placeholder='1 bis 50 Zeechen, z.B. FRA oder MAT'>
  <label>Funktioun:</label><input pattern=.{1,50} name=function
                                  title='1 bis 50 Zeechen, z.B. Professeur'
                             placeholder='1 bis 50 Zeechen, z.B. Professeur'>
  <label>Member säit:</label><input readonly name=adhesion title='Member säit'>
  <label>Lescht Cotisatioun:</label><input readonly name=lastPaymentDate>
  <label>BIC:</label><input pattern=[A-Z]{8} name=BIC title='8 grouss Buschtawen'
                            placeholder='8 grouss Buschtawen'>
  <label>IBAN:</label><input pattern='[A-Za-z]{2}[0-9 ]{18,22}' name=IBAN
                             title='2 Buschtawen 18 Zifferen an evtl. Espacen'
                             placeholder='2 Buschtawen 18 Zifferen an evtl. Espacen'>
  <label>Cotisatiounsmodus:</label>
  <div id=AP_V><label>Domiciliatioun</label><input type=radio name=AP_V value='Domiciliatioun'>
    <label>Virement</label><input type=radio name=AP_V value='Virement'></div>
  <!--<input
        placeholder='V -> Virement, APS -> Ordre permanent' pattern=.{1,3} name=AP_V
        title='1-3 characters'>-->
  <label>Emaile kréien:</label><input type=checkbox name=getEmail>
  <label id=pw>Falls Dir wëllt Äert Passwuert änneren da gitt dat neit Passwuert hei zweemol
    an, anerefalls loosst déi Felder eidel. Äert Passwuert gëtt verschlësselt
    gespäichert a ka vu kengem gesi ginn, och net vum Administrateur vum Site.</label>
  <input pattern=.{6,50} type=password name=pw1 placeholder='Neit Passwuert'
         title='6-50 Zeechen'>
  <input pattern=.{6,50} type=password name=pw2 placeholder='Neit Passwuert widderhuelen'
         title='6-50 Zeechen'>
  <button name=save>All Ännerunge späicheren</button>
<!--  <a href='members/pdf/Rapport Entrevue APESS-MENJE 16_05_18.pdf' target=_blank>
    Rapport Entrevue APESS-MENJE 16_05_18
  </a>-->
</section>
5.4.16.25. profil.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
main {
  overflow: auto;
}

main > section {
  max-width: 400px;
}

main > section > label {
  text-align:    right;
  display:       inline-block;
  width:         150px;
  padding-right: 10px;
}

main > section > input {
  width: 250px;
}

main > section > input:not([type=file]):valid {
  background-color: lawngreen;
}

main > section > input:not([type=file]):invalid {
  background-color: red;
}

main > section > input[type=checkbox], section > input[type=radio] {
  width: auto;
}

main > section > #AP_V {
  display: inline-block;
  width:   245px;
}

main > section > input[type=password] {
  width: 197px;
}

main > section > label#pw {
  text-align: justify;
  display:    block;
  width:      auto;
  padding:    0;
}

main > section > button[name=save] {
  width: 100%;
}
5.4.16.26. ressourcen.html
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<table>
  <tr>
    <td><a href=http://www.men.public.lu/fr/actualites/publications/index.html
           target=_blank>MENJE</a></td>
    <td><a
      href=http://www.men.public.lu/fr/actualites/publications/themes-transversaux/edi/index.html
      target=_blank>Infomagazin fir Elteren</a></td>
  </tr>
  <tr>
    <td colspan=2><a href=http://www.etat.public.lu target=_blank>Ëffentlech Siten</a></td>
  </tr>
  <tr>
    <td
      colspan=2><a href=http://www.fonction-publique.public.lu target=_blank>Portal vun
      der Fonction publique</a></td>
  </tr>
  <tr>
    <td colspan=2><a href=https://chfep.lu target=_blank>Chambre des fonctionnaires et employés
      publics</a></td>
  </tr>
  <tr>
    <td
      colspan=2><a href=http://www.gouvernement.lu target=_blank>Portal vun der
      Regierung</a></td>
  </tr>
  <tr>
    <td
      colspan=2><a href=http://www.cnel.lu target=_blank>CNEL - Conférence Nationale des
      Élèves du Luxembourg</a></td>
  </tr>
  <tr>
    <td
      colspan=2><a href=https://acel.lu target=_blank>ACEL - Association des Cercles
      d'Étudiants Luxembourgeois</a></td>
  </tr>
  <tr>
    <td
      colspan=2><a href=https://fapel.lu target=_blank>FAPEL – Fédération des
      Associations de Parents d'Élèves du Luxembourg</a></td>
  </tr>
  <tr>
    <td colspan=2><a href=http://leopold-loewenheim.uni.lu/apul target=_blank>APUL – The
      Association of Professors of the University of Luxembourg</a></td>
  </tr>
  <tr>
    <td colspan=2><a href=https://ei-ie.org target=_blank>Education International</a></td>
  </tr>
  <tr>
    <td colspan=2><a href=https://dysfocus.lu target=_blank>DysFocus</a></td>
  </tr>
  <tr>
    <td colspan=2>
      <a
        href=http://www.men.public.lu/fr/actualites/articles/communiques-conference-presse/2018/02/08-observatoire/index.html
        target=_blank>Observatoire national de la qualité scolaire et Conseil national des
        programmes</a></td>
  </tr>
  <tr>
    <td colspan=2>
      <a
        href=http://www.lifelong-learning.lu
        target=_blank>D'Portal fir liewenslaangt Léieren</a></td>
  </tr>
  <tr>
    <td colspan=2>
      <h2>Nationale Bildungsbericht 2018</h2>
      <ul>
        <li><a
          href=http://www.men.public.lu/fr/actualites/publications/themes-transversaux/statistiques-analyses/bildungsbericht/2018
          target=_blank>Bericht</a></li>
        <li><a href=https://www.bildungsbericht.lu target=_blank>Erklärungen an Auteuren</a>
        </li>
      </ul>
    </td>
  </tr>
  <tr>
    <td colspan=2>
      <h2>Elektromagnéitesch Felder (WiFi etc.)</h2>
      <ul>
        <li><a
          href=https://ehtrust.org/science/research-on-wireless-health-effects
          target=_blank>Peer Reviewed Scientific Research on Wireless Radiation</a></li>
        <li><a href=http://emf.foxi.lu target=_blank>Fuerschung an Impakt op eis Gesondheet</a>
        </li>
        <li><a
          href=http://www.men.public.lu/fr/actualites/articles/questions-parlementaires/2017/10/27-qp-3313/hansen.pdf
          target=_blank>QP 3313 21.9.17 : Usage du Wi-Fi à l'école</a></li>
      </ul>
    </td>
  </tr>
  <tr>
    <td colspan=2>
      <a
        href=http://www.men.public.lu/fr/actualites/articles/questions-parlementaires/2017/10/09-qp-3237/hansen.pdf
        target=_blank>QP 3237 23.8.17 : Réforme de la formation professionnelle</a></td>
  </tr>
  <tr>
    <td colspan=2>
      <a href=https://www.100komma7.lu/article/aktualiteit/d-zukunft-vun-der-aarbecht
         target=_blank>12.11.17 100komma7 Zukunft vun der Aarbecht</a></td>
  </tr>
  <tr>
    <td colspan=2>
      <a href=http://tele.rtl.lu/emissiounen/kloertext/1097227.html
         target=_blank>19.11.17 RTL Kloertext Aarbecht: Wéi, wéini a wou (muer)
        schaffen?</a></td>
  </tr>
  <tr>
    <td colspan=2>
      <a href=http://www.webcorp.org.uk
         target=_blank>WebCorp fir rar an/oder nei Aspekter vun der englescher Sprooch no ze
        sichen.</a></td>
  </tr>
  <tr>
    <td colspan=2>
      <h2>D'Lëtzebuerger Sprooch</h2>
      <ul>
        <li><a href=https://spellchecker.lu/online-checker target=_blank>Online-Checker</a>
        </li>
        <li><a href=http://www.lod.lu target=_blank>Lëtzebuerger Online Dictionnaire</a></li>
        <li><a href=http://dict.luxdico.com target=_blank>Luxdico.com</a></li>
        <li><a href=https://www.lexilogos.com/english/luxembourgish_dictionary.htm
               target=_blank>Lexilogos</a></li>
        <li><a href=http://engelmann.uni.lu:8080/portal/WBB2009/LWB//wbgui_py
          target=_blank>Luxemburger Wörterbuch</a></li>
        <li><a href=https://portal.education.lu/schreiwen target=_blank>Schreiwen</a></li>
        <li><a href=http://www.quattropole.org/en/e_learning target=_blank>Learn
          Luxembourgish!</a></li>
        <li><a href=http://www.cpll.lu target=_blank>Conseil fir d'Lëtzebuerger Sprooch</a>
        </li>
        <li><a
          href=http://www.men.public.lu/fr/actualites/grands-dossiers/systeme-educatif/letzebuerger-sprooch
          target=_blank>Promotioun vun der Lëtzebuerger Sprooch</a></li>
        <li><a href=http://tele.rtl.lu/emissiounen/kloertext/1149479.html
             target=_blank>14.3.18 RTL Kloertext Wéi vill Lëtzebuergesch brauch onst Land?</a>
        </li>
      </ul>
    </td>
  </tr>
  <!--<tr>
    <td><a href=http://legilux.public.lu/editorial/codes target=_blank>Code de la fonction
      publique</td>
  </tr>
  <tr>
    <td><a href=http://legilux.public.lu/eli/etat/leg/loi/2017/08/29/a789/jo
           target=_blank>Loi du 29 août 2017 portant sur l’enseignement secondaire</a></td>
  </tr>
  <tr>
    <td><a href=http://legilux.public.lu/eli/etat/leg/rgd/2016/08/31/n1/jo target=_blank>
      Règlement grand-ducal du 31 août 2016 portant sur l'évaluation et la promotion des élèves
      de la formation professionnelle</a>
    </td>
  </tr>-->
</table>
5.4.16.27. ressourcen.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
table {
  border-collapse: collapse;
  box-shadow:      2px 2px 2px #888888;
}

table, tr, th, td {
  border: 4px groove black;
}

th, td {
  padding: 5px;
}

6. Game development

From Scott Rogers (thanks to Michel Houche for the quote):

Gamers can feel when developers are passionate about their games. They can smell it like a dog smells fear. Don’t be afraid to hold onto your unique vision: just be aware that it may not turn out exactly how you envisioned.

Mozilla host a great resource for all things related to game development.

Here is a list of some games running in the browser:

Game engines

html5gameengine.com

HTML5 Game Development

www.html5gamedevelopment.com

6.1. JS game engines

phaser.io

kaboomjs.com

www.babylonjs.com

playcanvas.com

www.melonjs.org

screeps.com

cocos2d-x.org

6.1.1. Phaser

6.1.1.1. Introduction
Phaser is a fast, free, and fun open source HTML5 game framework that offers WebGL and Canvas rendering across desktop and mobile web browsers.
An excellent place to start is www.codecademy.com/learn/learn-phaser.
Take a close look at the examples at phaser.io/examples/v3. You can download all of them, including a large selection of very useful assets for your own games, at github.com/photonstorm/phaser3-examples.
Here is a gentle and very effective introduction to creating a breakout game in Phaser. blog.ourcade.co has a series of very thoughtful articles and tutorials that may accelerate your Phaser learning experience. This is a good example teaching you how to build a basic AI.

Further useful resources:

gamedevacademy.org/phaser-3-tutorial

rexrainbow.github.io/phaser3-rex-notes/docs/site

www.youtube.com/playlist?list=PLDyH9Tk5ZdFzEu_izyqgPFtHJJXkc79no

phaser.io/news/2019/02/build-a-space-shooter-in-phaser-3

www.emanueleferonato.com/2019/05/31/build-a-html5-hyper-casual-game-like-bouncing-ball-by-ketchapp-studio-using-phaser-3-and-arcade-physics

snowbillr.github.io/blog/2018-07-03-buttons-in-phaser-3

www.codeandweb.com/free-sprite-sheet-packer

gamedevacademy.org/how-to-use-pathfinding-in-phaser

www.html5gamedevs.com/topic/39880-rotate-sprite-to-face-a-specific-coordinate

www.html5gamedevs.com/topic/36164-trying-to-implement-movetopointer

www.html5gamedevs.com/topic/38808-movement-and-speed

www.html5gamedevs.com/topic/39840-collision-player-on-movable-platform

Multiplayer game phaser.discourse.group/t/phaser-3-real-time-multiplayer-game-with-physics/1739

blog.ourcade.co/posts/2020/make-first-phaser-3-game-modern-javascript-part1

If you’re looking for an easy, zero installation, IDE for your Phaser endeavours, check out repl.it and select the Phaser.js Game Starter template under languages.

The first thing we need to do is create a new Phaser.Game object and provide it with a configuration:

1
2
3
4
const config = {
}

const game = new Phaser.Game(config)

For details on the options that can be configured, see photonstorm.github.io/phaser3-docs/Phaser.Types.Core.html#.GameConfig.

Download this wonderful tutorial.

It can be very helpful to study some Phaser examples to find inspiration for the solution of some of the problems that you encounter during your own game development. For instance, the following is an example based initially on phaser.io/examples/v3/view/physics/arcade/space but with ideas from phaser.io/examples/v3/view/games/defenda/test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8>
  <title>Shooter 1</title>
  <script src=../phaser.js></script>
  <script src=Game1Scene1.js></script>
  <script src=game1.js type=module></script>
</head>
<body>
</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const gameSettings = {}

const gameConfig = {
  width: 800,
  height: 600,
  physics: {
    default: 'arcade',
    arcade: {
      debug: false
    }
  },
  scene: [Game1Scene1]
  //pixelArt: true,
}

const game = new Phaser.Game(gameConfig)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
class Game1Scene1 extends Phaser.Scene {
  constructor() {
    super("GEShooter1")
    this.lastFired = 0 // Store time when player last fired to handle firing cadence.
    this.lastEnemyFired = 0 // Store time when enemies last fired to handle firing cadence.
  }

  preload() {
    this.load.image('background', 'assets/nebula.jpg')
    this.load.image('stars', 'assets/stars.png')
    this.load.image('enemy', 'assets/xenon2_ship_right.png')
    this.load.image('enemy-bullet', 'assets/enemy-bullet.png')
    this.load.image('bullet', 'assets/bullet.png')
    this.load.atlas('space', 'assets/space.png', 'assets/space.json')
    this.load.spritesheet('explosion', 'assets/explosion.png', {
      frameWidth: 16,
      frameHeight: 16
    })
    this.load.audio('audio_explosion', ['assets/explosion.ogg', 'assets/explosion.mp3'])
    this.load.audio('audio_blaster', ['assets/blaster.mp3'])
  }

  create() {
    class Bullet extends Phaser.Physics.Arcade.Image {
      constructor(scene) {
        super(scene, 0, 0, 'bullet')
        this.setBlendMode(Phaser.BlendModes.ADD)
        this.setDepth(1)
        this.speed = 500
        this.lifespan = 500
        //this._temp = new Phaser.Math.Vector2();
      }

      fire(ship) {
        this.lifespan = 500
        this.setActive(true)
        this.setVisible(true)
        this.setAngle(ship.body.rotation)
        this.setPosition(ship.x, ship.y)
        this.body.reset(ship.x, ship.y)

        const angle = Phaser.Math.DegToRad(ship.body.rotation)

        // this.body.world.velocityFromRotation(angle, this.speed + ship.body.speed,
        // this.body.velocity);
        this.scene.physics.velocityFromRotation(angle, this.speed + ship.body.speed,
          this.body.velocity)

        this.body.velocity.x *= 2
        this.body.velocity.y *= 2
      }

      update(time, delta) {
        this.lifespan -= delta

        if (this.lifespan <= 0) {
          this.setActive(false)
          this.setVisible(false)
          this.body.stop()
        }
      }
    }

    class Explosion extends Phaser.GameObjects.Sprite {
      constructor(scene, x, y) {
        super(scene, x, y, "explosion")
        scene.add.existing(this)
        this.play("explode")
      }
    }

    class Enemy extends Phaser.Physics.Arcade.Image {
      constructor(scene, x, y) {
        super(scene, x, y, 'enemy')
        this.setDepth(1)
        this.speed = 50
      }

      update(time, delta) {
        const ship = this.scene.ship
        //this.setAngle(ship.body.rotation + 90 % 360);
        //this.setPosition(ship.x, ship.y);
        //this.body.reset(ship.x, ship.y);
        //https://www.html5gamedevs.com/topic/39880-rotate-sprite-to-face-a-specific-coordinate
        const currentPoint = new Phaser.Geom.Point(this.x, this.y)
        let pointToMoveTo = new Phaser.Geom.Point(ship.x, ship.y)
        this.rotation = Phaser.Math.Angle.BetweenPoints(currentPoint, pointToMoveTo)
        // https://photonstorm.github.io/phaser3-docs/Phaser.Physics.Arcade.ArcadePhysics.html#moveTo__anchor
        this.scene.physics.moveTo(this, ship.x, ship.y, 50)
        const angle = Phaser.Math.DegToRad(ship.body.rotation)

        // this.body.world.velocityFromRotation(angle, this.speed + ship.body.speed, this.body.velocity);
        //this.scene.physics.velocityFromRotation(angle, this.speed, this.body.velocity);

        this.speed *= 2
      }
    }

    const createTextures = () => {
      //  Prepare some spritesheets and animations
      this.textures.addSpriteSheetFromAtlas('mine-sheet', {
        atlas: 'space',
        frame: 'mine',
        frameWidth: 64
      })
      this.textures.addSpriteSheetFromAtlas('asteroid1-sheet', {
        atlas: 'space',
        frame: 'asteroid1',
        frameWidth: 96
      })
      this.textures.addSpriteSheetFromAtlas('asteroid2-sheet', {
        atlas: 'space',
        frame: 'asteroid2',
        frameWidth: 96
      })
      this.textures.addSpriteSheetFromAtlas('asteroid3-sheet', {
        atlas: 'space',
        frame: 'asteroid3',
        frameWidth: 96
      })
      this.textures.addSpriteSheetFromAtlas('asteroid4-sheet', {
        atlas: 'space',
        frame: 'asteroid4',
        frameWidth: 64
      })
    }
    const createAnims = () => {
      this.anims.create({
        key: 'mine-anim',
        frames: this.anims.generateFrameNumbers('mine-sheet', {start: 0, end: 15}),
        frameRate: 20,
        repeat: -1
      })
      this.anims.create({
        key: 'asteroid1-anim',
        frames: this.anims.generateFrameNumbers('asteroid1-sheet', {start: 0, end: 24}),
        frameRate: 20,
        repeat: -1
      })
      this.anims.create({
        key: 'asteroid2-anim',
        frames: this.anims.generateFrameNumbers('asteroid2-sheet', {start: 0, end: 24}),
        frameRate: 20,
        repeat: -1
      })
      this.anims.create({
        key: 'asteroid3-anim',
        frames: this.anims.generateFrameNumbers('asteroid3-sheet', {start: 0, end: 24}),
        frameRate: 20,
        repeat: -1
      })
      this.anims.create({
        key: 'asteroid4-anim',
        frames: this.anims.generateFrameNumbers('asteroid4-sheet', {start: 0, end: 24}),
        frameRate: 20,
        repeat: -1
      })
      this.anims.create({
        key: 'explode',
        frames: this.anims.generateFrameNumbers('explosion'),
        frameRate: 20,
        repeat: 0,
        hideOnComplete: true
      })
    }
    const createImages = () => {
      //  Add our planets, etc.
      this.add.image(512, 680, 'space', 'blue-planet').setOrigin(0).setScrollFactor(0.6)
      this.add.image(2833, 1246, 'space', 'brown-planet').setOrigin(0).setScrollFactor(0.6)
      this.add.image(3875, 531, 'space', 'sun').setOrigin(0).setScrollFactor(0.6)
      this.galaxy = this.add.image(5345 + 1024, 327 + 1024, 'space', 'galaxy').setBlendMode(1).setScrollFactor(0.6)
      this.add.image(908, 3922, 'space', 'gas-giant').setOrigin(0).setScrollFactor(0.6)
      this.add.image(3140, 2974, 'space', 'brown-planet').setOrigin(0).setScrollFactor(0.6).setScale(0.8).setTint(0x882d2d)
      this.add.image(6052, 4280, 'space', 'purple-planet').setOrigin(0).setScrollFactor(0.6)

      for (let i = 0; i < 8; i++)
        this.add.image(Phaser.Math.Between(0, 8000), Phaser.Math.Between(0, 6000), 'space', 'eyes').setBlendMode(1).setScrollFactor(0.8)
    }
    createTextures()
    createAnims()
    createImages()
    this.explosionSound = this.sound.add("audio_explosion")
    this.blasterSound = this.sound.add("audio_blaster")

    //  World size is 800 x 600
    this.bg = this.add.tileSprite(400, 300, 800, 600, 'background').setScrollFactor(0)

    this.stars = this.add.tileSprite(400, 300, 800, 600, 'stars').setScrollFactor(0)

    const particles = this.add.particles('space')
    //this.enemy = this.physics.add.image(Phaser.Math.Between(0, 8000), Phaser.Math.Between(0, 6000), 'enemy');
    //const enemy = this.physics.add.image(3900, 2900, 'enemy').setDepth(2);
    this.enemies = this.physics.add.group({
      classType: Enemy,
      maxSize: 30,
      runChildUpdate: true
    })
    const startGame = () => {
      this.enemies.get(3900, 2900)
      this.enemies.get(3800, 2800)
    }
    this.ship = this.physics.add.image(4000, 3000, 'space', 'ship').setDepth(2)
    this.physics.add.overlap(this.ship, this.enemies, (ship, enemy) => {
      const explosion1 = new Explosion(this, ship.x + ship.width / 2, ship.y + ship.height / 2)
      const explosion2 = new Explosion(this, enemy.x, enemy.y)
//      this.explosionSound.play();
      console.log('Children:')
      console.log(this.enemies.getChildren().length)
      while (this.enemies.getChildren().length) {
        this.enemies.getChildren()[0].destroy()
        console.log('Destroying enemy')
      }

      //ship.x = Phaser.Math.Between(0, 8000);
      //ship.y = Phaser.Math.Between(0, 6000);//setActive(false);
      //ship.setVisible(false);
      //ship.body.stop();
      //ship.disableBody(true, true);//destroy();
      //enemy.disableBody(true, true); //destroy();
    })
    this.ship.setDrag(300)
    this.ship.setAngularDrag(400)
    this.ship.setMaxVelocity(600)

    startGame()
    const emitter = particles.createEmitter({
      frame: 'blue',
      speed: 100,
      lifespan: {
        onEmit: (particle, key, t, value) => {
          return Phaser.Math.Percent(this.ship.body.speed, 0, 300) * 2000
        }
      },
      alpha: {
        onEmit: (particle, key, t, value) => {
          return Phaser.Math.Percent(this.ship.body.speed, 0, 300)
        }
      },
      angle: {
        onEmit: (particle, key, t, value) => {
          const v = Phaser.Math.Between(-10, 10)
          return (this.ship.angle - 180) + v
        }
      },
      scale: {start: 0.6, end: 0},
      blendMode: 'ADD'
    })

    this.bullets = this.physics.add.group({
      classType: Bullet,
      maxSize: 30,
      runChildUpdate: true
    })
    this.physics.add.overlap(this.bullets, this.enemies, (bullet, enemy) => {
      const explosion = new Explosion(this, enemy.x, enemy.y)
//      this.explosionSound.play();
      enemy.destroy()
    })

    this.enemyBullets = this.physics.add.group({
      classType: Bullet,
      defaultKey: 'enemy-bullet',
      maxSize: 30,
      runChildUpdate: true
    })
    this.physics.add.overlap(this.enemyBullets, this.ship, (bullet, ship) => {
      const explosion = new Explosion(this, ship.x, ship.y)
      //this.explosionSound.play();
      //ship.destroy();
    })

    emitter.startFollow(this.ship)
    //emitter.startFollow(this.enemies.getChildren()[0]);

    this.cameras.main.startFollow(this.ship)

    this.cursors = this.input.keyboard.createCursorKeys()
    this.fire = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE)

    this.add.sprite(4300, 3000).play('asteroid1-anim')

    this.tweens.add({
      targets: this.galaxy,
      angle: 360,
      duration: 100000,
      ease: 'Linear',
      loop: -1
    })
  }

  update(time, delta) {
    const fireEnemyBullets = () => {
      for (const enemy of this.enemies.getChildren()) {
        const ran = Math.random()
        if (ran >= 0.9) {
          const enemyBullet = this.enemyBullets.get()
          if (enemyBullet) {
            enemyBullet.fire(enemy)
            this.lastEnemyFired = time + 200
            this.blasterSound.play()
          }
        }
      }
    }
    if (time > this.lastEnemyFired) fireEnemyBullets()

    if (this.cursors.left.isDown) this.ship.setAngularVelocity(-150)
    else if (this.cursors.right.isDown) this.ship.setAngularVelocity(150)
    else this.ship.setAngularVelocity(0)

    if (this.cursors.up.isDown)
      this.physics.velocityFromRotation(this.ship.rotation, 600, this.ship.body.acceleration)
    else this.ship.setAcceleration(0)

    if (this.fire.isDown && time > this.lastFired && this.ship.active) {
      const bullet = this.bullets.get()
      if (bullet) {
        bullet.fire(this.ship)
        this.lastFired = time + 200
        this.blasterSound.play()
      }
    }

    this.bg.tilePositionX += this.ship.body.deltaX() * 0.5
    this.bg.tilePositionY += this.ship.body.deltaY() * 0.5

    this.stars.tilePositionX += this.ship.body.deltaX() * 2
    this.stars.tilePositionY += this.ship.body.deltaY() * 2
  }
}

There is a tailor made editor available for Phaser development.

6.2. Tools

A free level editor can be found at www.mapeditor.org. This article explains how to use it with Phaser.

6.5. Student game examples

6.5.1. Escape from Space by Marcelo Abbruzzese (Three.js)

EscapeFromSpace

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width/2, user-scalable=no">
    <title>test</title>
    <style>
       html, body {
            width: 100%;
            height: 100%;
            margin: 0;
            overflow: hidden;
        }
       canvas{
            width: 100%;
            height: 100%;
        }
        main{
            position: fixed;
            width: 100%;
            height: 100%;
            text-align: center;
            color: white;
            text-shadow: 2px 2px black;
        }
        header{
            position: fixed;
            color: white;
            width: 100%;
            height: 100%;
            text-align: center;
            font-size: 4em;
            top: 0.5em;
            font-family: sans-serif;
        }

        #Menu{
            position: fixed;
            width: 100%;
            height: 100%;
            text-align: center;
            top: 20%;
        }

        button{
            width: 250px;
            height: 100px;
            background: black;
            color: white;
            font-size: 1.5em;
            margin-top: 1em;
        }

        #Tutorial{
            position: fixed;
            width: 100%;
            height: 100%;
            text-align: center;
            top: 20%;
        }

        #End{
            position: fixed;
            width: 100%;
            height: 100%;
            text-align: center;
            top: 20%;
        }

        @media (min-width:1100px)  {
            #controls{
                display: none;
            }
        }

       #controls{
           position: fixed;
           bottom: 0;
       }

        #controls button{
            width: 4em;
            height: 2em;
        }

        #shoot{
            position: fixed;
            right: 0;
            bottom: 0;
        }



    </style>
    <script src="three.js"></script>
</head>
<body>
    <header id="Header">
        <b>Escape from Space</b>
    </header>
    <main>
        <canvas id="c"></canvas>
        <section id="HUD">
            <p id="Timer">Timer</p>
            <p id="Score">Score</p>
        </section>

        <section id="Menu">
            <button id="Start">Start Game</button><br>
            <button id="How">How to Play</button>
        </section>

        <section id="Tutorial">
            <p>You are playing as a green Spaceship.</p>
            <p>Control it with the <b>WASD</b> keys.</p>
            <p>Enemies will appear and try to shoot you down.</p>
            <p>You can defend yourself by shooting back with the <b>SPACE</b> key.</p>
            <p>On Mobile you can use the buttons to the left and right to control the ship.</p>
            <p>And do use landscape mode on mobile for a better experience.</p>
            <button id="Return">Back to Menu</button>
        </section>

        <section id="End">
            <p id="endScreen">AAAAAAAAAAAAAAAAAAA</p>
            <button id="Back">Back to Menu</button>
        </section>

        <section id="controls">
            <section>
                <button id="shoot">SHOOT</button>
            </section>
            <section>
                <button id="up">UP</button><br>
                <button id="left">LEFT</button>
                <button id="right">RIGHT</button><br>
                <button id="down">DOWN</button>
            </section>
        </section>
    </main>
<script>
    "use strict";

    document.getElementById('Start').addEventListener('click', startGame);
    document.getElementById('How').addEventListener('click', showControls);
    document.getElementById('Back').addEventListener('click', mainMenu);
    document.getElementById('Return').addEventListener('click', mainMenu);
    //Phone controls
    document.getElementById('up').addEventListener('mousedown', upClick);
    document.getElementById('down').addEventListener('mousedown', downClick);
    document.getElementById('left').addEventListener('mousedown', leftClick);
    document.getElementById('right').addEventListener('mousedown', rightClick);
    document.getElementById('up').addEventListener('mouseleave', stopClick);
    document.getElementById('down').addEventListener('mouseleave', stopClick);
    document.getElementById('left').addEventListener('mouseleave', stopClick);
    document.getElementById('right').addEventListener('mouseleave', stopClick);
    document.getElementById('shoot').addEventListener('click', spawnProjectile);

    let start = false;

    function mainMenu(){
        start = false;
        playerShip.position.z = 150;
        playerShip.position.y = -13;
        playerShip.position.x = 0;
        document.getElementById('Menu').style.display = "block";
        document.getElementById('Header').style.display = "block";
        document.getElementById('HUD').style.display = "none";
        document.getElementById('Tutorial').style.display = "none";
        document.getElementById('End').style.display = "none";
        enemyPrototypes.forEach((value, key, arr) => {
            arr[key].projectile.forEach((valueProjectile, keyProjectile, arrProjectile) => {
                arrProjectile[keyProjectile].projectile.position.z = 50;
                arrProjectile[keyProjectile].shooting = false;
            });
            arr[key].enemy.position.z = 50;
            arr[key].alive = false;
            arr[key].moving = false;
        });

        projectilePrototypes.forEach((value, key, arr) => {
            arr[key].projectile.position.z = 50;
            arr[key].shooting = false;
        });

        enemyActive = 0;
    }

    function showControls(){
        start = false;
        document.getElementById('Menu').style.display = "none";
        document.getElementById('Header').style.display = "block";
        document.getElementById('HUD').style.display = "none";
        document.getElementById('Tutorial').style.display = "block";
        document.getElementById('End').style.display = "none";
    }

    function endGame(){
        start = false;
        document.getElementById('Menu').style.display = "none";
        document.getElementById('Header').style.display = "none";
        document.getElementById('HUD').style.display = "none";
        document.getElementById('Tutorial').style.display = "none";
        document.getElementById('End').style.display = "block";
    }

    function startGame(){
        start = true;
        playerShip.position.z = 10;
        document.getElementById('Menu').style.display = "none";
        document.getElementById('Header').style.display = "none";
        document.getElementById('HUD').style.display = "block";
        document.getElementById('Tutorial').style.display = "none";
        document.getElementById('End').style.display = "none";
        startTimer = worldTimeGlobal;
        timeBefore = 0;
        score = 0;
    }


    //Select Canvas
    let canvas = document.querySelector('#c');
    let renderer = new THREE.WebGLRenderer({canvas});
    let scene = new THREE.Scene({renderer});
    document.body.appendChild(renderer.domElement);

    //Camera Options
    let camera = new THREE.PerspectiveCamera(90, 2, 0.1, 2000);
    camera.position.z = 30;
    camera.rotation.x = 0;
    camera.position.y = 0;

    //"Global" Variables
    let vertical = 0;
    let horizontal = 0;
    let thrust = 0;
    let playerShip = createPlayerShip();
    let playerSpeed = 50;
    let maxWallPosition;
    let difficulty = 10;
    let enemyAmount = 10;
    let enemyActive = 0;

    //Player Hitboxes
    let pHitboxXLeft;
    let pHitboxXRight;
    let pHitboxYUp;
    let pHitboxYDown;
    let pHitboxZFront;
    let pHitboxZBack;

    //Classes
    class Wall {
        constructor(line, linePosition) {
            this.line = line;
            this.linePosition = linePosition;
        }

        getPosition() {
            //position.z returns position in object space, lineposition has the worldposition
            return (this.line.position.z + this.linePosition);
        }
    }
    const wallPrototypes = [];


    class Projectile {
        constructor(projectile) {
        this.projectile = projectile;
        }
        shooting = false;
    }
    const projectilePrototypes = [];


    class Enemy{
        constructor(enemy) {
            this.enemy = enemy;
        }
        alive = false;
        moving = false;
        moveToPosition(x,y,z){
            this.enemy.position.z = z;
            this.enemy.position.x = Math.floor(Math.random() * (150 + 1));
            this.enemy.position.y = Math.floor(Math.random() * (100 + 1));

            if(x < 0){
                this.enemy.position.x *= -1
            }

            if(y < 0){
                this.enemy.position.y *= -1
            }
            this.x = x;
            this.y = y;
            this.moving = true;
        }
        projectile = [];
        hitboxXLeft;
        hitboxXRight;
        hitboxYUp;
        hitboxYDown;
        hitboxZFront;
        hitboxZBack;
    }
    const enemyPrototypes = [];


    class Enemyprojectile {
        constructor(projectile) {
            this.projectile = projectile;
        }
        shooting = false;
    }


    //Outer walls and floor
    {
        //Spawn Floor
        const geometryGround = new THREE.PlaneGeometry(500,50);
        const materialGround = new THREE.MeshLambertMaterial({color: 'grey'});

        const groundUp = new THREE.Mesh(geometryGround, materialGround);
        scene.add(groundUp);
        groundUp.position.y = -25;
        groundUp.rotation.x = Math.PI * -0.5;
        groundUp.rotation.z = Math.PI * -0.5;

        /*const groundDown = new THREE.Mesh(geometryGround, materialGround);
        scene.add(groundDown);
        groundDown.position.y = 25;
        groundDown.rotation.x = Math.PI * 0.5;
        groundDown.rotation.z = Math.PI * -0.5;*/

        //Spawn Walls
        let spawnAmount = 50;
        let spawnDistance = 20;

        for(let x = 0; x < spawnAmount; x++){
            const starMaterial = new THREE.LineBasicMaterial({color: "blue"});
            const starMaterial2 = new THREE.LineBasicMaterial({color: "darkblue"});
            let zPosition = -((x)*spawnDistance);
            let starLine;

            //Right side
            let points1 = [];
            points1.push( new THREE.Vector3(45, 200, zPosition));
            points1.push( new THREE.Vector3(45, -200, zPosition));

            const starGeometry1 = new THREE.BufferGeometry().setFromPoints(points1);

            if(x % 2 === 0 || x === 0){
                 starLine = new THREE.Line(starGeometry1, starMaterial2);
            }
            else{
                 starLine = new THREE.Line(starGeometry1, starMaterial);
            }

            scene.add(starLine);

            let WallClass1 = new Wall(starLine, zPosition);
            wallPrototypes.push(WallClass1);


            //Left Side
            let points2 = [];
            points2.push( new THREE.Vector3(-45, 200, zPosition));
            points2.push( new THREE.Vector3(-45, -200, zPosition));

            const starGeometry2 = new THREE.BufferGeometry().setFromPoints(points2);

            if(x % 2 === 0 || x === 0){
                starLine = new THREE.Line(starGeometry2, starMaterial2);
            }
            else{
                starLine = new THREE.Line(starGeometry2, starMaterial);
            }

            scene.add(starLine);

            let WallClass2 = new Wall(starLine, zPosition);
            wallPrototypes.push(WallClass2);

            if(x === spawnAmount-1){
                maxWallPosition = WallClass2.getPosition();
            }
        }
    }

    //Light
    {
        const color = 0xFFFFFF;
        const intensity = 1;
        const light = new THREE.SpotLight(color, intensity, 500);

        scene.add(light);
        light.position.set(0,0,50);
        light.rotation.x = Math.PI * 0.25;
    }

    //Player
    function createPlayerShip() {
        const geometry = new THREE.BoxGeometry(10,5,5);
        const material = new THREE.MeshLambertMaterial({color: 'green'});
        const playerShip = new THREE.Mesh(geometry, material);

        scene.add(playerShip);
        playerShip.position.y = -13;
        return playerShip;
    }

    //Projectile Player Spawn
    {
        let projectileAmount = 2;
        for(let x = 0; x < projectileAmount; x++) {
            const geometry = new THREE.OctahedronGeometry(2, 2, 2);
            const material = new THREE.MeshLambertMaterial({color: 'white'});
            const projectile = new THREE.Mesh(geometry, material);

            scene.add(projectile);
            projectile.position.z = 50;

            let projectileClass = new Projectile(projectile);
            projectilePrototypes.push(projectileClass);
        }
    }

    //Projectile "Spawn"
    function spawnProjectile(){
        let stopLoop = false;
        projectilePrototypes.forEach((value, key, arr) =>{
            if(!arr[key].shooting && !stopLoop){
                arr[key].projectile.position.z = 5;
                arr[key].projectile.position.x = playerShip.position.x;
                arr[key].projectile.position.y = playerShip.position.y;
                arr[key].shooting = true;
                stopLoop = true;
                //console.log("shoot");
            }
        });
    }

    //Enemy Spawn
    {
        for(let x = 0; x < enemyAmount; x++) {
            const geometry = new THREE.BoxGeometry(10,5,5);
            const material = new THREE.MeshLambertMaterial({color: 'red'});
            const enemy = new THREE.Mesh(geometry, material);

            scene.add(enemy);
            enemy.position.z = 50;

            let enemyClass = new Enemy(enemy);
            enemyPrototypes.push(enemyClass);

            //Enemy Projectile Spawn
            {
                let projectileAmount = 1;
                for(let x = 0; x < projectileAmount; x++) {
                    const geometry = new THREE.OctahedronGeometry(2, 2, 2);
                    const material = new THREE.MeshLambertMaterial({color: 'yellow'});
                    const projectile = new THREE.Mesh(geometry, material);

                    scene.add(projectile);
                    projectile.position.z = 50;

                    let projectileClass = new Enemyprojectile(projectile);
                    enemyClass.projectile.push(projectileClass);
                }
            }
        }
    }

    //Enemy "Spawn"
    function spawnEnemy(){
        let stopLoop = false;
        enemyPrototypes.forEach((value, key, arr) =>{
            if(!arr[key].alive && !arr[key].moving && !stopLoop){
                let z =  Math.floor(Math.random() * (50 - 35 + 1)) + 35;
                z *= -1;
                let x = Math.floor(Math.random() * 36);
                if(Math.floor(Math.random()*2) === 1){
                    x *= -1;
                }
                let y = Math.floor(Math.random() * 21);
                if(Math.floor(Math.random()*2) === 1){
                    y *= -1;
                }
                enemyActive ++;
                console.log("spawn");
                arr[key].moveToPosition(x,y,z);
                stopLoop = true;
            }
        });
    }

    renderer.render(scene,camera);

    //Resize renderer with canvas size
    function resizeRenderer(renderer){
        const canvas = renderer.domElement;
        const pixelRatio = window.devicePixelRatio;
        const width = window.innerWidth;
        const height = window.innerHeight;
        const needResize = canvas.width !== width || canvas.height !== height;
        if(needResize){
            renderer.setSize(width, height, false);
        }
        return needResize;
    }

    //Render every frame
    let score = 0;
    let timeBefore = 0;
    let startTimer = 0;
    let worldTimeGlobal;
    function renderAnimation(worldTime){
        //Get DeltaTime
        worldTime *= 0.001;
        worldTimeGlobal = worldTime;
        let time = worldTime-startTimer;

        const deltaTime = time - timeBefore;
        timeBefore = time;

        //Check if renderer needs to resize
        if(resizeRenderer(renderer)){
            const canvas = renderer.domElement;
            camera.aspect = canvas.clientWidth / canvas.clientHeight;
            camera.updateProjectionMatrix();
        }

        //Walls animation
        wallPrototypes.forEach((value, key, arr) => {
            const currentPosition = arr[key].getPosition();
            arr[key].line.position.z += 1*deltaTime*70;
            if(currentPosition > 20){
                //console.log("passthrough");
                arr[key].line.position.z = maxWallPosition - arr[key].linePosition;
            }
        });

        if(start){

            //Player Projectile movement
            projectilePrototypes.forEach((value, key, arr) => {
                if (arr[key].shooting) {
                    const currentPosition = arr[key].projectile.position.z;
                    arr[key].projectile.position.z -= 1 * deltaTime * 150;
                    if (currentPosition < -100) {
                        arr[key].shooting = false;
                        arr[key].projectile.position.z = 50;
                    }
                }

                //Check if enemy is hit
                enemyPrototypes.forEach((valueEnemy, keyEnemy, arrEnemy) => {
                    const projectileX = arr[key].projectile.position.x;
                    const projectileY = arr[key].projectile.position.y;
                    const projectileZ = arr[key].projectile.position.z;
                    const enemyXLeft = arrEnemy[keyEnemy].hitboxXLeft;
                    const enemyXRight = arrEnemy[keyEnemy].hitboxXRight;
                    const enemyYUp = arrEnemy[keyEnemy].hitboxYUp;
                    const enemyYDown = arrEnemy[keyEnemy].hitboxYDown;
                    const enemyZFront = arrEnemy[keyEnemy].hitboxZFront;
                    const enemyZBack = arrEnemy[keyEnemy].hitboxZBack;
                    if (projectileX < enemyXRight && projectileX > enemyXLeft && projectileY < enemyYUp && projectileY > enemyYDown && projectileZ > enemyZBack && projectileZ < enemyZFront && arr[key].shooting) {
                        arr[key].projectile.position.z = 50;
                        arr[key].shooting = false;
                        arrEnemy[keyEnemy].enemy.position.z = 50;
                        enemyActive--;
                        arrEnemy[keyEnemy].moving = false;
                        arrEnemy[keyEnemy].alive = false;
                        score += 100;
                        console.log("Enemy hit");
                    }
                });

            });

            //Player Movement
            playerShip.position.x += (horizontal * deltaTime) * playerSpeed;
            playerShip.position.y += (vertical * deltaTime) * playerSpeed;
            playerShip.position.z += (thrust * deltaTime) * playerSpeed;

            if (playerShip.position.x < -35) {
                playerShip.position.x = -35;
            } else if (playerShip.position.x > 35) {
                playerShip.position.x = 35;
            }

            if (playerShip.position.y < -20) {
                playerShip.position.y = -20;
            } else if (playerShip.position.y > 20) {
                playerShip.position.y = 20;
            }

            if(vertical > 1){
                vertical = 1;
            }
            else if(vertical < -1){
                vertical = -1;
            }
            else if(horizontal > 1){
                horizontal = 1;
            }
            else if (horizontal < -1){
                horizontal = -1;
            }

            //Enemy Spawner
            const spawnChance = (enemyActive - (time / difficulty)) / enemyAmount;
            if (spawnChance + 100 < 0) {
                spawnEnemy();
            } else if (spawnChance + 100 <= Math.floor(Math.random() * 101)) {
                spawnEnemy();
            }

            //Update player hitbox
            pHitboxXLeft = playerShip.position.x - 5;
            pHitboxXRight = playerShip.position.x + 5;
            pHitboxYUp = playerShip.position.y + 2.5;
            pHitboxYDown = playerShip.position.y - 2.5;
            pHitboxZFront = playerShip.position.z + 2.5;
            pHitboxZBack = playerShip.position.z - 2.5;


            enemyPrototypes.forEach((value, key, arr) => {
                //Enemy move to spawn
                if (arr[key].moving) {

                    if (arr[key].enemy.position.y > arr[key].y) {
                        arr[key].enemy.position.y -= 1;
                    } else if (arr[key].enemy.position.y < arr[key].y) {
                        arr[key].enemy.position.y += 1;
                    }

                    if (arr[key].enemy.position.x > arr[key].x) {
                        arr[key].enemy.position.x -= 1;
                    } else if (arr[key].enemy.position.x < arr[key].x) {
                        arr[key].enemy.position.x += 1;
                    }

                    if (arr[key].enemy.position.y === arr[key].y && arr[key].enemy.position.x === arr[key].x) {
                        arr[key].moving = false;
                        arr[key].alive = true;
                    }
                }

                //Enemy movement after spawn
                if (arr[key].alive && !arr[key].moving && Math.floor(Math.random() * (100 + 1)) > 100 - (time * 0.02)) {

                    let x = Math.floor(Math.random() * 36);
                    if (Math.floor(Math.random() * 2) === 1) {
                        x *= -1;
                    }
                    let y = Math.floor(Math.random() * 21);
                    if (Math.floor(Math.random() * 2) === 1) {
                        y *= -1;
                    }
                    arr[key].x = x;
                    arr[key].y = y;
                    arr[key].moving = true;
                }

                //Update Enemy Hitbox
                arr[key].hitboxXLeft = arr[key].enemy.position.x - 8;
                arr[key].hitboxXRight = arr[key].enemy.position.x + 8;
                arr[key].hitboxYUp = arr[key].enemy.position.y + 5;
                arr[key].hitboxYDown = arr[key].enemy.position.y - 5;
                arr[key].hitboxZFront = arr[key].enemy.position.z + 5;
                arr[key].hitboxZBack = arr[key].enemy.position.z - 5;

                //Enemy Projectile "Spawn"
                arr[key].projectile.forEach((valueProjectile, keyProjectile, arrProjectile) => {
                    let stopLoop = false;
                    //"Spawn" Proijectile

                    if (Math.floor(Math.random() * (100 + 1)) > 100 - (time * 0.05) && arr[key].alive) {
                        if (!arrProjectile[keyProjectile].shooting && !stopLoop) {
                            arrProjectile[keyProjectile].projectile.position.z = arr[key].enemy.position.z + 10;
                            arrProjectile[keyProjectile].projectile.position.x = arr[key].enemy.position.x;
                            arrProjectile[keyProjectile].projectile.position.y = arr[key].enemy.position.y;
                            arrProjectile[keyProjectile].shooting = true;
                            stopLoop = true;
                        }
                    }

                    //Move Projectile
                    if (arrProjectile[keyProjectile].shooting) {
                        const currentPosition = arrProjectile[keyProjectile].projectile.position.z;
                        arrProjectile[keyProjectile].projectile.position.z += 1 * deltaTime * 150;
                        if (currentPosition > 20) {
                            arrProjectile[keyProjectile].shooting = false;
                            arrProjectile[keyProjectile].projectile.position.z = 50;
                        }
                    }

                    //Check if Enemy Projectile hit player
                    const projectileX = arrProjectile[keyProjectile].projectile.position.x;
                    const projectileY = arrProjectile[keyProjectile].projectile.position.y;
                    const projectileZ = arrProjectile[keyProjectile].projectile.position.z;
                    // console.log(pHitboxXLeft);
                    if (projectileX < pHitboxXRight && projectileX > pHitboxXLeft &&
                      projectileY < pHitboxYUp && projectileY > pHitboxYDown &&
                      projectileZ > pHitboxZBack && projectileZ < pHitboxZFront) {
                        console.log("Player hit");
                        document.getElementById('endScreen').innerHTML =
                          "<h1>Overall Score</h1> Time " + Math.floor(time) + " + Score " +
                          score +" = <h1>" + (Math.floor(time) + score)+"</h1>";
                        endGame();
                    }
                });
            });
        }

        //Debug
        /*document.getElementById("Time").innerHTML = "Time " + Math.floor(time) +
                                                        "<br>Position X " + playerShip.position.x +
                                                        "<br> Position Y " + playerShip.position.y +
                                                        "<br> Rotation" + playerShip.rotation.z;*/

        document.getElementById('Timer').innerHTML = "Time: " + Math.floor(time);
        document.getElementById('Score').innerHTML = "Score: " + score;

        //Update renderer
        renderer.render(scene, camera);

        requestAnimationFrame(renderAnimation);
    }



    //Keybindings
    document.addEventListener("keydown", event => {
        if(event.code === "KeyW" && event.repeat === false){
            vertical += 1 ;
        }
        if(event.code === "KeyA" && event.repeat === false){
            horizontal += -1;
        }
        if(event.code === "KeyS" && event.repeat === false){
            vertical += -1;
        }
        if(event.code === "KeyD" && event.repeat === false){
            horizontal += 1;
        }
        if(event.code === "Space" && event.repeat === false){
            spawnProjectile();
        }
        //console.log(event.code);
    });

    document.addEventListener("keyup", event => {
        if(event.code === "KeyW" && event.repeat === false){
            vertical += -1;
        }
        if(event.code === "KeyA" && event.repeat === false){
            horizontal += 1;
        }
        if(event.code === "KeyS" && event.repeat === false){
            vertical += 1;
        }
        if(event.code === "KeyD" && event.repeat === false){
            horizontal += -1;
        }
    });

    function upClick(){
        vertical = 1;
    }
    function downClick() {
        vertical = -1;
    }
    function leftClick() {
        horizontal = -1;
    }
    function rightClick() {
        horizontal = 1;
    }
    function stopClick() {
        vertical = 0;
        horizontal = 0;
    }
    requestAnimationFrame(renderAnimation);

//Code from stat.js, display fps and ms
/*javascript:(function(){
    let script=document.createElement('script');
    script.onload=function(){
        let stats=new Stats();
        document.body.appendChild(stats.dom);requestAnimationFrame(function loop(){
            stats.update();
            requestAnimationFrame(loop);
        });
    };
    script.src='//mrdoob.github.io/stats.js/build/stats.min.js';
    document.head.appendChild(script);
})();*/

mainMenu();

</script>
</body>
</html>

6.5.2. Judgement Day by Mariano Cayzac (Phaser.js)

JudgementDay

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Judgement Day</title>
  <script src=//cdn.jsdelivr.net/npm/phaser@3.23.0/dist/phaser.min.js></script>
  <script src="jsFiles/Scene3.js"></script>
  <script src="jsFiles/Scene2.js"></script>
  <script src="jsFiles/Scene1.js"></script>
  <script src="jsFiles/main.js"></script>
  <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
  <header><h1> Judgement Day</h1></header>
  <main>
  <div id = "Game"></div>
  <h2>How to play?</h2>
  <p>You simply use your  <strong>&#60; right &#62;</strong> and  <strong>&#60; left &#62;</strong>  arrow to move and  <strong>&#60; spacebar &#62;</strong>  to shoot.</p>
  </main>
  <footer>
    <img src="assets/logo.png" alt = "BTS logo" >
  <section>
    <p>Made by: Mariano Cayzac</p>  
  </section>
  </footer>

</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
body
{
  background-color: black;
}

header
{
  background-color: rgb(203, 65, 96);
  font-color:white;
  color:white;
  text-align:center;
  border: solid 1px;
  width:auto;
    margin: auto;
}

div
{
  text-align: center;
  margin: 0px;
  padding: 0px;
  background:url(assets/backgroundStyle.png)bottom center no-repeat;
}

main
{
  color:white;
  font-color:white;
  margin: 0px;
  padding: 0px;
}

h2
{
  text-align:justify;
  width:auto;
    margin: auto;
  background-color: rgb(203, 65, 96);
  padding:5px;
  border: solid 1px;
}

p
{
  margin: 10px;
  padding: 10px;
}

section
{
  float:right;
  display: inline-block;
    margin: 25px;
    margin: 25px;
}

img
{
  float:left;
    margin-left: 25px;
    margin-top: 25px;
  width: 200px;
}

footer
{
  display: inline-block;
  width: 100%;
    border: solid 1px;
  background-color: rgb(203, 65, 96);
  color:white;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// create a new scene named "Game"
let gameScene = new Phaser.Scene('Game');

const gameSettings  = 
{
  playerSpeed:169,
}
// our game's configuration
const config = {
  type: Phaser.AUTO,  //Phaser will decide how to render our game (WebGL or Canvas)
  width: 360, // game width
  height: 640, // game height
  scene: [Scene1,Scene2,Scene3], // our newly created scene
  parent: "Game",
  pixelArt: true,
  physics:{
    default:"arcade",
    arcade:{
      debug:false,
      gravity: {
      x:0,
      y:0
    }
    }
  
    

  }

};
 
// create the game, and pass it the configuration
let game = new Phaser.Game(config);
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Scene1 extends Phaser.Scene
{
  constructor()
  {
    super("bootGame");

  }

  preload()
  {
    this.load.image("background" , "assets/BackGround.png");
    this.load.image("bullet", "assets/Bullet.png");
    this.load.image("enemBullet", "assets/Bullet02.png");
    this.load.image("player", "assets/Character.png");
    this.load.image("defEnemy", "assets/enemy001.png");
    this.load.image("attEnemy", "assets/enemy002.png");
    this.load.image("asteroid", "assets/rock.png");
    this.load.image("title", "assets/title.png");
    this.load.image("play", "assets/PLAY.png");
    this.load.image("finalScore", "assets/end.png");
    this.load.image("retry", "assets/retry.png");
    this.load.image("moveButton", "assets/movingButton.png");
    this.load.image("shootButton", "assets/shootButton.png");

  }
  

  create()
  {
    this.mbackground = this.add.image(0,0,"background");
    this.mbackground.setOrigin(0,0);
    let title = this.add.sprite(180,200,"title");
    title.scaleX = 3;
    title.scaleY = 3;
    this.playButton = this.add.image(180,300,"play");

    this.playButton.setInteractive();
    this.playButton.on("pointerover", ()=>{
      this.playButton.scaleX = 2;
      this.playButton.scaleY = 2;
    })

    this.playButton.on("pointerout", ()=>{
      this.playButton.scaleX = 1;
      this.playButton.scaleY = 1;
    })

    this.playButton.on("pointerup", ()=>{
      this.scene.start("playGame");
    })
    
    //Asteroids
    this.mAsteroids = this.physics.add.group();
    this.mAsteroidTime = 0.0;
    
    //enemies 01
    this.mEnemies = this.physics.add.group();
    let maxmEnemies = 5;
    let mEnemyX = 8;
    let mEnemyY = 100;
    let count = 0;

    for(let i = 0 ; i < maxmEnemies; i++)
    {
      let mEnemy = this.add.sprite(0,0, "defEnemy");
      mEnemy.setOrigin(0,0);
      this.mEnemies.add(mEnemy);
      mEnemy.x = mEnemyX;
      mEnemy.y = mEnemyY;
      this.mEnemySpeed = 0;
      this.mEnemySpeedY = 0;
      this.mNotStarted = true;

      if(count < 5)
      {
        mEnemyX += 36;
        count ++;
      }
    }


    //enemies 02
    this.mEnemies2 = this.physics.add.group();
    let maxmEnemies2 = 5;
    let mEnemyX2 = 8;
    let mEnemyY2 = 100;
    let count2 = 0;

    for(let i = 0 ; i < maxmEnemies2; i++)
    {
      let mEnemy2 = this.add.sprite(0,0, "defEnemy");
      mEnemy2.setOrigin(0,0);
      this.mEnemies2.add(mEnemy2);
      mEnemy2.x = mEnemyX2;
      mEnemy2.y = 400;
      this.mEnemySpeed2 = 0;
      this.mEnemySpeedY2 = 0;
      this.mNotStarted2 = true;

      if(count2 < 5)
      {
        mEnemyX2 += 36;
        count2 ++;
      }
    }   
  }

  update(time,delta)
  {
    if(this.mAsteroids.getChildren().length < 3 && time > this.mAsteroidTime)
      {
      this.createAsteroid();
      this.mAsteroidTime = time + 1000;
      }

      this.resetmAsteroid();
      this.displayEnemMovement();
      this.displayEnemMovement2();
  }

    createAsteroid()
  {
    let mAsteroid = this.add.sprite(0,0, "asteroid");
    mAsteroid.setOrigin(0,0);
    this.mAsteroids.add(mAsteroid);
    mAsteroid.x = Phaser.Math.Between(10,config.width);
    mAsteroid.y = -30;
    mAsteroid.body.velocity.y += 400;
  }

  resetmAsteroid()
  {
    this.mAsteroids.children.each(mAsteroid => 
      {
          if (mAsteroid.y > 700)
          {
            mAsteroid.y = -30;
            mAsteroid.x = Phaser.Math.Between(10,config.width);
          }
      });
  }


  displayEnemMovement()
  {
    this.mEnemies.children.each(mEnemy => 
      {
        if(this.mNotStarted === true)
        {
          this.mEnemySpeed = 100;
        this.mNotStarted = false;
        }
  
        if(mEnemy.x > 800)
      {
        this.mEnemySpeed = -100;
      }
      else if(mEnemy.x < -800)
      {
        this.mEnemySpeed = 100;
      }

        this.mEnemies.setVelocityX(this.mEnemySpeed);
      });
  }

  displayEnemMovement2()
  {
    this.mEnemies2.children.each(mEnemy2 => 
      {
        if(this.mNotStarted2 === true)
        {
          this.mEnemySpeed2 = -100;
        this.mNotStarted2 = false;
        }
  
        if(mEnemy2.x > 800)
      {
        this.mEnemySpeed2 = -100;
      }
      else if(mEnemy2.x < -800)
      {
        this.mEnemySpeed2 = 100;
      }

        this.mEnemies2.setVelocityX(this.mEnemySpeed2);
      });
  }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
class Scene2 extends Phaser.Scene
{
  constructor()
  {
    super("playGame");
  }

  create()
  {
    //Time related events
    this.score = 0;
    this.enemTimer = 4000.0;
    this.shootTimer = 0.0;
    this.asteroidTime = 4000.0;

    //Create background
    this.background = this.add.image(0,0,"background");
    this.background.setOrigin(0,0);

    //Create Player
    this.player = this.physics.add.image(180,600, "player");
    this.player.setOrigin(0,0);
    this.player.body.collideWorldBounds=true;
    this.player.bounce = 1;
    this.playerLife = 3;

    //Get Player Input
    this.pressed = false;
    this.pad1 = this.add.sprite(310,600,"moveButton");
    this.pad2 = this.add.sprite(40,600,"moveButton");
    this.pad3 = this.add.sprite(310,520,"shootButton");

    this.pad1.setScale(2);
    this.pad2.setScale(2);
    this.pad3.setScale(2);

    this.pad1.setInteractive();
    this.pad2.setInteractive();
    this.pad3.setInteractive();

    this.cursors = this.input.keyboard.createCursorKeys();
    this.shootBullet = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
     
    //Get Bullet Object
    this.bulletGroup = this.physics.add.group();
    this.enemBulletGp = this.physics.add.group();
    this.asteroids = this.physics.add.group();

    //Create defense enemies
    this.enemies = this.physics.add.group();
    let maxEnemies = 40;
    let enemyX = 8;
    let enemyY = 100;
    let count = 0;

    //Spawn each enemy next to each other in a formation and add that to a group
    for(let i = 0 ; i < 40; i++)
    {
      let enemy = this.add.sprite(0,0, "defEnemy");
      enemy.setOrigin(0,0);
      this.enemies.add(enemy);
      enemy.x = enemyX;
      enemy.y = enemyY;
      this.enemySpeed = 0;
      this.enemySpeedY = 0;
      this.notStarted = true;

      if(count < 9)
      {
        enemyX += 36;
        count ++;
      }

      else if(count == 9)
      {
        enemyY += 36;
        enemyX = 8;
        count = 0;
      }

    }
  

    //create enemy offense
    this.attEnemy = this.physics.add.sprite(180,50, "attEnemy");
    this.attEnemy.setOrigin(0,0);
    this.attEnemy.body.collideWorldBounds=true;
    this.attEnemy.bounce = 1;
    this.attEnemySpeed = 400;
    this.isAttackerDestroyed = 0;
  }

  update(time,delta)
  {
    this.destroyObjects();
      this.playerMovement();
      this.normalEnemMovement();
      this.playerMovementPad();

      if(this.isAttackerDestroyed === 0)
      {
      this.attEnemyMovement();
      if (time > this.enemTimer) // player is only allow to shoot once per certain amount of time.
          {
            this.enemTimer = time + Phaser.Math.Between(100,2000);
            this.enemShoot();
        }
      }
     
      if (this.shootBullet.isDown)
      {
         if (time > this.shootTimer) // player is only allow to shoot once per certain amount of time.
          {
            this.shootTimer = time + 900;
            this.playerShoot();
        }
      }

        this.pad3.on("pointerup", ()=>{
        if (time > this.shootTimer)
        {
        this.shootTimer = time + 900;
            this.playerShoot();
            }
      })

      if(this.playerLife <= 0)
      {
        this.scene.start("bootGame");
      }

      if(this.asteroids.getChildren().length < 20 && time > this.asteroidTime)
      {
      this.createAsteroid();
      this.asteroidTime = time + 5000;
      }

      this.resetAsteroid();

      if(this.score >= 4500)
      {
        this.scene.start("scoreScreen")
      }
  }

  normalEnemMovement()
  {
    this.enemies.children.each(enemy => 
      {
        if(this.notStarted === true)
        {
          this.enemySpeed = 100;
        this.enemySpeedY = 100;
        this.notStarted = false;
        }
  
        if(enemy.x > 340)
      {
        this.enemySpeed = -100;
      }
      else if(enemy.x < 2)
      {
        this.enemySpeed = 100;
      }
  
        if(this.enemies.getChildren().length > 10)
      {
        this.enemies.setVelocityX(this.enemySpeed);
      }

      else if(this.enemies.getChildren().length > 5 && this.enemies.getChildren().length <= 10)
      {
        this.enemies.setVelocityX(this.enemySpeed * 2);
      }

      else
      {
        this.enemies.setVelocityX(this.enemySpeed * 3);
      }

      if(enemy.y > 330)
      {
        this.enemySpeedY = -100;
      }
      else if(enemy.y < 100)
      {
        this.enemySpeedY = 100;
      }

      if(this.enemies.getChildren().length > 10)
      {
        this.enemies.setVelocityY(this.enemySpeedY);
      }

      else if(this.enemies.getChildren().length > 5 && this.enemies.getChildren().length <= 10)
      {
        this.enemies.setVelocityY(this.enemySpeedY * 2);
      }

      else
      {
        this.enemies.setVelocityY(this.enemySpeedY * 3);
      }

      });
  }

  createAsteroid()
  {
    let asteroid = this.add.sprite(0,0, "asteroid");
    asteroid.setOrigin(0,0);
    this.asteroids.add(asteroid);
    asteroid.x = Phaser.Math.Between(10,config.width);
    asteroid.y = -30;
    asteroid.body.velocity.y += 400;
  }

  resetAsteroid()
  {
    this.asteroids.children.each(asteroid => 
      {
          if (asteroid.y > 700)
          {
            asteroid.y = -30;
            asteroid.x = Phaser.Math.Between(10,config.width);
          }
      });
  }


  attEnemyMovement()
  {
      if(this.attEnemy.x > 330)
      {
        this.attEnemySpeed = -400;
      }
      else if(this.attEnemy.x < 20)
      {
        this.attEnemySpeed = 400;
      }
      this.attEnemy.setVelocityX(this.attEnemySpeed);
  }

  enemShoot()
  {

    let enemBullet = this.add.sprite(this.attEnemy.x , this.attEnemy.y + 30, "enemBullet");
    enemBullet.setOrigin(0,0);
    this.enemBulletGp.add(enemBullet);
      
    if (enemBullet)
    {
      enemBullet.body.velocity.y += 600;
    }
  }

  playerMovementPad()
  {
    this.pad2.on("pointerdown", ()=>
    {
      this.leftPressed = true;
    })

    this.pad1.on("pointerdown", ()=>
    {
      this.rightPressed = true;
    })

    this.pad2.on("pointerup", ()=>
    {
      this.leftPressed = false;
    })

    this.pad1.on("pointerup", ()=>
    {
      this.rightPressed = false;
    })

    if(this.leftPressed === true)
    {
      this.player.setVelocityX(-gameSettings.playerSpeed);
    }

    if(this.rightPressed === true)
    {
      this.player.setVelocityX(gameSettings.playerSpeed);
    }
  }


  playerMovement()
  {
    if(this.cursors.left.isDown)
    {
      this.player.setVelocityX(-gameSettings.playerSpeed);
    }
    else if(this.cursors.right.isDown)
    {
      this.player.setVelocityX(gameSettings.playerSpeed);
    }
    else
    {
      this.player.setVelocityX(0);
    }

  }

  playerShoot()
  {
    let bullet = this.add.sprite(this.player.x , this.player.y - 30, "bullet");
      bullet.setOrigin(0,0);
    this.bulletGroup.add(bullet);
      
    if (bullet)
    {
      bullet.body.velocity.y -= 600;
    }
  }

  destroyObjects()
  {
    this.physics.add.overlap(this.bulletGroup, this.enemies, (bullet, enemy) => {
            
            bullet.destroy();
            enemy.destroy();
            
        this.score += 100;
        });

        this.physics.add.overlap(this.bulletGroup, this.asteroids, (bullet, asteroid) => {
            
            asteroid.destroy();
            bullet.destroy();
        });

        this.physics.add.overlap(this.attEnemy, this.bulletGroup, (attEnemy, bullet) => {

          this.isAttackerDestroyed = 1;
          if(this.isAttackerDestroyed === 1)
          {
            attEnemy.destroy();
          }
            bullet.destroy();
        this.score += 500;
        });


        this.physics.add.overlap(this.player,this.enemBulletGp , (player, enemBullet) => {

          enemBullet.destroy();
          this.playerLife -= 1;
        });

        this.physics.add.overlap(this.player,this.asteroids , (player, asteroid) => {

          asteroid.destroy();
          this.playerLife -= 1;
        });


    this.bulletGroup.children.each(bullet => 
      {
          if (bullet.y < -700)
          {
            bullet.destroy();
          }
      });

      this.enemBulletGp.children.each(enemBullet => 
      {
          if (enemBullet.y > 700)
          {
            enemBullet.destroy();
          }
      });

  }

}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
class Scene3 extends Phaser.Scene
{
  constructor()
  {
    super("scoreScreen");

  }

  create()
  {
    this.ebackground = this.add.image(0,0,"background");
    this.ebackground.setOrigin(0,0);

    let end = this.add.sprite(180,200,"finalScore");
    end.scaleX = 2;
    end.scaleY = 2;
    this.retryButton = this.add.image(180,300,"retry");
    
    this.retryButton.setInteractive();
    this.retryButton.on("pointerover", ()=>{
      this.retryButton.scaleX = 2;
      this.retryButton.scaleY = 2;
    })

    this.retryButton.on("pointerout", ()=>{
      this.retryButton.scaleX = 1;
      this.retryButton.scaleY = 1;
    })

    this.retryButton.on("pointerup", ()=>{
      this.scene.start("bootGame");
    })
    
    //Asteroids
    this.eAsteroids = this.physics.add.group();
    this.eAsteroidTime = 0.0;
    
    //enemies 01
    this.eEnemies = this.physics.add.group();
    let maxeEnemies = 5;
    let eEnemyX = 8;
    let eEnemyY = 100;
    let count = 0;

    for(let i = 0 ; i < maxeEnemies; i++)
    {
      let eEnemy = this.add.sprite(0,0, "defEnemy");
      eEnemy.setOrigin(0,0);
      this.eEnemies.add(eEnemy);
      eEnemy.x = eEnemyX;
      eEnemy.y = eEnemyY;
      this.eEnemySpeed = 0;
      this.eEnemySpeedY = 0;
      this.mNotStarted = true;

      if(count < 5)
      {
        eEnemyX += 36;
        count ++;
      }
    }


    //enemies 02
    this.eEnemies2 = this.physics.add.group();
    let maxeEnemies2 = 5;
    let eEnemyX2 = 8;
    let eEnemyY2 = 100;
    let count2 = 0;

    for(let i = 0 ; i < maxeEnemies2; i++)
    {
      let eEnemy2 = this.add.sprite(0,0, "defEnemy");
      eEnemy2.setOrigin(0,0);
      this.eEnemies2.add(eEnemy2);
      eEnemy2.x = eEnemyX2;
      eEnemy2.y = 400;
      this.eEnemySpeed2 = 0;
      this.eEnemySpeedY2 = 0;
      this.mNotStarted2 = true;

      if(count2 < 5)
      {
        eEnemyX2 += 36;
        count2 ++;
      }
    }   
  }

  update(time,delta)
  {
    if(this.eAsteroids.getChildren().length < 3 && time > this.eAsteroidTime)
      {
      this.createAsteroid();
      this.eAsteroidTime = time + 1000;
      }

      this.resetmAsteroid();
      this.displayEnemMovement();
      this.displayEnemMovement2();
  }

    createAsteroid()
  {
    let mAsteroid = this.add.sprite(0,0, "asteroid");
    mAsteroid.setOrigin(0,0);
    this.eAsteroids.add(mAsteroid);
    mAsteroid.x = Phaser.Math.Between(10,config.width);
    mAsteroid.y = -30;
    mAsteroid.body.velocity.y += 400;
  }

  resetmAsteroid()
  {
    this.eAsteroids.children.each(mAsteroid => 
      {
          if (mAsteroid.y > 700)
          {
            mAsteroid.y = -30;
            mAsteroid.x = Phaser.Math.Between(10,config.width);
          }
      });
  }


  displayEnemMovement()
  {
    this.eEnemies.children.each(eEnemy => 
      {
        if(this.mNotStarted === true)
        {
          this.eEnemySpeed = 100;
        this.mNotStarted = false;
        }
  
        if(eEnemy.x > 800)
      {
        this.eEnemySpeed = -100;
      }
      else if(eEnemy.x < -800)
      {
        this.eEnemySpeed = 100;
      }

        this.eEnemies.setVelocityX(this.eEnemySpeed);
      });
  }

  displayEnemMovement2()
  {
    this.eEnemies2.children.each(eEnemy2 => 
      {
        if(this.mNotStarted2 === true)
        {
          this.eEnemySpeed2 = -100;
        this.mNotStarted2 = false;
        }
  
        if(eEnemy2.x > 800)
      {
        this.eEnemySpeed2 = -100;
      }
      else if(eEnemy2.x < -800)
      {
        this.eEnemySpeed2 = 100;
      }

        this.eEnemies2.setVelocityX(this.eEnemySpeed2);
      });
  }
}

6.5.3. Breakout by Fatou Cissé (Phaser.js)

Breakout

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <!--tablets and mobiles friendly-->
  <script src=//cdn.jsdelivr.net/npm/phaser@3.23.0/dist/phaser.min.js></script>

  <!--scenes have to come before gameConfig or it won't work-->
  <script src="scene1.js"></script>
  <script src="scene2.js"></script>
  <script src="scene3.js"></script>
  <script src="gameConfig.js"></script>

  <link rel="stylesheet" href="style.css">

  <title>Breakout</title>
</head>
<body>

  <h1>Breakout</h1>
  <main id="game"></main>

  <footer>
    <h3>Ressources (Sounds):</h3>
    <hr> <!--little fance separation between the title of the footer and the following content-->

    <!--a grid-->
    <div class="gridContainer">
      <div class="item1"><b>Background music:</b>  Travis Scott type beat "Drama" (prod. The Machinist Beats) :</div>
      <div class="item3"><b>Destroyed blocks:</b> impact 11 by neezen. :</div>
      <div class="item5"><b>Game over (sound):</b> you died by nfsmaster821 :</div>
      <div class="item7"><b>Game over (voice):</b> gameover by makkuzu :</div>
      <div class="item9"><b>Game won:</b> rhodesmas level-up by shinephoenixstormcrow :</div>

      <div class="item2"><a href="https://soundcloud.com/machinistprod/free-travis-scott-type-beat-evil-prod-by"
          target="_blank">https://soundcloud.com/machinistprod/free-travis-scott-type-beat-evil-prod-by</a></div>
      <div class="item4"><a href="https://freesound.org/people/neezen./sounds/482126/"
          target="_blank">https://freesound.org/people/neezen./sounds/482126/</a></div>
      <div class="item6"><a href="https://freesound.org/people/nfsmaster821/sounds/483471/"
          target="_blank">https://freesound.org/people/nfsmaster821/sounds/483471/</a></div>
      <div class="item8"><a href="https://freesound.org/people/makkuzu/sounds/234257/"
          target="_blank">https://freesound.org/people/makkuzu/sounds/234257/</a></div>
      <div class="item10"><a href="https://freesound.org/people/shinephoenixstormcrow/sounds/337049/"
          target="_blank">https://freesound.org/people/shinephoenixstormcrow/sounds/337049/</a></div>
    </div>

  </footer>

</body>
</html>
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
body    {
  margin: 0;
  background: #000;
  color: white;
}

h1 {
  text-align: center;
}

#game {
  display: flex;  /*a flex container*/
  justify-content: center;
  align-items: center;
}

canvas {  /*the game*/
  display : block;
  border: 5px inset grey; /*defines a 3d inset grey border*/
  border-bottom-style: none;  /*the border's bottom line will not be rendered*/
  line-height: 100px;
  text-align: center;
  width: 1024px;
  height: 768px;
}

#warningLand {
  display: none;
}

h3 {
  margin-left: 4%;
}

footer {
  margin: 1% 10% 1% 10%;
  background-color: #B2B2B2;
  font-size: 20px;
  border: 5px inset black;
  padding-bottom: 10px;
}

.gridContainer {
  display: grid;
  grid-template-columns: auto auto;  /*sets the number of columns of the grid to two and their size to auto*/
  grid-gap: 1px;
}

.gridContainer > div {
  padding-left: 5%;
}

/*.item1 is already in the right place*/
.item3 { grid-area: 2 / 1; } /*puts .item3 in the second row first column (swipes with .item2)*/
.item5 { grid-area: 3 / 1; } /*puts .item5 in the second row first column (swipes with .item4) and so on for the others*/
.item7 { grid-area: 4 / 1; }
.item9 { grid-area: 5 / 1; }


@media screen and (max-width: 1024px) and (orientation: landscape) {

  canvas {
    width: 920px;
    height: 690px;
  }

  .gridContainer {
    grid-auto-columns: auto;  /*sets the number of columns of the grid to one and its size to auto*/
  }

  .item1 { grid-area: 1; }   /*puts .item1 in the first row*/
  .item2 { grid-area: 2; }   /*puts .item2 in the second row and so on for the others*/
  .item3 { grid-area: 3; }
  .item4 { grid-area: 4; }
  .item5 { grid-area: 5; }
  .item6 { grid-area: 6; }
  .item7 { grid-area: 7; }
  .item8 { grid-area: 8; }
  .item9 { grid-area: 9; }
  .item10 { grid-area: 10; }

}

@media screen and (max-width: 768px) and (orientation: portrait) {

  canvas {
    width: 680px;
    height: 535px;
  }

  .gridContainer {
    grid-template-columns: auto;
  }

  .item1 { grid-area: 1; }
  .item2 { grid-area: 2; }
  .item3 { grid-area: 3; }
  .item4 { grid-area: 4; }
  .item5 { grid-area: 5; }
  .item6 { grid-area: 6; }
  .item7 { grid-area: 7; }
  .item8 { grid-area: 8; }
  .item9 { grid-area: 9; }
  .item10 { grid-area: 10; }

}

@media screen and (max-width: 667px) and (orientation: landscape) {

  body {
    min-width: 667px;
    min-height: 375px;
  }

  canvas {
    width: 454px;
    height: 340px;
  }

  footer {
    margin: 1% 5% 1% 5%;
    font-size: 11px;
  }

  .gridContainer > div {
    padding-left: 3%;  /*so that the content has more space: reduces it from 5% to 3%*/
  }
}

@media screen and (max-width: 375px) and (orientation: portrait) {
  body {
    min-width: 375px;
    min-height: 667px;
  }

  canvas {
    width: 360px;
    height: 300px;
  }

  footer {
    margin: 1% 5% 1% 5%;
    font-size: 11px;
  }

  .gridContainer > div {
    padding-left: 3%;
  }

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
'use strict';

const gameConfig = {
  parent: "game",
  width: 1024,
  height: 768, 
  scene: [scene1, scene2, scene3],
  physics: {
    default: 'arcade',
    arcade: {
      debug: false,
      gravity: false //we don't want everything to fall, just the ball to fall
    }
  }
};

const game = new Phaser.Game(gameConfig);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class scene1 extends Phaser.Scene {
  constructor() {
    super("loadingScreen");

  }



  //used to preload the sounds and images into memory
  preload() {
    this.add.text(gameConfig.width /2, gameConfig.height /2, "Loading game...", {font: "30px Arial"}).setOrigin(0.5); //to let the user now the assets are loading

    this.load.image("paddle", "assets/paddle.png");
    this.load.image("ball", "assets/ball.png");
    this.load.image("blockB", "assets/blockBlue.png");
    this.load.image("blockG", "assets/blockGreen.png");
    this.load.image("blockO", "assets/blockOrange.png");
    this.load.image("blockP", "assets/blockPink.png");
    this.load.image("blockV", "assets/blockViolett.png");

    this.load.audio("bgMusic", ["assets/bgMusic.mp3"]);
    this.load.audio("destroySound", ["assets/destroy.mp3"]);
    this.load.audio("gameOver1", ["assets/gameOver.ogg"]);
    this.load.audio("gameOver2", ["assets/gameOver.mp3"]);
    this.load.audio("won", ["assets/win.mp3"]);

    this.load.image("retryUp", "assets/retryUp.png");
    this.load.image("retryOver", "assets/retryOver.png");
    this.load.image("retryDown", "assets/retryDown.png");

  }


  //used to add objects to the game
  create() {

    this.scene.start("gameScreen"); //starts the next scene, the game
  }


  //a loop that runs constantly
  update() {

  }
  
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
class scene2 extends Phaser.Scene {
  constructor() {
    super("gameScreen");

    //global variables
    this.paddle;
    this.ball;
    this.blockB;
    this.blockG;
    this.blockO;
    this.blockP;
    this.blockV;
    this.level = 1;
    this.score = 0;
    this.scoreText;
    this.lives = 3;
    this.livesText;
    this.bgMusic;
    this.destroySound;
    this.gameOverSound;
    this.gameOverSound2;
    this.wonSound;
    this.retryButton;
  }



  preload() {

  }


  //used to add objects to the game
  create() {
    //Adding and setting the background music
    this.bgMusic = this.sound.add("bgMusic");
    this.bgMusic.volume = 5;
    this.bgMusic.loop = true;
    this.bgMusic.play();



    this.physics.world.setBoundsCollision(true, true, true, false); //sets all the world's bounderies to true except for its bottom part

    this.paddle = this.physics.add.sprite(gameConfig.width/2, gameConfig.height-50, "paddle").setCollideWorldBounds(true).setImmovable(); //sets the paddle's image and coordinates and enables collisons, this should not move unless we say so
    this.ball = this.physics.add.sprite(this.paddle.x, this.paddle.y-100, "ball").setCollideWorldBounds(true).setBounce(1); ////sets the ball's image and coordinates and enables collisons, allows it to bounce
    this.ball.setData("onPaddle", true) //gives the ball a value, so that we know when it's on the paddle

    //displays the rows of blocks
    this.blockB = this.physics.add.group({
      key: "blockB",
      repeat: 10,   //number of blocks per row
      setXY: {
        x: gameConfig.width/9,
        y: gameConfig.height*(1/10),    //the height of the image is 48 so it creates space between the blocks vertically
        stepX: 81,   //space between the blocks horizontally
      },
      immovable: true
    })
    this.blockB.children.iterate((child) => {child.setScale(0.8, 0.8);}); //scales the blocks down a bit

    this.blockG = this.physics.add.group({
      key: "blockG",
      repeat: 10,
      setXY: {
        x: gameConfig.width/9,
        y: gameConfig.height*(1.5/10),
        stepX: 81
      },
      immovable: true
    })
    this.blockG.children.iterate((child) => {child.setScale(0.8, 0.8);});

    this.blockO = this.physics.add.group({
      key: "blockO",
      repeat: 10,
      setXY: {
        x: gameConfig.width/9,
        y: gameConfig.height*(2/10),
        stepX: 81
      },
      immovable: true
    })
    this.blockO.children.iterate((child) => {child.setScale(0.8, 0.8);});

    this.blockP = this.physics.add.group({
      key: "blockP",
      repeat: 10,
      setXY: {
        x: gameConfig.width/9,
        y: gameConfig.height*(2.5/10),
        stepX: 81
      },
      immovable: true
    })
    this.blockP.children.iterate((child) => {child.setScale(0.8, 0.8);});

    this.blockV = this.physics.add.group({
      key: "blockV",
      repeat: 10,
      setXY: {
        x: gameConfig.width/9,
        y: gameConfig.height*(3/10),
        stepX: 81
      },
      immovable: true
    })
    this.blockV.children.iterate((child) => {child.setScale(0.8, 0.8);});


    //Add collisions with the paddle and the blocks
    this.physics.add.collider(this.ball, this.paddle, this.hitPaddle, null, this);
    this.physics.add.collider(this.ball, this.blockB, this.destroyBlock, null, this);
    this.physics.add.collider(this.ball, this.blockG, this.destroyBlock, null, this);
    this.physics.add.collider(this.ball, this.blockO, this.destroyBlock, null, this);
    this.physics.add.collider(this.ball, this.blockP, this.destroyBlock, null, this);
    this.physics.add.collider(this.ball, this.blockV, this.destroyBlock, null, this);



    //Input events
    this.input.on('pointermove', function(pointer) {
      this.paddle.x = Phaser.Math.Clamp(pointer.x, 82.5, gameConfig.width-82.5);  //keeps the paddle within the game
        //keeps the ball on the paddle before launch
        if (this.ball.getData('onPaddle'))
        {
            this.ball.x = this.paddle.x;
        }
    }, this);

    this.input.on('pointerup', function (pointer) {
      startText.setVisible(false);  //the user doesn't need this help anymore
      if (this.ball.getData('onPaddle'))
      {
          this.ball.setVelocity(0, -600); //to launch the ball straight up
          this.ball.setData('onPaddle', false); //it's not on the paddle anymore
      }
    }, this);

    //HUD
    this.scoreText = this.createText(gameConfig.width * (1/30), gameConfig.height/70, "Score: " + this.score);
    this.livesText = this.createText(gameConfig.width * (29/30), gameConfig.height/70, "Lives: " + this.lives).setOrigin(1, 0);
    let startText = this.createText(gameConfig.width/2, gameConfig.height/2,"Right-clic to lauch the ball", "60px").setOrigin(0.5);

    //retry/replay button
    this.retryButton =  this.add.sprite(gameConfig.width/2, gameConfig.height * (2.7/4), "retryUp").setOrigin(0.5).setVisible(false);

  }


  //destroyes the blocks when hit by the ball
  destroyBlock(ball, block)
  {

    //destroyed blocks sound settings
    this.destroySound = this.sound.add("destroySound");
    this.destroySound.volume = 0.6;
    this.destroySound.play();
    block.disableBody(true, true);

    //updates the score
    this.score += 10;
    this.scoreText.setText("Score: " + this.score);

    //manages the end of the game
    let countBlock = this.blockB.countActive() + this.blockG.countActive() + this.blockO.countActive() + this.blockP.countActive() + this.blockV.countActive(); //counts the number of blocks remaining
    if(countBlock === 0) {
      this.bgMusic.stop();
      this.wonSound = this.sound.add("won");
      this.wonSound.play();
      this.scene.start("endScreen", [this.score, this.lives]);  //starts the next scene, exports the score and the lives variables
    }

  }

  //sets the balls behaviour when hitting the paddle
  hitPaddle(ball, paddle) {

    let diff = 0; //the difference between the ball's position and the paddle's
    let cte = -600; //the vertical velocity of the ball

    if (ball.x < paddle.x) {  //the ball is on the left-hand side of the paddle
      //the further away from the center of the paddle the ball is the more the velocity is increased on the x axis
      diff = paddle.x - ball.x;
      ball.setVelocityX(-14 * diff).setVelocityY(cte);  //the ball will go left
    }
    else if (ball.x > paddle.x) { //the ball is on the right-hand side of the paddle
      //the further away from the center of the paddle the ball is the more the velocity is increased on the x axis
      diff = ball.x - paddle.x;
      ball.setVelocityX(14 * diff).setVelocityY(cte); // The ball will go left
    }
    else {  //the ball is in the middle of the paddle and just bounces
      ball.setVelocityX(0).setVelocityY(cte); // The ball go straight up
    }

  }

  //to create similar texts easily, used to create the HUD
  createText(x, y, text, size = "30px") {

    return this.add.text(
      x,
      y,
      text,
      {
        fontSize: size,
        fontFamily: "Arial"
      }
    )

  }


  //a loop that runs constantly
  update() {

    //when the ball falls in the bottom off the screen
    if(this.ball.y > 750 && this.lives > 1) {
      this.lives--; //the user loses one life point
      this.livesText.setText("Lives: " + this.lives); //updates the HUD
      this.resetBall();
    }
    //losing condition
    else if(this.ball.y > 750) {
      if(this.lives === 1) {
        //sets first game over sound
        this.gameOverSound = this.sound.add("gameOver1");
        this.gameOverSound.play();

        this.bgMusic.stop();  //stops the backgroung music

        //updates the number of lives left
        this.lives = 0
        this.livesText.setText("Lives: " + this.lives);

        //sets a game over text appearing after one second of delay
        let gameOverText = this.createText(gameConfig.width/2, gameConfig.height * (2/4), "GAME OVER", "85px").setOrigin(0.5).setVisible(false);
         this.time.addEvent({
          delay: 1000,
          callback: () => {
            gameOverText.setVisible(true);
            }
          })

        //sets the second game over sound with a delay of one second
        let soundConfig = {
          delay: 1
        }
        this.gameOverSound2 = this.sound.add("gameOver2");
        this.gameOverSound2.play(soundConfig);

        //sets the retry button after 3 seconds of delay
        this.time.addEvent({
         delay: 3000,
         callback: () => {
           this.retryButton.setVisible(true);
           this.addRetryButn();
           }
         })

      }

    }
  }


  //setting the retry button
  addRetryButn() {
    this.retryButton.setInteractive();  //so that we can access the over, out, up and down properties
    this.retryButton.on('pointerover', () => {
      this.retryButton = this.add.sprite(gameConfig.width/2, gameConfig.height * (2.7/4), "retryOver"); //when overed the button changes for a new sprite
    })
    this.retryButton.on('pointerout', () => {
      this.retryButton = this.add.sprite(gameConfig.width/2, gameConfig.height * (2.7/4), "retryUp"); //if not overed anymore the button changes back to the initial sprite
    })
    this.retryButton.on('pointerdown', () => {
      this.retryButton = this.add.sprite(gameConfig.width/2, gameConfig.height * (2.7/4), "retryDown"); //when clicked it changes one more time to the last sprite

      //so that the user has the time to see the sprite change before resetting the game
      this.time.addEvent({
       delay: 200,  //delay in millisenconds
       callback: () => {
         this.resetScene();
         }
       })
    })

  }

  //puts the ball back on the paddle
  resetBall() {
    this.ball.setVelocity(0);
    this.ball.setPosition(this.paddle.x, this.paddle.y-100);  //the ball will not be touching the paddle
    this.ball.setData('onPaddle', true);
  }

  //resetting the game when the user clicks on the retry button
  resetScene() {
    this.resetBall();
    this.score = 0;   //puts the score back to it's initial value
    this.lives = 3;  //puts the lives variable back to it's initial value
    this.scene.start("gameScreen");   //restarts the scene, itself
  }

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class scene3 extends Phaser.Scene {
  constructor() {
    super("endScreen");

  }


  preload() {

  }

  //used to add objects to the game
  create([score, lives]) {  //imports the score and lives from the prior scene, the game

    //Adding different delays to display the final score over time
    this.time.addEvent({
     delay: 500,
     callback: () => {
       this.add.text(gameConfig.width * (1/4), gameConfig.height * (1/10), "Lives: " + lives, {font: "20px Arial"}).setOrigin(0, 0.5);
       }
     })
    this.time.addEvent({
      delay: 1000,
      callback: () => {
        this.add.text(gameConfig.width * (3/4), gameConfig.height * (1/10),  "+" + lives * 10, {font: "20px Arial"}).setOrigin(1, 0.5);
      }
    })
    this.time.addEvent({
       delay: 1500,
       callback: () => {
         this.add.text(gameConfig.width * (1/4), gameConfig.height * (1.5/10), "Score: " + score, {font: "20px Arial"}).setOrigin(0, 0.5);
       }
    })
    this.time.addEvent({
      delay: 2000,
      callback: () => {
        this.add.text(gameConfig.width * (3/4), gameConfig.height * (1.5/10),  "+" + score, {font: "20px Arial"}).setOrigin(1, 0.5);
        }
    })
    this.time.addEvent({
      delay: 3000,
      callback: () => {
        this.add.text(gameConfig.width / 2, gameConfig.height / 2, "Your final score is: " + (score + lives * 10), {font: "60px Arial"}).setOrigin(0.5);
      }
    })

  }


  update() {

  }
  
}

6.5.4. Dodg’JS by Michel Houche

DodgJS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>DODG'JS</title>
    <meta charset="UTF-8">
    <meta name=viewport content="width=device-width, initial-scale=1.0">
    <link rel="shortcut icon" href="favicon.ico">
  
  
    <style>
      html, body {
        width:  100%;
        height: 100%;
        margin: 0;
        background-color: #000;
      }
      
      div{
        position: fixed;
        width:  100%;
        height: 100%;
        margin: 0;
        background-color: #aaa;
      }
      

      
      @media only screen and (orientation:portrait){
        #turn{ display:block; }
        #container{ display:none; }
      }
      @media only screen and (orientation:landscape){
        #turn{ display:none; }
        #container{ display:block; }
      }
  
      .center {
        display: block;
        margin-left: auto;
        margin-right: auto;
        width: 50%;
      }
      
    </style>
    
    
  </head>
  <body>
    <div id="turn">
      <img src="assets/images/rotate.png" alt="Please rotate" class="center">
      <p>
        <a href="https://jigsaw.w3.org/css-validator/check/referer">
          <img style="border:0;width:88px;height:31px"
            src="https://jigsaw.w3.org/css-validator/images/vcss"
            alt="Valid CSS!" />
        </a>
      </p>
      <p>
        <a href="https://jigsaw.w3.org/css-validator/check/referer">
          <img style="border:0;width:88px;height:31px"
            src="https://jigsaw.w3.org/css-validator/images/vcss-blue"
            alt="Valid CSS!" />
        </a>
      </p>
    </div>
    <main id="container">
      <script src="./js/phaser.js"></script>
      <script src="./js/main.js"></script>
      
    </main>
  </body>
</html>
   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
// ===========================================================
// PHASER CONFIG:
// -----------------------------------------------------------
var config = {
  type: Phaser.AUTO,

  scale: {
    mode: Phaser.Scale.FIT,
    autoCenter: Phaser.Scale.CENTER_BOTH,
    width: 1280,
    height: 800,
  },
  
  physics: {
    default: 'arcade',
    arcade: {
      gravity: { y: 0 },
      debug: false
    }
  },
  scene: {
    preload: preload,
    create: create,
    update: update
  }
};

var game = new Phaser.Game(config);

// ===========================================================
// CREATION OF GLOBAL VARIABLES:
// -----------------------------------------------------------
// Keyboard related:
var keys;       // keyboard inputs
var spaceKey;     // space key input
// -----------------------------------------------------------
// Mobile related:
var alphaUntouched;   // Mobile: how strong is the alpha for buttons that are not touched?
var pressedLeft;    // Mobile: has the "left" button been touched?
var pressedRight;   // Mobile: has the "Right" button been touched?
var pressedUp;      // Mobile: has the "Up" button been touched?
var pressedDown;    // Mobile: has the "Down" button been touched?
var go;         // Mobile: image of the turbo button
var left;       // Mobile: image of the left button
var right;        // Mobile: image of the right button
var up;         // Mobile: image of the up button
var down;       // Mobile: image of the down button
var pressedGo;      // Mobile: has the "turbo" button been touched?
// -----------------------------------------------------------
// Player related:
var playerCar;      // The sprite of the players car
var playerLane;     // In which line is the player's car driving?
var playerSize;     // How big (in px) is the height and width of the player's car
var playerScore;    // Points collected by player
var timer;        // Helper variable that avoids duplicated rotation within a few frames
var timer_cl;     // Helper variable that avoids duplicated lane changes within a few frames
var carSpeed;     // The very first speed the player's car get
var actualSpeedX;   // The actual horizontal speed of the player's car
var actualSpeedY;   // The actual vertical speed of the player's car
var score;        // Literaly the text "score" shown through a bitmap font
var playerLives;    // Amount of player's lives left
var lives;        // Literaly the text "lives" shown
var isCrashed;      // Has the player crash his car against the enemy?
var dontMove      // If true, the player won't be able to move the player's car
var normalSpeed;    // This is the speed used, when the player is not pressing the space key
var turboSpeed;     // This is the speed used, when the player presses the space key
var pressedCL;      // Did the player press the right button to change lane?
var isBoost;      // Is the player using the turbo?
// -----------------------------------------------------------
// Opponent/Enemy related:
var enemySize;      // How big (in px) is the height and width of the enemy's car
var enemyLane;      // In which line is the enemy's car driving?
var eTimer;       // Same as "timer" but for the enemy
var eCarSpeed;      // The very first speed the enemy's car get
var eActualSpeedX;    // The actual horizontal speed of the enemy's car
var eActualSpeedY;    // The actual vertical speed of the enemy's car
var eChangedLane;   // Boolean that helps restricting the enemy to one lane change per intersection
// -----------------------------------------------------------
//SFX:
var emitter;      // Emitting effect for the player's car
var particles;      // Sprites used for the emitter
// -----------------------------------------------------------
// Graphics & fonts related:
var isGameOver;     // Is the game over?
var gameOverText;   // Showing bitmap font for "Game over" 
// -----------------------------------------------------------
// Triggers:
var triggerTurn;    // Triggers that are in every corner. Makes all cars automaticaly rotate correctly
var triggerChangeLane;  // Triggers that are in every intersection. Permits a lane change if needed.
// --- All these triggers are used to detect if a specific "checkpoint" has been touched by the player:
var touchedTriggerOuterL;
var touchedTriggerOuterLD;
var touchedTriggerOuterD;
var touchedTriggerOuterDR;
var touchedTriggerOuterR;
var touchedTriggerOuterRU;
var touchedTriggerOuterU;
var touchedTriggerOuterUL;
var touchedTriggerMiddle1L;
var touchedTriggerMiddle1LD;
var touchedTriggerMiddle1D;
var touchedTriggerMiddle1DR;
var touchedTriggerMiddle1R;
var touchedTriggerMiddle1RU;
var touchedTriggerMiddle1U;
var touchedTriggerMiddle1UL;
var touchedTriggerMiddle2L;
var touchedTriggerMiddle2LD;
var touchedTriggerMiddle2D;
var touchedTriggerMiddle2DR;
var touchedTriggerMiddle2R;
var touchedTriggerMiddle2RU;
var touchedTriggerMiddle2U;
var touchedTriggerMiddle2UL;
var touchedTriggerInnerL;
var touchedTriggerInnerLD;
var touchedTriggerInnerD;
var touchedTriggerInnerDR;
var touchedTriggerInnerR;
var touchedTriggerInnerRU;
var touchedTriggerInnerU;
var touchedTriggerInnerUL;
// -----------------------------------------------------------
// Others:
var resetButton;    // An image of a reset button
var resetText;      // Showing bitmap font for the "reset button" 
var wall;       // Walls or separators between lanes
var isGameStarted;    // Can the player play the game?
var borderX;      // The variable changes the size of what lies left to the game
// -----------------------------------------------------------

// ===========================================================
// PRELOADING:
// -----------------------------------------------------------
function preload ()
{
  this.load.image('player', 'assets/images/player.png');
  this.load.image('enemy', 'assets/images/opponent.png');
  this.load.image('player_crash', 'assets/images/player_crash.png');
  this.load.image('enemy_crash', 'assets/images/opponent_crash.png');
  this.load.image('smoke', 'assets/images/smoke.png');
  this.load.image('wall', 'assets/images/wall.png');
  this.load.image('triggerTurn', 'assets/images/trigger.png');
  this.load.image('triggerChangeLane', 'assets/images/trigger.png');
  this.load.image('trigger', 'assets/images/trigger.png');
  this.load.image('go', 'assets/images/go.png');
  this.load.image('left', 'assets/images/left.png');
  this.load.image('up', 'assets/images/up.png');
  this.load.image('right', 'assets/images/right.png');
  this.load.image('down', 'assets/images/down.png');
  this.load.image('resetButton', 'assets/images/reset.png');
  this.load.image('drivenPart_outer', 'assets/images/drivenPart_outer.png');
  this.load.image('drivenPart_middle1', 'assets/images/drivenPart_middle1.png');
  this.load.image('drivenPart_middle2', 'assets/images/drivenPart_middle2.png');
  this.load.image('drivenPart_inner', 'assets/images/drivenPart_inner.png');
  this.load.bitmapFont('atari', 'assets/fonts/arcade.png', 'assets/fonts/arcade.fnt');
}

// ===========================================================
// CREATING:
// -----------------------------------------------------------
// Note: the order of most of the following components are important are should not be changed:
function create ()
{ 
  gameOver = false;
  dontMove = false;
  
  playerScore = 0;
  playerLives = 3;
  crashed = false;
  
  var scoreText = this.add.bitmapText(1120,100, 'atari', '', 32);
  scoreText.setText('Score');
  
  score = this.add.bitmapText(1120,130, 'atari', '', 32);
  score.setText(playerScore);
  
  var livesText = this.add.bitmapText(1120,190, 'atari', '', 32);
  livesText.setText('Lives');
  
  lives = this.add.bitmapText(1120,220, 'atari', '', 32);
  lives.setText(playerLives);
  
  pressTurbo = this.add.bitmapText(1120,500, 'atari', '', 32);
  pressTurbo.setText('Press\nturbo\nto\nstart');
  
  resetText = this.add.bitmapText(0,300, 'atari', '', 32).setAlpha(0.0);
  resetText.setText('Press\nreset\nto\nretry');
  
  this.scale.startFullScreen;
  
  this.input.addPointer(1);
  this.input.addPointer(1);
  
  borderX = 160;
  playerSize = 80;
  enemySize = 80;
  
  resetAllTriggers();
  
  touchedTriggerMiddle1L = true;
  
  pressedCL = false;
  pressedLeft = false;
  pressedRight = false;
  pressedUp = false;
  pressedDown = false;
  
  startGame = false;
  
  timer = this.time.now;
  eTimer = this.time.now;

  carSpeed = 0;
  normalSpeed = 300;
  turboSpeed = 700;
  
  actualSpeedX = 0;
  actualSpeedY = 0;
  pressedSpace = false;
  boost = false;

  eCarSpeed = 0;
  eNormalSpeed = 200;
  eActualSpeedX = 0;
  eActualSpeedY = 0;
  eChangedLane = false;

  drivenPart_outerLD = this.add.image(417,553,'drivenPart_outer');
  drivenPart_outerDR = this.add.image(862,553,'drivenPart_outer');
  drivenPart_outerRU = this.add.image(862,207,'drivenPart_outer');
  drivenPart_outerUL = this.add.image(417,207,'drivenPart_outer');
  drivenPart_middle1LD = this.add.image(457,513,'drivenPart_middle1');
  drivenPart_middle1DR = this.add.image(822,513,'drivenPart_middle1');
  drivenPart_middle1RU = this.add.image(822,247,'drivenPart_middle1');
  drivenPart_middle1UL = this.add.image(457,247,'drivenPart_middle1');
  drivenPart_middle2LD = this.add.image(497,473,'drivenPart_middle2');
  drivenPart_middle2DR = this.add.image(783,473,'drivenPart_middle2');
  drivenPart_middle2RU = this.add.image(783,287,'drivenPart_middle2');
  drivenPart_middle2UL = this.add.image(497,287,'drivenPart_middle2');
  drivenPart_innerLD = this.add.image(537,433,'drivenPart_inner');
  drivenPart_innerDR = this.add.image(743,433,'drivenPart_inner');
  drivenPart_innerRU = this.add.image(743,327,'drivenPart_inner');
  drivenPart_innerUL = this.add.image(537,327,'drivenPart_inner');
  
  resetAllDrivenParts();
  
  drivenPart_outerLD.flipY = true;
  drivenPart_outerDR.flipX = true;
  drivenPart_outerDR.flipY = true;
  drivenPart_outerRU.flipX = true;
  drivenPart_outerUL.flipY = false;
  drivenPart_middle1LD.flipY = true;
  drivenPart_middle1DR.flipX = true;
  drivenPart_middle1DR.flipY = true;
  drivenPart_middle1RU.flipX = true;
  drivenPart_middle1UL.flipY = false;
  drivenPart_middle2LD.flipY = true;
  drivenPart_middle2DR.flipX = true;
  drivenPart_middle2DR.flipY = true;
  drivenPart_middle2RU.flipX = true;
  drivenPart_middle2UL.flipY = false;
  drivenPart_innerLD.flipY = true;
  drivenPart_innerDR.flipX = true;
  drivenPart_innerDR.flipY = true;
  drivenPart_innerRU.flipX = true;
  drivenPart_innerUL.flipY = false;
  
  particles = this.add.particles('smoke');
  emitter = particles.createEmitter({
    speed: 50,
    scale: { start: 2, end: 0 },
    blendMode: 'ADD'
  });
  
  
  playerCar = this.physics.add.sprite(155+borderX, 420, 'player');
  playerCar.angle = 270;
  playerCar.setVelocity(actualSpeedX, actualSpeedY);
  playerCar.scale = 0.99;
  playerCar.setCollideWorldBounds(true);
  
  enemy = this.physics.add.sprite(155+borderX, 280, 'enemy');
  enemy.angle = 90;
  enemy.setVelocity(eActualSpeedX, eActualSpeedY);
  enemy.scale = 0.99;
  enemy.setCollideWorldBounds(true);
  
  keys = this.input.keyboard.createCursorKeys();
  spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

  emitter.startFollow(playerCar);
  emitter.stop();
  
  wall = this.physics.add.staticGroup();
  buildWalls();
  
  gameOverText = this.add.bitmapText(350,350, 'atari', '', 64).setAlpha(0.0);
  gameOverText.setText('Game over');
  
  triggerTurn = this.physics.add.staticGroup();
  buildTriggerTurn();
  
  this.physics.add.collider(playerCar, wall);
  this.physics.add.overlap(playerCar,triggerTurn,rotateCar, null, this);
  
  this.physics.add.collider(enemy, wall);
  this.physics.add.overlap(enemy,triggerTurn,rotateECar, null, this);
  this.physics.add.overlap(playerCar,enemy,crash, null, this);
  
// --- Player triggers and overlapings:
  trigger_cl_outerL = this.physics.add.staticGroup();
  trigger_outerLD = this.physics.add.staticGroup();
  trigger_cl_outerD = this.physics.add.staticGroup();
  trigger_outerDR = this.physics.add.staticGroup();
  trigger_cl_outerR = this.physics.add.staticGroup();
  trigger_outerRU = this.physics.add.staticGroup();
  trigger_cl_outerU = this.physics.add.staticGroup();
  trigger_outerUL = this.physics.add.staticGroup();
  trigger_cl_middle1L =this.physics.add.staticGroup();
  trigger_middle1LD =this.physics.add.staticGroup();
  trigger_cl_middle1D =this.physics.add.staticGroup();
  trigger_middle1DR =this.physics.add.staticGroup();
  trigger_cl_middle1R =this.physics.add.staticGroup();
  trigger_middle1RU =this.physics.add.staticGroup();
  trigger_cl_middle1U =this.physics.add.staticGroup();
  trigger_middle1UL =this.physics.add.staticGroup();
  trigger_cl_middle2L =this.physics.add.staticGroup();
  trigger_middle2LD =this.physics.add.staticGroup();
  trigger_cl_middle2D =this.physics.add.staticGroup();
  trigger_middle2DR =this.physics.add.staticGroup();
  trigger_cl_middle2R =this.physics.add.staticGroup();
  trigger_middle2RU =this.physics.add.staticGroup();
  trigger_cl_middle2U =this.physics.add.staticGroup();
  trigger_middle2UL =this.physics.add.staticGroup();
  trigger_cl_innerL = this.physics.add.staticGroup();
  trigger_innerLD = this.physics.add.staticGroup();
  trigger_cl_innerD = this.physics.add.staticGroup();
  trigger_innerDR = this.physics.add.staticGroup();
  trigger_cl_innerR = this.physics.add.staticGroup();
  trigger_innerRU = this.physics.add.staticGroup();
  trigger_cl_innerU = this.physics.add.staticGroup();
  trigger_innerUL = this.physics.add.staticGroup();
  
  trigger_cl_outerL.create(75+borderX,380,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_outerLD.create(75+borderX,685,'trigger').setScale(1,1).refreshBody();
  trigger_cl_outerD.create(480+borderX,685,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_outerDR.create(885+borderX,685,'trigger').setScale(1,1).refreshBody();
  trigger_cl_outerR.create(885+borderX,380,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_outerRU.create(885+borderX,75,'trigger').setScale(1,1).refreshBody();
  trigger_cl_outerU.create(480+borderX,75,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_outerUL.create(75+borderX,75,'trigger').setScale(1,1).refreshBody();
  trigger_cl_middle1L.create(155+borderX,380,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_middle1LD.create(155+borderX,605,'trigger').setScale(1,1).refreshBody();
  trigger_cl_middle1D.create(480+borderX,605,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_middle1DR.create(805+borderX,605,'trigger').setScale(1,1).refreshBody();
  trigger_cl_middle1R.create(805+borderX,380,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_middle1RU.create(805+borderX,155,'trigger').setScale(1,1).refreshBody();
  trigger_cl_middle1U.create(480+borderX,155,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_middle1UL.create(155+borderX,155,'trigger').setScale(1,1).refreshBody();
  trigger_cl_middle2L.create(235+borderX,380,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_middle2LD.create(235+borderX,525,'trigger').setScale(1,1).refreshBody();
  trigger_cl_middle2D.create(480+borderX,525,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_middle2DR.create(725+borderX,525,'trigger').setScale(1,1).refreshBody();
  trigger_cl_middle2R.create(725+borderX,380,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_middle2RU.create(725+borderX,235,'trigger').setScale(1,1).refreshBody();
  trigger_cl_middle2U.create(480+borderX,235,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_middle2UL.create(235+borderX,235,'trigger').setScale(1,1).refreshBody();
  trigger_cl_innerL.create(315+borderX,380,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_innerLD.create(315+borderX,445,'trigger').setScale(1,1).refreshBody();
  trigger_cl_innerD.create(480+borderX,445,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_innerDR.create(645+borderX,445,'trigger').setScale(1,1).refreshBody();
  trigger_cl_innerR.create(645+borderX,380,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_innerRU.create(645+borderX,315,'trigger').setScale(1,1).refreshBody();
  trigger_cl_innerU.create(480+borderX,315,'triggerChangeLane').setScale(1,1).refreshBody();
  trigger_innerUL.create(315+borderX,315,'trigger').setScale(1,1).refreshBody();
  
  this.physics.add.overlap(playerCar,trigger_cl_outerL, cl_outerL, null, this);
  this.physics.add.overlap(playerCar,trigger_outerLD, t_outerLD, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_outerD, cl_outerD, null, this);
  this.physics.add.overlap(playerCar,trigger_outerDR, t_outerDR, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_outerR, cl_outerR, null, this);
  this.physics.add.overlap(playerCar,trigger_outerRU, t_outerRU, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_outerU, cl_outerU, null, this);
  this.physics.add.overlap(playerCar,trigger_outerUL, t_outerUL, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_middle1L,cl_middle1L, null, this);
  this.physics.add.overlap(playerCar,trigger_middle1LD,t_middle1LD, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_middle1D,cl_middle1D, null, this);
  this.physics.add.overlap(playerCar,trigger_middle1DR,t_middle1DR, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_middle1R,cl_middle1R, null, this);
  this.physics.add.overlap(playerCar,trigger_middle1RU,t_middle1RU, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_middle1U,cl_middle1U, null, this);
  this.physics.add.overlap(playerCar,trigger_middle1UL,t_middle1UL, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_middle2L,cl_middle2L, null, this);
  this.physics.add.overlap(playerCar,trigger_middle2LD,t_middle2LD, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_middle2D,cl_middle2D, null, this);
  this.physics.add.overlap(playerCar,trigger_middle2DR,t_middle2DR, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_middle2R,cl_middle2R, null, this);
  this.physics.add.overlap(playerCar,trigger_middle2RU,t_middle2RU, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_middle2U,cl_middle2U, null, this);
  this.physics.add.overlap(playerCar,trigger_middle2UL,t_middle2UL, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_innerL, cl_innerL, null, this);
  this.physics.add.overlap(playerCar,trigger_innerLD, t_innerLD, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_innerD, cl_innerD, null, this);
  this.physics.add.overlap(playerCar,trigger_innerDR, t_innerDR, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_innerR, cl_innerR, null, this);
  this.physics.add.overlap(playerCar,trigger_innerRU, t_innerRU, null, this);
  this.physics.add.overlap(playerCar,trigger_cl_innerU, cl_innerU, null, this);
  this.physics.add.overlap(playerCar,trigger_innerUL, t_innerUL, null, this);

// --- Enemy triggers and overlapings:
  this.physics.add.overlap(enemy,trigger_cl_outerL, cl_eOuterL, null, this);
  this.physics.add.overlap(enemy,trigger_outerLD, t_eOuterLD, null, this);
  this.physics.add.overlap(enemy,trigger_cl_outerD, cl_eOuterD, null, this);
  this.physics.add.overlap(enemy,trigger_outerDR, t_eOuterDR, null, this);
  this.physics.add.overlap(enemy,trigger_cl_outerR, cl_eOuterR, null, this);
  this.physics.add.overlap(enemy,trigger_outerRU, t_eOuterRU, null, this);
  this.physics.add.overlap(enemy,trigger_cl_outerU, cl_eOuterU, null, this);
  this.physics.add.overlap(enemy,trigger_outerUL, t_eOuterUL, null, this);
  this.physics.add.overlap(enemy,trigger_cl_middle1L,cl_eMiddle1L, null, this);
  this.physics.add.overlap(enemy,trigger_middle1LD,t_eMiddle1LD, null, this);
  this.physics.add.overlap(enemy,trigger_cl_middle1D,cl_eMiddle1D, null, this);
  this.physics.add.overlap(enemy,trigger_middle1DR,t_eMiddle1DR, null, this);
  this.physics.add.overlap(enemy,trigger_cl_middle1R,cl_eMiddle1R, null, this);
  this.physics.add.overlap(enemy,trigger_middle1RU,t_eMiddle1RU, null, this);
  this.physics.add.overlap(enemy,trigger_cl_middle1U,cl_eMiddle1U, null, this);
  this.physics.add.overlap(enemy,trigger_middle1UL,t_eMiddle1UL, null, this);
  this.physics.add.overlap(enemy,trigger_cl_middle2L,cl_eMiddle2L, null, this);
  this.physics.add.overlap(enemy,trigger_middle2LD,t_eMiddle2LD, null, this);
  this.physics.add.overlap(enemy,trigger_cl_middle2D,cl_eMiddle2D, null, this);
  this.physics.add.overlap(enemy,trigger_middle2DR,t_eMiddle2DR, null, this);
  this.physics.add.overlap(enemy,trigger_cl_middle2R,cl_eMiddle2R, null, this);
  this.physics.add.overlap(enemy,trigger_middle2RU,t_eMiddle2RU, null, this);
  this.physics.add.overlap(enemy,trigger_cl_middle2U,cl_eMiddle2U, null, this);
  this.physics.add.overlap(enemy,trigger_middle2UL,t_eMiddle2UL, null, this);
  this.physics.add.overlap(enemy,trigger_cl_innerL, cl_eInnerL, null, this);
  this.physics.add.overlap(enemy,trigger_innerLD, t_eInnerLD, null, this);
  this.physics.add.overlap(enemy,trigger_cl_innerD, cl_eInnerD, null, this);
  this.physics.add.overlap(enemy,trigger_innerDR, t_eInnerDR, null, this);
  this.physics.add.overlap(enemy,trigger_cl_innerR, cl_eInnerR, null, this);
  this.physics.add.overlap(enemy,trigger_innerRU, t_eInnerRU, null, this);
  this.physics.add.overlap(enemy,trigger_cl_innerU, cl_eInnerU, null, this);
  this.physics.add.overlap(enemy,trigger_innerUL, t_eInnerUL, null, this);

  alphaUntouched = 0.3;
  
  this.go = this.add.sprite(960+borderX+80, 720, 'go').setAlpha(alphaUntouched).setInteractive({ draggable: true });
  this.left = this.add.sprite(80, 720, 'left').setAlpha(alphaUntouched).setInteractive();
  this.right = this.add.sprite(400, 720, 'right').setAlpha(alphaUntouched).setInteractive();
  this.up = this.add.sprite(240, 560, 'up').setAlpha(alphaUntouched).setInteractive();
  this.down = this.add.sprite(240, 720, 'down').setAlpha(alphaUntouched).setInteractive();
  this.resetButton = this.add.sprite(80,160, 'resetButton').setAlpha(alphaUntouched).setInteractive();

  this.resetButton.on('pointerdown', function(event){
    this.scene.restart();   
  }, this);
  
  this.input.on('gameobjectdown', function (pointer, gameObject) {
    gameObject.setAlpha(1.0);
    });
    this.input.on('gameobjectup', function (pointer, gameObject) {
        gameObject.setAlpha(alphaUntouched);
    });
  this.input.on('gameobjectout', function (pointer, gameObject) {
        gameObject.setAlpha(alphaUntouched);
    });

  this.go.on('pointerdown', function(){
    pressedGo = true;
  });
  this.go.on('pointerup', function(){
    pressedGo = false;
  });
  this.go.on('pointerout', function(){
    pressedGo = false;
  });

  this.left.on('pointerdown', function(){
    pressedLeft = true;
  });
  this.left.on('pointerup', function(){
    pressedLeft = false;
  });
  this.left.on('pointerout', function(){
    pressedGo = false;
  });
  
  this.right.on('pointerdown', function(){
    pressedRight = true;
  });
  this.right.on('pointerup', function(){
    pressedRight = false;
  });
  this.right.on('pointerout', function(){
    pressedGo = false;
  });

  this.up.on('pointerdown', function(){
    pressedUp = true;
  });
  this.up.on('pointerup', function(){
    pressedUp = false;
  });
  this.up.on('pointerout', function(){
    pressedGo = false;
  });
  
  this.down.on('pointerdown', function(){
    pressedDown = true;
  });
  this.down.on('pointerup', function(){
    pressedDown = false;
  });
  this.down.on('pointerout', function(){
    pressedGo = false;
  });
}

// ===========================================================
// ALL PLAYERCAR-TRIGGER FUNCTIONS:
// -----------------------------------------------------------
// -- OUTER:
// ----- LEFT trigger:
function cl_outerL() { 
  if ((pressedRight) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 155+borderX;
    playerLane = 'middle1';
  }
  if (touchedTriggerOuterL == false) {
    touchedTriggerOuterL = true;
    playerScore += 10;
  }
}

// ----- LEFT-DOWN trigger:
function t_outerLD(){
  if (touchedTriggerOuterLD == false) {
    touchedTriggerOuterLD = true;
    playerScore += 10;
  }
}

// ----- DOWN trigger:
function cl_outerD() { 
  if ((pressedUp) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 605;
    playerLane = 'middle1';
  }
  if (touchedTriggerOuterD == false) {
    touchedTriggerOuterD = true;
    playerScore += 10;
  }
}

// ----- DOWN-RIGHT trigger:
function t_outerDR(){
  if (touchedTriggerOuterDR == false) {
    touchedTriggerOuterDR = true;
    playerScore += 10;
    console.log(playerScore);
  }
}

// ----- RIGHT trigger:
function cl_outerR() { 
  if ((pressedLeft) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 805+borderX;
    playerLane = 'middle1';
  }
  if (touchedTriggerOuterR == false) {
    touchedTriggerOuterR = true;
    playerScore += 10;
  }
}

// ----- RIGHT-UP trigger:
function t_outerRU(){
  if (touchedTriggerOuterRU == false) {
    touchedTriggerOuterRU = true;
    playerScore += 10;
  }
}

// ----- UP trigger:
function cl_outerU() {
  if ((pressedDown) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 155;
    playerLane = 'middle1';
  } 
  if (touchedTriggerOuterU == false) {
    touchedTriggerOuterU = true;
    playerScore += 10;
  }
}

// ----- UP-LEFT trigger
function t_outerUL(){
  if (touchedTriggerOuterUL == false) {
    touchedTriggerOuterUL = true;
    playerScore += 10;
  }
}

// -----------------------------------------------------------
// -- MIDDLE1:
// ----- LEFT trigger
function cl_middle1L() { 
  if ((pressedLeft) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 75+borderX;
    playerLane = 'outer';
  }
  if ((pressedRight) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 235+borderX;
    playerLane = 'middle2';
  }
  if (touchedTriggerMiddle1L == false) {
    touchedTriggerMiddle1L = true;
    playerScore += 10;
  }
}

// ----- LEFT-DOWN trigger
function t_middle1LD(){
  if (touchedTriggerMiddle1LD == false) {
    touchedTriggerMiddle1LD = true;
    playerScore += 10;
  }
}

// ----- DOWN trigger
function cl_middle1D() { 
  if ((pressedUp) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 525;
    playerLane = 'middle2';
  }
  if ((pressedDown) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 685;
    playerLane = 'outer';
  }
  if (touchedTriggerMiddle1D == false) {
    touchedTriggerMiddle1D = true;
    playerScore += 10;
  }
}

// ----- DOWN-RIGHT trigger
function t_middle1DR(){
  if (touchedTriggerMiddle1DR == false) {
    touchedTriggerMiddle1DR = true;
    playerScore += 10;
  }
}

// ----- RIGHT trigger
function cl_middle1R() { 
  if ((pressedLeft) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 725+borderX;
    playerLane = 'middle2';
  }
  if ((pressedRight) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 885+borderX;
    playerLane = 'outer';
  }
  if (touchedTriggerMiddle1R == false) {
    touchedTriggerMiddle1R = true;
    playerScore += 10;
  }
}

// ----- RIGHT-UP trigger
function t_middle1RU(){
  if (touchedTriggerMiddle1RU == false) {
    touchedTriggerMiddle1RU = true;
    playerScore += 10;
  }
}

// ----- UP trigger
function cl_middle1U() { 
  if ((pressedUp) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 75;
    playerLane = 'outer';
  }
  if ((pressedDown) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 235;
    playerLane = 'middle2';
  }
  if (touchedTriggerMiddle1U == false) {
    touchedTriggerMiddle1U = true;
    playerScore += 10;
  }
}

// ----- UP-LEFT trigger
function t_middle1UL(){
  if (touchedTriggerMiddle1UL == false) {
    touchedTriggerMiddle1UL = true;
    playerScore += 10;
  }
}

// -----------------------------------------------------------
// -- MIDDLE2:
// ----- LEFT trigger:
function cl_middle2L() { 
  if ((pressedLeft) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 155+borderX;
    playerLane = 'middle1';
  }
  if ((pressedRight) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 315+borderX;
    playerLane = 'inner';
  }
  if (touchedTriggerMiddle2L == false) {
    touchedTriggerMiddle2L = true;
    playerScore += 10;
  }
}

// ----- LEFT-DOWN trigger:
function t_middle2LD(){
  if (touchedTriggerMiddle2LD == false) {
    touchedTriggerMiddle2LD = true;
    playerScore += 10;
  }
}

// ----- DOWN trigger:
function cl_middle2D() { 
  if ((pressedUp) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 445;
    playerLane = 'inner';
  }
  if ((pressedDown) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 605;
    playerLane = 'middle1';
  }
  if (touchedTriggerMiddle2D == false) {
    touchedTriggerMiddle2D = true;
    playerScore += 10;
  }
}

// ----- DOWN-RIGHT trigger:
function t_middle2DR(){
  if (touchedTriggerMiddle2DR == false) {
    touchedTriggerMiddle2DR = true;
    playerScore += 10;
  }
}

// ----- RIGHT trigger:
function cl_middle2R() { 
  if ((pressedLeft) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 645+borderX;
    playerLane = 'inner';
  }
  if ((pressedRight) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 805+borderX;
    playerLane = 'middle1';
  }
  if (touchedTriggerMiddle2R == false) {
    touchedTriggerMiddle2R = true;
    playerScore += 10;
  }
}

// ----- RIGHT-UP trigger:
function t_middle2RU(){
  if (touchedTriggerMiddle2RU == false) {
    touchedTriggerMiddle2RU = true;
    playerScore += 10;
  }
}

// ----- UP trigger:
function cl_middle2U() { 
  if ((pressedUp) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 155;
    playerLane = 'middle1';
  }
  if ((pressedDown) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 315;
    playerLane = 'inner';
  }
  if (touchedTriggerMiddle2U == false) {
    touchedTriggerMiddle2U = true;
    playerScore += 10;
  }
}

// ----- UP-LEFT trigger:
function t_middle2UL(){
  if (touchedTriggerMiddle2UL == false) {
    touchedTriggerMiddle2UL = true;
    playerScore += 10;
  }
}

// -----------------------------------------------------------
// -- INNER:
// ----- LEFT trigger:
function cl_innerL() {
  if ((pressedLeft) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 235+borderX;
    playerLane = 'middle2';
  } 
  if (touchedTriggerInnerL == false) {
    touchedTriggerInnerL = true;
    playerScore += 10;
  }
}

// ----- LEFT-DOWN trigger:
function t_innerLD(){
  if (touchedTriggerInnerLD == false) {
    touchedTriggerInnerLD = true;
    playerScore += 10;
  }
}

// ----- DOWN trigger:
function cl_innerD() { 
  if ((pressedDown) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 525;
    playerLane = 'middle2';
  } 
  if (touchedTriggerInnerD == false) {
    touchedTriggerInnerD = true;
    playerScore += 10;
  }
}

// ----- DOWN-RIGHT trigger:
function t_innerDR(){
  if (touchedTriggerInnerDR == false) {
    touchedTriggerInnerDR = true;
    playerScore += 10;
  }
}

// ----- RIGHT trigger:
function cl_innerR() {
  if ((pressedRight) && (pressedCL == false)){
    pressedCL = true;
    playerCar.x = 725+borderX;
    playerLane = 'middle2';
  } 
  if (touchedTriggerInnerR == false) {
    touchedTriggerInnerR = true;
    playerScore += 10;
  }
}

// ----- RIGHT-UP trigger:
function t_innerRU(){
  if (touchedTriggerInnerRU == false) {
    touchedTriggerInnerRU = true;
    playerScore += 10;
  }
}

// ----- UP trigger:
function cl_innerU() { 
  if ((pressedUp) && (pressedCL == false)){
    pressedCL = true;
    playerCar.y = 235;
    playerLane = 'middle2';
  } 
  if (touchedTriggerInnerU == false) {
    touchedTriggerInnerU = true;
    playerScore += 10;
  }
}

// ----- UP-LEFT trigger:
function t_innerUL(){
  if (touchedTriggerInnerUL == false) {
    touchedTriggerInnerUL = true;
    playerScore += 10;
  }
}

//============================================================
// ALL ENEMY-TRIGGER FUNCTIONS:
// -----------------------------------------------------------
// --OUTER:
// ----- LEFT trigger:
function cl_eOuterL() { 
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    enemy.x = 155+borderX;
    enemyLane = 'middle1';
    eChangedLane = true;
  }
}

// ----- LEFT-DOWN trigger:
function t_eOuterLD(){
  eChangedLane = false;
}

// ----- DOWN trigger:
function cl_eOuterD() {
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    enemy.y = 605;
    enemyLane = 'middle1';
    eChangedLane = true;
  }
}

// ----- DOWN-RIGHT trigger:
function t_eOuterDR(){
  eChangedLane = false;
}

// ----- RIGHT trigger:
function cl_eOuterR(){ 
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    enemy.x = 805+borderX;
    enemyLane = 'middle1';
    eChangedLane = true;
  }
}
// ----- RIGHT-UP trigger:
function t_eOuterRU(){
  eChangedLane = false;
}

// ----- UP trigger:
function cl_eOuterU() {
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    enemy.y = 155;
    enemyLane = 'middle1';
    eChangedLane = true;
  } 
}

// ----- UP-LEFT trigger:
function t_eOuterUL(){
  eChangedLane = false;
}

// -----------------------------------------------------------
// -- MIDDLE1:
// ----- LEFT trigger:
function cl_eMiddle1L() { 
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    if (playerLane == 'outer'){
      enemy.x = 75+borderX;
      enemyLane = 'outer'
    }
    else{
      enemy.x = 235+borderX;
      enemyLane = 'middle2';
    }
    eChangedLane = true;
  }
}

// ----- LEFT-DOWN trigger:
function t_eMiddle1LD(){
  eChangedLane = false;
}

// ----- DOWN trigger:
function cl_eMiddle1D() { 
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    if (playerLane == 'outer'){
      enemy.y = 685;
      enemyLane = 'outer';
    }
    else{     
      enemy.y = 525;
      enemyLane = 'middle2';
    }
    eChangedLane = true;
  }
}

// ----- DOWN-RIGHT trigger:
function t_eMiddle1DR(){
  eChangedLane = false;
}

// ----- RIGHT trigger:
function cl_eMiddle1R() { 
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    if (playerLane == 'outer'){
      enemy.x = 885+borderX;
      enemyLane = 'outer';
    }
    else {
      enemy.x = 725+borderX;
      enemyLane = 'middle2';
    }
    eChangedLane = true;
  }
}

// ----- RIGHT-UP trigger:
function t_eMiddle1RU(){
  eChangedLane = false;
}

// ----- UP trigger:
function cl_eMiddle1U() { 
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    if (playerLane == 'outer'){
      enemy.y = 75;
      enemyLane = 'outer';
    }
    else {
      enemy.y = 235;
      enemyLane = 'middle2';
    }
    eChangedLane = true;
  }
}

// ----- UP-LEFT trigger:
function t_eMiddle1UL(){
  eChangedLane = false;
}

// -----------------------------------------------------------
// -- MIDDLE2:
// ----- LEFT trigger:
function cl_eMiddle2L() { 
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    if (playerLane == 'inner'){
      enemy.x = 315+borderX;
      enemyLane = 'inner';
      }
    else {
      enemy.x = 155+borderX;
      enemyLane = 'middle1';
    }
    eChangedLane = true;
  }
}

// ----- LEFT-DOWN trigger:
function t_eMiddle2LD(){
  eChangedLane = false;
}

// ----- DOWN trigger:
function cl_eMiddle2D() { 
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    if (playerLane == 'inner'){
      enemy.y = 445;
      enemyLane = 'inner';
    }
    else {
      enemy.y = 605;
      enemyLane = 'middle1';
    }
    eChangedLane = true;
  }
}

// ----- DOWN-RIGHT trigger:
function t_eMiddle2DR(){
  eChangedLane = false;
}

// ----- RIGHT trigger:
function cl_eMiddle2R() {
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    if (playerLane == 'inner'){
      enemy.x = 645+borderX;
      enemyLane = 'inner';
    }
    else {
      enemy.x = 805+borderX;
      enemyLane = 'middle1';
    }
    eChangedLane = true;
  }
}

// ----- RIGHT-UP trigger:
function t_eMiddle2RU(){
  eChangedLane = false;
}

// ----- UP trigger:
function cl_eMiddle2U() { 
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    if (playerLane == 'inner'){
      enemy.y = 315;
      enemyLane = 'inner';
    }
    else {
      enemy.y = 155;
      enemyLane = 'middle1';
    }
    eChangedLane = true;
  }
}

// ----- UP-LEFT trigger:
function t_eMiddle2UL(){
  eChangedLane = false;
}

// -----------------------------------------------------------
// -- INNER:
// ----- LEFT trigger:
function cl_eInnerL() {
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    enemy.x = 235+borderX;
    enemyLane = 'middle2';
    eChangedLane = true;
  } 
}

// ----- LEFT-DOWN trigger:
function t_eInnerLD(){
  eChangedLane = false;
}

// ----- DOWN trigger:
function cl_eInnerD() { 
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    enemy.y = 525;
    enemyLane = 'middle2';
    eChangedLane = true;
  } 
}

// ----- DOWN-RIGHT trigger:
function t_eInnerDR(){
  eChangedLane = false;
}

// ----- RIGHT trigger:
function cl_eInnerR() {
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    enemy.x = 725+borderX;
    enemyLane = 'middle2';
    eChangedLane = true;
  }
}

// ----- RIGHT-UP trigger:
function t_eInnerRU(){
  eChangedLane = false;
}

// ----- UP trigger:
function cl_eInnerU() { 
  if ((enemyLane != playerLane) && (eChangedLane == false)){
    enemy.y = 235;
    enemyLane = 'middle2';
    eChangedLane = true;
  } 
}

// ----- UP-LEFT trigger:
function t_eInnerUL(){
  eChangedLane = false;
}

// ===========================================================

function buildWalls(){
  wall.create(220+borderX,35, 'wall').setScale(38,1).refreshBody();
  wall.create(740+borderX,35, 'wall').setScale(38,1).refreshBody();
  wall.create(260+borderX,115, 'wall').setScale(30,1).refreshBody();
  wall.create(700+borderX,115, 'wall').setScale(30,1).refreshBody();
  wall.create(300+borderX,195, 'wall').setScale(22,1).refreshBody();
  wall.create(660+borderX,195, 'wall').setScale(22,1).refreshBody();
  wall.create(340+borderX,275, 'wall').setScale(14,1).refreshBody();
  wall.create(620+borderX,275, 'wall').setScale(14,1).refreshBody();
  wall.create(340+borderX,485, 'wall').setScale(14,1).refreshBody();
  wall.create(620+borderX,485, 'wall').setScale(14,1).refreshBody();
  wall.create(300+borderX,565, 'wall').setScale(22,1).refreshBody();
  wall.create(660+borderX,565, 'wall').setScale(22,1).refreshBody();
  wall.create(260+borderX,645, 'wall').setScale(30,1).refreshBody();
  wall.create(700+borderX,645, 'wall').setScale(30,1).refreshBody();
  wall.create(220+borderX,725, 'wall').setScale(38,1).refreshBody();
  wall.create(740+borderX,725, 'wall').setScale(38,1).refreshBody();
  
  wall.create(35+borderX,180, 'wall').setScale(1,30).refreshBody();
  wall.create(35+borderX,580, 'wall').setScale(1,30).refreshBody();
  wall.create(115+borderX,220, 'wall').setScale(1,22).refreshBody();
  wall.create(115+borderX,540, 'wall').setScale(1,22).refreshBody();
  wall.create(195+borderX,260, 'wall').setScale(1,14).refreshBody();
  wall.create(195+borderX,500, 'wall').setScale(1,14).refreshBody();
  wall.create(275+borderX,300, 'wall').setScale(1,6).refreshBody();
  wall.create(275+borderX,460, 'wall').setScale(1,6).refreshBody();
  wall.create(685+borderX,300, 'wall').setScale(1,6).refreshBody();
  wall.create(685+borderX,460, 'wall').setScale(1,6).refreshBody();
  wall.create(765+borderX,260, 'wall').setScale(1,14).refreshBody();
  wall.create(765+borderX,500, 'wall').setScale(1,14).refreshBody();
  wall.create(845+borderX,220, 'wall').setScale(1,22).refreshBody();
  wall.create(845+borderX,540, 'wall').setScale(1,22).refreshBody();
  wall.create(925+borderX,180, 'wall').setScale(1,30).refreshBody();
  wall.create(925+borderX,580, 'wall').setScale(1,30).refreshBody();
  
  wall.create(480+borderX,380,'wall').setScale(26,6).refreshBody();
}


function buildTriggerTurn() {
  triggerTurn.create(40+borderX,40,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(40+borderX,720,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(120+borderX,120,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(120+borderX,640,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(200+borderX,200,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(200+borderX,560,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(280+borderX,280,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(280+borderX,480,'triggerTurn').setScale(1,1).refreshBody();
  
  triggerTurn.create(680+borderX,280,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(680+borderX,480,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(760+borderX,200,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(760+borderX,560,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(840+borderX,120,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(840+borderX,640,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(920+borderX,40,'triggerTurn').setScale(1,1).refreshBody();
  triggerTurn.create(920+borderX,720,'triggerTurn').setScale(1,1).refreshBody();
}


function rotateCar(){
  
  if (timer <= this.time.now) {
    playerCar.angle += 90;
  }
  timer = this.time.now + 20;
}


function rotateECar(){
  
  if (eTimer <= this.time.now) {
    enemy.angle -= 90;
  }
  eTimer = this.time.now + 20;
}


function crash() {
  startGame = false;
  playerCar.setTexture('player_crash');
  enemy.setTexture('enemy_crash');
  
  if (crashed == false){
    carSpeed = 0;
    eCarSpeed = 0;
    playerCar.setVelocity(0,0);
    enemy.setVelocity(0,0);
    playerLives -= 1;
    emitter.stop();
    
    if (playerLives < 0){
      gameOver = true;
      playerLives = 0;
    }
    
    dontMove = true;
    
    this.waiter = this.time.addEvent({
      delay: 2000,
      callback: ()=>{
        playerCar.x = 155+borderX;
        playerCar.y = 420;
        playerCar.angle = 270;
        playerCar.setVelocity(actualSpeedX, actualSpeedY);
        
        enemy.x = 155+borderX;
        enemy.y = 280;
        enemy.angle = 90;
        enemy.setVelocity(eActualSpeedX, eActualSpeedY);
        
        playerCar.setTexture('player');
        enemy.setTexture('enemy');
        
        if (gameOver == false){
          dontMove = false;
        }
      },
      loop: false
    })
    
    crashed = true;
    
    playerCar.setTexture('player');
    enemy.setTexture('enemy');
    
    if (gameOver){
      gameOverText.setAlpha(1.0);
      resetText.setAlpha(1.0);
    }
  }
}


function update () {
  
  lives.setText(playerLives);
  score.setText(playerScore);
  readStart();
  
  if (startGame) {    
    pressTurbo.setAlpha(0.0);
    crashed = false;
    
    if (boost) {
      emitter.start();
      carSpeed = turboSpeed;
    }
    else {
      carSpeed = normalSpeed;
      emitter.stop();
    }
    
    eCarSpeed = eNormalSpeed;
    
    readKeys();
    moveCar();
    moveECar();
    
    //When all triggers have been hit:
    if (touchedTriggerOuterL && touchedTriggerOuterLD && touchedTriggerOuterD && 
      touchedTriggerOuterDR && touchedTriggerOuterR && touchedTriggerOuterRU && 
      touchedTriggerOuterU && touchedTriggerOuterUL && touchedTriggerMiddle1L &&
      touchedTriggerMiddle1LD && touchedTriggerMiddle1D && touchedTriggerMiddle1DR && 
      touchedTriggerMiddle1R && touchedTriggerMiddle1RU && touchedTriggerMiddle1U && 
      touchedTriggerMiddle1UL && touchedTriggerMiddle2L && touchedTriggerMiddle2LD && 
      touchedTriggerMiddle2D && touchedTriggerMiddle2DR && touchedTriggerMiddle2R && 
      touchedTriggerMiddle2RU && touchedTriggerMiddle2U && touchedTriggerMiddle2UL && 
      touchedTriggerInnerL && touchedTriggerInnerLD && touchedTriggerInnerD && 
      touchedTriggerInnerDR && touchedTriggerInnerR && touchedTriggerInnerRU && 
      touchedTriggerInnerU && touchedTriggerInnerUL)
      {
        resetAllDrivenParts();
        resetAllTriggers();
        eNormalSpeed += 100;
        playerScore += 300;
      }
  }
  else{
    if (dontMove == false)
      pressTurbo.setAlpha(1.0);
  }
  
  if (touchedTriggerOuterL && touchedTriggerOuterLD && touchedTriggerOuterD){
    drivenPart_outerLD.setAlpha(1.0);
  }
  if (touchedTriggerOuterD && touchedTriggerOuterDR && touchedTriggerOuterR){
    drivenPart_outerDR.setAlpha(1.0);   
  }
  if (touchedTriggerOuterR && touchedTriggerOuterRU && touchedTriggerOuterU){
    drivenPart_outerRU.setAlpha(1.0);
  }
  if (touchedTriggerOuterU && touchedTriggerOuterUL && touchedTriggerOuterL){
    drivenPart_outerUL.setAlpha(1.0);
  }
  
  
  if (touchedTriggerMiddle1L && touchedTriggerMiddle1LD && touchedTriggerMiddle1D){
    drivenPart_middle1LD.setAlpha(1.0);
  }
  if (touchedTriggerMiddle1D && touchedTriggerMiddle1DR && touchedTriggerMiddle1R){
    drivenPart_middle1DR.setAlpha(1.0);   
  }
  if (touchedTriggerMiddle1R && touchedTriggerMiddle1RU && touchedTriggerMiddle1U){
    drivenPart_middle1RU.setAlpha(1.0);
  }
  if (touchedTriggerMiddle1U && touchedTriggerMiddle1UL && touchedTriggerMiddle1L){
    drivenPart_middle1UL.setAlpha(1.0);
  }
  
  
  if (touchedTriggerMiddle2L && touchedTriggerMiddle2LD && touchedTriggerMiddle2D){
    drivenPart_middle2LD.setAlpha(1.0);
  }
  if (touchedTriggerMiddle2D && touchedTriggerMiddle2DR && touchedTriggerMiddle2R){
    drivenPart_middle2DR.setAlpha(1.0);   
  }
  if (touchedTriggerMiddle2R && touchedTriggerMiddle2RU && touchedTriggerMiddle2U){
    drivenPart_middle2RU.setAlpha(1.0);
  }
  if (touchedTriggerMiddle2U && touchedTriggerMiddle2UL && touchedTriggerMiddle2L){
    drivenPart_middle2UL.setAlpha(1.0);
  }
  
  
  if (touchedTriggerInnerL && touchedTriggerInnerLD && touchedTriggerInnerD){
    drivenPart_innerLD.setAlpha(1.0);
  }
  if (touchedTriggerInnerD && touchedTriggerInnerDR && touchedTriggerInnerR){
    drivenPart_innerDR.setAlpha(1.0);   
  }
  if (touchedTriggerInnerR && touchedTriggerInnerRU && touchedTriggerInnerU){
    drivenPart_innerRU.setAlpha(1.0);
  }
  if (touchedTriggerInnerU && touchedTriggerInnerUL && touchedTriggerInnerL){
    drivenPart_innerUL.setAlpha(1.0);
  }
}

function resetAllTriggers(){
  
  touchedTriggerOuterL = false;
  touchedTriggerOuterLD= false;
  touchedTriggerOuterD = false;
  touchedTriggerOuterDR= false;
  touchedTriggerOuterR = false;
  touchedTriggerOuterRU= false;
  touchedTriggerOuterU = false;
  touchedTriggerOuterUL= false;

  touchedTriggerMiddle1L = false;
  touchedTriggerMiddle1LD= false;
  touchedTriggerMiddle1D = false;
  touchedTriggerMiddle1DR= false;
  touchedTriggerMiddle1R = false;
  touchedTriggerMiddle1RU= false;
  touchedTriggerMiddle1U = false;
  touchedTriggerMiddle1UL= false;

  touchedTriggerMiddle2L = false;
  touchedTriggerMiddle2LD= false;
  touchedTriggerMiddle2D = false;
  touchedTriggerMiddle2DR= false;
  touchedTriggerMiddle2R = false;
  touchedTriggerMiddle2RU= false;
  touchedTriggerMiddle2U = false;
  touchedTriggerMiddle2UL= false;

  touchedTriggerInnerL = false;
  touchedTriggerInnerLD= false;
  touchedTriggerInnerD = false;
  touchedTriggerInnerDR= false;
  touchedTriggerInnerR = false;
  touchedTriggerInnerRU= false;
  touchedTriggerInnerU = false;
  touchedTriggerInnerUL= false;
}

function resetAllDrivenParts(){

  drivenPart_outerLD.setAlpha(0.0);
  drivenPart_outerDR.setAlpha(0.0);
  drivenPart_outerRU.setAlpha(0.0);
  drivenPart_outerUL.setAlpha(0.0);
  drivenPart_middle1LD.setAlpha(0.0);
  drivenPart_middle1DR.setAlpha(0.0);
  drivenPart_middle1RU.setAlpha(0.0);
  drivenPart_middle1UL.setAlpha(0.0);
  drivenPart_middle2LD.setAlpha(0.0);
  drivenPart_middle2DR.setAlpha(0.0);
  drivenPart_middle2RU.setAlpha(0.0);
  drivenPart_middle2UL.setAlpha(0.0);
  drivenPart_innerLD.setAlpha(0.0);
  drivenPart_innerDR.setAlpha(0.0);
  drivenPart_innerRU.setAlpha(0.0);
  drivenPart_innerUL.setAlpha(0.0);
}

function moveCar(){
  if (playerCar.angle == 0)  {
      playerCar.setVelocity(carSpeed,0);
    }
    else if (playerCar.angle == 90)  {
      playerCar.setVelocity(0,-carSpeed);
    }
    else if (playerCar.angle == -180)  {
      playerCar.setVelocity(-carSpeed,0);
    }
    else {
      playerCar.setVelocity(0,carSpeed);
    }
}

function moveECar(){
  if (enemy.angle == 0)  {
      enemy.setVelocity(eCarSpeed,0);
    }
    else if (enemy.angle == 90)  {
      enemy.setVelocity(0,-eCarSpeed);
    }
    else if (enemy.angle == -180)  {
      enemy.setVelocity(-eCarSpeed,0);
    }
    else {
      enemy.setVelocity(0,eCarSpeed);
    }
}


function readKeys() {
  
  if (keys.left.isDown)
    pressedLeft = true;
  else
    pressedLeft = false;

  if (keys.right.isDown)
    pressedRight = true;
  else
    pressedRight = false;
  
  if (keys.up.isDown)
    pressedUp = true;
  else
    pressedUp = false;
  
  if (keys.down.isDown)
    pressedDown = true;
  else
    pressedDown = false;
  
  if ((pressedLeft == false) && (pressedRight == false) && (pressedUp == false) &&
    (pressedDown == false) && (pressedCL == true)) {
    pressedCL = false;
    
  }
}

function readStart() {
  if (dontMove == false){
    if ((spaceKey.isDown) || (pressedGo)) { 
      if (startGame == false) {
        playerCar.setVelocity(carSpeed,0);
        enemy.setVelocity(eCarSpeed,0);
        startGame = true;
      } 
      boost = true;
    }
    else {
      boost = false;
    }
  }
}

6.5.5. Space Exterminator by Jay Hriscu (Phaser.js)

SpaceExterminator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WEMOB2-Game</title>
    <script src="//cdn.jsdelivr.net/npm/phaser@3.22.0/dist/phaser.js"></script>
    <!-- https://github.com/photonstorm/phaser -->
    <script src="include/game-script.js"></script>
    <link rel="stylesheet" href="./include/stylesheet.css">
    <link rel="stylesheet" href="./include/w3.css">
</head>
<body>

<main>
    <noscript>To use this website please enable Javascript!</noscript>
    <div id="scoreBox">
        <div id="scoreBoxContent">
            <h1>Your score is: </h1>
            <h1 id="score"></h1><br><br>
            <p>Speeder destroyed: </p>
            <p id="speederKills"></p><br><br>
            <p>Chaser destroyed: </p>
            <p id="chaserKills"></p><br><br>
            <p>Bomb destroyed: </p>
            <p id="bombKills"></p><br><br>
            <button class="w3-button w3-green w3-hover-blue w3-text-black w3-large w3-round-large"
                    onclick="location.reload();">Restart
            </button>
        </div>
    </div>
</main>

<div id="arrowButtons">
    <button class="w3-button w3-grey w3-round-large updownArrows" onclick="moveUp()">&uparrow;</button>
    <br>
    <button class="w3-button w3-grey w3-round-large" onclick="moveLeft()">&leftarrow;</button>
    <button class="w3-button w3-grey w3-round-large rightArrow" onclick="moveRight()">&rightarrow;</button>
    <br>
    <button class="w3-button w3-grey w3-round-large updownArrows" onclick="moveDown()">&downarrow;</button>
    <br>
</div>

<button id="shootButton" class="w3-button w3-red w3-round-large" onclick="buttonShoot()">Shoot</button>

</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#scoreBox {
    border: 5px solid Green;
    color: white;
    background-color: black;
    display: none;
    position: fixed;
    z-index: 1;
    padding-top: 100px;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    overflow: auto;
}

#scoreBoxContent {
    margin: auto;
    padding: 20px;
    border: 1px solid #888;
    width: 80%;
    text-align: center;
}

h1, p {
    display: inline;
}

#arrowButtons {
    display: none;
    position: absolute;
    bottom: 50px;
    left: 50px;
}


#shootButton {
    display: none;
    position: absolute;
    bottom: 75px;
    right: 100px;
}

.updownArrows{
    margin-left: 50px;
}

.rightArrow{
    margin-left: 40px;
}

@media only screen and (max-device-width: 1000px) and (orientation: landscape) {

    #arrowButtons {
        display: block;
    }

    #shootButton {
        display: block;
    }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
'use strict';

const config = {
    type: Phaser.AUTO,
    width: window.innerWidth,
    height: window.innerHeight,
    backgroundColor: "black",
    physics:{
        default: 'arcade',
        arcade: {
            debug: false,
            gravity: {
                x: 0,
                y: 0
            }
        }
    },
    scene: {
        preload: preload,
        create: create,
        update: update
    }
};

let game = new Phaser.Game(config);

const gameState = {};

function preload ()
{
    //Player Character sprite
    this.load.image('playerUp', './image/Player/player-up.png');
    this.load.image('playerDown', './image/Player/player-down.png');
    this.load.image('playerLeft', './image/Player/player-left.png');
    this.load.image('playerRight', './image/Player/player-right.png');
    //Laser
    this.load.image('laserRight', './image/Laser/laser-right.png');
    this.load.image('laserLeft', './image/Laser/laser-left.png');
    this.load.image('laserUp', './image/Laser/laser-up.png');
    this.load.image('laserDown', './image/Laser/laser-down.png');
    //Enemy-Speeder
    this.load.image('speederUp', './image/Enemy-Speeder/Speeder-up.png');
    this.load.image('speederDown', './image/Enemy-Speeder/Speeder-down.png');
    this.load.image('speederLeft', './image/Enemy-Speeder/Speeder-left.png');
    this.load.image('speederRight', './image/Enemy-Speeder/Speeder-right.png');
    //Enemy-Chaser
    this.load.image('chaser', './image/Enemy-Chaser/Chaser.png');
    this.load.image('chaserBomb', './image/Enemy-Chaser/Chaser-Bomb.png');
}

function create ()
{


    gameState.player = this.add.sprite((window.innerWidth / 2), (window.innerHeight / 2), "playerRight");
    this.physics.world.enable(gameState.player);

    gameState.cursors = this.input.keyboard.createCursorKeys();
    gameState.positionState = "right";

    gameState.enemyAmount = 0;
    gameState.enemyMax = 8;

    gameState.multiplier = 1;
    gameState.multiplierCounter = 0;
    gameState.score = 0;
    gameState.lives = 3;

    gameState.speederKill = 0;
    gameState.chaserKill = 0;
    gameState.bombKill = 0;

    gameState.textLives = this.add.text((window.innerWidth / 2) - 40, 120, "Lives: " + gameState.lives, { font: "32px Arial", fill: "YELLOW", align: "center" });
    gameState.textMultiplier = this.add.text(180, 90, "Multiplier: " + gameState.multiplier, { font: "32px Arial", fill: "RED", align: "center" });
    gameState.textScore = this.add.text(window.innerWidth - 380, 90, "Score: " + scoreZeros(gameState.score), { font: "32px Arial", fill: "RED", align: "center" });


    //timer
    gameState.gameTime = 150;
    gameState.textTime = this.add.text((window.innerWidth / 2) - 180, 25, "Counter: " + formatTime(gameState.gameTime) , { font: "64px Arial", fill: "GREEN", align: "center" });
    this.time.addEvent({
        delay: 1000,
        callback: ()=>{
            updateTimer();
        },
        loop: true
    });

    //Spawn enemy
    gameState.speeders = this.add.group();
    gameState.speedersHor = this.add.group();
    gameState.chasers = this.add.group();
    gameState.chaserLaser = this.add.group();

       this.time.addEvent({
        delay: 1000,
        callback: ()=>{
            if(gameState.enemyAmount < gameState.enemyMax ) {
                let spawnRandom = Math.floor(Math.random() * 2);
                if (spawnRandom == 0) {

                    let sideSpawn = Math.floor(Math.random() * 2);
                    if (sideSpawn == 0) {
                        gameState.speeder = new Speeder(this, Math.floor(Math.random() * (window.innerWidth - 50)), 80);
                        gameState.speeders.add(gameState.speeder);
                    } else if (sideSpawn == 1) {
                        gameState.speederHor = new SpeederHor(this, 80, Math.floor(Math.random() * (window.innerHeight - 50)));
                        gameState.speedersHor.add(gameState.speederHor);
                    }
                    gameState.enemyAmount++;

                } else if (spawnRandom == 1) {
                    gameState.chaser = new Chaser(this, Math.floor(Math.random() * (window.innerWidth - 50)), Math.floor(Math.random() * (window.innerHeight - 50)));
                    gameState.chasers.add(gameState.chaser);
                    gameState.enemyAmount++;
                }

            }
        },
        loop: true
    });

    //Laser groups
    gameState.lasersUp = this.physics.add.group({
        defaultKey: 'laserUp',
        maxSize: -1
    });

    gameState.lasersDown = this.physics.add.group({
        defaultKey: 'laserDown',
        maxSize: -1
    });

    gameState.lasersLeft = this.physics.add.group({
        defaultKey: 'laserLeft',
        maxSize: -1
    });

    gameState.lasersRight = this.physics.add.group({
        defaultKey: 'laserRight',
        maxSize: -1
    });


    gameState.keySpace = this.input.keyboard.addKey('SPACE');  // Get key object
    gameState.keySpace.on('down', function(event) {
        //Shoot lasers
        shoot(gameState.positionState);
    });



}

function shoot(position) {

    let dist = 40;
    let laserSpeed = 900;

    if(position == "up"){
        let laser = gameState.lasersUp.get(gameState.player.x, gameState.player.y - dist);
        if (laser) {
            laser.setActive(true);
            laser.setVisible(true);
            laser.body.velocity.y -= laserSpeed;
        }
    }else if(position == "down"){
        let laser = gameState.lasersDown.get(gameState.player.x, gameState.player.y + dist);
        if (laser) {
            laser.setActive(true);
            laser.setVisible(true);
            laser.body.velocity.y += laserSpeed;
        }
    }else if(position == "left"){
        let laser = gameState.lasersLeft.get(gameState.player.x - dist, gameState.player.y);
        if (laser) {
            laser.setActive(true);
            laser.setVisible(true);
            laser.body.velocity.x -= laserSpeed;
        }
    }else if(position == "right"){
        let laser = gameState.lasersRight.get(gameState.player.x + dist, gameState.player.y);
        if (laser) {
            laser.setActive(true);
            laser.setVisible(true);
            laser.body.velocity.x += laserSpeed;
        }
    }


}
//For mobile

function moveUp() {
    gameState.player.body.setSize(64, 113);
    //gameState.player.y -= 5;
    gameState.player.body.velocity.x = 0;
    gameState.player.body.velocity.y = 0;
    gameState.player.body.velocity.y = -200;
    gameState.positionState = "up";
    gameState.player.setTexture("playerUp");
}

function moveDown() {
    gameState.player.body.setSize(64, 113);
   // gameState.player.y += 5;
    gameState.player.body.velocity.x = 0;
    gameState.player.body.velocity.y = 0;
    gameState.player.body.velocity.y = 200;
    gameState.positionState = "down";
    gameState.player.setTexture("playerDown");
}

function moveLeft(){
    gameState.player.body.setSize(113, 64);
   // gameState.player.x -= 5;
    gameState.player.body.velocity.x = 0;
    gameState.player.body.velocity.y = 0;
    gameState.player.body.velocity.x = -200;
    gameState.positionState = "left";
    gameState.player.setTexture("playerLeft");
}

function moveRight() {
    gameState.player.body.setSize(113, 64);
   // gameState.player.x += 5;
    gameState.player.body.velocity.x = 0;
    gameState.player.body.velocity.y = 0;
    gameState.player.body.velocity.x = 200;
    gameState.positionState = "right";
    gameState.player.setTexture("playerRight");
}

function buttonShoot() {
    shoot(gameState.positionState);
}

function update(){
    //Arena/Player collision & movement

    if(gameState.cursors.down.isDown || gameState.cursors.up.isDown){
      //  gameState.player.setDisplaySize(64, 113);
        gameState.player.body.setSize(64, 113);
    }else if(gameState.cursors.right.isDown || gameState.cursors.left.isDown){
       // gameState.player.setDisplaySize(113, 64);
        gameState.player.body.setSize(113, 64);
    }

    if(gameState.cursors.right.isDown && gameState.player.x <=  window.innerWidth - 80){
        gameState.player.x += 5;
        gameState.positionState = "right";
        gameState.player.setTexture("playerRight");
    }
    if(gameState.cursors.left.isDown && gameState.player.x >= 60){
        gameState.player.x -= 5;
        gameState.positionState = "left";
        gameState.player.setTexture("playerLeft");
    }
    if(gameState.cursors.down.isDown && gameState.player.y <=  window.innerHeight - 60){
        gameState.player.y += 5;
        gameState.positionState = "down";
        gameState.player.setTexture("playerDown");
    }
    if(gameState.cursors.up.isDown && gameState.player.y >= 60){
        gameState.player.y -= 5;
        gameState.positionState = "up";
        gameState.player.setTexture("playerUp");

    }else{
     // gameState.player.body.velocity.x = 0;
      //gameState.player.body.velocity.y = 0;
    }

    gameState.speeders.children.each(function(speeder){

        if(speeder.y >= window.innerHeight - 80){
            speeder.moveUp();
        }

        if(speeder.y <= 80){
            speeder.moveDown();
        }

        speeder.position(this, speeder.x, speeder.y);

        this.physics.collide(speeder, [gameState.lasersUp, gameState.lasersDown, gameState.lasersLeft, gameState.lasersRight], function(){
            speeder.destroy();
            speeder.destroyed();
            gameState.speederKill++;
            gameState.multiplierCounter++;
            gameState.enemyAmount--;
            gameState.score += (gameState.multiplier * 100);
            scoreZeros();
            gameState.textScore.setText("Score: " + scoreZeros(gameState.score));

        });


        this.physics.collide(speeder, gameState.player, function(){
            speeder.destroy();
            speeder.destroyed();
            gameState.lives--;
            gameState.enemyAmount--;
            gameState.multiplierCounter = 0;
            gameState.multiplier = 1;
            gameState.textLives.setText("Lives: " + gameState.lives);
        });

    }, this);

    gameState.speedersHor.children.each(function(speederHor){

        if(speederHor.x <= 80){
            speederHor.moveRight();
        }

        if(speederHor.x >= window.innerWidth - 100){
            speederHor.moveLeft();
        }

        speederHor.position(this, speederHor.x, speederHor.y);

        this.physics.collide(speederHor, [gameState.lasersUp, gameState.lasersDown, gameState.lasersLeft, gameState.lasersRight], function(){
            speederHor.destroy();
            speederHor.destroyed();
            gameState.speederKill++;
            gameState.multiplierCounter++;
            gameState.enemyAmount--;
            gameState.score += (gameState.multiplier * 100);
            scoreZeros();
            gameState.textScore.setText("Score: " + scoreZeros(gameState.score));

        });

        this.physics.collide(speederHor, gameState.player, function(){
            speederHor.destroy();
            speederHor.destroyed();
            gameState.lives--;
            gameState.enemyAmount--;
            gameState.multiplierCounter = 0;
            gameState.multiplier = 1;
            gameState.textLives.setText("Lives: " + gameState.lives);
        });

    }, this);


    gameState.chasers.children.each(function(chaser) {

        chaser.moveSpin();

        let r = Math.floor(Math.random() * 100);
        if(r == 0){
            chaser.shootLaser(this, chaser.x, chaser.y);
        }


        this.physics.collide(chaser, [gameState.lasersUp, gameState.lasersDown, gameState.lasersLeft, gameState.lasersRight], function(){
            chaser.destroy();
            chaser.destroyed();
            gameState.chaserKill++;
            gameState.multiplierCounter++;
            gameState.enemyAmount--;
            gameState.score += (gameState.multiplier * 100);
            scoreZeros();
            gameState.textScore.setText("Score: " + scoreZeros(gameState.score));

        });

        this.physics.collide(chaser, gameState.player, function(){
            chaser.destroy();
            chaser.destroyed();
            gameState.lives--;
            gameState.enemyAmount--;
            gameState.multiplierCounter = 0;
            gameState.multiplier = 1;
            gameState.textLives.setText("Lives: " + gameState.lives);
        });

    }, this);

    gameState.chaserLaser.children.each(function (bomb) {

        this.physics.collide(bomb, [gameState.lasersUp, gameState.lasersDown, gameState.lasersLeft, gameState.lasersRight], function(){
            bomb.destroy();
            gameState.score += (gameState.multiplier * 10);
            gameState.bombKill++;
        });


        this.physics.collide(bomb, gameState.player, function(){
            bomb.destroy();
            gameState.lives--;
            gameState.multiplierCounter = 0;
            gameState.multiplier = 1;
            gameState.textLives.setText("Lives: " + gameState.lives);
        });
    }, this);

    updateMultiplier();

    if(gameState.lives <= 0){
        endGame();
    }
}


//Enemy

class Speeder extends Phaser.GameObjects.Sprite{
    constructor(scene, x, y){
        super(scene, x, y);
        this.speeder = scene.add.sprite(this.x, this.y , "speederDown");
        this.setDisplaySize(this.speeder.width, this.speeder.height);
        this.setSize(this.speeder.width, this.speeder.height, false);
        scene.physics.world.enable(this);

    }

    moveUp(){
        this.body.velocity.y = -200;
    }

    moveDown(){
        this.body.velocity.y = 200;
    }


    position(scene, x, y){
        this.speeder.x = x;
        this.speeder.y = y;

        if(this.speeder.y >= window.innerHeight - 80){
            this.speeder.destroy();
            this.speeder = scene.add.sprite(this.x, this.y , "speederUp");
        }
        if(this.speeder.y <= 80){
            this.speeder.destroy();
            this.speeder = scene.add.sprite(this.x, this.y , "speederDown");
        }
    }

    destroyed(){
        this.speeder.destroy();
    }

}

class SpeederHor extends Phaser.GameObjects.Sprite{

    constructor(scene, x, y){
        super(scene, x, y);
        this.speeder = scene.add.sprite(this.x, this.y , "speederRight");
        this.setDisplaySize(this.speeder.width, this.speeder.height);
        this.setSize(this.speeder.width, this.speeder.height);
        scene.physics.world.enable(this);
    }

    moveLeft(){
        this.body.velocity.x = -200;
    }

    moveRight(){
        this.body.velocity.x = 200;
    }

    position(scene, x, y){
        this.speeder.x = x;
        this.speeder.y = y;

        if(this.speeder.x <= 80){
            this.speeder.destroy();
            this.speeder = scene.add.sprite(this.x, this.y , "speederRight");
        }

        if(this.speeder.x >=  window.innerWidth - 100){
            this.speeder.destroy();
            this.speeder = scene.add.sprite(this.x, this.y , "speederLeft");
        }

    }

    destroyed(){
        this.speeder.destroy();
    }
}


class Chaser extends Phaser.GameObjects.Sprite{
    constructor(scene, x, y){
        super(scene, x, y);
        this.chaser = scene.add.sprite(this.x, this.y , "chaser");
        this.setDisplaySize(this.chaser.width, this.chaser.height);
        this.setSize(this.chaser.width, this.chaser.height);
        scene.physics.world.enable(this);
    }

    moveSpin(){
        this.chaser.angle += 1;
    }

//https://blog.ourcade.co/posts/2020/fire-bullets-from-facing-direction-phaser-3/
    shootLaser(scene, x, y){
        this.bomb = scene.add.sprite(x, y, "chaserBomb");
        gameState.chaserLaser.add(this.bomb);
        this.bomb.setDisplaySize(20, 20);
        this.bomb.setSize(20, 20);
        scene.physics.world.enable(this.bomb);
        let vx = Math.cos(this.chaser.rotation) * 200;
        let vy = Math.sin(this.chaser.rotation) * 200;
        this.bomb.body.velocity.x = vx;
        this.bomb.body.velocity.y = vy;
    }

    destroyed(){
        this.chaser.destroy();
    }
}


function formatTime(seconds){

    let minutes = Math.floor(seconds/60);

    let partInSeconds = seconds%60;
    // Adds left zeros to seconds
    partInSeconds = partInSeconds.toString().padStart(2,'0');

    return `${minutes}:${partInSeconds}`;
}

function updateTimer() {

    gameState.gameTime--;

    if(gameState.gameTime <= 0){
        endGame();
    }

    gameState.textTime.setText('Counter: ' + formatTime(gameState.gameTime));
}

function endGame(){
    //Add box of information about the player score, etc... and reset button.
    document.getElementById("score").innerHTML = gameState.score;
    document.getElementById("speederKills").innerHTML = gameState.speederKill;
    document.getElementById("chaserKills").innerHTML = gameState.chaserKill;
    document.getElementById("bombKills").innerHTML = gameState.bombKill;
    document.getElementById("scoreBox").style.display = "block";
}


function scoreZeros(score){
    if(score < 10){
        score = "00000" + score;
    }else if(score < 100 && score > 9){
        score = "0000" + score;
    }else if(score < 1000 && score > 99){
        score = "000" + score;
    }else if(score < 10000 && score > 999){
        score = "00" + score;
    }else if(score < 100000 && score > 9999){
        score = "0" + score;
    }else if(score < 1000000 && score > 99999){
        score = "" + score;
    }

    return score;
}

function updateMultiplier(){

    if(gameState.multiplierCounter == gameState.multiplier){
        gameState.multiplierCounter = 0;
        gameState.multiplier++;
        gameState.textMultiplier.setText("Multiplier: " + gameState.multiplier);
    }

}

6.5.6. Nameless Snake by Alessio Volta (Phaser.js)

namelessSnake

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <!--CSS-->
    <link rel="stylesheet" type="text/css" href="./style/style.css">
    <!--Game engine PHASER3-->
    <script src=//cdn.jsdelivr.net/npm/phaser@3.23.0/dist/phaser.min.js></script>
    <!--JS-->
    <script src="./functions/sounds.js"></script>

    <title>index of nameless snake</title>
    <!-- sample html for this button-->

  </head>
  <body>
    <script>

      alert("Use arrows-keys to move")

    </script>

    <div id="backgroundaudio">

      <audio id="myAudio" controls autoplay>
        <source src="./music/USSR.ogg" type="audio/ogg">
        <source src="./music/USSR.mp3" type="audio/mpeg">
        Your browser does not support the audio element.
      </audio>

      <script>

        let backgroundMusic = document.getElementById("greatMusic");
        backgroundMusic.autoplay = true;
        backgroundMusic.loop = true;
        backgroundMusic.load();

      </script>

    </div>

    <div class=".flex-container, .flex-item:nth-child(1)">
      <button id="up" class="button" onclick="snake.heading = UP">Up</button>
      <button id="left" class="button" onclick="snake.heading = LEFT">Left</button>
    </div>
    <div class=".flex-container, .flex-item:nth-child(2)" id="gameArea">
      <script src="./functions/setUpGame.js"></script>
      <script src="./functions/food.js"></script>
      <script src="./functions/Buttons.js" defer></script>
    </div>
    <div class=".flex-container, .flex-item:nth-child(3)">
      <button id="right" class="button" onclick="snake.heading = RIGHT">Right</button>
      <button id="down" class="button" onclick="snake.heading = DOWN">Down</button>
    </div>

    <footer><p><i>Copyright © Volta Alessio</i></p></footer>

  </body>
</html>
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
body
{
  background: rgb(36,36,38);
  background: linear-gradient(90deg, rgba(36,36,38,1) 0%, rgba(0,0,3,1) 35%, rgba(40,49,51,1) 100%);
  text-align: center;
}

@media screen and (min-width: 601px)
{
  /*normal on my phone*/
  div
  {
    -size: 10px;
  }
}

@media screen and (min-width: 801px)
{
  div
  {
    -size: 12px;
  }
}

@media screen and (min-width: 1001px)
{
  /*normal on my pc*/
  div
  {
    -size: 15px;
  }
}

@media screen and (min-width: 1201px)
{
  div
  {
    -size: 25px;
  }
}

@media screen and (min-width: 1401px)
{
  div
  {
    -size: 40px;
  }
}

button
{
  width: 30%;
  height: 20%;
  font-size: large;
}

#joystick-container
{
  border: solid 1px #000;
  display: inline-block;
  background-color: white;
}
#joystick
{
  width: 100px;
  height: 100px;
  margin: 1px;
  cursor: crosshair;
}

.flex-container
{
  display: -ms-flexbox;
  display: -webkit-flex;
  display: flex;
  -webkit-flex-direction: row;
  -ms-flex-direction: row;
  flex-direction: row;
  -webkit-flex-wrap: wrap;
  -ms-flex-wrap: wrap;
  flex-wrap: wrap;
  -webkit-justify-content: center;
  -ms-flex-pack: center;
  justify-content: center;
  -webkit-align-content: center;
  -ms-flex-line-pack: center;
  align-content: center;
  -webkit-align-items: center;
  -ms-flex-align: center;
  align-items: center;
}

.flex-item:nth-child(1) {
  -webkit-order: 0;
  -ms-flex-order: 0;
  order: 0;
  -webkit-flex: 3 1 auto;
  -ms-flex: 3 1 auto;
  flex: 3 1 auto;
  -webkit-align-self: center;
  -ms-flex-item-align: center;
  align-self: center;
}

.flex-item:nth-child(2) {
  -webkit-order: 1;
  -ms-flex-order: 1;
  order: 1;
  -webkit-flex: 3 1 auto;
  -ms-flex: 3 1 auto;
  flex: 3 1 auto;
  -webkit-align-self: center;
  -ms-flex-item-align: center;
  align-self: center;
}

.flex-item:nth-child(3) {
  -webkit-order: 2;
  -ms-flex-order: 2;
  order: 2;
  -webkit-flex: 3 1 auto;
  -ms-flex: 3 1 auto;
  flex: 3 1 auto;
  -webkit-align-self: center;
  -ms-flex-item-align: center;
  align-self: center;
}

footer
{
  color: white;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/* The Modal (background) */
.modal
{
    display: none; /* Hidden by default */
    position: fixed; /* Stay in place */
    z-index: 1; /* Sit on top */
    padding-top: 100px; /* Location of the box */
    left: 0;
    top: 0;
    width: 100%; /* Full width */
    height: 100%; /* Full height */
    overflow: auto; /* Enable scroll if needed */
    background-color: rgb(0,0,0); /* Fallback color */
    background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}

/* Modal Content */
.modal-content
{
    position: relative;
    background-color: #fefefe;
    margin: auto;
    padding: 0;
    border: 1px solid #888;
    width: 80%;
    box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
    -webkit-animation-name: animatetop;
    -webkit-animation-duration: 0.4s;
    animation-name: animatetop;
    animation-duration: 0.4s
}

/* Add Animation */
@-webkit-keyframes animatetop
{
    from {top:-300vw; opacity:0}
    to {top:0; opacity:1}
}

@keyframes animatetop
{
    from {top:-300px; opacity:0}
    to {top:0; opacity:1}
}

/* The Close Button */
.close
{
    color: white;
    float: right;
    font-size: 28px;
    font-weight: bold;
}

.close:hover,
.close:focus
{
    color: #000;
    text-decoration: none;
    cursor: pointer;
}

.modal-header
{
    padding: 2px 16px;
    background-color: black;
    color: white;
}

.modal-body
{
    padding: 2px 16px;
}

.modal-footer
{
    padding: 2px 16px;
    background-color: black;
    color: white;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#backgroundaudio {
    display: block;
    position: fixed;
    bottom: -43px;
    left: 5px;
    -webkit-transition: all 1s ease-in-out;
    -moz-transition: all 1s ease-in-out;
    -ms-transition: all 1s ease-in-out;
    -o-transition: all 1s ease-in-out;
    transition: all 1s ease-in-out;
}
#backgroundaudio:hover {
    bottom: 0;
    -webkit-transition: all 1s ease-in-out;
    -moz-transition: all 1s ease-in-out;
    -ms-transition: all 1s ease-in-out;
    -o-transition: all 1s ease-in-out;
    transition: all 1s ease-in-out;
}
#backgroundaudio audio {
    background: #ffffff;
    padding: 5px;
    display: table-cell;
    vertical-align: middle;
    height: 43px;
    z-index: 9998;
}
#backgroundaudio i {
    font-size: 40px;
    display: block;
    background: #ffffff;
    padding: 5px;
    width: 50px;
    float: none;
    margin-bottom: -1px;
    z-index: 9999;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
 * The food will be placed anywhere in the 50*50 (perhaps allowing user to change?) grid
 * *except* on-top of the snake --> eliminate snakes positions out of the possible food locations.
 * If there is no locations left for the food player wins!
 *
 */
function repositionFood ()
{
  //  First create an array that assumes all positions
  //  are valid for the new piece of food

  //  A Grid we'll use to reposition the food each time it's eaten
  let playField = [];

  //to make sure the food doesnt spawn on the border
  let safespace = 15

  for (let y = 0; y < config.height-safespace; y++)
  {
    playField[y] = [];

    for (let x = 0; x < config.width-safespace; x++)
    {
      playField[y][x] = true;
    }
  }

  snake.updateGrid(playField);

  let validLocations = [];

  for (let y = 0; y < config.height-safespace; y++)
  {
    for (let x = 0; x < config.width-safespace; x++)
    {
      if (playField[y][x] === true)
      {
        //check if there is any place left for food
        validLocations.push({ x: x, y: y });
      }
    }
  }

  if (validLocations.length > 0)
  {
    //  Use the RNG to pick a random food position (Phaser tutorial)
    let pos = Phaser.Math.RND.pick(validLocations);
    food.setPosition(pos.x , pos.y );

    return true;
  }
  else
  {
    return false;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//movement functions
function keyCodeMover(e)
{
  if (e.keyCode == 39)
  {
    moveRight();
  }
  if (e.keyCode == 37)
  {
    moveLeft();
  }
  if (e.keyCode == 38)
  {
    moveUp();
  }
  if (e.keyCode == 40)
  {
    moveDown();
  }
}

function moveRight()
{

}

function moveLeft()
{

}

function moveUp()
{

}

function moveDown()
{

}

document.onkeydown = keyCodeMover; //to move with the keys






  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
//config is copy pasted
let config =
    {
        type: Phaser.WEBGL,
        width: window.innerWidth-(window.innerWidth/6),
        height: window.innerHeight-(window.innerHeight/8),
        backgroundColor: 'white',
        margin: 0,
        parent: 'gameArea',
        scene:
            {
                preload: preload,
                create: create,
                update: update
            }
    };

let snake;
let food;
let cursors;
let points = 0;

//direction in which player can move
let UP = 0;
let DOWN = 1;
let LEFT = 2;
let RIGHT = 3;

let game = new Phaser.Game(config);
let gameOverSound;
let eating

function preload ()
{
    this.load.image('food', './img/food.png');
    this.load.image('head', './img/head.png');
    this.load.image('body', './img/snake.png');
    gameOverSound = new playSound("./music/gameOver.ogg");
    eating = new playSound("./music/eating.ogg");
}

function create ()
{
    //this.add.image(10, 10, 'food');
    //this.add.image(10, 10, 'body');
    let Food = new Phaser.Class
    (

        {

            Extends: Phaser.GameObjects.Image,

            initialize:

                function Food (scene, x, y)
                {
                    Phaser.GameObjects.Image.call(this, scene)

                    this.setTexture('food');
                    //first position of food
                    this.setPosition(x * 16, y * 16);
                    //places the top-left of the texture on (sprite.x, sprite.y) theoretically
                    this.setOrigin(0);

                    this.total = 0;

                    scene.children.add(this);
                },

            eat: function ()
            {
                this.total++;
            }
        }
    );

    let Snake = new Phaser.Class(
        {

            initialize:

                //major snake attributes
                function Snake (scene, x, y)
                {
                    this.headPosition = new Phaser.Geom.Point(x, y);

                    this.body = scene.add.group();

                    this.head = this.body.create(x * 16, y * 16, 'head');
                    this.head.setOrigin(0);

                    this.alive = true;

                    this.speed = 100;

                    this.moveTime = 0;

                    this.tail = new Phaser.Geom.Point(x, y);

                    this.heading = RIGHT;
                    this.direction = RIGHT;
                },

            //major snake functions
            update: function (time)
            {
                if (time >= this.moveTime)
                {
                    return this.move(time);
                }
            },

            faceLeft: function ()
            {
                if (this.direction === UP || this.direction === DOWN)
                {
                    this.heading = LEFT;
                }
            },

            faceRight: function ()
            {
                if (this.direction === UP || this.direction === DOWN)
                {
                    this.heading = RIGHT;
                }
            },

            faceUp: function ()
            {
                if (this.direction === LEFT || this.direction === RIGHT)
                {
                    this.heading = UP;
                }
            },

            faceDown: function ()
            {
                if (this.direction === LEFT || this.direction === RIGHT)
                {
                    this.heading = DOWN;
                }
            },

            move: function (time)
            {
                let minX = 0;
                let maxX = config.width;
                let minY = 0;
                let maxY = config.height;
                switch (this.heading)
                {
                    case LEFT:
                        this.headPosition.x = Phaser.Math.Wrap(this.headPosition.x - 16, minX, maxX);
                        break;

                    case RIGHT:
                        this.headPosition.x = Phaser.Math.Wrap(this.headPosition.x + 16, minX, maxX);
                        break;

                    case UP:
                        this.headPosition.y = Phaser.Math.Wrap(this.headPosition.y - 16, minY, maxY);
                        break;

                    case DOWN:
                        this.headPosition.y = Phaser.Math.Wrap(this.headPosition.y + 16, minY, maxY);
                        break;
                }

                this.direction = this.heading;

                //Update the body and place the last coordinate into this.tail
                Phaser.Actions.ShiftPosition(this.body.getChildren(), this.headPosition.x , this.headPosition.y , 1, this.tail);

                //Has the player biten himself?

                let hitBody = Phaser.Actions.GetFirst(this.body.getChildren(), { x: this.head.x, y: this.head.y }, 1);

                if (hitBody)
                {
                    gameOverSound.play();

                    alert("You died! You have received: " + points + "points!");
                    console.log('dead');
                    this.alive = false;
                    if (confirm("Do you want to play another round?"))
                    {
                        location.reload();
                    }

                    return false;

                }
                else
                {

                    //  Update the timer ready for the next movement (phaser tutorial)
                    this.moveTime = time + this.speed;
                    return true;

                }
            },

            grow: function ()
            {

                let newPart = this.body.create(this.tail.x, this.tail.y, 'body');
                newPart.setOrigin(0);

            },

            collideWithFood: function (food)
            {
                let radius = 16;
                if (this.head.x >= food.x-radius && this.head.x <= (food.x + radius) && this.head.y >= food.y-radius && this.head.y <= (food.y + radius))
                {

                    this.grow();
                    food.eat();
                    //Each fifth item increases speed (combine factor with difficulty?)
                    if (this.speed > 20 && food.total % 5 === 0)
                    {

                        this.speed -= 10;
                        points += 10;

                    }
                    else
                    {
                        points++;
                    }

                    return true;
                }
                else
                {
                    return false;
                }

            },

            updateGrid: function (grid)
            {
                this.body.children.each(function (segment)
                {

                    let bx = parseInt(segment.x / 16);
                    let by = parseInt(segment.y / 16);

                    grid[by][bx] = false;

                });

                return grid;
            }

        });

        food = new Food(this, 3, 4);

        snake = new Snake(this, 8, 8);

        //game.debug.body(snake);

        //set keyboard inputs
        cursors = this.input.keyboard.createCursorKeys();
}

function update (time, delta)
{
    if (!snake.alive)
    {
        return;
    }

    if (cursors.left.isDown)
    {
        snake.faceLeft();
    }
    else if (cursors.right.isDown)
    {
        snake.faceRight();
    }
    else if (cursors.up.isDown)
    {
        snake.faceUp();
    }
    else if (cursors.down.isDown)
    {
        snake.faceDown();
    }

    if (snake.update(time))
    {
        //  If the snake updated, we need to check for collision against food

        if (snake.collideWithFood(food))
        {
            eating.play();
            repositionFood();
        }
    }

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function playSound(src)
{
    this.sound = document.createElement("audio");
    this.sound.src = src;
    this.sound.setAttribute("preload", "auto");
    this.sound.setAttribute("controls", "none");
    this.sound.style.display = "none";
    document.body.appendChild(this.sound);
    this.play = function()
    {
        this.sound.play();
    }
    this.stop = function(){
        this.sound.pause();
    }
}

6.5.7. Definitely Not Castlevania by Kevin Fayard (Phaser.js)

definitelyNotCastlevania

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Not Castlevania</title>
        <meta charset="utf-8">
        
        <link rel="stylesheet" href="style.css">
        
        <!-- Global site tag (gtag.js) - Google Analytics -->
        <script async src="https://www.googletagmanager.com/gtag/js?id=G-PZVPPFPME7"></script>
        <script>
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            
            gtag('config', 'G-PZVPPFPME7');
        </script>
        
        <!-- Engine -->
        <script type="text/javascript" src="phaser.min.js"></script>
        
        <!-- Scenes -->
        <script type="text/javascript" src="sceneBoot.js"></script>
        <script type="text/javascript" src="sceneLevel1.js"></script>
        <script type="text/javascript" src="sceneGameOver.js"></script>
        
        <!-- Entities -->
        <script type="text/javascript" src="playerChar.js"></script>
        <script type="text/javascript" src="enemy.js"></script>
        <script type="text/javascript" src="enemy1.js"></script>
        <script type="text/javascript" src="enemy2.js"></script>
        
        <!-- Game -->
        <script type="text/javascript" src="game.js"></script>
    </head>
    <body>
        <h1>Definitely Not Castlevania</h1>
        <div class="flexContainer">
            <section class ="gameInfo">
                <h2>Controls</h2>
                <ul>
                    <li>Walk: Cursor Left/Right</li>
                    <li>Duck: Cursor Down</li>
                    <li>Attack: A</li>
                    <li>Jump: D</li>
                    <li>Slide/Drop Down: Down + D</li>
                </ul>
                <h2>Contact</h2>
                <p>
                    This game was developed by Kevin Fayard as part of a graded assignment for Web and Mobile Programming 2 in the BTS Game Programming and Game Design at the Lycée des Arts et Métiers in Luxembourg.<br><br>
                    
                    It's pretty rough and basically just a foundation for a real game. Still, I hope you like it. If you somehow found this game without being a classmate of mine, feel free to hit me up on Discord if you would like to talk.<br><br>
                    
                    <b>Discord:</b> Ashgan#6716
                </p>
            </section>                       
        </div>        
    </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
body {
    margin: 0;
    background-color: black;
    color: white;
}
canvas {
    display: block;
    width: 80%;
    height: auto;
}
.flexContainer {
    display: flex;
    flex-direction: row-reverse;
    justify-content: flex-end;
    align-items: flex-start;
    width: 90%;
    max-width: 2000px;
    margin: auto;
}
.gameInfo {
    padding: 0 1em;
}
h1 {
    text-align: center;
}
h2 {
    margin: 0;
}
ul {
    margin-top: 0;
}
.gameInfo p {
    max-width: 250px;
    margin-top: 0;
    text-align: justify;
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
class sceneBoot extends Phaser.Scene {
    constructor() {
        super("bootGame");
        this.loadTime = 2.5;
        this.loadTimeCurrent = 0;
    }
    
    preload() {
        //Load Player Assets
        this.load.spritesheet('playerWalk', 'assets/Character_Walk.png',{
            frameWidth: 32,
            frameHeight: 32
        });
        this.load.spritesheet('playerStand', 'assets/Character_Stand.png',{
            frameWidth: 32,
            frameHeight: 32
        });
        this.load.spritesheet('playerDuck', 'assets/Character_Duck.png',{
            frameWidth: 32,
            frameHeight: 32
        });
        this.load.spritesheet('playerAttack', 'assets/Character_Attack_Wide.png',{
            frameWidth: 96,
            frameHeight: 32
        });
        this.load.spritesheet('playerDead', 'assets/Character_Dead.png',{
            frameWidth: 32,
            frameHeight: 32
        });
        this.load.image('playerAttackHit', 'assets/Character_Attack_Hit.png');
        this.load.image('playerAttackHit1', 'assets/Character_Attack_Hit1.png');
                
        this.load.image('enemyProjectile1', 'assets/Projectile.png');
        this.load.image('healthBlip', 'assets/HealthBlip.png');
        this.load.image('playerFont', 'assets/PlayerFont.png');
        this.load.image('playerFontS', 'assets/PlayerFontSmaller.png');
        this.load.image('interfaceBG', 'assets/UI_BG.png');
        
        //Load Level Assets
        this.load.image('tileSet', 'assets/tileSet.png');
        this.load.image('platformThin', 'assets/thinPlatform.png');
        this.load.image('spike1', 'assets/Spike1.png');
        this.load.image('spike2', 'assets/Spike2.png');
        this.load.image('spike3', 'assets/Spike3.png');
        this.load.image('enemy1', 'assets/Enemy1.png');
        this.load.image('enemy2', 'assets/Enemy2.png');
        this.load.tilemapTiledJSON('map-level1', 'assets/Level1.json');
    }
    
    getDelta() {
        let currentTime = this.time.now;
        let delta = currentTime - this.lastTime;
        this.lastTime = this.time.now;
        return delta/1000;
    }
    
    create() {
        this.lastTime = this.time.now;
        
        //Display Load Text
        this.add.text(29, 20, "Loading game...");
        
        //Prepare Player Animations
        this.anims.create({
            key: "playerIdle",
            frames: this.anims.generateFrameNumbers("playerStand"),
            frameRate: 2,
            repeat: 0
        });
        this.anims.create({
            key: "playerStand",
            frames: this.anims.generateFrameNumbers("playerStand"),
            frameRate: 2,
            repeat: 0
        });
        this.anims.create({
            key: "playerDuck",
            frames: this.anims.generateFrameNumbers("playerDuck"),
            frameRate: 2,
            repeat: 0
        });
        this.anims.create({
            key: "playerWalk",
            frames: this.anims.generateFrameNumbers("playerWalk"),
            frameRate: 2,
            repeat: -1
        });
        this.anims.create({
            key: "playerAttack",
            frames: this.anims.generateFrameNumbers("playerAttack"),
            frameRate: 2,
            repeat: -1
        });
        this.anims.create({
            key: "playerDead",
            frames: this.anims.generateFrameNumbers("playerDead"),
            frameRate: 2,
            repeat: 0
        });
    }
    
    update() {
        //Wait 5 seconds, then launch first level
        this.loadTimeCurrent += this.getDelta();
        if (this.loadTimeCurrent > this.loadTime) this.scene.start("playGame");
    }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class sceneLevel1 extends Phaser.Scene {
    constructor() {
        super("playGame");
    }
    
    preload() {
        if (DEBUG) {
            counter += 1;
            console.log("Iteration: " + counter);
        }
        
        //Reset Level
        this.events.once(Phaser.Scenes.Events.WAKE, () => this.scene.restart());
        
        //Create Map
        const map = this.make.tilemap({key:'map-level1'});
        const tileset = map.addTilesetImage('tileSet', 'tileSet');
        
        this.background         = map.createLayer('Background', tileset, 0,0);
        this.detailsbackground  = map.createLayer('DetailsBackground', tileset, 0,0);
        this.detailsdeeper      = map.createLayer('DetailsDeeper', tileset, 0,0);
        this.details            = map.createLayer('Details', tileset, 0,0);
        this.platforms          = map.createLayer('Platforms', tileset, 0,0);        
        
        this.platformsThin      = this.physics.add.group({
            allowGravity: false,
            immovable: true
        });
        const platformsThinObjects = map.getObjectLayer('PlatformsThin')['objects'];
        
        this.spikes             = this.physics.add.group({
            allowGravity: false,
            immovable: true
        });
        const spikesObjects = map.getObjectLayer('Spikes')['objects'];
        
        this.roomTeleporters    = this.physics.add.group({
            allowGravity: false,
            immovable: true
        });
        const roomTeleportersObjects = map.getObjectLayer('RoomTeleporters')['objects'];
        
        //Spawn Player
        this.player = new playerChar(this, (3*16), (17*16), "playerStand");
        
        //Create Enemy Group
        this.enemyGroup = this.physics.add.group();
        const enemyGroupObjects = map.getObjectLayer('Enemy')['objects'];
        
        //Set Up Camera
        this.cameras.main.setScroll(16, 16);
        //this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
        //this.cameras.main.startFollow(this.player);
        
        //Set Up Colliders        
        //Define which tiles on the layer collide
        this.platforms.setCollisionByExclusion(-1, true);
        
        //Set Up Thin Platforms
        platformsThinObjects.forEach(platformsThinObject => {
            const platformThin = this.platformsThin.create(platformsThinObject.x+8, platformsThinObject.y-8, 'platformThin');
            //platformThin.body.setSize(platformThin.width, platformThin.height - 15).setOffset(0, 0);
            platformThin.body.checkCollision.down = false;
            platformThin.body.checkCollision.left = false;
            platformThin.body.checkCollision.right = false;
        });
        
        //Set Up Spikes
        spikesObjects.forEach(spikesObject => {
            let spike;
            let type;
            switch(spikesObject.gid) {                    
                case 141:
                    spike = this.spikes.create(spikesObject.x+8, spikesObject.y-8, 'spike1');
                    type = "normalSpike";
                    break;
                case 142:
                    spike = this.spikes.create(spikesObject.x+8, spikesObject.y-8, 'spike2');
                    type = "normalSpike";
                    break;
                case 143:
                    spike = this.spikes.create(spikesObject.x+8, spikesObject.y-8, 'spike3');
                    type = "normalSpike";
                    break;
                case 22:
                    spike = this.spikes.create(spikesObject.x+8, spikesObject.y-8, 'spike1');
                    type = "instantDeath";
                    break;
            }
            switch(type){
                case "normalSpike":{
                    spike.contactDamage = 1;
                    if (spikesObject.flippedVertical) {
                        spike.body.setSize(spike.width, spike.height-8).setOffset(0,0);
                        spike.flipY = true;
                    }
                    else {
                        spike.body.setSize(spike.width, spike.height-8).setOffset(0,8);
                    }
                    break;
                }
                case "instantDeath":{
                    spike.setAlpha(0);
                    spike.contactDamage = 10;
                    break;
                }
            }
            //spike.body.setOffset(0, 16);
            //spike.body.checkCollision.down = false;
            //spike.body.checkCollision.left = false;
            //spike.body.checkCollision.right = false;
            //const spike = this.spikes.create(spikesObject.x+8, spikesObject.y-8, 'spike1');
            //platformThin.body.setSize(platformThin.width, platformThin.height);
        });
        
        //Set Up Teleporters
        roomTeleportersObjects.forEach(roomTeleportersObject => {
            let roomTeleporter;
            roomTeleporter = this.roomTeleporters.create(roomTeleportersObject.x+8, roomTeleportersObject.y-8, 'enemy1');
            roomTeleporter.body.setSize(1, roomTeleporter.height*2.5).setOffset(8,-24);
            roomTeleporter.alpha = 0;
            roomTeleporter.name = roomTeleportersObject.name;
        });
        
        //Set Up Enemies
        enemyGroupObjects.forEach(enemyGroupObject => {
            let enemyObject;
            switch(enemyGroupObject.gid) {
                case 6:
                    enemyObject = new enemy1(enemyGroupObject.flippedHorizontal, this, enemyGroupObject.x+8, enemyGroupObject.y-8, "enemy1");
                    this.enemyGroup.add(enemyObject);
                    this.physics.add.collider(enemyObject, this.platforms);
                    this.physics.add.collider(enemyObject, this.platformsThin);
                    break;
                case 7:
                    enemyObject = new enemy2(enemyGroupObject.flippedHorizontal, this, enemyGroupObject.x+8, enemyGroupObject.y-8, "enemy2");
                    this.enemyGroup.add(enemyObject);
                    this.physics.add.collider(enemyObject, this.platforms);
                    this.physics.add.collider(enemyObject, this.platformsThin);
                    break;
            }
        });
        
        //Create colliders
        this.physics.add.collider(this.player, this.platforms);
        this.physics.add.collider(this.player, this.platformsThin);
        this.physics.add.overlap(this.player, this.spikes, function(player, spike) {
            player.takeDamage(spike.contactDamage);
        });
        this.physics.add.overlap(this.player, this.enemyGroup, function(player, enemy) {
            player.takeDamage(enemy.contactDamage);
        });
        
        this.physics.add.overlap(this.player, this.roomTeleporters, function(player, teleporter) {
            teleporter.scene.switchRoom(teleporter);
        });
        
        //Set Up Delta Time
        this.lastTime = this.time.now;
        
        //FPS Count
        this.fpstime = 0;
        this.framecount = 0;
        this.stop = false;
    }    
    
    create(){        
        
    }
    
    switchRoom(teleporter) {
        switch(teleporter.name){
            case "room1_exit1":
                this.player.setPosition(33*16, 15*16);
                this.cameras.main.setScroll(33*16, 1*16);
                break;
            case "room2_exit1":
                this.player.setPosition(65*16, 5*16);
                this.cameras.main.setScroll(65*16, 1*16);
                break;
            case "room2_exit2":
                this.player.setPosition(65*16, 15*16);
                this.cameras.main.setScroll(65*16, 1*16);
                break;
            case "room3_exit1":
                //Win Game
                this.cameras.main.fade(3500, 0, 0, 0);
                this.time.addEvent({
                    delay: 5000,
                    callback: () => {
                        //Remove Key Events
                        this.input.keyboard.removeKey('A', true);
                        this.input.keyboard.removeKey('D', true);
                        
                        //Run Victory
                        this.scene.sleep();
                        this.scene.run('gameOver', { victory: true });
                    }
                });
                break;
            default:
                return;
        }
    }
    
    getDelta() {
        let currentTime = this.time.now;
        let delta = currentTime - this.lastTime;
        this.lastTime = this.time.now;
        return delta/1000;
    }
    
    fpsTimerUpdate() {
        if (this.fpstime < 1) {
            this.fpstime = this.fpstime + this.getDelta();
            this.framecount += 1;
        }
        else if (this.fpstime >= 1 && !this.stop) {
            console.log("Frames in first second: " + this.framecount);
            this.stop = true;
        }
    }
    
    update(){
        //this.fpsTimerUpdate();
        let delta = this.getDelta();
        this.player.playerUpdate(delta);
        this.enemyGroup.children.each(function(enemy) {
           enemy.enemyUpdate(delta);
        }, this);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class sceneGameOver extends Phaser.Scene {
    constructor() {
        super("gameOver");
        this.victory = false;
    }
    
    init(data) {
        this.victory = data.victory;
    }
    
    preload() {
        this.load.image('victoryText', 'assets/VictoryScreen.png');
        this.load.image('gameOverText', 'assets/GameOverScreen.png');        
        this.load.image('gameOverImage', 'assets/GameOverScreenThing.png');
    }
    
    create() {        
        this.thingMid = 280;
        this.thingVariance = 15;
        
        this.cameras.main.setBackgroundColor('#E0E0E0');        
        this.movingThing = this.physics.add.sprite(240,this.thingMid,'gameOverImage');
        if (this.victory) {
            this.add.sprite(240,160,'victoryText');
        }
        else {
            this.add.sprite(240,160,'gameOverText');
        }        
        
        this.movingThing.setVelocityY(-10);
        
        //Reset Game
        //let bootscene = this.scene.get('bootGame');
        //let gameScene = this.scene.get('playGame');
        //bootscene.scene.restart('bootGame');
        //gameScene.scene.restart('playGame');
        
        //gameScene.player.currentHealth = gameScene.player.maxHealth;
        //gameScene.player.dead = false;
        
        /*if (gameScene.player) {
            gameScene.player.destroy();
            delete gameScene.player;
            console.log(gameScene.player);
        }*/
        
        //Input
        this.keySpace = this.input.keyboard.addKey('SPACE');
        this.keySpace.on('down', () => {
            this.cameras.main.fade(900, 0, 0, 0);
            this.time.addEvent({
                    delay: 2000,
                    callback: () => {
                        this.scene.wake('playGame');
                        this.scene.stop('gameOver');
                    }
                });
        });
    }
    
    update() {
        if(this.movingThing.y < this.thingMid-this.thingVariance) {
            this.movingThing.setVelocityY(10);
        }
        else if(this.movingThing.y > this.thingMid+this.thingVariance) {
            this.movingThing.setVelocityY(-10);
        }
    }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
class playerChar extends Phaser.Physics.Arcade.Sprite {    
    constructor(scene, x, y, texture, frame) {
        super(scene, x, y, texture, frame);
        scene.add.existing(this);
        scene.physics.world.enableBody(this);
        
        //Power Variables
        this.moveSpeed = 43;        //Default 40
        this.jumpVelocity = 195;    //Default 195
        this.setGravityY(500);      //Default 500
        this.maxHealth = 10;        //Default 10
        this.invulnTime = 1.5;      //Default 1.5
        this.damage = 1;            //Default 1
        
        //Set Collision Box
        this.setSize(96, 32);
        this.hitbox = {
            width: 8,
            height: 22
        };
        this.setBodySize(this.hitbox.width,this.hitbox.height, false);
        this.setOffset((this.width-this.body.sourceWidth)/2, (this.height-this.body.sourceHeight));
        
        //Helper Variables
        this.airborne = false;
        this.ducking = false;
        this.slide = false;
        this.attacking = false;
        this.facingDir = 1;
        
        this.currentHealth = this.maxHealth;
        this.dead = false;
        
        this.attackTime = 0.3;
        this.attackTimeCurrent = this.attackTime+1;
        
        this.invulnTimeCurrent = this.invulnTime+1;
        
        this.blinkInterval = 0.1;
        this.blinkTimer = 0;
        this.blinkState = false;
        
        //Set Up Input
        this.keyA = scene.input.keyboard.addKey('A');
        this.keyD = scene.input.keyboard.addKey('D');
        this.cursors = scene.input.keyboard.createCursorKeys();

        this.keyA.on('down', () => {this.attack(this)});
        this.keyD.on('down', () => {this.jump(this)});

        //Set Up UI
        this.interfaceBG = this.scene.add.sprite(240, 288+16, 'interfaceBG');
        this.interfaceBG.setScrollFactor(0);
        
        this.playerFont = this.scene.add.sprite(32+4, 288+15, 'playerFontS');
        this.playerFont.setScrollFactor(0);
        
        this.healthGroup = this.scene.add.group();
        
        let i;
        for (i = 0; i<this.maxHealth; i++) {
            let blip = this.healthGroup.create(82+(i*8), 287+17, 'healthBlip');
            blip.setScrollFactor(0);
        }
        
        //DEBUG
        if (DEBUG) {
            console.log("Player successfully created!");
            console.log(this);
            console.log(this.scene.input.keyboard.keys);
        }
    }
    
    uiUpdateHealth() {
        let childArray = this.healthGroup.children.entries;
        let i;
        for (i=0;i<this.maxHealth;i++){
            if(i>=this.currentHealth) {
                childArray[i].setAlpha(0);
            }
        }
    }
    
    invulnBlinking(deltaTime) {
        if (this.invulnTimeCurrent < this.invulnTime) {
            this.blinkTimer += deltaTime;
            
            if (this.blinkTimer >= this.blinkInterval) {
                if (!this.blinkState) {
                    this.setAlpha(0.2);
                    this.blinkState = true;
                }
                
                else {
                    this.setAlpha(1);
                    this.blinkState = false;
                }
                this.blinkTimer = 0;
            }
        }
        else this.blinkTimer = 0;
    }
    
    resetTint() {
        if (this.invulnTimeCurrent > this.invulnTime) this.setTint();
    }
    
    setHitBox(x, y = x) {
        this.hitbox.width = x;
        this.hitbox.height = y;
        this.body.setEnable = false;
    }
    
    adjustHitBox() {
        this.setSize(96, 32);
        this.setBodySize(this.hitbox.width,this.hitbox.height, false);
        this.setOffset((this.width-this.body.sourceWidth)/2, (this.height-this.body.sourceHeight));
    }
    
    attack(parent) {        
        if (!this.attacking && !this.slide && !this.dead) {
            this.attacking = true;
            this.attackTimeCurrent = 0;
            
            //Set Up Attack Entity
            this.attackHit = parent.scene.physics.add.sprite(parent.x, parent.y, "playerAttackHit1");
            if (this.facingDir == 1) {
                this.attackHit.setPosition(this.x+21, this.y-2);
            }
            else {
                this.attackHit.setPosition(this.x-21, this.y-2);
                this.attackHit.flipX = true;
            }
            this.attackHit.contactDamage = this.damage;
            this.attackHit.alpha = 0; //Invisible sprite to fix the visual delay
            
            //Add Scene Collider For Entity
            parent.scene.physics.add.overlap(this.attackHit, parent.scene.enemyGroup, function(playerHit, enemy) {
                enemy.takeDamage(playerHit.contactDamage);
            });
        }
    }
    
    moveAttack() {
        if (this.attackHit) {
            if (this.facingDir == 1) {
                this.attackHit.setPosition(this.x+21, this.y-2);
                this.attackHit.flipX = false;
            }
            else {
                this.attackHit.setPosition(this.x-21, this.y-2);
                this.attackHit.flipX = true;
            }
        }
    }
    
    isInvuln() {
        if (this.invulnTimeCurrent > this.invulnTime) return false;
        else return true;
    }
    
    takeDamage(damage) {
        if (!this.isInvuln() && !this.slide && !this.dead) {
            //Reduce Health
            this.currentHealth -= damage;
            
            //Set Tint
            //this.setTint(0xffff00);
            
            //Launch Player
            this.setVelocityY(-100);
            this.setVelocityX(60*(-this.facingDir));
            this.airborne = true;
            this.ducking = false;
            if(this.slide){
                this.slide = false;              
                this.setHitBox(8,22);
            }
            
            //Update UI
            this.uiUpdateHealth();
            
            if (DEBUG) console.log("Player: " + this.currentHealth + " / " + this.maxHealth);
            
            //Check if player died
            if (this.currentHealth <= 0) {
                //Debug
                if (DEBUG) console.log("Player died.");
                
                //Set Up Game Over + Prepare Player To Be Refreshed
                this.dead = true;
                this.scene.cameras.main.fade(3500, 0, 0, 0);
                this.scene.time.addEvent({
                    delay: 5000,
                    callback: () => {
                        //Remove Key Events
                        this.scene.input.keyboard.removeKey('A', true);
                        this.scene.input.keyboard.removeKey('D', true);
                        
                        //Run Game Over
                        this.scene.scene.sleep();
                        this.scene.scene.run('gameOver', { victory: false });
                    }
                });
            }
            else {
                this.invulnTimeCurrent = 0;
            }
        }
    }
    
    invulnTimer(deltaTime) {
        if (this.invulnTimeCurrent < this.invulnTime) this.invulnTimeCurrent += deltaTime;
    }
    attackTimer(deltaTime) {
        if (this.attackTimeCurrent < this.attackTime) {
            this.attackTimeCurrent += deltaTime;
        }
        else if (this.attacking) {            
            //Destroy Attack Hitbox
            this.attacking = false;
            this.attackHit.destroy();
            this.attackHit = null;
        }
    }
    
    isBusy() {
        return (this.airborne || this.ducking || this.slide || this.attacking);
    }
    
    playerUpdate(deltaTime) {
        //This function is looped in the scene update. It is not event-based.
        if(this.invulnTimeCurrent > 0.05) {
            this.moveHorizontal();
            this.duck();
            this.fallingAndLanding();
            this.slideMove(deltaTime);
            this.moveAttack();
            
            //if (DEBUG) console.log(this.body.velocity);
            //if (DEBUG) console.log(this.invulnTimeCurrent);
            //if (DEBUG) console.log("Player X Velocity: " + this.body.velocity.x);
            //if (DEBUG) console.log(this.facingDir);
        }
        this.animationHandler();
        this.adjustHitBox();
        this.invulnTimer(deltaTime);
        this.attackTimer(deltaTime);
        //this.resetTint();
        this.invulnBlinking(deltaTime);
    }
    
    moveHorizontal() {
        //Set horizontal input direction
        let moveDirX = (this.cursors.right.isDown) - (this.cursors.left.isDown);
        
        //Walking
        if (this.dead && !this.airborne) {
            this.setVelocityX(0);
        }
        else {
            if (!this.isBusy() && !this.dead) {
                this.setVelocityX(this.moveSpeed * moveDirX);
                if (moveDirX != 0) this.facingDir = moveDirX;
            }
            else if (this.ducking || this.attacking && !this.airborne) this.setVelocityX(0);
        }
    }
    
    jump(parent) {
        //console.log("Parent:");
        //console.log(parent);
        //Regular Jump
        if (!parent.isBusy() && !parent.dead) {
            parent.setVelocityY(-(parent.jumpVelocity));
            parent.airborne = true;
        }
        //Slide or Fall Through
        else if (!parent.airborne && parent.ducking && !parent.slide && !parent.dead) {
            let xCoord = this.x - (this.width / 2);
            let yCoord = this.y + (this.height / 2) + 1;
            let collideBodies = parent.scene.physics.overlapRect(xCoord, yCoord, this.width, 1);
            
            if (!collideBodies.length) {
                parent.slideTrigger();
            }
            else {
                parent.setPosition(parent.x, parent.y+5);
            }
        }
    }
    
    duck() {
        if (this.cursors.down.isDown && !this.airborne && !this.slide && !this.dead) {
            if (!this.ducking){
                this.setHitBox(8,15);
            }
            this.ducking = true;
        }
        else {
            if (this.ducking) {                
                this.setHitBox(8,22);
            }
            this.ducking = false;
            
        }
    }
    
    slideTrigger() {
        this.slide = true;
        this.ducking = false;
        this.setVelocityX(200 * this.facingDir);
    }
    
    slideMove(deltaTime) {        
        if (this.slide) {
            if (this.body.velocity.x < 0 && this.facingDir == 1 || this.body.velocity.x > 0 && this.facingDir == -1) {
                this.slide = false;
                if (this.cursors.down.isDown) {
                    this.setHitBox(8,15);
                    this.ducking = true;
                }
                else {
                    this.setHitBox(8,22);
                }
            }
            else {
                this.body.velocity.x -= ((300 * this.facingDir) * deltaTime); //4
                //console.log((4 * this.facingDir) * deltaTime);
                //console.log(this.body.velocity.x);
            }
        }
    }
    
    fallingAndLanding() {
        if (!this.body.blocked.down) {
            this.airborne = true;
        }
        else {
            this.airborne = false;
        }
    }
    
    animationHandler() {
        let moveDirX = (this.cursors.right.isDown) - (this.cursors.left.isDown);
        if (moveDirX == 0 && !this.airborne) {
            this.play("playerStand");
        }
        if (moveDirX < 0 && !this.airborne && !this.ducking && !this.slide && !this.dead) {
            this.flipX = true;
            this.facingDir = moveDirX;
            this.play("playerWalk", true);
        }
        else if (moveDirX > 0 && !this.airborne && !this.ducking && !this.slide && !this.dead) {
            this.flipX = false;
            this.facingDir = moveDirX;
            this.play("playerWalk", true);
        }
        if (this.ducking || this.slide) {
            this.play("playerDuck", true);
            if (moveDirX < 0  && !this.slide) {this.flipX = true; this.facingDir = moveDirX;}
            else if (moveDirX > 0 && !this.slide) {this.flipX = false; this.facingDir = moveDirX;}
        }
        if (this.body.velocity.y < 0 && this.airborne){
            this.play("playerDuck", true);
        }
        else if (this.airborne) {
            this.play("playerStand");
        }
        if (this.attacking) {
            this.play("playerAttack");
        }
        if (!this.airborne && this.dead) {
            this.play("playerDead");
        }
    }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
class enemy extends Phaser.Physics.Arcade.Sprite {    
    constructor(flipped, scene, x, y, texture, frame) {
        super(scene, x, y, texture, frame);
        scene.add.existing(this);
        scene.physics.world.enableBody(this);
        
        //Power Variables
        this.moveSpeed;
        this.maxHealth = 1;        
        this.invulnTime = 0.5;
        this.contactDamage = 0;
        
        //Helper Variables
        this.airborne = false;
        this.lifeTime = 0;
        this.currentHealth = this.maxHealth;
        
        this.blinkInterval = 0.1;
        this.blinkTimer = 0;
        this.blinkState = false;
        
        if (flipped) {
            this.facingDir = -1;
        }
        else {
            this.facingDir = 1;
        }
        
        this.invulnTimeCurrent = this.invulnTime+1;
        
        this.active = false;
    }
    
    invulnBlinking() {
        if (this.invulnTimeCurrent < this.invulnTime) {
            this.blinkTimer += this.deltaT;
            
            if (this.blinkTimer >= this.blinkInterval) {
                if (!this.blinkState) {
                    this.setAlpha(0.2);
                    this.blinkState = true;
                }
                
                else {
                    this.setAlpha(1);
                    this.blinkState = false;
                }
                this.blinkTimer = 0;
            }
        }
        else this.blinkTimer = 0;
    }
    
    activateWhenOnScreen() {
        if(this.scene.cameras.main.worldView.contains(this.x, this.y) && this.active == false) {
            this.active = true;
        }
    }
    
    takeDamage(damage) {
        if (this.invulnTimeCurrent > this.invulnTime) {
            //Reduce Health
            this.currentHealth -= damage;
            
            if (DEBUG) console.log("Enemy: " + this.currentHealth + " / " + this.maxHealth);
            
            //Feedback
            //this.setTint(0x0000ff);
            
            //Check if enemy died
            if (this.currentHealth <= 0) {
                if (DEBUG) console.log("Enemy destroyed.");
                this.destroy();
            }
            else {
                this.invulnTimeCurrent = 0;
            }
        }
    }
    
    resetTint() {
        if (this.invulnTimeCurrent > this.invulnTime) this.setTint();
    }
    
    invulnTimer() {
        if (this.invulnTimeCurrent < this.invulnTime) this.invulnTimeCurrent += this.deltaT;
    }
        
    enemyUpdate(deltaTime) {
        //This function is looped in the scene update. It is not event-based.
        this.activateWhenOnScreen();
        if (this.active){
            this.deltaT = deltaTime;
            this.behaviorLoop();
            this.fallingAndLanding();
            this.animationHandler();
            this.invulnTimer();
            this.resetTint();
            this.invulnBlinking();   
        }        
    }
    
    behaviorLoop() {
        //Placeholder
    }
    
    fallingAndLanding() {
        if (!this.body.blocked.down) {
            this.airborne = true;
        }
        else {
            this.airborne = false;
        }
    }
    
    animationHandler() {
        if (this.facingDir == -1) {
            this.flipX = true;
        }
        else this.flipX = false;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class enemy1 extends enemy {
    constructor(flipped, scene, x, y, texture, frame) {
        super(flipped, scene, x, y, texture, frame);
                
        //Power Variables
        this.contactDamage = 1;
        this.moveSpeed = 20;
        this.setGravityY(500);
        this.maxHealth = 2;
        
        //Helper Variables
        this.currentHealth = this.maxHealth;
    }
    
    behaviorLoop() {
        if (!this.airborne) this.constantMove();
        this.checkBodyBlock();
    }
    
    constantMove() {
        this.setVelocityX(this.moveSpeed * this.facingDir);
        if (this.body.gravity.y == 0) this.setGravityY(500);
    }
    
    checkBodyBlock() {
        if (this.body.blocked.left) {
            this.facingDir = 1;
        }
        else if (this.body.blocked.right) {
            this.facingDir = -1;
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class enemy2 extends enemy {
    constructor(flipped, scene, x, y, texture, frame) {
        super(flipped, scene, x, y, texture, frame);
        
        //Power Variables
        this.contactDamage = 2;
        this.moveSpeed = 30;
        this.setGravityY(500);
        this.projectileSpeed = 110;
        this.projectileDamage = 2;
        this.attackCooldown = 1.5;
        
        //Helper Variables
        this.attackCooldownCurrent = this.attackCooldown+1;
        this.currentHealth = this.maxHealth;
    }
    
    behaviorLoop() {
        this.attackTimer();
        if (!this.checkAttackCooldown()) {
            this.spawnProjectile();
            //console.log("Spawn!");
        }
    }
    
    spawnProjectile() {
        //Initialize Attack Cooldown
        this.attackCooldownCurrent = 0;
        
        //Create Entity
        this.projectile = this.scene.physics.add.sprite(this.x, this.y, "enemyProjectile1"); 
        this.projectile.body.setVelocity(this.projectileSpeed * this.facingDir, 0);
        this.projectile.contactDamage = this.projectileDamage;
        this.projectile.setBodySize(7, 7);
        
        //Set Up Entity Colliders
        this.scene.physics.add.overlap(this.projectile, this.scene.player, function(projectile, player) {
            player.takeDamage(projectile.contactDamage);
            projectile.destroy();
        });
        this.scene.physics.add.collider(this.projectile, this.scene.platforms, function(projectile, player) {
            projectile.destroy();
        });
    }
    
    checkAttackCooldown() {
        if (this.attackCooldownCurrent < this.attackCooldown) return true;
        else return false;
    }
    
    attackTimer() {
        if (this.checkAttackCooldown()) this.attackCooldownCurrent += this.deltaT;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const config = {
    width: (30*16), //Default: 30 Tiles
    height: (18*16+32), // Default: 18 Tiles + 32px (interface)
    backgroundColor: 0x1E1E1E,
    pixelArt: true,
    scene: [sceneBoot, sceneLevel1, sceneGameOver],
    title: "Not Castlevania",
    url: "https://students.btsi.lu/fayke019/WEMOB2 Game/",
    physics: {
        default: "arcade",
        arcade:{
            debug: false
        }
    }
}

const DEBUG = false;
var counter = 0;

window.onload = function () {
    var game = new Phaser.Game(config);
    const flexContainer = document.querySelector(".flexContainer");
    const gameCanvas = document.querySelector("canvas");
    flexContainer.appendChild(gameCanvas);
};

6.5.8. Sokoban by Benjamin Pitzmann (Phaser.js)

Sokoban

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<html>
  <head>
    <meta charset=UTF-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <title>Sokoban Game</title>
    <script src=//cdn.jsdelivr.net/npm/phaser@3.53.1/dist/phaser.min.js></script>
    <script src=main.js type=module></script>
  </head>
  <body>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Game from './scenes/Game.js'
import LevelFinishedScene from './scenes/LevelFinishedScene.js'
import Preloader from './scenes/Preloader.js'

const config = {
  type: Phaser.AUTO,
  width: 800,
  height: 512,
  render: {
    pixelArt: true
  },
  physics: {
    default: 'arcade',
    arcade: {
      debug: true,
      gravity: {
        y: 200
      }
    }
  },
  scene: [Preloader, Game, LevelFinishedScene]
}
export default new Phaser.Game(config)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
export default class Game extends Phaser.Scene
{
  constructor()
  {
    super('game')
    }

  preload()
    {
      //preloading my source images
        this.load.spritesheet("envi", "public/tileset/sprites.png",{
            frameWidth: 64,
            startFrame: 0
        })


        this.load.spritesheet("char", "public/tileset/slime.png",{
            frameWidth: 32,
            frameHeight: 32,
        })
    }


    create(mylevel)
    {
      const templateLevel =
      [
        [99, 99, 99, 99, 99, 99, 99, 99, 99, 99],
        [99, 99, 99, 99, 99, 99, 99, 99, 99, 99],
        [99, 99, 99, 99, 99, 99, 99, 99, 99, 99],
        [99, 99, 99, 99, 99, 99, 99, 99, 99, 99],
        [99, 99, 99, 99, 99, 99, 99, 99, 99, 99],
        [99, 99, 99, 99, 99, 99, 99, 99, 99, 99],
        [99, 99, 99, 99, 99, 99, 99, 99, 99, 99],
        [99, 99, 99, 99, 99, 99, 99, 99, 99, 99]
      ]

      const level1 =
      [
        [99, 99, 32, 32, 32, 99, 99, 99, 99, 99],
        [99, 99, 32, 15, 32, 99, 99, 99, 99, 99],
        [99, 99, 32, 25, 32, 32, 32, 32, 99, 99],
        [32, 32, 32,  3, 25,  3, 15, 32, 99, 99],
        [32, 15, 25,  3, 50, 32, 32, 32, 99, 99],
        [32, 32, 32, 32,  3, 32, 99, 99, 99, 99],
        [99, 99, 99, 32, 15, 32, 99, 99, 99, 99],
        [99, 99, 99, 32, 32, 32, 99, 99, 99, 99]
      ]

      const level2 =
      [
        [99, 99, 99, 99, 99, 99, 99, 32, 32, 32],
        [99, 99, 99, 99, 99, 99, 99, 32, 16, 32],
        [99, 32, 32, 32, 32, 32, 32, 32, 15, 32],
        [32, 32, 50, 25, 25, 25, 25, 32, 14, 32],
        [32, 25,  0,  1,  2,  3,  4, 25, 13, 32],
        [32, 25, 25, 25, 25, 25, 25, 25, 12, 32],
        [32, 32, 32, 32, 32, 32, 25, 25, 32, 32],
        [99, 99, 99, 99, 99, 32, 32, 32, 32, 99]
      ]

      const level3 =
      [
        [99, 99, 99, 99, 99, 99, 99, 99, 99, 99],
        [99, 99, 99, 99, 99, 99, 99, 99, 99, 99],
        [99, 32, 32, 32, 32, 32, 32, 32, 99, 99],
        [32, 32, 25, 25, 25, 25, 25, 32, 32, 99],
        [32, 25, 13,  1, 14,  3, 15, 25, 32, 99],
        [32, 25,  0, 12,  2, 16,  4, 25, 32, 99],
        [32, 50, 25, 25, 25, 32, 32, 32, 32, 99],
        [32, 32, 32, 32, 32, 32, 99, 99, 99, 99]
      ]

      this.level =
      [
        level1,
        level2,
        level3
      ]

      //Creating all my objects which can be enums in modern js
      this.boxColors =
      Object.freeze(
      {
      Boxblue: 2,
      Boxbrown: 3,
      Boxgrey: 4,
      Boxpurple: 5,
      Boxblack: 1,
      Boxwhite: 0
      })

      this.targetColors =
      Object.freeze(
      {
      Targetblue: 14,
      Targetbrown: 15,
      Targetgrey: 16,
      Targetpurple: 17,
      Targetblack: 13,
      Targetwhite: 12
      })

      this.coveredTargetByBox =
      {
        14: 0,
        15: 0,
        16: 0,
        17: 0,
        13: 0,
        12: 0
      }

      this.boxesByColor = {}

      this.currentLevel = mylevel.level

      this.moveCounter = 0
      this.label = this.add.text(700, 10, `Moves: ${this.moveCounter}`)

      this.updateClick = () =>
      {
        this.scene.start(`preloader`, {level: this.currentLevel})
      }

      this.enterHoverState = (text) =>
      {
        text.setStyle({backgroundColor: 'orange'})
      }

      this.leaveHoverState = (text) =>
      {
        text.setStyle({backgroundColor: 'lightblue'})
      }

      this.restart = this.add.text(748, 50, `Retry`,{
        backgroundColor: 'lightblue',
        fill: 'black',
        fontSize: 20,
        stroke: '#000000',
        strokeThickness: 2,
        padding: { top: 5, bottom: 5, left: 5, right: 35 }})
        .setOrigin(0.5, 0.5)
        this.restart.setInteractive()
        this.restart.on('pointerdown', () => this.updateClick() )
        this.restart.on('pointerover', () => this.enterHoverState(this.restart) )
        this.restart.on('pointerout', () => this.leaveHoverState(this.restart) );



      //Defined Functions
      this.boxColorToTargetColor = (myBoxColor) =>
      {
        switch (myBoxColor)
        {
          default:
          case this.boxColors.Boxblue:
            return this.targetColors.Targetblue

          case this.boxColors.Boxgrey:
            return this.targetColors.Targetgrey

          case this.boxColors.Boxbrown:
            return this.targetColors.Targetbrown

          case this.boxColors.Boxblack:
            return this.targetColors.Targetblack

          case this.boxColors.Boxwhite:
            return this.targetColors.Targetwhite

          case this.boxColors.Boxpurple:
            return this.targetColors.Targetpurple
        }
      }

      //create all our boxes of spezific colors
      this.getSpezificBox = (layer) =>
      {
        const arrBoxColors = [
          this.boxColors.Boxblue,
          this.boxColors.Boxwhite,
          this.boxColors.Boxbrown,
          this.boxColors.Boxgrey,
          this.boxColors.Boxpurple,
          this.boxColors.Boxblack
        ]
        arrBoxColors.forEach(color => {
          this.boxesByColor[color] = layer.createFromTiles(color, 25, {key: "envi", frame: color})
            .map(box => box.setOrigin(0))
        })
      }

      //get my box
      this.getBoxDataAt = (x, y) =>
      {
        const keys = Object.keys(this.boxesByColor)
        for (let i = 0; i < keys.length; ++i)
        {
          const color = keys[i]
          const box = this.boxesByColor[color].find(box => {
            const square = box.getBounds();
            return square.contains(x,y);
          })
          if (!box){
            continue
          }
          return {
            box,
            color: parseInt(color)
          }
        }

        return undefined
      }


      //let me know if my box is on his target
      this.hasTargetAt = (x, y, tileIndex) =>
      {
        if(!this.layer){
          return false
        }

        const tile = this.layer.getTileAt(x/64, y/64)
        if (!tile)
        {
          return false
        }

        return tile.index === tileIndex
      }


      //check for winning
      this.allTargetsCovered = () =>
      {
        this.allBoxes = 0
        for (let i = 0; i < Object.keys(this.boxesByColor).length; i++){
          for (let j = 0; j < this.boxesByColor[i].length; j++){
            this.allBoxes += 1
          }
        }
        this.allcoveredTargets = 0
        Object.values(this.coveredTargetByBox).forEach(val => {
          this.allcoveredTargets += val
        });

        if (this.allcoveredTargets === this.allBoxes){
          return true
        }
        else {
          return false
        }
      }


      //Wall before me?
      this.getWallAt = (x, y) =>
      {
        return this.walls.find(wall => {
          this.rectangle = wall.getBounds();
          return this.rectangle.contains(x,y);
        })
      }


      //change the number of goals covered by its box
      this.changeCountOnTarget = (color, change) =>
      {
        this.coveredTargetByBox[color] += change
      }

      //get modifier to check for 2nd box or wall after box
      this.getBoxModifier = (direction) =>
      {
        switch (direction)
        {
          case "left":
            return {
              x: -64,
              y: 0
            }

          case "right":
            return {
              x: 64,
              y: 0
            }

          case "up":
            return {
              x: 0,
              y: -64
            }

          case "down":
            return {
              x: 0,
              y: 64
            }
        }
      }

      //setting up tween and moving player
      this.tweening = (x, y, standartTween, direction) =>
      {
        if (this.tweens.isTweening(this.char)){
          return
        }
        const boxData = this.getBoxDataAt(x, y);
        if (boxData){
          const boxModifier = this.getBoxModifier(direction)
          const isNotSpace = this.getBoxDataAt(x + boxModifier.x, y + boxModifier.y)
          const isWall = this.getWallAt(x + boxModifier.x, y + boxModifier.y)
          if (isNotSpace || isWall){
            return
          }
        }
        const wall = this.getWallAt(x, y);

        if (wall){
           return
        }

        switch(direction){
          case "down":
            this.char.anims.play("down", true)
          break;
          case "right":
            this.char.anims.play("right", true)
          break;
          case "up":
            this.char.anims.play("up", true)
          break;
          case "left":
            this.char.anims.play("left", true)
        }

        if (boxData){
           const box = boxData.box
           const boxColor = boxData.color
           const targetColor = this.boxColorToTargetColor(boxColor)
           const coveredTarget = this.hasTargetAt(box.x, box.y, targetColor)
           if (coveredTarget){
             this.changeCountOnTarget(targetColor, -1)
           }
           this.tweens.add(Object.assign(
             standartTween,
             {
             targets: box,
             onComplete: () =>
             {
                const coveredTarget = this.hasTargetAt(box.x, box.y, targetColor)
                if (coveredTarget){
                  this.changeCountOnTarget(targetColor, 1)
                }
                this.allTargetsCovered()
             },
            }
          ))
        }
        this.tweens.add(Object.assign(
          standartTween,
          {
            targets: this.char,
            onComplete: () =>
            {
              this.moveCounter++
              this.label.text = `Moves: ${this.moveCounter}`
              const levelComplete = this.allTargetsCovered()
              if (levelComplete)
              {
                this.scene.start(`level-finished`, {
                  moves: this.moveCounter,
                  level: this.currentLevel
                })
              }
            },
            onCompleteScope: this
          }
        ))
       }


        //creating my level
        this.loadLevel = this.level[this.currentLevel]

        //declaring my tilemap
        const map = this.make.tilemap({
            data: this.loadLevel,
            tileWidth: 64,
            tileHeight: 64
        })

        //setting up layer
        const envi = map.addTilesetImage("envi")
        this.layer = map.createLayer(0, envi, 0, 0)


        //adding WASD to my game inputs
        this.keyA = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A);
        this.keyS = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S);
        this.keyD = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D);
        this.keyW = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W);

        //creating my character and setting its initial position
        this.char = this.layer.createFromTiles(50, 25, {key: "char", frame: 0}).pop();
        this.char.x += 32
        this.char.y += 32
        this.char.setScale(3);


        //create all my walls
        this.walls = this.layer.createFromTiles(32, 32, {key: "envi", frame: 32})
          .map(wall => wall.setOrigin(0));

        //create spezific box
        this.getSpezificBox(this.layer)

        //Creating my animations
        this.anims.create({
            key: "left",
            frames: this.anims.generateFrameNumbers("char", {frames: [5, 9]}),
            frameRate: 6,
            repeat: -1
        })

        this.anims.create({
            key: "idle",
            frames: this.anims.generateFrameNumbers("char", {frames: [0, 7]}),
            frameRate: 4,
            repeat: -1
        })

        this.anims.create({
            key: "right",
            frames: this.anims.generateFrameNumbers("char", {frames: [6, 8]}),
            frameRate: 6,
            repeat: -1
        })

        this.anims.create({
            key: "up",
            frames: this.anims.generateFrameNumbers("char", {frames: [1, 2]}),
            frameRate: 6,
            repeat: -1
        })

        this.anims.create({
            key: "down",
            frames: this.anims.generateFrameNumbers("char", {frames: [3, 4]}),
            frameRate: 6,
            repeat: -1
        })
        this.allTargetsCovered()
    }

    update(){
      const justDown = Phaser.Input.Keyboard.JustDown(this.keyS)
      const justLeft = Phaser.Input.Keyboard.JustDown(this.keyA)
      const justUp = Phaser.Input.Keyboard.JustDown(this.keyW)
      const justRight = Phaser.Input.Keyboard.JustDown(this.keyD)

      if (justDown){
        const standartTween =
        {
          y: "+=64",
          duration: 500
        }
        this.tweening(this.char.x, this.char.y + 64, standartTween, "down")
      }

      else if (justLeft){
        const standartTween =
        {
          x: "-=64",
          duration: 500
        }
        this.tweening(this.char.x - 64, this.char.y, standartTween, "left")
      }

      else if (justUp){
        const standartTween =
        {
          y: "-=64",
          duration: 500
        }
        this.tweening(this.char.x, this.char.y - 64, standartTween, "up")
      }

      else if (justRight){
        const standartTween =
        {
          x: "+=64",
          duration: 500
        }
        this.tweening(this.char.x + 64, this.char.y, standartTween, "right")
      }
      else {
        if (!this.tweens.isTweening(this.char)){
          this.char.anims.play("idle", true)
        }
      }
    }
  }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
export default class LevelFinishedScene extends Phaser.Scene
{
  constructor()
  {
    super(`level-finished`)
  }

  create(mydata =
  {
    moves: 0,
    level: 0
  })
  {
    const width = 800
    const height = 512

    this.restart = () =>
    {
      this.scene.start(`preloader`, {level: 0})
    }

    this.updateNextClick = (data) =>
    {
      this.scene.start(`preloader`, {level: data.level + 1})
    }

    this.updateClick = (data) =>
    {
      this.scene.start(`preloader`, {level: data.level})
    }

    this.enterHoverState = (text) =>
    {
      text.setStyle({backgroundColor: 'orange'})
    }

    this.leaveHoverState = (text) =>
    {
      text.setStyle({backgroundColor: 'lightblue'})
    }

    this.add.text(width * 0.5, height * 0.4, 'Level Complete',{
      fill: 'lightblue',
      fontSize: 48
    }).setOrigin(0.5, 0.5)

    this.add.text(width * 0.5, height * 0.5, `Moves: ${mydata.moves}`,{
      fill: 'lightblue',
      fontSize: 30
    }).setOrigin(0.5, 0.5)

    this.startOver = this.add.text(width * 0.35, height * 0.6, `Retry`,{
      backgroundColor: 'lightblue',
      fill: 'black',
      fontSize: 20,
      stroke: '#000000',
      strokeThickness: 2,
      padding: { top: 5, bottom: 5, left: 5, right: 5 }})
      .setOrigin(0.5, 0.5)
      this.startOver.setInteractive()
      this.startOver.on('pointerdown', () => this.updateClick(mydata) )
      this.startOver.on('pointerover', () => this.enterHoverState(this.startOver) )
      this.startOver.on('pointerout', () => this.leaveHoverState(this.startOver) );

    if (mydata.level == 2)
    {
      this.nextLevel = this.add.text(width * 0.65, height * 0.6, `To Level 1`,{
        backgroundColor: 'lightblue',
        fill: 'black',
        fontSize: 20,
        stroke: '#000000',
        strokeThickness: 2,
        padding: { top: 5, bottom: 5, left: 5, right: 5 }})
        .setOrigin(0.5, 0.5)
        this.nextLevel.setInteractive()
        this.nextLevel.on('pointerdown', () => this.restart() )
        this.nextLevel.on('pointerover', () => this.enterHoverState(this.nextLevel) )
        this.nextLevel.on('pointerout', () => this.leaveHoverState(this.nextLevel) );
    }

    else
    {
      this.nextLevel = this.add.text(width * 0.65, height * 0.6, `Next Level -->`,{
        backgroundColor: 'lightblue',
        fill: 'black',
        fontSize: 20,
        stroke: '#000000',
        strokeThickness: 2,
        padding: { top: 5, bottom: 5, left: 5, right: 5 }})
        .setOrigin(0.5, 0.5)
        this.nextLevel.setInteractive()
        this.nextLevel.on('pointerdown', () => this.updateNextClick(mydata) )
        this.nextLevel.on('pointerover', () => this.enterHoverState(this.nextLevel) )
        this.nextLevel.on('pointerout', () => this.leaveHoverState(this.nextLevel) );
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default class Preloader extends Phaser.Scene
{
  constructor()
  {
    super(`preloader`)
  }

  init(data)
  {
    if (!data.level)
    {
      this.level = 0
    }
    else
    {
      this.level = data.level
    }
  }

  create()
  {
    this.scene.start(`game`, {level: this.level})
  }
}

7. Machine learning

Artificial intelligence (AI, also machine intelligence, MI) is intelligence demonstrated by machines, in contrast to the natural intelligence (NI) displayed by humans and other animals. In computer science AI research is defined as the study of "intelligent agents": any device that perceives its environment and takes actions that maximize its chance of successfully achieving its goals. Colloquially, the term "artificial intelligence" is applied when a machine mimics "cognitive" functions that humans associate with other human minds, such as "learning" and "problem solving".
Machine learning is a field of computer science that often uses statistical techniques to give computers the ability to "learn" (i.e., progressively improve performance on a specific task) with data, without being explicitly programmed.

A great white paper can be found here.

A good overview of books is here, here and here.

Make use of this fantastic cheatsheet.

github.com/ageron/handson-ml3

www.stateof.ai

developers.google.com/machine-learning

github.com/jctillman/js-ml-workshop

burakkanber.com/tag/ml-in-js

tutorialzine.com/2017/04/10-machine-learning-examples-in-javascript

www.robinwieruch.de/machine-learning-javascript-web-developers

bloomberg.github.io/foml

ai100.stanford.edu

www.reddit.com/r/learnmachinelearning/comments/gzk4wz/50_free_machine_learning_and_data_science_ebooks

medium.com/towards-artificial-intelligence/machine-learning-algorithms-for-beginners-with-python-code-examples-ml-19c6afd60daa

stackabuse.com/the-best-machine-learning-books-for-all-skill-levels

www.ritchieng.com

www.technologyreview.com/2020/11/03/1011616/ai-godfather-geoffrey-hinton-deep-learning-will-do-everything

blogs.microsoft.com/ai/machine-teaching

app.korbit.ai

deepai.org

course.fast.ai

atcold.github.io/pytorch-Deep-Learning

www.deeplearningbook.org

github.com/fritzlabs/Awesome-Mobile-Machine-Learning

laion.ai/projects

lilianweng.github.io

www.coursera.org/learn/machine-learning

machinelearningmastery.com/improve-deep-learning-performance

To solve the mathematical imprecision problem in JS (try 0.1 + 0.2 in the console and see what you get) we can use decimal.js. For details on the problem in different programming languages see 0.30000000000000004.com.

Machine learning (ML) requires a reasonable understanding of derivatives and gradient descent. See www.khanacademy.org/math/ap-calculus-ab/ab-derivative-intro and www.khanacademy.org/math/multivariable-calculus/multivariable-derivatives for a very intuitive explanation.

A basic understanding of statistics is helpful in getting a deeper understanding of how the algorithms work. See www.openintro.org/book/stat.

7.1. Mathematical foundations

7.2. The right model to solve the problem

The following table is based on "Mastering Machine Learning with R" from Cory Lesmeister:

Text data

  • Word frequency

  • Word cloud

  • Topic models

  • Lexical analysis

Univariate time series data

  • Auto-regressive integrated moving average (ARIMA)

  • Exponential smoothing

  • Linear regression

  • ARCH

  • GARCH

Multivariate time series data

  • Dynamic linear regression

  • Vector autoregression (VAR)

  • Granger causality

  • Vector error correction model (VECM)

  • Cointegration

Making recommendations

  • User-based collaborative filtering (UBCF)

  • Item-based collaborative filtering (IBCF)

  • Singular value decomposition (SVD)

  • Principal components analysis (PCA)

Looking for associations

  • Apriori

  • Equivalence class clustering with bottom-up lattice traversal (ECLAT)

Predicting a quantity

  • Linear regression

  • Least absolute shrinkage and selection operator (LASSO)

  • Ridge regression

  • ElasticNet

  • PCA regression

  • Generalized additive models (GAM)

  • artial least squares (PLS)

Categorize unlabeled data that is not text or time series

Clustering:

  • Hierarchical

  • K-means

  • Partition around medoids

  • Self-organizing map (SOM)

  • Fuzzy clustering

Categorize labeled data that is not text or time series

Classification:

  • Logistic regression

  • Linear discriminant analysis (LDA)

  • K-nearest neighbors (KNN)

  • Support vector machines

  • Neural networks/deep learning

  • Decision trees

  • Random forest

  • Gradient boosting

  • Naïve Bayes

  • Survival analysis

7.4. Tools

blog.soshace.com/top-11-javascript-machine-learning-data-science-libraries

7.4.2. Python

wiki.python.org/moin/FrontPage

dev.to/underdogio/python-for-javascript-developers

www.valentinog.com/blog/python-for-js

book.pythontips.com/en/latest/object_introspection.html

docs.python.org/3/reference/datamodel.html

www.programiz.com/python-programming

jakevdp.github.io/PythonDataScienceHandbook

docs.python.org/3/tutorial

docs.python.org/3/reference/expressions.html

www.learnpython.org

realpython.com/primer-on-python-decorators

www.geeksforgeeks.org/print-lists-in-python-4-different-ways

colab.research.google.com/github/hmok/Tutorials/blob/master/beginnersPythonCheatSheet.ipynb

stackoverflow.com/questions/4383571/importing-files-from-different-folder

Great tutorials for NumPy, Pandas, Matplotlib, linear algebra, calculus etc. can be found at homl.info/tutorials.
Spyder is a great scientific Python development environment, ideal for machine learning.
To experiment with Python in your browser, see www.online-python.com.

To use Python 3 by default, run sudo apt install python-is-python3.

7.4.2.2. pip

Sometimes it may be necessary to clear the pip package cache. From perplexity.ai:

  • pip cache purge: This will remove all cached packages from the pip cache.

  • pip cache remove <package_name>: This will remove a specific package from the pip cache.

  • pip cache dir: This will show the directory where the pip cache is stored. You can also manually remove the cache directory:

  • On Linux/macOS: rm -rf ~/.cache/pip

  • On Windows: rmdir /s %APPDATA%\pip\cache Or disable the cache entirely:

  • pip config set global.no-cache-dir true: This will prevent pip from using the cache for all future installations. The key points are:

  • The pip cache command provides a convenient way to manage the pip cache starting from pip 20.1.

  • For older versions of pip, you can manually remove the cache directory.

  • Disabling the cache entirely using pip config is an option if you want to ensure a fresh installation every time.

It’s important to note that the pip cache is designed to improve installation speed by reusing previously downloaded packages. Clearing the cache may result in slower installation times, but it can be necessary in certain situations, such as when you need to ensure a specific package version is installed.

To save a list of all installed packages in a file:

pip freeze > requirements.txt

To uninstall them:

pip uninstall -r requirements.txt -y
7.4.2.3. Miniconda

Install Miniconda.

To install packages, use conda install -c conda-forge <package name>
7.4.2.5. Mamba

mamba.readthedocs.io

ross-dobson.github.io/posts/2021/01/setting-up-python-virtual-environments-with-mambaforge

mamba create -n <name of env> python=3.10
mamba env create -f environment.yml
mamba env remove -n <name of env>
mamba env list
mamba clean -a
To install a requirements.txt file in Mamba, use:
while read requirement; do mamba install --yes $requirement; done < requirements.txt
7.4.2.7. Kivy

python -m pip install --user github.com/kivy/kivy/archive/master.zip

7.4.2.8. PTAN

To get PTAN installed, first install pyenv as shown above, then install and activate Python 3.7.8 as PTAN requires Torch 1.3.0, which does not work with Python 3.8 or above.

So:

1
2
3
git clone https://github.com/Shmuma/ptan.git
python setup.py install
pip freeze

Verify that the requirements in requirements.txt are exactly met.

7.4.2.11. Matplotlib

www.tutorialspoint.com/matplotlib

pyimagesearch.com/2015/08/24/resolved-matplotlib-figures-not-showing-up-or-displaying

Use the Spyder IDE to see the plots in an Ubuntu Python virtual environment.
7.4.2.12. numPy

numpy.org/doc/stable/reference

7.4.2.12.1. CuPy

github.com/cupy/cupy

7.4.2.14. CodeSkulptor

CodeSkulptor is a tool for teaching Python programming, especially to beginners. A driving goal is to be very easy to use. Some of its main advantages for teaching are:

  • students do not need to install any software,

  • students will all have the same Python version and the same editor, and

  • students can access the same programming environment and code files from any computer.

7.4.2.16. pypdf
pypdf is a free and open-source pure-python PDF library capable of splitting, merging, cropping, and transforming the pages of PDF files. It can also add custom data, viewing options, and passwords to PDF files. pypdf can retrieve text and metadata from PDFs as well.
7.4.2.17. PyTorch

To install PyTorch:

pip install torch torchvision torchaudio
7.4.2.17.1. GPU

stackabuse.com/how-to-use-gpus-with-pytorch

If you have an Nvidia GPU you can test whether CUDA is working:

import torch
print(torch.cuda.is_available())
print(torch.version.cuda)
print(torch.cuda.device_count())
print(torch.cuda.get_device_name(0))

To ensure that all tensors created by PyTorch will be stored on the GPU by default, use:

torch.set_default_tensor_type('torch.cuda.FloatTensor')

You can move your model to the GPU using:

model.cuda()

And the data: data.cuda()

7.4.2.18. Tensorflow
7.4.2.18.1. GPU

www.tensorflow.org/guide/gpu

If you have an Nvidia GPU, install tensorflow using mamba install tensorflow-gpu and see www.osc.edu/resources/getting_started/howto/howto_add_python_packages_using_the_conda_package_manager/howto_use to compare the performance.

7.4.3. R

R is a free software environment for statistical computing and graphics

7.4.4. Google Colaboratory

colab.research.google.com

neptune.ai/blog/how-to-use-google-colab-for-deep-learning-complete-tutorial

stackoverflow.com/questions/48796169/how-to-fix-ipykernel-launcher-py-error-unrecognized-arguments-in-jupyter

stackoverflow.com/questions/48774285/how-to-download-file-created-in-colaboratory-workspace

analyticsindiamag.com/5-alternatives-to-google-colab-for-data-scientists

www.kdnuggets.com/2020/03/4-best-jupyter-notebook-environments-deep-learning.html

www.kdnuggets.com/2019/01/more-google-colab-environment-management-tips.html

stackoverflow.com/questions/52733786/how-to-import-custom-modules-in-google-colab

stackoverflow.com/questions/4383571/importing-files-from-different-folder

ipython.readthedocs.io/en/stable/interactive/magics.html#magic-cd

medium.com/lean-in-women-in-tech-india/google-colab-the-beginners-guide-5ad3b417dfa

androidkt.com/how-to-import-py-python-file-or-module-and-run-in-google-colab

zerowithdot.com/colab-github-workflow

towardsdatascience.com/cheat-sheet-for-google-colab-63853778c093

nbviewer.jupyter.org/github/ipython/ipython/blob/1.x/examples/notebooks/Cell%20Magics.ipynb

stackoverflow.com/questions/49345375/google-colaboratory-not-refreshing-drive-content

github.com/googlecolab/colabtools/issues/287

medium.com/the-innovation/quickly-open-a-github-repository-notebook-in-google-colab-58c1f44c9d4a

medium.com/@steve7an/how-to-test-jupyter-notebook-from-github-via-google-colab-7dc4b9b11a19

bcrf.biochem.wisc.edu/2021/02/05/download-google-drive-files-using-wget

This article explains how to use JS in Colab. Also see this.

7.4.6. Google Teachable Machine

teachablemachine.withgoogle.com

7.4.7. Saturn Cloud

7.5. Regression

7.5.1. Linear regression

Linear regression can be used if we have a set of input and output data and suspect that there is a linear relationship between them, i.e. the output value can be calculated from the input value(s) by applying a linear function. If we have only one input value, also called explanatory variable, per output value we talk about univariate linear regression. If we have several explanatory variables we use multivariate linear regression.

7.5.1.1. Univariate linear regression

Let’s say we have a series of systolic blood pressure measurements from people of different ages and we’d like to be able to estimate the systolic blood pressure for a person of any age using a linear function of the form \$y = ax + b\$. This function is called our hypothesis. We need to determine \$a\$ and \$b\$ so that our hypothesis matches our given data set as closely as possible, which will give us some confidence that it will produce a sensible result for a new data point. This amounts to finding a line that passes through our data points so as to minimise the average distance to each data point.

To determine the best values for \$a\$ and \$b\$ we will proceed as follows:

  1. Start with some random values for \$a\$ and \$b\$.

  2. Calculate the average squared distance of our hypothesis from the correct data using the formula \$\frac{1}{2m} sum_{i=1}^m(h(x^i) - y^i)^2\$ where \$m\$ is the number of data points, \$x^i\$ the ith input and \$y^i\$ the ith output value and \$h\$ our hypothesis function. This formula is known as the cost or error function and our goal is to minimize it.

  3. Calculate the derivative of \$h\$ wrt to \$a\$ as well as the derivative of \$h\$ wrt to \$b\$.

  4. Deduct the derivative times a factor, called the learning rate, from \$a\$ and \$b\$ respectively. This method is called gradient descent and moves our parameters closer to their optimum values. How to we determine the learning rate? If we set it too low the algorithm will take a long time to converge to a minimum as it is taking only very small steps. If we set the rate too high we may never reach the minimum as we take too big steps. We can use our cost function to check whether our learning rate is too high. Normally the cost should go down with each iteraton as it converges to the minimum. If it goes up that’s an indication that our learning rate is too high and needs to be reduced. In the case of univariate regression we can use the inverse of the second derivative of \$h\$, i.e. \$\frac{1}{f^'""^'(x)}\$ (cf. developers.google.com/machine-learning/crash-course/reducing-loss/learning-rate).

  5. Repeat steps 2 to 4 above until a set number of iterations has been executed or the cost decreases by less than a given amount for an iteration.

Let’s calculate the derivatives required for point 3 above. If you don’t remember derivatives rules see www.mathsisfun.com/calculus/derivatives-rules.html for a quick refresher.

\$(dh)/(da) = (d\frac{1}{2m} sum_{i=1}^m(h(x^i) - y^i)^2)/(da) = (d\frac{1}{2m} sum_{i=1}^m(ax^i + b - y^i)^2)/(da) = 2\frac{1}{2m} sum_{i=1}^m(ax^i + b - y^i)x^i = \frac{1}{m} sum_{i=1}^m(ax^i + b - y^i)x^i\$
\$(dh)/(db) = (d\frac{1}{2m} sum_{i=1}^m(h(x^i) - y^i)^2)/(db) = (d\frac{1}{2m} sum_{i=1}^m(ax^i + b - y^i)^2)/(db) = 2\frac{1}{2m} sum_{i=1}^m(ax^i + b - y^i) = \frac{1}{m} sum_{i=1}^m(ax^i + b - y^i)\$
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Based on https://www.robinwieruch.de/linear-regression-gradient-descent-javascript
// Systolic blood pressure by age.
// Source: http://people.sc.fsu.edu/~jburkardt/datasets/regression/x03.txt
const X = [39, 47, 45, 47, 65, 46, 67, 42, 67, 56, 64, 56, 59, 34, 42, 48, 45, 17, 20, 19,
    36, 50, 39, 21, 44, 53, 63, 29, 25, 69],
  Y = [144, 220, 138, 145, 162, 142, 170, 124, 158, 154, 162, 150, 140, 110, 128, 130,
    135, 114, 116, 124, 136, 142, 120, 120, 160, 158, 144, 130, 125, 175]
const LEARNING_RATE = 0.0003
let a = 0 // a in y = ax + b
let b = 0 // b in y = ax + b

// The hypothesis function is our current best fit estimate for the linear regression function.
const hypothesis = x => a * x + b

// In linear regression the cost function calculates the sum of the squared differences.
const cost = () => {
  let sum = 0
  for (let i = 0; i < X.length; i++) sum += Math.pow(hypothesis(X[i]) - Y[i], 2)
  return sum / (2 * X.length)
}

const learn = () => {
  let aSum = 0
  let bSum = 0

  for (let i = 0; i < X.length; i++) {
    aSum += (hypothesis(X[i]) - Y[i]) * X[i]
    bSum += hypothesis(X[i]) - Y[i]
  }

  a = a - (LEARNING_RATE / X.length) * aSum
  b = b - (LEARNING_RATE / X.length) * bSum
}

const plot = () => {
  const MINX = Math.min(...X), MAXX = Math.max(...X), MINY = Math.min(...Y),
    MAXY = Math.max(...Y), WIDTH = 1000, HEIGHT = 770
  let html = `<svg width=${WIDTH} height=${HEIGHT} style='background-color: lightblue'>`
  for (let i = 0; i <= 70; i += 5)
    html += `<text x=${50 + 10 * i} y=${HEIGHT - 15}>${i}</text>
             <line x1=${50 + 10 * i} y1=${HEIGHT - 50} x2=${50 + 10 * i}
              y2=${HEIGHT - 3 * 230 - 50} stroke=black stroke-width=1></line>`
  for (let i = 0; i <= 230; i += 10)
    html += `<text x=10 y=${HEIGHT - 3 * i - 50}>${i}</text>
            <line x1=50 y1=${HEIGHT - 3 * i - 50} x2=${50 + 10 * 70}
              y2=${HEIGHT - 3 * i - 50}
             stroke=black stroke-width=1></line>`
  for (let i = 0; i < X.length; i++)
    html += `<circle cx=${50 + 10 * X[i]} cy=${HEIGHT - 3 * Y[i] - 50} r=10 stroke=black
              stroke-width=3 fill=yellow></circle>`
  const X1 = MINX, Y1 = hypothesis(X1), X2 = MAXX, Y2 = hypothesis(X2)
  html += `<line x1=${50 + 10 * X1} y1=${HEIGHT - 3 * Y1 - 50} x2=${50 + 10 * X2}
            y2=${HEIGHT - 3 * Y2 - 50} stroke=black stroke-width=5></line>`
  html += `<text x=${WIDTH - 230} y=32 font-size=26 fill=black>Learn</text>
          <rect x=${WIDTH - 240} y=10 height=30 width=80 
            stroke=black stroke-width=2 fill=green opacity=.5 rx=10></rect>
          <foreignObject x=${WIDTH - 150} y=12 width=70 height=50>
            <div xmlns=http://www.w3.org/1999/xhtml><input value=1></div>
          </foreignObject>
          <text x=${WIDTH - 75} y=27>iteration(s)</text>
          <text x=${WIDTH - 240} y=70>Hypothesis: y = ${a.toFixed(2)} * x +
                ${b.toFixed(2)}</text>
          <text x=${WIDTH - 240} y=100>Cost: ${cost().toFixed(2)}</text> 
          </svg>`
  document.querySelector('body').innerHTML = html
  document.querySelector('rect').addEventListener('click', iterate)
}

const iterate = () => {
  let iterations = document.querySelector('input').value
  if (!iterations) iterations = 1
  for (let i = 1; i <= iterations; i++) learn()
  plot()
}

plot()

We can simplify our life and use an external library to do the work:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const ml = require('ml-regression')
const csv = require('csvtojson')

// Data from http://mathbits.com/MathBits/TISection/Statistics2/linearREAL.htm
let inputs = [20, 16, 19.8, 18.4, 17.1, 15.5, 14.7, 15.7, 15.4, 16.3, 15, 17.2, 16, 17,
  14.4]
let outputs = [88.6, 71.6, 93.3, 84.3, 80.6, 75.2, 69.7, 71.6, 69.4, 83.3, 79.6, 82.6,
  80.6, 83.5, 76.3]

let regression = new ml.SLR(inputs, outputs)
console.log(regression.toString(3)) // === 'f(x) = 3.41 * x + 22.8';
7.5.1.1.1. Vectorization

In our linear regression example each learning iteration implies the execution of a loop over the \$m\$ data points which is very time consuming. We can simplify this by using matrix multiplication. Given the optimisations implemented in good math libraries (cf. softwareengineering.stackexchange.com/questions/312445/why-does-expressing-calculations-as-matrix-multiplications-make-them-faster) this can lead to significant speed improvements.

7.6. Neural networks

A neural network can be described as a universal function approximator.

www.3blue1brown.com

www.youtube.com/watch?v=aircAruvnKk

www.youtube.com/watch?v=IHZwWFHWa-w

wagenaartje.github.io/neataptic

github.com/ssusnic/Machine-Learning-Flappy-Bird

www.asimovinstitute.org/neural-network-zoo-prequel-cells-layers

neuralnetworksanddeeplearning.com

WebDNN is the fastest DNN execution framework for the browser.

7.6.2. Natural Languague Processing (NLP)

web.stanford.edu/class/cs224n

ai.googleblog.com/2017/08/transformer-novel-neural-network.html

www.makeuseof.com/chatgpt-alternative-for-coding-programs-automatically

voyager.minedojo.org

helphub.commandbar.com

ai.meta.com/blog/seamless-m4t

llava-vl.github.io

craftjarvis-jarvis1.github.io

github.com/imoneoi/openchat

deepmind.google/discover/blog/alphageometry-an-olympiad-level-ai-system-for-geometry

lumalabs.ai

lightning.ai/lightning-ai/studios/code-lora-from-scratch

github.com/datamllab/LongLM

huggingface.co/spaces/lmsys/chatbot-arena-leaderboard

github.com/kuleshov/minillm

spacy.io

junzhan2000.github.io/AnyGPT.github.io

www.youtube.com/watch?v=kCc8FmEb1nY

github.com/karpathy/nanoGPT

github.com/Vahe1994/AQLM

developer.nvidia.com/blog/generate-code-answer-queries-and-translate-text-with-leading-generative-ai-models

ai.google.dev/gemma

github.com/NVIDIA/TensorRT-LLM

github.com/NVIDIA/GenerativeAIExamples/tree/main

ollama.com

github.com/microsoft/unilm

Chain-of-thought prompting

Beyond the Imitation Game: Quantifying and extrapolating the capabilities of language models

www.youtube.com/watch?v=zduSFxRajkE

www.youtube.com/watch?v=kCc8FmEb1nY

github.com/databricks/dbrx

Mamba: Linear-Time Sequence Modeling with Selective State Spaces

From twitter.com/itsPaulAi/status/1744764796011155947: To run Phi-2 locally for private AI download lmstudio.ai, a free platform for local AI models. Inside LM Studio, search for "phi-2", select “TheBloke/phi-2-GGUF” and download the Q6_K or Q4_K_S version. Open the Chat tab, ensure Phi-2 is selected, and then start prompting away!
7.6.2.2. Video summarizers

summarize.tech: AI-powered video summaries

www.summarize.tech

YouTube Video Summarizer - AI Summarizer for Videos HIX.AI

hix.ai/summarizer/video

Free Online Summary Video Maker with Templates & Tools - FlexClip

www.flexclip.com/create/summary-video.html

YouTube Video Summary Generator for creating perfect Video Summaries

www.unifire.ai/tools/youtube-video-summary-generator

Video Summary Generator Free for creating perfect Video Summaries

www.unifire.ai/tools/video-summary-generator-free

7.6.2.8. Tensorflow

www.tensorflow.org/tfmodels/nlp

7.6.4. Generating interactive environments

sites.google.com/view/genie-2024

7.6.6. Scalable Instructable Multiworld Agent (SIMA)

deepmind.google/discover/blog/sima-generalist-ai-agent-for-3d-virtual-environments

7.6.7. Hyperparameter tuning

arxiv.org/abs/1803.09820

7.8. Reinforcement learning

github.com/vwxyzjn/cleanrl

github.com/tinkoff-ai/CORL

medium.com/@v_maini/reinforcement-learning-6eacf258b265

rl-book.com

papers.nips.cc/paper_files/paper/1999/hash/464d828b85b0bed98e80ade0a5c43b0f-Abstract.html

github.com/MorvanZhou/Reinforcement-learning-with-tensorflow/tree/master

github.com/DLR-RM/stable-baselines3

stable-baselines3.readthedocs.io/en/master/guide/rl.html

github.com/dennybritz/reinforcement-learning

rail.eecs.berkeley.edu/deeprlcourse

deep-reinforcement-learning.net

7.8.1. Introduction

Reinforcement learning (RL) is an area of machine learning inspired by behaviourist psychology, concerned with how software agents ought to take actions in an environment so as to maximize some notion of cumulative reward. …​ Reinforcement learning differs from standard supervised learning in that correct input/output pairs need not be presented, and sub-optimal actions need not be explicitly corrected. Instead the focus is on performance, which involves finding a balance between exploration (of uncharted territory) and exploitation (of current knowledge).

RL is also called neuro-dynamic programming (NDP) or approximate dynamic programming (ADP).

Among many other applications, RL is used to discover new algorithms that outperform the best ones developed by humans.

Based on incompleteideas.net/book/the-book-2nd.html, a reinforcement learning system consists of four main elements:

  1. A policy defines the learning agent’s way of behaving at a given time.

  2. A reward signal defines the immediate goal.

  3. A value function specifies what is good in the long run. The value of a state is the total amount of reward an agent can expect to accumulate over the future, starting from that state.

  4. A model (optional) mimics the behavior of the environment and is used for planning.

These elements enable our learning agent to have explicit goals, sense aspects of its environment and choose actions to influence its environment. The agent’s sole goal is to maximize the total reward it receives over the long run.

We assume that the system we wish to control is stochastic, i.e. random. We also assume that the problem we try to solve is a Markov Decision Process (MDP):

A Markov decision process is a discrete time stochastic control process. At each time step, the process is in some state \$s\$, and the decision maker may choose any action \$a\$ that is available in state \$s\$. The process responds at the next time step by randomly moving into a new state \$s'\$, and giving the decision maker a corresponding reward \$R_a(s,s')\$.

The probability that the process moves into its new state \$s\$ is influenced by the chosen action. Specifically, it is given by the state transition function \$P_a(s,s')\$. Thus, the next state \$s'\$ depends on the current state \$s\$ and the decision maker’s action \$a\$. But given \$s\$ and \$a\$, it is conditionally independent of all previous states and actions; in other words, the state transitions of an MDP satisfy the Markov property.

The action \$pi(s)\$ taken in state \$s\$ under deterministic policy \$pi\$ is: \$pi(s):=\arg \max_asum_{s'}P_{a}(s,s')\left(R_{a}(s,s')+\gamma V(s'))\$

An excellent deep reinforcement learning course can be found at simoninithomas.github.io/Deep_reinforcement_learning_Course.

RL libraries:

keras.io

github.com/google/dopamine

github.com/ray-project/ray

github.com/tensorflow/agents

github.com/facebookresearch/ReAgent

github.com/catalyst-team/catalyst

github.com/kengz/SLM-Lab

7.8.2. Q-learning

One approach to learning the value function is the Q-learning algorithm. To each couple of state and action \$(s, a)\$, we associate a value \$Q(s, a)\$. Initially, we can set all Q-values to zero. In order to update them, we calculate what is called the temporal difference:

\$TD_t(s_t, a_t)=R(s_t, a_t)+\gamma \max_aQ(s_{t+1},a)-Q(s_t, a_t)\$

Given a state and action we then use the Bellman equation to update the Q-values:

\$Q_t(s_t,a_t)=Q_{t-1}(s_t,a_t)+\alphaTD_t(s_t,a_t)\$

\$Q^pi(s_t,a_t)=E[R_{t+1}+\gamma R_{t+2} + \gamma^2R_{t+3} + \cdots|s_t,a_t\$]

web.mst.edu/~gosavia/tutorial.pdf

sites.ualberta.ca/~szepesva/RLBook.html

janhuenermann.com/projects/learning-to-drive

www.robinwieruch.de/machine-learning-javascript-web-developers

arxiv.org/abs/1804.04577

skymind.ai/wiki/deep-reinforcement-learning

towardsdatascience.com/curiosity-driven-learning-made-easy-part-i-d3e5a2263359

researchers.lille.inria.fr/~lazaric

github.com/yandexdataschool/Practical_RL

github.com/dennybritz/reinforcement-learning

www.coursera.org/learn/practical-rl

towardsdatascience.com/deep-reinforcement-learning-and-monte-carlo-tree-search-with-connect-4-ba22a4713e7a

www.sicara.ai/blog/2018-05-30-intro-deep-learning-javascript-synapticjs-convnetjs

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// Corrected version of http://mnemstudio.org/ai/path/q_learning_js_ex1.txt
const qSize = 6
const gamma = 0.8
const iterations = 1000
let initialStates = [1, 3, 5, 2, 4, 0]

/*
Each row represents actions for a state, i.e. R[0][1] = -1 -> we cannot go from room/state
 0 to 1. R[2][3] = 0 means we can go from room 2 to 3 but have not yet reached the goal
  (room 5). R[1][5] = 1000 -> we can go from room 1 directly to 5 and reach our goal.
 */
let R = [[-1, -1, -1, -1, 0, -1],
  [-1, -1, -1, 0, -1, 100],
  [-1, -1, -1, 0, -1, -1],
  [-1, 0, 0, -1, 0, -1],
  [0, -1, -1, 0, -1, 100],
  [-1, 0, -1, -1, 0, 100]]

let Q = new Array(qSize)
for (let a = 0; a < qSize; a++) Q[a] = new Array(qSize)
let currentState = 0

const initialize = () => {
  for (let i = 0; i <= (qSize - 1); i++)
    for (let j = 0; j <= (qSize - 1); j++) Q[i][j] = 0
}

const maximum = (state, returnIndexOnly) => {
  // if returnIndexOnly === true, a Q matrix index is returned.
  // if returnIndexOnly === false, a Q matrix element is returned.

  let winner = 0
  let foundNewWinner = false
  let done = false

  winner = 0

  do {
    foundNewWinner = false
    for (let m = 0; m < qSize; m++) {
      if ((m < winner) || (m > winner)) //Avoid self-comparison.
        if (Q[state][m] > Q[state][winner]) {
          winner = m
          foundNewWinner = true
        }
    }

    if (!foundNewWinner) done = true
  } while (!done)

  if (returnIndexOnly) return winner
  else return Q[state][winner]
}

const reward = action => {
  return Math.round(R[currentState][action] + (gamma * maximum(action, false)))
}

const episode = initialState => {
  currentState = initialState

  //Travel from state to state until goal state is reached.
  do chooseAnAction(); while (currentState !== 5)

  //When currentState = 5, run through the set once more for convergence.
  for (let i = 0; i < qSize; i++) chooseAnAction()
}

const chooseAnAction = () => {
  let possibleAction = 0

  //Randomly choose a possible action connected to the current state.
  possibleAction = getRandomAction(qSize, 0)

  if (R[currentState][possibleAction] >= 0) {
    Q[currentState][possibleAction] = reward(possibleAction)
    currentState = possibleAction
  }
}

const getRandomAction = (upperBound, lowerBound) => {
  let action = 0
  let choiceIsValid = false
  let range = upperBound - lowerBound

  //Randomly choose a possible action connected to the current state.
  do {
    //Get a random value between 0 and 6.
    action = lowerBound + Math.round(range * Math.random())

    if (R[currentState][action] > -1) choiceIsValid = true
  } while (!choiceIsValid)

  return action
}

const init = () => {
  let initialState = 0
  initialize()
  for (let i = 0; i < iterations; i++)
    for (let j = 0; j < qSize; j++) episode(initialStates[j])
  let str = ''
  for (let i = 0; i < qSize; i++) {
    for (let j = 0; j < qSize; j++) str += Q[i][j] + ' '
    str += '<br>'
  }
  document.body.innerHTML = 'Q matrix values:<br>' + str +
    '<br>Shortest routes from initial states:<br>'
// Now let's display the shortest path from each starting node.
  for (let state = 0; state < qSize; state++) {
    let str = state + ' '
    let maxState = state
    do {
      maxState = maximum(maxState, true)
      str += maxState + ' '
    } while (maxState < 5)
    document.body.innerHTML += str + '<br>'
  }
}

init()

7.8.6. OpenAI Gym

github.com/openai/gym

github.com/openai/gym/wiki/Table-of-environments

colab.research.google.com/github/udacity/deep-learning/blob/master/reinforcement/Q-learning-cart.ipynb

www.oreilly.com/learning/introduction-to-reinforcement-learning-and-openai-gym

simoninithomas.github.io/Deep_reinforcement_learning_Course

github.com/simoninithomas/Deep_reinforcement_learning_Course/blob/master/Deep%20Q%20Learning/Doom/Deep%20Q%20learning%20with%20Doom.ipynb

simoninithomas.github.io/deep-rl-course

karpathy.github.io/2016/05/31/rl

gist.github.com/Taresin/a090274fbaf092ad649e4e32e22ecaf4

7.8.6.1. Installation

You need to have Python and pip installed.

apt install python3 python3-pip
apt install -y python-numpy python-dev cmake zlib1g-dev libjpeg-dev xvfb ffmpeg xorg-dev
python-opengl libboost-all-dev libsdl2-dev swig
pip3 install 'gym[all]'

Create a test file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import gym
env = gym.make('MsPacman-v0')
state = env.reset()
reward, info, done = None, None, None
maxReward = 0
while done != True:
    state, reward, done, info = env.step(env.action_space.sample())
    env.render()
    if reward > maxReward:
        maxReward = reward
        print(reward)
# https://github.com/openai/gym/issues/893
env.close()

To use Gym Retro we’ll also need Docker installed. Run pip3 install gym-retro (cf. github.com/openai/retro).

Create a test file:

1
2
3
4
5
6
7
8
import retro
env = retro.make(game='Airstriker-Genesis', state='Level1')
env.reset()
for _ in range(1000):
    env.render()
    env.step(env.action_space.sample()) # take a random action
# https://github.com/openai/gym/issues/893
env.close()

To run the gym remotely use ssh -X username@hostname (cf. stackoverflow.com/questions/40195740/how-to-run-openai-gym-render-over-a-server).

To get a list of all the environments installed on your system, see github.com/openai/gym/blob/master/examples/scripts/list_envs. Or run:

1
2
from gym import envs
print(envs.registry.all())

7.8.7. ViZDoom

Doom-based AI Research Platform for Reinforcement Learning from Raw Visual Information.

github.com/mwydmuch/ViZDoom

github.com/mwydmuch/ViZDoom/issues/365

7.8.8. Arcade Learning Environment (ALE)

7.8.9. Tabular solution methods

If the state and action spaces are small enough for the value functions to be represented as arrays RL can often find the optimal value function and the optimal policy.

7.9. Tensorflow

www.tensorflow.org/js

github.com/tensorflow/tfjs-examples

playground.tensorflow.org

medium.com/emergent-future/simple-reinforcement-learning-with-tensorflow-part-0-q-learning-with-tables-and-neural-networks-d195264329d0

ml5js.org

github.com/BeTomorrow/ReImproveJS

www.youtube.com/watch?v=SV-cgdobtTA

2019.jsconf.eu/nick-kreeger/tensorflowjs-bringing-machine-learning-to-the-web-and-beyond.html

www.analyticsvidhya.com/blog/2019/06/build-machine-learning-model-in-your-browser-tensorflow-js-deeplearn-js

blog.apptension.com/2018/06/27/tensorflow-js-machine-learning-and-flappy-bird-frontend-artificial-intelligence

becominghuman.ai/machine-learning-in-the-browser-using-tensorflow-js-3e453ef2c68c

blog.tensorflow.org/2020/01/run-tensorflow-savedmodel-in-nodejs-directly-without-conversion.html

www.codeproject.com/Articles/5272760/Getting-Started-With-Deep-Learning-in-Your-Browser

curiousily.com/posts/build-a-simple-neural-network-with-tensorflow-js-in-javascript

What can you do with Tensorflow.js? Take a look at great examples and resources.
TensorFlow Hub lets you search and discover hundreds of trained, ready-to-deploy machine learning models in one place.

7.9.1. Including the library

The easiest way to get Tensorflow.js into our web page is to include our JS file as a module and in the JS file import Tensorflow.

So in our HTML head we need:

<script src=index.js type=module></script>

In ìndex.js, we can either import the latest minimized file from the CDN:

import 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js'

Or, if we want to benefit from code completion in our IDE, import the locally stored non minimized library:

import './tf.js'

To download the latest version of the library to our server, in Linux we can run the following command:

wget https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.js

To get rid of source map error messages in the developer console, we need to remove the last line of the source file containing the comment //# sourceMappingURL=tf.js.map (cf. github.com/pkp/healthSciences/issues/117).

If, for some reason, we want to use a specific TF version, in this example version 2.0.0 (cf. release notes), we can specify it like so:

import 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js'

To determine which version of TF we are running, we can check tf.version.

7.9.1.1. In Node.js
If you are having problems getting to run TFJS with Node.js, see gist.github.com/thierryntoh24/0bbf857e21eaf5acd1fc28cde1eea263.

7.9.2. Workflow

When solving an ML problem using TF, we generally follow the following five steps:

  1. Get input data consisting of training and test data.

  2. Convert input to tensors.

  3. Create a model.

  4. Fit model to data.

  5. Use model on new data.

7.9.3. Tensors

A tensor is a structure of numbers, e.g. a number, a vector or a matrix. A tensor is defined by its rank, which is its number of dimensions. For instance a number (or scalar) has rank 0, a vector rank 1, a m by n matrix rank 2 and a m by n by p matrix rank 3. Study medium.freecodecamp.org/a-quick-introduction-to-tensorflow-js-a046e2c3f1f2 for a quick introduction to tensors in TensorFlow.js.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let t1 = tf.tensor(4.5)
t1.print()
let t2 = tf.tensor([2, 5, 99])
t2.print()

// The second parameter specifies the shape, i.e. 4 rows and 1 column.
let xs = tf.tensor2d([1, 3, 5, 7], [4, 1])
let ys = tf.tensor2d([1, 3, 5, 7], [1, 4])
let zs = tf.tensor2d([1, 3, 5, 7], [2, 2])
xs.print()
ys.print()
zs.print()

t1 = t1.add(t1)
t1.print()
xs.mul(ys).print()
ys.mul(xs).print()

If we don’t need a tensor anymore we can call its dispose method to free memory:

1
2
const x = tf.tensor([1,2,3]);
x.dispose();

Having to do this for each tensor would quickly become unwieldy therefore TF.js offers a tidy operator that works as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*const xs = tf.tensor3d([
  [0, 0, 0],
  [1, 1, 1],
  [2, 2, 2],
  [4, 4, 4],
  [7, 7, 7],
  [10, 10, 10],
  [0, 0, 0],
  [0, 0, 0],
  [0, 0, 0],
  [0, 0, 0],
]);*/

/*const model = tf.sequential();
model.add(tf.layers.dense({units: 32, inputShape: [50]}));
model.add(tf.layers.dense({units: 4}));
console.log(JSON.stringify(model.outputs[0].shape));*/

// y = 2 ^ 2 + 1
const y = tf.tidy(() => {
  // a, b, and one will be cleaned up when the tidy ends.
  const one = tf.scalar(1)
  const a = tf.scalar(2)
  const b = a.square()

  console.log('numTensors (in tidy): ' + tf.memory().numTensors)

  // The value returned inside the tidy function will return
  // through the tidy, in this case to the variable y.
  return b.add(one)
})

console.log('numTensors (outside tidy): ' + tf.memory().numTensors)
y.print()

7.9.4. Datasets

TensorFlow.js Data provides simple APIs to load and parse data from disk or over the web in a variety of formats, and to prepare that data for use in machine learning models (e.g. via operations like filter, map, shuffle, and batch).
7.9.4.1. Data access

The two main ways to access data from a dataset are toArray and forEachAsync. Both of these are async functions.

7.9.4.2. Data manipulation

TF provides a number of chainable methods for dataset manipulation.

7.9.5. Layers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Notice there is no 'import' statement. 'tf' is available on the index-page
// because of the script tag above.

// Define a model for linear regression.
const model = tf.sequential()
model.add(tf.layers.dense({units: 1, inputShape: [1]}))

// Prepare the model for training: Specify the loss and the optimizer.
model.compile({loss: 'meanSquaredError', optimizer: 'sgd'})

// Generate some synthetic data for training.
// The second parameter specifies the shape, i.e. 4 rows and 1 column.
const xs = tf.tensor2d([1, 2, 3, 4], [4, 1])
const ys = tf.tensor2d([1, 3, 5, 7], [4, 1])

// Train the model using the data.
await model.fit(xs, ys)
// Use the model to do inference on a data point the model hasn't seen before:
// Open the browser devtools to see the output
model.predict(tf.tensor2d([5], [1, 1])).print()

Here’s a more elaborate example from www.tensorflow.org/js/tutorials/training/linear_regression:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/**
 * Get the car data reduced to just the variables we are interested
 * and cleaned of missing data.
 */
const getData = async () => {
  const carsDataReq =
    await fetch('https://storage.googleapis.com/tfjs-tutorials/carsData.json')
  const carsData = await carsDataReq.json()
  const cleaned = carsData.map(car => ({
    mpg: car.Miles_per_Gallon,
    horsepower: car.Horsepower
  })).filter(car => (car.mpg != null && car.horsepower != null))
  console.log('Raw car data:')
  console.log(carsData)
  console.log('Filtered car data:')
  console.log(cleaned)
  return cleaned
}

/**
 * Convert the input data to tensors that we can use for machine
 * learning. We will also do the important best practices of _shuffling_
 * the data and _normalizing_ the data
 * MPG on the y-axis.
 */
const convertToTensor = data => {
  // Wrapping these calculations in a tidy will dispose any
  // intermediate tensors.
  return tf.tidy(() => {
    // Step 1. Shuffle the data
    tf.util.shuffle(data)

    // Step 2. Convert data to Tensor
    const inputs = data.map(d => d.horsepower)
    const labels = data.map(d => d.mpg)
    console.log(inputs)
    console.log(labels)
    const inputTensor = tf.tensor2d(inputs, [inputs.length, 1])
    const labelTensor = tf.tensor2d(labels, [labels.length, 1])

    //Step 3. Normalize the data to the range 0 - 1 using min-max scaling
    const inputMax = inputTensor.max()
    const inputMin = inputTensor.min()
    const labelMax = labelTensor.max()
    const labelMin = labelTensor.min()

    const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin))
    const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin))

    return {
      inputs: normalizedInputs,
      labels: normalizedLabels,
      // Return the min/max bounds, so we can use them later.
      inputMax,
      inputMin,
      labelMax,
      labelMin
    }
  })
}

const createModel = () => {
  // Create a sequential model
  const model = tf.sequential()

  // Add a single hidden layer
  model.add(tf.layers.dense({inputShape: [1], units: 50, useBias: true}))
  model.add(tf.layers.dense({units: 50, activation: 'relu'}))
  model.add(tf.layers.dense({units: 50, activation: 'relu'}))

  // Add an output layer
  model.add(tf.layers.dense({units: 1, useBias: true}))

  return model
}

const trainModel = async (model, inputs, labels) => {
  // Prepare the model for training.
  model.compile({
    optimizer: tf.train.adam(),
    loss: tf.losses.meanSquaredError,
    metrics: ['mse']
  })

  const batchSize = 32
  const epochs = 50

  return await model.fit(inputs, labels, {
    batchSize,
    epochs,
    shuffle: true,
    callbacks: tfvis.show.fitCallbacks(
      {name: 'Training Performance'},
      ['loss', 'mse'],
      {height: 200, callbacks: ['onEpochEnd']}
    )
  })
}

const testModel = (model, inputData, normalizationData) => {
  const {inputMax, inputMin, labelMin, labelMax} = normalizationData

  // Generate predictions for a uniform range of numbers between 0 and 1;
  // We un-normalize the data by doing the inverse of the min-max scaling
  // that we did earlier.
  const [xs, preds] = tf.tidy(() => {

    const xs = tf.linspace(0, 1, 100)
    const preds = model.predict(xs.reshape([100, 1]))

    const unNormXs = xs
      .mul(inputMax.sub(inputMin))
      .add(inputMin)

    const unNormPreds = preds
      .mul(labelMax.sub(labelMin))
      .add(labelMin)

    // Un-normalize the data
    return [unNormXs.dataSync(), unNormPreds.dataSync()]
  })


  const predictedPoints = Array.from(xs).map((val, i) => {
    return {x: val, y: preds[i]}
  })

  const originalPoints = inputData.map(d => ({
    x: d.horsepower, y: d.mpg
  }))


  tfvis.render.scatterplot(
    {name: 'Model Predictions vs Original Data'},
    {values: [originalPoints, predictedPoints], series: ['original', 'predicted']},
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  )
}

// Load and plot the original input data that we are going to train on.
const data = await getData()
const values = data.map(d => ({
  x: d.horsepower,
  y: d.mpg
}))

// Create the model
const model = createModel()
tfvis.show.modelSummary({name: 'Model Summary'}, model)
// Convert the data to a form we can use for training.
const tensorData = convertToTensor(data)
const {inputs, labels} = tensorData

// Train the model  
await trainModel(model, inputs, labels)
console.log('Done Training')

// Make some predictions using the model and compare them to the
// original data
testModel(model, data, tensorData)

7.9.6. Hyperparameters

  • Number of units

  • Kernel initializers

  • Activation functions

  • Number of layers

  • Weight regularization

  • Dropout layers and dropout rate

  • Optimizer

  • Learning rate

  • Decrease in learning rate

  • Batch size

7.9.6.1. Activation functions

Watch this great video!

A good overview of activation functions can be found in this paper.

In summary, use ELU for hidden layers. At the output layer level, use sigmoid for binary, softmax for multivariate classification and linear for regression.

Tensorflow supports the following actvation functions:

7.9.6.1.1. Exponential Linear Unit (elu)

From the paper:

The exponential linear units (ELUs) is another type of AF proposed by Clevert et al., 2015, and they are used to speed up the training of deep neural networks. The main advantage of the ELUs is that they can alleviate the vanishing gradient problem by using identity for positive values and also improves the learning characteristics. They have negative values which allows for pushing of mean unit activation closer to zero thereby reducing computational complexity thereby improving learning speed [66]. The ELU represents a good alternative to the ReLU as it decreases bias shifts by pushing mean activation towards zero during training.

Where \$\alpha\$ is a hyperparameter that controls the saturation point for negative net inputs which is usually set to 1.0 The ELUs have a clear saturation plateau in the negative regime thereby learning more robust representations, and they offer faster learning and better generalisation compared to the ReLU and LReLU with specific network structure especially above five layers and guarantees state-of-the-art results compared to ReLU variants. However, a critical limitation of the ELU is that the ELU does not centre the values at zero, and the parametric ELU was proposed to address this issue [67].

\$R(x)={(x,x>0),(\alphae^x-1,x<=0):}\$

ELU

Derivative:

\$R^'(x)={(1,x>0),(\alphae^x,x<=0):}\$

ELU derivative
7.9.6.1.2. hardSigmoid
The standard sigmoid is slow to compute because it requires computing the exp() function, which is done via complex code (with some hardware assist if available in the CPU architecture). In many cases the high-precision exp() results aren’t needed, and an approximation will suffice. Such is the case in many forms of gradient-descent/optimization neural networks: the exact values aren’t as important as the "ballpark" values, insofar as the results are comparable with small error.

The derivative is obviously extremely fast to compute, as it is just a constant. Hard Sigmoid provides reasonable results on classification tasks but should not be used for regression analysis as errors might reduce too slowly and never become small enough.

\$S(x)={(0,x<-2.5),(1,x>2.5),(0.2x+0.5,-2.5<=x<=2.5):}\$

HardSigmoid

7.9.6.1.3. Linear (linear)

The linear AF simply translates input into output and as such is only really used in the output layer, if at all, for instance in a regression problem. Back-propagation is not possible with linear AFs as the derivative is a constant and has no relation to the input.

\$f(x)=x\$

Linear

Derivative:

\$f^'(x)=1\$

7.9.6.1.4. Rectified Linear Unit (relu)

The formula is very simple: \$max(0,x)\$ It provides the same benefits as sigmoid but with better performance and avoids the vanishing gradient problem for \$x>0\$. It should only be used in a hidden layer.

\$x<0\$ can result in dead neurons, also known as the dying ReLU problem.

Use ELU to avoid this problem.

\$f(x)={(x,x>0),(0,x<=0):}\$

ReLU

Derivative:

\$f^'(x)={(1,x>0),(0,x<0):}\$

ReLU derivative
7.9.6.1.5. ReLU6 (relu6)

ReLU6 is a modified ReLU where the output is limited to 6 in order to achieve better robustness with low-precision computation.

\$f(x)={(0,x<=0),(x,0<x<6),(6,x>=6):}\$

ReLU6

Derivative:

\$f^'(x)={(0,x<=0),(1,0<x<6),(0,x>=6):}\$

7.9.6.1.6. Scaled Exponential Linear Unit (selu)

SELU

7.9.6.1.7. Sigmoid, also known as logistic AF.

From the paper:

The Sigmoid is a non-linear AF used mostly in feedforward neural networks. It is a bounded differentiable real function, defined for real input values, with positive derivatives everywhere and some degree of smoothness. The sigmoid function appears in the output layers of the DL architectures, and they are used for predicting probability based output and has been applied successfully in binary classification problems, modeling logistic regression tasks as well as other neural network domains, with Neal [49] highlighting the main advantages of the sigmoid functions as, being easy to understand and are used mostly in shallow networks. Moreover, Glorot and Bengio, 2010 suggesting that the Sigmoid AF should be avoided when initializing the neural network from small random weights [18].

However, the Sigmoid AF suffers major drawbacks which include sharp damp gradients during backpropagation from deeper hidden layers to the input layers, gradient saturation, slow convergence and non-zero centred output thereby causing the gradient updates to propagate in different directions. Other forms of AF including the hyperbolic tangent function was proposed to remedy some of these drawbacks suffered by the Sigmoid AF.

\$S(z)=\frac{1}{1+e^{-z}}\$

Sigmoid

Derivative:

\$S^'(z)=S(z)*(1-S(z))\$

Sigmoid derivative
7.9.6.1.8. Softmax (softmax)

\$f(x_i)=\frac{e^(x_i)}{sum_je^(x_j)}\$

7.9.6.1.9. Softplus (softplus)
7.9.6.1.10. Softsign (softsign)
7.9.6.1.11. Tanh (tanh)
7.9.6.1.12. Swish (swish)
7.9.6.1.13. Mish (mish)

7.9.7. Visualisation

7.9.7.1. tfjs-vis

import 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@latest'

Here too, if we want to get rid of the console source map error, we need to remove the last comment in the file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//import 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js'
import '../tf.js'
//import 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@latest'
import '../tfjs-vis.min.js'

const trainData = {
  sizeMB: [0.080, 9.000, 0.001, 0.100, 8.000,
    5.000, 0.100, 6.000, 0.050, 0.500,
    0.002, 2.000, 0.005, 10.00, 0.010,
    7.000, 6.000, 5.000, 1.000, 1.000],
  timeSec: [0.135, 0.739, 0.067, 0.126, 0.646,
    0.435, 0.069, 0.497, 0.068, 0.116,
    0.070, 0.289, 0.076, 0.744, 0.083,
    0.560, 0.480, 0.399, 0.153, 0.149]
}
const testData = {
  sizeMB: [5.000, 0.200, 0.001, 9.000, 0.002,
    0.020, 0.008, 4.000, 0.001, 1.000,
    0.005, 0.080, 0.800, 0.200, 0.050,
    7.000, 0.005, 0.002, 8.000, 0.008],
  timeSec: [0.425, 0.098, 0.052, 0.686, 0.066,
    0.078, 0.070, 0.375, 0.058, 0.136,
    0.052, 0.063, 0.183, 0.087, 0.066,
    0.558, 0.066, 0.068, 0.610, 0.057]
}

const trainTensors = {
  sizeMB: tf.tensor1d(trainData.sizeMB),
  timeSec: tf.tensor1d(trainData.timeSec)
}
const testTensors = {
  sizeMB: tf.tensor1d(testData.sizeMB),
  timeSec: tf.tensor1d(testData.timeSec)
}

const model = tf.sequential()
model.add(tf.layers.dense({units: 1, inputShape: [1]}))
const optimizer = tf.train.sgd(0.0005)
model.compile({optimizer: optimizer/*'sgd'*/, loss: 'meanAbsoluteError'});
(async () => {
  await model.fit(trainTensors.sizeMB,
    trainTensors.timeSec,
    {epochs: 200})
})()
console.log('Model evaluation:')
model.evaluate(testTensors.sizeMB, testTensors.timeSec).print()

const avgDelaySec = tf.mean(trainData.timeSec);
console.log('Average delay: ')
avgDelaySec.print();

tf.mean(tf.abs(tf.sub(testData.timeSec, 0.295))).print();

const smallFileMB = 1
const bigFileMB = 100
const hugeFileMB = 10000
model.predict(tf.tensor1d([smallFileMB, bigFileMB, hugeFileMB])).print()

const values = [], series = ['Training data']
for (let i = 0; i < trainData.sizeMB.length; i++)
  values.push({x: trainData.sizeMB[i], y: trainData.timeSec[i]})
console.log(values)
//tfvis.render.linechart(plotDiv, {values, series}, {width: 400, xLabel: 'sizeMB', yLabel: 'timeSec'})
tfvis.render.scatterplot({name: 'Size vs time'}, {values, series}, {width: 400, xLabel: 'sizeMB', yLabel: 'timeSec'})
/*let values = [{x: 1, y: 20}, {x: 2, y: 30}, {x: 3, y: 5}, {x: 4, y: 12}];
tfvis.render.linechart(plotDiv, {values}, {
  width: 400
})*/

7.9.8. Using pre-trained models

There is a repository of pre-trained Tensorflow.js models.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang=en>
<head>
    <meta charset=UTF-8>
    <title>Image detection</title>
    <script src=https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.js></script>
    <script src=https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet></script>
    <script type=module src=index.mjs></script>
    <!-- https://www.pexels.com/search/truck -->
</head>
<body>
<header>
    <input type=file>
    File size: <span></span>
</header>
</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const handleFile = e => {
  const file = e.target.files[0]
  const reader = new FileReader()
  const display = async e => {
    const img = document.createElement('img')
    img.src = e.target.result
    const oldChild = document.querySelector('img')
    if (oldChild) document.body.replaceChild(img, oldChild)
    else document.body.appendChild(img)
    const model = await mobilenet.load()
    const predictions = await model.classify(img)
    console.log('Predictions: ', predictions)
  }
  reader.addEventListener('load', display)
  reader.readAsDataURL(file)
  document.querySelector('span').innerHTML = `${file.size} bytes`
}

document.querySelector('input').addEventListener('change', handleFile)

7.9.9. Examples

7.9.9.1. Boston housing
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest'
import 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@latest'

const NUM_EPOCHS = 200, BATCH_SIZE = 40, LEARNING_RATE = 0.01
// Boston Housing data constants:
const BASE_URL = 'https://storage.googleapis.com/tfjs-examples/multivariate-linear-regression/data/'
const filenames = ['test-data.csv', 'test-target.csv', 'train-data.csv', 'train-target.csv']
const tensors = {}

const determineMeanAndStddev = data => {
    const dataMean = data.mean(0)
    // TODO(bileschi): Simplify when and if tf.var / tf.std added to the API.
    const diffFromMean = data.sub(dataMean)
    const squaredDiffFromMean = diffFromMean.square()
    const variance = squaredDiffFromMean.mean(0)
    const dataStd = variance.sqrt()
    return {dataMean, dataStd}
}

const normalizeTensor = (data, dataMean, dataStd) => {
    return data.sub(dataMean).div(dataStd)
}

const linearRegressionModel = numFeatures => {
    const model = tf.sequential()
    model.add(tf.layers.dense({inputShape: [numFeatures], units: 1}))
    model.summary()
    return model
}

// Read and process input data.
// First we create CSVDatasets (cf. https://js.tensorflow.org/api/latest/#data.csv)
const testDataDS = tf.data.csv(BASE_URL + filenames[0])
const testTargetDS = tf.data.csv(BASE_URL + filenames[1])
const trainDataDS = tf.data.csv(BASE_URL + filenames[2])
const trainTargetDS = tf.data.csv(BASE_URL + filenames[3])
// If we want to take a look at the data.
//await testDataDS.forEachAsync(e => console.log(e))
// Get the number of features and labels.
const features = await testDataDS.columnNames()
const numFeatures = features.length
const labels = await testTargetDS.columnNames()
const numLabels = labels.length
// Convert the data from object to array form to serve as input for the model fit method (cf.
// https://js.tensorflow.org/api/latest/#tf.LayersModel.fitDataset)
const flattenedTestDataDS = await testDataDS.map(xs => {
    return Object.values(xs)
})
const testDataArr = await flattenedTestDataDS.toArray()
const flattenedTestTargetDS = await testTargetDS.map(xs => {
    return Object.values(xs)
})
const testTargetArr = await flattenedTestTargetDS.toArray()
const flattenedTrainDataDS = await trainDataDS.map(xs => {
    return Object.values(xs)
})
const trainDataArr = await flattenedTrainDataDS.toArray()
const flattenedTrainTargetDS = await trainTargetDS.map(xs => {
    return Object.values(xs)
})
const trainTargetArr = await flattenedTrainTargetDS.toArray()
// Convert data arrays to tensors.
tensors.rawTrainFeatures = tf.tensor2d(trainDataArr)
//tensors.rawTrainFeatures.print(true)
tensors.trainTarget = tf.tensor2d(trainTargetArr)
tensors.rawTestFeatures = tf.tensor2d(testDataArr)
//tensors.rawTestFeatures.print(true)
tensors.testTarget = tf.tensor2d(testTargetArr)
// Normalize mean and standard deviation of data.
let {dataMean, dataStd} = determineMeanAndStddev(tensors.rawTrainFeatures)
tensors.trainFeatures = normalizeTensor(tensors.rawTrainFeatures, dataMean, dataStd)
tensors.testFeatures = normalizeTensor(tensors.rawTestFeatures, dataMean, dataStd)
console.log('*********************')
console.log(tensors)
tensors.rawTrainFeatures.print()
console.log('*********************')

// Regression analysis model
const modelRA = tf.sequential()
modelRA.add(tf.layers.dense({inputShape: [numFeatures], units: 1}))
modelRA.summary()
tfvis.show.modelSummary({name: 'Model RA summary'}, modelRA)
modelRA.compile(
    {
        optimizer: tf.train.sgd(LEARNING_RATE),
        loss: 'meanSquaredError',
        metrics: ['accuracy']
    })

let surface = {name: 'show.history live', tab: 'Training RA'}
let trainLogs = []
const resultRA = await modelRA.fit(tensors.trainFeatures, tensors.trainTarget, {
    batchSize: BATCH_SIZE,
    epochs: NUM_EPOCHS,
    shuffle: true,
    validationSplit: 0.2,
    callbacks: {
        onEpochEnd: async (epoch, logs) => {
            trainLogs.push(logs)
            tfvis.show.history(surface, trainLogs, ['loss', 'val_loss'])
            const featureDescriptions = [
                'Crime rate', 'Land zone size', 'Industrial proportion', 'Next to river',
                'Nitric oxide concentration', 'Number of rooms per house', 'Age of housing',
                'Distance to commute', 'Distance to highway', 'Tax rate', 'School class size',
                'School drop-out rate'
            ]
            const describeKernelElements = kernel => {
                tf.util.assert(
                    kernel.length == 12,
                    `kernel must be a array of length 12, got ${kernel.length}`)
                const outList = []
                for (let idx = 0; idx < kernel.length; idx++) {
                    outList.push({description: featureDescriptions[idx], value: kernel[idx]})
                }
                return outList
            }

            modelRA.layers[0].getWeights()[0].data().then(kernelAsArr => {
                const weightsList = describeKernelElements(kernelAsArr)
                console.log(weightsList)
            })
        }
    }
})
console.log(resultRA)

// MultiLayer perceptron regression model with 1 hidden layer
const modelMLPR = tf.sequential()
modelMLPR.add(tf.layers.dense({
    inputShape: [numFeatures],
    units: 50,
    activation: 'sigmoid',
    kernelInitializer: 'leCunNormal'
}))
modelMLPR.add(tf.layers.dense({units: 1}))
modelMLPR.summary()
tfvis.show.modelSummary({name: 'Model MLPR 1 hl summary'}, modelMLPR)
modelMLPR.compile(
    {
        optimizer: tf.train.sgd(LEARNING_RATE),
        loss: 'meanSquaredError',
        metrics: ['accuracy']
    })
surface = {name: 'show.history live', tab: 'Training MLPR'}
trainLogs = []
const resultMLPR = await modelMLPR.fit(tensors.trainFeatures, tensors.trainTarget, {
    batchSize: BATCH_SIZE,
    epochs: NUM_EPOCHS,
    shuffle: true,
    validationSplit: 0.2,
    callbacks: {
        onEpochEnd: async (epoch, logs) => {
            trainLogs.push(logs)
            tfvis.show.history(surface, trainLogs, ['loss', 'val_loss'])
        }
    }
})
modelMLPR.layers[0].getWeights()[0].print()
console.log(resultMLPR)

// MultiLayer perceptron regression model with 2 hidden layers
const model2MLPR = tf.sequential()
model2MLPR.add(tf.layers.dense({
    inputShape: [numFeatures],
    units: 50,
    activation: 'sigmoid',
    kernelInitializer: 'leCunNormal'
}))
model2MLPR.add(tf.layers.dense(
    {units: 50, activation: 'sigmoid', kernelInitializer: 'leCunNormal'}))
model2MLPR.add(tf.layers.dense({units: 1}))
model2MLPR.summary()
tfvis.show.modelSummary({name: 'Model MLPR 2 hl summary'}, model2MLPR)
model2MLPR.compile(
    {
        optimizer: tf.train.sgd(LEARNING_RATE),
        loss: 'meanSquaredError',
        metrics: ['accuracy']
    })
surface = {name: 'show.history live', tab: 'Training 2 MLPR'}
trainLogs = []
const result2MLPR = await model2MLPR.fit(tensors.trainFeatures, tensors.trainTarget, {
    batchSize: BATCH_SIZE,
    epochs: NUM_EPOCHS,
    shuffle: true,
    validationSplit: 0.2,
    callbacks: {
        onEpochEnd: async (epoch, logs) => {
            trainLogs.push(logs)
            tfvis.show.history(surface, trainLogs, ['loss', 'val_loss'])
        }
    }
})
model2MLPR.layers[0].getWeights()[0].print()
console.log(result2MLPR)

7.9.10. Natural language processing (NLP)

www.nltk.org

A great and free starting point: www.nltk.org/book

7.10. Resources

7.10.1. Research

Unsupervised feature learning

Contrastive predictive coding

arxiv.org/abs/1807.03748

7.10.4. Machine learning on encrypted data

8. Exercises

8.1. HTML5

8.1.1. Exercise 1

Write an HTML5 document with the title My first HTML5 page that includes the empty files ex1.css (CSS3) and ex1.js (JavaScript), which you create. Your page displays the text I’m learning HTML5! and passes HTML5 and CSS3 validation without errors.

8.1.2. Exercise 2

Create an HTML5 document, which produces the following output (without the border):

xercise 2

Make sure to choose tags that are semantically correct, i.e. convey the right meaning. Don’t forget to validate your page.

8.1.3. Exercise 3

Create an HTML5 document with a header and an ordered list that uses the three ordered list attributes.

8.1.4. Exercise 4

Create an HTML5 document with a description list that describes the competences of the SYSEX1 module (cf. portal.education.lu/programmes/Programme-Formation-professionnelle).

8.1.5. Exercise 5

Create two HTML5 documents that look like this:

ex5html
ex5p2html

The first page is named ex5.html, the second ex5p2.html. The first link opens the second page in the same window/tab. The second link opens the page www.ltam.lu, also in the same window/tab. The first page includes the following external style sheet, which you should save under the name ex5.css:

1
2
3
4
5
6
7
8
9
ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

li {
  display: inline;
}

8.1.6. Exercise 6

Create an HTML5 document that uses an image, that you have created, as navigation bar. The user can click on different parts of the image, which will take him to other pages. The main part of the document can be empty. Here is a sample solution:

8.2. JavaScript

8.2.1. Exercise 1

In a group execute the "marching orders" found in the book at csunplugged.org/books.

8.2.2. Exercise 2

Complete all 15 levels of lightbot.lu.

8.2.3. Exercise 3

Create a valid HTML5 document that includes an external script. The script defines a function that displays a message box with a text of your choice. Your <main> element includes an image which, if clicked, displays the message box.

8.2.4. Exercise 4

Create a valid HTML5 document with an embedded script that uses all the basic input and output functions seen in Basic input and output. Take the opportunity to experiment to your heart’s content.

8.2.5. Exercise 5

Write JavaScript that declares a variable, gives it a value and writes its value to the console.

8.2.6. Exercise 6

Create a web page that declares four variables with the values 'Text', 123, 23.45 and false respectively. It then generates an alert that displays the type of each one of the four variables.

8.2.7. Exercise 7

Write a script that defines two string variables. The first one contains your first name, the second one your last name. Define a third variable that contains the first variable followed by a space followed by the second variable. Verify the result in the console.

8.2.8. Exercise 8

Write a script that defines two variables with the values 5 and 7.233453543 respectively. It then writes the sum with 2 decimals to the console.

8.2.9. Exercise 9

Without using a computer, write down the answers to the following questions, then check your solutions using the console:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
"use strict";
// Let's declare a few variables first.
var x, y, s1 = 'Hello', car = {weight: 1500, color: 'black'};
// Q1: What is the value of x after this statement?
x = Math.round(10 / 3);
// Q2: What is the value of y after this statement?
y = 10 % 3;
// Q3: What is the value of x after this statement?
x += y;
// Q4: What value does this statement give?
x === y;
// Q5: And this one?
x % x;
// Q6: What value does this statement give?
111 + 111;
// Q7: And this one?
111 + '111';
// Q8: And this one?
'111' + 111;
// Q9: And this one?
x & 0;
// Q10: And this one?
x | 1;
// Q11: And this one?
x << 2;
// Q12: And this one?
typeof x;
// Q13: And this one?
typeof s1;
// Q14: And this one?
+s1;
// Q15: What is the value of car after this statement?
delete car.weight;
// Q16: What value does this statement give?
x === '5';
// Q17: And this one?
x == '5';
// Q18: And this one?
x === '4';
// Q19: And this one?
x == '4';
// Q20: And this one?
x === 4;
// Q21: And this one?
y !== '1';
// Q22: And this one?
y >= 1;
// Q23: And this one?
(x > 1) && (y < 27);
// Q24: And this one?
!(x < 1) || (y > 27);
// Q25: And this one?
(x < 1) ? 'yes' : 'no';

8.2.10. Exercise 10

Write a valid HTML5 document that implements the following items:

  1. The user is asked to enter two integers, which are saved in variables x and y.

  2. Variable r contains the remainder of x divided by y.

  3. Variable bool contains true if x is bigger than y, otherwise false.

  4. Variable s1 contains the text 'Hello', variable s2 the text 'guest'.

  5. Variable s contains the text 'Hello guest!', which is created from s1 and s2 in a single statement.

  6. A window displays the values of r, bool and s.

8.2.11. Exercise 11

Create a valid HTML5 document that asks the user a question and then informs him whether his answer was correct or not.

8.2.12. Exercise 12

Create a valid HTML5 document that asks for a grade between 1 and 60 and then tells the user whether it is very bad (<10), bad (<20), insufficient (<30), sufficient (<40), good (<50), very good (<60) or excellent (60).

8.2.13. Exercise 13

Create a valid HTML5 document that displays the current day of the week in text form using switch.

8.2.14. Exercise 14

Write a script that calculates and displays the factorial of a positive integer stored in the variable x. Remember, \$!\$ is the mathematical symbol for factorial, \$x! = x * (x-1)!\$ and \$1! = 1\$.

8.2.15. Exercise 15

The user is asked for a number. If the number is not positive, nothing happens. Otherwise, a count down, starting at the number and counting down to 0, is displayed using a message box.

8.2.16. Exercise 16

Write a script that calculates the greatest common divisor (gcd, cf. en.wikipedia.org/wiki/Greatest_common_divisor) of two given positive integers stored in variables a and b. A simple method to do this is to realize that the gcd cannot be bigger than the smallest of the two numbers. So that’s your starting point. Now you can find the gcd by testing whether the smallest of a and b divides both. If not, you subtract 1 from that value and test again. Your loop must terminate, because 1 is a divisor of every number.

8.2.17. Exercise 17

Write a function sumUpTo that takes a positive integer as parameter and returns the sum of all integers from 1 up to this parameter.

8.2.18. Exercise 18

Improve the previous function so that if the parameter is not a positive integer, the function returns false.

8.2.19. Exercise 19

Write a function createTable that takes two parameters, w and h. The function writes an HTML table with w columns and h rows. Put a random number from [1, 9] into each cell. Validate the result.

8.2.20. Exercise 20

Write a function average that returns the average of all the parameters provided. The function can take any number of parameters, including none. If no parameters are provided or if any of the parameters is not a number, the function returns false. Analyze and verify your function in the debugger.

8.2.21. Exercise 21

Write a script that creates an array containing the numbers from 1 to 10. Add a function displayArr that iterates over the array and writes each array element to the console. Call displayArr, remove the last element and call displayArr again.

8.2.22. Exercise 22

Write a script that creates a matrix, i.e. a 2-dimensional array of 10 rows, each containing 20 elements. Use Math.random (cf. www.w3schools.com/jsref/jsref_random.asp) to give a random value to each array element. Write a function displayMat that displays the matrix, which is passed as parameter in a readable way, i.e. each row on a new line.

8.2.23. Exercise 23

Create a page that triggers an alert when the user tries to resize the browser window.

8.2.24. Exercise 24

Create a page that generates an alert saying "Hi!" when the h key is pressed.

8.2.25. Exercise 25

Create a web app with a button. Clicking on the button displays an alert with the text "You clicked me!".

8.2.26. Exercise 26

Write a function that takes any number of string arguments representing element ids. For each ID the function retrieves the corresponding element. If any of the ids is invalid, an error message is thrown, otherwise the function returns an object that maps each id to its DOM element.

8.2.27. Exercise 27

Create a web app with 3 buttons. You may not use getElementById. Clicking on any of the buttons displays an alert with the text "You clicked button number " followed by the number of the button (1-3). For experts: enhance your script so that it works for any number of buttons.

8.2.28. Exercise 28

Create a web page with 3 article tags, two of which belong to class special. Using JavaScript, change the background color of the articles that are special.

8.2.29. Exercise 29

Go to one of the well known web pages. Open the console and take a look at the HTML objects listed above. Change the title of the page.

8.2.30. Exercise 30

Create a page with an image and 2 buttons. Pressing the first button sets the first image, pressing the second button sets the second image.

8.2.31. Exercise 31

Simplify students.btsi.lu/evegi144/WAD/JS/DOM_CSS2.html by replacing the four functions moveLeft, moveRight, moveUp and moveDown with a single function move.

8.2.32. Exercise 32

Add the possibility for the user to change the number of pixels that the gorilla moves when a button is clicked.

8.2.33. Exercise 33

Remove the buttons from the previous example and modify the script so that the gorilla can be moved with the cursor keys.

8.2.34. Exercise 34

Create an HTML page with a button. If the user clicks the button, he will be automatically transferred to wsers.foxi.lu after 2 seconds.

8.2.35. Exercise 35

Create an empty HTML page. After 1 second the page background color changes to red, then after 4 seconds it changes to green.

8.2.36. Exercise 36

Create an HTML page with a button. After 1 second the page background color changes to red, then after 1 second it changes to green, after another second it changes back to red, then back to green etc. When the button is clicked, the background color does not change anymore.

8.2.37. Exercise 37

Repeat the previous exercise using setInterval instead of setTimeout.

8.2.38. Exercise 38

Create a document with an image and use requestAnimationFrame to implement a simple animation of your choice.

8.2.39. Exercise 39

Create a valid HTML5 page that contains an image. If the user clicks on the image, a PHP script is executed that sends additional HTML code, which is inserted after the image in the current document.

8.3. Node

8.3.1. Exercise 1

Topics: AJAX.

Write a web app that provides asynchronous registration and login.

8.3.2. Exercise 2

Topics: JSON.

Write a web app that retrieves large amounts of data from an external API.

8.3.3. Exercise 3

Topics: IIFEs.

Create a web page that runs JS without polluting the global namespace.

8.3.4. Exercise 4

Topics: Promises.

Write a web app that creates and uses promises.

8.3.5. Exercise 5

Topics: Async/await.

Write a web app that creates asynchronous functions and uses them synchronously.

8.3.6. Exercise 6

Topics: Installation, configuration and package management.

Install and configure Node and NPM on your own server. Install some packages that you find useful. Submit a document explaining all relevant steps and provide evidence that your installation works correctly.

8.3.7. Exercise 7

Topics: Streams.

Write a Node script that generates a very large text file with some random text.

Write a second script that can read a very large text file and display the number of occurrences of a given character in it without consuming much server memory. The file name and the character to count are specified via the command line e.g. node ex2.js a1.txt a. The script also displays the maximum amount of memory used.

Delete the large text file to avoid the server backup task being strained unnecessarily.

8.3.8. Exercise 8

Topics: Express basics.

Write an Express web server.

8.4. PHP

8.4.1. Exercise 1

Topics: variables, HTML generation.

Write a PHP script that stores your name in a variable and displays the text `My name is ` followed by your name followed by a point. Your script may not use more than one echo statement. The generated page needs to be HTML5 validated.

8.4.2. Exercise 2

Topics: potentially variables, potentially loops, HTML generation, PHP functions.

Write a script that calculates and displays the sum of all integers between 1 and the value of a random integer variable $limit. Use the rand function.

8.4.3. Exercise 3

Topics: conditions, loops, HTML generation, PHP functions.

Write a loop that generates a new random integer from [23, 77] and prints the number on a separate line during each iteration. The loop stops if the number is a multiple of 13.

8.4.4. Exercise 4

Topics: conditions, loops, HTML generation, PHP functions.

Modify exercise 3 so that the loop skips all iterations where the random number is a multiple of 2.

8.4.5. Exercise 5

Topics: nested loops, HTML generation.

Print a matrix of (x, y) coordinates, x from [1, 50] and y from [10, 20]. Your output must be generated programmatically i.e. you may not use one gigantic echo statement! Your solution should be very easy to adapt for different ranges of x and y.

8.4.6. Exercise 6

Topics: variables, loops, HTML generation, PHP functions.

Write a PHP script that:

  1. stores 5 random integer values in an array

  2. displays the array

  3. displays a value randomly selected from the array

  4. calculates the sum of the array elements using a standard for loop

  5. displays the sum

  6. recalculates the sum of the array elements using a foreach loop

  7. displays the sum

  8. deletes the third array element without creating a hole in the array

  9. recalculates and displays the sum of the array elements using a loop of your choice.

8.4.7. Exercise 7

Topics: variables, arrays, conditions, HTML generation, PHP functions.

Write a script that stores the words "sunshine", "rain" and "cloudy" in an array. It then draws a random integer from [0, 2] and displays the text "Today’s weather forecast: " followed by the element at the random array position. It then displays one of the following comments, depending on the weather forecast: "Beautiful day ahead!", "Never mind…​" or "It could be worse…​".

8.4.8. Exercise 8

Topics: variables, arrays, conditions, loops, HTML generation, PHP functions.

Write a PHP script that stores the random grades (from [1, 60]) of 3 students, so that each grade is associated with the corresponding student’s name. Display the names with their corresponding grades. Now increase each grade that is less than 59 by 2 points and if it is 59 by 1 point using array_keys and a standard for loop and display the names and grades again.

8.4.9. Exercise 9

Topics: variables, arrays, conditions, loops, HTML generation, PHP functions.

Create an associative array that uses the names of a few dogs (at least 3) as the keys and the dogs' ages as values (the age is a random integer from [1, 15]). Display the array, loop through the array and print the name of each dog followed by its age. If the age is 1 it is followed by "year", otherwise by "years".

8.4.10. Exercise 10

Topics: variables, arrays, loops, HTML generation.

Write a script that stores the following collection of software titles and displays them nicely:

Microsoft

Office

Word
Excel
Access
Powerpoint

OS

Windows 7
Windows 8
Windows 8.1

Mozilla

Desktop

Firefox
Thunderbird

OS

Firefox OS
Top Secret

8.4.11. Exercise 11

Topics: variables, arrays, loops, HTML generation, PHP functions.

Modify the multidimensional arrays example so that it uses only standard for loops instead of foreach.

8.4.12. Exercise 12

Topics: variables, arrays, loops, functions, HTML generation, PHP functions.

Write a function sumArray that returns the sum of all elements (we assume they are numbers) contained in an array passed as parameter. You may not use array_sum.

8.4.13. Exercise 13

Topics: variables, loops, functions, HTML generation, PHP functions.

Write a function sum that returns the sum of all the numbers passed as parameters. The user can pass as many parameters as they like.

8.4.14. Exercise 14

Topics: external content inclusion, HTML generation.

Write a pure PHP script that includes a header file containing a valid HTML doctype and head declaration as well as the opening body tag. Your PHP script then sends some HTML to the browser and then includes a footer file that closes the html tag.

8.4.15. Exercise 15

Topics: conditions, forms, HTML generation.

Create a single file web page with a navigation menu offering a selection of different content choices.

8.4.16. Exercise 16

Topics: conditions, forms, HTML generation.

Write a login form that verifies the user name is "T2IF" and the password "PHP". The latter should not be displayed in the form.

Create two versions of your script. Each one uses a different HTTP method to send the form data to the server.

8.4.17. Exercise 17

Topics: conditions, functions, forms, external content inclusion, HTML generation.

Write a script that reads in an email address via a form and validates it, i.e. displays a message telling the user whether the email address is valid or not. Use Carl Henderson’s function as well as the checkdnsrr function.

8.4.18. Exercise 18

Topics: variables, arrays, conditions, loops, forms, HTML generation, PHP functions.

Write a grade spreadsheet that allows the user to enter 3 integers. After submission, the script displays the list of all the number triples entered so far and the average for each triple is shown at the end of the line. In addition, the average of all the averages is displayed. You may NOT use sessions. Use form fields instead.

8.4.19. Exercise 19

Topics: variables, cookies, HTML generation, PHP functions.

Write a script that sets a cookie with a value of your choice. The cookie expires after 2 minutes and is only active in the directory cookies and its subdirectories. The cookie will only be sent via HTTPS. Write a test script that proves that the cookie will only be active in the cookies directory tree (you may want to use the dirname or the header function).

8.4.20. Exercise 20

Topics: variables, conditions, forms, sessions, HTML generation, PHP functions.

Create a login form for the user name T2IF1 and the password WSERS1. After successful login, the user is taken to the main page, from which he/she can log out. All of the files for this exercise need to be protected, i.e. only the logged in user can make them execute their core functionality.

8.4.21. Exercise 21

Topics: variables, arrays, conditions, loops, functions, HTML generation, PHP functions.

Write the function

1
array reverse(array $array)

that can be called with any array and returns the reverse array, e.g. reverse([1, 2, 3]) returns [3, 2, 1]. You may NOT use array_reverse!

8.4.22. Exercise 22

Topics: variables, arrays, conditions, loops, functions, HTML generation, PHP functions.

Create the function

1
string create_inputs(array $data)

that can be called with a 2-dimensional array, like this:

1
echo create_inputs([['User name', 'user_name'], ['Password', 'password']]);

Here is the result:

More generally, the function can be called with an array which contains any number of arrays, each one consisting of the placeholder and the name to be used to generate the input elements. The function returns the input tags ready to be written to the HTML document (see the HTML source).

8.4.23. Exercise 23

Topics: debugging.

Debug the following script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<html lang=en>
  <head>
    <meta charset=UTF-8>
    <title>Debug 1</title>
    <from method=prost>
      <input type=tv name=action value=radial checked>Radial gradient<br>
      <input type=tv name=action value=linear>Linear gradient<br>
      Number of stops: <input type=range name=stops min=2 max=10 value=6 step=1><br>
      <button name=submit>Submit</button>
    </from>
    <!php
      fun get_gradiant_JS($style = 'radial', $stop_count = 2) {
        $style !== 'radial' && $style !== 'linear' || $stop_count < 2 || $stop_count
    > 10)
          return '';
        $stops === [];
        $JS    = "<scrip>document.body.style.background = 'repeating-$style-gradient(";
        for ($i == 0; $i < $stop_count; $i--) {
          $stops = [rand(0, 255), rand(0, 255), rand(0, 255)];
          $JS .= 'rgb($stops[$i][0], $stops[$i][1], $stops[$i][2])';
          if ($i < ($stop_count - 1)) $JS .= ', ';
        }
        echo '<pre>' . print_r($stops) . '</pre>';
        if ($style = 'radial') $idx = 0;
        else $idx = 1;
        return $JS . ") " .
        "fixed'; document.forms[0].querySelectorAll('input')[2].value=$stop_count;" .
        "document.forms[0].querySelectorAll('input')[$idx].checked = true;</scrip>";
      }
      if (!isset($_POST['submit'])) echo get_gradiant_JS($_POST['action'],
    $_POST['stops']);
    !>
  </head>
</html>

The correct result can be seen at youtu.be/0H-WwVeoK0U.

8.4.24. Exercise 24

Topics: variables, arrays, conditions, loops, functions, forms, sessions, HTML generation, PHP functions.

Create an online shop that sells your favorite articles (e.g. cars, computer games, music, . ..). Consider the following:

  • Anyone can visit your shop and take a look at your articles.

  • Only a logged in user can buy anything, so you need to provide a login and a logout facility.

  • For now, there is only one user and you can store the user name and password in a script.

  • When a logged in user buys something, his purchase gets stored so that as long as he does not log out or his session has expired, he can always see his past purchases and the total price of all articles bought.

  • The session must be secure.

  • When a user logs out, the session cookie must be deleted from the user’s browser.

Provide login details.

8.4.25. Exercise 25

Topics: variables, arrays, loops, HTML generation, PHP functions.

For your web shop you want to store the details of the logged in user in a global variable named $loggedin_user.

This information consists of:

  • User name accessible via the key user_name.

  • Join date accessible via the key join_date.

  • Number of past purchases accessible via the key num_past_purchases.

  • Total value of past purchases accessible via the key val_past_purchases.

For now you can store fixed values in $loggedin_user. Select a reasonable data type for each item.

Create the global variable and display its content in an HTML table exactly as shown.

You must use the function array_keys as well as a loop to iterate through the global variable.

8.4.26. Exercise 26

Topics: variables, conditions, forms, navigation, HTML generation, PHP functions.

Create the single file app exactly as shown. This represents a basic template for your shop’s navigation.

8.4.27. Exercise 27

Topics: variables, conditions, forms, navigation, HTML generation, PHP functions.

In this exercise you add login, registration and logout functionality to your shop exactly as shown. The hard coded login is T2IF with password PHP. Explain what is missing for this to be fully functional and suggest possible solutions.

8.4.28. Exercise 28

Topics: variables, conditions, forms, functions, navigation, HTML generation, PHP functions.

Add the function get_articles to the previous exercise. This function returns an array containing all the articles of your shop. For now you should randomly generate the articles in your script. Soon we’ll retrieve them from our database.

Add the function get_table that takes an array of articles as parameter and returns a string containing an HTML table with all articles.

It does not matter what articles your shop offers for sale. What matters is that you think about the information that characterises your articles and that your functions can handle any number of articles.

8.4.29. Exercise 29

Topics: variables, arrays, conditions, loops, forms, sessions, functions, HTML generation, PHP functions.

Create the single file app exactly (including styling) as shown. This app allows to add a new user by specifying a name. Furthermore, existing users can be deleted. Note that reloading the page does not delete the user information.

Your script may not store any data in a file or database.

To implement your solution proceed as follows:

  1. Each user is stored with a unique id which is determined by your script. The easiest is to start with id 0 and increase it by 1 for every new user created. Create the function get_next_id which returns the next id that is available. For instance, if you have already three users with ids 0, 1 and 2, this function will return 3.

  2. Create the function delete_user which takes a user id as parameter. The function deletes the user with the given id.

  3. Create the function display_users which displays the users, each with its own delete button.

  4. Create the function display_add_form which displays the form to add a new user.

8.5. MySQL

8.5.1. Exercise 1

Topics: Problem modelling and SQL table creation.

Plan and formulate in SQL the structure of a user table for your online shop from PHP exercise 24.

8.5.2. Exercise 2

Topics: phpMyAdmin, SQL table creation and data insertion.

Create the user table that you planned in the previous exercise using phpMyAdmin and store a few users in it. Delete the table and recreate it using your SQL script either in PhpStorm or via the MySQL command line.

Provide your SQL script as well as screenshots proving the table and user creation in phpMyAdmin and PhpStorm and/or MySQL.

8.5.3. Exercise 3

Topic: Access MySQL from PHP via mysqli.

Enhance your shop by using your new user table for the login, i.e. any user stored in the table can log in.

Provide login details.

8.5.4. Exercise 4

Topic: Access MySQL from PHP via prepared statements.

Modify the previous exercise so that it uses prepared statements to prevent SQL injection attacks.

Provide login details.

8.5.5. Exercise 5

Topics: Insert and read MySQL data from PHP.

Add a signup functionality to your shop, i.e. a new user can create an account by registering. After a successful registration, the user can log in and shop. Passwords may only be stored in hashed form in the DB. Remember that security is very important. Do your best to hijack your solution and improve it until you are confident that it is secure.

8.5.6. Exercise 6

Topics: Problem modelling, SQL table creation, insert and read MySQL data from PHP.

Plan and formulate in SQL the structure of one or more tables that you need for your shop to store all available articles as well as every customer’s purchases in the DB so that they are not lost even after session termination. Implement the new structure in your app and let the logged-in user see his full purchase history.

8.5.7. Exercise 7

Topics: MySQL-CRUD (create, read, update and delete) operations from PHP.

Add an administrator account to your shop. The admin is the only user who can add, edit and delete the products that your shop is offering. Only products that have not yet been purchased can be deleted. If the admin tries to delete a product that has already been bought an error message will be shown.

Provide login details.

8.5.8. Exercise 8

Topics: MySQL-CRUD (create, read, update and delete) operations from PHP using PDO.

Modify your solution of the previous exercise so that all DB access is done via PHP Data Objects (PDO).

Provide login details.

9. Problems

9.1. WMOTU Games

Our company, WMOTU (Web Masters Of The Universe), has decided to enter the online computer game sales business. Plan and develop the the web site using most of the material covered in sections HTML5 and CSS3.

9.2. WMOTU League

We have been asked to develop a football league information app.

9.2.1. WMOTU League Service

Develop a script that displays the rank and number of points for a given football club. For instance, index.php?club=Leverkusen displays the current rank and number of Leverkusen. For now store the club data in an array.

9.2.1.1. Solution
1
2
3
4
5
6
7
8
9
<?php
  $rankings = array("Leverkusen"     => array(1, 22),
                    "Bayern München" => array(2, 20),
                    "Dortmund"       => array(3, 19));
  if (isset($_GET['club'])) {
    $res = $rankings["{$_GET['club']}"];
    echo "Rank: $res[0]  points: $res[1]";
  }
?>

9.2.2. WMOTU League

Develop an app that displays the football results saved in the file league.csv. The result should look like this screenshot. Use the PHP functions file and explode.

9.2.2.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU League</title>
    <meta charset=UTF-8>
    <link href=style.css rel=stylesheet>
  </head>
  <body>
    <?php
      $contents = @file('league.csv');
      if (!$contents) $array = null;
      else {
        echo "<table><thead><tr><th>Rank</th><th>Club</th><th>Points</th></tr></thead>";
        foreach ($contents as $line) {
          $line = explode(',', $line);
          echo "<tr><td>{$line[0]}</td><td>{$line[1]}</td><td>{$line[2]}</td></tr>";
        }
        echo "</table>";
      }
    ?>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
label {
  float:         left;
  clear:         left;
  width:         60px;
  text-align:    right;
  padding-right: 10px;
  margin-top:    5px;
  display:       inline-block;
}

input {
  margin-top: 9px;
}

table {
  margin-top:       10px;
  border:           1px solid black;
  border-spacing:   0;
  background-color: green;
}

table th, td {
  border:  4px groove green;
  padding: 4px;
}

table th {
  color:      gold;
  text-align: left;
}

table tr:nth-of-type(even) {
  color: lightgreen;
}

thead {
  background-color: darkgreen;
}

9.3. WMOTU Sorter

Develop an app (students.btsi.lu/evegi144/WAD/WMOTUSorter) that offers the user a drop down list to choose from the files testdata1.csv, testdata2.csv and testdata3.csv containing a number of records. After form submission the app displays the records in a table in alphabetical order based on the last name. Use the usort and array_splice functions.

9.3.1. Solution

We need to take precautions to prevent someone from hijacking our form and submitting a choice that we have not offered. Consider the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Sorter Hijacker</title>
    <meta charset=UTF-8>
    <link href=style.css rel=stylesheet>
  </head>
  <body>
    <form method=post action=index_vulnerable.php>
      <label for=filelist></label>
      <select id=filelist name=filelist>
        <?php
          $choices = array('hijack1.csv', 'hijack2.csv', 'hijack3.csv');
          foreach ($choices as $choice) {
            echo "<option value=$choice";
            if (isset($_POST['filelist']) && $_POST['filelist'] === $choice)
              echo " selected";
            echo ">$choice</option>";
          }
        ?>
      </select>
      <input type=submit name=submit value="Get addresses sorted by last name">
    </form>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Sorter Vulnerable</title>
    <meta charset=UTF-8>
    <link href=style.css rel=stylesheet>
  </head>
  <body>
    <form method=post>
      <label for=filelist></label>
      <select id=filelist name=filelist>
        <?php // We create the selection dynamically, so that we can change choices easily.
          $choices = array('testdata1.csv', 'testdata2.csv', 'testdata3.csv');
          foreach ($choices as $choice) {
            echo "<option value=$choice";
            if (isset($_POST['filelist']) && $_POST['filelist'] === $choice)
                echo " selected";
            echo ">$choice</option>";
          }
        ?>
      </select>
      <input type=submit name=submit value="Get addresses sorted by last name">
    </form>
    <?php
      if (isset($_POST['submit'])) {
        function compare($x, $y) {
          if ($x[2] === $y[2]) return 0;
          if ($x[2] < $y[2]) return -1;
          return 1;
        }

        $contents = @file("{$_POST['filelist']}");
        echo "Submitted filename: {$_POST['filelist']}";
        if ($contents) {
          foreach ($contents as $line) $array[] = explode('|', $line);
          echo "<table><thead><tr>";
          foreach ($array[0] as $head) echo "<th>$head</th>";
          echo "</tr></thead>";
          array_splice($array, 0, 1);
          if (usort($array, 'compare')) {
            foreach ($array as $line) {
              echo "<tr>";
              foreach ($line as $cell) echo "<td>$cell</td>";
              echo "</tr>";
            }
          }
          echo "</table>";
        }
      }
    ?>
  </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Sorter</title>
    <meta charset=UTF-8>
    <link href=style.css rel=stylesheet>
  </head>
  <body>
    <form method=post>
      <label for=filelist></label>
      <select id=filelist name=filelist>
        <?php // We create the selection dynamically, so that we can change choices easily.
          $choices = array('testdata1.csv', 'testdata2.csv', 'testdata3.csv');
          foreach ($choices as $choice) {
            echo "<option value=$choice";
            if (isset($_POST['filelist']) && $_POST['filelist'] === $choice)
              echo " selected";
            echo ">$choice</option>";
          }
        ?>
      </select>
      <input type=submit name=submit value="Get addresses sorted by last name">
    </form>
    <?php // Make sure $_POST['filelist'] is indeed one of the choices we offer and not a
        // hijacker injection.
      if (isset($_POST['submit']) && in_array($_POST['filelist'], $choices)) {
        function compare($x, $y) {
          if ($x[2] === $y[2]) return 0;
          if ($x[2] < $y[2]) return -1;
          return 1;
        }

        $contents = @file("{$_POST['filelist']}");
        if ($contents) {
          foreach ($contents as $line) $array[] = explode('|', $line);
          echo "<table><thead><tr>";
          foreach ($array[0] as $head) echo "<th>$head</th>";
          echo "</tr></thead>";
          array_splice($array, 0, 1);
          if (usort($array, 'compare')) {
            foreach ($array as $line) {
              echo "<tr>";
              foreach ($line as $cell) echo "<td>$cell</td>";
              echo "</tr>";
            }
          }
          echo "</table>";
        }
      }
    ?>
  </body>
</html>

9.4. WMOTU Sub

Develop the sink the ship game (students.btsi.lu/evegi144/WAD/WMOTUSub). The sea has a size of 20 by 20. There are 10 ships with sizes from 2 to 5.

9.4.1. Solution

9.4.1.1. Standard solution
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
<?php
  ##################
  # Session security
  ##################
  # Only send session id cookie over SSL.
  ini_set('session.cookie_secure', true);
  # Session IDs may only be passed via cookies, not appended to URL.
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_path', dirname(htmlspecialchars($_SERVER['PHP_SELF'])));
  if (!isset($_SERVER['HTTPS'])) // # SSL is not active, activate it.
  header('Location: https://' . $_SERVER['HTTP_HOST'] .
  dirname(htmlspecialchars($_SERVER['PHP_SELF'])));
  if (!isset($_SESSION)) session_start();
  # After 30 seconds we'll generate a new session ID to prevent a session
  # fixation attack (cf. PHP cookbook p. 338).
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }

  #############
  # Main script
  #############
  if (isset($_POST['newGame'])) new_game(); # User has clicked 'New game' button.

  # User has clicked 'Show ships'/'Hide ships' button
  if (isset($_POST['showShips'])) $_SESSION['showShips'] = TRUE;
  elseif (isset($_POST['hideShips'])) $_SESSION['showShips'] = FALSE;

  # Define constants to increase program readability and flexibility.
  define('SUB_BOARD_SIDE', 20);
  define('SUB_NUMBER_OF_SHIPS', pow(SUB_BOARD_SIDE, 2) / 40);
  define('SUB_HORIZONTAL', 0);
  define('SUB_VERTICAL', 1);
  define('SUB_EMPTY', 0);
  define('SUB_OCCUPIED', 1);
  define('SUB_HIT', 2);
  define('SUB_MISSED', 3);

  # If we already have an existing sea, we retrieve data from the session array.
  if (isset($_SESSION['sea']) && isset($_SESSION['ships'])) {
    $sea     = $_SESSION['sea'];
    $ships   = $_SESSION['ships'];
    $maxHits = $_SESSION['maxHits'];
  }
  else { # Otherwise we create a new one.
    $sea   = array();
    $ships = array();
    initialize();
    $_SESSION['sea']       = $sea;
    $_SESSION['ships']     = $ships;
    $_SESSION['maxHits']   = $maxHits;
    $_SESSION['showShips'] = FALSE;
    $_SESSION['shots']     = 0;
    $_SESSION['hits']      = 0;
    $_SESSION['gameOver']  = FALSE;
  }

  # If a shot been fired, handle it.
  if (isset($_POST['x']) && isset($_POST['y']))
    handle_shot($_POST['x'] - 1, $_POST['y'] - 1);

  #######################
  # Function declarations
  #######################
  # This is just a helper function for debugging purposes.
  function print_ships() {
    global $ships;
    for ($i = 0; $i < SUB_NUMBER_OF_SHIPS; $i++)
      echo "Ship " . ($i + 1) . ": " . ($ships[$i][0] + 1) . " " . ($ships[$i][1] + 1) .
        " " . $ships[$i][2] . " " . $ships[$i][3] . "<br>";
  }

  # Create sea table. If the parameter is set to TRUE, ships are shown.
  function display_sea($show_ships = FALSE) {
    if ($_SESSION['gameOver'])
      echo '<script>alert("Congratulations, you\'ve sunk all enemies!")</script>';
    global $sea;
    # Create the sea table.
    echo '<table id=sea><thead><tr><th style="width: ' . (100.0 / (SUB_BOARD_SIDE + 1)) .
      '%"></th>';
    for ($i = 1; $i <= SUB_BOARD_SIDE; $i++) {
      echo '<th style="width:' . (100 / (SUB_BOARD_SIDE + 1)) . '%">' . $i . '</th>';
    }
    echo '</tr></thead><tbody>';
    for ($row = 0; $row < SUB_BOARD_SIDE; $row++) {
      echo '<tr><th>' . ($row + 1) . '</th>';
      for ($col = 0; $col < SUB_BOARD_SIDE; $col++) {
        echo '<td style="width:' . (100 / (SUB_BOARD_SIDE + 1)) . '%"';
        if ($sea[$col][$row] === SUB_HIT) echo ' class=hit';
        elseif ($sea[$col][$row] === SUB_MISSED) echo ' class=missed';
        elseif ($show_ships && ($sea[$col][$row] === SUB_OCCUPIED))
          echo ' class=occupied';
        echo '></td>';
      }
      echo '</tr>';
    }
    echo '</tbody></table>';
    # Display shots fired and hits.
    echo 'Shots fired: ' . $_SESSION['shots'] . ' &nbsp;&nbsp;hits: ' . $_SESSION['hits'];
  }

  # Create $sea array filled with 0s.
  function initialize_sea() {
    global $sea;
    $sea = array();
    $row = array();
    for ($j = 0; $j < SUB_BOARD_SIDE; $j++) $row[] = SUB_EMPTY;
    for ($i = 0; $i < SUB_BOARD_SIDE; $i++) $sea[] = $row;
  }

  # Returns TRUE if the area starting at ($x, $y) in direction $orientation $num_cells tall
  # is not occupied. Else FALSE.
  function area_available($x, $y, $num_cells, $orientation) {
    global $sea;
    if ($orientation === SUB_HORIZONTAL) {
      for ($i = 0; $i < $num_cells; $i++)
        if ((($x + $i) >= SUB_BOARD_SIDE) || $sea[$x + $i][$y]) return FALSE;
    }
    else {
      for ($i = 0; $i < $num_cells; $i++)
        if ((($y + $i) >= SUB_BOARD_SIDE) || $sea[$x][$y + $i]) return FALSE;
    }
    return TRUE;
  }

  # Places a ship randomly on the sea, without overlapping another one.
  function place_ship($num_cells, $orientation) {
    global $sea;
    do {
      $x = rand(0, SUB_BOARD_SIDE - 1);
      $y = rand(0, SUB_BOARD_SIDE - 1);
    } while (!area_available($x, $y, $num_cells, $orientation));
    if ($orientation === SUB_HORIZONTAL)
      for ($i = 0; $i < $num_cells; $i++) $sea[$x + $i][$y] = 1;
    else
      for ($i = 0; $i < $num_cells; $i++) $sea[$x][$y + $i] = 1;
    return array($x, $y, $num_cells, $orientation);
  }

  # Create 10 ships with random size and orientation and place them randomly on the sea.
  function initialize_ships() {
    global $ships, $maxHits;
    for ($i = 0; $i < SUB_NUMBER_OF_SHIPS; $i++) {
      $ship_size        = rand(2, 5);
      $ship_orientation = rand(SUB_HORIZONTAL, SUB_VERTICAL);
      $ships[$i]        = place_ship($ship_size, $ship_orientation);
      $maxHits += $ship_size;
    }
  }

  function initialize() {
    initialize_sea();
    initialize_ships();
  }

  function handle_shot($x, $y) {
    if ($x < 0 || $x >= SUB_BOARD_SIDE || $y < 0 || $y >= SUB_BOARD_SIDE) return;
    global $sea, $ships;
    $hit         = FALSE;
    $already_hit = FALSE;
    foreach ($ships as $ship) {
      if ($ship[3] === SUB_HORIZONTAL) {
        for ($i = 0; $i < $ship[2]; $i++) { # Need to check whole length of ship.
          if (($ship[0] + $i) === $x && $ship[1] === $y) {
            if ($sea[$x][$y] === SUB_HIT) $already_hit = TRUE;
            $sea[$x][$y] = SUB_HIT;
            $hit         = TRUE;
          }
        }
      }
      else {
        for ($i = 0; $i < $ship[2]; $i++) { # Need to check whole length of ship.
          if ($ship[0] === $x && ($ship[1] + $i) === $y) {
            if ($sea[$x][$y] === SUB_HIT) $already_hit = TRUE;
            $sea[$x][$y] = SUB_HIT;
            $hit         = TRUE;
          }
        }
      }
    }
    if (!$hit) $sea[$x][$y] = SUB_MISSED;
    $_SESSION['sea']   = $sea;
    $_SESSION['ships'] = $ships;
    $_SESSION['shots']++;
    if ($hit && !$already_hit) {
      $_SESSION['hits']++;
      if ($_SESSION['hits'] === $_SESSION['maxHits']) $_SESSION['gameOver'] = TRUE;
    }

    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
    htmlspecialchars($_SERVER['PHP_SELF']));
  }

  function new_game() {
    if (!isset($_SESSION)) session_start();
    $_SESSION = array();
    if (session_id() != "" || isset($_COOKIE[session_name()])) setcookie(session_name(),
      '', time() - 2592000, '/');
    session_destroy();
    header('Location: https://' . $_SERVER['HTTP_HOST'] .
    dirname(htmlspecialchars($_SERVER['PHP_SELF'])));
  }

?>

<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Sub</title>
    <meta charset=UTF-8>
    <style>
      #sea {
        border-collapse: collapse;
        width:           100%;
        table-layout:    fixed;
      }

      #sea, #sea th, #sea td {
        border: 1px solid red;
      }

      .occupied {
        background-color: green;
      }

      .hit {
        background-color: red;
      }

      .missed {
        background-color: black;
      }
    </style>
  </head>
  <body>
    <?php $_SESSION['showShips'] ? display_sea(TRUE) : display_sea(); ?>
    <form method=post>
      x: <input name=x required>
      y: <input name=y required>
      <input type=submit value=Fire>
    </form>
    <form method=post>
      <input type=submit id=btn onclick='toggleButton()'
             value=<?php if ($_SESSION['showShips']) echo '"Hide ships"';
             else echo '"Show ships"'; ?>
             name=<?php if ($_SESSION['showShips']) echo 'hideShips';
      else echo 'showShips'; ?>>
    </form>
    <form method=post>
      <input type=submit value='New game' name=newGame>
    </form>
    <script>
      function toggleButton() {
        var b = document.getElementById('btn');
        if (b.value === 'Show ships') {
          b.value = 'Hide ships';
          b.name = hideShips;
        }
        else {
          b.value = 'Show ships';
          b.name = showShips;
        }
      }
    </script>
  </body>
</html>
9.4.1.2. Evolved object oriented solution

Ralph Hermes has developed an improved solution that beautifully illustrates the application of object orientation in PHP. students.btsi.lu/evegi144/WAD/WMOTUSub/Battleship_HerRa036.php

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
<?php

  /*
   *  Title:  PHP Battleship
   *  Author: HERMES Ralph
   *  Date:   18.12.2013
   */

  // Initialize Local Variables
  $BattleTable = new Table();
  $isVisible = false;
  $score = 0;
  $shots = 0;

  session_start();
  if (!isset($_SESSION["PlaygroundSession"])) {
    $Ships  = array(); // Create Array to store the Battleships
    $Deaths = array(); // Create Array to store Dead Ships
    $Water  = array(); // Create Array to store
    for ($i = 0; $i < 10; $i++) {
      $Ships[$i] = new Ship(); // Create Ship
      $Ships[$i]->generate(); // Generate Ship Parameters
      $Ships[$i]->cc(0, 0); // Check Collision for the first time
    }
    $_SESSION["PlaygroundSession"]      = $Ships; // Store ships in session
    $_SESSION["PlaygroundDeaths"]       = $Deaths; // Store deaths in session
    $_SESSION["PlaygroundWater"]        = $Water; // Store water shots in session
    $_SESSION["PlaygroundShipsVisible"] = false; // Store 'Visible Ships' in session

    $_SESSION["Score"] = 0; // Reset score
    $_SESSION["Shots"] = 0; // Reset shots
  }
  else {
    $Ships     = $_SESSION["PlaygroundSession"]; // Update ship array in session
    $Deaths    = $_SESSION["PlaygroundDeaths"]; // Update deaths array in session
    $Water     = $_SESSION["PlaygroundWater"]; // Update water array in session
    $isVisible = $_SESSION["PlaygroundShipsVisible"]; // Update 'Visible Ships' in session

    $score = $_SESSION["Score"]; // Update score in session
    $shots = $_SESSION["Shots"]; // Update shots in session
  }
?>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>BattleShip</title>
    <style>
      .playground {
        float:            left;
        background-color: #007eff;
      }

      .playground td {
        width:            25px;
        height:           25px;
        background-color: #000097;
        transition:       all 0.2s;
      }

      .playground td:hover {
        background-color: #0000d6;
      }

      .playground .title {
        background-color: #20435c;
        text-align:       center;
      }

      .playground .shipfield {
        background-color: #FFF0F0;
      }

      .playground .shipfield:hover {
        background-color: #b3b3b3;
      }

      .playground .shipinput {
        opacity: 0;
        width:   25px;
      }

      .playground .waterinput {
        opacity: 0;
        width:   25px;
      }

      #UserControl {
        float: left;
      }

      #UserControl input {
        width: 120px;
      }

      .playground .destroyed {
        background-color: #FF0000;
      }

      .playground .destroyed:hover {
        background-color: #FF0000;
      }

      .playground .waterfieldhit {
        background-color: #20435c;
      }

      .playground .waterfieldhit:hover {
        background-color: #20435c;
      }

    </style>
  </head>

  <body>
    <div id="wrapper">
      <?php
        $BattleTable->start(); // Start HTML table (Playground)
        $BattleTable->drawTemplate(); // Draw table
        $BattleTable->end(); // Finish drawing
      ?>
      <div id="UserControl">
        <form method="post">
          <input type="submit" name="newgame" value="New Game"><br>
          <input type="submit" name="showships" value=<?php if ($isVisible)
            echo "'Hide Ships'";
          else echo "'Show Ships'"; ?>>
        </form>
        <label class="stats">Score: <?php echo $score ?></label><br>
        <label class="stats">Shots: <?php echo $shots ?></label><br>
      </div>
    </div>
  </body>
</html>
<?php
  class Table {
    function start() {
      echo "<table class='playground'>";
    }

    function drawTemplate() {
      // Use global Variables
      global $isVisible;
      global $Deaths;
      global $Water;

      // Create Charlists
      $charlistA = array("", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L",
        "M", "N", "O", "P", "Q", "R", "S", "T");
      $charlist1 = array("", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11",
        "12", "13", "14", "15", "16", "17", "18", "19", "20");
      for ($y = 0; $y < 21; $y++) {
        echo "<tr>";
        for ($x = 0; $x < 21; $x++) {
          if ($x != 0) {
            if ($y == 0) {
              echo "<td class='title'>" . $charlist1[$x] . "</td>
                            ";
            }
            else {
              if (!FrameCheck($x, $y)) {
                $isWater = false;
                for ($i = 0; $i < count($Water); $i++) {
                  if ($x == $Water[$i][0] and $y == $Water[$i][1]) {
                    $isWater = true;
                  }
                }
                if ($isWater) {
                  echo "
                                            <td class='waterfieldhit'></td>";
                }
                else {
                  // Draw Water Fields
                  echo "
                                            <td class='waterfield'>
                                                <form method='post'>
                                                    <input type='hidden' name='parameterX'
                                                    value='" . $x . "'>
                                                    <input type='hidden' name='parameterY'
                                                    value='" . $y . "'>
                                                    <input class='waterinput' type='submit'
                                                    name='watershot'>
                                                </form>
                                            </td>";
                }

              }
              else {
                $destroyed = false;
                for ($i = 0; $i < count($Deaths); $i++) {
                  if ($x == $Deaths[$i][0] and $y == $Deaths[$i][1]) {
                    $destroyed = true;
                  }
                }
                if ($destroyed) {
                  echo "<td class='destroyed'></td>";
                }
                else {
                  // Place a Ship Button at Position: $x ,$y
                  // Hidden Fields are used to post coordinates
                  // Form submits when a Field was triggered
                  if ($isVisible) {
                    echo "
                                            <td class='shipfield'>
                                                <form method='post'>
                                                    <input type='hidden' name='parameterX'
                                                    value='" . $x . "'>
                                                    <input type='hidden' name='parameterY'
                                                    value='" . $y . "'>
                                                    <input class='shipinput' type='submit'
                                                    name='shipshot'>
                                                </form>
                                            </td>";
                  }
                  else {
                    echo "
                                            <td class='waterfield'>
                                                <form method='post'>
                                                    <input type='hidden' name='parameterX'
                                                    value='" . $x . "'>
                                                    <input type='hidden' name='parameterY'
                                                    value='" . $y . "'>
                                                    <input class='shipinput' type='submit'
                                                    name='shipshot'>
                                                </form>
                                            </td>";
                  }

                }
              }
            }
          }
          else {
            echo "<td class='title'>" . $charlistA[$y] . "</td>
                        ";
          }
        }
        echo "</tr>";
      }
    }

    function end() {
      echo "</table>";
    }
  }

  class Ship {
    public $coor = array(0, 0);
    private $size = 0;
    private $rota = 0;

    function generate() {
      global $Ships;

      $randomSize = rand(2, 5); // Generate Ship Length
      $randomRota = rand(0, 1); // Generate Ship Rotation

      $randomX = rand(1, 20); // Randomize Coordinates
      $randomY = rand(1, 20);

      // Check if random data is valid
      for ($i = 0; $i < $randomSize; $i++) {
        if ($randomRota == 0) {
          for ($j = 0; $j < count($Ships); $j++) {
            if ($Ships[$j]->coor[0] == $randomX + $i + 1) {
              $randomX = rand(1, 20);
              $i       = 0;
              $j       = 0;
            }
          }
        }
        else if ($randomRota == 1) {
          for ($j = 0; $j < count($Ships); $j++) {
            if ($Ships[$j]->coor[1] == $randomY + $i + 1) {
              $randomY = rand(1, 20);
              $i       = 0;
              $j       = 0;
            }
          }
        }
      }

      $this->coor[0] = $randomX;
      $this->coor[1] = $randomY;

      $this->size = $randomSize;
      $this->rota = $randomRota;

    }

    // Collision Checking
    function cc($x, $y) {
      for ($i = 0; $i < $this->size; $i++) {
        if ($this->rota == 0) {
          if ($x == $this->coor[0] + $i) {
            if ($y == $this->coor[1]) {
              return true;
            }
          }
        }
        else if ($this->rota == 1) {
          if ($x == $this->coor[0]) {
            if ($y == $this->coor[1] + $i) {
              return true;
            }
          }
        }
      }
      return false;
    }
  }

  function FrameCheck($x, $y) {
    global $Ships;
    for ($i = 0; $i < 10; $i++) {
      if ($Ships[$i]->cc($x, $y)) {
        for ($j = 0; $j < 10; $j++) {
          return true;
        }
      }
    }
    return false;
  }

  if (isset($_POST['shipshot'])) {
    $x = $_POST["parameterX"];
    $y = $_POST["parameterY"];

    $Deaths[] = array($x, $y);
    $score++;
    $shots++;
    $_SESSION["PlaygroundDeaths"] = $Deaths;
    $_SESSION["Score"]            = $score;
    $_SESSION["Shots"]            = $shots;
    echo "<script>window.location.href = '" . $_SERVER['PHP_SELF'] . "'</script>";
  }

  if (isset($_POST['watershot'])) {
    $x = $_POST["parameterX"];
    $y = $_POST["parameterY"];

    $shots++;
    $_SESSION["Shots"] = $shots;

    $Water[]                     = array($x, $y);
    $_SESSION["PlaygroundWater"] = $Water;
    echo "<script>window.location.href = '" . $_SERVER['PHP_SELF'] . "'</script>";
  }

  if (isset($_POST['newgame'])) {
    $Ships  = null;
    $Deaths = null;
    $Water  = null;
    $Ships  = array();
    $Deaths = array();
    $Water  = array();
    $shots  = 0;
    $score  = 0;
    for ($i = 0; $i < 10; $i++) {
      $Ships[$i] = new Ship(); // Create Ship
      $Ships[$i]->generate(); // Generate Ship
      $Ships[$i]->cc(0, 0); // Check Collision
    }

    // Reset Session Variables
    $_SESSION["PlaygroundSession"] = $Ships;
    $_SESSION["PlaygroundDeaths"]  = $Deaths;
    $_SESSION["PlaygroundWater"]   = $Water;
    $_SESSION["Score"]             = $score;
    $_SESSION["Shots"]             = $shots;
    echo "<script>window.location.href = '" . $_SERVER['PHP_SELF'] . "'</script>";
  }

  if (isset($_POST['showships'])) {
    $isVisible                          = !$isVisible;
    $_SESSION["PlaygroundShipsVisible"] = $isVisible;
    echo "<script>window.location.href = '" . $_SERVER['PHP_SELF'] . "'</script>";
  }
?>

9.5. WMOTU Mailer

WMOTU has been asked to develop a web mailing app.

9.5.1. WMOTU Mailer v1

Develop a web app (students.btsi.lu/evegi144/WAD/WMOTUMailerv1) to send an email. Sender, recipient, subject and message are provided by the user. The email gets only submitted if all fields are filled in (no data validation).

9.5.1.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTUMailer v1</title>
    <meta charset=UTF-8>
    <style>
      form label {
        display:     inline-block;
        width:       225px;
        font-weight: bold;
      }
    </style>
  </head>
  <body>
    <form method=post>
      <label>Sender:</label>
      <input type=email name=sender required><br>
      <label>Recipient:</label>
      <input type=email name=recipient required><br>
      <label>Subject:</label>
      <input name=subject required><br>
      <label>Content:</label>
      <textarea name=content required></textarea><br>
      <label></label>
      <input type=submit name=send value=Send>
    </form>
    <?php
      if (isset($_POST['send']))
        if (mail($_POST['recipient'], $_POST['subject'], $_POST['content'], 'From:' .
          $_POST['sender']))
          echo '<script>alert("Email sent successfully!")</script>';
        else echo '<script>alert("Email could not be sent!")</script>';
    ?>
  </body>
</html>

We use an HTML form to submit the data. If no action attribute is specified, the current script gets called upon submission. Note that all input and textarea tags have the required attribute set. This forces the client browser to submit the form only if all fields have been filled in. We therefore do not need to check again on the server side that all fields have been filled in. We do it here nevertheless for illustrative purposes.

Also note how we can not only generate HTML and CSS, but also JavaScript on the server side.

9.5.2. WMOTU Mailer v1++

For aspiring WMOTUs: What is the problem with WMOTUMailer v1? Come up with a solution (students.btsi.lu/evegi144/WAD/WMOTUMailerv1++)!

9.5.2.1. Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang=en>
    <head>
        <title>WMOTUMailer v1++</title>
        <meta charset=UTF-8>
        <style>
            form label {
                display:     inline-block;
                width:       225px;
                font-weight: bold;
            }
        </style>
    </head>
    <body>
        <form method=post action=sendmail.php>
            <label>Sender:</label>
            <input type=email name=sender required><br>
            <label>Recipient:</label>
            <input type=email name=recipient required><br>
            <label>Subject:</label>
            <input name=subject required><br>
            <label>Content:</label>
            <textarea name=content required></textarea><br>
            <label></label>
            <input type=submit name=send value=Send>
        </form>
    </body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
  if (isset($_POST['send'])) {
    if (mail($_POST['recipient'], $_POST['subject'], $_POST['content'], 'From:' .
      $_POST['sender']))
      $s = '<script>alert("Email sent successfully!");';
    else $s = '<script>alert("Email could not be sent!");';
    echo $s . 'window.location = "https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/index.php"</script>';
  }
?>

The problem is the browser refresh function. If the user refreshes the page, another email with the same information than the previous one gets sent immediately. This is no good! The problem originates from our combining the client and server side processing in the same script. This means that the previous values entered by the user are still stored in the $_POST array and the sending of another email is triggered automatically. The improved version splits the client and server side processing into two scripts and triggers the send process only after the user has pressed the submit button, not after a page reload. Furthermore, the form is empty, given that the form page gets reloaded after the first email submission.

9.5.3. WMOTU Mailer v2

Enhance WMOTUMailer by storing all emails sent in a MySQL database and displaying the current number of emails sent.

9.5.3.1. Solution
9.5.3.1.1. createDB.sql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# createDB.sql -> create the database table for the WMOTU Mailer v2
DROP TABLE WMOTUMailerv2;

CREATE TABLE WMOTUMailerv2 (id        INT UNSIGNED AUTO_INCREMENT NOT NULL UNIQUE,
                            sender    TEXT                        NOT NULL,
                            recipient TEXT                        NOT NULL,
                            subject   TEXT                        NOT NULL,
                            message   TEXT                        NOT NULL,
  PRIMARY KEY (id))
  ENGINE = INNODB
  DEFAULT CHARSET = utf8;

Create the database as described in [DBCreation]. ====== database.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
  require_once 'db_credentials.php';

  function store_email($sender, $recipient, $subject, $message) {
    $dbc = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
    if ($dbc->connect_error) trigger_error('Database connection failed: ' .
      $dbc->connect_error, E_USER_ERROR);
    $dbc->set_charset("utf8");
    $message = strip_tags($message);
    $query   = "INSERT INTO " . DB_TABLE . " (sender, recipient, subject, message) " .
      "VALUES (?, ?, ?, ?)";
    $stmt    = $dbc->prepare($query);
    if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' . $dbc->error,
      E_USER_ERROR);
    $stmt->bind_param('ssss', $sender, $recipient, $subject, $message);
    $stmt->execute();
    $stmt->close();
  }

  function get_number_of_emails() {
    $dbc = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
    if ($dbc->connect_error) trigger_error('Database connection failed: ' .
      $dbc->connect_error, E_USER_ERROR);
    $dbc->set_charset("utf8");
    $query = "SELECT COUNT(*) FROM " . DB_TABLE;
    $res   = $dbc->query($query);
    if (!$res) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
      $dbc->error, E_USER_ERROR);
    return $res->fetch_row();
  }

?>
9.5.3.1.2. index.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html lang=en>
    <head>
        <title>WMOTUMailer v2</title>
        <meta charset=UTF-8>
        <style>
            form label {
                display:     inline-block;
                width:       225px;
                font-weight: bold;
            }
        </style>
    </head>
    <body>
        <form method=post action=sendmail.php>
            <label>Sender:</label>
            <input type=email name=sender required><br>
            <label>Recipient:</label>
            <input type=email name=recipient required><br>
            <label>Subject:</label>
            <input name=subject required><br>
            <label>Content:</label>
            <textarea name=content required></textarea><br>
            <label></label>
            <input type=submit name=send value=Send>
        </form>
      Number of emails sent so far:
      <?php
        require_once 'database.php';
        $num = get_number_of_emails();
        echo $num[0];
      ?>
    </body>
</html>
9.5.3.1.3. sendmail.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
  require_once 'database.php';
  if (isset($_POST['send'])) {
    if (mail($_POST['recipient'], $_POST['subject'], $_POST['content'], 'From:' .
      $_POST['sender'])) {
      store_email($_POST['sender'], $_POST['recipient'], $_POST['subject'],
        $_POST['content']);
      $s = '<script>alert("Email sent successfully!");';
    }
    else $s = '<script>alert("Email could not be sent!");';
    echo $s . 'window.location = "https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/index.php"</script>';
  }
?>
9.5.3.1.4. db_credentials.php

This file contains the credentials for accessing the DB. You need to replace these values with your own in order to access your DB.

1
2
3
4
5
6
7
<?php
  define('DB_HOST', 'p:localhost');
  define('DB_USER', '');
  define('DB_PASSWORD', '');
  define('DB_NAME', '');
  define('DB_TABLE', 'WMOTUMailerv2');
?>

9.5.4. WMOTU Mailer v3

Further enhance WMOTUMailer by adding sign up and login functionality. Only registered users can send email. The registration includes the user’s email address, which is filled in as the sender by default. The user name is displayed.

9.5.5. WMOTU Mailer v4

A user can get a listing of all their emails, sorted by date (the newest one at the top). He/she can read and delete each one. Our client also wants a good looking and user-friendly interface.

9.5.6. WMOTU Mailer v5

A user can upload an avatar that is displayed next to the user name. Before the email gets sent, an in-depth data validation is performed.

9.6. WMOTU Quack

Impressed by our performance on the mailer problem, our customer has tasked us with the development of a full fledged social network.

9.6.1. Security

Create a protected folder that is secured as explained in Security.

9.6.2. DB

Create a SQL script named createDB.sql that creates the table named WMOTUQuack_users to store the following fields:

  1. id: primary key, positive, automatically incremented

  2. first_name: maximum 40 characters.

  3. last_name: maximum 40 characters.

  4. email_address: maximum 255 characters.

  5. user_name: maximum 32 characters.

  6. password: maximum 40 characters.

  7. activated: indicates whether a user is activated, i.e. has clicked on the validation link.

  8. description: long text.

  9. last_time_seen: the current time stamp (see dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html). The data type to be used is TIMESTAMP, the default value is CURRENT_TIMESTAMP.

If the table already exists it is first deleted and then created. The DB engine is set to INNODB. The user name needs to be unique. Only the description can be empty.

Add an instruction to insert test data into the table. Execute the script (remember that this is very easy to do with PhpStorm (cf. Via PhpStorm) and verify that the database has been created correctly. Where do you save your script? Why?

9.6.3. db_credentials.php

Create the db_credentials.php file (cf. db_credentials.php). In this file we call the static set_credentials method in the Database class, which we’ll create next.

9.6.4. database.php`

Create class Database in the file database.php. For efficiency reasons all properties and methods of this class are static, which means that we do not need to create an object of this class. Study Classes and objects and database.php.

9.6.4.1. Constants

Define the constants $DB_HOST, $DB_USER, $DB_PASSWORD, $DB_NAME, $DB_USER_TABLE and $DB_LOGGEDIN_USER_TABLE. These constants are all private to the class. Assign the correct values to $DB_USER_TABLE and $DB_LOGGEDIN_USER_TABLE. The other constants will get their values from the method set_credentials that we’ll create in the next step.

9.6.4.2. set_credentials

Create the method set_credentials, which sets the values of the $DB_HOST, $DB_USER, $DB_PASSWORD and $DB_NAME constants (cf. database.php).

9.6.4.3. connect

Create the method connect, which creates a connection to the DB and returns the connection handle. We set the character set of the connection to utf8.

9.6.4.4. get_user_id

Create the method get_user_id, which returns the user id for a given user name, or FALSE if such a user does not exist.

9.6.4.5. get_user_name

Create the method get_user_name, which returns the user name for a given user id, or FALSE if such an id does not exist.

9.6.4.6. get_user_data

Create the method get_user_data, which returns the user name and description for a given user id as an associative array, or FALSE if such an id does not exist.

9.6.4.7. get_users

Create the method get_users, which returns an associate array containing all data for all users or FALSE if no users exist.

9.6.4.8. get_description

Create the method get_description, which returns the description for a given user id, or FALSE if such an id does not exist.

9.6.4.9. update_description

Create the method update_description, which updates the description for a given user id. The method returns FALSE if something went wrong, for instance if such an id does not exist, otherwise TRUE.

9.6.4.10. is_logged_in

Create the method is_logged_in, which returns TRUE if the user with a given user id has a time stamp that is not older than 2 seconds. Use the UNIX_TIMESTAMP function (cf. http://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions .html#function_unix-timestamp) to convert last_time_seen and then compare it to the current time in PHP using the time function (cf. www.php.net/manual/en/function.time.php). If the time stamp is older than 2 seconds, the method returns FALSE. In the main script, we’ll install a timer that updates the user’s time stamp every 2 seconds. This will allow us to monitor in near real time who is logged in and who is not.

9.6.4.11. update_login_timestamp

Create the method update_login_timestamp, which sets last_time_seen to the current time for the user with a given id. Use the MySQL function NOW (cf. dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_now). The method returns TRUE if the update succeeded, otherwise FALSE.

9.6.4.12. register_login

Create the method register_login, which first checks whether a user with a given id is already logged in, in which case FALSE is returned. Otherwise, the method updates the user’s time stamp and returns the result of this update.

9.6.4.13. login

Create the method login, which registers the login and returns the user id if a user with a given name and password exists in the user table and is activated, otherwise FALSE.

9.6.4.14. activate_user

Create the method activate_user, which activates the user with a given id and returns the result of the operation.

9.6.4.15. create_user

Create the method create_user, which creates a user with the given first name, last name, email, user name and password. Leading and trailing spaces are removed from all parameters except the password. The password is stored in encrypted form using the SHA1 function of MySQL (cf. dev.mysql.com/doc/refman/5.7/en/encryption-functions.html#function_sha1). The method returns the user id or FALSE.

9.6.4.16. delete_user

Create the method delete_user, which deletes the user with the given id. The method returns TRUE if the operation succeeded, otherwise FALSE.

9.6.5. index.php

Develop the login and sign up page. You can use the HTML and CSS from the original. The following features need to be implemented:

  1. Session IDs may only be passed via cookies, not appended to URLs.

  2. The path for the cookie is set to the current directory in order to prevent it from being available to scripts in other directories.

  3. In this particular case, we do NOT use SSL, as the SSL certificate on Foxi is currently not accessible from PHP, which prevents the WebSocket server from being able to use SSL.

  4. If no session is currently started, we start one.

  5. After 30 seconds we’ll generate a new session ID to prevent a session fixation attack (cf. PHP Cookbook p. 338).

  6. If a user is already logged in, we let him through to main.php.

  7. Else, if the user has submitted his login details, we need to check them. If the check fails, we go to index.php. If a user with these credentials exists, we set the session data and go to the main page.

  8. Else, if the user has submitted the sign up form, we need to make sure that a password has been entered. We also have to check the CAPTCHA and the user name. If the user name already exists, we create an alert informing the user and stop the script. Otherwise, we validate the email address (cf. Email validation). If the email address is invalid, we go to index.php. Otherwise, we create the new user and send a validation email. The subject is Validation email, the sender is do_not_reply@ltam.lu and the body contains the text To validate this email address click the following link: wsers.foxi.lu/Tutorial/WMOTUQuack/index.php?s= followed by the email address followed by &m= followed by the hash value followed by &i= followed by the user id multiplied by 5. The hash value is the string T2IF2 followed by the user’s email address. This whole string is then encrypted using the password_hash function of PHP. After sending the email, we display an alert with the text A validation email has been sent. Please check your inbox. and stop the script.

  9. Else, if a user has clicked on the link in the validation email that was sent during sign up, we check whether the email address corresponds to the hash value. If that’s the case, we activate the user. We retrieve the user name. If it exists, we create a directory protected/users/ followed by the user name. Then we set the directory permissions so that the owner and group have full rights and others have none. We then go to index.php.

  10. Else, we generate the CAPTCHA. We use the font Jumpman (cf. www.dafont.com/jumpman.font). The CAPTCHA generation is based on pages 613-614 of "Head First PHP & MySQL". It has however been adapted in order to display a simple calculation that the user has to perform to prove his or her humanness. For this purpose, we generate two random numbers $num1 and $num2, each between 0 and 9. We then create an array $ops with the following operators: +, - and * stored as characters. Now we choose a random number $op between 0 and 2. The pass phrase will then be $num1 . $ops[$op] . $num2. The result of the calculation needs to be stored in the session, which can be done like this:

    $_SESSION['pass_phrase'] = eval("return intval($pass_phrase);");

    The approach chosen in the book, to send the image via a header, did not work for me, so I used the imagepng function to save the image on disk from where the HTML document retrieves it in the img tag.

9.6.6. main.php

You can use the HTML, CSS and JavaScript from the original. The following features need to be implemented:

  1. The bouncer needs to be included to prevent unauthorized usage of our app.

  2. We need our DB class.

  3. Behind the Logout text we display the user name of the logged in user.

  4. We need to store the user id of the currently logged in user in a JavaScript variable

  5. userId and the user name in a variable named userName. These two variables are used by websocket.js.

  6. In the profile section we display the description of the user. Be careful not to display inexisting spaces!

9.6.7. logout.php

The script performs the following steps:

  1. If no session is started we start one.

  2. We delete the session array.

  3. If a session ID exists we expire the session cookie.

  4. We destroy the session.

  5. we send the browser to the login page.

9.6.8. updatedescription.php

The script is executed when the user clicks the Update description link and performs the following steps:

  1. We include the bouncer.

  2. We need the DB class.

  3. We update the description with the user id stored in the session and the description submitted by the updateDescription function in the main object in main.js.

9.6.9. deleteprofile.php

The script is executed when the user clicks the Delete profile link and performs the following steps:

  1. We include the bouncer.

  2. We need the DB class.

  3. We delete the user from the DB.

  4. We delete the user directory with all sub directories using the following code:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    $dir_path = "./protected/users/{$_SESSION['user_name']}";
    #http://stackoverflow.com/questions/1407338/a-recursive-remove-directory-function-for-php
    try {
      foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_path,
        FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path) {
        $path->isFile() ? unlink($path->getPathname()) : rmdir($path->getPathname());
      }
      rmdir($dir_path);
    } catch (Exception $e) {
    }
    
  5. We send the browser to the logout script.

9.6.10. deleteprofileimage.php

The script is executed when the user clicks the Delete image link and performs the following steps:

  1. We include the bouncer.

  2. We delete the file using the following code:

    1
    2
    3
    4
    5
    6
    7
    8
    
    $file_path = '';
    $dir_path = "./protected/users/{$_SESSION['user_name']}";
    try { # Run through all files in the directory. This primitive approach is good for now.
      foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_path,
        FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path)
        if ($path->isFile()) $file_path = $path->getPathname();
    } catch (Exception $e) {}
    if ($file_path !== '') unlink($file_path);
    

9.6.11. getprofileimage.php

The script is executed when the user clicks the Delete image link and when the Members button is activated. It performs the following steps:

  1. We include the bouncer.

  2. We declare a variable $file_path and initialize it with an empty string.

  3. If the user name has been transmitted via GET we set $dir_path to /protected/users/{$_GET['user_name']}, else we set $dir_path to /protected/users/{$_SESSION['user_name']}.

  4. We run through all the files in $dir_path and save the path of the last one in $file_path.

  5. We tell the browser that we will now be sending a png image.

  6. If the file path is not empty we read the file.

9.6.12. getmember.php

The script is executed when the Profile button is activated. It performs the following steps:

  1. We include the bouncer.

  2. We need the DB class.

  3. We declare the variable $output and initialize it with an empty string.

  4. If the user id has been submitted via POST, we get the user data. If this data exists, we

    1. build a string with a paragraph containing the user name,

    2. followed by an image with the profile image,

    3. followed by

      draggable=false onmousedown="event.preventDefault(); alt="",
    4. followed by a new line,

    5. followed by a new paragraph with as preformatted content the user’s description.

  5. We send the string to the browser.

9.6.13. Solution

WMOTU has been asked to develop a full-fledged communication platform. students.btsi.lu/evegi144/WAD/WMOTUQuack

9.6.13.1. DB
9.6.13.1.1. Structure
WMOTUQuackDB
9.6.13.1.2. createDB.sql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# createDB.sql -> create the database tables for WMOTUQuack
/*DROP TABLE IF EXISTS WMOTUQuack_loggedin_users;*/
DROP TABLE IF EXISTS WMOTUQuack_messages;
DROP TABLE IF EXISTS WMOTUQuack_users;

CREATE TABLE WMOTUQuack_users (id             INT UNSIGNED AUTO_INCREMENT NOT NULL UNIQUE,
                               first_name     VARCHAR(40)                 NOT NULL,
                               last_name      VARCHAR(40)                 NOT NULL,
                               email_address  VARCHAR(255)                NOT NULL,
                               user_name      VARCHAR(32)                 NOT NULL UNIQUE,
                               password       VARCHAR(40)                 NOT NULL,
                               activated      BOOL                        NOT NULL,
                               description    TEXT,
                               last_time_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP/* ON
                                                            UPDATE CURRENT_TIMESTAMP*/,
  PRIMARY KEY (id))
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;

/*CREATE TABLE WMOTUQuack_loggedin_users (user_id  INT UNSIGNED NOT NULL UNIQUE,
                                    last_time_seen TIMESTAMP DEFAULT
                                    CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
  FOREIGN KEY (user_id) REFERENCES WMOTUQuack_users (id)
    ON DELETE CASCADE
    ON UPDATE CASCADE)
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;*/

INSERT INTO WMOTUQuack_users (first_name, last_name, email_address, user_name, password,
                              activated)
VALUES ("Dum1", "Dum", "dummy1@dumdum.com", "dummy1", SHA("d1pw"), TRUE),
  ("Dum2", "Dum", "dummy2@dumdum.com", "dummy2", SHA("d2pw"), TRUE);
INSERT INTO WMOTUQuack_messages (user_id, message) VALUES
  ((SELECT
      id
    FROM WMOTUQuack_users
    WHERE user_name = "dummy1"), "Test message 1"),
  ((SELECT
      id
    FROM WMOTUQuack_users
    WHERE user_name = "dummy2"), "Test message 2");

CREATE TABLE WMOTUQuack_messages (id         INT UNSIGNED AUTO_INCREMENT NOT NULL UNIQUE,
                                  user_id    INT UNSIGNED                NOT NULL,
                                  message    TEXT                        NOT NULL,
                                  time_stamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE
                                  CURRENT_TIMESTAMP                      NOT NULL,
  PRIMARY KEY (id),
  FOREIGN KEY (user_id) REFERENCES WMOTUQuack_users (id)
    ON DELETE CASCADE
    ON UPDATE CASCADE)
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;
9.6.13.2. database.php
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
<?php
  require_once 'db_credentials.php';

  class Database {
    private static $DB_HOST;
    private static $DB_USER;
    private static $DB_PASSWORD;
    private static $DB_NAME;
    private static $DB_USER_TABLE = 'WMOTUQuack_users';
    #private static $DB_LOGGEDIN_USER_TABLE = 'WMOTUQuack_loggedin_users';
    private static $DB_MESSAGE_TABLE = 'WMOTUQuack_messages';

    static function set_credentials($db_host, $db_user, $db_password, $db_name) {
      self::$DB_HOST     = $db_host;
      self::$DB_USER     = $db_user;
      self::$DB_PASSWORD = $db_password;
      self::$DB_NAME     = $db_name;
    }

    static private function connect() {
      $dbc = new mysqli(self::$DB_HOST, self::$DB_USER, self::$DB_PASSWORD,
        self::$DB_NAME);
      if ($dbc->connect_error) trigger_error('Database connection failed: ' .
        $dbc->connect_error, E_USER_ERROR);
      $dbc->set_charset("utf8");
      return $dbc;
    }

    # Returns a string.
    /*static private function sanitize_string($dbc, $string) {
      $string = strip_tags(trim($string));
      if (get_magic_quotes_gpc()) $string = stripslashes($string);
      $result = mysqli_real_escape_string($dbc, $string);
      return $result;
    }*/

    # Returns id if a user with the given name exists, otherwise FALSE.
    static function get_user_id($user_name) {
      $dbc = self::connect();
      # Look up user id.
      $query = 'SELECT id FROM ' . self::$DB_USER_TABLE . ' WHERE user_name = ?';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('s', $user_name);
      $stmt->execute();
      $stmt->store_result();
      $result = $stmt->num_rows === 1;
      if ($result) {
        $stmt->bind_result($user_id);
        $result = $stmt->fetch();
      }
      $dbc->close();
      if ($result) return $user_id;
      else return $result;
    }

    # Returns user name for given id if id exists, otherwise FALSE.
    static function get_user_name($user_id) {
      $dbc   = self::connect();
      $query = 'SELECT user_name FROM ' . self::$DB_USER_TABLE . ' WHERE id = ?';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('s', $user_id);
      $stmt->execute();
      $stmt->bind_result($user_name);
      $result = $stmt->fetch();
      $dbc->close();
      if ($result) return $user_name;
      else return $result;
    }

    # Returns user name and description for given id if id exists, otherwise FALSE.
    static function get_user_data($user_id) {
      $dbc   = self::connect();
      $query = 'SELECT user_name, description FROM ' . self::$DB_USER_TABLE .
        ' WHERE id = ?';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('s', $user_id);
      $stmt->execute();
      $stmt->bind_result($user_name, $description);
      $result = $stmt->fetch();
      $dbc->close();
      if ($result) return array('user_name'   => $user_name,
                                'description' => $description);
      else return $result;
    }

    # Returns associative array or FALSE.
    static function get_users() {
      $dbc    = self::connect();
      $result = FALSE;
      $query  = 'SELECT * FROM ' . self::$DB_USER_TABLE;
      $res    = $dbc->query($query);
      if (!$res) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      while ($dat = $res->fetch_array(MYSQLI_ASSOC)) $result[] = $dat;
      $dbc->close();
      return $result;
    }

    # Returns associative array or FALSE.
    /*static function get_logged_in_user_ids() {
      $dbc    = self::connect();
      $result = FALSE;
      $query  = 'SELECT id FROM ' . self::$DB_USER_TABLE;
      $res    = $dbc->query($query);
      if (!$res) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      while ($dat = $res->fetch_array(MYSQLI_ASSOC)) $result[] = $dat;
      return $result;
    }*/

    # Returns description for given id if id exists, otherwise FALSE.
    static function get_description($user_id) {
      $dbc   = self::connect();
      $query = 'SELECT description FROM ' . self::$DB_USER_TABLE . ' WHERE id = ?';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('s', $user_id);
      $stmt->execute();
      $stmt->bind_result($description);
      $result = $stmt->fetch();
      $dbc->close();
      if ($result) return $description;
      else return $result;
    }

    # Returns TRUE if description update succeeded, otherwise FALSE.
    static function update_description($user_id, $description) {
      $dbc   = self::connect();
      $query = 'UPDATE ' . self::$DB_USER_TABLE . ' SET description = ? WHERE id = ?';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('ss', $description, $user_id);
      $result = $stmt->execute();
      $dbc->close();
      return $result;
    }

    # Returns FALSE if user could not be created, otherwise user id.
    static function create_user($first_name, $last_name, $email, $user_name, $password) {
      $dbc   = self::connect();
      $query = 'INSERT INTO ' . self::$DB_USER_TABLE .
        ' (first_name, last_name, email_address, user_name, password, activated)' .
        ' VALUES (?, ?, ?, ?, SHA1(?), FALSE)';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $first_name = trim($first_name);
      $last_name  = trim($last_name);
      $email      = trim($email);
      $user_name  = trim($user_name);
      $stmt->bind_param('sssss', $first_name, $last_name, $email, $user_name,
        $password);
      if ($stmt->execute()) $result = self::get_user_id($user_name);
      else $result = FALSE;
      $dbc->close();
      return $result;
    }

    # Returns FALSE if user could not be deleted, otherwise TRUE.
    static function delete_user($user_id) {
      $dbc   = self::connect();
      $query = 'DELETE FROM ' . self::$DB_USER_TABLE . ' WHERE id = ?';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('s', $user_id);
      $result = $stmt->execute();
      $dbc->close();
      return $result;
    }

    # Returns user_id of the user or FALSE.
    static function login($user_name, $password) {
      $dbc   = self::connect();
      $query = 'SELECT id FROM ' . self::$DB_USER_TABLE .
        ' WHERE user_name = ? AND password = SHA(?) AND activated = TRUE';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('ss', $user_name, $password);
      $stmt->execute();
      $stmt->store_result();
      if ($stmt->num_rows === 1) {
        $stmt->bind_result($result);
        $stmt->fetch();
      }
      else $result = FALSE;
      $dbc->close();
      if ($result && self::register_login($result)) return $result;
      else return FALSE;
    }

    # Called by login. Returns FALSE if user is already logged in.
    private static function register_login($user_id) {
      if (self::is_logged_in($user_id)) return FALSE;
      return self::update_login_timestamp($user_id);
    }

    # Returns TRUE if description update succeeded, otherwise FALSE.
    static function update_login_timestamp($user_id) {
      $dbc   = self::connect();
      $query = 'UPDATE ' . self::$DB_USER_TABLE . ' SET last_time_seen = ' .
        'NOW() WHERE id = ?';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('s', $user_id);
      $result = $stmt->execute();
      $dbc->close();
      return $result;
    }

    /*static function logout($user_id) {
      $dbc   = self::connect();
      $query = 'DELETE FROM ' . self::$DB_LOGGEDIN_USER_TABLE .
        " WHERE user_id = ?";
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('s', $user_id);
      $result = $stmt->execute();
      $stmt->close();
      return $result;
    }*/

    # Returns TRUE if user was seen within the last 2 seconds, otherwise FALSE.
    static function is_logged_in($user_id) {
      $dbc   = self::connect();
      $query = 'SELECT UNIX_TIMESTAMP(last_time_seen) FROM ' .
        self::$DB_USER_TABLE . ' WHERE id=?';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('s', $user_id);
      $stmt->execute();
      $stmt->store_result();
      if ($stmt->num_rows !== 1) return FALSE;
      $stmt->bind_result($timestamp);
      $result = $stmt->fetch();
      $dbc->close();
      if ((time() - $timestamp) <= 2) return TRUE;
      return FALSE;
    }

    # Search by user id.
    # Returns associative array or FALSE.
    static function get_messages($user_id = FALSE) {
      $dbc    = self::connect();
      $result = FALSE;
      $query  = 'SELECT * FROM ' . self::$DB_MESSAGE_TABLE . " WHERE user_id=$user_id";
      $res    = $dbc->query($query);
      if (!$res) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      while ($dat = $res->fetch_array(MYSQLI_ASSOC)) $result[] = $dat;
      $dbc->close();
      return $result;
    }

    # Inserts message for user id stored in session.
    static function insert_message($message) {
      $dbc   = self::connect();
      $query = 'INSERT INTO ' . self::$DB_MESSAGE_TABLE . ' (user_id,
      message) VALUES (?, ?)';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' . $dbc->error,
        E_USER_ERROR);
      $stmt->bind_param('ss', $_SESSION['user_id'], $message);
      $result = $stmt->execute();
      $dbc->close();
      return $result;
    }

    # Returns TRUE if deletion succeeded, otherwise FALSE.
    static function delete_message($id) {
      $dbc    = self::connect();
      $result = mysqli_query($dbc, 'DELETE FROM ' . self::$DB_MESSAGE_TABLE .
        " WHERE id=$id");
      mysqli_close($dbc);
      return $result;
    }

    # Activate user with given id.
    static function activate_user($user_id) {
      $dbc   = self::connect();
      $query = 'UPDATE ' . self::$DB_USER_TABLE . ' SET activated = TRUE WHERE id = ?';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' . $dbc->error,
        E_USER_ERROR);
      $stmt->bind_param('s', $user_id);
      $result = $stmt->execute();
      $dbc->close();
      return $result;
    }
  }

?>
9.6.13.3. index.php
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
<?php
  // Only send session id cookie over SSL.
  //ini_set('session.cookie_secure', true);
  // Session IDs may only be passed via cookies, not appended to URL.
  ini_set('session.use_only_cookies', true);
  // Set the path for the cookie to the current directory in order to prevent it from
  // being available to scripts in other directories.
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  /*if ($_SERVER['SERVER_PORT'] != 443)
    header('Location: https://' . $_SERVER['SERVER_NAME'] . $_SERVER['SCRIPT_NAME']);*/
  /*if (!isset($_SERVER['HTTPS'])) // If SSL is not active, activate it.
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
      dirname(htmlspecialchars($_SERVER['PHP_SELF'])));*/
  // If no session is started yet, we'll start one.
  if (!isset($_SESSION)) session_start();
  // After 30 seconds we'll generate a new session ID to prevent a session fixation
  // attack (cf. PHP cookbook p. 338).
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  // Include the database class needed to access the database.
  require_once 'protected/database.php';
  $file_name = 'pp.png'; # CAPTCHA image
  // If a user is already logged in, let him through to the main page.
  if (isset($_SESSION['user_id']))
    header('Location: ' . 'http://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/main.php');
  // Else, if the user has submitted his login details, we need to check them.
  elseif (isset($_POST['logIn'])) {
    if (isset($_POST['username']) && isset($_POST['password1'])) {
      $result = Database::login($_POST['username'], $_POST['password1']);
      // If a user with this login exists, we load the main page.
      if ($result) {
        $_SESSION['user_id'] = $result;
        $_SESSION['user_name'] = $_POST['username'];
        header('Location: ' . 'http://' . $_SERVER['HTTP_HOST'] .
          dirname($_SERVER['PHP_SELF']) . '/main.php');
      }
    }
  } elseif (isset($_GET['m'])) { # Activate user
    if (md5('T2IF2' . $_GET['s']) === $_GET['m']) {
      if (Database::activate_user(intval($_GET['i'] / 5))) {
        $user_name = Database::get_user_name(intval($_GET['i'] / 5));
        if ($user_name) {
          if (!mkdir('protected/users/' . $user_name, 0770))
            die("User dir creation failed!");
          chmod('protected/users/' . $user_name, 0770);
        }
      }
      header('Location: ' . 'http://' . $_SERVER['HTTP_HOST'] .
        dirname($_SERVER['PHP_SELF']) . '/index.php');
    }
  }
  // Else, if the user has signed up for a new account, we need to check if
  // such a user already exists.
  elseif (isset($_POST['signUp'])) {
    if (isset($_SESSION['pass_phrase']) && isset($_POST['captcha']) &&
      $_SESSION['pass_phrase'] === intval($_POST['captcha'])
    ) {
      if (isset($_POST['username']) && isset($_POST['password1'])) {
        if (Database::get_user_id($_POST['username'])) {
          echo "<script>window.alert('User exists already!');</script>";
          exit;
        } else { // If not, we'll send an opt-in email
          require_once "protected/rfc822.php";
          if (is_valid_email_address($_POST['email']) &&
            checkdnsrr(preg_replace('/^[a-zA-Z0-9][a-zA-Z0-9\._\-&!?=#]*@/ ',
              '', $_POST['email']))
          ) {
            if ($result = Database::create_user
            ($_POST['firstname'], $_POST['lastname'], $_POST['email'],
              $_POST['username'], $_POST['password1'])
            ) {
              $hash = md5('T2IF2' . $_POST['email']);
              $mail_body = 'To validate this email address click the ' .
                'following link: '
                . 'http://wsers.foxi.lu/WAD/WMOTUQuack/index.php?s=' .
                $_POST['email'] . '&m=' . $hash . '&i=' . (5 * $result);
              if (mail($_POST['email'], 'Validation email', $mail_body,
                'From: webmaster@example.com'))
                echo "<script>window.alert('A validation email has been sent. " .
                  "Please check your inbox.');</script>";
              else
                echo "<script>window.alert('A validation email could not be" .
                                    " sent!');</script>";
              exit;
            }
          } else header('Location: ' . 'http://' . $_SERVER['HTTP_HOST'] .
            dirname($_SERVER['PHP_SELF']) . '/index.php');
        }
      }
    } else header('Location: ' . 'http://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/index.php');
  } else {
    define('CAPTCHA_WIDTH', 40);
    define('CAPTCHA_HEIGHT', 22);
    $num1 = rand(0, 9);
    $num2 = rand(0, 9);
    $ops = array('+', '-', '*');
    $op = rand(0, 2);
    $pass_phrase = $num1 . $ops[$op] . $num2;
    $_SESSION['pass_phrase'] = eval("return intval($pass_phrase);");
    $img = imagecreatetruecolor(CAPTCHA_WIDTH, CAPTCHA_HEIGHT);
    imagefilledrectangle($img, 0, 0, CAPTCHA_WIDTH, CAPTCHA_HEIGHT,
      imagecolorallocate($img, 202, 168, 13));
    for ($i = 0; $i < 5; $i++)
      imageline($img, 0, rand() % CAPTCHA_HEIGHT, CAPTCHA_WIDTH,
        rand() % CAPTCHA_HEIGHT, imagecolorallocate($img, 128, 128,
          64));
    for ($i = 0; $i < 50; $i++) imagesetpixel($img, rand() % CAPTCHA_WIDTH,
      rand() % CAPTCHA_HEIGHT, imagecolorallocate($img, 64, 64, 64));
    imagettftext($img, 18, 0, 5, CAPTCHA_HEIGHT - 5,
      imagecolorallocate($img, 0, 0, 0), './Jumpman.ttf',
      $pass_phrase);
    unlink($file_name);
    imagepng($img, $file_name);
  }
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Quack</title>
    <meta charset=UTF-8>
    <link href=index.css rel=stylesheet>
    <script src=index.js></script>
  </head>
  <body>
    <header>
      <img id=logo src=logo.png alt=Logo width=64 height=64>
    </header>
    <main>
      <form method=post id=logInForm>
        <label>User name:</label>
        <input type=text name=username required autofocus>
        <label>Password:</label>
        <input type=password name=password1 required>
        <input type=submit name=logIn value='Log in'><br>
        <input type=button onclick='index.signUp();' value='Sign up'>
      </form>
      <form method=post id=signUpForm onSubmit='return index.validateSignUpForm();'>
        <label>First name:</label>
        <input type=text name=firstname required>
        <label>Last name:</label>
        <input type=text name=lastname required>
        <label>Email:</label>
        <input type=email name=email required>
        <label>Username:</label>
        <input type=text name=username id=username required
               onBlur=index.checkUser(this);>
        <span id=info></span>
        <label>Password:</label>
        <input type=password id=pw1 name=password1 required>
        <label>Retype pw:</label>
        <input type=password id=pw2 name=password2 required>
        <label>CAPTCHA:</label>
        <img src='<?php echo $file_name; ?>' alt='Pass phrase'>
        <input type=number min=-9 max=81 name=captcha required>
        <input type=submit name=signUp value='Sign up'><br>
        <input type=button onclick='index.logIn();' value='Log in'>
      </form>
    </main>
  </body>
</html>
9.6.13.4. index.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
"use strict";

var index = {
    init: function () {
        document.getElementById('signUpForm').style.display = 'none';
        var tmp = document.documentMode, e, isIE; // http://www.pinlady.net/PluginDetect/IE
        // Try to force this property to be a string.
        try {
            document.documentMode = "";
        }
        catch (e) {
        }

        // If document.documentMode is a number, then it is a read-only property, and so
        // we have IE 8+.
        // Otherwise, if conditional compilation works, then we have IE < 11.
        // Otherwise, we have a non-IE browser.
        isIE = typeof document.documentMode == "number" || eval("/*@cc_on!@*/!1");

        // Switch back the value to be unobtrusive for non-IE browsers.
        try {
            document.documentMode = tmp;
        }
        catch (e) {
        }
        if (isIE)
            window.alert("This site requires at least version 11 of " +
            "Internet Explorer. And even then drag and drop does " +
            "not work.\nFirefox or Chrome work best.");
    },

    signUp: function () {
        document.getElementById('logInForm').style.display = 'none';
        document.getElementById('signUpForm').style.display = 'block';
        document.getElementById('signUpForm')[0].focus();
    },

    logIn: function () {
        document.getElementById('logInForm').style.display = 'block';
        document.getElementById('signUpForm').style.display = 'none';
        document.getElementById('logInForm')[0].focus();
    },

    checkUser: function (userInput) {
        if (userInput.value != '') {
            var myRegExp = /^[0-9a-zA-z_]*$/;
            if (!myRegExp.test(userInput.value)) {
                window.alert('User name may only contain digits, letters and _!');
                return;
            }
            var data = new FormData();
            data.append('user_to_check', userInput.value);
            var URL = 'checkuser.php';
            var request = new XMLHttpRequest();
            request.addEventListener('load', function (e) {
                document.getElementById('info').innerHTML = e.target.responseText;
            });
            request.open('POST', URL, true);
            request.send(data);
        }
    },

    validateSignUpForm: function () {
        var pw1 = document.getElementById('pw1').value;
        var pw2 = document.getElementById('pw2').value;
        if (pw1 !== pw2) {
            window.alert("Passwords don\'t match!");
            return false;
        }
        if (document.getElementById('available') === null) {
            window.alert('This user name is not available.');
            document.getElementById('username').focus();
        }
        return document.getElementById('available') !== null;
    }
};

window.addEventListener('load', index.init);
9.6.13.5. index.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
body {
  background:  linear-gradient(to bottom right, yellow, #772222) fixed;
  text-shadow: 1px 1px 1px white;
  margin:      0;
}

main {
  position: absolute;
  top:      64px;
  left:     0;
  right:    0;
  bottom:   0;
}

#logo {
  transition:         transform 1s;
  -webkit-transition: -webkit-transform 1s;
}

#logo:hover {
  transform:         rotate(45deg);
  -webkit-transform: rotate(45deg); /* Safari, Chrome, mobile Safari, and Android */
}

form {
  width:        280px;
  margin-left:  auto;
  margin-right: auto;
}

form > label {
  float:         left;
  width:         80px;
  text-align:    right;
  padding-right: 10px;
  margin-top:    10px;
}

form > input {
  margin-top:    10px;
  text-shadow:   1px 1px 1px white;
  border-radius: 5px;

}

form > input[type=text], form > input[type=password], form > input[type=email],
form > input[type=number] {
  opacity: 0.5;
}

form > input:focus {
  background-color: yellow;
}

form > input[type=submit], form > input[type=button] {
  background:  linear-gradient(to bottom right, yellow, red);
  margin-left: 90px;
  width:       140px;
}

form > input[type=submit]:focus, form > input[type=button]:focus {
  border: 2px solid grey;
}

form > input::-moz-focus-inner {
  border: 0;
}

form > img {
  float:      left;
  margin-top: 10px;
}

form > input[name=captcha] {
  width:       84px;
  margin-left: 10px;
}
9.6.13.6. main.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php
  require_once 'protected/bouncer.php';
  require_once 'protected/database.php';
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Quack</title>
    <meta charset=UTF-8>
    <link href=main.css rel=stylesheet>
    <script src=main.js></script>
    <script src=websocket.js></script>
  </head>
  <body>
    <header>
      <img id=logo src=logo.png alt=Logo width=64 height=64>
      <nav>
        <ul id=navList>
          <li><a id=quackButton onclick="main.toggle('quack');">Quack</a></li>
          <li><a id=memberButton onclick="main.toggle('member');">Members</a></li>
          <li><a id=profileButton onclick="main.toggle('profile');">Profile</a></li>
          <!--<li><a id=settingsButton onclick="">Settings</a></li>-->
          <li><a onclick="window.location='logout.php'">Logout
              <?php
                if (isset($_SESSION['user_name']))
                  echo $_SESSION['user_name'];
              ?>
            </a></li>
        </ul>
      </nav>
    </header>
    <main>
      <section id=quackSection>
        <?php
          if (isset($_SESSION['user_id']))
            echo "<script>var userId = {$_SESSION['user_id']};</script>";
          if (isset($_SESSION['user_name']))
            echo "<script>var userName = '{$_SESSION['user_name']}';</script>";
        ?>
        <span class="drag" draggable=true></span>
        Message:<br>
        <textarea id=sendText cols=50 rows=5></textarea><br>
        <textarea id=receiveText cols=50 rows=10 readonly></textarea>
      </section>
      <section id=memberSection>
        <span class="drag" draggable=true></span>
        <article id=memberListArticle>
          <ul id=memberList></ul>
        </article>
        <article id=memberSelectArticle></article>
      </section>
      <section id=profileSection>
        <span class="drag" draggable=true></span>
        <div id=filebox></div>
        <textarea id=profileText cols=50 rows=10 spellcheck=true><?php
            if (isset($_SESSION['user_id']))
              echo Database::get_description($_SESSION['user_id']);
          ?></textarea><br>
        <a id=updateDescriptionButton onclick=main.updateDescription();>
          Update description</a>
        <a id=deleteProfileButton onclick=main.deleteProfile();>Delete profile</a>
        <a id=deleteImageButton onclick=main.deleteImage();>Delete image</a>
      </section>
    </main>
  </body>
</html>
9.6.13.7. main.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
"use strict";
var main = {
    filebox: undefined,

    initiate: function () {
        var elems = document.getElementsByClassName('drag');
        for (var i = 0; i < elems.length; i++)
            elems[i].addEventListener('dragstart', main.dragStart.bind(this), false);
        document.body.addEventListener('dragover', main.dragOver.bind(this), false);
        document.body.addEventListener('drop', main.drop.bind(this), false);
        this.filebox = document.getElementById('filebox');
        this.filebox.addEventListener('dragenter', function (e) {
            e.preventDefault();
        });
        this.filebox.addEventListener('dragover', function (e) {
            e.preventDefault();
        });
        this.filebox.addEventListener('drop', main.dropped.bind(this));
        elems = document.getElementsByTagName('section');
        for (i = 0; i < elems.length; i++)
            elems[i].addEventListener('click', main.manageZIndex.bind(this), false);
        main.startMemberListUpdateTimer();
        window.addEventListener('beforeunload', main.unload);
    },

    unload: function () {
        window.location = 'logout.php';
    },

    updateDescription: function () {
        var data = new FormData();
        data.append('description', document.getElementById('profileText').value);
        var URL = 'updatedescription.php';
        var request = new XMLHttpRequest();
        request.open('POST', URL, true);
        request.send(data);
    },

    deleteProfile: function () {
        if (confirm('Do you really want to delete your profile?'))
            window.location = 'deleteprofile.php';
    },

    deleteImage: function () {
        var request = new XMLHttpRequest();
        request.open('POST', 'deleteprofileimage.php', true);
        request.send();
        var image = document.getElementById('profileImage');
        if (image)
            image.src = 'getprofileimage.php?' + Math.random(); // Force script reload.
    },

    viewMemberProfile: function (id) {
        var data = new FormData();
        data.append('user_id', id);
        var URL = 'getmember.php';
        var request = new XMLHttpRequest();
        request.addEventListener('load', function (e) {
            var dat = e.target;
            if (dat.status === 200) {
                var article = document.getElementById('memberSelectArticle');
                article.innerHTML = dat.responseText;
            }
        });
        request.open('POST', URL, true);
        request.send(data);
    },

    toggle: function (name) {
        var section = document.getElementById(name + 'Section');
        var button = document.getElementById(name + 'Button');
        this.zeroZIndex();
        if (!section.hasAttribute('active')) {
            var request = new XMLHttpRequest();
            request.addEventListener('load', function (e) {
                var data = e.target;
                if (data.status === 200 && data.responseText === 'false')
                    window.location = 'logout.php';
            });
            request.open('POST', 'stillloggedin.php', true);
            request.send();
            section.setAttribute('active', '');
            button.setAttribute('active', '');
            if (name === 'profile') {
                var image = document.getElementById('profileImage');
                if (image) // Force script reload.
                    image.src = 'getprofileimage.php?' + Math.random();
                else {
                    image = document.createElement('img');
                    image.id = 'profileImage';
                    image.alt = 'Drop your image here.';
                    image.src = 'getprofileimage.php?' + Math.random(); // Force reload.
                    image.setAttribute('draggable', 'false');
                    // No image drag.
                    image.setAttribute('onmousedown', 'event.preventDefault();');
                    document.getElementById('filebox').appendChild(image);
                }
            }
            section.style.zIndex = '1';
        }
        else {
            section.removeAttribute('active');
            button.removeAttribute('active');
        }
    },

    dropped: function (e) {
        e.preventDefault();
        var files = e.dataTransfer.files;
        if (files.length) {
            // Based on J.D. Gauchat's "HTML5 for Masterminds 2nd ed." p.397-398.
            /*var list = '';
             for (var f = 0; f < files.length; f++) {
             var file = files[f];
             list += '<div>File. ' + file.name;
             list += '<br><span><progress value=0 max=100>0%</progress></span>';
             list += '</div>';
             }
             filebox.innerHTML = list;*/
            //var count = 0;
            var upload = function () {
                var myfile = files[0]; //count];
                var data = new FormData();
                data.append('file', myfile);
                var url = 'upload.php';
                var request = new XMLHttpRequest();
                //var xmlupload = request.upload;

                /*xmlupload.addEventListener('progress', function (e) {
                 if (e.lengthComputable) {
                 var child = count + 1;
                 var per = parseInt(e.loaded / e.total * 100);
                 var progressbar = filebox.querySelector('div:nth-child(' + child +
                 ') > span > progress');
                 progressbar.value = per;
                 progressbar.innerHTML = per + '%';
                 }
                 });*/
                request.addEventListener('load', function (e) {
                    /*var child = count + 1;
                     var elem = filebox.querySelector('div:nth-child(' + child +
                       ') > span');
                     elem.innerHTML = 'Done!';
                     count++;
                     if (count < files.length) upload();*/
                    var data = e.target;
                    if (data.status === 200)
                        document.getElementById('profileImage').src = 'getprofileimage.php?'
                            + Math.random(); // Force script reload.
                });
                request.open('POST', url, true);
                request.send(data);
            };
            upload();
        }
    },

    dragSource: undefined,
    dragStart: function (event) {
        this.dragSource = event.target.parentNode;
        main.zeroZIndex();
        this.dragSource.style.zIndex = '1';
        var style = window.getComputedStyle(this.dragSource, null);
        var x = window.getComputedStyle(this.dragSource, null).getPropertyValue("width");
        event.dataTransfer.setDragImage(this.dragSource, parseInt(x), 0);
        event.dataTransfer.setData("text/plain",
            (parseInt(style.getPropertyValue("left"), 10) - event.clientX) + ',' +
                (parseInt(style.getPropertyValue("top"), 10) - event.clientY));
    },

    dragOver: function (event) {
        event.preventDefault();
        return false;
    },

    drop: function (event) {
        if (event.dataTransfer.files.length === 0) { // Do not handle dragged files here.
            var offset = event.dataTransfer.getData("text/plain").split(',');
            this.dragSource.style.left = (event.clientX + parseInt(offset[0], 10)) + 'px';
            this.dragSource.style.top = (event.clientY + parseInt(offset[1], 10)) + 'px';
        }
        event.preventDefault();
        return false;
    },

    zeroZIndex: function () {
        var elems = document.getElementsByTagName('section');
        for (var i = 0; i < elems.length; i++)
            elems[i].style.zIndex = '0';
    },

    manageZIndex: function (event) {
        main.zeroZIndex();
        event.currentTarget.style.zIndex = '1';
    },

    startMemberListUpdateTimer: function () {
        setInterval(function () {
            var request = new XMLHttpRequest();
            request.addEventListener('load', function (e) {
                var data = e.target;
                var ul = document.getElementById('memberList');
                if (data.status === 200) ul.innerHTML = data.responseText;
            });
            request.open('POST', 'getmembers.php', true);
            request.send();
        }, 2000);
    }
};

window.addEventListener('load', main.initiate);
9.6.13.8. main.css
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
body {
  background:            linear-gradient(to bottom right, yellow, #772222);
  background-attachment: fixed;
  text-shadow:           1px 1px 1px white;
  margin:                0;
}

header {
  position: fixed;
  left:     0;
  top:      0;
  width:    100%;
  height:   64px;
}

nav {
  position: fixed;
  left:     80px;
  top:      0px;
  height:   64px;
  right:    0;
}

main {
  position: absolute;
  left:     0;
  top:      64px;
  bottom:   20px;
  right:    0;
}

section {
  position:   absolute;
  left:       0;
  top:        130px;
  overflow-y: auto;
  margin:     0;
}

footer {
  position:   fixed;
  left:       0;
  bottom:     0;
  width:      100%;
  text-align: center;
  font-size:  0.8em;
}

#logo {
  float:              left;
  transition:         transform 1s;
  -webkit-transition: -webkit-transform 1s;
}

#logo:hover {
  transform:         rotate(45deg);
  -webkit-transform: rotate(45deg); /* Safari, Chrome, mobile Safari, and Android */
}

ul {
  margin:  0;
  padding: 0;
}

li {
  display: inline;
}

/*article {
  background: lightgray;
  padding:    5px;
  margin:     0 10px 20px;
  box-shadow: 10px 10px 10px black;*/
/*transition: 5s;*/
/*}*/

/*article:hover {
  background: darkslategray;
}*/

h1 {
  text-shadow: 2px 2px 2px white;
  text-align:  center;
}

h2 {
  margin-top:  0;
  text-shadow: 1px 1px 1px white;
}

a {
  text-decoration:       none;
  /* stackoverflow.com/questions/826782/css-rule-to-disable-text-selection-highlighting */
  -webkit-touch-callout: none;
  -webkit-user-select:   none;
  -khtml-user-select:    none;
  -moz-user-select:      none;
  -ms-user-select:       none;
  /*user-select:           none;*/
}

#navList {
  position: fixed;
  left:     80px;
  top:      20px;
  height:   64px;
}

#navList > li {
  margin:  0;
  padding: 10px 0;
  top: 15px;
}

#navList > li > a {
  padding:          10px;
  border:           outset yellow;
  background-color: yellow;
  /*vertical-align:   middle;*/
  color:            blue;
  box-shadow:       3px 3px 3px black;
  transition:       5s;
}

a:hover {
  background: linear-gradient(to bottom right, yellow, red);
  cursor:     default;
}

#navList > li > a[active] {
  background-color: lightgreen;
  border:           inset yellow;
}

#navList > li > a:visited {
  color: blue;
}

*[draggable=true] {
  cursor: move;
}

*[draggable=false] {
  cursor: default;
}

#quackSection, #profileSection, #memberSection {
  display:               none;
  min-width:             200px;
  min-height:            200px;
  resize:                both;
  background:            linear-gradient(to bottom right, yellow, #772222);
  background-attachment: fixed;
  box-shadow:            10px 10px 10px black;
}

#memberSection {
  left: 500px;
}

#profileSection {
  left: 800px;
}

#quackSection[active], #profileSection[active], #memberSection[active] {
  display: block;
  border:  outset gold;
}

input[disabled] {
  background:            linear-gradient(to bottom right, yellow, #772222);
  background-attachment: fixed;
}

textarea {
  background:            linear-gradient(to bottom right, yellow, #772222);
  background-attachment: fixed;
  border:                inset gold;
}

#sendText {
  margin-top: 5px;
}

#receiveText {
  margin-bottom: 20px;
}

#profileText:focus, #sendText:focus {
  background: yellow;
}

#filebox {
  border:           2px inset black;
  background-color: black;
  max-width:        200px;
  max-height:       200px;
}

.drag {
  background-color: gold;
  opacity:          0.6;
  position:         absolute;
  right:            0px;
  top:              0px;
  width:            24px;
  height:           24px;
}

#memberListArticle {
  float: left;
  width: 200px;
}

#memberSelectArticle {
  margin-left: 200px;
}
9.6.13.9. websocket.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// The global variables userId and userName need to be defined before invoking this script.
"use strict";
var databox, socket;

function initiate() {
    databox = document.getElementById('receiveText');
    document.getElementById('sendText').addEventListener('keyup', send, false);
    socket = new WebSocket("ws://foxi.ltam.lu:35000"); // CHANGE!!!
    socket.addEventListener('open', opened, false);
    socket.addEventListener('message', received, false);
    socket.addEventListener('close', closed, false);
    socket.addEventListener('error', error, false);
}

function opened() {
    databox.innerHTML += getTimeString() + ' CONNECTION OPENED  Status: ' +
        socket.readyState + '\n';
    sendJSONConnectionId();
}

function received(e) {
    databox.innerHTML += getTimeString() + " " + e.data + "\n";
    databox.scrollTop = databox.scrollHeight; // Scroll to end of textarea.
}

function closed() {
    databox.innerHTML += getTimeString() + ' CONNECTION CLOSED\n';
    document.getElementById('sendText').disabled = true;
}

function error(e) {
    databox.innerHTML += e.data + '\n';
}

function createJSONMessage(msg) {
    return JSON.stringify({
        type: "message",
        text: msg,
        user_id: userId,
        user_name: userName,
        date: getTimeString()
    });
}

function sendJSONConnectionId() {
    socket.send(JSON.stringify({
        type: "identification",
        text: "",
        user_id: userId,
        user_name: userName,
        date: getTimeString()
    }));
}

function send(e) {
    if (e.keyCode === 13 && e.shiftKey === false) {
        var command = document.getElementById('sendText').value;
        socket.send(createJSONMessage(command));
        document.getElementById('sendText').value = '';
        databox.innerHTML += getTimeString() + ' ' + command;
        databox.scrollTop = databox.scrollHeight; // Scroll to end of textarea.
    }
}

function getTimeString() {
    var d = new Date();
    var s = d.getDate() + '.' + d.getMonth() + '.' + (d.getFullYear() - 2000) + ' ' +
        d.getHours() + ':' + d.getMinutes();
    return s;
}

addEventListener('load', initiate);
9.6.13.10. checkuser.php
1
2
3
4
5
6
7
8
<?php
  require_once 'protected/database.php';

  if (isset($_POST['user_to_check']))
    if (Database::get_user_id($_POST['user_to_check']))
      echo '<span>&nbsp;&#x2718;</span>';
    else echo '<span id=available>&nbsp;&#x2714;</span>';
?>
9.6.13.11. logout.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
  #require_once 'protected/database.php';
  if (!isset($_SESSION)) session_start();
  /*if (isset($_SESSION['user_id'])) {
    Database::logout($_SESSION['user_id']);
  }*/
  $_SESSION = array();
  if (session_id() != "" || isset($_COOKIE[session_name()])) setcookie(session_name(),
    '', 1, '/');
  session_destroy();
  header('Location: index.php');
?>
9.6.13.12. deleteprofile.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
  require_once 'protected/bouncer.php';
  require_once 'protected/database.php';
  Database::delete_user($_SESSION['user_id']);
  $dir_path = "./protected/users/{$_SESSION['user_name']}";
  # http://stackoverflow.com/questions/1407338/a-recursive-remove-directory-function-for-php
  try {
    foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_path,
      FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path) {
      $path->isFile() ? unlink($path->getPathname()) : rmdir($path->getPathname());
    }
    rmdir($dir_path);
  } catch (Exception $e) {
  }
  header('Location: logout.php');
?> 
9.6.13.13. deleteprofileimage.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
require_once 'protected/bouncer.php';
$file_path = '';
$dir_path = "./protected/users/{$_SESSION['user_name']}";
try { # Run through all files in the directory. This primitive approach is good for now.
    foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_path,
        FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path)
        if ($path->isFile()) $file_path = $path->getPathname();
} catch (Exception $e) {
}
if ($file_path !== '') unlink($file_path);
?> 
9.6.13.14. getprofileimage.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
  require_once 'protected/bouncer.php';
  $file_path = '';
  if (isset($_GET['user_name'])) $dir_path = "./protected/users/{$_GET['user_name']}";
  else $dir_path = "./protected/users/{$_SESSION['user_name']}";
  try { # Run through all files in the directory. This primitive approach is good for now.
    foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_path,
      FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path) {
      if ($path->isFile()) $file_path = $path->getPathname();
    }
  } catch (Exception $e) {
  }
  header('Content-type: image/png');
  if ($file_path !== '') readfile($file_path);
?>
9.6.13.15. getmember.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
  require_once 'protected/bouncer.php';
  require_once 'protected/database.php';
  $output = '';
  if (isset($_POST['user_id'])) {
    $desc = Database::get_user_data($_POST['user_id']);
    if ($desc) {
      $output .= '<p>' . $desc['user_name'] . '</p>';
      $output .= '<img src=getprofileimage.php?user_name=' . $desc['user_name'];
      $output .= ' draggable=false onmousedown="event.preventDefault();"';
      $output .= ' alt=""><br>';
      $output .= '<p><pre>' . $desc['description'] . '</pre></p>';
    }
  }
  echo $output;
?>
9.6.13.16. getmembers.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
  require_once 'protected/bouncer.php';
  require_once 'protected/database.php';
  if (isset($_SESSION['user_id']))
    Database::update_login_timestamp($_SESSION['user_id']);
  $users  = Database::get_users();
  $output = '';
  if ($users) {
    foreach ($users as $user) {
      $output .= '<li><a onclick="main.viewMemberProfile(' . $user['id'] . ');">' .
        $user['first_name'] . ' ' . $user['last_name'] . '</a>';
      if (Database::is_logged_in($user['id']))
        $output .= '&nbsp;&nbsp;<img src=green.png alt=green.png>';
      $output .= '</li><br>';
    }
  }
  echo $output;
?>
9.6.13.17. stillloggedin.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_path', dirname(htmlspecialchars($_SERVER['PHP_SELF'])));
  /*  if (!isset($_SERVER['HTTPS'])) // # SSL is not active, activate it.
      header('Location: https://' . $_SERVER['HTTP_HOST'] .
        dirname(htmlspecialchars($_SERVER['PHP_SELF'])));*/
  if (!isset($_SESSION)) session_start();
  # After 30 seconds we'll generate a new session ID to prevent a session
  # fixation attack (cf. PHP cookbook p. 338).
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)){
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  if (!isset($_SESSION['user_id'])) echo 'false';
  else echo 'true';
?>
9.6.13.18. updatedescription.php
1
2
3
4
5
6
<?php
  require_once 'protected/bouncer.php';
  require_once 'protected/database.php';

  Database::update_description($_SESSION['user_id'], $_POST['description']);
?>
9.6.13.19. upload.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
  require_once 'protected/bouncer.php';
  $dir_path = "protected/users/{$_SESSION['user_name']}";
  try { # First we delete all files in the user directory.
    foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_path,
      FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path) {
      if ($path->isFile()) unlink($path->getPathname());
    }
  } catch (Exception $e) {
  }
  if (isset($_FILES['file'])) { # Then we upload the new image.
    $save_to = $dir_path . '/' . $_FILES['file']['name'];
    move_uploaded_file($_FILES['file']['tmp_name'], $save_to);
    # From Robin Nixon's "Learning PHP, MySQL, JavaScript & CSS" 2nd ed. p. 484
    $type_OK = TRUE;
    switch ($_FILES['file']['type']) {
      case 'image/gif':
        $src = imagecreatefromgif($save_to);
        break;
      case 'image/jpeg': # Allow both regular and progressive jpegs.
      case 'image/pjpeg':
        $src = imagecreatefromjpeg("$save_to");
        break;
      case 'image/png':
        $src = imagecreatefrompng("$save_to");
        break;
      default: $type_OK = FALSE;
    }

    if ($type_OK) {
      list($w, $h) = getimagesize($save_to);
      $max = 200;
      $tw = $w;
      $th = $h;
      if ($w > $h && $max < $w) {
        $th = $max / $w * $h;
        $tw = $max;
      }
      elseif ($h > $w && $max < $h) {
        $tw = $max / $h * $w;
        $th = $max;
      }
      elseif ($max < $w) $tw = $th = $max;
      $tmp = imagecreatetruecolor($tw, $th);
      imagecopyresampled($tmp, $src, 0, 0, 0, 0, $tw, $th, $w, $h);
      imageconvolution($tmp, array(array(-1, -1, -1), array(-1, 16, -1),
        array(-1, -1, -1)), 8, 0);
      imagepng($tmp, $save_to);
      imagedestroy($tmp);
      imagedestroy($src);
    }
  }
?>
9.6.13.20. bouncer.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
  #echo '<pre>' . print_r($_SERVER, true) . '</pre>';
  #echo $_SERVER['HTTP_REFERER'];
  # Only send session id cookie over SSL.
  //ini_set('session.cookie_secure', true);
  # Session IDs may only be passed via cookies, not appended to URL.
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  /*if (!isset($_SERVER['HTTPS'])) // # SSL is not active, activate it.
    header('Location: https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']));*/
  if (!isset($_SESSION)) session_start();
  # After 30 seconds we'll generate a new session ID to prevent a session
  # fixation attack (cf. PHP cookbook p. 338).
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  if (!isset($_SESSION['user_id'])) {// No user logged in -> go to the login page.
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/index.php');
    exit;
  }
  /*echo '<pre>' . print_r($_SESSION, true) . '</pre>';
  if(isset($_SESSION['lastpage']) && $_SESSION['lastpage'] == __FILE__){
    if($_SERVER['QUERY_STRING'] != $_SESSION['querystring']){
      echo "Same page but querystring changed";
    }elseif($_SERVER['REQUEST_METHOD'] == "POST"){
      echo "This is in response to a form submission";
    }else{
      echo "Either a refresh or same page as last page when re-entering site within
              session timeout period.";
    }
  }else{
    echo "New page";
  }*/
  /*$_SESSION['lastpage'] = __FILE__;
  $_SESSION['querystring'] = $_SERVER['QUERY_STRING'];*/
?>
9.6.13.21. server.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/php -q
<?PHP
  require_once("websocket.server.php");

  class DemoEchoHandler extends WebSocketUriHandler {
    public function onMessage(IWebSocketConnection $user, IWebSocketMessage $msg) {
      $this->say("[ECHO] " . strlen($msg->getData()) . " bytes");
      $user->sendMessage($msg);
    }

    public function onAdminMessage(IWebSocketConnection $user, IWebSocketMessage $obj) {
      $this->say("[DEMO] Admin TEST received!");

      $frame = WebSocketFrame::create(WebSocketOpcode::PongFrame);
      $user->sendFrame($frame);
    }
  }

  class DemoSocketServer implements IWebSocketServerObserver {
    protected $debug = true;
    protected $server;

    public function __construct() {
      $this->server = new WebSocketServer("tcp://0.0.0.0:35000",
        'superdupersecretkey');
      $this->server->addObserver($this);
      $this->server->addUriHandler("echo", new DemoEchoHandler());
    }

    public function onConnect(IWebSocketConnection $user) {

      $this->say("[DEMO] {$user->getId()} connected");
    }

    public function onMessage(IWebSocketConnection $user, IWebSocketMessage $msg) {
      $thisuser = $user->getId();
      $msg      = json_decode($msg->getData());
      print_r($msg);
      if ($msg->type === 'identification') $this->say('Identification');
      else {
        $msgback = WebSocketMessage::create($msg->user_name . ' quacks: ' .
          trim($msg->text));
        foreach ($this->server->getConnections() as $user)
          if ($user->getId() != $thisuser) $user->sendMessage($msgback);
      }
    }

    public function onDisconnect(IWebSocketConnection $user) {
      $this->say("[DEMO] {$user->getId()} disconnected");
    }

    public function onAdminMessage(IWebSocketConnection $user, IWebSocketMessage $msg) {
      $this->say("[DEMO] Admin Message received!");
      $frame = WebSocketFrame::create(WebSocketOpcode::PongFrame);
      $user->sendFrame($frame);
    }

    public function say($msg) {
      echo "$msg \r\n";
    }

    public function run() {
      $this->server->run();
    }
  }

  $server = new DemoSocketServer();
  $server->run();
?>

10. Tutorials

10.1. WMOTU Lab

WMOTULab1

10.1.1. index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Lab v1</title>
    <meta charset=utf-8>
    <link href=style.css rel=stylesheet>
  </head>
  <body>
    <header>
      <img id=logo src=logo.png alt=Logo width=64 height=64>
      <nav>
        <ul id=navList>
          <li><a id=selectedPage>Home</a></li>
          <li><a href=html5.html>HTML5</a></li>
          <li><a href=css3.html>CSS3</a></li>
          <li><a href=javascript.html>JavaScript</a></li>
          <li><a href=php5.html>PHP5</a></li>
          <li><a href=mysql5.html>MySQL5</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <h1>Latest news</h1>
      <section>
        <ul id=articleList>
          <li>
            <a href=#>
              <article>
                <h2>The sixth alpha version of WMOTU Lab goes live!</h2>
                <time datetime=2014-01-04>4.1.14</time>
                <p>Exciting times...</p>
              </article>
            </a>
          </li>
          <li>
            <a href=#>
              <article>
                <h2>The fifth alpha version of WMOTU Lab goes live!</h2>
                <time datetime=2014-01-04>4.1.14</time>
                <p>Exciting times...</p>
              </article>
            </a>
          </li>
          <li>
            <a href=#>
              <article>
                <h2>The fourth alpha version of WMOTU Lab goes live!</h2>
                <time datetime=2014-01-04>4.1.14</time>
                <p>Exciting times...</p>
              </article>
            </a>
          </li>
          <li>
            <a href=#>
              <article>
                <h2>The third alpha version of WMOTU Lab goes live!</h2>
                <time datetime=2014-01-04>4.1.14</time>
                <p>Exciting times...</p>
              </article>
            </a>
          </li>
          <li>
            <a href=#>
              <article>
                <h2>The second alpha version of WMOTU Lab goes live!</h2>
                <time datetime=2014-01-04>4.1.14</time>
                <p>Exciting times...</p>
              </article>
            </a>
          </li>
          <li>
            <a href=#>
              <article>
                <h2>The first alpha version of WMOTU Lab goes live!</h2>
                <time datetime=2014-01-04>4.1.14</time>
                <p>Exciting times...</p>
              </article>
            </a>
          </li>
        </ul>
      </section>
    </main>
    <footer>&copy; 2014 WMOTU</footer>
  </body>
</html>

10.1.2. html5.html

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Lab v1</title>
    <meta charset=utf-8>
    <link href=style.css rel=stylesheet>
  </head>
  <body>
    <header>
      <img id=logo src=logo.png alt=Logo width=64 height=64>
      <nav>
        <ul id=navList>
          <li><a href=index.html>Home</a></li>
          <li><a id=selectedPage>HTML5</a></li>
          <li><a href=css3.html>CSS3</a></li>
          <li><a href=javascript.html>JavaScript</a></li>
          <li><a href=php5.html>PHP5</a></li>
          <li><a href=mysql5.html>MySQL5</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <table>
        <caption>HTML5 Resources</caption>
        <thead>
          <tr>
            <th>Web</th>
            <th>Books</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td><a href=http://developers.whatwg.org
                 target=_blank>HTML: The Living Standard</a></td>
            <td><a href=http://shop.oreilly.com/product/9780596159924.do
                 target=_blank>Head First HTML and CSS</a></td>
          </tr>
          <tr>
            <td><a href=http://w3schools.com/html/html5_intro.asp
                 target=_blank>w3schools.com</a></td>
            <td></td>
          </tr>
          <tr>
            <td><a href=http://www.w3.org/TR/html51
                 target=_blank>The official working draft</a></td>
            <td></td>
          </tr>
          <tr>
            <td><a href=http://css3-html5.de/html-css-lernen
                 target=_blank>CSS3 & HTML5 (German)</a></td>
            <td></td>
          </tr>
          <tr>
            <td><a href=http://www.html-seminar.de
                 target=_blank>HTML-Seminar (German)</a></td>
            <td></td>
          </tr>
          <tr>
            <td><a href=http://htmldog.com
                 target=_blank>HTML Dog</a></td>
            <td></td>
          </tr>
          <tr>
            <td><a href=http://learn.shayhowe.com
                 target=_blank>A Practical Guide to HTML & CSS</a></td>
            <td></td>
          </tr>
          <tr>
            <td><a href=http://www.quackit.com
                 target=_blank>Quackit</a></td>
            <td></td>
          </tr>
          <tr>
            <td><a href=http://www.html5code.nl
                 target=_blank>HTML5 Code</a></td>
            <td></td>
          </tr>
          <tr>
            <td><a href=http://coding.smashingmagazine.com
                 target=_blank>Smashing Magazine</a></td>
            <td></td>
          </tr>
          <tr>
            <td><a href=https://bitbucket.org/webrtc/codelab
                 target=_blank>Codelab WebRTC</a></td>
            <td></td>
          </tr>
          <tr>
            <td><a href=http://www.html5rocks.com/en/tutorials/webrtc/basics
                 target=_blank>HTML5 Rocks WebRTC Tutorial</a></td>
            <td></td>
          </tr>
          <tr>
            <td><a href=http://www.w3.org/WAI/intro/wcag.php
                 target=_blank>Web Content Accessibility Guidelines</a></td>
            <td></td>
          </tr>
        </tbody>
      </table>
    </main>
    <footer>&copy; 2014 WMOTU</footer>
  </body>
</html>

10.1.3. style.css

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
@import url("https://fonts.googleapis.com/css?family=Prosto+One");

body {
  background:            linear-gradient(to bottom right, yellow, #772222);
  background-attachment: fixed;
  font-family:           'Prosto One', cursive;
}

header {
  position: fixed;
  left:     0;
  top:      0;
  width:    100%;
  height:   64px;
}

nav {
  height:     44px;
  padding-top: 20px;
  text-align: center;
}

main {
  position: fixed;
  left:     0;
  top:      64px;
  bottom:   20px;
  right:    0;
}

section {
  position:   fixed;
  left:       0;
  top:        130px;
  bottom:     20px;
  right:      0px;
  overflow-y: auto;
  margin:     0;
}

footer {
  position:   fixed;
  left:       0;
  bottom:     0;
  width:      100%;
  text-align: center;
  font-size:  0.8em;
}

#logo {
  float:              left;
  transition:         transform 1s;
  -webkit-transition: -webkit-transform 1s;
}

#logo:hover {
  transform:         rotate(45deg);
  -webkit-transform: rotate(45deg); /* Safari, Chrome, mobile Safari, and Android */
}

ul {
  margin:  0;
  padding: 0;
}

li {
  display: inline;
}

#articleList > li {
  display: block;
}

article {
  background: lightgray; /*linear-gradient(to bottom right, lightgray, darkslategray);*/
  padding:    5px;
  margin:     0 10px 20px;
  box-shadow: 10px 10px 10px black;
  transition: 5s;
}

article:hover {
  background: darkslategray; /*linear-gradient(to bottom right, darkslategray, lightgray);*/
}

h1 {
  text-shadow: 2px 2px 2px white;
  text-align:  center;
}

h2 {
  margin-top:  0;
  text-shadow: 1px 1px 1px white;
}

time {
  border:  1px outset gold;
  padding: 1px 3px;
}

a {
  text-decoration: none;
}

#navList > li {
  margin:  0;
  padding: 0;
}

#navList > li > a {
  padding:          10px;
  border:           outset yellow;
  background-color: yellow;
  vertical-align:   middle;
}

#navList > li > a:hover {
  background: linear-gradient(to bottom right, yellow, red);
}

#navList > li > #selectedPage {
  background-color: red;
}

#navList > li > #selectedPage:hover {
  background: red;
}

table {
  overflow-y: auto;
}

table, th, td {
  border: 2px outset yellow;
}

table a {
  padding:          0 5px;
  background-color: yellow;
  vertical-align:   middle;
  position:         relative;
  display: block;
}

table a:hover {
  background: linear-gradient(to bottom right, yellow, red);
}

10.2. WMOTU Invaders

WMOTUInvaders1

Developed in the context of the CLISS1 and CLISS2 modules, this game illustrates the simple functional structuring of the classic space invaders.

The solution consists of three files and demonstrates array, canvas, event and collision handling. JSONP is used to read and save the scores to a MySQL database.

10.2.1. index.html

The HTML file provides the HTML content, the CSS styling and

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Invaders v1.0</title>
    <meta charset=UTF-8>
    <link href=index.css rel=stylesheet>
  </head>
  <body>
    <section id=splash>
      <h2>WMOTU Invaders v1.0</h2>
      <ul>
        <li>
          <button id=newGame onclick=newGame();>New Game</button>
        </li>
        <li>
          <button id=highScores onclick=displayHighScores();>High Scores</button>
        </li>
        <li>
          <button id=keys onclick=displayKeys();>Keys</button>
        </li>
      </ul>
    </section>
    <section id=highScoreSection>
      <button style="position: fixed; top: 0;" onclick=hideHighScores();>OK</button>
      <table id=highScoreListTable>
        <caption>Hall of Fame</caption>
        <thead>
          <tr>
            <th>Rank</th>
            <th>Player</th>
            <th>Score</th>
            <th>Level</th>
          </tr>
        </thead>
      </table>
    </section>
    <section id=keysSection>
      <p>Use left and right cursor keys to move your spaceship and the space bar to fire.
        The game can be paused at any time by pressing <code>P</code>. Pressing
        <code>P</code> again will resume.
        You can exit the game early using <code>Esc</code>.</p>
      <button onclick=hideKeys();>OK</button>
    </section>
    <section id=board>
      <p>
        Score: <span id=score></span>&nbsp;
        Level: <span id=level></span>
      </p>
      <canvas id=canvas width=600 height=400>This browser does not run this game (canvas
        support missing).
      </canvas>
      <section>
        <img id=life1 src=playerspaceship36x46.png alt=playerspaceship36x46.png>
        <img id=life2 src=playerspaceship36x46.png alt=playerspaceship36x46.png>
      </section>
    </section>
    <script src=index.js></script>
  </body>
</html>

10.2.2. index.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
"use strict";
// Declaration of global variables
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const imagePlayer = new Image(36, 46);
const imageShot = new Image(12, 23);
const imageAlien = new Image(42, 27);
const imageAlienBomb = new Image(10, 10);
imagePlayer.src = 'playerspaceship36x46.png';
imageShot.src = 'shot12x23.png';
imageAlien.src = 'alien42x27.png';
imageAlienBomb.src = 'alienbomb10x10.png';
let currPlayerX = (canvas.width - imagePlayer.width) / 2;
let currPlayerY = canvas.height - imagePlayer.height - 5;
let playerShotsX = [], playerShotsY = [], initialPlayerSpeed = 20, initialNumOfLives = 2;
let numAliens = 40, numAliensPerRow = 10, minFireThreshold = 300;
const DBServerURL = "index.php?callback=";
let aliensX = [], aliensY = [], alienBombsX = [], alienBombsY = [];
let highScoresReady, alienDirection, fireButton = false, moveLeft = false,
  moveRight = false, timeOfLastShot = 0;
let alienXSpeed, alienYSpeed, loseLife, numOfLives, alienBombSpeed;
let score = ~0, highScoreList, currLevel = 1, currPlayerSpeed, lastName = "", numShots,
  pauseButton, invisible, lastAnimationTime = 0, adjustmentFactor = 0.2;

// http://codeincomplete.com/posts/2013/12/4/javascript_game_foundations_the_game_loop
const timestamp = () => {
  return window.performance && window.performance.now ?
    window.performance.now() : new Date().getTime();
};

/* Called by newGame.
 * Draws aliens and adjusts global variables. */
const init = () => {
  let x = 1, y = 1;
  for (let i = 0; i < numAliens; i++) {
    aliensX[i] = x;
    aliensY[i] = y;
    context.drawImage(imageAlien, aliensX[i], aliensY[i]);
    x += imageAlien.width + 10;
    if (i % numAliensPerRow == 9) { // 10 aliens per row
      x = 1;
      y += imageAlien.height + 7;
    }
  }
  document.getElementById("score").innerHTML = ~(score >> 2);
  document.getElementById("level").innerHTML = currLevel;
  alienXSpeed = (5 + currLevel) * adjustmentFactor;
  alienYSpeed = currLevel;
  alienBombSpeed = Math.max(1, Math.floor(alienYSpeed / 3));
  alienDirection = 1; // aliens start moving to the right
  moveLeft = false; // no keys pressed
  moveRight = false;
  playerShotsX = []; // delete shots
  playerShotsY = [];
  alienBombsX = []; // delete bombs
  alienBombsY = [];
};

// Called by gameLoop.
const moveAliens = () => {
  let leftBorderTouched = false;
  for (let i = 0; i < aliensX.length; i++) {
    // We need those to determine whether there is enough room to continue in the current
    // direction. If there isn't enough room, we need to change direction.
    if (aliensX[i] <= alienXSpeed && alienDirection === -1 || aliensX[i] + imageAlien.width
      > canvas.width && alienDirection === 1) {
      alienDirection = -alienDirection;
      // If we touched the left border, we need to move the aliens down.
      if (alienDirection === 1) leftBorderTouched = true;
      break; // Once we have changed direction, no need to check the other aliens.
    }
  }
  if (leftBorderTouched) {
    alienYSpeed += 2 * adjustmentFactor;
    for (let i = 0; i < aliensY.length; i++) aliensY[i] += alienYSpeed;
  }
  // Move aliens horizontally.
  for (let i = 0; i < aliensX.length; i++) {
    aliensX[i] += alienDirection * alienXSpeed;
    context.drawImage(imageAlien, aliensX[i], aliensY[i]);
  }
};

// Called by gameLoop
const moveShotsAndBombs = () => {
  for (let i = 0; i < playerShotsY.length; i++) {
    playerShotsY[i] -= imageShot.height * adjustmentFactor;
    if (playerShotsY[i] < 0) { // delete bullet as it has left space
      playerShotsX.splice(i, 1);
      playerShotsY.splice(i, 1);
      i--;
    } else context.drawImage(imageShot, playerShotsX[i], playerShotsY[i]);
  }
  for (let i = 0; i < alienBombsY.length; i++) {
    alienBombsY[i] += alienBombSpeed;
    if (alienBombsY[i] > canvas.height) { // delete bomb as it has left space
      alienBombsX.splice(i, 1);
      alienBombsY.splice(i, 1);
      i--;
    } else context.drawImage(imageAlienBomb, alienBombsX[i], alienBombsY[i]);
  }
};

// Called by gameLoop.
// check whether a bullet has hit an alien or an alien or alien bomb touches the player
const checkCollisions = () => {
  let shotLeft, shotRight, shotTop, shotBottom, alienLeft, alienRight, alienTop,
    alienBottom, playerRight, playerBottom, alienIndex = 0, alienBombLeft,
    alienBombRight, alienBombTop, alienBombBottom, alienBombIndex = 0;

  // first check whether player was touched by an alien or a bullet touched an alien
  playerRight = currPlayerX + imagePlayer.width;
  playerBottom = currPlayerY + imagePlayer.height;

  while (alienIndex < aliensX.length) {
    alienLeft = aliensX[alienIndex];
    alienRight = alienLeft + imageAlien.width;
    alienTop = aliensY[alienIndex];
    // if an alien manages to leave via the bottom of the frame, we lose a life
    alienBottom = alienTop + imageAlien.height;
    if (alienRight >= currPlayerX && playerRight >= alienLeft && alienBottom >=
      currPlayerY || alienBottom > canvas.height) {
      loseLife = true;
      return;
    }
    let shotIndex = 0;
    // for each bullet check whether it touches the alien
    while (shotIndex < playerShotsY.length) {
      shotLeft = playerShotsX[shotIndex];
      shotRight = shotLeft + imageShot.width;
      shotTop = playerShotsY[shotIndex];
      shotBottom = shotTop + imageShot.height;
      if (alienRight >= shotLeft && shotRight >= alienLeft && alienBottom >= shotTop &&
        shotBottom >= alienTop) {
        playerShotsX.splice(shotIndex, 1);
        playerShotsY.splice(shotIndex, 1);
        aliensX.splice(alienIndex, 1);
        aliensY.splice(alienIndex, 1);
        alienIndex--;
        score = ~((~(score >> 2) + 1) << 2);
        document.getElementById("score").innerHTML = ~(score >> 2);
        break;
      }
      shotIndex++;
    }
    alienIndex++;
  }

  while (alienBombIndex < alienBombsX.length) { // check whether bomb has touched player
    alienBombLeft = alienBombsX[alienBombIndex];
    alienBombRight = alienBombLeft + imageAlienBomb.width;
    alienBombTop = alienBombsY[alienBombIndex];
    alienBombBottom = alienBombTop + imageAlienBomb.height;
    if (alienBombRight >= currPlayerX && playerRight >= alienBombLeft && alienBombBottom
      >= currPlayerY && alienBombTop <= playerBottom) {
      loseLife = true;
      return;
    }
    alienBombIndex++;
  }
};

const handleKeyDown = event => {
  if (event.keyCode === 80) {
    pauseButton = !pauseButton; // P pressed
    lastAnimationTime = timestamp();
  } else if (event.keyCode === 27) {// ESC
    loseLife = true;
    numOfLives = 0;
  } else if (event.keyCode === 32) {// fire shot
    fireButton = true;
  } else if (event.keyCode === 37) { // move left
    if (moveLeft) currPlayerSpeed += 2;
    moveLeft = true;
    moveRight = false;
  } else if (event.keyCode === 39) { // move right
    if (moveRight) currPlayerSpeed += 2;
    moveRight = true;
    moveLeft = false;
  }
};

const handleKeyUp = event => {
  if (event.keyCode === 32) {// fire shot
    fireButton = false;
  } else if (event.keyCode === 37) { // move left
    moveLeft = false;
    currPlayerSpeed = initialPlayerSpeed;
  } else if (event.keyCode === 39) { // move right
    moveRight = false;
    currPlayerSpeed = initialPlayerSpeed;
  }
};

const handleVisibilityChange = () => {
  if (invisible) lastAnimationTime = timestamp();
  invisible = !invisible;
};

const restartLevel = () => {
  init();
  lastAnimationTime = 0;
  gameLoop(timestamp());
};

// This is the function that controls the game. Called by newGame and requestAnimationFrame.
const gameLoop = currTime => {
  if (pauseButton || invisible) requestAnimationFrame(gameLoop);  // if paused, do nothing.
  else {
    let timeElapsed = currTime - lastAnimationTime;
    if (lastAnimationTime === 0) adjustmentFactor = 0.2;
    else adjustmentFactor = timeElapsed / 100;
    context.clearRect(0, 0, canvas.width, canvas.height);
    // If space key pressed and enough time since the last shot has elapsed, we shoot again.
    if (fireButton && ((currTime - timeOfLastShot) > minFireThreshold)) {
      timeOfLastShot = currTime;
      if (numShots === 3) {
        playerShotsX.push(currPlayerX);
        playerShotsY.push(currPlayerY);
        context.drawImage(imageShot, currPlayerX, currPlayerY);
        playerShotsX.push(currPlayerX + (imagePlayer.width - imageShot.width) / 2);
        playerShotsY.push(currPlayerY);
        context.drawImage(imageShot, currPlayerX + (imagePlayer.width -
          imageShot.width) / 2, currPlayerY);
        playerShotsX.push(currPlayerX + imagePlayer.width - imageShot.width);
        playerShotsY.push(currPlayerY);
        context.drawImage(imageShot, currPlayerX + imagePlayer.width - imageShot.width,
          currPlayerY);
      } else if (numShots === 2) {
        playerShotsX.push(currPlayerX);
        playerShotsY.push(currPlayerY);
        context.drawImage(imageShot, currPlayerX, currPlayerY);
        playerShotsX.push(currPlayerX + imagePlayer.width - imageShot.width);
        playerShotsY.push(currPlayerY);
        context.drawImage(imageShot, currPlayerX + imagePlayer.width - imageShot.width,
          currPlayerY);
      } else {
        playerShotsX.push(currPlayerX + (imagePlayer.width - imageShot.width) / 2);
        playerShotsY.push(currPlayerY);
        context.drawImage(imageShot, currPlayerX + (imagePlayer.width -
          imageShot.width) / 2, currPlayerY);
      }
    }
    if (moveLeft) { // If left arrow key pressed, move spaceship to the left.
      if (currPlayerX > currPlayerSpeed)
        currPlayerX -= currPlayerSpeed * adjustmentFactor;
      else currPlayerX = 1;
    }
    if (moveRight) { // If right arrow key pressed, move spaceship to the right.
      if ((currPlayerX + imagePlayer.width) < (canvas.width - currPlayerSpeed))
        currPlayerX += currPlayerSpeed * adjustmentFactor;
      else currPlayerX = canvas.width - imagePlayer.width - 1;
    }
    for (let i = 0; i < aliensX.length; i++) { // generate bombs
      if (Math.random() > (1 - (currLevel * adjustmentFactor / 3000))) {
        alienBombsX.push(aliensX[i] + (imageAlien.width - imageAlienBomb.width) / 2);
        alienBombsY.push(aliensY[i] + imageAlien.height);
        context.drawImage(imageAlienBomb, aliensX[i] + (imageAlien.width -
          imageAlienBomb.width) / 2, aliensY[i] + imageAlien.height);
      }
    }
    checkCollisions();
    if (loseLife) { // If we have been hit or touched...
      if (numOfLives >= 1)
        document.getElementById("life" + numOfLives).style.display = "none";
      numOfLives--;
      loseLife = false;
      if (numOfLives < 0) gameOver();
      else restartLevel();
    } else if (aliensX.length === 0) nextLevel();
    else { // Move everything and continue the fun.
      alienXSpeed = (5 + currLevel) * adjustmentFactor;
      context.drawImage(imagePlayer, currPlayerX, currPlayerY);
      moveShotsAndBombs();
      moveAliens();
      lastAnimationTime = timestamp(); //new Date().getTime();
      requestAnimationFrame(gameLoop); //setTimeout("gameLoop()", timeOut);
    }
  }
};

const newGame = () => {
  document.getElementById("splash").style.display = "none";
  document.getElementById("board").style.display = "block";
  score = ~0;
  currLevel = 1;
  currPlayerSpeed = initialPlayerSpeed;
  // Display spare spaceships.
  for (var i = 1; i <= initialNumOfLives; i++)
    document.getElementById("life" + i).style.display = "inline";
  numOfLives = initialNumOfLives;
  loseLife = false;
  fireButton = false;
  currPlayerX = (canvas.width - imagePlayer.width) / 2;
  currPlayerY = canvas.height - imagePlayer.height - 5;
  window.onkeydown = handleKeyDown;
  window.onkeyup = handleKeyUp;
  document.addEventListener('visibilitychange', handleVisibilityChange, false);
  numShots = 1;
  minFireThreshold = 300;
  pauseButton = false;
  invisible = false;
  init();
  lastAnimationTime = 0;
  gameLoop(timestamp());
};

const nextLevel = () => {
  currLevel++;
  if (currLevel >= 25) numShots = 3;
  else if (currLevel >= 15) numShots = 2;
  if (currLevel >= 20) minFireThreshold = 100;
  else if (currLevel >= 10) minFireThreshold = 200;
  document.getElementById("level").innerHTML = currLevel;
  lastAnimationTime = 0;
  init();
  gameLoop(timestamp());
};

const gameOver = () => {
  window.onkeydown = null;
  window.onkeyup = null;
  alert("Game over!");
  updateHighScores();
  displayHighScores();
};

const displayHighScores = () => {
  if (highScoresReady) {
    document.getElementById("board").style.display = "none";
    document.getElementById("splash").style.display = "none";
    document.getElementById("highScoreSection").style.display = "block";
  } else setTimeout(displayHighScores, 100);
};

const hideHighScores = () => {
  showSplash();
};

const readHighScores = scores => { // callback for PHP
  highScoreList = scores;
  const HL = document.getElementById("highScoreListTable");
  if (HL) {
    const childNodes = HL.tBodies;
    let x = childNodes.length;
    while (x > 0) {
      HL.removeChild(childNodes[0]);
      x = childNodes.length;
    }
  }
  const body = document.createElement("tbody");
  for (let i = highScoreList.length - 1; i >= 0; i--) {
    let row = body.insertRow(0);
    let cell1 = row.insertCell(0);
    let cell2 = row.insertCell(1);
    let cell3 = row.insertCell(2);
    let cell4 = row.insertCell(3);
    cell1.innerHTML = (i + 1);
    cell2.innerHTML = highScoreList[i].Player.slice(0, Math.min(30,
      highScoreList[i].Player.length));
    cell3.innerHTML = highScoreList[i].HighScore;
    cell4.innerHTML = highScoreList[i].Level;
    cell1.style.cssText = "text-align: right";
    cell3.style.cssText = "text-align: right";
    cell4.style.cssText = "text-align: right";
  }
  HL.appendChild(body);
  highScoresReady = true;
};

const loadSaveHighScores = (action, player) => { // action = "insert" or nothing
  const newScriptElement = document.createElement("script");
  highScoresReady = false;
  if (action && player) newScriptElement.setAttribute("src", DBServerURL +
    "readHighScores&action=insert&player=" + player + "&score=" + ~(score >> 2) +
    "&level=" + currLevel);
  else newScriptElement.setAttribute("src", DBServerURL + "readHighScores");
  newScriptElement.setAttribute("id", "jsonp");
  const oldScriptElement = document.getElementById("jsonp");
  const head = document.getElementsByTagName("head")[0];
  if (oldScriptElement === null) head.appendChild(newScriptElement);
  else head.replaceChild(newScriptElement, oldScriptElement);
};

const updateHighScores = () => {
  loadSaveHighScores();
  if (~(score >> 2) > 0) {
    lastName = prompt("Enter your name", lastName);
    loadSaveHighScores("insert", lastName);
  }
  if (lastName === null) lastName = "";
};

const showSplash = () => {
  document.getElementById("splash").style.display = "block";
  document.getElementById("highScoreSection").style.display = "none";
  document.getElementById("keysSection").style.display = "none";
  document.getElementById("board").style.display = "none";
};

const displayKeys = () => {
  document.getElementById("splash").style.display = "none";
  document.getElementById("highScoreSection").style.display = "none";
  document.getElementById("keysSection").style.display = "block";
  document.getElementById("board").style.display = "none";
};

const hideKeys = () => {
  showSplash();
};

loadSaveHighScores();

10.2.3. index.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
  require_once 'db_credentials.php';
  @$db = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
  if (mysqli_connect_errno()) {
    echo "Error: could not connect to database. Please try again later.";
    exit;
  }

  if (isset($_GET["callback"])) {
    if (isset($_GET["action"]) && $_GET["action"] === "insert") {
      if (isset($_GET["player"]) && isset($_GET["score"]) && isset($_GET["level"])) {
        $sqlq   = "INSERT INTO T1IF_Invaders VALUES (NULL, '" . $_GET["player"] . "', '"
          . $_GET["score"] . "', '" . $_GET["level"] . "');";
        $result = $db->query($sqlq);
      }
    }
    $sqlq   = "SELECT * FROM T1IF_Invaders ORDER BY HighScore DESC";
    $result = $db->query($sqlq);
    while ($row = $result->fetch_assoc()) {
      $results_array[] = $row;
    }
    echo $_GET["callback"] . "(" . json_encode($results_array) . ")";
  }
?>

10.3. WMOTU Invaders object-oriented

In order to avoid polluting the global namespace, everything has been packaged into the game object. This illustrates basic object-orientation in JavaScript.

10.3.1. index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Invaders v1.0</title>
    <meta charset=UTF-8>
    <link href=index.css rel=stylesheet>
    <script src=index.js type=module></script>
  </head>
  <body>
    <section id=splash>
      <h2>WMOTU Invaders v1.0</h2>
      <ul>
        <li>
          <button id=newGame>New Game</button>
        </li>
        <li>
          <button id=highScores>High Scores</button>
        </li>
        <li>
          <button id=keys>Keys</button>
        </li>
      </ul>
    </section>
    <section id=highScoreSection>
      <button id=OKScores style="position: fixed; top: 0;">OK</button>
      <table id=highScoreListTable>
        <caption>Hall of Fame</caption>
        <thead>
          <tr>
            <th>Rank</th>
            <th>Player</th>
            <th>Score</th>
            <th>Level</th>
          </tr>
        </thead>
      </table>
    </section>
    <section id=keysSection>
      <p>Use left and right cursor keys to move your spaceship and the space bar to fire.
        The game can be paused at any time by pressing <code>P</code>. Pressing
        <code>P</code> again will resume.
        You can exit the game early using <code>Esc</code>.</p>
      <button id=OKKeys>OK</button>
    </section>
    <section id=board>
      <p>
        Score: <span id=score></span>&nbsp;
        Level: <span id=level></span>
      </p>
      <canvas id=canvas width=600 height=400>This browser does not run this game (canvas
        support missing).
      </canvas>
      <section>
        <img id=life1 src=playerspaceship36x46.png alt=playerspaceship36x46.png>
        <img id=life2 src=playerspaceship36x46.png alt=playerspaceship36x46.png>
      </section>
    </section>
  </body>
</html>

10.3.2. index.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
const init = () => {
  const game = {
    canvas: document.getElementById('canvas'),
    context: canvas.getContext('2d'),
    imagePlayer: new Image(36, 46),
    imageShot: new Image(12, 23),
    imageAlien: new Image(42, 27),
    imageAlienBomb: new Image(10, 10),
    playerShotsX: [],
    playerShotsY: [],
    numAliens: 40,
    numAliensPerRow: 10,
    minFireThreshold: 300,
    initialPlayerSpeed: 20,
    initialNumOfLives: 2,
    aliensX: [],
    aliensY: [],
    alienBombsX: [],
    alienBombsY: [],
    fireButton: false,
    numShots: 1,
    pauseButton: false,
    invisible: false,
    moveLeft: false,
    moveRight: false,
    timeOfLastShot: 0,
    score: ~0,
    currLevel: 1,
    lastName: "",
    lastAnimationTime: 0,
    adjustmentFactor: 0.2,
    keys: {
      LEFT: 37,
      RIGHT: 39,
      ESC: 27,
      FIRE: 32,
      P: 80
    },

    // http://codeincomplete.com/posts/2013/12/4/javascript_game_foundations_the_game_loop
    timestamp() {
      return window.performance && window.performance.now ?
        window.performance.now() : new Date().getTime()
    },

    /* Called by newGame.
     * Draws aliens and adjusts game object attributes. */
    init() {
      let x = 1, y = 1
      for (let i = 0; i < this.numAliens; i++) {
        this.aliensX[i] = x
        this.aliensY[i] = y
        this.context.drawImage(this.imageAlien, this.aliensX[i], this.aliensY[i])
        x += this.imageAlien.width + 10
        if (i % this.numAliensPerRow == 9) { // 10 aliens per row
          x = 1
          y += this.imageAlien.height + 7
        }
      }
      document.getElementById("score").innerHTML = ~(this.score >> 2)
      document.getElementById("level").innerHTML = this.currLevel
      this.alienXSpeed = (5 + this.currLevel) * this.adjustmentFactor
      this.alienYSpeed = this.currLevel
      this.alienBombSpeed = Math.max(1, Math.floor(this.alienYSpeed / 3))
      this.alienDirection = 1 // aliens start moving to the right
      this.moveLeft = false // no keys pressed
      this.moveRight = false
      this.playerShotsX = [] // delete shots
      this.playerShotsY = []
      this.alienBombsX = [] // delete bombs
      this.alienBombsY = []
    },

    // Called by gameLoop.
    moveAliens() {
      this.leftBorderTouched = false
      for (let i = 0; i < this.aliensX.length; i++) {
        // We need those to determine whether there is enough room to continue in the
        // current direction. If there isn't enough room, we need to change direction.
        if (this.aliensX[i] <= this.alienXSpeed && this.alienDirection === -1 ||
          this.aliensX[i] + this.imageAlien.width > this.canvas.width &&
          this.alienDirection === 1) {
          this.alienDirection = -this.alienDirection
          // If we touched the left border, we need to move the aliens down.
          if (this.alienDirection === 1) this.leftBorderTouched = true
          break // Once we have changed direction, no need to check other aliens.
        }
      }
      if (this.leftBorderTouched) {
        this.alienYSpeed += 2 * this.adjustmentFactor
        for (let i = 0; i < this.aliensY.length; i++) this.aliensY[i] += this.alienYSpeed
      }
      // Move aliens horizontally.
      for (let i = 0; i < this.aliensX.length; i++) {
        this.aliensX[i] += this.alienDirection * this.alienXSpeed
        this.context.drawImage(this.imageAlien, this.aliensX[i], this.aliensY[i])
      }
    },

    // Called by gameLoop
    moveShotsAndBombs() {
      for (let i = 0; i < this.playerShotsY.length; i++) {
        this.playerShotsY[i] -= this.imageShot.height * this.adjustmentFactor
        if (this.playerShotsY[i] < 0) { // delete bullet as it has left space
          this.playerShotsX.splice(i, 1)
          this.playerShotsY.splice(i, 1)
          i--
        } else this.context.drawImage(this.imageShot, this.playerShotsX[i],
          this.playerShotsY[i])
      }
      for (let i = 0; i < this.alienBombsY.length; i++) {
        this.alienBombsY[i] += this.alienBombSpeed
        if (this.alienBombsY[i] > this.canvas.height) {
          this.alienBombsX.splice(i, 1) // delete bomb as it has left space
          this.alienBombsY.splice(i, 1)
          i--
        } else this.context.drawImage(this.imageAlienBomb, this.alienBombsX[i],
          this.alienBombsY[i])
      }
    },

    // Called by gameLoop.
    // Check whether a bullet has hit an alien or an alien/alien bomb touches the player.
    checkCollisions() {
      let shotLeft, shotRight, shotTop, shotBottom, alienLeft, alienRight, alienTop,
        alienBottom, playerRight, playerBottom, alienIndex = 0, alienBombLeft,
        alienBombRight, alienBombTop, alienBombBottom, alienBombIndex = 0

      // Check whether alien has touched player or bullet has touched alien.
      playerRight = this.currPlayerX + this.imagePlayer.width
      playerBottom = this.currPlayerY + this.imagePlayer.height

      while (alienIndex < this.aliensX.length) {
        alienLeft = this.aliensX[alienIndex]
        alienRight = alienLeft + this.imageAlien.width
        alienTop = this.aliensY[alienIndex]
        // If an alien manages to leave via the bottom of the frame, we lose a life.
        alienBottom = alienTop + this.imageAlien.height
        if (alienRight >= this.currPlayerX && playerRight >= alienLeft && alienBottom
          >= this.currPlayerY || alienBottom > this.canvas.height) {
          this.loseLife = true
          return
        }
        let shotIndex = 0
        // For each bullet check whether it touches the alien.
        while (shotIndex < this.playerShotsY.length) {
          shotLeft = this.playerShotsX[shotIndex]
          shotRight = shotLeft + this.imageShot.width
          shotTop = this.playerShotsY[shotIndex]
          shotBottom = shotTop + this.imageShot.height
          if (alienRight >= shotLeft && shotRight >= alienLeft && alienBottom >=
            shotTop && shotBottom >= alienTop) {
            this.playerShotsX.splice(shotIndex, 1)
            this.playerShotsY.splice(shotIndex, 1)
            this.aliensX.splice(alienIndex, 1)
            this.aliensY.splice(alienIndex, 1)
            alienIndex--
            this.score = ~((~(this.score >> 2) + 1) << 2)
            document.getElementById("score").innerHTML = ~(this.score >> 2)
            break
          }
          shotIndex++
        }
        alienIndex++
      }

      while (alienBombIndex < this.alienBombsX.length) { // Has bomb touched player?
        alienBombLeft = this.alienBombsX[alienBombIndex]
        alienBombRight = alienBombLeft + this.imageAlienBomb.width
        alienBombTop = this.alienBombsY[alienBombIndex]
        alienBombBottom = alienBombTop + this.imageAlienBomb.height
        if (alienBombRight >= this.currPlayerX && playerRight >= alienBombLeft &&
          alienBombBottom >= this.currPlayerY && alienBombTop <= playerBottom) {
          this.loseLife = true
          return
        }
        alienBombIndex++
      }
    },

    handleKeyDown(event) {
      if (event.keyCode === this.keys.P) {
        this.pauseButton = !this.pauseButton // P pressed
        this.lastAnimationTime = this.timestamp()
      } else if (event.keyCode === this.keys.ESC) {// ESC
        this.loseLife = true
        this.numOfLives = 0
      } else if (event.keyCode === this.keys.FIRE) {// fire shot
        this.fireButton = true
      } else if (event.keyCode === this.keys.LEFT) { // move left
        if (this.moveLeft) this.currPlayerSpeed += 2
        this.moveLeft = true
        this.moveRight = false
      } else if (event.keyCode === this.keys.RIGHT) { // move right
        if (this.moveRight) this.currPlayerSpeed += 2
        this.moveRight = true
        this.moveLeft = false
      }
    },

    handleKeyUp(event) {
      if (event.keyCode === this.keys.FIRE) {// fire shot
        this.fireButton = false
      } else if (event.keyCode === this.keys.LEFT) { // move left
        this.moveLeft = false
        this.currPlayerSpeed = this.initialPlayerSpeed
      } else if (event.keyCode === this.keys.RIGHT) { // move right
        this.moveRight = false
        this.currPlayerSpeed = this.initialPlayerSpeed
      }
    },

    handleVisibilityChange() {
      if (this.invisible) this.lastAnimationTime = this.timestamp()
      this.invisible = !this.invisible
    },

    restartLevel() {
      this.init()
      this.lastAnimationTime = 0
      this.gameLoop(this.timestamp())
    },

    // This function controls the game. Called by newGame and requestAnimationFrame.
    gameLoop(currTime) { // paused -> do nothing
      if (this.pauseButton) requestAnimationFrame(this.gameLoop.bind(this))
      else {
        this.timeElapsed = currTime - this.lastAnimationTime
        if (this.lastAnimationTime === 0) this.adjustmentFactor = 0.2
        else this.adjustmentFactor = this.timeElapsed / 100
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
        // If space key pressed and enough time has elapsed, we shoot again.
        if (this.fireButton && ((currTime - this.timeOfLastShot) >
          this.minFireThreshold)) {
          this.timeOfLastShot = currTime
          if (this.numShots === 3) {
            this.playerShotsX.push(this.currPlayerX)
            this.playerShotsY.push(this.currPlayerY)
            this.context.drawImage(this.imageShot, this.currPlayerX,
              this.currPlayerY)
            this.playerShotsX.push(this.currPlayerX + (this.imagePlayer.width -
              this.imageShot.width) / 2)
            this.playerShotsY.push(this.currPlayerY)
            this.context.drawImage(this.imageShot, this.currPlayerX + (
              this.imagePlayer.width - this.imageShot.width) / 2, this.currPlayerY)
            this.playerShotsX.push(this.currPlayerX + this.imagePlayer.width -
              this.imageShot.width)
            this.playerShotsY.push(this.currPlayerY)
            this.context.drawImage(this.imageShot, this.currPlayerX +
              this.imagePlayer.width - this.imageShot.width, this.currPlayerY)
          } else if (this.numShots === 2) {
            this.playerShotsX.push(this.currPlayerX)
            this.playerShotsY.push(this.currPlayerY)
            this.context.drawImage(this.imageShot, this.currPlayerX,
              this.currPlayerY)
            this.playerShotsX.push(this.currPlayerX + this.imagePlayer.width -
              this.imageShot.width)
            this.playerShotsY.push(this.currPlayerY)
            this.context.drawImage(this.imageShot, this.currPlayerX +
              this.imagePlayer.width - this.imageShot.width, this.currPlayerY)
          } else {
            this.playerShotsX.push(this.currPlayerX + (this.imagePlayer.width -
              this.imageShot.width) / 2)
            this.playerShotsY.push(this.currPlayerY)
            this.context.drawImage(this.imageShot, this.currPlayerX +
              (this.imagePlayer.width - this.imageShot.width) / 2, this.currPlayerY)
          }
        }
        if (this.moveLeft) { // If left arrow key pressed, move spaceship to the left.
          if (this.currPlayerX > this.currPlayerSpeed)
            this.currPlayerX -= this.currPlayerSpeed * this.adjustmentFactor
          else this.currPlayerX = 1
        }
        if (this.moveRight) { // If right arrow key pressed, move spaceship right.
          if ((this.currPlayerX + this.imagePlayer.width) < (this.canvas.width -
            this.currPlayerSpeed))
            this.currPlayerX += this.currPlayerSpeed * this.adjustmentFactor
          else this.currPlayerX = this.canvas.width - this.imagePlayer.width - 1
        }
        for (let i = 0; i < this.aliensX.length; i++) { // generate bombs
          if (Math.random() > (1 - (this.currLevel *
            this.adjustmentFactor / 3000))) {
            this.alienBombsX.push(this.aliensX[i] + (this.imageAlien.width -
              this.imageAlienBomb.width) / 2)
            this.alienBombsY.push(this.aliensY[i] + this.imageAlien.height)
            this.context.drawImage(this.imageAlienBomb, this.aliensX[i] +
              (this.imageAlien.width - this.imageAlienBomb.width) / 2,
              this.aliensY[i] + this.imageAlien.height)
          }
        }
        this.checkCollisions()
        if (this.loseLife) { // If we have been hit or touched...
          if (this.numOfLives >= 1)
            document.getElementById("life" + this.numOfLives).style.display =
              "none"
          this.numOfLives--
          this.loseLife = false
          if (this.numOfLives < 0) this.gameOver()
          else this.restartLevel()
        } else if (this.aliensX.length === 0) this.nextLevel()
        else { // Move everything and continue the fun.
          this.alienXSpeed = (5 + this.currLevel) * this.adjustmentFactor
          this.context.drawImage(this.imagePlayer, this.currPlayerX,
            this.currPlayerY)
          this.moveShotsAndBombs()
          this.moveAliens()
          this.lastAnimationTime = this.timestamp()
          requestAnimationFrame(this.gameLoop.bind(this))
        }
      }
    },

    newGame() {
      document.getElementById("splash").style.display = "none"
      document.getElementById("board").style.display = "block"
      this.score = ~0
      this.currLevel = 1
      this.currPlayerSpeed = this.initialPlayerSpeed
      // Display spare spaceships.
      for (let i = 1; i <= this.initialNumOfLives; i++)
        document.getElementById("life" + i).style.display = "inline"
      this.numOfLives = this.initialNumOfLives
      this.loseLife = false
      this.fireButton = false
      this.currPlayerX = (this.canvas.width - this.imagePlayer.width) / 2
      this.currPlayerY = this.canvas.height - this.imagePlayer.height - 5
      window.onkeydown = this.handleKeyDown.bind(this)
      window.onkeyup = this.handleKeyUp.bind(this)
      document.addEventListener('visibilitychange',
        this.handleVisibilityChange.bind(this))
      this.numShots = 1
      this.minFireThreshold = 300
      this.pauseButton = false
      this.invisible = false
      this.init()
      this.lastAnimationTime = 0
      this.gameLoop(this.timestamp())
    },

    nextLevel() {
      this.currLevel++
      if (this.currLevel >= 25) this.numShots = 3
      else if (this.currLevel >= 15) this.numShots = 2
      if (this.currLevel >= 20) this.minFireThreshold = 100
      else if (this.currLevel >= 10) this.minFireThreshold = 200
      document.getElementById("level").innerHTML = this.currLevel
      this.lastAnimationTime = 0
      this.init()
      this.gameLoop(this.timestamp())
    },

    gameOver() {
      window.onkeydown = null
      window.onkeyup = null
      alert("Game over!")
      this.updateHighScores()
      this.displayHighScores()
    },

    displayHighScores() {
      if (this.highScoresReady) {
        document.getElementById("board").style.display = "none"
        document.getElementById("splash").style.display = "none"
        document.getElementById("highScoreSection").style.display = "block"
      } else setTimeout(this.displayHighScores.bind(this), 100)
    },

    hideHighScores() {
      this.showSplash()
    },

    readHighScores(e) { // callback for PHP
      this.highScoreList = JSON.parse(e.target.response)
      const HL = document.getElementById("highScoreListTable")
      if (HL) {
        const childNodes = HL.tBodies
        let x = childNodes.length
        while (x > 0) {
          HL.removeChild(childNodes[0])
          x = childNodes.length
        }
      }
      const body = document.createElement("tbody")
      for (let i = this.highScoreList.length - 1; i >= 0; i--) {
        const row = body.insertRow(0)
        const cell1 = row.insertCell(0)
        const cell2 = row.insertCell(1)
        const cell3 = row.insertCell(2)
        const cell4 = row.insertCell(3)
        cell1.innerHTML = (i + 1)
        cell2.innerHTML = this.highScoreList[i].Player.slice(0, Math.min(30,
          this.highScoreList[i].Player.length))
        cell3.innerHTML = this.highScoreList[i].HighScore
        cell4.innerHTML = this.highScoreList[i].Level
        cell1.style.cssText = "text-align: right"
        cell3.style.cssText = "text-align: right"
        cell4.style.cssText = "text-align: right"
      }
      HL.appendChild(body)
      this.highScoresReady = true
    },

    loadSaveHighScores(action, player) { // action = "insert" or nothing
      const req = new XMLHttpRequest()
      const data = {
        action: action,
        player: player,
        score: ~this.score >> 2,
        level: this.currLevel
      }
      req.open('POST', 'index.php')
      req.addEventListener('load', this.readHighScores.bind(this))
      req.send(JSON.stringify(data))
      this.highScoresReady = false
    },

    updateHighScores() {
      this.loadSaveHighScores()
      if (~(this.score >> 2) > 0) {
        this.lastName = prompt("Enter your name", this.lastName)
        this.loadSaveHighScores("insert", this.lastName)
      }
      if (this.lastName === null) this.lastName = ""
    },

    showSplash() {
      document.getElementById("splash").style.display = "block"
      document.getElementById("highScoreSection").style.display = "none"
      document.getElementById("keysSection").style.display = "none"
      document.getElementById("board").style.display = "none"
    },

    displayKeys() {
      document.getElementById("splash").style.display = "none"
      document.getElementById("highScoreSection").style.display = "none"
      document.getElementById("keysSection").style.display = "block"
      document.getElementById("board").style.display = "none"
    },

    hideKeys() {
      this.showSplash()
    }
  }
  game.imagePlayer.src = 'playerspaceship36x46.png'
  game.imageShot.src = 'shot12x23.png'
  game.imageAlien.src = 'alien42x27.png'
  game.imageAlienBomb.src = 'alienbomb10x10.png'
  game.loadSaveHighScores()
  document.querySelector('#newGame').addEventListener('click', game.newGame.bind(game))
  document.querySelector('#highScores').addEventListener('click',
    game.displayHighScores.bind(game))
  document.querySelector('#keys').addEventListener('click', game.displayKeys)
  document.querySelector('#OKKeys').addEventListener('click', game.hideKeys.bind(game))
  document.querySelector('#OKScores').addEventListener('click',
    game.hideHighScores.bind(game))
}

init()

10.3.3. index.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
  $obj = json_decode(file_get_contents('php://input'));
  if (!$obj) exit;
  require_once 'db_credentials.php';
  @$db = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
  if (mysqli_connect_errno()) {
    error_log("Error: could not connect to database. Please try again later.");
    exit;
  }

  if (isset($obj->action)) {
    $sqlq   = "INSERT INTO T1IF_Invaders VALUES (NULL, '" . $obj->player .
      "', '" . $obj->score . "', '" . $obj->level . "');";
    $result = $db->query($sqlq);
  }
  $sqlq   = "SELECT * FROM T1IF_Invaders ORDER BY HighScore DESC";
  $result = $db->query($sqlq);
  while ($row = $result->fetch_assoc()) $results_array[] = $row;
  echo json_encode($results_array);
?>

10.4. WMOTU Address Book

WMOTUAddressBook1

WMOTUAddressBook2

WMOTU has been asked to develop a web app to manage an address book. The user needs to be able to add a new address, display all existing addresses as well as delete or edit them.

10.4.1. createDB.sql

The user probably does not want to reenter all the addresses each time he intends to use our address book, so we need to store them. A MySQL database is the ideal container for this type of data. So let’s create our database with a MySQL script with all the required instructions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# createDB.sql -> create the database tables for the WMOTU Address Book
DROP TABLE IF EXISTS AB_addresses;
DROP TABLE IF EXISTS AB_users;

CREATE TABLE AB_users (id        INT UNSIGNED AUTO_INCREMENT NOT NULL UNIQUE,
                       user_name VARCHAR(32)                 NOT NULL UNIQUE,
                       password  VARCHAR(40)                 NOT NULL,
  PRIMARY KEY (id))
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;
;

CREATE TABLE AB_addresses (id        INT UNSIGNED AUTO_INCREMENT NOT NULL UNIQUE,
                           user_id   INT UNSIGNED                NOT NULL,
                           name      VARCHAR(50)                 NOT NULL,
                           address1  VARCHAR(50)                 NOT NULL,
                           address2  VARCHAR(50),
                           city      VARCHAR(50)                 NOT NULL,
                           country   VARCHAR(50)                 NOT NULL,
                           post_code VARCHAR(50)                 NOT NULL,
                           phone     VARCHAR(15),
                           mobile    VARCHAR(15),
                           fax       VARCHAR(15),
                           email     VARCHAR(50),
  PRIMARY KEY (id),
  FOREIGN KEY (user_id) REFERENCES AB_users (id)
    ON DELETE CASCADE
    ON UPDATE CASCADE)
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;
;

INSERT INTO AB_users (user_name, password) VALUES ("dummy1", SHA("d1pw")),
  ("dummy2", SHA("d2pw"));
INSERT INTO AB_addresses (user_id, name, address1, city, country, post_code) VALUES
  ((SELECT
      id
    FROM AB_users
    WHERE user_name = "dummy1"), "Dummy1a", "1a, Dummystreet", "Dummyburg", "Dummyland",
   "Du-11111"),
  ((SELECT
      id
    FROM AB_users
    WHERE user_name = "dummy1"), "Dummy1b", "1b, Dummystreet", "Dummyburg", "Dummyland",
   "Du-11111"),
  ((SELECT
      id
    FROM AB_users
    WHERE user_name = "dummy2"), "Dummy2", "2, Dummystreet", "Dummyburg", "Dummyland",
   "Du-11112");

In order to create the database, we can either run this script in the MySQL command line or we use phpMyAdmin.

10.4.1.1. Using MySQL command line

If you work on the school server, open a SSH shell (using Putty for instance). On Windows, open a Windows command prompt in the project restricted folder and execute the following:

  1. mysql -u yourusername -p

  2. use your_DB_name;

  3. source createDB.sql;

10.4.1.2. Security

We create a folder WMOTUAddressBook, which will contain our app. This folder will be accessible to anyone on the Web. However, our SQL database creation script should not be visible to anyone, as it reveals the whole structure of our database. Therefore we’ll put this file into a folder that we name protected and that is only accessible by ourselves. To do this, we will place a file named .htaccess with the following contents into this folder:

1
2
3
4
AuthType Basic
AuthName "Restricted Files"
AuthUserFile /full path/.htpasswd
Require user username

Authentication type Basic sends the user name and password unencrypted. In a production environment we should therefore make sure that all traffic is encrypted using SSL. The Apache mod_ssl module would thus need to be configured and enabled. Line 2 specifies the realm for which the authentication applies. In this case it applies to restricted files. Line 3 specifies the file where the encrypted password(s) is/are stored. Line 4 specifies which user(s) is/are authorized to access the restricted files. For further information about .htaccess files, see httpd.apache.org/docs/current/howto/htaccess.html.

Now we’ll generate the .htpasswd file using the Apache tool htpasswd. Go to the bin directory of your Apache installation and execute the command htpasswd -c .htpasswd xyz, replacing xyz with your login name. The tool will ask for the password, which you’ll have to enter twice. If the two passwords you’ve entered are identical, you’ll now have a file, which contains the name of the user and his encrypted password. Move this file to the protected folder. Try to enter the restricted folder using your web browser. It should now ask for authentication.

Now the folder is protected against unauthorized access via the web, but we also need to protect it from villains on the server itself. Therefore we need to remove all rights from other users, using the command chmod -R o-rwx protected in the main directory of our app, which contains the protected folder.

10.4.1.3. Using phpMyAdmin

If you run the script for the first in phpMyAdmin, you need to comment out lines 2 and 3 that drop the tables, as the tables do not yet exist, otherwise phpMyAdmin will stop executing the script.

AB1

If all went well, you now have two new tables in your database. You can check this using desc AB_users; and desc AB_addresses; in the MySQL command line. Let’s take a closer look at what this script does. Line 1 is a comment to briefly explain the purpose of the script. Lines 2 and 3 drop any existing tables with these names in our DB. We do this in order to be able to use the script to recreate our tables in case we make modifications. Be careful: lines 2 and 3 delete the tables, including any data they contain! Lines 4-8 create table AB_users, which, as the name suggests, will hold our user data. Lines 10-26 create table AB_addresses, which will hold our addresses. Lines 28-41 create dummy user and address data, so that we do not have to type those in manually.

By using a MySQL script we avoid having to retype the commands every time we make changes to our DB structure and we can easily look up the structure of our tables.

10.4.2. index.php

Our app should be usable by any number of users. Each user will be able to manage his own addresses. So we need a sign up and login system and this should be the first page anyone accessing our app sees, unless they are still logged in, in which case they should be taken automatically to the main page.

The HTTP protocol is stateless, meaning that when a new page is loaded, there is no information left from the previous page. This is not acceptable, as we cannot ask the user to log in again on each page of our app! Sessions allow us to save state information and use it across scripts. Details can be found in the Head First book and the usual online resources (particularly php.net, cf. appendix Resources).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<?php
  // Only send session id cookie over SSL.
  ini_set('session.cookie_secure', true);
  // Session IDs may only be passed via cookies, not appended to URL.
  ini_set('session.use_only_cookies', true);
  // Set the path for the cookie to the current directory in order to prevent it from
  // being available to scripts in other directories.
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  //if ($_SERVER['SERVER_PORT'] != 443)
  //  header('Location: https://' . $_SERVER['SERVER_NAME'] . $_SERVER['SCRIPT_NAME']);
  if (!isset($_SERVER['HTTPS'])) // If SSL is not active, activate it.
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']));
  // If no session is started yet, we'll start one.
  if (!isset($_SESSION)) session_start();
  // After 30 seconds we'll generate a new session ID to prevent a session fixation
  // attack (cf. PHP cookbook p. 338).
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  // Include the database class needed to access the database.
  require_once 'database.php';
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Address Book</title>
    <meta charset=UTF-8>
    <link href=style.css rel=stylesheet>
  </head>
  <body>
    <main>
      <h1 id=logo class=threeD style="text-align: center">WMOTU Address Book</h1>
      <h2 style="text-align: center; font-size: 200%">Please login</h2>
      <form method=post id=loginForm>
        <div>
          <label>Username:</label>
          <input name=username required>
        </div>
        <div>
          <label>Password:</label>
          <input type=password name=password required>
        </div>
        <div id=loginInput>
          <input type=submit name=login value=Login>
          <input type=submit name=signup value="Sign Up">
        </div>
      </form>
    </main>
    <?php
      // If a user is already logged in, let him through to the main page.
      if (isset($_SESSION['user_id']))
        header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
          dirname($_SERVER['PHP_SELF']) . '/main.php');
      // Else, if the user has submitted his login details, we need to check them.
      elseif (isset($_POST['login'])) {
        if (isset($_POST['username']) && isset($_POST['password'])) {
          $result = Database::login($_POST['username'], $_POST['password']);
          // If a user with this login exists, we load the main page.
          if ($result) {
            $_SESSION['user_id'] = $result;
            $user_name = $_POST['username'];
            $_SESSION['user_name'] = $user_name;
            header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
              dirname($_SERVER['PHP_SELF'])) . '/main.php';
          }
        }
      }
      // Else, if the user has signed up for a new account, we need to check if
      // such a user already exists.
      elseif (isset($_POST['signup'])) {
        if (isset($_POST['username']) && isset($_POST['password'])) {
          if (Database::get_user_id($_POST['username']))
            echo "User exists already!";
          else { // If not, we'll create the new user and if successful, we'll
            // forward to the main page.
            if ($result = Database::create_user($_POST['username'],
              $_POST['password'])
            ) {
              $_SESSION['user_id'] = $result;
              $_SESSION['user_name'] = $_POST['username'];
              header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
                dirname($_SERVER['PHP_SELF'])) . '/main.php';
            } else echo "Sign up failed!";
          }
        }
      }
    ?>
  </body>
</html>

The user needs to be able to enter new or modify existing data. The ideal HTML element for this purpose is a <form> (lines 22-35).

10.4.3. db_credentials.php

This file contains the credentials for accessing the DB. You need to replace these values with your own in order to access your DB. The p: in front of the hostname implies the use of a persistent DB connection (cf. php.net/manual/en/mysqli.persistconns.php).

1
2
3
<?php
  Database::set_credentials('p:localhost', 'your user name', 'your password', 'your DB name');
?>

10.4.4. database.php

The database class offers our app’s DB interface. Thus all DB access takes place in one class and the rest of the app can reuse the functionality provided.

All the properties and methods of the database class are declared static, as they are not related to any specific object. This means we do not need to create a database object, we can simply use the class methods directly using Database::method.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
<?php
  require_once 'db_credentials.php';

  class Database {
    private static $DB_HOST;
    private static $DB_USER;
    private static $DB_PASSWORD;
    private static $DB_NAME;
    private static $DB_USERS = 'AB_users';
    private static $DB_ADDRESSES = 'AB_addresses';

    static function set_credentials($db_host, $db_user, $db_password, $db_name) {
      self::$DB_HOST     = $db_host;
      self::$DB_USER     = $db_user;
      self::$DB_PASSWORD = $db_password;
      self::$DB_NAME     = $db_name;
    }

    static function connect() {
      $dbc = new mysqli(self::$DB_HOST, self::$DB_USER, self::$DB_PASSWORD,
        self::$DB_NAME);
      if ($dbc->connect_error) trigger_error('Database connection failed: ' .
        $dbc->connect_error, E_USER_ERROR);
      $dbc->set_charset("utf8");
      return $dbc;
    }

    // Returns the id if a user with the given name already exists,
    // otherwise FALSE.
    static function get_user_id($user_name) {
      $dbc = self::connect();
      // Look up the user id in the database
      $query = 'SELECT id FROM ' . self::$DB_USERS . ' WHERE user_name = ?';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $stmt->bind_param('s', $user_name);
      $stmt->execute();
      $stmt->store_result();
      if ($stmt->num_rows === 1) {
        $stmt->bind_result($result);
        $stmt->fetch();
      }
      else $result = FALSE;
      $stmt->close();
      $dbc->close();
      return $result;
    }

    // Returns FALSE if the user could not be created, otherwise the user id.
    static function create_user($user_name, $password) {
      $dbc   = self::connect();
      $query = 'INSERT INTO ' . self::$DB_USERS .
        ' (user_name, password) VALUES (?, SHA1(?))';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $user_name = trim($user_name);
      $stmt->bind_param('ss', $user_name, $password);
      $result = $stmt->execute();
      $stmt->close();
      $dbc->close();
      if ($result) return self::get_user_id($user_name);
      else return $result;
    }

    // Returns the user_id of the user or FALSE.
    static function login($user_name, $password) {
      $dbc = self::connect();
      // Look up user id in database.
      $query = 'SELECT id FROM ' . self::$DB_USERS .
        ' WHERE user_name = ? AND password = SHA(?)';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $user_name = trim($user_name);
      $stmt->bind_param('ss', $user_name, $password);
      $stmt->execute();
      $stmt->store_result();
      if ($stmt->num_rows === 1) {
        $stmt->bind_result($result);
        $stmt->fetch();
      }
      else $result = FALSE;
      $stmt->close();
      $dbc->close();
      return $result;
    }

    // Search by address id OR user id.
    // One of the two parameters should ALWAYS be FALSE!
    // Returns an associative array or FALSE.
    static function get_addresses($id = FALSE, $user_id = FALSE) {
      $dbc    = self::connect();
      $result = FALSE;
      $query  = 'SELECT * FROM ' . self::$DB_ADDRESSES . ' WHERE ' .
        ($id ? "id = $id" : "user_id = $user_id");
      $res    = $dbc->query($query);
      if (!$res) trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      while ($dat = $res->fetch_array(MYSQLI_ASSOC)) $result[] = $dat;
      $res->free();
      $dbc->close();
      return $result;
    }

    // Returns FALSE if an address with this name and address1 already exists,
    // otherwise TRUE.
    static function insert_address($name, $address1, $address2, $city, $country,
                                   $post_code, $phone, $mobile, $fax, $email) {
      $dbc   = self::connect();
      $query = 'SELECT id FROM ' . self::$DB_ADDRESSES .
        ' WHERE name = ? AND address1 = ?';
      $stmt  = $dbc->prepare($query);
      if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' . $dbc->error,
        E_USER_ERROR);
      $name     = trim($name);
      $address1 = trim($address1);
      $stmt->bind_param('ss', $name, $address1);
      $result = $stmt->execute();
      if ($result) {
        $stmt->store_result();
        if ($stmt->num_rows > 0) return FALSE;
        $stmt->close();
        $query = 'INSERT INTO ' . self::$DB_ADDRESSES . ' (name, user_id, address1,
          address2, city, country, post_code, phone, mobile, fax, email) VALUES
          (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
        $stmt  = $dbc->prepare($query);
        if (!$stmt) trigger_error('Wrong SQL: ' . $query . ' Error: ' . $dbc->error,
          E_USER_ERROR);
        $address2  = trim($address2);
        $city      = trim($city);
        $country   = trim($country);
        $post_code = trim($post_code);
        $phone     = trim($phone);
        $mobile    = trim($mobile);
        $fax       = trim($fax);
        $email     = trim($email);
        $stmt->bind_param('sssssssssss', $name, $_SESSION['user_id'], $address1,
          $address2, $city, $country, $post_code, $phone, $mobile, $fax, $email);
        $result = $stmt->execute();
        $stmt->close();
      }
      $dbc->close();
      return $result;
    }

    // Returns TRUE if the update succeeded, otherwise false.
    static function update_address($id, $name, $address1, $address2, $city, $country,
                                   $post_code, $phone, $mobile, $fax, $email) {
      $dbc       = self::connect();
      $id        = self::sanitize_string($dbc, $id);
      $name      = self::sanitize_string($dbc, $name);
      $address1  = self::sanitize_string($dbc, $address1);
      $address2  = self::sanitize_string($dbc, $address2);
      $city      = self::sanitize_string($dbc, $city);
      $country   = self::sanitize_string($dbc, $country);
      $post_code = self::sanitize_string($dbc, $post_code);
      $phone     = self::sanitize_string($dbc, $phone);
      $mobile    = self::sanitize_string($dbc, $mobile);
      $fax       = self::sanitize_string($dbc, $fax);
      $email     = self::sanitize_string($dbc, $email);
      $result    = mysqli_query($dbc, 'UPDATE ' . self::$DB_ADDRESSES . "
      SET name='$name', address1='$address1', address2='$address2', city='$city',
        country='$country', post_code='$post_code', phone='$phone',
        mobile='$mobile', fax='$fax', email='$email' WHERE id=$id");
      mysqli_close($dbc);
      return $result;
    }

    // Returns TRUE if deletion succeeded, otherwise FALSE.
    static function delete_address($id) {
      $dbc    = self::connect();
      $id     = self::sanitize_string($dbc, $id);
      $result = mysqli_query($dbc, 'DELETE FROM ' . self::$DB_ADDRESSES .
        " WHERE id=$id");
      mysqli_close($dbc);
      return $result;
    }
  }

?>

10.4.5. bouncer.php

This script is used to make sure, that only logged in users can access the page. Everyone else is forwarded to the login and sign up page.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
  // Protect from session fixation via session adoption.
  ini_set('session.use_strict_mode', true);
  # Prevent CSRF attacks
  ini_set('session.cookie_samesite', 'Strict');
  # Only send session id cookie over SSL.
  ini_set('session.cookie_secure', true);
  # Session IDs may only be passed via cookies, not appended to URL.
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_httponly', true);
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  if (!isset($_SERVER['HTTPS'])) {# If SSL is not active, activate it.
    header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']);
    exit;
  }
  if (!isset($_SESSION)) session_start(); // Start session.
  # After 30 seconds we'll generate a new session ID to prevent a session
  # fixation attack (cf. PHP cookbook p. 338).
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  if (!isset($_SESSION['user_id'])) {// No user logged in -> go to the login page.
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/index.php');
    exit;
  }
?>

10.4.6. main.php

This is the main page, where the addresses of the current user are displayed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
  require_once 'database.php';
  require_once 'bouncer.php';
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Address Book</title>
    <meta charset=UTF-8>
    <link href=style.css rel=stylesheet>
  </head>
  <body>
    <a href=logout.php>Logout</a>
    <a href=add.php>Add a new address</a>
    <?php
      echo "Current user: " . $_SESSION['user_name'] . "<br>";
      $addresses = Database::get_addresses(false, $_SESSION['user_id']);
      if ($addresses)
        foreach ($addresses as $address) {
          echo '<article class=address>';
          echo $address['name'] . "<br>";
          echo $address['address1'] . "<br>";
          if ($address['address2']) echo $address['name'] . "<br>";
          echo $address['city'] . "<br>";
          echo $address['country'] . "<br>";
          echo $address['post_code'] . "<br>";
          if ($address['phone']) echo $address['phone'] . "<br>";
          if ($address['mobile']) echo $address['mobile'] . "<br>";
          if ($address['fax']) echo $address['fax'] . "<br>";
          if ($address['email']) echo $address['email'] . "<br>";
          echo "<a href='delete.php?id=" . $address['id'] . "'>Delete</a>&nbsp;";
          echo "<a href='edit.php?id=" . $address['id'] . "'>Edit</a>";
          echo '</article>';
        }
    ?>
  </body>
</html>

10.4.7. logout.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
  if (!isset($_SESSION)) session_start(); # Start session if not done already.
  $_SESSION = []; # Empty session array.
  # If session cookie exists, kill it.
  if (session_id() != "" || isset($_COOKIE[session_name()]))
    setcookie(session_name(), '', 1, '/');
  session_destroy(); # Kill session.
  # Now it's time to return home.
  header('Location: https://' . $_SERVER['HTTP_HOST'] .
    dirname($_SERVER['PHP_SELF']) . '/index.php');
?>

10.4.8. header.php

To increase the ease of maintenance of our app, we put the parts occurring several times into external scripts. Future changes will only have to be done in a single script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Address Book</title>
    <meta charset=UTF-8>
    <link href=style.css rel=stylesheet>
  </head>
  <body>
    <a href=logout.php>Logout</a>
    <a href=main.php>Display addresses</a>
1
2
</body>
</html>

10.4.10. add.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
  require_once 'database.php';
  require_once 'bouncer.php';
  include 'header.php';
?>
  <form method=post>
    <label for=name>Name:</label>
    <input id=name name=name required><br>
    <label for=address1>Address line 1:</label>
    <input id=address1 name=address1 required><br>
    <label for=address2>Address line 2:</label>
    <input id=address2 name=address2><br>
    <label for=city>City:</label>
    <input id=city name=city required><br>
    <label for=country>Country:</label>
    <input id=country name=country required><br>
    <label for=post_code>Post code:</label>
    <input id=post_code name=post_code required><br>
    <label for=phone>Phone number:</label>
    <input id=phone name=phone><br>
    <label for=mobile>Mobile number:</label>
    <input id=mobile name=mobile><br>
    <label for=fax>Fax:</label>
    <input id=fax name=fax><br>
    <label for=email>Email:</label>
    <input type=email id=email name=email><br>
    <label></label>
    <input type=submit>
  </form>
<?php
  if (isset($_POST['name'])) {
    if (Database::insert_address($_POST['name'], $_POST['address1'],
      $_POST['address2'], $_POST['city'], $_POST['country'], $_POST['post_code'],
      $_POST['phone'], $_POST['mobile'], $_POST['fax'], $_POST['email'])
    )
      header('Location: ' . 'https://' . $_SERVER['HTTP_HOST']
        . dirname(htmlspecialchars($_SERVER['PHP_SELF'])) . '/main.php');
    else echo 'Insert failed!';
  }
  include 'footer.php';
?>

10.4.11. edit.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
  require_once 'database.php';
  require_once 'bouncer.php';
  include 'header.php';
?>
<?php
  if (isset($_GET['id'])) {
    $result = Database::get_addresses($_GET['id']);
    if ($result) {
      $address = $result[0];
      echo <<<_END
      <form method=post action={$_SERVER['SCRIPT_NAME']}>
        <input type=hidden name=id value="{$_GET['id']}">
        <label for=name>Name:</label>
        <input id=name name=name required value="{$address['name']}"><br>
        <label for=address1>Address line 1:</label>
        <input id=address1 name=address1 required value="{$address['address1']}">
        <br>
        <label for=address2>Address line 2:</label>
        <input id=address2 name=address2 value="{$address['address2']}"><br>
        <label for=city>City:</label>
        <input id=city name=city required value="{$address['city']}"><br>
        <label for=country>Country:</label>
        <input id=country name=country required value="{$address['country']}"><br>
        <label for=post_code>Post code:</label>
        <input id=post_code name=post_code required
        value="{$address['post_code']}"><br>
        <label for=phone>Phone number:</label>
        <input id=phone name=phone value="{$address['phone']}"><br>
        <label for=mobile>Mobile number:</label>
        <input id=mobile name=mobile value="{$address['mobile']}"><br>
        <label for=fax>Fax:</label>
        <input id=fax name=fax value="{$address['fax']}"><br>
        <label for=email>Email:</label>
        <input id=email name=email value="{$address['email']}"><br>
        <label></label>
        <input type=submit>
      </form>
_END;
    }
  }

  elseif (isset($_POST['name'])) {
    if (Database::update_address($_POST['id'], $_POST['name'], $_POST['address1'],
      $_POST['address2'], $_POST['city'], $_POST['country'], $_POST['post_code'],
      $_POST['phone'], $_POST['mobile'], $_POST['fax'], $_POST['email'])
    )
      header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
        dirname($_SERVER['PHP_SELF']) . '/main.php');
    else echo 'Update failed!';
  }
  include 'footer.php';
?>

10.4.12. delete.php

1
2
3
4
5
6
7
<?php
  require_once 'database.php';
  require_once 'bouncer.php';
  if (isset($_GET['id'])) Database::delete_address($_GET['id']);
  header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
    dirname($_SERVER['PHP_SELF']) . '/main.php');
?>

10.4.13. style.css

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
body {
  background-color: black;
  color:            gold;
}

#loginForm {
  width:        400px;
  margin-left:  auto;
  margin-right: auto;
}

label {
  float:         left;
  clear:         left;
  width:         120px;
  text-align:    right;
  padding-right: 10px;
  margin-top:    10px;
}

input {
  margin-top: 10px;
}

#loginInput {
  width:        135px;
  margin-right: auto;
  margin-left: auto;
}

.threeD {
  font-family: 'Bookman Old Style', serif;
  line-height: 1em;
  color:       gold;
  font-weight: bold;
  font-size:   90px;
  text-shadow: 0px 0px 0 rgb(190, 215, 1), 1px 1px 0 rgb(176, 201, -13),
         2px 2px 0 rgb(161, 186, -28), 3px 3px 0 rgb(146, 171, -43),
         4px 4px 0 rgb(131, 156, -58), 5px 5px 0 rgb(117, 142, -72),
         6px 6px 0 rgb(102, 127, -87), 7px 7px 0 rgb(87, 112, -102),
         8px 8px 0 rgb(72, 97, -117), 9px 9px 0 rgb(58, 83, -131),
         10px 10px 0 rgb(43, 68, -146), 11px 11px 0 rgb(28, 53, -161),
         12px 12px 0 rgb(13, 38, -176), 13px 13px 12px rgba(0, 0, 0, 0.9),
         13px 13px 1px rgba(0, 0, 0, 0.5), 0px 0px 12px rgba(0, 0, 0, .2);
}

@keyframes logoRotate {
  50% {
    transform:         rotateY(180deg);
    -webkit-transform: rotateY(180deg);
  }
  100% {
    transform:         rotateY(0deg);
    -webkit-transform: rotateY(0deg);
  }
}

@-webkit-keyframes logoRotate {
  50% {
    transform:         rotateY(180deg);
    -webkit-transform: rotateY(180deg);
  }
  100% {
    transform:         rotateY(0deg);
    -webkit-transform: rotateY(0deg);
  }
}

#logo {
  animation:         logoRotate 60s infinite;
  /* Safari and Chrome: */
  -webkit-animation: logoRotate 60s infinite;
}

form label {
  display:     inline-block;
  width:       160px;
  font-weight: bold;
}

.address {
  border:        2px black ridge;
  margin-bottom: 10px;
}

a {
  outline:         none;
  text-decoration: none;
  margin:          5px;
  color:           lightblue;
}

a:hover {
  background-color: lightskyblue;
  color:            white;
}

10.5. WMOTU Tank

We will develop a tank game, where the player battles against computer controlled tanks of increasing intelligence on different playing fields.

The tank body and gun have been created with Inkscape and saved as optimized SVG using the following settings:

Inkscape optimized SVG

10.5.1. index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>WMOTU Tank v0.01a</title>
    <meta charset=utf-8>
    <script src=tank.js></script>
  </head>
  <body>
    <main>
      <canvas width=1000 height=700>This browser does not run this game
        (canvas support missing).
      </canvas>
    </main>
  </body>
</html>

10.5.2. tank.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
"use strict";

var game;

function init() {
  game = {
    canvasWidth: 1000,
    canvasHeight: 700,
    canvas: document.querySelector('canvas'),
    context: document.querySelector('canvas').getContext('2d'),
    tankSVG: undefined,
    gunSVG: undefined,
    player: undefined,
    bot1: undefined,
    pauseButton: false,
    invisible: false,
    rotateLeft: false,
    rotateRight: false,
    moveForward: false,
    moveBackward: false,
    rotateGunLeft: false,
    rotateGunRight: false,
    timeOfLastShot: 0,
    score: ~0,
    currLevel: 1,
    lastName: "",
    lastAnimationTime: 0,
    currPlayerSpeed: 5, // pixels per key event
    currPlayerRotationSpeed: 2, // degrees per key event
    currPlayerGunRotationSpeed: 4, // degrees per key event
    keys: {
      LEFT: 37,
      UP: 38,
      RIGHT: 39,
      DOWN: 40,
      ESC: 27,
      SPACE: 32,
      B: 66,
      N: 78,
      P: 80
    },

    Tank: function (centerX, centerY, bodyColor, gunColor, turretColor) {
      this.centerX = centerX;
      this.centerY = centerY;
      this.bodyColor = bodyColor;
      this.gunColor = gunColor;
      this.turretColor = turretColor;
      this.angle = 33;
      this.gunAngle = 0; //200;
      this.tankImage = new Image();
      this.gunImage = new Image();
      this.lastAnimationTime = 0;
      game.tankSVG.getElementById('tankBody').style.fill = this.bodyColor;
      var svgAsString = new XMLSerializer().serializeToString(game.tankSVG);
      this.tankImage.src = 'data:image/svg+xml;base64,' + btoa(svgAsString);
      game.gunSVG.getElementById('gun').style.fill = this.gunColor;
      game.gunSVG.getElementById('turret').style.fill = this.turretColor;
      svgAsString = new XMLSerializer().serializeToString(game.gunSVG);
      this.gunImage.src = 'data:image/svg+xml;base64,' + btoa(svgAsString);
      this.draw();
    },

    Player: function (posX, posY, bodyColor, gunColor, turretColor) {
      game.Tank.call(this, posX, posY, bodyColor, gunColor, turretColor);
    },

    Bot: function (centerX, centerY, bodyColor, gunColor, turretColor) {
      game.Tank.call(this, centerX, centerY, bodyColor, gunColor, turretColor);
    },

    handleKeyDown: function (event) {
      if (event.keyCode === this.keys.P) {
        this.pauseButton = !this.pauseButton; // P pressed
        this.lastAnimationTime = this.timestamp();
      }
      else if (event.keyCode === this.keys.ESC) {// ESC
        this.loseLife = true;
        this.numOfLives = 0;
      }
      else if (event.keyCode === this.keys.SPACE) {// fire shot
        this.fireButton = true;
      }
      else if (event.keyCode === this.keys.LEFT) { // move left
        this.rotateLeft = true;
      }
      else if (event.keyCode === this.keys.RIGHT) { // move right
        this.rotateRight = true;
      }
      else if (event.keyCode === this.keys.UP) { // move up
        this.moveForward = true;
      }
      else if (event.keyCode === this.keys.DOWN) { // move down
        this.moveBackward = true;
      }
      else if (event.keyCode === this.keys.B) { // move down
        this.rotateGunLeft = true;
      }
      else if (event.keyCode === this.keys.N) { // move down
        this.rotateGunRight = true;
      }
    },

    handleKeyUp: function (event) {
      if (event.keyCode === this.keys.FIRE) {// fire shot
        this.fireButton = false;
      }
      else if (event.keyCode === this.keys.LEFT) { // move left
        this.rotateLeft = false;
      }
      else if (event.keyCode === this.keys.RIGHT) { // move right
        this.rotateRight = false;
      }
      else if (event.keyCode === this.keys.UP) { // move up
        this.moveForward = false;
      }
      else if (event.keyCode === this.keys.DOWN) { // move down
        this.moveBackward = false;
      }
      else if (event.keyCode === this.keys.B) { // move down
        this.rotateGunLeft = false;
      }
      else if (event.keyCode === this.keys.N) { // move down
        this.rotateGunRight = false;
      }
    },

    handleVisibilityChange: function () {
      if (this.invisible) this.lastAnimationTime = this.timestamp();
      this.invisible = !this.invisible;
    },

    // http://codeincomplete.com/posts/2013/12/4/javascript_game_foundations_the_game_loop
    timestamp: function () {
      return window.performance && window.performance.now ?
        window.performance.now() : new Date().getTime();
    },

    init: function () {
      // Load the tank and gun SVGs asynchronously.
      var req1 = new XMLHttpRequest(), req2 = new XMLHttpRequest(), filesRemaining = 2;

      function r1() {
        this.tankSVG = req1.responseXML;
        if (--filesRemaining === 0) this.init2();
      }

      req1.onload = r1.bind(this);
      req1.open('get', 'tank_o.svg');
      req1.send();

      function r2() {
        this.gunSVG = req2.responseXML;
        filesRemaining--;
        if (filesRemaining === 0) this.init2();
      }

      req2.onload = r2.bind(this);
      req2.open('get', 'gun_o.svg');
      req2.send();
    },

    init2: function () {
      window.addEventListener('keydown', this.handleKeyDown.bind(this));
      window.addEventListener('keyup', this.handleKeyUp.bind(this));
      document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
      this.clearCanvas();
      this.Player.prototype = Object.create(this.Tank.prototype);
      this.Bot.prototype = Object.create(this.Tank.prototype);
      this.player = new this.Player(100, 100, '#000000', '#222222', '#00ff00');
      //this.player.draw();
      this.bot1 = new this.Bot(200, 200, '#0000ff', '#2222ff', '#00ccff');
      this.bot1.angle = 0;
      this.bot1.gunAngle = 45;
      //this.bot1.draw();
      this.lastAnimationTime = this.timestamp();
      requestAnimationFrame(this.gameLoop.bind(this));
    },

    clearCanvas: function () {
      this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
    },

    gameLoop: function (currTime) { // paused -> do nothing
      if (this.pauseButton) requestAnimationFrame(this.gameLoop.bind(this));
      else {
        /*this.timeElapsed = currTime - this.lastAnimationTime;
         if (this.lastAnimationTime === 0) this.adjustmentFactor = 0.2;
         else this.adjustmentFactor = this.timeElapsed / 100;*/
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // If space key pressed and enough time has elapsed, we shoot again.
        /*if (this.fireButton && ((currTime - this.timeOfLastShot) > this.minFireThreshold)) {
         this.timeOfLastShot = currTime;
         }
         if (this.moveLeft) { // If left arrow key pressed, move spaceship to the left.
         if (this.currPlayerX > this.currPlayerSpeed)
         this.currPlayerX -= this.currPlayerSpeed * this.adjustmentFactor;
         else this.currPlayerX = 1;
         }
         if (this.moveRight) { // If right arrow key pressed, move spaceship to the right.
         if ((this.currPlayerX + this.imagePlayer.width) < (this.canvas.width -
         this.currPlayerSpeed))
         this.currPlayerX += this.currPlayerSpeed * this.adjustmentFactor;
         else this.currPlayerX = this.canvas.width - this.imagePlayer.width - 1;
         }*/
        if (this.rotateLeft) this.player.rotate(-this.currPlayerRotationSpeed);
        if (this.rotateRight) this.player.rotate(this.currPlayerRotationSpeed);
        if (this.moveForward) this.player.move(Math.round(Math.sin(this.player.angle *
          Math.PI / 180) * this.currPlayerSpeed),
          Math.round(Math.cos(this.player.angle * Math.PI / 180) * -this.currPlayerSpeed));
        if (this.moveBackward) this.player.move(Math.round(Math.sin(this.player.angle *
          Math.PI / 180) * -this.currPlayerSpeed),
          Math.round(Math.cos(this.player.angle * Math.PI / 180) *
          this.currPlayerSpeed));
        if (this.rotateGunLeft) this.player.rotateGun(-this.currPlayerGunRotationSpeed);
        if (this.rotateGunRight) this.player.rotateGun(this.currPlayerGunRotationSpeed);
        this.clearCanvas();
        this.player.draw();
        this.bot1.draw();
        this.lastAnimationTime = this.timestamp();
        requestAnimationFrame(this.gameLoop.bind(this));
      }
    }
  };

  game.Tank.prototype = {
    width: 44, // Tank width in pixels.
    height: 52, // Tank height in pixels.
    gunWidth: 24, // Gun width in pixels.
    gunHeight: 52, // Gun height in pixels.
    constructor: game.Tank,
    draw: function () { // Draw tank and gun at the current position and angles.
      game.context.save();
      // Draw a black border around canvas.
      game.context.strokeStyle = '#000';
      game.context.strokeRect(0, 0, game.canvas.width, game.canvas.height);
      game.context.translate(this.centerX + this.width / 2,
        this.centerY + this.height / 2);
      game.context.save();
      game.context.rotate((this.angle * Math.PI) / 180);
      game.context.drawImage(this.tankImage, -this.width / 2, -this.height / 2);
      game.context.restore();
      game.context.rotate((this.gunAngle * Math.PI) / 180);
      game.context.drawImage(this.gunImage, -this.gunWidth / 2, -1 - (this.height +
      this.gunWidth) / 2);
      game.context.restore();
    },

    // Returns true if moving and/or rotating the gun by the given values would
    // cause a border collision.
    gunCheckBorderCollision: function (dX, dY, angle) {
      return false;
    },

    // Returns true if moving and/or rotating the player by the given values would
    // cause a border collision.
    tankCheckBorderCollision: function (dX, dY, angle) {
      //result.Y = (int)Math.Round( centerPoint.Y + distance * Math.Sin( angle ) );
      //result.X = (int)Math.Round( centerPoint.X + distance * Math.Cos( angle ) );
      var alpha = ((this.angle + angle) % 360) * Math.PI / 180;
      var cos = Math.cos(alpha), sin = Math.sin(alpha), tan = Math.tan(alpha);
      var w = this.width, h = this.height;
      var distance = Math.sqrt(Math.pow(w / 2, 2) + Math.pow(h / 2, 2));
      var centerX = this.centerX + this.width / 2;
      var centerY = this.centerY + this.height / 2;
      /*var topY = this.posY - diagonal * Math.cos(angle);
       var bottomY = this.posY + this.height + diagonal * Math.cos(angle);
       */
      var leftX = centerX - (sin * h + cos * w) / 2;
      var rightX = centerX + (sin * h + cos * w) / 2;
      /*console.log('topY: ' + topY);
       console.log('bottomY: ' + bottomY);

       console.log('centerX: ' + centerX);
       console.log('centerY: ' + centerY);
       console.log('leftX: ' + leftX);
       console.log('rightX: ' + rightX);*/
      var x = sin * w / 2;
      console.log('x: ' + x);
      return false;
    },

    move: function (dX, dY) {
      if (!this.tankCheckBorderCollision(dX, dY, 0)) {
        this.centerX += dX;
        this.centerY += dY;
      }
    },

    rotate: function (angle) {
      if (!this.tankCheckBorderCollision(0, 0, angle))
        this.angle = (this.angle + angle) % 360;
    },

    rotateGun: function (angle) {
      this.gunAngle = (this.gunAngle + angle) % 360;
    }
  };

  game.init();
}

addEventListener('load', init);

10.6. Web Note

WebNote1

10.6.1. Requirements specification

We often come across information on the web that we would like to store in order to access it from anywhere at a later point in time. Web Note allows registered users to create timestamped notes with editable HTML content.

The following requirements need to be met:

  • Secure user sign up, login, logout and password change.

  • Timestamps and names of all user notes are displayed. Clicking on one of them will display the note name and content together with edit and delete functionality.

  • A new note with name and HTML content can be added with the current timestamp generated automatically or the new note can be discarded.

  • High speed app with no page reload except for login/logout.

10.6.2. Analysis

This app consists of the following parts:

  • A MySQL DB to store users and notes.

  • A PHP backend to manage the DB operations.

  • A PHP AJAX API, providing JavaScript access to the PHP backend.

  • A HTML5/CSS3/JavaScript frontend.

10.6.3. Design and implementation

10.6.3.1. DB
dbWebNote1
10.6.3.1.1. createDB.sql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
DROP DATABASE IF EXISTS evegi144_dbWebNote;
CREATE DATABASE evegi144_dbWebNote
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;
USE evegi144_dbWebNote;

CREATE TABLE tblUser (
  idUser     INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  dtUserName VARCHAR(32)  NOT NULL UNIQUE,
  dtPassword VARCHAR(255) NOT NULL
)
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;

CREATE TABLE tblNote (
  idNote    INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  dtContent LONGBLOB     NOT NULL,
  dtName    VARCHAR(255) NOT NULL,
  dtDate    TIMESTAMP    NOT NULL    DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  fiUser    INT UNSIGNED NOT NULL,
  FOREIGN KEY (fiUser) REFERENCES tblUser (idUser)
    ON DELETE CASCADE
    ON UPDATE CASCADE
)
  ENGINE = INNODB
  DEFAULT CHARSET utf8
  DEFAULT COLLATE utf8_bin;
10.6.3.2. Backend

The backend provides a Database class with the following functionality:

  • set_credentials

  • connect

  • get_user_name

  • insert_note

  • update_note

  • delete_note

  • get_note

  • get_notes

  • change_password

  • login

  • create_user

10.6.3.2.1. Database.php
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<?php
  require_once 'db_credentials.php';

  class Database {
    private static $DB_HOST;
    private static $DB_USER;
    private static $DB_PASSWORD;
    private static $DB_NAME;

    static function set_credentials($db_host, $db_user, $db_password, $db_name) {
      self::$DB_HOST     = $db_host;
      self::$DB_USER     = $db_user;
      self::$DB_PASSWORD = $db_password;
      self::$DB_NAME     = $db_name;
    }

    static function connect() {
      $dbc = @new mysqli(self::$DB_HOST, self::$DB_USER, self::$DB_PASSWORD, self::$DB_NAME);
      if ($dbc->connect_error) die('Database connection failed: ' . $dbc->connect_error);
      $dbc->set_charset("utf8");
      return $dbc;
    }

    static function get_user_name($user_id) {
      $dbc   = self::connect();
      $query = "SELECT dtUserName FROM tblUser WHERE idUser = $user_id";
      $result = $dbc->query($query) or die('Error reading from DB.' . $dbc->error);
      if ($result) return $result->fetch_row()[0];
      return false;
    }

    static function insert_note($note, $user_id) {
      $dbc     = self::connect();
      $content = $note[0];
      $name    = $note[1];
      $query   = "INSERT INTO tblNote(dtName, dtContent, fiUser) VALUES ('$name', '$content',
          $user_id)";
      $result = $dbc->query($query) or die('Error inserting into DB.' . $dbc->error);
    }

    static function update_note($note, $user_id) {
      $dbc     = self::connect();
      $content = $note[0];
      $name    = $note[1];
      $id      = $note[2];
      $query   = "UPDATE tblNote set dtName = '$name', dtContent = '$content' WHERE
        idNote = $id";
      $result = $dbc->query($query) or die('Error inserting into DB.' . $dbc->error);
    }

    static function delete_note($note_id) {
      $dbc   = self::connect();
      $query = "DELETE FROM tblNote WHERE idNote = $note_id";
      $result = $dbc->query($query) or die('Error deleting DB.' . $dbc->error);
    }

    // Return array with all notes for user.
    static function get_notes($user_id) {
      $dbc   = self::connect();
      $query = "SELECT idNote, dtName, dtContent, dtDate FROM
        tblNote WHERE fiUser = $user_id ORDER BY dtDate DESC";
      $result = $dbc->query($query) or die('Error reading from DB.' . $dbc->error);
      if ($result) {
        $all_rows = [];
        while ($row = $result->fetch_assoc()) $all_rows[] = $row;
        return $all_rows;
      }
      return false;
    }

    static function change_password($user_id, $old_pw, $new_pw) {
      $dbc   = self::connect();
      $query = "SELECT dtPassword FROM tblUser";
      $result = $dbc->query($query) or trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      if ($result && $result->num_rows === 1) error_log($result->fetch_assoc()['dtPassword']);
      $query = "SELECT COUNT(*) FROM tblUser WHERE idUser = $user_id AND dtPassword = '"
        . password_hash($old_pw, PASSWORD_DEFAULT) . "'";
      error_log($query);
      $result = $dbc->query($query) or trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      $result = false;
      if ($result && $result->fetch_row()[0] === 1) {
        $query = 'UPDATE tblUser SET dtPassword = "' . password_hash($new_pw,
            PASSWORD_DEFAULT) . '"';
        error_log($query);
        $result = $dbc->query($query) or trigger_error('Wrong SQL: ' . $query . ' Error: ' .
          $dbc->error, E_USER_ERROR);
      }
      $dbc->close();
      return $result;
    }

    // Returns user id or FALSE.
    static function login($user_name, $password) {
      $dbc   = self::connect();
      $query = 'SELECT idUser, dtPassword FROM tblUser WHERE dtUserName = "' .
        $dbc->real_escape_string(($user_name)) . '"';
      $result = $dbc->query($query) or trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);
      if ($result && $result->num_rows === 1) {
        $res = $result->fetch_assoc();
        if (password_verify($password, $res['dtPassword'])) return $res['idUser'];
      }
      return false;
    }

    # Returns FALSE if user could not be created, otherwise user id.
    static function create_user($user_name, $password) {
      $dbc   = self::connect();
      $query = 'INSERT INTO tblUser (dtUserName, dtPassword) VALUES("' .
        $dbc->real_escape_string($user_name) . '","' .
        password_hash($password, PASSWORD_DEFAULT) . '")';
      $result = $dbc->query($query);/* or trigger_error('Wrong SQL: ' . $query . ' Error: ' .
        $dbc->error, E_USER_ERROR);*/
      return $result;
    }
  }

?>
10.6.3.3. API

The API provides the following self-explanatory functionality:

  • get_notes

  • insert_note

  • update_note

  • delete_note

  • logout

  • change_password

API.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
  // API to be used via HTTP POST requests.
  require_once 'protected/bouncer.php';
  require_once 'protected/Database.php';

  if (isset($_POST['function'])) {
    if ($_POST['function'] === 'insert_note' && isset($_POST['parameter']))
      insert_note($_POST['parameter']);
    elseif ($_POST['function'] === 'update_note' && isset($_POST['parameter']))
      update_note($_POST['parameter']);
    elseif ($_POST['function'] === 'delete_note' && isset($_POST['parameter']))
      delete_note($_POST['parameter']);
    elseif ($_POST['function'] === 'logout') logout();
    elseif ($_POST['function'] === 'change_password' && isset($_POST['parameter'])) {
      $parms = json_decode($_POST['parameter']);
      change_password($parms[0], $parms[1]);
    }
    elseif ($_POST['function'] === 'get_notes') get_notes();
  }

  function get_notes() {
    echo json_encode(Database::get_notes($_SESSION['user_id']));
  }

  function insert_note($note) {
    Database::insert_note(json_decode($note), $_SESSION['user_id']);
  }

  function update_note($note) {
    Database::update_note(json_decode($note), $_SESSION['user_id']);
  }

  function delete_note($note_id) {
    Database::delete_note($note_id);
  }

  function logout() {
    require_once 'logout.php';
  }

  function change_password($old_pw, $new_pw) {
    if (Database::change_password($_SESSION['user_id'], $old_pw, $new_pw))
      echo 'Password changed successfully';
    else echo 'Password change failed';
  }
?>
10.6.3.4. Frontend

First we have the usual login, logout and bouncer scripts:

index.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
  ini_set('session.cookie_secure', true);
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  if (!isset($_SERVER['HTTPS'])) // If SSL is not active, activate it.
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']));
  if (!isset($_SESSION)) session_start();
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  if (isset($_SESSION['user_id']))
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/main.php');
  elseif (isset($_POST['username'], $_POST['password'])) {
    require_once 'protected/Database.php';
    $result = Database::login($_POST['username'], $_POST['password']);
    if ($result) {
      $_SESSION['user_id'] = $result;
      $_SESSION['user_name'] = $_POST['username'];
      header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
        dirname($_SERVER['PHP_SELF']) . '/main.php');
    }
  } elseif (isset($_POST['register'])) {
    require_once 'protected/Database.php';
    if (isset($_POST['username'], $_POST['pw1'], $_POST['pw2']) && $_POST['pw1'] ===
      $_POST['pw2']
    )
      if (Database::create_user($_POST['username'], $_POST['pw1']))
        echo "<script>alert('Registration succeeded, please log in!');</script>";
      else echo "<script>alert('Registration failed!');</script>";
  }
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Web Note</title>
    <meta charset=UTF-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <link href=index.css rel=stylesheet>
  </head>
  <body>
    <main>
      <form method=post>
        <input name=username placeholder="User name" required autofocus>
        <input type=password name=password placeholder=Password required>
        <button name=login>Log in</button>
      </form>
      <form method=post>
        <input name=username placeholder="User name" required>
        <input name=pw1 type=password placeholder=Password required>
        <input name=pw2 type=password placeholder="Repeat password" required>
        <button name=register>Register</button>
      </form>
    </main>
  </body>
</html>
10.6.3.4.1. index.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
body {
  background:  linear-gradient(to bottom right, yellow, #772222) fixed;
  text-shadow: 1px 1px 1px white;
  margin:      0;
}

main {
  position: absolute;
  top:      0;
  left:     0;
  right:    0;
  bottom:   0;
  display:  flex;
}

form {
  width:  200px;
  margin: auto;
}

form > input, form > button {
  margin-top:    10px;
  text-shadow:   1px 1px 1px white;
  border-radius: 5px;
}

form > input {
  width:   196px;
  opacity: 0.5;
}

form > input:focus {
  background-color: yellow;
}

form > button {
  background: linear-gradient(to bottom right, yellow, red);
}

form > button:focus {
  border: 2px solid grey;
}

form > input::-moz-focus-inner {
  border: 0;
}
10.6.3.4.2. logout.php
1
2
3
4
5
6
7
8
9
<?php
  if (!isset($_SESSION)) session_start();
  $_SESSION = [];
  if (session_id() != "" || isset($_COOKIE[session_name()])) setcookie(session_name(),
    '', 1, '/');
  session_destroy();
  header('Location: https://' . $_SERVER['HTTP_HOST'] .
    dirname($_SERVER['PHP_SELF']) . '/index.php');
?>
10.6.3.4.3. bouncer.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
  # Only send session id cookie over SSL.
  ini_set('session.cookie_secure', true);
  # Session IDs may only be passed via cookies, not appended to URL.
  ini_set('session.use_only_cookies', true);
  ini_set('session.cookie_path', rawurlencode(dirname($_SERVER['PHP_SELF'])));
  if (!isset($_SERVER['HTTPS'])) // # SSL is not active, activate it.
    header('Location: https://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']));
  if (!isset($_SESSION)) session_start();
  # After 30 seconds we'll generate a new session ID to prevent a session
  # fixation attack (cf. PHP cookbook p. 338).
  if (!isset($_SESSION['generated']) || $_SESSION['generated'] < (time() - 30)) {
    session_regenerate_id();
    $_SESSION['generated'] = time();
  }
  if (!isset($_SESSION['user_id']))
    header('Location: ' . 'https://' . $_SERVER['HTTP_HOST'] .
      dirname($_SERVER['PHP_SELF']) . '/index.php');
?>

The frontend defines an object main, with the following functionality:

  • init attaches event listeners and gets all notes from the DB.

  • AJAXFunctionCall is a helper function to call the API asynchronously via AJAX.

  • getNotesFromDB retrieves the user’s notes from the DB via the API and displays them.

  • showNote displays a specific note.

  • deleteNote deletes the currently shown note.

  • editNote opens the editor for the currently shown note.

  • cancelEdit closes the editor without saving.

  • saveNote closes the editor and saves the note.

  • newNote opens an empty editor.

  • insertNote saves the new note.

  • discardNewNote closes the editor without saving.

  • changePW displays the password change form.

The editor component CKEditor (ckeditor.com) is used.

10.6.3.4.4. main.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
  require_once 'protected/bouncer.php';
  require_once 'protected/Database.php';
?>
<!DOCTYPE html>
<html lang=en>
  <head>
    <title>Web Note</title>
    <meta charset=UTF-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <link href=main.css rel=stylesheet>
    <script src="//cdn.ckeditor.com/4.4.7/full/ckeditor.js"></script>
    <script src=main.js type=module></script>
  </head>
  <body>
    <nav>
      <ul>
        <li><a>Change PW</a></li>
        <li><a>Logout <?php echo $_SESSION['user_name']; ?></a></li>
        <li><a>New note</a></li>
        <li><a>Refresh</a></li>
      </ul>
    </nav>
    <main>
      <nav><ol></ol></nav>
      <section id=showNoteSection hidden></section>
      <section id=newNoteSection hidden>
        <input id=name placeholder=Name>
        <button>Save</button>
        <button>Discard</button>
        <textarea id=newNoteTA></textarea>
      </section>
      <article id=changePWForm hidden>
        <input type=password name=curr_pw placeholder="Current password" required
               autofocus autocomplete=off>
        <input pattern=.{8,} type=password name=new_pw1 placeholder="New password" required>
        <input pattern=.{8,} type=password name=new_pw2 placeholder="New password" required>
        <button>Change password</button>
      </article>
    </main>
  </body>
</html>
10.6.3.4.5. main.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
const main = { // main app object
  notes: undefined,
  shownNoteId: undefined,
  newNoteEditor: undefined,
  editNoteEditor: undefined,

  init: function () {
    const button = document.querySelector('#changePWForm > button')
    button.addEventListener('click', function (e) {
      // Create a new form and a new XMLHttpRequest.
      const inputs = document.querySelectorAll('#changePWForm > input')
      const currPWInput = inputs[0].value
      const newPW1Input = inputs[1].value, newPW2Input = inputs[2].value
      if (newPW1Input.length >= 8 && newPW2Input.length >= 8 && newPW1Input === newPW2Input) {
        main.AJAXFunctionCall('change_password', JSON.stringify([currPWInput, newPW1Input]),
          function (e) {
            alert(e.target.response)
          })
        // Open the HTTP connection to the server script using POST method.
        document.getElementById('changePWForm').setAttribute('hidden', '')
      } else alert('Passwords don\'t match or are empty')
    })

    // Add top navigation event listeners.
    const lis = document.querySelectorAll('body > nav > ul > li')
    lis[0].addEventListener('click', main.changePW)
    lis[1].addEventListener('click', function () {
      window.location = 'logout.php'
    })
    lis[2].addEventListener('click', main.newNote)
    lis[3].addEventListener('click', main.getNotesFromDB)

    // Add newNoteSection button event listeners.
    const buttons = document.querySelectorAll('#newNoteSection > button')
    buttons[0].addEventListener('click', main.insertNote)
    buttons[1].addEventListener('click', main.discardNewNote)

    //window.addEventListener('beforeunload', main.unload);
    main.getNotesFromDB()
  },

  /*unload: function () {
   main.AJAXFunctionCall('logout');
   },*/

  AJAXFunctionCall: function (functionName, parameter, callback) {
    const req = new XMLHttpRequest()
    const data = new FormData()
    data.append('function', functionName)
    data.append('parameter', parameter)
    req.open('POST', 'API.php')
    if (callback) req.addEventListener('load', callback)
    req.send(data)
  },

  getNoteWithId: function (id) {
    for (let i = 0; i < main.notes.length; i++)
      if (main.notes[i].idNote === id) return main.notes[i]
  },

  getNotesFromDB: function () {
    main.AJAXFunctionCall('get_notes', '', function (e) {
      main.notes = JSON.parse(e.target.response)

      function f1(i) {
        return function () {
          main.showNote(i)
        }
      }

      // List note names.
      const ol = document.querySelector('main > nav > ol')
      ol.innerHTML = ''
      ol.style.margin = '0'
      for (let i = 0; i < main.notes.length; i++) {
        main.notes[i].idNote = parseInt(main.notes[i].idNote)
        main.notes[i].dtContent = decodeURIComponent(main.notes[i].dtContent)
        const li = document.createElement('li')
        const a = document.createElement('a')
        const t = main.notes[i].dtDate.split(/[- :]/)
        const d = new Date(t[0], t[1] - 1, t[2], parseInt(t[3]) -
          (new Date().getTimezoneOffset()) / 60 || 0, t[4] || 0, t[5] || 0, 0)
        const day = d.getDate() > 9 ? "" + d.getDate() : "0" + d.getDate()
        const month = (d.getMonth() + 1) > 9 ? "" + (d.getMonth() + 1) : "0" + (d.getMonth() + 1)
        const hours = d.getUTCHours() > 9 ? "" + d.getUTCHours() : "0" + d.getUTCHours()
        const minutes = d.getMinutes() > 9 ? "" + d.getMinutes() : "0" + d.getMinutes()
        const seconds = d.getSeconds() > 9 ? "" + d.getSeconds() : "0" + d.getSeconds()
        const s = day + '.' + month + '.' + d.getFullYear() + ' ' +
          hours + ':' + minutes + ':' + seconds
        const l = 19 - s.length
        for (let x = 0; x < l; x++) s += '&nbsp;'
        a.innerHTML = s + ' ' + main.notes[i].dtName
        a.style.fontFamily = "Courier New, monospace"
        a.addEventListener('click', f1(main.notes[i].idNote))
        li.appendChild(a)
        ol.appendChild(li)
      }
      if (main.shownNoteId !== undefined) main.showNote(main.shownNoteId)
    })
  },

  showNote: function (noteId) {
    if (main.notes) {
      const section = document.getElementById('showNoteSection')
      section.innerHTML = ''
      const nav = document.createElement('nav')
      let button = document.createElement('button')
      button.innerHTML = 'Edit'
      button.addEventListener('click', main.editNote)
      nav.appendChild(button)
      button = document.createElement('button')
      button.innerHTML = 'Delete'
      button.addEventListener('click', main.deleteNote)
      nav.appendChild(button)
      const span = document.createElement('span')
      const note = main.getNoteWithId(noteId)
      span.innerHTML = note.dtName
      span.style.marginLeft = '10px'
      nav.appendChild(span)
      section.appendChild(nav)
      const p = document.createElement('p')
      p.style.cssText = 'margin: 0; overflow: auto;'
      section.style.cssText = 'overflow: hidden; display: flex; flex-flow: column;'
      p.innerHTML = note.dtContent
      section.appendChild(p)
      section.removeAttribute('hidden')
      main.shownNoteId = noteId
    }
  },

  deleteNote: function () {
    if (!confirm('Do you really want to delete this note?')) return
    const section = document.getElementById('showNoteSection')
    const children = section.children
    for (let i = 0; i < children.length; i++)
      if (children[i].tagName !== 'BUTTON') section.removeChild(children[i])
    const id = main.shownNoteId
    main.shownNoteId = undefined
    main.AJAXFunctionCall('delete_note', id, main.getNotesFromDB)
    const p = section.querySelector('p')
    if (p) section.removeChild(p)
    const div = section.querySelector('div')
    if (div) section.removeChild(div)
    section.setAttribute('hidden', '')
  },

  editNote: function () {
    const section = document.getElementById('showNoteSection')
    const span = section.querySelector('nav > span')
    if (span) section.querySelector('nav').removeChild(span)
    const p = section.querySelector('p')
    const input = document.createElement('input')
    const note = main.getNoteWithId(main.shownNoteId)
    input.value = note.dtName
    section.appendChild(input)
    main.editNoteEditor = CKEDITOR.appendTo('showNoteSection', {}, p.innerHTML)
    section.removeChild(p)
    let button = document.querySelector('#showNoteSection > nav > button')
    button.removeEventListener('click', main.editNote)
    button.innerHTML = 'Save'
    button.addEventListener('click', main.saveNote)
    button = document.createElement('button')
    button.innerHTML = 'Cancel'
    button.addEventListener('click', main.cancelEdit)
    section.querySelector('nav').appendChild(button)
  },

  cancelEdit: function () {
    const section = document.getElementById('showNoteSection')
    const input = section.querySelector('input')
    if (input) section.removeChild(input)
    const p = section.querySelector('p')
    if (p) section.removeChild(p)
    const div = section.querySelector('div')
    if (div) section.removeChild(div)
    section.setAttribute('hidden', '')
    const buttons = section.querySelectorAll('button')
    if (buttons.length > 0) {
      section.querySelector('nav').removeChild(buttons[buttons.length - 1])
      buttons[0].innerHTML = 'Edit'
      buttons[0].removeEventListener('click', main.saveNote)
      buttons[0].addEventListener('click', main.editNote)
    }
    main.editNoteEditor = undefined
    if (main.shownNoteId !== undefined) main.showNote(main.shownNoteId)
  },

  saveNote: function () {
    main.editNoteEditor.updateElement()
    const section = document.getElementById('showNoteSection')
    const name = section.querySelector('input').value
    if (name.length > 0) {
      main.AJAXFunctionCall('update_note',
        JSON.stringify([encodeURIComponent(main.editNoteEditor.getData()), name,
          main.shownNoteId]), main.getNotesFromDB)
      main.editNoteEditor.destroy()
      main.editNoteEditor = undefined
    } else alert('Note must have a name')
  },

  newNote: function () {
    document.getElementById('showNoteSection').innerHTML = ''
    if (!main.newNoteEditor)
      main.newNoteEditor = CKEDITOR.replace('newNoteTA'/*, {filebrowserUploadUrl: 'upload.php'}*/)
    main.newNoteEditor.setData('')
    document.querySelector('#newNoteSection').removeAttribute('hidden')
    document.getElementById('name').focus()
  },

  insertNote: function () {
    main.newNoteEditor.updateElement()
    const name = document.getElementById('name').value
    if (name.length > 0) {
      main.AJAXFunctionCall('insert_note',
        JSON.stringify([encodeURIComponent(main.newNoteEditor.getData()), name]),
        main.getNotesFromDB)
      main.discardNewNote()
    } else alert('Note must have a name')
  },

  discardNewNote: function () {
    document.querySelector('#newNoteSection').setAttribute('hidden', '')
    if (main.newNoteEditor) {
      main.newNoteEditor.destroy()
      main.newNoteEditor = undefined
      document.getElementById('name').value = ''
    }
  },

  changePW: function () {
    if (document.getElementById('changePWForm').hasAttribute('hidden'))
      document.getElementById('changePWForm').removeAttribute('hidden')
    else document.getElementById('changePWForm').setAttribute('hidden', '')
  }
}

main.init()
10.6.3.4.6. main.css
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
html, body {
  height:  100%;
  width:   100%;
  padding: 0;
  margin:  0;
}

body {
  background:  linear-gradient(to bottom right, yellow, #772222) fixed;
  text-shadow: 1px 1px 1px white;
  display:     flex;
  flex-flow:   column;
  overflow:    hidden;
}

body > main > nav {
  overflow: auto;
}

body > nav > ul {
  padding: 10px 0;
}

body > nav > ul > li {
  margin:  0;
  padding: 0;
  display: inline;
}

body > nav > ul > li > a {
  padding:          10px;
  border:           outset yellow;
  background-color: yellow;
  color:            blue;
  box-shadow:       3px 3px 3px black;
}

body > nav > ul > li > a:visited {
  color: blue;
}

body > nav > ul > li > a[active] {
  background-color: lightgreen;
  border:           inset yellow;
}

main {
  margin-top: 10px;
  display:    flex;
  flex:       auto;
  overflow:   hidden;
}

main > nav, main > #showNoteSection {
  display: flex;
  flex:    auto;
  padding: 5px;
}

main > #newNoteSection {
  clear: left;
}

main > nav > ol {
  list-style-type: none;
  padding:         0;
}

ul {
  margin:  0;
  padding: 0;
}

a {
  text-decoration:       none;
  /* stackoverflow.com/questions/826782/css-rule-to-disable-text-selection-highlighting */
  -webkit-touch-callout: none;
  -webkit-user-select:   none;
  -khtml-user-select:    none;
  -moz-user-select:      none;
  -ms-user-select:       none;
  /*user-select:           none;*/
}

a:hover {
  background: linear-gradient(to bottom right, yellow, red);
  cursor:     default;
}

#changePWForm {
  position:         fixed;
  top:              100px;
  left:             100px;
  padding:          10px;
  background-color: black;
}

#changePWForm > input:valid {
  background-color: lawngreen;
}

#changePWForm > input:invalid {
  background-color: red;
}

This chapter shows the works of the greatest WAD artists that attended my courses.

11.1. Bill Brinck

11.1.1. Publi

Publi1

11.2. Marco Campana

11.2.1. Pacman

Pacman1

11.3. Nicolas Detombes

11.3.1. Dodge The Asteroids

DodgeTheAsteroids1 This game, developed by Nicolas Detombes, initially as the final problem solution for module CLISS2 2012 and later on extended with additional functionality, proves convincingly that canvas animation can be done in real time without the usage of prefabricated images. All animated objects are created programmatically in real time. Advanced array usage, program structuring using functions, canvas programming, key event handling and collision detection are beautifully illustrated.

11.3.2. Angry Chickens

AngryChickens1 Another game developed by Nicolas Detombes that illustrates mouse handling and DOM manipulation in addition to many features of Dodge The Asteroids.

11.3.3. Square Invasion

SquareInvasion1 The third game developed by Nicolas Detombes, short and sweet.

11.4. Aleksandar Veselinovic and Steve Weiwert

11.4.1. TrésBien

These artists do not yet fully master the French language, but their JavaScript skills are impressive.

TresBien1

12. Resources

12.2. Editors, IDEs and tools

List of cloud IDEs

www.slant.co/topics/713/~best-cloud-ides

Syntax highlighters

webdesign.tutsplus.com/articles/25-syntax-highlighters-tried-and-tested--cms-23931

Online UML editor

www.umletino.com

www.atheos.io

12.2.1. Browsers

support.mozilla.org/en-US/kb/recovering-important-data-from-an-old-profile

Turn off control media playback via hardware media keys in Firefox

support.mozilla.org/en-US/questions/1301997

Disable hardware media key handling in Microsoft Edge

winaero.com/disable-hardware-media-key-handling-in-microsoft-edge

Track network traffic of each tab

addons.mozilla.org/en-US/firefox/addon/http-tracker

Restore classic download confirmation prompt in Mozilla Firefox

www.askvg.com/tip-restore-classic-download-confirmation-prompt-in-mozilla-firefox

12.3. HTML5

12.3.1. Web sites

The official working draft

www.w3.org/TR/html51

HTML: The Living Standard

developers.whatwg.org

w3schools.com

w3schools.com/html/html5_intro.asp

HTML-Seminar (German)

www.html-seminar.de

HTML Dog

htmldog.com

A Practical Guide to HTML & CSS

learn.shayhowe.com

Quackit

www.quackit.com

HTML5 Code

www.html5code.nl

Smashing Magazine

coding.smashingmagazine.com

Web Content Accessibility Guidelines

www.w3.org/WAI/intro/wcag.php

Extended list of ASCII and HTML entity codes

www.ascii-code.com

12.3.2. Books

Head First HTML and CSS, 2nd Edition

shop.oreilly.com/product/9780596159924.do

12.4. CSS3

12.4.1. Web sites

W3C Cascading Style Sheets home page

www.w3.org/Style/CSS

w3schools.com

www.w3schools.com/css/default.asp

Mozilla Developer Network CSS Reference

developer.mozilla.org/en-US/docs/Web/CSS/Reference

CSS Portal

www.cssportal.com

CSS3.info

www.css3.info

CSS-Tricks

css-tricks.com

CSSDeck

cssdeck.com

Codrops

tympanus.net/codrops

Web Animation Resources

dev.to/nickylewlew/web-animation-resources-413e

CSS3 & HTML5 (German)

css3-html5.de/html-css-lernen

HTML-Seminar (German)

www.html-seminar.de

The Future Of CSS: Experimental CSS Properties

coding.smashingmagazine.com/2011/05/11/the-future-of-css-experimental-css-properties

100 Awesome CSS/Javascript Plugins and Coding Techniques

www.topdesignmag.com/100-awesome-cssjavascript-plugins-and-coding-techniques

Nifty Modal Window Effects

tympanus.net/Development/ModalWindowEffects

CSS3Gen

css3gen.com

CSS3 Patterns Gallery

lea.verou.me/css3patterns

Zen BG

mudcu.be/bg

The Star Wars Intro in Pure CSS3

www.htmlgoodies.com/html5/client/the-star-wars-intro-in-pure-css3-thanks-to-3d-transform-animations.html#fbid=D79H_1jCDzk

CSS3, please!

css3please.com

CSS3 Generator

css3generator.com

HTML Dog

htmldog.com

12.4.2. Books

12.5. JavaScript

12.5.1. Web sites

CLISS site of Robert Fisch

ltam.fisch.lu/index.php?scan=CLISS1.T1IF

CLISS site of Laurent Haan

www.haan.lu/#cliss

JavaScript Tutorial

learn.javascript.info

JS Fiddle

www.jsfiddle.net

JS Utility

jsutility.pjoneil.net

hilite.me

hilite.me

Mozilla Developer Network

developer.mozilla.org/en-US/docs/JavaScript

JavaScript-Garden

bonsaiden.github.io/JavaScript-Garden

The Code Player

thecodeplayer.com

Web Education Community

www.w3.org/community/webed/wiki/Main_Page

Codecademy

www.codecademy.com

Eloquent JavaScript

eloquentjavascript.net

SELFHTML

de.selfhtml.org/javascript/index.htm

JavaScript Forum

forum.jswelt.de/javascript

HTML Goodies

www.htmlgoodies.com

The Complete Guide to Building HTML5 Games with Canvas & SVG

www.htmlgoodies.com/html5/client/the-complete-guide-to-building-html5-games-with-canvas-svg.html#fbid=Xq5JJBKUCda

HTML5 Game Development

www.udacity.com/course/cs255

WebPlatform.org

www.webplatform.org

HTML5 Canvas Tutorials

www.html5canvastutorials.com

Code inComplete

codeincomplete.com/archive

Dive into HTML5

diveintohtml5.info

HTML5 Demos and Examples

html5demos.com

OPENCLASSROOMS

fr.openclassrooms.com

tutorialspoint

www.tutorialspoint.com

JavaScript Kit

www.javascriptkit.com

HTML5 rocks

www.html5rocks.com

Script Tutorials

www.script-tutorials.com

Javascript Development Tools Resources

clausreinke.github.io/js-tools/resources.html

Coding Math

www.youtube.com/watch?v=zm9bqSSiIdo

Creative JavaScript

creativejs.com

w3schools how tos

www.w3schools.com/howto

Learn to code with interactive challenges

edabit.com

jsComplete

jscomplete.com

12.5.2. Books

JSbooks

jsbooks.revolunet.com

Learning JavaScript Design Patterns

addyosmani.com/resources/essentialjsdesignpatterns/book

19 Free Javascript Ebooks & Resources

www.getfreeebooks.com/19-free-javascript-ebooks-resources

Building Front-End Web Apps with Plain JavaScript

Eloquent JavaScript

eloquentjavascript.net

HTML5 for Masterminds

www.minkbooks.com

JavaScript The Definitive Guide

shop.oreilly.com/product/9780596805531.do

Learning Three.js: The JavaScript 3D Library for WebGL - Second Edition

https://www.packtpub.com/web-development/learning-threejs-javascript-3d-library-webgl -second-edition

Three.js Essentials

www.packtpub.com/web-development/threejs-essentials

WebGL Beginner’s Guide

www.packtpub.com/game-development/webgl-beginners-guide

WebGL Game Development

www.packtpub.com/game-development/webgl-game-development

HTML5 Games: Creating Fun with HTML5, CSS3, and WebGL

eu.wiley.com/WileyCDA/WileyTitle/productCd-1118855388.html

Foundation HTML5 Animation with JavaScript

www.apress.com/9781430236658

WebGL Programming Guide

sites.google.com/site/webglbook

Effective JavaScript

effectivejs.com

Learning jQuery Fourth Edition

www.packtpub.com/web-development/learning-jquery-fourth-edition

12.6. PHP

12.6.2. Books

PHP in a Nutshell

shop.oreilly.com/product/9780596100674.do

12.7. MySQL

12.7.1. Web sites

mysql.com

www.mysql.com

Essential MySQL Cheat Sheet

www.cheatography.com/guslong/cheat-sheets/essential-mysql

12.10. Scripts

Free programmers', webmasters' and security resources

www.thefreecountry.com

HotScripts

www.hotscripts.com

Gerry’s Script Library

www.gerd-tentler.de/tools

12.11. Programming

15 Free Games to Level Up Your Coding Skills

skillcrush.com/2017/04/03/free-coding-games

CodinGame

www.codingame.com

Code Combat

codecombat.com

Programming tutorials

www.java2s.com

Learn shell programming

www.learnshell.org

The Bash Guide

guide.bash.academy

Regular expressions

ihateregex.io

www.regular-expressions.info

regexr.com

12.13. Artificial intelligence

A Course in Machine Learning

ciml.info

Understanding Machine Learning: From Theory to Algorithms

www.cs.huji.ac.il/~shais/UnderstandingMachineLearning/copy.html

Hacker’s guide to neural networks

karpathy.github.io/neuralnets

Derivative rules

www.mathsisfun.com/calculus/derivatives-rules.html

webkid.io/blog/neural-networks-in-javascript

www.deeplearningbook.org

burakkanber.com/blog/machine-learning-in-other-languages-introduction

harthur.github.io/txjs-slides

github.com/josephmisiti/awesome-machine-learning

www.technologyreview.com/s/541276/deep-learning-machine-teaches-itself-chess-in-72-hours-plays-at-international-master

neuralnetworksanddeeplearning.com

github.com/cazala/synaptic/wiki/Neural-Networks-101

www.nytimes.com/2016/12/14/magazine/the-great-ai-awakening.html

tutorialzine.com/2017/04/10-machine-learning-examples-in-javascript

www.nanalyze.com/2017/03/free-artificial-intelligence-ai-software

The matrix cookbook

www2.imm.dtu.dk/pubdb/views/publication_details.php?id=3274

Building our own self-driving car

www.streetdrone.com

blog.otoro.net/2015/06/19/neural-network-generative-art

deeplearn.js

deeplearnjs.org

Browser learns to play Flappy Bird

github.com/xviniette/FlappyLearning

MLweb

mlweb.loria.fr

Machine learning comes to your browser via JS

www.infoworld.com/article/3212884/machine-learning/machine-learning-comes-to-your-browser-via-javascript.html

Run Keras models in the browser, with GPU support using WebGL

github.com/transcranial/keras-js

TensorFire is a framework for running neural networks in the browser, accelerated by WebGL.

tenso.rs

Neural network generative art javascript

blog.otoro.net/2015/06/19/neural-network-generative-art

RecurrentJS

github.com/karpathy/recurrentjs

medium.com/@francois.chollet/the-impossibility-of-intelligence-explosion-5be4a9eda6ec

Artificial intelligence index

www.aiindex.org

Mastering Chess and Shogi by Self-Play with a General Reinforcement Learning Algorithm

arxiv.org/abs/1712.01815

Audio samples from "Natural TTS Synthesis by Conditioning WaveNet on Mel Spectrogram Predictions"

google.github.io/tacotron/publications/tacotron2

Colah’s blog

colah.github.io

Jürgen Schmidhuber

people.idsia.ch/~juergen

The future of deep learning

blog.keras.io/the-future-of-deep-learning.html

YOLO (You only look once) real-time object detection

pjreddie.com/darknet

Elements of AI

www.elementsofai.com

12.14. Internet

Internet Assigned Numbers Authority

www.iana.org

Free DNS query and whois tools

dnsquery.org

Outgoing port tester

portquiz.net

Check DNS propagation

dnschecker.org

MX toolbox

mxtoolbox.com

www.nslookup.io

Global DNS propagation checker

dnsmid.com

digwebinterface.com

check-host.net

whois.domaintools.com

12.15. Security

Privacy tools

www.privacytools.io

20 Best Tips to Stay Anonymous and Protect Your Online Privacy

www.extremetech.com/internet/180485-the-ultimate-guide-to-staying-anonymous-and-protecting-your-privacy-online

Hacksplaining

www.hacksplaining.com

Open Web Application Security Project

www.owasp.org

Electronic Frontier Foundation

www.eff.org

Security Engineering — The Book

www.cl.cam.ac.uk/~rja14/book.html

Canary tokens

blog.thinkst.com/p/canarytokensorg-quick-free-detection.html

Cross-browser fingerprinting

www.ghacks.net/2017/02/14/researchers-develop-cross-browser-fingerprinting-technique

Penetration testing tools

www.softwaretestinghelp.com/penetration-testing-tools

DDOS protection

javapipe.com/blog/iptables-ddos-protection

JS crypto libraries

gist.github.com/jo/8619441

Browser Privacy Test

tenta.com/test

Hack a web app

sourceforge.net/projects/bwapp/files/bee-box

The best penetration testing tool for Windows

pentestbox.org

Example of a successful phishing attack on a Google extension

www.ghacks.net/2017/07/31/chrome-extension-copyfish-hijacked-remove-now

Free cyber security learning

www.cybrary.it

Online platform to test and advance your skills in cyber security

www.hackthebox.eu

Sonar scanner

sonarwhal.com/scanner

malwaretips.com/blogs/reimageplus-popup-virus

Privacy and security

restoreprivacy.com

Check Microsoft Office files for malware

github.com/decalage2/oletools

www.troyhunt.com/ive-just-launched-pwned-passwords-version-2

Windows 10 VPN Users at Big Risk of DNS Leak

proprivacy.com/privacy-news/warning-windows-10-vpn-users-big-risk-dns-leak

12.18. Public domain pictures

Creative Commons

search.creativecommons.org

Pixabay

pixabay.com

Pexels

www.pexels.com

StockSnap.io

stocksnap.io

FreeImages

www.freeimages.com

Getty Images

www.gettyimages.in

Unsplash

unsplash.com

SplitShire

www.splitshire.com

Morguefile

morguefile.com

Free photos

The Metropolitan Museum of Art

www.metmuseum.org/art/collection#!?perPage=20&showOnly=openaccess&sortBy=Relevance&sortOrder=asc&offset=0&pageSize=0

NASA Image and Video Library

images.nasa.gov

Free to use and reuse sets from the US Library of Congress

www.loc.gov/free-to-use

We scan and index the best free photos from the top stock sites, so that you can find that perfect image much, much quicker.

librestock.com

Search millions of royalty-free photos from around the web

app.pixelmob.co

12.19. Public domain music

digccmixter

dig.ccmixter.org

Bensound

www.bensound.com

Icompetech

incompetech.com

IMSLP

imslp.org

MUSOPEN

musopen.org

16,000 BBC sound effects

bbcsfx.acropolis.org.uk

Royalty free music for YouTube and social media, free to use even commercially.

www.chosic.com/free-music/all

freesound.org

pixabay.com/sound-effects

12.21. Internet references

devdocs.io

12.23. Search engines

Search engines

www.hongkiat.com/blog/100-alternative-search-engines-you-should-know

Non-profit library of millions of free books, movies, software, music, websites, and more

archive.org

fossbytes.com/google-alternative-best-search-engine

Search the world’s historical newspaper titles

www.elephind.com

Visual art search engine

museo.app

www.cloudwards.net/duckduckgo-alternative

PDF search engine

www.pdfdrive.com

PDF search engine

pdf-downloads.net

Search engine for students and researchers

www.refseek.com

Locating unique, trustworthy materials that you often can’t find anywhere except in a library.

www.worldcat.org

Providing researchers with access to millions of scientific documents from journals, books, series, protocols, reference works and proceedings.

link.springer.com

Bioline International is a not-for-profit scholarly publishing cooperative committed to providing open access to quality research journals published in developing countries.

www.bioline.org.br

Research Papers in Economics

repec.org

www.science.gov

Bielefeld academic search engine

www.base-search.net

Listing over 3 million free books on the Web

onlinebooks.library.upenn.edu

12.24. Office software

Zero knowledge realtime collaborative editor

github.com/xwiki-labs/cryptpad

ONLYOFFICE

support.microsoft.com/en-us/office/change-the-location-of-your-onedrive-folder-f386fb81-1461-40a7-be2c-712676b2c4ae

medium.com/@josegabrielortegac/how-to-synchronize-folders-from-different-drives-on-onedrive-f400395f203c

www.howtogeek.com/764727/libreoffice-vs-microsoft-office

12.24.1. Microsoft

12.26. Media editing

To convert an SVG to EMF, use the export function of OpenOffice or LibreOffice Draw.

Top video compressors

icecreamapps.com/blog/top-video-compressors

Reduce video size with Avidemux

filmora.wondershare.com/avidemux/reduce-video-size-with-avidemux.html

Free video editor designed for simple cutting, filtering and encoding tasks

avidemux.sourceforge.net

Free, open source, cross-platform video editor

shotcut.org

github.com/mifi/lossless-cut

Squoosh is an image compression web app that reduces image sizes through numerous formats

squoosh.app

Smart PNG and JPEG compression

tinypng.com

imagecompressor.io

moviemaker.minitool.com

www.shutterencoder.com

Tool for anonymizing photographs

everestpipkin.github.io/image-scrubber

Free online converter for 1000+ formats

www.converter365.com

PhotoDemon is a free, portable, open-source, fast, light and powerful photo editor for Windows

photodemon.org

Convert any media file to multiple formats

axiomui.github.io

windowsreport.com/image-compression-software-windows-10

Radical Image Optimization Tool

riot-optimizer.com

Generate video thumbnails

fraigo.github.io/online-video-thumbnail-generator

Excellent open source video transcoder

handbrake.fr

www.rapidseedbox.com/blog/guide-to-mastering-handbrake

handbrake.fr/docs/en/latest/technical/video-qsv.html

www.videoconverterfactory.com/tips/handbrake-m4v-to-mp4.html

Free Photoshop alternative

krita.org

www.freevideojoiner.com

onlinejpgtools.com

imageresizer.com

Free online photo editing. No signup, login or install

www4.lunapic.com/editor

12.27. Music

askubuntu.com/questions/10402/is-there-software-like-music-maker

ubuntustudio.org

LMMS open source digital audio workstation

lmms.io

Audiotool

www.audiotool.com

Professional editing, color, effects and audio post

www.blackmagicdesign.com/products/davinciresolve

Useful resources

lmms.io/wiki/index.php?title=Useful_resources

Ultimate List of FREE Soundfonts

cymatics.fm/blogs/production/soundfonts

Free Quality SoundFonts (sf2)

sites.google.com/site/soundfonts4u

MuseScore Orchestra SoundFont

musescore.org/en/node/304354

Petrucci Music Library

imslp.org/wiki/Main_Page

Improve focus and boost your productivity

www.noisli.com

www.ghacks.net/2017/11/06/the-ultimate-game-sounds-list

Free sound effects & royalty free music

www.zapsplat.com

freesound.org

Ishkur’s Guide to Electronic Music

music.ishkur.com

12.28. Graphics

vectr.com

Easy design

crello.com

GIFs

giphy.com

Advanced image editor

www.photopea.com

mara.photos

Extract vector graphic from PDF

smallbusiness.chron.com/extract-vector-graphic-pdf-47224.html

Inkscape cropping

www.youtube.com/watch?v=v-JxhF3D4No

Insert SVG into Word

superuser.com/questions/397644/inserting-svg-files-in-a-microsoft-word-document/1171183

Programmable 3D CAD modeller

www.openscad.org

Simple but powerful cross platform image editor

photoflare.io

www.freelogodesign.org

Web service that generates chart images on-the-fly

quickchart.io

12.30. Website hosting

hostlater.com/free-web-hosting-sites-servers

Free website buider

www.wix.com

Netlify unites an entire ecosystem of modern tools and services into a single, simple workflow for building high performance sites and apps.

www.netlify.com

12.31. PDF

Freely read, edit, convert, merge, and sign PDF files

www.pdfgear.com

Okular The Universal Document Viewer

okular.kde.org

Open source multi-format reader

github.com/sumatrapdfreader/sumatrapdf

Dark mode for SumatraPDF

oak.dev/2020/05/06/dark-mode-for-sumatra-pdf

Pale mode, solving dark mode issues

notepad.patheticcockroach.com/4581/sumatrapdf-dark-mode

Versatile tool to convert and edit PDFs

pdfcandy.com

Open source software to split, merge and rotate PDF files

www.pdfsam.org/download-pdfsam-basic

Online PDF editing

smallpdf.com/edit-pdf

PDF to Word converter

smallpdf.com/pdf-to-word

PDF compressor and editor

www.jsoft.fr/logiciel-pdf-reducteur

Merge or split pdf documents and rotate, crop and rearrange their pages

github.com/pdfarranger/pdfarranger

All-in-one online PDF solution

www.pdfyeah.com

robin-horton.medium.com/how-to-convert-adobe-digital-editions-to-pdf-with-calibre-e5ce75887fe5

PDF24 Tools

tools.pdf24.org

Edit, annotate, sign, and share PDFs on desktop, mobile, and web

pdf.online

Edit PDF metadata

www.sejda.com/edit-pdf-metadata

www.ghacks.net/2021/10/01/want-to-edit-a-pdf-online-heres-a-list-of-the-best-9-pdf-extensions-for-chrome

Edit PDF

www.pdfzorro.com

12.32. Presentation software

alternativeto.net/software/reveal-js

12.33. Tools

The software engineer’s swiss knife

www.online-toolz.com

Lipsum

lipsum.com

listoffreeware.com/free-scroll-screen-capture-software-windows

www.nirsoft.net/utils/search_my_files.html

www.ultimatebootcd.com

www.favicon-generator.org

realfavicongenerator.net

www.urlencoder.net

www.digitalcitizen.life/easiest-way-legally-download-iso-images-windows-and-office

Wayback Machine

archive.org/web

Webpage capture

archive.is/

www.ghacks.net/2017/04/04/website-downloader-download-entire-wayback-machine-site-archives

VPS hosting

contabo.com/?show=vps

Webrecorder

webrecorder.io

addons.mozilla.org/en-US/firefox/addon/youtube-flash-video-player

www.calculator.net

www.google.com/trends

List of website speed test tools

www.keycdn.com/blog/website-speed-test-tools

Optimize web page speed

developers.google.com/speed

Analyze site speed

gtmetrix.com

See what your website looks like in different browsers and operating systems

comparium.app

Firefox backup

addons.mozilla.org/En-us/firefox/addon/febe

File unlockers

www.ghacks.net/2017/03/08/unlock-and-delete-locked-files-and-folders-with-thisismyfile

Email backup

www.mailstore.com/en/mailstore-home-email-archiving.aspx

Host provider

www.webtropia.com

Hosting, DNS etc.

www.namecheap.com

ShellCheck

www.shellcheck.net

Tiny proxy

tinyproxy.github.io

PC building simulator

pcbuildingsimulator.wordpress.com

The Ultimate Distraction-Free Text Editor

getcoldturkey.com/writer

Online sticky notes

pinup.com

IF This Then That (IFTTT)

www.ghacks.net/2017/03/20/top-ifttt-recipes-improve-android

Hide files or folders in JPEG images

www.filefriend.net

Email delivery service for developers

www.sparkpost.com

Quickly OCR a portion of the screen

capture2text.sourceforge.net

Online OCR

www.onlineocr.net

Taskbar system stats for Windows

entropy6.com/xmeters

A better way to view & analyze data

tadviewer.com

AstroGrep file content search

astrogrep.sourceforge.net

Low-level PC hardware read/write tool

rweverything.com

Top quality online translation

www.deepl.com/translator

Translate documents and get dictionary definitions instantly

www.ghacks.net/2020/10/04/translate-microsoft-office-documents-or-text-from-any-word-editor-and-get-dictionary-definitions-instantly-with-gt4t

Find the files and folders that use the most disk space

antibody-software.com/web/software/software/wiztree-finds-the-files-and-folders-using-the-most-disk-space-on-your-hard-drive

PC benchmarking

novabench.com

Disk cleaning

www.koshyjohn.com/software/diskmax

IFTTT

ifttt.com

Easily convert files into SQL databases

sqlizer.io

Bootice, edit MBR and PBR

www.softpedia.com/get/System/Boot-Manager-Disk/Bootice.shtml

SpeechTexter (requires Chrome)

www.speechtexter.com

Convert anything to anything

cloudconvert.com

Useful applications including OSArmor

www.novirusthanks.org

Online survey tools

opensource.com/article/17/2/tools-online-surveys-polls

Online tone generator

www.szynalski.com/tone-generator

Backup Windows program settings

www.cloneapp.de

Websites change. Perma Links don’t.

perma.cc

Itty bitty sites are contained entirely within their own link.

itty.bitty.site/edit

High quality translation

www.deepl.com

Link checker

validator.w3.org/checklink

Check My Links for Chrome

chrome.google.com/webstore/detail/check-my-links/ojkcdipcgfaekbeaelaapakgnjflfglf

Download your Google data

takeout.google.com/settings/takeout

Google search console

search.google.com/search-console

Pinetools

pinetools.com

The app highlights lengthy, complex sentences and common errors

www.hemingwayapp.com

Cyotek WebCopy

www.cyotek.com/cyotek-webcopy/downloads

Third-party tools that find Windows keys

www.techspot.com/guides/1760-find-your-windows-product-key

www.wikihow.com/Download-All-Images-on-a-Web-Page-at-Once

Disposable email

temp-mail.org

The world’s tiniest spreadsheet

tinysheet.com

merabheja.com/12-best-free-file-comparison-tools-for-windows-10

Full list of Skype emoticons

support.skype.com/en/faq/fa12330/what-is-the-full-list-of-emoticons

Plagiarism checker

www.grammarly.com/plagiarism-checker

Easy to use open source video editor

www.ghacks.net/2019/12/18/simplevideocutter-is-a-free-and-incredibly-easy-to-use-video-trimming-tool-for-windows

Fast and easy digital audio editing software for Windows

www.wave-editor.com

TaskExplorer

github.com/DavidXanatos/TaskExplorer

www.maketecheasier.com/check-hard-disk-health-windows

www.2brightsparks.com/freeware/index.html

Everything fast file and folder search

www.voidtools.com

Symbololology

tabatkins.github.io/symbols

Temporary email

msk.sh

Performance monitoring in the taskbar

www.ghacks.net/2020/08/19/view-the-cpu-ram-and-network-usage-on-the-taskbar-with-perfmonbar

Visual graph editor

github.com/ArsMasiuk/qvge

Meeting scheduler

zcal.co

Help authoring tools

www.indoition.com/online-help-authoring-tools-survey.htm

Alternative, free, open-source YouTube application for Android

skytube-app.com

Video, image editing, backlink, website management and tracking, domain, text, writing, proxy, PDF, etc.

entiretools.com

Open source intelligence tools

qintel.pro/os-int

Export or delete Skype data

support.skype.com/en/faq/FA34894/how-do-i-export-or-delete-my-skype-data

12.33.3. Collaboration

nextcloud.com

12.33.4. Messaging and video conferencing

tox.chat

jitsi.org

bigbluebutton.org

github.com/D4koon/WhatsappTray

12.33.5. Note taking

joplinapp.org

www.thewindowsclub.com/how-to-compare-two-files-in-notepad

12.33.5.1. SilentNotes

SilentNotes is a great open source note taking app for Windows and Android with cloud synchronization. To set up synchronization with Nextcloud, see github.com/martinstoeckli/SilentNotes/issues/61.

12.33.7. File sharing

www.udrop.com

www.docdroid.net

12.33.12. File management

A simple, fast and free app to remove unnecessary files.

github.com/qarmin/czkawka

www.ghacks.net/2014/08/13/the-best-free-desktop-search-programs-for-windows

Open-source cross-platform GUI tool to find duplicate files

dupeguru.voltaicideas.net

12.33.14. Screen annotation

github.com/pubpub-zz/ppInk

12.33.16. Password generator

passwords-generator.org

12.34. Other

Live nature cam network and documentary film channel

explore.org