What a Year…

If someone had approached me at the beginning of this year and said “this, this and this is going to happen” I’d have probably laughed in their face because 2006 has been so… life-changing! In so many ways, I think this year really was the start of my life as a grown-up.

Around January I gave up my first job — out of necessity rather than lack of love for it. I enjoyed it, I got on with the people I worked with and was completing tasks that I hadn’t done before. It’s such a shame that after my ‘contract’ ran out it was completely unpaid and I was doing it for experience. Heyho, can’t have everything!

I started looking for jobs and got my first ever job interview (I wasn’t interviewed for my first job) at New College — another local college. I froze in the interview, getting a basic question wrong but otherwise did OK. They promised me on that they’d get in touch either way. Not only did they not call, but they didn’t answer when I rang them. They really messed me about.

To this day, I can’t decide what is more irritating — not hearing from someone at all after applying for a job/sending over a copy of my CV, or hearing back from them once and then not again.. or worse, going to the interview and not hearing back. This was all irrelevant when I landed a part-time position at a school in Shrewsbury.

I didn’t want the job, but really had no other choice. Still, the first week was actually quite good and I started to enjoy myself. Unfortunately, my boss turned out to be a complete prat and by the end of my first month I was coming home mentally exhausted and frustrated almost to the point of tears. After about four months I gave up listening to people telling me to stick with it, and during the summer I started eyeing up new jobs again.

It was my absolute pleasure to return the favour to New College when they rang me around July/August asking if I was yet employed. I confirmed that I was, but was looking for a new job and would be happy to come in. What I didn’t tell them was that I was in the process of being interviewed for my current job. The morning I was due at New College I took great delight in ringing them up to tell them that I couldn’t make it. I hope I inconvenienced them as much as they inconvenienced and disappointed me earlier in the year. Petty? Yes. A great decision? Definitely.

The reason why I turned down the second interview at NC was not purely ‘revenge’ anyway — the evening before I had accepted an offer from my now-boss, landing myself a shiny new job back doing web development. Hoorah! I think going for the position, doing the interviews and accepting the job was probably the best decision I’ve made this year. I’ve learnt new things, made new friends and what’s more: I found a place to live!

Finally, Karl and I moved out this year. Had I not taken the job I know I’d have not even seen this place up for rent and Karl and I would have been still stuck at his Mum’s. As grateful as I am to her for letting me stay (rent free, I might add) for so long, it is great to be in a place of our own; peace and quiet at last!

Other highlights of the year include seeing my Dad in January, my niece saying “I love you” for the first time and the release of Wii. (Sorry, I had to chuck that one in there. It’s just too awesome!) Writing this out makes it all sound so trivial, but if next year is anywhere near as busy or involves as many “life changes” I think I’m going to be grey-haired by the time it’s over. Hip hip hooray, three cheers for 2006, and here’s to the new year!

Ho ho ho, and all that!

I am sure if most of you took a guess at what I’d been doing for the past 5 days since I last blogged you’d probably say something like “spending Christmas with your family!” Of course, it’d be a huge lie; I’ve actually been playing Animal Crossing non-stop for about a week.

After doing the brief family thing on Christmas Day/Boxing Day eve, Karl and I returned home to play with the goodies Santa brought, and relax away from noise and children (much the same thing, honestly). Talking of which — goodies that is, not children — I am now the owner of a Fujifilm FinePix S5600 which (compared to the previous camera) is an absolute beast. (Thank you Mummies!) I’ve not had chance to really play with it yet, but rest assured I’ll be filling up my web space with gratuitous shots of boring crap shortly.

Amongst other gifts, I received a gorgeous smelling leather camera bag and 1Gb xD-Picture card from Karl (lots of “cuddles” for him :9 ) and plenty of chocs from my brother (Kristofer) and sister.

Material obsessions possessions aside, I think one of the best moments this Christmas was watching my niece open the Tickle Me Elmo X. For those who’ve not had the opportunity to see it (the Elmo), I direct you to YouTube. She immediately starting copying it — lying on the floor kicking her legs and patting her belly. It was hilarious, and if I didn’t have a hatred for children being exploited for cheap web hits you KNOW I’d upload the video.

Right. My potatoes are boiling over, so I must dash.. safe to say I’ve had a jolly Christmas so far, only marred by the lack of my father. Heyho, maybe next year!

Animal Crossing: Wild World

I’ve got into AC:WW again, after rebuilding my town. If you’ve got the game and have wi-fi capabilities, send me an e-mail with your friend code, name and town name to jem@jemjabella.co.uk. :)

Feeling of Relief

I am feeling just a little bit relaxed this evening. Work has finished and I don’t have to worry about it again until January 2nd. Don’t get me wrong, I still love my job, but these past couple of weeks have been very busy. It’s not going to change — I have a major project to complete in the first month or two of the new year, and new jobs coming in constantly!

Still got Christmas shopping to do, a mass of e-mails to read properly and reply to and sod knows what else. I haven’t even linked to the participants of the q*bee blog day either, which I really need to do. I’ve noticed a lot of familiar faces applying btw — it’ll be great to see you in the club and I hope you find it as cute as I do.

I’ve tried to get round and leave individual Christmas greetings for lots of people today. If I haven’t got to you yet, please don’t be offended because it’s not personal. Likewise, if I end up not getting to you — it’s not because I don’t want to, but rather because I’m bound to miss someone.

Right. It’s late, and I’ve not even eaten yet. I should probably go and do that, then rest my weary head. Happy Holidays!

No Time, No Motivation

I am swamped at work and “at home” (i.e. personal ‘net interests) right now, finding it very hard to catch up after my week away due to illness recently. As such, mails are going unanswered and I just don’t feel like rushing anything to get back on top of things. I will get back to everyone that has contacted me over the next couple of weeks, but in the mean time please avoid e-mailing me unless absolutely necessary.

Those looking for general coding assistance should find Codegrrl Forums of use, and I don’t plan on doing any reviews/further Pants Awards until after Christmas so there’s no point sending in nominations. As promised, I will be linking to q*bee member’s blog entries in my previous post and sending out Christmas gifts.

In case I don’t get chance to say it to you all individually beforehand, have a very merry Christmas.

Quilting Bee Blog Day

Those of you who’ve been readers of jemjabella for a while now may remember me talking about the quilting bee this time last year. I’m going to talk about it again for a while.

Things have changed since last year. I’m no longer just a wide-eyed little bee in a swarm of buzzing buddies — oh no! I took over the club in May this year after the previous “Queen” left to spend more time with her family and have been having tons of fun ever since. Don’t get me wrong, running the hive is a lot more work than I ever thought it was and I feel guilty for thinking to myself “huh, what’s the hold up!” every time there was a delay back when I was a humble bumble, but it’s been interesting to use my own experience of the club over the years to make little changes that I think are, ultimately, for the better.

Not only have I been fiddling with the coding both up front and for the backend, but I’ve also been trying to encourage people to join who have dismissed the club previously. I think the key point is getting rid of stereotypes and by having the likes of Amelie join us, that is the proof of the pudding so to speak. Of all the people I spoke to she definitely had a few ideas about the place that weren’t necessarily true and getting her to join AND enjoy herself has been a big achievement.

It’s not all about administration and bribing people to join though. There’s a lot of fun stuff that goes on behind the scenes and even with my complete lack of skill in anything graphics-related I often manage to take part and feel quite proud of myself. It’s been both a learning experience (in terms of pixelling) and a great way to make online friends, for which I will always be grateful.

I really enjoy being a part of the quilting bee, and I really enjoy running it. To show you how much fun it can be, I’m going to edit this post tomorrow morning and link to as many member’s q*bee blog entries as possible so you can see that it’s not just my bias talking here. I hope that you give some of those entries a read, and perhaps checking us out. We are the first & original pixel trading club after all!

Bloody Politics

Tory leader David Cameron has said his party must do more to keep families together, as a report suggests parental splits are creating an underclass.

What bollocks. I’m not an “underclass” because my parents split, I’m an underclass because it’s better than being a toffee-nosed snob with my head stuck up my arse. I would rather be an “underclass” if it means being true to myself than having to conform to some socially acceptable “nice guy” routine just so I don’t piss people off, or appear corrupted because of my “broken home” and upbringing. It doesn’t affect my intelligence, my ability to complete mundane daily tasks or how I pay my taxes so what business is it of the government what “class” I am?

..and you know where to stick it if you don’t like it — up your upperclass arse.

Wii Envy

My brother called me up at about 9:30pm Thursday evening to tell me he was in Tesco (local supermarket) and that he was queuing for a Wii. He was about 8th in line, they had 20 consoles to sell and he was offering to save me a place…

…WHY, GOD, DID I SAY NO?!

I’ve been vowing that I’d wait until after Christmas to indulge in the latest Nintendo console because a) games will have dropped in price by then, b) any new-console bugs will have been figured out and fixed and c) the console itself may have dropped in price. That was before I saw it in action.

I got to play on the Wii yesterday. I now have Wii Envy. Yep, it’s a real medical condition caused by playing on someone else’s Wii. The futuristic console, smooth game play (Wii Sports ftw) and intuitive controls make this not just a console: it’s a bloody experience. There’s no fancy button combos to learn, no unnecessary fuss — just point and bowl, bat, punch, golf, etc. It’s fun, and I want one. :D

Basic PHP Security Checklist

Due to the relative simplicity of PHP, more and more young webmasters are getting their hooks into scripting. This can be a good thing — it increases the range of functionality and fun that we can add to our websites without the need to learn how to code ourselves — the problem is, a lot of these scripters don’t know what they’re doing. They’re either unaware of massive holes in their scripts, or oblivious to the damage they can do. This checklist covers some of the most basic mistakes, and how to fix them.

Lack of Validation

Lack of input validation — or ‘cleaning’ of user submitted data — is one of the most common errors I see in a huge variety of scripts. This often varies from complete trust in the user with data being sent to the browser or a database with no restrictions at all, to accidental slip-ups like the display of information through the $_SERVER superglobal array, which not many people realise is manipulatable. Often people rely on pre-specified options in a drop-down menu or hidden form fields too; these can all be altered at another location and processed through your script!

By not validating data you risk all sorts of problems. Minor problems like broken formatting on your page because of user-added mark-up, huge problems like injected JavaScript (which could steal your cookies), or lines of SQL which can empty your tables and destroy your database. If you’ve got confidential data stored, don’t expect it to stay private for long.

You can combat this problem by making sure that any data the user has access to is cleaned before it is stored and displayed. Stripping tags, converting entities and trimming whitespace is a good start, and can be done with a custom function:

function cleanData($data) {
	$data = trim(htmlentities(strip_tags($data)));
	
	return $data;
}

Or, to clean data prior to inserting into a MySQL database:

function cleanDataForDB($data) {
	$data = trim(htmlentities(strip_tags($data)));
	
	if (get_magic_quotes_gpc())
		$data = stripslashes($data);
	
	$data = mysql_real_escape_string($data);
	
	return $data;
}

You can then apply these functions to user-submitted data, either one at a time:

$cleanName = cleanData($_POST['name']);

…or by looping through all of the data at once, assigning it to a ‘clean’ array):

foreach($_POST as $key => $value) {
	$clean[$key] = cleanData($value);
}

(Although the example above is designed to loop through the $_POST superglobal array, the same can be done with $_SERVER, $_GET and $_COOKIE — in fact, any array, using any function you fancy.)

Further checks on specific fields can be done with regular expressions. Input should specifically match a pattern before it is processed: e.g. an e-mail address could be matched against a pattern of letters and/or numbers, an @, and then a domain and tld. Regular expressions (regex) are great for this purpose because you don’t need to know exactly what the user will submit, just what it should ‘look’ like.

Relying on register_globals

The majority of those who code for register_globals turned on either learnt PHP in the dark ages, or haven’t read a decent article on PHP recently. When register_globals is turned on, data that would normally need to be accessed/manipulated through the specific superglobal arrays can be accessed like a normal variable (e.g. a url of page.php?x=foo would create a variable of $x with a value of “foo”). Because PHP does not require variable initilisation to execute, variables that are only supposed to be set when certain arguments are met can be maliciously altered. Take this piece of code:

if (is_logged_in())
	$authorised = true;
if ($authorised) // show sensitive admin panel data here

Even if our is_logged_in() function does fancy password checking logic, with register_globals turned on we can bypass it all and access the restricted area simply by visiting our-page.php?authorised=1. How secure is that?

Avoid this by declaring variables before they’re checked, e.g:

$authorised = false;
if (is_logged_in())
	$authorised = true;
if ($authorised) // show sensitive admin panel data here

…and by using the predefined superglobals $_ENV, $_GET, $_POST, $_COOKIE, and $_SERVER.

Plain text Password Cookies

Storing passwords as plain text is a bad idea. Worse still, is storing plain text passwords in a cookie. No ifs, buts or maybes: it’s bad. Cookies can be stolen a variety of ways (including using JavaScript injected into unvalidated scripts) and if they’re not hashed or mangled (not 100% secure in itself) they can be used to ‘break in’ to whatever site the unsuspecting cookie-holder is using that same password for.

The alternative is hashing the password using md5() or sha1() with a ‘salt’ (a word or phrase that normal users don’t know), which helps prevent the password being guessed even if the password hash is obtained. Lastly, you can avoid storing the password at all and use sessions instead. Sessions themselves have their own set of problems, but you can learn more about that on Shiflett’s Truth About Sessions article.

While we’re on the subject of cookies: don’t rely on their existance for verification that a person is who they say they are! Make sure that after checking for the existance of cookies, that you check a cookie contains what it should. Otherwise a person will be able to create a cookie with fake data and be logged straight in. Again, this is more data validation.

Stop Using $_REQUEST

$_REQUEST takes its data from $_GET, $_POST and $_COOKIE (in that order of priority). You should avoid using $_REQUEST when possible unless you don’t care where your data is coming from. As all good scripters should track where their data is coming from and what it contains, using it defeats the point of even reading this article.

SQL Injections

SQL injections are usually caused by data that hasn’t been validated! (See how important cleaning up is yet?) A common mistake when working with databases is allowing user submitted data to interact with the database without any sort of checking. A good example of bad coding is as follows:

<?php
$pass = md5($_POST['pass']);
$sql = "SELECT * FROM users WHERE username = $_POST[user] AND password = $pass";
?>

First, the data is not clean, secondly, it’s not escaped. You should always form SQL statements using backticks around table/field names and apostrophes around values being passed. This is not foolproof though, take a look at what the query would become if we added user' OR 'foo' = 'foo' -- as our username (even with backticks and apostrophes):

<?php
$pass = md5($_POST['pass']);
$sql = "SELECT * FROM `users` WHERE `username` = 'user' OR 'foo' = 'foo' -- ' AND password = 'random hashed password'";
?>

The query checks to see if user is a valid username, which is probably wrong, but it’s the next part that is the important part: OR 'foo' = 'foo'. Of course foo equals foo (and the — comments out everything left), leaving your malicious user logged in. You can avoid this by escaping submitted data. The PHP manual has an entry on mysql_real_escape_string() and how best to use it (see: “Example 3. A “Best Practice” query”). Alternatively, refer to the cleanDataForDB() function above.

Summary

Of course, the best way to avoid writing insecure PHP scripts is to not write scripts at all. Unfortunately this probably isn’t what you want to hear, and doesn’t condone a healthy learning environment. If you’re going to put your website and your users potentially at risk by writing your own scripts, do as much studying beforehand as possible to reduce the risk. Take advice from experts in the field and don’t base your scripts on known insecure code.

Recommended Reading

These are a few books and articles I’ve found beneficial when studying PHP security:

The following links are also interesting:

CAPTCHAs

A CAPTCHA is “a type of challenge-response test used in computing to determine whether or not the user is human” (source). More specifically this little rant is aimed at the variety of images containing letters and/or numbers which may be distorted, placed on a background or otherwise messed with to supposedly prevent non-humans from getting past a certain point. I hate them.

You may think it odd that someone who has two public scripts both with CAPTCHA capability should hate them, but it is true. I only implemented the bloody things into my scripts because it was what ‘the people’ wanted.

Generally, I can see the point of CAPTCHAs. They do help to reduce spam at some sites, but at what cost? If I, a fully able human being with perfect eyesight and a decent browser, have problems seeing captchas or getting the letter combinations right, what hope do those with poor eyesight or other reading/visual disabilities? Must they be relegated to a silent corner, never allowed to join a website or post a comment again, for fear of angering the giant CAPTCHA making machine monster thingy?

I wish that people would think before inserting CAPTCHAs on parts of their website that simply do not need it (for there are other tools which can help prevent spam which do not require visual confirmation). I also wish that people would clearly mark where CAPTCHAs are being used so as not to catch people out — if you’re going to put a captcha on your weblog, put it on the page with the rest of the comment form. By placing it on a confirmation page, it is very likely that I’m going to have browsed away in a separate tab and closed the page assuming that my comment has gone through without even looking.

Simply put: before adding a CAPTCHA, make sure you have exhausted all other options. For my sake (think of my blood pressure), for the sake of your poor-sighted victims and those with reading difficulties. Remember, if spammers can get past Hotmail and Yahoo’s complex CAPTCHAs, there’s no way in Hell that your freebie WordPress plugin is going to stop them in the long run. You’ll be the one losing out in the end.

Basic PHP Security Checklist

Due to the relative simplicity of PHP, more and more young webmasters are getting their hooks into scripting. This can be a good thing — it increases the range of functionality and fun that we can add to our websites without the need to learn how to code ourselves — the problem is, a lot of these scripters don’t know what they’re doing. They’re either unaware of massive holes in their scripts, or oblivious to the damage they can do. This checklist covers some of the most basic mistakes, and how to fix them.

Lack of Validation

Lack of input validation — or ‘cleaning’ of user submitted data — is one of the most common errors I see in a huge variety of scripts. This often varies from complete trust in the user with data being sent to the browser or a database with no restrictions at all, to accidental slip-ups like the display of information through the $_SERVER superglobal array, which not many people realise is manipulatable. Often people rely on pre-specified options in a drop-down menu or hidden form fields too; these can all be altered at another location and processed through your script!

By not validating data you risk all sorts of problems. Minor problems like broken formatting on your page because of user-added mark-up, huge problems like injected JavaScript (which could steal your cookies), or lines of SQL which can empty your tables and destroy your database. If you’ve got confidential data stored, don’t expect it to stay private for long.

You can combat this problem by making sure that any data the user has access to is cleaned before it is stored and displayed. Stripping tags, converting entities and trimming whitespace is a good start, and can be done with a custom function:

function cleanData($data) {
	$data = trim(htmlentities(strip_tags($data)));
	
	return $data;
}

Or, to clean data prior to inserting into a MySQL database:

function cleanDataForDB($data) {
	$data = trim(htmlentities(strip_tags($data)));
	
	if (get_magic_quotes_gpc())
		$data = stripslashes($data);
	
	$data = mysql_real_escape_string($data);
	
	return $data;
}

You can then apply these functions to user-submitted data, either one at a time:

$cleanName = cleanData($_POST['name']);

…or by looping through all of the data at once, assigning it to a ‘clean’ array):

foreach($_POST as $key => $value) {
	$clean[$key] = cleanData($value);
}

(Although the example above is designed to loop through the $_POST superglobal array, the same can be done with $_SERVER, $_GET and $_COOKIE — in fact, any array, using any function you fancy.)

Further checks on specific fields can be done with regular expressions. Input should specifically match a pattern before it is processed: e.g. an e-mail address could be matched against a pattern of letters and/or numbers, an @, and then a domain and tld. Regular expressions (regex) are great for this purpose because you don’t need to know exactly what the user will submit, just what it should ‘look’ like.

Relying on register_globals

The majority of those who code for register_globals turned on either learnt PHP in the dark ages, or haven’t read a decent article on PHP recently. When register_globals is turned on, data that would normally need to be accessed/manipulated through the specific superglobal arrays can be accessed like a normal variable (e.g. a url of page.php?x=foo would create a variable of $x with a value of “foo”). Because PHP does not require variable initilisation to execute, variables that are only supposed to be set when certain arguments are met can be maliciously altered. Take this piece of code:

if (is_logged_in())
	$authorised = true;
if ($authorised) // show sensitive admin panel data here

Even if our is_logged_in() function does fancy password checking logic, with register_globals turned on we can bypass it all and access the restricted area simply by visiting our-page.php?authorised=1. How secure is that?

Avoid this by declaring variables before they’re checked, e.g:

$authorised = false;
if (is_logged_in())
	$authorised = true;
if ($authorised) // show sensitive admin panel data here

…and by using the predefined superglobals $_ENV, $_GET, $_POST, $_COOKIE, and $_SERVER.

Plain text Password Cookies

Storing passwords as plain text is a bad idea. Worse still, is storing plain text passwords in a cookie. No ifs, buts or maybes: it’s bad. Cookies can be stolen a variety of ways (including using JavaScript injected into unvalidated scripts) and if they’re not hashed or mangled (not 100% secure in itself) they can be used to ‘break in’ to whatever site the unsuspecting cookie-holder is using that same password for.

The alternative is hashing the password using md5() or sha1() with a ‘salt’ (a word or phrase that normal users don’t know), which helps prevent the password being guessed even if the password hash is obtained. Lastly, you can avoid storing the password at all and use sessions instead. Sessions themselves have their own set of problems, but you can learn more about that on Shiflett’s Truth About Sessions article.

While we’re on the subject of cookies: don’t rely on their existance for verification that a person is who they say they are! Make sure that after checking for the existance of cookies, that you check a cookie contains what it should. Otherwise a person will be able to create a cookie with fake data and be logged straight in. Again, this is more data validation.

Stop Using $_REQUEST

$_REQUEST takes its data from $_GET, $_POST and $_COOKIE (in that order of priority). You should avoid using $_REQUEST when possible unless you don’t care where your data is coming from. As all good scripters should track where their data is coming from and what it contains, using it defeats the point of even reading this article.

SQL Injections

SQL injections are usually caused by data that hasn’t been validated! (See how important cleaning up is yet?) A common mistake when working with databases is allowing user submitted data to interact with the database without any sort of checking. A good example of bad coding is as follows:

<?php
$pass = md5($_POST['pass']);
$sql = "SELECT * FROM users WHERE username = $_POST[user] AND password = $pass";
?>

First, the data is not clean, secondly, it’s not escaped. You should always form SQL statements using backticks around table/field names and apostrophes around values being passed. This is not foolproof though, take a look at what the query would become if we added user' OR 'foo' = 'foo' -- as our username (even with backticks and apostrophes):

<?php
$pass = md5($_POST['pass']);
$sql = "SELECT * FROM `users` WHERE `username` = 'user' OR 'foo' = 'foo' -- ' AND password = 'random hashed password'";
?>

The query checks to see if user is a valid username, which is probably wrong, but it’s the next part that is the important part: OR 'foo' = 'foo'. Of course foo equals foo (and the — comments out everything left), leaving your malicious user logged in. You can avoid this by escaping submitted data. The PHP manual has an entry on mysql_real_escape_string() and how best to use it (see: “Example 3. A “Best Practice” query”). Alternatively, refer to the cleanDataForDB() function above.

Summary

Of course, the best way to avoid writing insecure PHP scripts is to not write scripts at all. Unfortunately this probably isn’t what you want to hear, and doesn’t condone a healthy learning environment. If you’re going to put your website and your users potentially at risk by writing your own scripts, do as much studying beforehand as possible to reduce the risk. Take advice from experts in the field and don’t base your scripts on known insecure code.

Recommended Reading

These are a few books and articles I’ve found beneficial when studying PHP security:

The following links are also interesting:

Don’t Trust Awstats

What with the talk of hits recently and a few Awstats/Webaliser screenshots being flashed about as if they were some sort web badge (been there, done that, made a prat of myself) I have to chuck in my own tuppence worth: “hahahaha”. If you’re using any of the bog-standard, built-in stats trackers to try and measure your hits, don’t. Or — more specifically — don’t trust them to give you an accurate measurement.

The problem with the likes of awstats is that they don’t just track the people coming in to your website, they track hits to any file on you’ve uploaded. Imagine you’re a member at a popular forum and your chosen avatar is stored on your web space… post in a thread that is viewed by a hundred or so members and that’s a hundred of so hits making your website look oh-so-popular. The typical teeny webmaster is a member of a handful of these forums giving 300-400 hits (and God knows how many “page” views) before anyone has even looked at the website. Then of course there’s all the bots, scripts like the W3C validator…

If you’re keen on tracking the kind of traffic you get, don’t rely on Awstats: get yourself a JavaScript-based tracker. While there is the small problem in that those who browse with JS disabled or don’t support it won’t be tracked, these people are generally <1% of your web traffic anyway. If you can’t afford (or can’t justify) the likes of Mint, go for a free extreme tracker see the edit.

Don’t be naive — stats tracking is not a precise business generally. Use your common sense before bragging about your 309230349 hits (that are actually more likely to be direct linkers) because you’re just making an arse of yourself to those who know better.

Edit: Ouch! Didn’t realise extreme tracker had pop-ups now (it’s been a while since I used it). There are alternatives which get hosted externally, plus Shortstat and Slimstat if you’re up for installing a script.