'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 1:[1]

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);
    }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 AgBRAT