Showing posts with label AJAX. Show all posts
Showing posts with label AJAX. Show all posts

Tuesday, 22 September 2026

Web Tutorial: The List View (Part 1/4)

List views are a classic way to present data. I used to work with them in PHP, back in the 2010s. As my experience accumulated, I got a better sense of what worked, what didn't, what scaled, and what sounded like a good idea and just wasn't in practice.

With the help of jQuery, I'd like to walk you through some of the regular features of a list view that I worked over and over with.

To facilitate this Web Tutorial, I have also created a database of sample records in Oracle APEX and created an API endpoint to retrieve those records. It's a collection of records from a hypothetical library, with imaginary titles, authors and people. The categories, though, are real - they're from BISAC.

Here's what a sample of the data looks like in CSV. At least, this was what I imported into Oracle APEX. In this Web Tutorial, we wont be dealing with the API endpoint creation at all - we'll just call the endpoint and grab the data.
Title,Category,Author,Name,BorrowDate,ReturnBy,ReturnDate,Fine
Help my program just crashed,Computers,RJ Neville,Olive Yao,2025-03-01,2025-04-01,2025-03-22,0
The Saraville Murders,True Crime,Lillian Yang,Faith Wentz,2025-03-01,2025-05-01,2025-04-21,0
The Moon's Reflection in the Padi Fields,Fiction,Rudy Ismail,Charles Wong,2025-03-01,2025-04-01,2025-03-30,0
It's Now or Never!,Young Adult Fiction,Kara Leow,Lin Yiheng,2025-03-01,2025-04-01,2025-04-02,0.1
Dreaded Doberman: Man's Best Friend... or are they?,Pets,Selina Perez,Teo Teng Hwee,2025-03-01,202

...


Here's a sample API endpoint for these records. In this URL, "2026" and "3" are the year and month that the books were borrowed. Do note that we're not doing security for this - it's a purely display feature.
https://oracleapex.com/ords/teochewthunder/borrowing/2026/3


Here's some boilerplate HTML. We want to include jQuery, and the starting object, lv. In the styles, the outline of divs has been set to red. This will help us with layout until we're ready to turn it off.
<!DOCTYPE html>
<html>
  <head>
    <title>List View</title>

    <style>
      div {outline: 1px solid red;}
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

    <script>
      const lv =
      {

      };
    </script>
  </head>

  <body>

  </body>
</html>


In the body tag, we have a div tag with the id listContent. Within it, here are four different divs with the ids listFilters, listHeader, listRows and listPaging. The first and last div tag have a hr tag within. You may not think much about it now, but that horizontal rule will be a Godsend later.
<body>
  <div id="listContent">
    <div id="listFilters">
      <hr />
    </div>  
        
    <div id="listHeader">

    </div>

    <div id="listRows">
      
    </div>

    <div id="listPaging">
      <hr />
    </div>
  </div>

</body>


listContent is a thousand pixels wide, with the height property set to auto. We center it using the margin property, and set a font. The other divs within will just inherit their parent's width.
<style>
  div {outline: 1px solid red;}

  #listContent
  {
    width: 1000px;
    height: auto;
    margin: 0 auto 0 auto;
    font-size: 12px;
    font-family: verdana;
  }

  #listHeader
  {
    width: 100%
  }

  #listRows
  {
    width: 100%
  }

  #listPaging
  {
    width: 100%;
  }

</style>


Now for every column we'll have, we add a div...
<div id="listHeader">
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>

</div>


...and insert a button with that column's name in it. Note that there is one more button at the beginning - that is just a column header for serialization of records. Note that each of these buttons, save the first one, has an unique id.
<div id="listHeader">
  <div><button>No</button></div>
  <div><button id="btnSortTitle">Title</button></div>
  <div><button id="btnSortAuthor">Author</button></div>
  <div><button id="btnSortCategory">Category</button></div>
  <div><button id="btnSortName">Name</button></div>
  <div><button id="btnSortBorrowdate">Borrowed</button></div>
  <div><button id="btnSortReturnBy">Return By</button></div>
  <div><button id="btnSortReturndate">Returned</button></div>
  <div><button id="btnSortFine">Fine</button></div>
</div>


Now you see a bunch of buttons. These are supposed to be clickable headers.


So let's style them accordingly. Each of these buttons is styled using the sortButton CSS class. Only the first button uses noButton.
<div id="listHeader">
  <div><button class="noButton">No</button></div>
  <div><button id="btnSortTitle" class="sortButton">Title</button></div>
  <div><button id="btnSortAuthor" class="sortButton">Author</button></div>
  <div><button id="btnSortCategory" class="sortButton">Category</button></div>
  <div><button id="btnSortName" class="sortButton">Name</button></div>
  <div><button id="btnSortBorrowdate" class="sortButton">Borrowed</button></div>
  <div><button id="btnSortReturnBy" class="sortButton">Return By</button></div>
  <div><button id="btnSortReturndate" class="sortButton">Returned</button></div>
  <div><button id="btnSortFine" class="sortButton">Fine</button></div>
</div>


Here's the styling. Visually, sortButton and noButton share the same visuals. No background or border - it just looks like black text wth a fixed height.
#listPaging
{
  width: 100%;
}

.sortButton, .noButton
{
  width: auto;
  padding: 0px;
  height: 1.5em;
  font-size: 1em;
  background: transparent;
  border: none;
  color: rgb(0, 0, 0);
}


Now they no longer look like buttons.


Time for a bit of layout magic. Add the CSS class listViewColumns to the listHeader div.
<div id="listHeader" class="listViewColumns">
  <div><button class="noButton">No</button></div>
  <div><button id="btnSortTitle" class="sortButton">Title</button></div>
  <div><button id="btnSortAuthor" class="sortButton">Author</button></div>
  <div><button id="btnSortCategory" class="sortButton">Category</button></div>
  <div><button id="btnSortName" class="sortButton">Name</button></div>
  <div><button id="btnSortBorrowdate" class="sortButton">Borrowed</button></div>
  <div><button id="btnSortReturnBy" class="sortButton">Return By</button></div>
  <div><button id="btnSortReturndate" class="sortButton">Returned</button></div>
  <div><button id="btnSortFine" class="sortButton">Fine</button></div>
</div>


In the CSS, we set the display property to grid, and then specify the widths of 8 columns, with a 5 pixel gap between columns.
#listRows
{
  width: 100%
}

.listViewColumns
{
  display: grid;
  grid-template-columns: 1fr 10fr 6fr 6fr 6fr 5fr 5fr 5fr 3fr;
  gap: 5px;
}


#listPaging
{
  width: 100%;
}


There, it's in one line now.


Time for JavaScript!

Now we're going to populate those rows. We'll first need to flesh out the lv object. We first have some properties.
- pageSize: How many records are shown on a page at any time. We'll set it at 50 as a default, but this won't affect anything right now.
- pageNo: The current page being shown. The default is 1. The value may change as pages are gone through.
- sort: An array which we'll use to carry out sorting later. Leave it empty for now.
- search: A string that will be used for searching values. Leave empty for now.
- records: An array of the current records being worked on. Leave it empty for now.
<script>
  const lv =
  {
    pageSize: 50,
    pageNo: 1,
    sort: [],
    search: "",
    records: []

  };
</script>


The next two are methods which will be relevant immediately. getRecords() will populate the records array. By default, it will fetch records in March 2025, so its two parameters, year and month, are set accordingly. filterRecords() will display the contents of records.
<script>
  const lv =
  {
    pageSize: 50,
    pageNo: 1,
    sort: [],
    search: "",
    records: [],
    getRecords: function(year = 2025, month = 3)
    {

    },
    filterRecords: function()
    {

    }

  };
</script>


In getRecords(), we have a jQuery AJAX call.
getRecords: function(year = 2025, month = 3)
{
  $.ajax(
    {

    }
  );  
      
},


In it, we specify the URL and that it will be a GET operation. The URL is basically a template string that incorporates year and month.
getRecords: function(year = 2025, month = 3)
{
  $.ajax(
    {
      url: `https://oracleapex.com/ords/teochewthunder/borrowing/${year}/${month}`,
      method: "GET",
      success: function(data)
      {

      },
      error: function (xhr, status, error)
      {
        console.log(xhr);
      }

    }
  );        
},


On success, we set records to the items array of data, and then run the filterRecords() method.
getRecords: function(year = 2025, month = 3)
{
  $.ajax(
    {
      url: `https://oracleapex.com/ords/teochewthunder/borrowing/${year}/${month}`,
      method: "GET",
      success: function(data)
      {
        lv.records = data.items;
        lv.filterRecords();

        return;

      },
      error: function (xhr, status, error)
      {
        console.log(xhr);
      }
    }
  );        
},


We begin the method with a return statement at the end. At the start, we clear the contents of the listRows div. Then we declare filtered. It's an independent copy of records, using the structuredClone() function.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  return;

},


Then we iterate through filtered using a forEach() loop. x is the element and i is the index. If you're wondering why the array's named "filtered", that's because later on we'll be putting it through filtering.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  filtered.forEach(
    (x, i) =>
    {
        
    }
  );


  return;
},


Here, we create a div element, row. It will be styled using the CSS class listViewColumns, which means it will be a single-line grid just like the header! Then we append row to listRows.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  filtered.forEach(
    (x, i) =>
    {
      const row = $("<div></div>");
      row.addClass("listViewColumns");

      $("#listRows").append(row);      
      
    }
  );

  return;
},


But before that, we declare col. Repeatedly, we define col as a div element, use the text() method to insert that element's data, and append it to row. We begin with the serial number, which is just 1 added to i so we don't have the 0 value at the start. The 0 would make sense to programmers. To everyone else, not so much.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  filtered.forEach(
    (x, i) =>
    {
      const row = $("<div></div>");
      row.addClass("listViewColumns");

      let col;
      col = $("<div></div>");
      col.text(i + 1);
      row.append(col);
      col = $("<div></div>");
      col.text(x.title);
      row.append(col);
      col = $("<div></div>");
      col.text(x.author);
      row.append(col);
      col = $("<div></div>");
      col.text(x.category);
      row.append(col);
      col = $("<div></div>");
      col.text(x.name);
      row.append(col);
      col = $("<div></div>");
      col.text(x.borrowdate);
      row.append(col);
      col = $("<div></div>");
      col.text(x.returnby);
      row.append(col);
      col = $("<div></div>");
      col.text(x.returndate);
      row.append(col);
      col = $("<div></div>");
      col.text(x.fine);
      row.append(col);

      $("#listRows").append(row);            
    }
  );

  return;
},


Note that for all numerical and date data, we'll also style col using the figure CSS class.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  filtered.forEach(
    (x, i) =>
    {
      const row = $("<div></div>");
      row.addClass("listViewColumns");

      let col;
      col = $("<div></div>");
      col.text(i + 1);
      col.addClass("figure");
      row.append(col);
      col = $("<div></div>");
      col.text(x.title);
      row.append(col);
      col = $("<div></div>");
      col.text(x.author);
      row.append(col);
      col = $("<div></div>");
      col.text(x.category);
      row.append(col);
      col = $("<div></div>");
      col.text(x.name);
      row.append(col);
      col = $("<div></div>");
      col.text(x.borrowdate);
      col.addClass("figure");
      row.append(col);
      col = $("<div></div>");
      col.text(x.returnby);
      col.addClass("figure");
      row.append(col);
      col = $("<div></div>");
      col.text(x.returndate);
      col.addClass("figure");
      row.append(col);
      col = $("<div></div>");
      col.text(x.fine);
      col.addClass("figure");
      row.append(col);
      $("#listRows").append(row);            
    }
  );

  return;
},


figure is just setting the text to align right.
#listPaging
{
  width: 100%;
}

.figure
{
  text-align: right;
}


.sortButton, .noButton
{
  width: auto;
  padding: 0px;
  height: 1.5em;
  font-size: 1em;
  background: transparent;
  border: none;
  color: rgb(0, 0, 0);
}


After the lv object, set getRecords() to run as soon as the document loads.
          col.text(x.fine);
          col.addClass("figure");
          row.append(col);
          $("#listRows").append(row);            
        }
      );

      return;
    }
  };

  $(document).ready(
    function()
    {
      lv.getRecords();
    }
  );

</script>


You see here, we have the records! But the dates look weird...


No matter. Let's run all the date values through the prettifyDate() method.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  filtered.forEach(
    (x, i) =>
    {
      const row = $("<div></div>");
      row.addClass("listViewColumns");

      let col;
      col = $("<div></div>");
      col.text(i + 1);
      col.addClass("figure");
      row.append(col);
      col = $("<div></div>");
      col.text(x.title);
      row.append(col);
      col = $("<div></div>");
      col.text(x.author);
      row.append(col);
      col = $("<div></div>");
      col.text(x.category);
      row.append(col);
      col = $("<div></div>");
      col.text(x.name);
      row.append(col);
      col = $("<div></div>");
      col.text(this.prettifyDate(x.borrowdate));
      col.addClass("figure");
      row.append(col);
      col = $("<div></div>");
      col.text(this.prettifyDate(x.returnby));
      col.addClass("figure");
      row.append(col);
      col = $("<div></div>");
      col.text(this.prettifyDate(x.returndate));
      col.addClass("figure");
      row.append(col);
      col = $("<div></div>");
      col.text(x.fine);
      col.addClass("figure");
      row.append(col);
      $("#listRows").append(row);            
    }
  );

  return;
},


Create this method. It basically just cleans up the value by using the split() method to divide up the string by the "T" character, and then returning only the first element of the resultant array.
          col.text(x.fine);
          col.addClass("figure");
          row.append(col);
          $("#listRows").append(row);            
        }
      );

      return;
    },
    prettifyDate: function(dt)
    {
      var arr = dt.split("T");
      return arr[0];
    }

  };
</script>


Now it's looking much better.


It's a small thing, but let's get this out of the way. Create CSS class shade. Its background color is a very translucent black.
.listViewColumns
{
  display: grid;
  grid-template-columns: 1fr 10fr 6fr 6fr 6fr 5fr 5fr 5fr 3fr;
  gap: 5px;
}

.shade
{
  background-color: rgba(0, 0, 0, 0.1);
}


#listPaging
{
  width: 100%;
}


Then in filterRecords(), when styling row using the listViewColumns CSS class, we check to see if index i is even, using the modulus operator. If so, also style row using shade.
const row = $("<div></div>");
row.addClass("listViewColumns");
if (i % 2 == 0) row.addClass("shade");

let col;
col = $("<div></div>");
col.text(i + 1);
col.addClass("figure");
row.append(col);



More presentable and readable now.

That's about it for now. We'll do paging through records next.

Next

Implementing paging.

Sunday, 9 May 2021

Your AJAX Starter Kit, WordPress edition (Part 2/2)

I'm back!

Implementing AJAX, of course, requires a lot more than just JavaScript. We've done the front-end, now we need a back-end. AJAX in WordPress has its own implementation, via admin-ajax.php. To do this, we need to pass the URL of that location, like so.

plugins/tt_ajax_test/tt_ajax.php
<?php
/**
 * Plugin Name: AJAX Test
 * Plugin URI: http://www.teochewthunder.com
 * Description: This plugin implements and tests AJAX functionality
 * Version: 1.0.0
 * Author: TeochewThunder
 * Author URI: http://www.teochewthunder.com
 * License: GPL2
 */

function tt_ajax_listener() {
    wp_register_script( 'ajax_listener', plugin_dir_url( __FILE__ ) . '/js/tt_ajax_listener.js', ['jquery'] );
    wp_localize_script( 'ajax_listener', 'objAjax', ['ajaxurl' => admin_url( 'admin-ajax.php' )]);        

    wp_enqueue_script( 'ajax_listener' );
}

add_action( 'wp_enqueue_scripts', 'tt_ajax_listener' );


Then have some back-end code, defined in a function. In this case, it will take a string passed in via a POST, and use it as the format for a date() function. And most importantly, encode the output using json_encode(), then echo it.

plugins/tt_ajax_test/tt_ajax.php
<?php
/**
 * Plugin Name: AJAX Test
 * Plugin URI: http://www.teochewthunder.com
 * Description: This plugin implements and tests AJAX functionality
 * Version: 1.0.0
 * Author: TeochewThunder
 * Author URI: http://www.teochewthunder.com
 * License: GPL2
 */

function tt_ajax_listener() {
    wp_register_script( 'ajax_listener', plugin_dir_url( __FILE__ ) . '/js/tt_ajax_listener.js', ['jquery'] );
    wp_localize_script( 'ajax_listener', 'objAjax', ['ajaxurl' => admin_url( 'admin-ajax.php' )]);        

    wp_enqueue_script( 'ajax_listener' );
}

add_action( 'wp_enqueue_scripts', 'tt_ajax_listener' );

function tt_ajax_call() {
    $date = date($_POST['format'], strtotime('now'));
    echo json_encode(['date' => $date]);
    die();
}


After that,we use add_action() to add this function to the hooks wp_ajax_tt_ajax_call and wp_ajax_nopriv_tt_ajax_call, for logged in and anonymous access respectively. What happens is that every AJAX call is registered in a named variable at runtime, and we just did it for tt_ajax_call(). I'm aware that this isn't a great way of explaining it, but you get my drift...

plugins/tt_ajax_test/tt_ajax.php
<?php
/**
 * Plugin Name: AJAX Test
 * Plugin URI: http://www.teochewthunder.com
 * Description: This plugin implements and tests AJAX functionality
 * Version: 1.0.0
 * Author: TeochewThunder
 * Author URI: http://www.teochewthunder.com
 * License: GPL2
 */

function tt_ajax_listener() {
    wp_register_script( 'ajax_listener', plugin_dir_url( __FILE__ ) . '/js/tt_ajax_listener.js', ['jquery'] );
    wp_localize_script( 'ajax_listener', 'objAjax', ['ajaxurl' => admin_url( 'admin-ajax.php' )]);        

    wp_enqueue_script( 'ajax_listener' );
}

add_action( 'wp_enqueue_scripts', 'tt_ajax_listener' );

function tt_ajax_call() {
    $date = date($_POST['format'], strtotime('now'));
    echo json_encode(['date' => $date]);
    die();
}

add_action("wp_ajax_nopriv_tt_ajax_call", "tt_ajax_call");
add_action("wp_ajax_tt_ajax_call", "tt_ajax_call");


The AJAX call is pretty much what was presented in Your AJAX Start Kit, jQuery edition. For added security, we could implement a nonce, but I'm going with the bare minimum here. In here, we pass in a test string, to this URL. This references the function I just created in the backend. For data, I will pass in "j M Y H:i:s".

plugins/tt_ajax_test/js/ajax_listener.js
jQuery(document).ready(function(){
    if (window.location.href.indexOf("ajax-test") > -1) {
        jQuery('#btnAjax').click(()=>{
            jQuery.ajax({
                type : "post",
                dataType : "json",
                url : objAjax.ajaxurl,
                data : {action: "tt_ajax_call", format: "j M Y H:i:s"},
                success: function(data, textStatus, jqXHR) {

                },
                error: function(err) {
                    alert('Error');
                }
            });        
        });

    
        alert('test js');
    }
});


Once the data is returned, I populate the placeholder with the results. Oh yes, and I'll remove the earlier test command to remove the annoying pop-up.

plugins/tt_ajax_test/js/ajax_listener.js
jQuery(document).ready(function(){
    if (window.location.href.indexOf("ajax-test") > -1) {
        jQuery('#btnAjax').click(()=>{
            jQuery.ajax({
                type : "post",
                dataType : "json",
                url : objAjax.ajaxurl,
                data : {action: "tt_ajax_call", format: "j M Y H:i:s"},
                success: function(data, textStatus, jqXHR) {
                    jQuery('.tt_placeholder').html(data.date);
                },
                error: function(err) {
                    alert('Error');
                }
            });        
        });
    
        //alert('test js');
    }
});


Now click the button, and we should see the current date in the specified format. Every time you click, the format should change!


Finally!

That seemed simple enough.

AJAX isn't really that straightforward in WordPress. But now that I've documented which hoops to jump through, hopefully it's a lot easier from this point forward.

(Word)Press on,
T___T

Thursday, 6 May 2021

Your AJAX Starter Kit, WordPress edition (Part 1/2)

AJAX, or if you want to be unnecessarily verbose, Asynchronous JavaScript And XHTML, is simple enough to implement on its own. In the past, I've performed a few basic demos on how to implement AJAX, and even done a jQuery version. Today's post is analogous to the other two; except that there's a little complication.

Because we'll be doing it in WordPress.

For those of you who are blissfully unaware of what WordPress is, it's an open-source Content Management System used to develop websites and applications. However, the applications part would be pretty hard if a developer had no idea how to wrangle some AJAX out of WordPress. Having gone through the process myself, it's my opinion that this is not at all straightforward for the uninitiated. And what I would like to do today, is take you through the process of creating a plugin in WordPress which we can then use to implement AJAX.

This is going to have a few moving parts.

To begin, I will create a plugin in WordPress. This is really nothing more than creating a folder in the plugins folder of the wp-content directory, then creating a PHP file. I'm not going to elaborate on this; because this isn't a web tutorial and even if it were, that's not what it's about.

plugins/tt_ajax_test/tt_ajax.php
<?php
/**
 * Plugin Name: AJAX Test
 * Plugin URI: http://www.teochewthunder.com
 * Description: This plugin implements and tests AJAX functionality
 * Version: 1.0.0
 * Author: TeochewThunder
 * Author URI: http://www.teochewthunder.com
 * License: GPL2
 */


JavaScript

Now, before you can have AJAX, you first need to be able to run JavaScript. We will be creating a JavaScript file in the folder we just created. I'd prefer, as a matter of good practice, to put that file in a sub-folder, perhaps named js. Call it anything you like. We'll use jQuery for this. Use a ready() method to run our test command, which will simply pop up an alert message.

plugins/tt_ajax_test/js/ajax_listener.js
jQuery(document).ready(function(){
    alert('test js');
});


Then in the PHP file, we use add_action() to add this newly-created function tt_ajax_listener().
plugins/tt_ajax_test/tt_ajax.php
<?php
/**
 * Plugin Name: AJAX Test
 * Plugin URI: http://www.teochewthunder.com
 * Description: This plugin implements and tests AJAX functionality
 * Version: 1.0.0
 * Author: TeochewThunder
 * Author URI: http://www.teochewthunder.com
 * License: GPL2
 */

function tt_ajax_listener() {

}

add_action( 'wp_enqueue_scripts', 'tt_ajax_listener' );


This function basically calls wp_enqueue_script() to ensure that the JS file we just wrote, is included in the pages of the WordPress site. Before that, we also have to run wp_register_script() using the URL of the JavaScript file, and because we're going to use jQuery, we pass that in as an argument.

plugins/tt_ajax_test/tt_ajax.php
<?php
/**
 * Plugin Name: AJAX Test
 * Plugin URI: http://www.teochewthunder.com
 * Description: This plugin implements and tests AJAX functionality
 * Version: 1.0.0
 * Author: TeochewThunder
 * Author URI: http://www.teochewthunder.com
 * License: GPL2
 */

function tt_ajax_listener() {
    wp_register_script( 'ajax_listener', plugin_dir_url( __FILE__ ) . '/js/tt_ajax_listener.js', ['jquery'] );

    wp_enqueue_script( 'ajax_listener' );

}

add_action( 'wp_enqueue_scripts', 'tt_ajax_listener' );


Now when you refresh, this should come up. That's how you know it's working.

Of course, there's a possibility that we don't want this script to fire off just anywhere. So here's a really cheap way of making sure that it only fires off on specific pages. Basically, check for the URL and make sure it confirms to certain requirements. Now if you run the page again, you won't see anything.

Be warned that this is really not a great way to implement things, but this is just an example. A very clumsy example.

plugins/tt_ajax_test/js/ajax_listener.js
jQuery(document).ready(function(){
    if (window.location.href.indexOf("ajax-test") > -1) {    
        alert('test js');
    }
});


I'm creating a page here. This one is titled ajax-test, and in the screenshot, you can see the URL. And you can also see that the popup appears here because "ajax-test" is in the URL.

For this page, I added the following code via the built-in CMS.
<div>
    <div class="tt_placeholder">This is a placeholder</div>
    <input type="button" id="btnAjax" value="AJAX Call">
</div>



This ensures that now we have a button and a placeholder, with id and class attributes we can leverage on later.

All good? Moving on...

Now we have working JavaScript, and a test page from which to launch our AJAX call. All we're lacking now is more JavaScript to make that AJAX call, and back-end code to execute the command.

Next

Don't quit on me just yet! We will be implementing an AJAX call.

Wednesday, 18 September 2019

Spot The Bug: Internet Explorer Strikes Again!

Hey guys, the Spot The Bug wagon just rolled back into town, and it's time to kick some bug butt!

Die, bugs, die!

Cross-browser compatibility can be a nightmare. And every once in a while, I'm forcibly reminded of that fact.

So here I was, using a jQuery AJAX call to populate a table. The endpoint led to a PHP file (named, imaginatively, getdata.php) which was grabbing data from a MYSQL database.

index.html
        <script>
            $(document).ready
            (
                $.ajax
                (
                    {
                        url: "getdata.php?type=quotes",
                        type: "GET",
                        success: function(result)
                        {
                            var data = JSON.parse(result);

                            $(data.quotes).each
                            (
                                function(i, x)
                                {
                                    $("#tblQuotes").append("<tr><td style='text-align:right'><b>" + x.person + "</b></td><td><i>" + x.quote + "</i></td></tr>");                                                      
                                }
                            )
                        }
                    }
                )
            );
        </script>


All was fine and dandy, until I detected a typo around the last row, and fixed it in the database. See that I spelled "Linus" wrongly? It's an easy mistake to make, given that "s" and "x" are just about right nest next to each other and he did create Linux. Anyways...


I ran the code again in Chrome, everything seemed fine. But once I tried to do the same in Internet Explorer, the typo came back!

What went wrong

It certainly wasn't the PHP. I ran the file directly in the browser, and even in Internet Explorer, it produced the correct results. This was what it was sending back to the AJAX call. Or was it?

getdata.php
$result = array("quotes" => $techQuotes);
echo json_encode($result);




It seemed a little suspicious that this was only happening in Internet Explorer. And since the PHP wasn't at fault, the next moving part was the AJAX call. Which meant that it was a front-end problem, which in turn gelled with the fact that it was only happening in Internet Explorer.

Why it went wrong

Apparently, Internet Explorer caches all GET requests by default. Therefore, since the endpoint was the same, it simply recycled the previous data! Nice going, Microsoft!

How I fixed it

I just explicitly set caching to off, right there.

index.html
                $.ajax
                (
                    {
                        url: "getdata.php?type=quotes",
                        type: "GET",
                        cache: false,
                        success: function(result)
                        {
                            var data = JSON.parse(result);

                            $(data.quotes).each
                            (
                                function(i, x)
                                {
                                    $("#tblQuotes").append("<tr><td style='text-align:right'><b>" + x.person + "</b></td><td><i>" + x.quote + "</i></td></tr>");                                                      
                                }
                            )
                        }
                    }
                )


And presto! That was how it looked in both Chrome and Internet Explorer now.

Conclusion

You know in this day and age it's so easy to forget that web developers are at the mercy of the browsers of the end-users, and what an utter pain it is to ensure cross-browser compatibility. You think using jQuery will solve all your problems, and surprise, surprise, it really doesn't. Sometimes it introduces new ones.

Cache you later!
T___T

Tuesday, 19 June 2018

Web Tutorial: AngularJS Password Strength Validator (Part 2/2)

In the previous part, we implemented quite a few checks for password strength. Now, we're going to implement a Dictionary Check. This basically checks for any words in the password that can be found in the dictionary. This is important because most password-guessing attacks use a dictionary. So in order to be safe, you probably want to check the given password for such vulnerabilities.

There's a catch, though. Unless you're willing to host an entire dictionary database on your server, you'll have to use an API. The good news is, the Internet is full of these APIs. The one we're going to use today is at Oxford Dictionaries. We are going to use an AJAX call to this URL, passing in a series of strings that might be words, and the returned result will determine if any of the strings you sent, are actually dictionary words.

So, first, register an account and obtain a key.


Next, create a file, validate.php. In there, obtain the POST variables, url and words, and assign them to local variables. words will be a JSON array, so we need to run it through the json_decode() function.

Then declare two variables, wordsFound (which defaults to false) and wordToValidate. This file will print out an array, result, which contains wordsFound in JSON format.

validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";

$result = array("wordsFound" =>$wordsFound);
echo json_encode($result);
?>


Next, implement a For loop to iterate through the words array. For each iteration, set wordToValidate to the current element of the words array.

validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";

for ($i = 0; $i < sizeof($words); $i++)
{
    $wordToValidate = $words[$i];
}

$result = array("wordsFound" =>$wordsFound);
echo json_encode($result);
?>


We'll use cURL to access the URL. First, we have to initialize an object using the curl_init() function. Then we need to disable verification. Note that this is not recommended in a live environment - you need to verify stuff to ensure it's safe. But for the purposes of this exercise, sure, disable it.

validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";

for ($i = 0; $i < sizeof($words); $i++)
{
    $wordToValidate = $words[$i];
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
}

$result = array("wordsFound" =>$wordsFound, "word" => $wordToValidate);
echo json_encode($result);
?>


Now we use the curl_setopt_array() function, passing in the curl object and an array as arguments. And after that, we execute the curl_close() function on the curl object.

validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";

for ($i = 0; $i < sizeof($words); $i++)
{
    $wordToValidate = $words[$i];
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);

    curl_setopt_array($curl, array()));

    curl_close($curl);
}

$result = array("wordsFound" =>$wordsFound);
echo json_encode($result);
?>


In the array, we include key-value pairs that will work as configuration. CURLOPT_URL will be set to the URL and the current word in the words array. The rest is pretty straightforward until you get to the last part, which is yet another array, CURLOPT_HTTPHEADER. This is where your id and key for the API are to be passed in. This is important. The API host is not going to let you use its services without some form of verification. No, my id and key aren't "xxxxx". Just get your own, already.

validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";

for ($i = 0; $i < sizeof($words); $i++)
{
    $wordToValidate = $words[$i];
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);

    curl_setopt_array($curl, array(
    CURLOPT_URL => $url . $wordToValidate,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
    CURLOPT_CUSTOMREQUEST => "GET",
    CURLOPT_HTTPHEADER => array(
    "app_id: xxxxx",
    "app_key: xxxxx"
    ),
    ));

    curl_close($curl);
}

$result = array("wordsFound" =>$wordsFound);
echo json_encode($result);
?>


Now, run the function curl_exec() on the curl object, and return the result to the variable response.

validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";

for ($i = 0; $i < sizeof($words); $i++)
{
    $wordToValidate = $words[$i];
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);

    curl_setopt_array($curl, array(
    CURLOPT_URL => $url . $wordToValidate,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
    CURLOPT_CUSTOMREQUEST => "GET",
    CURLOPT_HTTPHEADER => array(
    "app_id: xxxxx",
    "app_key: xxxxx"
    ),
    ));

    $response = curl_exec($curl);

    curl_close($curl);
}

$result = array("wordsFound" =>$wordsFound);
echo json_encode($result);
?>


The response will contain a "404" if no matches are found, and an array of matches otherwise. Now, we don't actually care how many matches there are - we just need to know that there is at least one match. So, if there's no "404" in the result, wordsFound is true. Not exactly ironclad logic, but it'll do for now.

validate.php
<?php
$url = $_POST["url"];
$words = json_decode($_POST["words"]);
$wordsFound = false;
$wordToValidate = "";

for ($i = 0; $i < sizeof($words); $i++)
{
    $wordToValidate = $words[$i];
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);

    curl_setopt_array($curl, array(
    CURLOPT_URL => $url . $wordToValidate,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
    CURLOPT_CUSTOMREQUEST => "GET",
    CURLOPT_HTTPHEADER => array(
    "app_id: xxxxx",
    "app_key: xxxxx"
    ),
    ));

    $response = curl_exec($curl);

    if (strpos($response, "404") === false) $wordsFound = true;

    curl_close($curl);
}

$result = array("wordsFound" =>$wordsFound, "word" => $wordToValidate);
echo json_encode($result);
?>


Now, we'll want to work on the JavaScript some more. Let's start by dissecting the entered password and retrieving all the possible words of five letters and above from it. For this, we'll use the getWords() function which we'll create later. We'll pass in the scope variable enteredPassword, and the result will be the value of a new variable, possibleWords.

js/main.js
        if (/[0-9]/g.test($scope.enteredPassword))
        {
            pts ++;
        }
        else
        {
            $scope.strengthMessage += "Try including numbers in your password.\n";
        }

        var possibleWords = getWords($scope.enteredPassword)


Let's create the getWords() function. In this function, we declare the variable newArr as an empty array. At the end of the function, we will return newArr. We'll also create another array, arr, splitting the password by any non-alphanumeric characters. Thus, if you enter in a password like "teochew-thunder3pass@word", you'll get an array containing "teochew", "thunder", "pass" and "word".

js/main.js
    function getWords(password)
    {
        var arr = password.split(/[^A-Za-z]/);
        var newArr = [];

        return newArr;
    }

    function getCode(pts)
    {
        if (pts <= 0) return "weak";   
        if (pts == 1) return "moderate";   
        if (pts == 2) return "strong";   
        if (pts >= 3) return "excellent";
    }


However, we only want words that are 5 characters and above. Because searching for words 4 letters and below would return too many positives. Thus, we iterate through the arr array, and push any result greater than 4 characters, into the newArr array. We also convert the pushed string to lowercase. Thus, if the array contained "teochew", "thunder", "pass" and "word", only "teochew" and "thunder" would get through.

js/main.js
    function getWords(password)
    {
        var arr = password.split(/[^A-Za-z]/);
        var newArr = [];

        for (var i = 0; i <arr.length; i++)
        {
            if (arr[i].length > 4)
            {
                newArr.push(arr[i].toLowerCase());
            }
        }

        return newArr;
    }


Next, we're going to get all possible words within the strings in newArr. Declare another variable, tempArr, as an empty array. Iterate through newArr with a For loop. The operation will only affect those strings that are longer than 5 characters, so put in a conditional for that.

js/main.js
    function getWords(password)
    {
        var arr = password.split(/[^A-Za-z]/);
        var newArr = [];

        for (var i = 0; i <arr.length; i++)
        {
            if (arr[i].length > 4)
            {
                newArr.push(arr[i].toLowerCase());
            }
        }

        var tempArr = [];

        for (var i = 0; i < newArr.length; i++)
        {
            if (newArr[i].length > 5)
            {

            }
        }

        return newArr;
    }


Now, this is how we process the strings that are greater than 5 characters. Say, for the string "teochew", we will derive the strings "teoch", "teoche", "teochew", "eoche", "eochew" and "ochew".

Next up is another nested For loop. The outer loop will start the search from strings beginning with the first three letters of the string "teochew", because starting the string with anything after those first three letters will net you a string of less than 5 characters.

The inner loop will process strings of 5 characters and above, to the maximum allowed by the length of the string "teochew".

js/main.js
    function getWords(password)
    {
        var arr = password.split(/[^A-Za-z]/);
        var newArr = [];

        for (var i = 0; i <arr.length; i++)
        {
            if (arr[i].length > 4)
            {
                newArr.push(arr[i].toLowerCase());
            }
        }

        var tempArr = [];

        for (var i = 0; i < newArr.length; i++)
        {
            if (newArr[i].length > 5)
            {
                for (var j = 0; j < newArr[i].length - 4; j++)
                {
                    for (var k = 5; k < newArr[i].length; k++)
                    {
               
                    }
                }
            }
        }

        return newArr;
    }


Thus, the sub-string is represented by newArr[i].substr(j, k). We then check if it's already in tempArr before pushing it in.

js/main.js
    function getWords(password)
    {
        var arr = password.split(/[^A-Za-z]/);
        var newArr = [];

        for (var i = 0; i <arr.length; i++)
        {
            if (arr[i].length > 4)
            {
                newArr.push(arr[i].toLowerCase());
            }
        }

        var tempArr = [];

        for (var i = 0; i < newArr.length; i++)
        {
            if (newArr[i].length > 5)
            {
                for (var j = 0; j < newArr[i].length - 4; j++)
                {
                    for (var k = 5; k < newArr[i].length; k++)
                    {
                        if (j + k <= newArr[i].length)
                        {
                            if (tempArr.indexOf(newArr[i].substr(j, k).toLowerCase()) == -1)
                            tempArr.push(newArr[i].substr(j, k).toLowerCase());   
                        }               
                    }
                }
            }
        }

        return newArr;
    }


After that, we use another For loop to iterate through tempArr. If newArr does not already contain the element in tempArr (this is possible because you may be processing multiple identical strings, for example, if the password was "teochew-teochew3pass@word"), then push that element into newArr.

js/main.js
    function getWords(password)
    {
        var arr = password.split(/[^A-Za-z]/);
        var newArr = [];

        for (var i = 0; i <arr.length; i++)
        {
            if (arr[i].length > 4)
            {
                newArr.push(arr[i].toLowerCase());
            }
        }

        var tempArr = [];

        for (var i = 0; i < newArr.length; i++)
        {
            if (newArr[i].length > 5)
            {
                for (var j = 0; j < newArr[i].length - 4; j++)
                {
                    for (var k = 5; k < newArr[i].length; k++)
                    {
                        if (j + k <= newArr[i].length)
                        {
                            if (tempArr.indexOf(newArr[i].substr(j, k).toLowerCase()) == -1)
                            tempArr.push(newArr[i].substr(j, k).toLowerCase());   
                        }               
                    }
                }
            }
        }

        for (var i = 0; i < tempArr.length; i++)
        {
            if (newArr.indexOf(tempArr[i].toLowerCase()) == -1)
            newArr.push(tempArr[i].toLowerCase());
        }

        return newArr;
    }


Now back to the processPassword() scope function!  Wrap the call to the getCode() function in an If statement. If the getWords() function had returned a non-empty array, we'll do more processing. If not, we end the function there with a return statement.

js/main.js
        var possibleWords = getWords($scope.enteredPassword)

        if (possibleWords.length > 0)
        {

        }
        else
        {
            $scope.strengthCode = getCode(pts);
            return;
        }


To process a non-empty array containing all the words you want to validate against the dictionary,  call some AJAX. Everything's pretty standard here. The file you are sending the POST to, is the validate.php file we wrote earlier. You pass in arguments such as a JSON string of the words, and the URL where your API resides.

js/main.js
        var possibleWords = getWords($scope.enteredPassword)

        if (possibleWords.length > 0)
        {
            var xmlhttp = new XMLHttpRequest();
            xmlhttp.onreadystatechange = function()
            {
                if (this.readyState == 4 && this.status == 200)
                {

                }
            };

            xmlhttp.open("POST", "validate.php", true);
            xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xmlhttp.send("words=" +  JSON.stringify(possibleWords) + "&url=https://od-api.oxforddictionaries.com/api/v1/entries/en/");
        }
        else
        {
            $scope.strengthCode = getCode(pts);
            return;
        }



Next, you use the parse() method of the JSON object on the returned response. If wordsFound is true, then decrement pts, set the strengthMessage and strengthCode variables, and return.

js/main.js
        var possibleWords = getWords($scope.enteredPassword)

        if (possibleWords.length > 0)
        {
            var xmlhttp = new XMLHttpRequest();
            xmlhttp.onreadystatechange = function()
            {
                 if (this.readyState == 4 && this.status == 200)
                 {
                      var result = JSON.parse(this.responseText);

                      if (result.wordsFound)
                      {
                          pts --;
                          $scope.strengthMessage += "Avoid using dictionary words. (" + result.word + ")\n";   
                          $scope.strengthCode = getCode(pts);
                          return;
                       }
                  }
            };

            xmlhttp.open("POST", "validate.php", true);
            xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xmlhttp.send("words=" +  JSON.stringify(possibleWords) + "&url=https://od-api.oxforddictionaries.com/api/v1/entries/en/");
        }
        else
        {
            $scope.strengthCode = getCode(pts);
            return;
        }


Try it!


Something seems to have gone wrong... and I'm getting a warning from oxforddictionaries.com!




Yeah, that's to be expected. If you're a cheap bastard like me, you got the free version of the API, which means you can only make that many calls to the API within one minute. If you tested with an extra long password, that would have sent a gazillion requests to the API.

But the logic for this validation is sound... I think.

Till we meet again, stay STRONG.
T___T