Search code examples
phpjsonajaxcodeigniterrating-system

CodeIgniter 4: Like and Dislike Functionality


I have implemented the following like/dislike code inspired by Code with AWA.

EDIT I'm using CodeIgniter now if that changes anything. I have updated my question with the code in MVC style. I also have CSRF tokens enabled and that might be my issue?

The post request appears to be working (when I click like or dislike buttons the correct values are displayed in inspect mode. But other than that, nothing else is working (i.e. updating database, displaying like/dislike counts, etc.).

POST Request

Error Response This is my Ajax script view_image.js:

$(document).ready(function() {

        // if the user clicks the subscribe button
        $(".subscibe-button").on("click", function() {
            var user = $(this).data("user");
            $clicked_btn = $(this);

            if ($clicked_btn.hasClass("fa-rss")) {
                action = "subscribe";
            } else if ($clicked_btn.hasClass("fa-user-check")) {
                action = "unsubscribe";
            }

            $.ajax ({
                url: "<?= site_url('images/view_image/index') ?>",
                type: "post",
                dataType: 'json',
                data: {
                    "action": action,
                    "user": user
                },
                success: function(data) {
                    var res = JSON.parse(data);
                    $("input[name='csrf_test_name']").val(result['csrf']);
                }

            });
        });

        // if the user clicks on the like button
        $(".like-button").on("click", function() {
            var viewkey = $(this).data("viewkey");
            $clicked_btn = $(this);

            if ($clicked_btn.hasClass("fa-thumbs-o-up")) {
                action = "like";
            } else if ($clicked_btn.hasClass("fa-thumbs-up")) {
                action = "unlike";
            }

            $.ajax({
                url: "<?= site_url('images/view_image/index') ?>",
                type: "post",
                dataType: "json",
                data: {
                    "action": action,
                    "viewkey": viewkey
                },
                success: function(data) {
                    var res = JSON.parse(data);

                    if (action == "like") {
                        $clicked_btn.removeClass("fa-thumbs-o-up");
                        $clicked_btn.addClass("fa-thumbs-up");
                    } else if (action == "unlike") {
                        $clicked_btn.removeClass("fa-thumbs-up");
                        $clicked_btn.addClass("fa-thumbs-o-up");
                    }

                    // display number of likes and dislikes
                    $clicked_btn.siblings('span.likes').text(res.likes);
                    $clicked_btn.siblings('span.dislikes').text(res.dislikes);

                    // Change button styling of the other button if user is reacting the second time to image
                    $clicked_btn.siblings("i.fa-thumbs-down").removeClass("fa-thumbs-down").addClass("fa-thumbs-o-down");
                }
            });
        });
        // If the user clicks on the dislike button
        $(".dislike-button").on("click", function() {
            var viewkey = $(this).data("viewkey");
            $clicked_btn = $(this);

            if ($clicked_btn.hasClass("fa-thumbs-o-down")) {
                action = "dislike";
            } else if ($clicked_btn.hasClass("fa-thumbs-down")) {
                action = "undislike";
            }

            $.ajax({
                url: "<?= site_url('images/view_image/index') ?>",
                type: "post",
                dataType: "json",
                data: {
                    "action": action,
                    "viewkey": viewkey
                },
                success: function(data) {
                    var res = JSON.parse(data);

                    if (action == "dislike") {
                        $clicked_btn.removeClass("fa-thumbs-o-down");
                        $clicked_btn.addClass("fa-thumbs-down");
                    } else if (action == "undislike") {
                        $clicked_btn.removeClass("fa-thumbs-down");
                        $clicked_btn.addClass("fa-thumbs-o-down");
                    }

                    // display number of likes and dislikes
                    $clicked_btn.siblings('span.likes').text(res.likes);
                    $clicked_btn.siblings('span.dislikes').text(res.dislikes);

                    // Change button styling of the other button if user is reacting the second time to image
                    $clicked_btn.siblings("i.fa-thumbs-up").removeClass("fa-thumbs-up").addClass("fa-thumbs-o-up");
                }
            });
        });

This is an excerpt from my View view_image.php file showing where the Ajax request is referring to.

<div class='view-image-info-r'>
                        <i <?php if ($userLiked): ?>
                            class='fa fa-thumbs-up like-button'  
                        <?php else: ?>
                            class='fa fa-thumbs-o-up like-button' 
                        <?php endif ?>
                            data-viewkey="<?= $image['viewkey']; ?>"></i>

                        <span class='likes'><?= esc($likes); ?></span>
                    </div>
                    
                    <div class='view-image-info-r'>
                        <i <?php if ($userDisliked): ?>
                            class='fa fa-thumbs-down dislike-button' 
                        <?php else: ?>
                            class='fa fa-thumbs-o-down dislike-button' 
                        <?php endif ?>
                            data-viewkey="<?= esc($image['viewkey']); ?>"></i>

                        <span class='dislikes'><?= esc($dislikes); ?></span>
                    </div>

And this is my Model ActionModel.php, containing the functions that insert/update which action, be it like/unlike/dislike/undislike.

public function insertAction($input, $where)
    {
        $this->$db = \Config\Database::connect();
        $this->$builder = $db->table('actions');

        $data = [];

        $viewkey = $input['viewkey'];

        switch($input['action']) {

            case 'dislike':

                $input['action'] = 0;

                if (userLiked($viewkey) == false) {
                    
                    $this->builder->insert($input);
                
                } else {

                    $this->builder->where($where);
                    $this->builder->update($input);
                }

                break;

            case 'undislike':

                $where = [
                    'action' => 0,
                ];

                $this->builder->where($where)->delete();

                break;

            case 'like':

                $data = [
                    'action' => 1,
                ];

                if (userDisliked($viewkey) == false) {
                    
                    $this->builder->insert($input);
                
                } else {

                    $this->builder->where($where);
                    $this->builder->update($input);
                }

                break;

            case 'unlike':

                $where = [
                    'action' => 1,
                ];

                $this->builder->where($where)->delete();

                break;

            case 'view':
                                    
                $data = [
                    'action' => 2,
                ];

                if (userViewed($viewkey) == false) {
                    
                    $this->builder->insert($input);
                
                } else {

                    $this->builder->where($where);
                    $this->builder->update($input);
                }

                break;

            default:

                break;
        }

        return getActions($viewkey);
        exit(0);
    }

public function getActions($viewkey)
    {
        $actionModel = new ActionModel();

        $data = [];

        $data['dislikes'] = $actionModel->getDislikes($viewkey);
        $data['likes'] = $actionModel->getLikes($viewkey);
        $data['views'] = $actionModel->getViews($viewkey);
        $data['favorites'] = $actionModel->getFavorites($viewkey);

        return json_encode(['data' => $data, 'csrf' => csrf_hash()]);
    }

public function userDisliked($viewkey) 
    {
        $actionModel = new ActionModel();
        
        $where = [];

        $where = [
            'viewkey'   => $viewkey,
            'username'  => session()->get('username'),
            'action'    => 0,
        ];

        if ($actionModel->where($where)->first()) {

            return true;
        
        } else {

            return false;
        } 
    }

    public function userLiked($viewkey) 
    {
        $actionModel = new ActionModel();

        $where = [];

        $where = [
            'viewkey'   => $viewkey,
            'username'  => session()->get('username'),
            'action'    => 1,
        ];

        if ($actionModel->where($where)->first()) {

            return true;
        
        } else {

            return false;
        } 
    }

Here is my Controller View_Image.php:

public function index() 
    {
        $viewkey = $this->request->uri->getSegment(4);

        if ($this->request->isAJAX()) {

            $request = service('request')->getPost('data');
            $postData = $request->getPost();

            $data = [];

            $data['token'] = csrf_hash();

            $validation = \Config\Services::validation();

            if ($validation->withRequest($this->request)->run() == FALSE) {

                if (! empty($this->request->getVar('action')) && ! empty($this->request->getVar('user')) && ! empty(session()->get('username'))) {

                    $data = $input = [];

                    $input = [
                        'subscriber'    => $session()->get('username'),
                        'user_profile'  => $this->request->getVar('user'), 
                        'action'        => $this->request->getVar('action'),
                    ];

                    $this->subscribeModel->insertSubscriber($input);
                }

                if (! empty($this->request->getVar('action')) && ! empty($viewkey) && ! empty(session()->get('username'))) {

                    $data = $where = [];

                    $input = [
                        'viewkey'   => $viewkey,
                        'username'  => session()->get('username'),
                        'action'    => $this->request->getVar('action'),
                        'modified'  => date('Y-m-d H:i:s'),
                    ];

                    $where = [
                        'viewkey'   => $viewkey,
                        'username' => session()->get('username'),
                    ];

                    $actionCount = $this->actionModel->insertAction($input, $where);

                    return $actionCount;
                
                } 
            
            } 
        } 

I have tried to limit pasting of my code here so all the echo views and namespace/class declarations have been excised.

Here is my UI: Like/Dislike UI


Solution

  • I solved my own question. This was a security issue. This is how you can regenerate the CSRF token within the AJAX request. Be sure to add the line <?= csrf_field() ?> in your view somewhere. And pass the csrf_token() and csrf_hash() in your controller/model (wherever you are returning the JSON response. I do so in my model.

    <script>
        $(document).ready(function() {  
            var csrfName = "<?= csrf_token(); ?>";
            var csrfHash = "<?= csrf_hash(); ?>"; 
    
            // if the user clicks on the like button
            $(".like-button").on("click", function() {
                var viewkey = $(this).data("viewkey");
                $clicked_btn = $(this);
    
                if ($clicked_btn.hasClass("fa-thumbs-o-up")) {
                    action = "like";
                } else if ($clicked_btn.hasClass("fa-thumbs-up")) {
                    action = "unlike";
                }
    
                $.ajax({
                    url: "<?= base_url('/images/view_image/index'); ?>",
                    type: "post",
                    dataType: "json",
                    data: {
                        [csrfName]: csrfHash,
                        "action": action,
                        "viewkey": viewkey,
                    },
                    headers: {
                        'X-Requested-With': 'XMLHttpRequest',
                    },
                    success: function(data) {
                        var res = data;
                        csrfName = data.csrfName;
                        csrfHash = data.csrfHash;
    
                        if (action == "like") {
                            $clicked_btn.removeClass("fa-thumbs-o-up");
                            $clicked_btn.addClass("fa-thumbs-up");
                        } else if (action == "unlike") {
                            $clicked_btn.removeClass("fa-thumbs-up");
                            $clicked_btn.addClass("fa-thumbs-o-up");
                        }
    
                        // display number of likes and dislikes
                        $clicked_btn.siblings('span.likes').text(res.likes);
                        $clicked_btn.siblings('span.dislikes').text(res.dislikes);
    
                        // Change button styling of the other button if user is reacting the second time to image
                        $clicked_btn.siblings("i.fa-thumbs-down").removeClass("fa-thumbs-down").addClass("fa-thumbs-o-down");
                    }
                });
            });
            // If the user clicks on the dislike button
            $(".dislike-button").on("click", function() {
                var viewkey = $(this).data("viewkey");
                $clicked_btn = $(this);
    
                if ($clicked_btn.hasClass("fa-thumbs-o-down")) {
                    action = "dislike";
                } else if ($clicked_btn.hasClass("fa-thumbs-down")) {
                    action = "undislike";
                }
    
                $.ajax({
                    url: "<?= base_url('/images/view_image/index'); ?>",
                    type: "post",
                    dataType: "json",
                    data: {
                        [csrfName]: csrfHash,
                        "action": action,
                        "viewkey": viewkey
                    },
                    headers: {
                        'X-Requested-With': 'XMLHttpRequest',
                    },
                    success: function(data) {
                        var res = data;
                        csrfName = data.csrfName;
                        csrfHash = data.csrfHash;
    
                        if (action == "dislike") {
                            $clicked_btn.removeClass("fa-thumbs-o-down");
                            $clicked_btn.addClass("fa-thumbs-down");
                        } else if (action == "undislike") {
                            $clicked_btn.removeClass("fa-thumbs-down");
                            $clicked_btn.addClass("fa-thumbs-o-down");
                        }
    
                        // display number of likes and dislikes
                        $clicked_btn.siblings('span.likes').text(res.likes);
                        $clicked_btn.siblings('span.dislikes').text(res.dislikes);
    
                        // Change button styling of the other button if user is reacting the second time to image
                        $clicked_btn.siblings("i.fa-thumbs-up").removeClass("fa-thumbs-up").addClass("fa-thumbs-o-up");
                    }
                });
            });
       });
    </script>
    

    Here is my controller View_Image.php index function:

    $viewkey = $this->request->uri->getSegment(4);
    
            if ($this->request->IsAjax()) {
    
                if (! empty($this->request->getPost('action')) && ! empty($this->request->getPost('viewkey')) && ! empty(session()->get('username'))) {
    
                    $input = $where = [];
    
                    $input = [
                        'action'    => $this->request->getPost('action'),
                        'username' => session()->get('username'),
                        'viewkey'   => $this->request->getPost('viewkey'),
                        'modified'  => date('Y-m-d H:i:s'),
                    ];
    
                    $actionCount = $this->actionModel->insertAction($input);
    
                    return $actionCount;
                
                } 
            } 
    

    ActionModel.php

    public function insertAction($input)
        {
            $db = \Config\Database::connect();
            $builder = $db->table('actions');
    
            $data = $where = [];
    
            $viewkey = $input['viewkey'];
    
            switch($input['action']) {
    
                case 'dislike':
    
                    $data = [
                        'username'  => $input['username'],
                        'action'    => 0,
                        'viewkey'   => $input['viewkey'],
                        'modified'  => $input['modified'],
                    ];
    
                    if ($this->userLiked($viewkey) == false) {
                        
                        $builder->insert($data);
                    
                    } else {
    
                        $where = [
                            'username'  => $input['username'],
                            'viewkey'   => $input['viewkey'],
                            'action'    => 1,
                        ];
    
                        $builder->where($where)->delete();
                        $builder->insert($data);
                    }
    
                    break;
    
                case 'undislike':
    
                    $where = [
                        'action' => 0,
                    ];
    
                    $builder->where($where)->delete();
    
                    break;
    
                case 'like':
    
                   $data = [
                        'username'  => $input['username'],
                        'action'    => 1,
                        'viewkey'   => $input['viewkey'],
                        'modified'  => $input['modified'],
                    ];
    
                    if ($this->userDisliked($viewkey) == false) {
                        
                        $builder->insert($data);
                    
                    } else {
    
                        $where = [
                            'username'  => $input['username'],
                            'viewkey'   => $input['viewkey'],
                            'action'    => 0,
                        ];
    
                        $builder->where($where)->delete();
                        $builder->insert($data);
                    }
    
                    break;
    
                case 'unlike':
    
                    $where = [
                        'action' => 1,
                    ];
    
                    $builder->where($where)->delete();
    
                    break;
        return $this->getActions($viewkey);
            exit(0);
        }
    
    public function userDisliked($viewkey) 
        {
            $actionModel = new ActionModel();
            
            $where = [];
    
            $where = [
                'viewkey'   => $viewkey,
                'username'  => session()->get('username'),
                'action'    => 0,
            ];
    
            if ($actionModel->where($where)->first()) {
    
                return true;
            
            } else {
    
                return false;
            } 
        }
    
        public function userLiked($viewkey) 
        {
            $actionModel = new ActionModel();
    
            $where = [];
    
            $where = [
                'viewkey'   => $viewkey,
                'username'  => session()->get('username'),
                'action'    => 1,
            ];
    
            if ($actionModel->where($where)->first()) {
    
                return true;
            
            } else {
    
                return false;
            } 
        }
    
     public function getActions($viewkey)
        {
            $data = [];
    
            $data = [
                'csrfName'  => csrf_token(),
                'csrfHash'  => csrf_hash(),
                'dislikes'  => $this->getDislikes($viewkey),
                'likes'     => $this->getLikes($viewkey),
                'views'     => $this->getViews($viewkey),
                'favorites' => $this->getFavorites($viewkey),
            ];
    
            return json_encode($data);
        }