/*
      _______                          .__ .__   .__   .__                 
      \      \    ____   __ __ _______ |__||  |  |  |  |__|  ____    ____  
      /   |   \ _/ __ \ |  |  \\_  __ \|  ||  |  |  |  |  | /  _ \  /    \ 
     /    |    \\  ___/ |  |  / |  | \/|  ||  |__|  |__|  |(  <_> )|   |  \
     \____|__  / \___  >|____/  |__|   |__||____/|____/|__| \____/ |___|  /
    =========\/======\/=================================================\/==
  v0.01 04/JUL/2007 © Copyright 2007-2007 Scott D. Yelich SOME RIGHTS RESERVED
 .,-*~'`^`'~*-,._.,-*~'`^`'~*-,._.,-*~'`^`'~*-,._.-*~'`^`'~*-,._.,-*~'`^`'~*-,. 


  LICENSE:  Creative Commons Attribution 3.0 License.
  SEE:      http://creativecommons.org/licenses/by/3.0/

  http://www.neurillion.com/p/33/counters/bug/v1/src/counter_bug_v1.cpp

  Fri Jul 27 21:04:44 EDT 2007, v0.02 sdy


  This is a *very* simple program written to do a web page
  hit counter using sqlite3.  This program doesn't check
  for errors or other things like db locking to allow for
  concurrency.

  no classes, uses globals, etc. is this good style? no.
  is this code quality code... uh, no.  does it work, ya...
  fails silently also (ie: no referrer = no hit recorded =
  no error reported)

*/

//  C++

#include <iostream>
#include <sstream>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>

//  SQLITE3

#include <sqlite3.h>

//  HELPER  VARIABLES

int header_size; //  header size

std::vector<std::string> headers;  //  used to hold reponse headers.
std::vector<std::string> data;     //  used to hold response data

//  SQLITE  VARIABLES  FOR  C  API

sqlite3 *db;          //  sqlite3 database
int rc;               //  return code -- c style 
int cols;             //  columns in response data
int rows;             //  rows in response data
const char *db_name;  //  database name
char **result;        //  result array
char *err_msg;        //  error message

//  BUILT IN DEFAULT 1x1t.gif IMAGE created with GIMP:
static const char gif1x1t[] = "\
\x47\x49\x46\x38\x39\x61\x01\x00\x01\
\x00\x80\x00\x00\xff\xff\xff\xff\xff\
\xff\x21\xf9\x04\x01\x0a\x00\x01\x00\
\x2c\x00\x00\x00\x00\x01\x00\x01\x00\
\x00\x02\x02\x4c\x01\x00\x3b\x00\
";

//  the database name to store the hits:
//  sqlite3 "quirk" --  sqlite3 needs to be able to open both the file *and*
//  the directory for writing.  If a symlink is used for the database name,
//  the directory where the *symlink* is will be opened -- oops!
//  that one took me a while to track down.  It's best that the database here
//  be a regular file and not a symlink.  Yes, you need to edit this path to
//  point to where you want te database to be -- just make sure there will
//  be write access to the directory as well as the file.

//  CHANGE THE FOLLOWING LINE TO POINT TO YOUR DATABASE FILE!
static const char DBNAME[] = "/www/neurillion/p/33/databases/hits.sq3";


//  
//  SQLITE3  EXEC  SQL  FUNCTION
//  

//  The sql3x function is intended to "execute" an sql statement via sqlite3.
//  It does minimal error checking and no real concurrency checking.

int
sql3x(std::string const & sql_text)
{

  rc = sqlite3_get_table(
    db,                /* An open database */
    sql_text.c_str(),  /* SQL to be executed */
    &result,           /* Result written to a char *[]  that this points to */
    &rows,             /* Number of result rows written here */
    &cols,             /* Number of result columns written here */
    &err_msg           /* Error msg written here */
  );

  // clear out the storage space to hold fresh results for headers and data:
  headers.clear();
  data.clear();

  if (SQLITE_OK != rc) {

    //  std::cout << "NOT ok=(" << rc << ") = (" << err_msg << ") sql=(" << sql_text << ")\n";

  } else {

    //  first row is headers
    for (int i=0; i<cols; ++i) {
      headers.push_back(result[i]);
    }

    //  rest is data
    for(int i=0; i<cols*rows; ++i) {
      data.push_back(result[cols+i]);  //  cols+ = skip headers
    }

  }

  //  always remember to free the table (releases malloc'd memory)
  sqlite3_free_table(result);

  return rc;

}


//  
//  MAIN  FUNCTION  FOR  "BUG"  COUNTER
//  

//  The idea here is to make a simple counter with the following steps:
//  (1)  Open the database
//  (2)  Check for the table
//  (3)  If table doesn't exist, create it
//  (4)  Get REFERRING page from the HTTP environment
//  (5)  If referer not null, do the hitcount on the page in the db
//  (6)  If hit entry doesn't exists INSERT, otherwise, UPDATE
//  (7)  Check HTTP QUERY_STRING for image_type=image_path
//  (8)  Output either image provided or default 1x1 pixel transparent gif 


int
main()
{

  //  (1)  Open the database

  std::string dbfn(DBNAME);
  db_name = dbfn.c_str();

  rc = sqlite3_open(db_name, &db);
  if (rc) {
    //  sqlite3_errmsg(db)
    sqlite3_close(db);
    exit(1);
  }

  //  (2)  Check for the table

  rc = sql3x("SELECT rowid,page,hits FROM counter_bug_totals LIMIT 1;");

  header_size = headers.size();

  //  (3)  If table doesn't exist, create it

  if (0 == header_size) {
    rc = sql3x("CREATE TABLE counter_bug_totals (page,hits);");
  }

  //  (4)  Get REFERRING page from the HTTP environment

  char * referer_cstr = getenv("HTTP_REFERER");

  //  (5)  If referer not null, do the hitcount on the page in the db

  if ( referer_cstr ) {

    //  stringstream is used for << conversions into it 
    std::stringstream sql_command_ss;
    sql_command_ss.str("");

    std::string page;
    page.append(referer_cstr, strlen(referer_cstr));

    sql_command_ss << "SELECT hits FROM counter_bug_totals WHERE page='" << page << "';";
    rc = sql3x(sql_command_ss.str()); // order by?

    //  (6)  If hit entry doesn't exists INSERT, otherwise, UPDATE

    header_size = headers.size();

    if (0 == header_size) {

      sql_command_ss.str("");
      sql_command_ss << "INSERT INTO counter_bug_totals VALUES('" << page << "',1);";
      rc = sql3x(sql_command_ss.str());

    } else {

      int hits;

      std::istringstream buffer(data[0]);
      if (!(buffer >> hits)); // throw?

      //  increase hitcount for the page by 1
      hits++;

      //  prepare new/next sql statement
      sql_command_ss.str("");
      sql_command_ss << "UPDATE counter_bug_totals SET hits=hits+1 WHERE page='" << page <<"';";

      //  execute sql statement
      rc = sql3x(sql_command_ss.str());

    }

  }

  sqlite3_close(db);

  //  (7)  Check HTTP QUERY_STRING for image_type=image_path

  char *qs;
  qs = getenv("QUERY_STRING");

  std::string image_type("gif");
  std::string image_path;

  if ( qs ) {

    std::string q(qs,strlen(qs));
    std::istringstream isstr;
    isstr.str(q);
    std::getline(isstr, image_type, '='); //  assumes clean params!
    std::getline(isstr, image_path);

  }

  //  (8)  Output either image provided or default 1x1 pixel transparent gif 

  std::cout << "Content-Type:  image/" << image_type << std::endl << std::endl;

  //  WARNING:  Any file that can be opened for reading may be accessed here!
  //  Example:  ?gif=/etc/passwd
  std::ifstream i(image_path.c_str());

  if ( i ) {
    i.seekg(0);
    std::cout << i.rdbuf();
  } else {
    std::string gif(gif1x1t,sizeof(gif1x1t));
    std::cout << gif;
  }

  //  

  return 0;

}
