1. Kuvaus
Palvelun lähtökohtana on palautteen antaminen ja vastaanottaminen avoimen keskustelun hengessä. Palautteen antaminen tapahtuu nimettömänä.
Käyttäjä voi rekisteröitymällä luoda itselleen seinän. Muut käyttäjät voivat jättää seinälle viestejä (palautetta). Jos viestin lähettäjän yksilöiminen ei muuten ole mahdollista, voidaan siihen liittää Facebook-kirjautuminen. Lähettäjän henkilöllisyys jää silloin ainoastaan ylläpidon tietoon, eikä sitä näytetä sivulla.
Viestejä voi kommentoida tai äänestää joko ylös tai alas. Äänestettäessä ylös viestin arvo nousee yhdellä. Äänestettäessä alas viestin arvo laskee yhdellä. Arvo esitetään kokonaislukuna viestin yhteydessä ja ne järjestetään sen mukaan. Käyttäjä voi äänestää vain kerran yksittäistä viestiä.
2. Tekninen toteutus
Demo löytyy osoitteesta:
http://195.148.97.131/~noora_savolainen/index.php
2.1 MySQL
Palvelun tietokanta on jaettu kahteen tauluun: Käyttäjiin ja viesteihin. Viestit-taulu sisältää kaikki palveluun lähetetyt viestit. Käyttäjät-taulu tallentaa tiedot rekisteröityneistä käyttäjistä ja luoduista seinistä.
2.1.1 Käyttäjät
Ei vielä toteutettu.
2.1.2 Viestit
Taulun tietueet:
+-------------+--------------+------+-----+-------------------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+--------------+------+-----+-------------------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | message | varchar(500) | YES | | NULL | | | parent | int(11) | YES | | -1 | | | rating | int(11) | YES | | 0 | | | user_id | int(11) | YES | | NULL | | | submit_time | timestamp | NO | | CURRENT_TIMESTAMP | | +-------------+--------------+------+-----+-------------------+----------------+
- id: Tietueen yksilöllinen tunnus. Luodaan automaattisesti.
- message: Käyttäjän kirjoittama viesti.
- parent: Määrittää onko viestin aseman hierarkiassa. Jos arvo on muu kuin -1 se on kommentti, jolloin arvo on kommentoidun viestin id. Oletusarvona on -1, joka tarkoittaa kyseessä olevan ensimmäisen tason viestin.
- Rating: Ylös- ja alas-äänien kokonaisarvo.
- user_id: Sen seinän id, johon viesti kuuluu
- submit_time: Aika, jolloin viesti on tallennettu tietokantaan.
2.2 PHP
Palveluun liittyvä php voidaan jakaa kolmeen kategoriaan: Järjestelmän ominaisuuksien alustaminen, sivulle lähetetyn tiedon käsittely ja tietokannasta haetun tiedon perusteella sivun sisällön muodostava osa.
2.2.1 Ominaisuuksien alustaminen
Yhdistäminen tietokantaan ja sen onnistuessa asetusten määrittely:
//connect to database try { $mysql_conn = new PDO ("mysql:host=localhost;dbname=koala", "vompatti", "opossumi"); }catch (PDOException $e){ die("Virhe: " . $e->getMessage()); } // error handling $mysql_conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // encoding $mysql_conn->exec("SET NAMES utf8");
Jos sovelluksen vaatimaa taulua ei ole tietokannassa se luodaan (IF NOT EXISTS):
//create table if doesn't exist try { $mysql_conn->exec("CREATE TABLE IF NOT EXISTS peukku_viestit ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, message VARCHAR(500), parent INT DEFAULT -1, rating INT DEFAULT 0, user_id INT, submit_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP)"); }catch (PDOException $e){ die("Virhe: " . $e->getMessage()); }
Luodaan käyttäjälle istunto, jonka perusteella hänet voidaan myöhemmin yksilöidä.
// create session cookie session_start(); if (!isset($_SESSION['session']) || !isset($_SESSION['token'])) { // this variable is to make sure we get post from correct form // we will add it to every form as a hidden field $_SESSION['token'] = uniqid(md5(microtime()), true); // this is for making it harder to hijack one's session cookie // save ip and user agent which created the session $_SESSION['session'] = md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']); }
2.2.2 Lähetetyn tiedon käsittely
Jokaisessa palvelun lomakkeessa on kaksi piilotettua kenttää, joka liitetään käyttäjän lähettämiin tietoihin.
"action" määrittää minkä toiminnon lomake suorittaa.
<input name="action" type="hidden" value="new_message" />
"token" mahdollistaa sen lomakkeen yksilöinnin, josta käyttäjä on lähettänyt tiedot.
<input name="token" type="hidden" value="new_message" />
Ensimmäiseksi tarkistamme onko käyttäjän lähettämää dataa olemassa. Se tehdään selvittämällä onko tietoihin liitetty aikaisemmin mainitsemani action-kenttä. Jos sitä ei tarkisteta, tapahtuu virhe lähetetyn datan puuttuessa.
// Check if there's post data if (isset($_POST['action'])){ ... }
Tämän jälkeen varmistamme, että tiedot on lähetetty lomakkeesta, joka oikeasti sijaitsee sivullamme (vrt. istunnon luominen).
// our security check from session cookie if($_SESSION['token'] == $_POST['token'] && $_SESSION['session'] == md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT'])) { ... }
Edellisen ehtolausekkeen sisältä löytyy varsinainen käyttäjän lähettämän tiedon käsittely.
Toiminto valitaan selvittämällä action-kentän arvo, jonka PHP on tallentanut post-datan sisältävään $_POST-muuttujaan.
Ensimmäiseksi käsittelemme tapauksen, jossa käyttäjä haluaa lisätä uuden viestin tietokantaan. Viestin erikoismerkit muunnetaan HTML-muotoon htmlspecialchars-funktiolla, jotta käyttäjän ei olisi mahdollista liittää omaa koodia sivuille viestien välityksellä.
PHP:n PDO-kirjaston prepare-metodi huolehtii siitä, ettei käyttäjän ole mahdollista syöttää palveluun omia SQL-kyselyjä.
Käsiteltyämme tiedot varmistamme, että käyttäjä ei voi lähettää tietoja uudestaan käyttämällä selaimen edellinen-toimintoa. Käytämme header-funktiota: header( 'Location: index.php')
. Koska muokkaamme suoraan sivun HTTP-otsaketta (HTTP header), tulee tämä tehdä ennen kuin yhtään HTML-koodia on tulostettu tiedostoon tai siitä seuraa virhe. Huomaa, että syötämme osoitteeseen muuttujan, joka määrittää uuden tai muuttunen viestin yksilöllisen tunnuksen (ID). Tällä tavalla voimme myöhemmin korostaa sen erottumaan muista viesteistä Javascriptin ja CSS:n avulla.
// Choose the correct action based on the value of the $_POST['action'] if ($_POST['action'] == 'new_message') { // Add a message to the database // prepare query // save it with htmlspecialchars in order to stop clever javascripting and HTML-tags ;) $query = $mysql_conn->prepare("INSERT INTO peukku_viestit (message) VALUES ('" . htmlspecialchars($_POST['message']) . "')"); // run query $query->execute(); //remove post data by redirecting to ourselves header( 'Location: index.php?new=' . $rivi['LAST_INSERT_ID()'] ) ; }
Peukku ylös:
else if ($_POST['action'] == 'peukuta') { // Update +rating to the database // prepare query $query = $mysql_conn->prepare("UPDATE peukku_viestit SET rating = rating + 1 WHERE id = '" . $_POST['id'] . "'"); // run query $query->execute(); //remove post data by redirecting to ourselves header( 'Location: index.php?new=' . $_POST['id'] ); }
Peukku alas:
else if ($_POST['action'] == 'peukuta_nega') { // Update -rating to the database // prepare query $query = $mysql_conn->prepare("UPDATE peukku_viestit SET rating = rating - 1 WHERE id = '" . $_POST['id'] . "'"); // run query $query->execute(); //remove post data by redirecting to ourselves header( 'Location: index.php?new=' . $_POST['id'] ); } }
2.2.3 Sisällön muodostaminen
Aloitamme suorittamalla kyselyn tietokannasta. ORDER BY-lause järjestää tiedon haluamallamme tavalla.
// prepare the query $kysely = $mysql_conn->prepare("SELECT * FROM peukku_viestit ORDER BY rating DESC, submit_time DESC"); // run it $kysely->execute();
While-silmukka käy yksitellen jokaisen kyselyn sisältämän rivin läpi. Käytimme PDO::FETCH_ASSOC-ominaisuutta, jotta pääsisimme käsiksi tietueen arvoon tekstiavaimella indeksin numeron sijaan. $rivi["rating"]
vastaan $rivi[0]
.
// process results from query one row at a time $i=1; while ($rivi = $kysely->fetch(PDO::FETCH_ASSOC)) { echo "<hr/>" . $i . ". <br />Rating: " . $rivi["rating"] . " <br />Time: " . $rivi["submit_time"] . " <p>Message:<br />" . $rivi["message"] ."</p>"; $i++; }
2.3 HTML ja CSS
Ensimmäinen vaihe html- koodauksessa oli luoda käyttöliittymäsuunnitelman (rautalangan) pohjalta elementit html- sivulle.
Tämän jälkeen luotiin css- tiedosto, jolla saatiin elementit suunnitelman mukaisesti paikoilleen ja selkeytettiin käyttöliittymää väreillä ja reunaviivoilla.
Html- pohjan ollessa valmis lisättiin php- toiminnallisuudet; uuden toiveen lisääminen, toiveiden plus / negatiivinen peukutus sekä toiveiden järjestäminen pisteytyksen mukaan.
Toiminnallisuuksien lisäämisen jälkeen niitä testattiin ja hienosäädettiin, jonka jälkeen voitiin aloittaa käyttöliittymän ja ulkoasun hiominen.
2.4 Javascript
Sivuun on lisätty javascript-toiminnallisuus, joka automaattisesti siirtää sivun uuden tai muokatun elementin kohdalle.
Ensimmäiseksi selvitetään, missä elementti sijaitsee suhteessa dokumenttiin.
// Determine the destination coordinate // elmYPosition( string elementID ) // Returns target element Y position function elmYPosition(eID) { var elm = document.getElementById(eID); var y = elm.offsetTop; var node = elm; while (node.offsetParent && node.offsetParent != document.body) { node = node.offsetParent; y += node.offsetTop; } return y; }
Siirtyminen oikealle kohdalle dokumentissa.
// Scrolls straight from current page Y coordinate to specified element Y coordinate. // Dependencies: elemYPosition() // roughScroll( string elementID ) function roughScroll(eID) { var stopY = parseInt(elmYPosition(eID)); stopY = stopY - parseInt(window.innerHeight / 2); window.scrollTo(0, stopY); }
Sijoitamme viestin HTML-koodiin sen yksilöllisen tunnuksen (ID), jos se on uusi tai sitä on muutettu. Muutoksen olemassaolo tarkistetaan isset-funktiolla.
$i=1; while ($rivi = $kysely->fetch(PDO::FETCH_ASSOC)) { ... ?> <div class="single-comment" <?php // check if this item has been updated if(isset($_GET['new'])) { if($rivi["id"] == $_GET['new']) { echo 'id="new-comment"'; } } ... i++; } ?>>
Lopuksi tarvitsemme vielä tavan selvittää, onko luotu uutta viestiä tai muutettu vanhaa. Toteutamme sen tarkistamalla new-muuttujan olemassaolon isset-funktiolla. Uuden tai muutetun viestin tapauksessa printtaamme Javascript-komennon, joka suorittaa sivun vierityksen.
<?php if(isset($_GET['new'])) { echo '<script type="text/javascript">roughScroll("new-comment");</script>'; } ?>
2.5 Lähdekoodi
2.5.1 PHP
<?php /*if (!ini_get('display_errors')) { ini_set('display_errors', 1); }*/ //connect try { $mysql_conn = new PDO ("mysql:host=localhost;dbname=koala", "vompatti", "opossumi"); }catch (PDOException $e){ die("Virhe: " . $e->getMessage()); } // virheenkäsittely: virheet aiheuttavat poikkeuksen $mysql_conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // merkistä $mysql_conn->exec("SET NAMES utf8"); //create table if doesn't exist try { $mysql_conn->exec("CREATE TABLE IF NOT EXISTS peukku_viestit ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, message VARCHAR(500), parent INT DEFAULT -1, rating INT DEFAULT 0, user_id INT, submit_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP)"); }catch (PDOException $e){ die("Virhe: " . $e->getMessage()); } // create session cookie session_start(); if (!isset($_SESSION['session']) || !isset($_SESSION['token'])) { // this variable is to make sure we get post from correct form // we will add it to every form as a hidden field $_SESSION['token'] = uniqid(md5(microtime()), true); // this is for making it harder to hijack one's session cookie // save ip and user agent which created the session $_SESSION['session'] = md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']); } // Check if there's post data if (isset($_POST['action']) && isset($_POST['token'])){ // our security check from session cookie if($_SESSION['token'] == $_POST['token'] && $_SESSION['session'] == md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT'])) { // Choose the correct action based on the value of the $_POST['action'] if ($_POST['action'] == 'new_message') { // Add a message to the database // valmistetaan kysely // save it with htmlspecialchars in order to stop clever javascripting ;) $query = $mysql_conn->prepare("INSERT INTO peukku_viestit (message) VALUES ('" . htmlspecialchars($_POST['message']) . "')"); // suoritetaan kysely $query->execute(); // save latest updated item $query = $mysql_conn->prepare("SELECT LAST_INSERT_ID()"); $query->execute(); $rivi = $query->fetch(PDO::FETCH_ASSOC); // this will regenerate users session every time something is posted // harder to hijack session cookie session_regenerate_id(); //reomve post data by redirecting to ourselves header( 'Location: index.php?new=' . $rivi['LAST_INSERT_ID()'] ) ; //if(isset($rivi['LAST_INSERT_ID()'])) //header( 'Location: index.php?new=' . $rivi['LAST_INSERT_ID()'] . '#new-comment'); //else //header( 'Location: index.php' ) ; } else if ($_POST['action'] == 'peukuta') { // Add a Peukku to the database // valmistetaan kysely $query = $mysql_conn->prepare("UPDATE peukku_viestit SET rating = rating + 1 WHERE id = '" . $_POST['id'] . "'"); // suoritetaan kysely $query->execute(); // this will regenerate users session every time something is posted // harder to hijack session cookie session_regenerate_id(); //reomve post data by redirecting to ourselves //THIS IS NEW header( 'Location: index.php?new=' . $_POST['id'] . '&y=' . $_POST['current_y'] ) ; //header( 'Location: index.php?new=' . $_POST['id'] ) ; } else if ($_POST['action'] == 'peukuta_nega') { // Add a Peukku to the database // valmistetaan kysely $query = $mysql_conn->prepare("UPDATE peukku_viestit SET rating = rating - 1 WHERE id = '" . $_POST['id'] . "'"); // suoritetaan kysely $query->execute(); // this will regenerate users session every time something is posted // harder to hijack session cookie session_regenerate_id(); //reomve post data by redirecting to ourselves //THIS IS NEW header( 'Location: index.php?new=' . $_POST['id'] . '&y=' . $_POST['current_y'] ) ; //header( 'Location: index.php?new=' . $_POST['id'] ) ; } } } ?> <!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script type="text/javascript" src="script.js"></script> <title>Profiili- Peukkupalvelu</title> <link href="tyyli.css" rel="stylesheet" type="text/css"> </head> <body> <div id="wrapper"> <div id="content"> <h1>PEUKKUPALVELU</h1> <div id="profilewrapper"> <div id="profilebox"> <img id="profileimg" src="images/profiilikuva.jpg" width="180" height="180" alt="Profiilikuva"> <ul class="profiletext"> <h2>KD11S1VDV</h2> <li>Kevättä odotellessa!</li> <li>Digilabra</li> <li>Hämeentie 161, Helsinki</li> </ul> </div> <div id="commentbox"> <?php //YKSITTÄINEN VIESTI ALKAA // valmistetaan kysely $kysely = $mysql_conn->prepare("SELECT * FROM peukku_viestit ORDER BY rating DESC, submit_time DESC"); // suoritetaan kysely $kysely->execute(); // käsitellään tulostaulun rivit yksi kerrallaan $i=1; while ($rivi = $kysely->fetch(PDO::FETCH_ASSOC)) { // peukutus, plussa ?> <div class="single-comment" <?php // check if this item has been updated if(isset($_GET['new'])) { if($rivi["id"] == $_GET['new']) { echo 'id="new-comment"'; } } ?>> <div id="date"><?php echo htmlspecialchars($rivi["submit_time"]); ?></div> <!--span><?php echo $i; $i++; ?></span--> <div id="message-text"><?php echo htmlspecialchars($rivi["message"]); ?></div> <div id="rating"> <form name="vote_message_plus<?php echo htmlspecialchars($rivi["id"]); ?>" action="index.php" method="post" onsubmit="this.elements['current_y'].value=currentYPosition()"> <input name="current_y" type="hidden" value="0" /> <input name="token" type="hidden" value="<?php echo $_SESSION['token']?>" /> <input name="action" type="hidden" value="peukuta" /> <input name="id" type="hidden" value="<?php echo htmlspecialchars($rivi["id"]); ?>" /> <img onClick="document.vote_message_plus<?php echo htmlspecialchars($rivi["id"]); ?>.submit();" src="images/thumb-up.png" width="25" height="32" alt="Tykkään"/> <?php echo htmlspecialchars($rivi["rating"]); ?> </form> <?php // peukutus, nega ?> <form name="vote_message_nega<?php echo htmlspecialchars($rivi["id"]); ?>" action="index.php" method="post" onsubmit="this.elements['current_y'].value=currentYPosition()"> <input name="current_y" type="hidden" value="0" /> <input name="token" type="hidden" value="<?php echo $_SESSION['token']?>" /> <input name="action" type="hidden" value="peukuta_nega" /> <input name="id" type="hidden" value="<?php echo htmlspecialchars($rivi["id"]); ?>" /> <img onClick="document.vote_message_nega<?php echo htmlspecialchars($rivi["id"]); ?>.submit();" src="images/thumb-down.png" width="25" height="32" alt="En tykkää"/> </form> </div> <!--RATING LOPPUU--> <br> <div class="float-clear"></div> </div> <!--SINGLE COMMENT LOPPUU--> <?php } //YKSITTÄINEN VIESTI LOPPUU ?> </div> <!--COMMENTBOX LOPPUU--> </div> <!--PROFILEBOX LOPPUU--> </div> <!--PROFILEWRAPPER LOPPUU--> <div id="messagewrapper"> <div id="messagebox"> <form name="statusupdate" action="index.php" method="post"> <input name="token" type="hidden" value="<?php echo $_SESSION['token']?>" /> <input name="action" type="hidden" value="new_message" /> <p> <label for="message">Kirjoita uusi toive: </label><br/> <textarea name="message" id="message" rows="20" cols="50"></textarea> </p> <span class="float-right"> <input type="submit" value="Lähetä" /> <input type="reset" value="Tyhjennä" /> </span> </form> </div> </div> <div class="float-clear"></div> </div> </div> <?php // THIS IS NEW // check if the message is shown on-screen if(isset($_GET['y'])) { echo '<script type="text/javascript">window.scrollTo(0,' . $_GET['y'] . ');</script>'; ?> <script type="text/javascript"> var elemY = elmYPosition("new-comment"); var curY = currentYPosition(); var winHeight = window.innerHeight; var winHalf = winHeight / 2; curY += winHalf; var test = 0; if(elemY < curY) { test = curY + winHalf; if(test > elemY) { smoothScroll("new-comment"); } } else { test = curY - winHalf; if(test < elemY) { smoothScroll("new-comment"); } } </script> <?php } else { echo '<script type="text/javascript">smoothScroll("new-comment");</script>'; } ?> </body> </html>
2.5.2 Javascript
// Determine the point on the Y Axis at which the scrolling has to start // Returns current page Y position function currentYPosition() { // Firefox, Chrome, Opera, Safari if (self.pageYOffset) return self.pageYOffset; // Internet Explorer 6 - standards mode if (document.documentElement && document.documentElement.scrollTop) return document.documentElement.scrollTop; // Internet Explorer 6, 7 and 8 if (document.body.scrollTop) return document.body.scrollTop; return 0; } // Determine the destination coordinate // elmYPosition( string elementID ) // Returns target element Y position function elmYPosition(eID) { var elm = document.getElementById(eID); var y = elm.offsetTop; var node = elm; while (node.offsetParent && node.offsetParent != document.body) { node = node.offsetParent; y += node.offsetTop; } return (y - elm.scrollTop) - (window.innerHeight / 2); } // Scrolls smoothly from current page Y coordinate to specified element Y coordinate. // Dependencies: currentYPosition(), elemYPosition() // smoothScroll( string elementID ) function smoothScroll(eID) { var startY = currentYPosition(); var stopY = elmYPosition(eID); var distance = stopY > startY ? stopY - startY : startY - stopY; if (distance < 100) { scrollTo(0, stopY); return; } var speed = Math.round(distance / 100); if (speed >= 20) speed = 20; var step = Math.round(distance / 25); var leapY = stopY > startY ? startY + step : startY - step; var timer = 0; if (stopY > startY) { for ( var i=startY; i<stopY; i+=step ) { setTimeout("window.scrollTo(0, "+leapY+")", timer * speed); leapY += step; if (leapY > stopY) leapY = stopY; timer++; } return; } for ( var i=startY; i>stopY; i-=step ) { setTimeout("window.scrollTo(0, "+leapY+")", timer * speed); leapY -= step; if (leapY < stopY) leapY = stopY; timer++; } } // Scrolls straight from current page Y coordinate to specified element Y coordinate. // Dependencies: elemYPosition() // roughScroll( string elementID ) function roughScroll(eID) { var stopY = parseInt(elmYPosition(eID)); stopY = stopY - parseInt(window.innerHeight / 2); window.scrollTo(0, stopY); //alert(stopY); }