Jelszóvédelem az admin oldalra, avagy az authentikáció egyszerű megoldása

Az adminisztrátori vagyis foglalás-menedzsment funkcionalitás eleve rejtve van a kíváncsi tekintetek elől, hiszen ha kint is van az Interneten, vagyis a “webtérben” az oldal, link nincs az admin oldalunkra a honlapon. Azonban ennél azért nagyobb biztonságra lesz szükségünk, ezért az egész admin funkcionalitást el kell rejtsük egy jelszóval védett területre.

A jelszavas védelmet a CodeIgniter keretrendszer alapban nem támogatja, viszont egyszerűen megvalósíthatjuk. Egyszerűen úgy működik, hogy a controller-ben a kívánt funkciók hívása előtt megvizsgáljuk, hogy előzőleg tároltunk-e el a szerver session-jében “user” változót a korábban lezajló login folyamat során, amelyben adatbázisban ellenőrizzük a felhasználó/jelszó párt. Van benne még technikailag pár átirányítás, hogy a webalkalmazásunk “visszataláljon” a “hívó” oldalra (referer) a sikeres jelszó hitelesítés (authentikáció) vagyis belépés (login) után.

És akkor most nézzük a konkrét implementációt.

Először is kell egy modell. Ez az models/Auth_model.php file-unk. Összezsúfoltam picit a kódot – nem szokásom amúgy -, hogy minél kevesebb sorban oldjam meg a feladatot. 🙂

<?php
class Auth_model extends CI_Model {
 public function login(){
  $query = $this->db->get_where('users', array('name' => $this->input->post('username'), 'password' => $this->input->post('password'))); 
  $result = $query->result();
  return (empty($result)?false:(empty($result[0])?false:true));
 }
}

Ez a login() metódus egy boolean – logikai – értékkel tér vissza attól függően, hogy megvan-e az adatbázisban a felhasználó/jelszó páros. Pár technikai észrevétel: a felhasználónévnek egyedinek kell lenni (ezt egy ún. unique key vagyis egyedi kulcs bitosítja SQL technikában), hogy nyilvánvalóan különbözzenek a felhasználóink. A gyakorlatban általában az e-mail címeket választják ki a mérnökök erre a célra, hiszen az mindenképpen egyedi. 🙂 Aztán a jelszó. Nem kódoltam le ebben a pilot alkalmazásban, de illik valamilyen egyirányú kódoló algoritmussal titkosítani, hogy senki se fejthesse vissza a jelszavakat az adatbázis ismeretében. 🙂

Nézzük a meg views/login.php view-nkat.

<?php
        $my_base_url = '/apartman/';
?>
                                <div class="row">
                                        <div class="col-sm-2">
                                                <div id="fh5co-logo"><a href="index.html">Apartman<span>.</span></a></div>
                                        </div>
                                        <div class="col-sm-10 text-right menu-1">
                                                <ul>
                                                        <li><a href="photos"><?php echo $this->lang->line('menu_photos'); ?></a></li>
                                                        <li><a href="booking"><?php echo $this->lang->line('menu_booking'); ?></a></li>
                                                        <li><a href="contact"><?php echo $this->lang->line('menu_contact'); ?></a></li>
                                                </ul>
                                        </div>
                                </div>
                                
                        </div>
                </div>
        </nav>
        <div class="container">
                <div id="fh5co-intro">
                        <div class="row animate-box">
                                <div class="col-md-8 col-md-offset-2 col-md-pull-2">
                                        <h2><?php echo $this->lang->line('menu_booking'); ?></h2>
                                </div>
                        </div>
                </div>
                <div id="fh5co-contact">
                        <div class="row">
                                <div class="col-md-8 animate-box">
                                        <div class="form-wrap">
                                                <div class="row">
                                                        <form action="login" method="POST">
                                                        <?php if ($auth_errors) { ?>
                                                        <div class="col-md-12">
                                                                <div class="form-group">
                                                                        <h3><font color="red"><?php echo $auth_errors; ?></font></h3>
                                                                </div>
                                                        </div>
                                                        <?php } ?>
                                                        <div class="col-md-12">
                                                                <div class="form-group">
                                                                        <input type="text" name="username" class="form-control" placeholder="User Name">
                                                                </div>
                                                        </div>
                                                        <div class="col-md-12">
                                                                <div class="form-group">
                                                                        <input type="password" name="password" class="form-control" placeholder="Password">
                                                                </div>
                                                        </div>
                                                        <div class="col-md-12">
                                                                <div class="form-group">
                                                                        <input type="submit" value="Login" class="btn btn-primary btn-modify">
                                                                </div>
                                                        </div>
                                                        </form>
                                                </div>
                                        </div>
                                </div>
                        </div>
                </div>
        </div><!-- END container -->

Ebben érdekes, hogy egy sima formot használunk, nem JavaScript-eset, és hogy password típusú mezőbe kérjük be a jelszót, amit nem lehet elolvasni a képernyőn, miközben begépeljük, csak pontokat látni a karakterek helyén. Ez a biztonságot szolgálja. Más kérdés, hogy a billentyűzetről is le tudja olvasni egy ügyes tekintetű illető a jelszót, úgyhogy ilyenkor illik “nem oda nézni” a harmadik félnek. 🙂

Majd nézzük a szintén nem túl bonyolult controller-ünket (controllers/AuthController.php). Megjegyzem, hogy végre beállítottam a config/config.php => $config['base_url'] = 'http://localhost/apartman/'; értéket, hogy használhassam az a base_url($path) metódust a redirect-ek (átirányítások) miatt. 🙂

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class AuthController extends CI_Controller {
  public function __construct(){
    parent::__construct();
  }
  public function is_logged($referer){
    $user = $this->session->userdata('user');
    if (empty($user)) {
      // not authenticated
      $this->session->set_userdata('login_url', $referer);
      redirect(base_url('index.php/AuthController/login_form'));
    }
  }
  public function login_form(){
    $data = array();

    $data['auth_errors'] = '';
    if (!empty($this->session->flashdata('auth_errors'))) {
      $data['auth_errors'] = $this->session->flashdata('auth_errors');
    }

    $this->template->set('title', 'Login');
    $this->template->load('menu_layout', 'contents', 'login', $data);
  }
  public function login(){
   $this->load->model('auth_model');

   $success = $this->auth_model->login(); 

   if ($success) {
     $this->session->set_userdata('user', $this->input->post('username'));
     redirect($this->session->userdata('login_url'));
   } else {
     // login fail
     $this->session->set_flashdata('auth_errors', 'Login failed.');
     redirect($_SERVER['HTTP_REFERER']);
   }
  }
  public function logout(){
    $this->session->set_userdata('login_url', null);
    $this->session->set_userdata('user', null);
  }
}

Látható, hogy három fontos metódust tartamaz. Az is_logged($referer) gondoskodik arról, hogy megvizsgáljuk, van-e belépett felhasználó (user) a session-ben. A login_form() hibaüzenetek kezelését, illetve a login form kirajzolását végzi, míg a login() a tulajdonképpeni beléptetést a modellhívással. A logout() csak játék, technikailag vettem fel. Egyszerűen töröl minden session-ban tárolt változót.

Ezt a controller-t az alkalmazásunk fő vezérlőjében (controllers/Apartments.php) az alábbi módon használjuk.

<?php
include APPPATH.'controllers/AuthController.php';
class Apartments extends AuthController {

        public function admin() {

                // check if user is authenticated
                // config/config.php => $config['base_url'] = 'http://localhost/apartman/';
                $this->is_logged(base_url('index.php/apartments/admin'));
                
                $this->load->model('reservations_model');

                $data['query'] = $this->reservations_model->get_reservations();

                $this->template->set('title', 'Admin');
                $this->template->load('menu_layout', 'contents' , 'admin', $data);
        }

        public function admin_approve_ajax($id) {

                // check if user is authenticated
                $this->is_logged(base_url('index.php/apartments/admin'));

                $this->load->model('reservations_model');

                $this->reservations_model->approve_reservation($id);
                $data = $this->reservations_model->get_reservation($id)[0];

                header('Content-Type: application/json');
                echo json_encode($data);
        }

}

Látható, hogy az AuthController-t include-oljuk a kód elején, majd extends-szel alaposztályként hivatkozunk rá. Innentől egy is_logged($referer) hívással mindkét adminisztrátori metódus legelején eldöntjük, hogy “be van-e lépve” az adminisztrátor. Ha nem, akkor átirányítjuk a login form-ra, ha igen, megy tovább a buli. 🙂

És akkor nézzük a login form-unkat! A http://localhost/apartman/index.php/AuthController/login_form webcím alatt látható, ha az admin oldalt szeretnénk megnyitni, és a session változó már elévült a webszerverünkön vagy netán kipróbáltuk a logout() funkciót. 🙂

Admin funkció, foglalás jóváhagyása, AJAX JSON :)

Most már, hogy egy űrlapon keresztül foglalásokat lehet elküldeni, szükségünk lesz egy admin oldalra, ahol az apartman ügyintézője meg tudja tekinteni, illetve jóvá tudja hagyni a foglalásokat.

Hogy mit jelent itt az hogy AJAX JSON? Elég érdekes öszvér elnevezés. 🙂 AJAX-szal normál esetben egy XML adatstruktúrát vinnénk át az aszinkron javascript üzenetben, ami valahogy így nézne ki egy konkrét foglalás (reservation) adataira.

<xml>
<reservation>
<apartment_id>1</apartment_id>
<approved>1</approved>
..
</reservation>
</xml>

Mégis azonban az ún. JSON formátumot használjuk, ami egy konkrét esetben az alábbi szerverválaszt adja.

{
apartment_id: "1",
approved: "1",
checkin: "2020-07-29",
checkout: "2020-08-01",
created_at: "2020-08-03 03:34:11",
email: "aaa@aaa.hu",
id: "10",
locale: null,
message: "aaa",
name: "aaa",
phone: "",
updated_at: "2020-08-05 01:08:08"
}

Látható, hogy egy eléggé egyszerű adatszerkezet. A JSON leírása részletesen az alábbi címen érhető el: https://hu.wikipedia.org/wiki/JSON.

És akkor nézzük meg a konkrét implementációt. Először is bővítenünk kell a models/Reservations_model.php modellünket három metódussal.


        public function get_reservations() {
                $this->db->select()->from('reservations')->order_by('created_at', 'DESC');
                $query = $this->db->get();
                return $query->result();
        }

        public function get_reservation($id) {
                $query = $this->db->get_where('reservations', array('id' => $id));
                return $query->result();
        }

        public function approve_reservation($id) {
                $date = date('Y-m-d H:i:s');
                $this->db->set('updated_at', $date);
                $this->db->set('approved', 1);
                $this->db->where('id', $id);
                $this->db->update('reservations');
        }

Az első get_reservations() metódus a foglalások keletkezésének idejével csökkenő sorrendben egy listát ad vissza a foglalások adataival.

A második get_reservation($id) metódus egy konkrét foglalás adatait adja vissza.

A harmadik approve_reservation($id) metódus pedig UPDATE-eli, tehát frissíti a megadott ID-jű foglalás rekordunkat a frissítés dátumával és beállítja az jóváhagyott foglalás értékét 1-esre, ami esetünkben a jóváhagyás tényét jelzi adatbázis szinten.

Nézzük a views/admin.php view-nkat, hogy mi is kerül bele.

<?php
        $my_base_url = '/apartman/';
?>
                                <div class="row">
                                        <div class="col-sm-2">
                                                <div id="fh5co-logo"><a href="index.html">Apartman<span>.</span></a></div>
                                        </div>
                                        <div class="col-sm-10 text-right menu-1">
                                                <ul>
                                                        <li><a href="photos"><?php echo $this->lang->line('menu_photos'); ?></a></li>
                                                        <li><a href="booking_ajax"><?php echo $this->lang->line('menu_booking'); ?></a></li>
                                                        <li><a href="contact"><?php echo $this->lang->line('menu_contact'); ?></a></li>
                                                </ul>
                                        </div>
                                </div>
                                
                        </div>
                </div>
        </nav>
        <div class="container">
                <div id="fh5co-intro">
                        <div class="row animate-box">
                                <div class="col-md-8 col-md-offset-2 col-md-pull-2">
                                        <h2><?php echo $this->lang->line('menu_booking'); ?></h2>
<table border="1">
<tr>
<th>Name</th><th>Email</th><th>Message</th><th>Checkin</th><th>Checkout</th><th>Created at</th><th>Updated at</th><th>Locale</th><th>Approved</th><th></th>
</tr>
<?php foreach ($query as $item):?>
<tr>
<form>
<td><?php echo $item->name;?></td>
<td><?php echo $item->email;?></td>
<td><?php echo $item->message;?></td>
<td><?php echo $item->checkin;?></td>
<td><?php echo $item->checkout;?></td>
<td><?php echo $item->created_at;?></td>
<td id="updated<?php echo $item->id;?>"><?php echo $item->updated_at;?></td>
<td><?php echo $item->locale;?></td>
<td id="approved<?php echo $item->id;?>"><?php echo $item->approved==1?'true':'false';?></td>
<td><input id="approve_button<?php echo $item->id;?>" type="button" value="Approve" class="btn btn-primary btn-modify" <?php echo $item->approved==1?'disabled':'';?>></td>
</form>
<script>
    $(document).ready(function(){
        $( "#approve_button<?php echo $item->id;?>" ).click(function(event)
        {
            $.ajax(
                {
                    type:"post",
                    url: "<?php echo $my_base_url; ?>index.php/apartments/admin_approve_ajax/<?php echo $item->id;?>",
                    data:{ },
                    contentType: "application/json",
                    dataType:"json", 
                    success:function(response)
                    {
                        console.log(response);
                        var obj = $.parseJSON(JSON.stringify(response));
                        $("#updated<?php echo $item->id;?>").html(obj.updated_at);
                        $('#approved<?php echo $item->id;?>').html(obj.approved==1?'true':'false');
                        $('#approve_button<?php echo $item->id;?>').prop('disabled', obj.approved==1?true:false);
                    },
                    error: function(response)
                    {
                        console.log(response);
                        alert("Invalide id=<?php echo $item->id;?>!");
                    }
                }
            );
        });
    });
</script>
</tr>
<?php endforeach;?>
</table>
                                </div>
                        </div>
                </div>
        </div><!-- END container -->

Látható, hogy ebbe egy táblázat került a foglalások adataival, melynek minden sora egy-egy foglalást tartalmaz. Erről az iterációról a <?php foreach ($query as $item):?><?php endforeach;?> blokk gondoskodik. Minden sort a foglalás ID-je azonosít. Minden sorhoz kerül egy-egy JavaScript blokk is a megfelelő Approve (Jóváhagy) gomb megfelelő “clickeseményének lekezelésével.

Felhívnám a figyelmet a JSON üzenet dekódolására. Ez a var obj = $.parseJSON(JSON.stringify(response)); sorban történik. Először szükséges sztringgé alakítani a válaszüzenetet, mivel az objektum-ként érkezik. Ez a JSON.stringify metódussal történik. Utána csak egy sima parse-olásra lesz szükségünk. 🙂

Nézzük meg végül a controllers/Apartments.php controller-ünket. Két új metódussal bővül. Az admin() metódusban csak kirajzoljuk az oldalunkat a modell réteg lista SQL SELECT metódusának meghívásával. Az admin_approve_ajax($id) metódusban pedig SQL UPDATE hívás majd egy SQL SELECT hívás történik a modell rétegben, illetve a válasz üzenet JSON becsomagolása.

        public function admin() {
                
                $this->load->model('reservations_model');

                $data['query'] = $this->reservations_model->get_reservations();

                $this->template->set('title', 'Admin');
                $this->template->load('menu_layout', 'contents' , 'admin', $data);
        }

        public function admin_approve_ajax($id) {

                $this->load->model('reservations_model');

                $this->reservations_model->approve_reservation($id);
                $data = $this->reservations_model->get_reservation($id)[0];

                header('Content-Type: application/json');
                echo json_encode($data);
        }

Látható, hogy viszonylag egyszerűen megoldottuk az egész adminisztrátori funkcionalitást az AJAX form vezérlés / adatátvitel és a JSON adatformátum hsználatával. 🙂

És akkor lássuk az admin oldalt (http://localhost/apartman/index.php/apartments/admin), egy jóváhagyott foglalással!

Látható, hogy a legfelső, az Approved true, tehát jóváhagyott rendelés – mivel csak angol nyelven implementáltam mindent az egyszerűség kedvéért – melletti Approve gomb már kiszürkült, nem megnyomható újra. Az Updated at – frissítés dátuma – pedig a késői vagy kora hajnali órát jelzi, amikor elkészültem a pilot kódokkal. 🙂

jQuery AJAX – form elküldése a “háttérben” – foglalás oldal újratöltve

Az AJAX, azaz Asynchronous JavaScript and XML arra szolgál, hogy ne kelljen az egész weblapot újratölteni a form elküldésekor, hanem csak az adatokat, mintegy csendben a “háttérben” elküldeni, majd valami <div> tag-ben megjeleníteni a kívánt felhasználói üzeneteket. Részletesebb leírás az alábbi címen található https://hu.wikipedia.org/wiki/Ajax_(programoz%C3%A1s).

Nem nagyobb feladatra vállalkozunk, minthogy az előbbi foglalás-űrlap elküldésünket aszinkron JavaScript hívásra cseréljük.

Először pár apróság. A views/layouts/menu_layout.php file-ban előre kell vennünk a fejlécbe a JavaScript forrás beillesztéseket, hogy tudjuk használni a jQuery funkcionalitásait.

Aztán át kell “drótoznunk” a menüt pl. a views/photos.php file-ban.

<li><a href="booking_ajax"><?php echo $this->lang->line('menu_booking'); ?></a></li>

Nézzük a views/booking_ajax.php oldalunk tartalmát.

<?php
        $my_base_url = '/apartman/';
?>
                                <div class="row">
                                        <div class="col-sm-2">
                                                <div id="fh5co-logo"><a href="index.html">Apartman<span>.</span></a></div>
                                        </div>
                                        <div class="col-sm-10 text-right menu-1">
                                                <ul>
                                                        <li><a href="photos"><?php echo $this->lang->line('menu_photos'); ?></a></li>
                                                        <li class="active"><a href="booking_ajax"><?php echo $this->lang->line('menu_booking'); ?></a></li>
                                                        <li><a href="contact"><?php echo $this->lang->line('menu_contact'); ?></a></li>
                                                </ul>
                                        </div>
                                </div>
                                
                        </div>
                </div>
        </nav>
        <div class="container">
                <div id="fh5co-intro">
                        <div class="row animate-box">
                                <div class="col-md-8 col-md-offset-2 col-md-pull-2">
                                        <h2><?php echo $this->lang->line('menu_booking'); ?></h2>
                                </div>
                        </div>
                </div>
                <div id="fh5co-contact">
                        <div class="row">
                                <div class="col-md-4 animate-box">
                                        <h3>Contact Information</h3>
                                        <ul class="contact-info">
                                                <li><i class="icon-location4"></i>1117 Budapest, Irinyi J. utca 42.</li>
                                                <li><i class="icon-phone3"></i>+ 1235 2355 98</li>
                                                <li><i class="icon-location3"></i><a href="#">info@yoursite.com</a></li>
                                                <li><i class="icon-globe2"></i><a href="#">www.yoursite.com</a></li>
                                        </ul>
                                </div>
                                <div class="col-md-8 animate-box">
                                        <div class="form-wrap">
                                                <div class="row">
                                                        <form id="ajax_form" action="book_ajax" method="POST">
                                                        <div class="col-md-12" id="response_message" style="display: none;">
                                                                <div class="form-group" id="response_message_text">
                                                                        <!--<h3><font color="red">MESSAGE</font></h3>-->
                                                                </div>
                                                        </div>
                                                        <div class="col-md-12">
                                                                <div class="form-group">
                                                                        <input id="name" type="text" name="name" class="form-control" placeholder="Name">
                                                                </div>
                                                        </div>
                                                        <div class="col-md-12">
                                                                <div class="form-group">
                                                                        <input id="email" type="text" name="email" class="form-control" placeholder="Email">
                                                                </div>
                                                        </div>
                                                        <div class="col-md-12">
                                                                <div class="form-group">
                                                                        <input id="phone" type="text" name="phone" class="form-control" placeholder="Phone">
                                                                </div>
                                                        </div>
                                                        <div class="col-md-12">
                                                                <div class="form-group">
                                                                        <textarea id="message" name="message" class="form-control" id="" cols="30" rows="15" placeholder="Message"></textarea>
                                                                </div>
                                                        </div>
                                                        <div class="col-md-12">
                                                                <div class="form-group">
                                                                        <input id="checkin" type="text" name="checkin" class="form-control" placeholder="Check-in">
                                                                </div>
                                                        </div>
                                                        <div class="col-md-12">
                                                                <div class="form-group">
                                                                        <input id="checkout" type="text" name="checkout" class="form-control" placeholder="Check-out">
                                                                </div>
                                                        </div>
                                                        <div class="col-md-12">
                                                                <div class="form-group">
                                                                        <input id="submit_button" type="button" value="Book" class="btn btn-primary btn-modify">
                                                                </div>
                                                        </div>
                                                        </form>
                                                </div>
                                        </div>
                                </div>
                        </div>
                </div>
        </div><!-- END container -->
<script>
    $(document).ready(function(){
        $( "#submit_button" ).click(function(event)
        {
            event.preventDefault();
            var name = $("#name").val();
            var email = $("#email").val();
            var phone = $("#phone").val();
            var message = $("#message").val();
            var checkin = $("#checkin").val();
            var checkout = $("#checkout").val();
            var data = $("#ajax_form").serialize();

            $.ajax(
                {
                    type:"post",
                    url: "<?php echo $my_base_url; ?>index.php/apartments/book_ajax",
                    data:{ name: name, email: email, phone: phone, message: message, checkin: checkin, checkout: checkout },
                    success:function(response)
                    {
                        console.log(response);
                        $("#response_message_text").html("<h3><font color=\"red\">" + response + "</font></h3>");
                        $('#response_message').show();
                    },
                    error: function()
                    {
                        alert("Invalide!");
                    }
                }
            );
        });
    });
</script>

Nézzük mi változott! Minden form mezőnek, az elküldő gombnak és a felhasználói üzenet résznek (#response_message_text és #response_message) egyedi azonosítót, id-attribútumot adtunk, hogy a JavaScript részben #id néven hivatkozhassunk rájuk. Ezt a hivatkozási módszert amúgy css szelektornak hívjuk, ezen belül is ún. ID Selector, részletek itt https://www.w3.org/wiki/CSS3/Selectors.

Látható a <script> tag-ben az ajax hívásunk. Kiolvassuk az űrlapmezők értékeit, majd egy hívást indítunk, mind success (siker), mind error (hiba) esetére megfelelő válaszokkal.

Modellünk nem változik viszont a controller-ünkbe elkél két új metódus. Nézzük a controllers/Apartments.php file-t.

        public function booking_ajax() {
                $data = array();

                $this->template->set('title', $this->lang->line('menu_booking'));
                $this->template->load('menu_layout', 'contents', 'booking_ajax', $data);

        }

        public function book_ajax() {
                $this->load->library('form_validation');
                $this->form_validation->set_rules('name', 'Name', 'required');
                $this->form_validation->set_rules('email','Email','trim|required|valid_email');
                $this->form_validation->set_rules('checkin', 'Check-in', 'required');
                $this->form_validation->set_rules('checkout', 'Check-out', 'required');

                if($this->form_validation->run() == FALSE) {
                        echo validation_errors();
                } else {
                        $this->load->model('reservations_model');
                        $this->reservations_model->insert_reservation();
                        echo "Thanks for booking!";
                }
        }

A booking_ajax metódus csak betölti a view-nkat.

A book_ajax metódus már összetettebb. Tartalmaz egy ellenőrzést, ami email címnél valid_email-lel is bővül, tehát megvizsgálja a formátumot is és egy trim-mel, ami az esetleges felesleges ún. “trailing space” karaktereket vágja le (üres karakterek az elején és a végén).

Látható, hogy validációs hibák esetén visszatér hibával, ez ki is lesz írva a view-nkban a megfelelő div-nél, illetve ha sikerült, akkor a “Köszönet a foglalásért!” üzenettel válaszol. Ennyi az egész :).

És akkor vessünk egy pillantást egy sikeres foglalásra!

Nézzük a http://localhost/apartman/index.php/apartments/booking_ajax oldalunkat.

Láthatjuk, hogy az űrlap (form) helyes kitöltése és elküldése után az adatbázis konzolban látható az adatbázisba felvett sor.

mysql> select * from reservations where id = 10;
+----+------+------------+-------+------------+------------+---------+----------+--------------+--------+---------------------+---------------------+
| id | name | email      | phone | checkin    | checkout   | message | approved | apartment_id | locale | created_at          | updated_at          |
+----+------+------------+-------+------------+------------+---------+----------+--------------+--------+---------------------+---------------------+
| 10 | aaa  | aaa@aaa.hu |       | 2020-07-29 | 2020-08-01 | aaa     |     NULL |            1 | NULL   | 2020-08-03 03:34:11 | 2020-08-03 03:34:11 |
+----+------+------------+-------+------------+------------+---------+----------+--------------+--------+---------------------+---------------------+
1 row in set (0.00 sec)

CodeIgniter modell réteg az adatbázissal és a Hello, World!

Programozóknak ugyan nem kell bemutatni Hello, World! fogalmát, de most kell hogy beszéljünk róla. A mai fejezetben szeretnénk létrehozni az adatbázist MySQL-ben, legyártani a modell réteget, majd adattal feltölteni, és a weben megjelentíteni. Mindezt mind saját kézzel írt kóddal, lehetőleg kevés kódsorban megvalósítva! Ez a Hello, World! – Helló, Világ! példánkban.

https://en.wikipedia.org/wiki/%22Hello,_World!%22_program

Hozzunk létre egy adatbázist a MySQL szerverünkön a https://wlammpp.wordpress.com/2019/10/04/i-resz-wordpress-webfejlesztes-a-wordpress-telepitese korábbi fejezetben ismertetett módon. Van egy adatbázis dump-om, ezt kell “bejátszani”.

-- MySQL dump 10.13 Distrib 5.7.29, for Linux (x86_64)
-- Host: localhost Database: evaap_devel

-- Server version 5.7.29
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table apartments
DROP TABLE IF EXISTS apartments;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE apartments (
id bigint(20) NOT NULL AUTO_INCREMENT,
name varchar(255) DEFAULT NULL,
address varchar(255) DEFAULT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table apartments
LOCK TABLES apartments WRITE;
/*!40000 ALTER TABLE apartments DISABLE KEYS */;
INSERT INTO apartments VALUES (1,'Budapest I','VII. kerulet','2020-03-20 00:26:35','2020-03-20 00:26:35');
/*!40000 ALTER TABLE apartments ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table reservations
DROP TABLE IF EXISTS reservations;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE reservations (
id bigint(20) NOT NULL AUTO_INCREMENT,
name varchar(255) DEFAULT NULL,
email varchar(255) DEFAULT NULL,
phone varchar(255) DEFAULT NULL,
checkin date DEFAULT NULL,
checkout date DEFAULT NULL,
message text,
approved tinyint(1) DEFAULT NULL,
apartment_id int(11) DEFAULT NULL,
locale varchar(255) DEFAULT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table users
DROP TABLE IF EXISTS users;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE users (
id bigint(20) NOT NULL AUTO_INCREMENT,
name varchar(255) DEFAULT NULL,
email varchar(255) DEFAULT NULL,
password varchar(255) DEFAULT NULL,
is_admin tinyint(1) DEFAULT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table users
LOCK TABLES users WRITE;
/*!40000 ALTER TABLE users DISABLE KEYS */;
INSERT INTO users VALUES (1,'admin','gvamosi@linux.hu','manager',1,'2020-03-19 00:00:00','2020-03-19 00:00:00');
/*!40000 ALTER TABLE users ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2020-04-07 14:05:14

Az alábbi módon:

gvamosi@gergo1:/var/www/html/apartman$ mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 50
Server version: 5.7.29 MySQL Community Server (GPL)
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> CREATE DATABASE apartman_devel CHARACTER SET utf8 COLLATE utf8_general_ci;
Query OK, 1 row affected (0.01 sec)
mysql> CREATE USER 'apartman'@'localhost' IDENTIFIED BY 'apartman1';
Query OK, 0 rows affected (0.02 sec)
mysql> GRANT ALL PRIVILEGES ON apartman_devel.* TO 'apartman'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.01 sec)
mysql> \q
Bye
gvamosi@gergo1:/var/www/html/apartman$ mysql -u apartman -p apartman_devel < db/create_and_insert_tables.sql
Enter password:
gvamosi@gergo1:/var/www/html/apartman$

Hozzuk létre az alkalmazás modell rétegét! Ehhez először be kell konfigurálnunk az adatbázist.

gvamosi@gergo1:/var/www/html/apartman/application$ vi config/database.php

$db['default'] = array(
'dsn' => '',
'hostname' => 'localhost',
'username' => 'apartman',
'password' => 'apartman1',
'database' => 'apartman_devel',
'dbdriver' => 'mysqli',
'dbprefix' => '',
'pconnect' => FALSE,
'db_debug' => (ENVIRONMENT !== 'production'),
'cache_on' => FALSE,
'cachedir' => '',
'char_set' => 'utf8',
'dbcollat' => 'utf8_general_ci',
'swap_pre' => '',
'encrypt' => FALSE,
'compress' => FALSE,
'stricton' => FALSE,
'failover' => array(),
'save_queries' => TRUE
);

Ez eddig 3 string (karakterlánc)! 🙂 Aztán meg kell írnunk az Apartments_model.php file-t!

gvamosi@gergo1:/var/www/html/apartman/application$ vi models/Apartments_model.php

<?php
class Apartments_model extends CI_Model {
public function get_last_ten_entries()
{
$query = $this->db->get('apartments', 10);
return $query->result();
}
}

Ez 8 sor. 🙂 Most írjuk meg a controller-t!

gvamosi@gergo1:/var/www/html/apartman/application$ vi controllers/Apartments_controller.php
<?php
class Apartments_controller extends CI_Controller {
public function index() {
echo 'Helló, Világ!!';
$this->load->model('apartments_model');
$data['query'] = $this->apartments_model->get_last_ten_entries();
$this->load->view('apartments', $data);
}
}

Ez újabb 9 sor! Nem sok ez egy listáért? És akkor írjuk meg a view-t is!

gvamosi@gergo1:/var/www/html/apartman/application/views$ vi apartments.php

<ul>
<?php foreach ($query as $item):?>
<li><?php echo $item->address;?></li>
<?php endforeach;?>
</ul>

Ez húzós 5 sor! 🙂 Állítsuk be az útvonalakat, az autoload-ot (még két sor :)), majd hívjuk meg a következő módon a böngészőnkből az “oldalunkat”!

gvamosi@gergo1:/var/www/html/apartman/application$ vi config/routes.php
$route['apartmanok'] = 'apartments_controller/index';

gvamosi@gergo1:/var/www/html/apartman/application$ vi config/autoload.php
$autoload['libraries'] = array('database','session');

http://localhost/apartman/index.php/apartmanok

Listázza az egy darab apartmanunk címét, Budapesten. Siker! 🙂

A WordPress – és ami mögötte van: adatbázis és DbVisualizer

Tehát már van egy honlap-vázunk WordPress-ben. De mi köze ennek az adatbázishoz? Ennek járunk ma utána. 🙂 A DbVisualizer egy JAVA-ban írt adatbázis-feltérképező szoftver. Van Debian “.deb” csomagban is, az alábbi linkről telepíthetjük.

https://www.dbvis.com/download

Installálni igen egyszerű.

root@gergo1:~# dpkg -i dbvis_linux_10_0_22.deb

Ezután jöhet a konfiguráció – új kapcsolat varázsló – pár lépésben. A DbVisualizer ún. JDBC-n (Java Database Connectivity) keresztül kapcsolódik a MySQL adatbázisunkhoz. Esetünkben reverse engineering-re fogjuk használni a szoftvert, azaz a kész adatbázisból nyerjük ki a “tervet”. 🙂

Megfelelő konfiguráció után a DbVisualizer gombnyomásra felrajzolja nekünk mind a 12 adatbázistáblát a “wordpress2019” sémában. Az alábbi ábra egy ún. ER-diagram (Entity-Relationship), magyarul EK-diagram (egyed-kapcsolat). A táblák közti kapcsolatok ugyan nincsenek jelölve, azonban indirekten léteznek. Ennyi az egész WordPress! 🙂

Egy adatbázison (sémán) belül az adatok táblákba vannak rendezve, azon belül mezők vannak, és sorokban adatok, hasonlóan egy fejléccel ellátott Excel-táblázathoz. Az adatbázisunk táblái (entitásai, egyedei) rendre:

  • wp_commentmeta
  • wp_comments
  • wp_links
  • wp_options
  • wp_postmeta
  • wp_posts
  • wp_term_relationships
  • wp_term_taxonomy
  • wp_termmeta
  • wp_terms
  • wp_usermeta
  • wp_users

Talán hihetetlenül hangzik, de a fenti 12 táblában minden WordPress oldal, blog bejegyzés (post), felhasználó (user), beállítás, megjegyzés és metaadatok – tehát minden elfér. Ha ügyesek vagyunk, megnézhetjük az egyes táblák mezőit is. Látható, hogy minden táblában van ID, azaz egyedi azonosító vagy elsődleges kulcs – lásd pl. wp_users tábla, bizonyos táblákban pedig hivatkozásként szerepelnek az egyedi azonosítók, ún. idegen kulcsként – lásd wp_usermeta. (Ezek volnának az indirekt kapcsolatok.) Nézzük meg e két tábla mezőit.

Ez az adatszerkezet egy gyakori “trükk” SQL alapú adatbázisoknál. Tetszőleges kulcsokkal (meta_key) tetszőleges értékeket, azaz meta adatokat (meta_value) tárolhatunk el egy felhasználóhoz. A “wp_usermeta” tábla értékeit a “user_id” mezőben lévő kulcsérték köti a “wp_users” táblához, miközben “umeta_id” néven egyedi azonosítója is van minden táblabejegyzésnek, azaz “sornak”. Később látni fogjuk, hogy miért jó dolog egyedi azonosítót adni minden adatbázis táblának. 🙂

Hogy mi az az SQL (Structured Query Language)? Egész röviden: önálló programnyelv; a séma és az adatbázis objektumok (pl. táblák) definiálása – DDL (Data Definition Language), az adatok lekérdezése (SELECT) – DQL (Data Query Language), az adatok változtatása (INSERT, UPDATE, DELETE) – DML (Data Manipulation Language), továbbá a DCL(Data Control Language) illetve a TCL (Transaction Control Language). Ennyi nagyjából. Bővebben az alábbi linken olvashatunk róla, magyarul.

https://hu.wikipedia.org/wiki/SQL

A legtöbb webes alkalmazásnak az ún. CRUD felépítésnek kell megfelelnie. Ez pedig a Create (létrehoz), Read (olvas), Update (módosít) és Delete (töröl). A terminológia a perzisztens adattárolás alapja. Így tárolhatunk adatokat akár webes környezetben, ún. webes form-ok, azaz űrlapok segítségével. A CRUD-ot könnyen párosíthatjuk az SQL INSERT, SELECT, UPDATE és DELETE parancsokkal vagy akár a HTTP protokollal (bemutatása később). Részletek alább – sajna csak angolul.

https://en.wikipedia.org/wiki/Create,_read,_update_and_delete

És egy jó tanács a végére. Ne próbáljunk meg kézzel belejavítani a tábla-adatokba! Dobhat tőle egy hátast az egész WordPress rendszerünk. Elégedjünk meg az architekturális betekintéssel és mérnöki feltárással! 😀

I. rész – WordPress webfejlesztés – A WordPress telepítése

Hogy miért választottam a WordPress-t? Mivel itt gyakorlatilag nem is kell programozni; ezt a rendszert elég testreszabni és feltölteni tartalommal. A beállítások nagy része programozási ismeretek nélkül is elvégezhető. 🙂

A telepítés előtt győződjünk meg róla, hogy a szerveren rendelkezésre áll-e adatbázis szerver (MySQL vagy MariaDB), Apache vagy nginx webszerver és telepítve van-e a PHP aktuális verziója. Ezekre ugyanis mind szükségünk lesz a saját és ingyenes WordPress oldalunk elindításához. Hogy hogyan győződhetünk meg arról, hogy minden szükséges rendszer telepítve van, és készen áll? Jó megoldás a phpinfo() parancs meghívása a következő módon.

gvamosi@gergo1:/var/www/html$ cat info.php
<?php
phpinfo();
?>

Ezt a file-t ne hagyjuk később “kint’ a szerveren, mert ha valaki véletlenül megtalálja, akkor mindent kideríthet a szerverünk beállításairól! (A webszerver általában a “.php” kiterjesztésű file-okat értelmezi kódként, ezért pl. átnevezhetjük “.akarmi” kiterjesztésűre vagy törölhetjük is.) Nézzünk pár részletet a kimenetéből, ha megnyitjuk böngészőben a http://localhost/info.php cím alatt.

PHP Version 7.0.33-0+deb9u3
Apache Version Apache/2.4.38 (Debian)
MysqlI Support enabled
mysqli.default_port 3306 3306
Client API version mysqlnd 5.0.12-dev - 20150407 - $Id: b5c5906d452ec590732a93b051f3827e02749b83 $

Ezek az adatok úgy vélem magukról beszélnek. 🙂

A MySQL szerverhez unix domain socket-en keresztül és port alapon (3306) tudunk csatlakozni. A portot jól tesszük, ha kizárólag localhost-on engedélyezzük a tűzfalunkban – ez egyébként alapbeállítás -, a socket (mysqld.sock) pedig úgyis csak filerendszerben van – így kizárjuk az adatbázis közvetlen külső elérhetőségét, ezzel is növelve szerverünk biztonságát. 🙂

gvamosi@gergo1:~$ l /var/run/mysqld/
total 8
-rw-r----- 1 mysql mysql 6 Oct 5 01:11 mysqld.pid
srwxrwxrwx 1 mysql mysql 0 Oct 5 01:11 mysqld.sock
-rw------- 1 mysql mysql 6 Oct 5 01:11 mysqld.sock.lock

Ha tárhelyet bérlünk valahol, akkor általában gombnyomásra működik a WordPress telepítése. Én most a hagyományos megoldást mutatnám be röviden terminál használatával. A letöltéshez információkat az alábbi honlapon találunk.

https://hu.wordpress.org/download/

A WordPress telepítése tényleg egyszerű. Legelőször is szükségünk lesz egy új MySQL adatbázisra, felhasználóval és jelszóval – ez tárhelybérlésnél gombnyomásra történik. Egy (MySQL) adatbázis kapcsolathoz négy fontos adatra lesz szükségünk: adatbázis neve, felhasználó neve és jelszava, illetve a hoszt neve. Konzolból az alábbi módon – SQL parancsok használatával – tudunk MySQL adatbázist létrehozni.

gvamosi@gergo1:~$ mysql -u root -p
mysql> CREATE DATABASE dbname CHARACTER SET utf8 COLLATE utf8_general_ci;
mysql> CREATE USER 'user'@'localhost' IDENTIFIED BY 'password';
-- minden jog hozzárendelése az adatbázishoz a felhasználónknak
mysql> GRANT ALL PRIVILEGES ON dbname.* TO 'user'@'localhost';
mysql> FLUSH PRIVILEGES;

Ezután csupán ki kell bontanunk a letöltött állományt a megfelelő helyre, mint “gyökérbe”.

gvamosi@gergo1:/var/www/html/hegypasztor$ wget https://hu.wordpress.org/latest-hu_HU.tar.gz
..
gvamosi@gergo1:/var/www/html/hegypasztor$ tar xzvf latest-hu_HU.tar.gz
..
gvamosi@gergo1:/var/www/html/hegypasztor$ mv wordpress/* .

Eközben a következő – WordPress webes telepítés előtti – képernyő fogad bennünket a http://localhost/hegypasztor/ webcím alatt.

Ezután a wp-config-sample.php állományt át kell másolnunk a wp-config.php állományba, majd kitöltenünk az adatbázis-hozzáférést és a titkos kulcsokat – ahogy a magyar nyelvű dokumentáció írja – pl. egy vi editorral.

gvamosi@gergo1:/var/www/html/hegypasztor$ cp wp-config-sample.php wp-config.php
gvamosi@gergo1:/var/www/html/hegypasztor$ vi wp-config.php

Mihelyst elkészülünk a wp-config.php file kitöltésével, és megnyomtuk a “Rajta!” gombot, az alábbi képernyőt láthatjuk.

Ezután a webes felületen pár mező (honlapnév, webes adminisztrátor felhasználó neve és jelszava) kitöltésével gombnyomásra települ a WordPress honlapunk.

És máris az első bejelentkezésünknél tartunk.

Ezután vethetünk egy pillantást az alap honlapunkra. Ez a kiindulási pontunk a fejlesztéshez! Mivel be vagyunk jelentkezve adminisztrátorként, ezért a képernyő tetején egy menüsort találunk.

Illetve megnézhetjük az adminisztrátori képernyőt, a vezérlőpulttal. 🙂

Mit értünk webprogramozás, webfejlesztés alatt

A 21. századra rárobbant az információs kor. A World Wide Web, azaz a WWW kora, az Internet multimédia-tartalommal bíró kommunikációs csatornája, a kiszolgáló oldali (web)szerverek – ez volna az Apache vagy az nginx -, és a kliens oldali böngészők – Chrome, Firefox, Edge – kommunikációjaként előálló interakció. Az egész Internet lényegében tekinthető egy online adatfeldolgozó, -tároló és -továbbító rendszernek. Ezért fontos beszélni az adatok tárolására és feldolgozására szolgáló többnyire SQL alapú adatbázis szerverekről is – ezek a MySQL vagy a MariaDB -, illetve a szerver oldali adatfeldolgozó script-ekről, azaz webszerverbe ágyazott szoftverrendszerekről is, melyek sok esetben Perl és PHP nyelveken íródnak. Fontos még említeni a kliens oldal legütősebb script-nyelvét, a JavaScript-et. Legismertebb képviselője a széles körben elterjedt JavaScript könyvtár, a jQuery (https://jquery.com). Az adatok továbbítása valójában a hálózaton keresztül valósul meg, mindig kliens-szerver kommunikációban – lévén ugyanis a szerver egyszerre több klienst is kiszolgál, továbbá a szerverek egymással is tudnak beszélgetni, de adott esetben egy kliens is egyszerre több szerverhez kapcsolódhat. 🙂 Jelen írás a fentiekben leírtak bemutatásával szeretne foglalkozni.

A webfejlesztés tehát minden esetben egy szoftver-rendszerben történik. Egy jó kiindulási szoftver környezet található az alábbi linken.

https://www.apachefriends.org/hu/index.html

Ebben az írásomban szeretném az operációs rendszer, az adatbázis szerver, a webszerver és a kliens oldal összhangját három esettanulmányon keresztül bemutatni. Egyébként a fenti link alatt található XAMPP-ot hívják még LAMP-nak is. Szinte mindegy, hogy melyik variánst vesszük, ugyanaz az eredmény. 🙂 Ez az architektúra (szoftverépítészeti megoldás) igen elterjedt és nagy népszerűségnek örvend, többek között a nem enterprise (nem nagyvállalati vagy nagy volumenű üzleti) felhasználási területen.

https://hu.wikipedia.org/wiki/LAMP_(szoftvercsomag)

Megjegyezném, hogy ezen technológiák ismerete enterprise megoldások kiindulási pontja lehet, illetve hogy nagy dolgokat is meg lehet a segítségükkel valósítani. 🙂

A három esettanulmány – három rész – rendre a következők lesznek:

  1. A nyílt forráskódú, PHP-ban írt WordPress (https://hu.wordpress.org/), weboldalak, blogok és egyéb webes alkalmazások motorjának bemutatása egy webalkalmazáson keresztül.
  2. A nyílt forráskódú CodeIgniter (http://codeigniter.hu/) PHP keretrendszer megismertetése egy webalkalmazáson keresztül. Ez a keretrendszer a letisztult MVC (Model – View – Controller) programtervezési mintát (Design Patterns) követi.
  3. Harmadikként pedig a 30 éve folyamatosan fejlesztett Perl nyelven fogunk webalkalmazást, azon belül is klasszikus CGI-t (Common Gateway Interface https://hu.wikipedia.org/wiki/Common_Gateway_Interface) illetve FastCGI-t írni, hova tovább mod_perl Apache beépülő modul (https://en.wikipedia.org/wiki/Mod_perl) alkalmazást. 🙂

Végül lássuk egy szemléltető diagramon az MVC pattern-t. A lényege, hogy mindent a Controller (vezérlő) végez, a modellváltoztatást is, és a felhasználó nézetének változtatását is. 🙂