Making YouTube (very slightly) more child-safe with a Firefox extension

Kids stuff on YouTubeOur six year old daughter, Grace, has lost interest in kids TV recently – she’s discovered the joys of YouTube!

She can happily spend a half-hour sat in front of the TV on Firefox (our TV set-up is a Linux-based media centre, so it’s proper Firefox with a keyboard and mouse) clicking from video to video.

I’m fine with this. It’s good: she’s getting more familiar with how to use a web browser, getting used to starting the browser, typing “youtube” into the address bar, using the search box to search for what she wants, using the ‘Back’ button to go back to the search results if it’s not what she wanted, and so on. This is all good stuff, let alone the fact that there is a lot of content on YouTube that is actually ideal for kids.

But…

Well, she’s six. Not every video on YouTube is suitable for her. I’m not just talking about the stuff for over-18s. I don’t even want her to come across stuff with, for example, more swearing and violence – such as stuff that you might be happy to show a 12 year old.

The real solution to this is what we do now – she’s doing this in the sitting room on the TV, while we’re in the room watching stuff with her. I’m not saying I want to give her a laptop, send her up to her room, and say “here’s YouTube – off you go, have fun!”.

Even so, I wanted something to help out a little.

Existing solutions

kidzuiLaura suggested kidzui – a web browser with a simple, kid-friendly UI that will only visit pre-approved websites, games and videos.

It’s really very cool. I’ve installed it on my laptop, and will definitely let Grace have a go with it – I think she’ll like it. It is something I think I’ll be able to leave her to play with without having to worry.

But, it has two issues:

  1. It’s Windows-only – and the media centre computer we have runs Ubuntu. She’ll only be able to use it on my laptop.
  2. It’s a really, very simple UI. This is nice, but I also want Grace to get used to using “real” browsers

So, it’s not enough. And I thought I could help plug the gap with a simple Firefox extension.

A Firefox extension – the features

I wrote something that lets me maintain a white-list of videos it’s okay for her to watch. This is a combination of:

If she tries to watch something that hasn’t been approved by video ID or user, it will:

  1. prevent it
  2. display a message saying she’s not allowed to watch that
  3. display a Yes/No prompt asking if she’d like to be able to watch that

The idea of the prompt is that if she clicks Yes, it’ll add info about the video to an approvals list. Then, if the video she wanted to watch is fine, I can use the approvals list to update the white-list.

Making a quick Firefox extension

First step, use Add-on Builder to make a skeleton Firefox add-on.

Then I started hacking around with the overlay.js file – reacting to page loaded events, and comparing the URL with a couple of white-lists.

I’ve put the source at the bottom of this post, in case it’s useful to anyone.

I didn’t get around to automating the approvals bit yet… this script took me about 30 or 40 minutes, and that was all the time I had to play on it this evening! I might come back to it another time, though.

Pointing out the obvious flaws

This isn’t foolproof. It’s not meant to be – it’s something to stop her accidentally clicking through to an unsuitable video if I have to go into another room for a minute. It’s not meant to be a replacement for parental supervision.

There’s a ton of stuff on the Internet that’s unsuitable for kids that isn’t on youtube.com – and this script ignores that. But, again, the idea is to stop her unwittingly clicking on a ‘Suggested Videos’ link for an unsuitable video. It’s not NetNanny.

It’s not rocket science to get around it – she could disable the Add-on! But… she’s six. So I’ve got a little while before she’ll work out that yet 🙂

A manually maintained white-list isn’t very scalable. But – with only one child making requests to add stuff to it, at times when I’m either in the room or just next door, and with an automated way for me to add items – it doesn’t need to be scalable.

It’s good enough to give me a little bit of peace of mind. And it was an excuse to muck about with a bit of JavaScript. What more could you want? 🙂

The source

//
//   Grace on YouTube
//      
//        16-Nov-2010
//
//       a quick-and-dirty hack of a Firefox extension that 
//        will react to any attempts to visit pages on youtube.com
//       
//       it will prevent any attempts to watch a video that isn't
//        on the extension's whitelist
// 
//       videos can be added to the whitelist:
//         - individually, by their unique video id
//         - by user - user who uploaded/created them
//
//       http://dalelane.co.uk/blog/?p=1550    
//
//
var myExt_urlBarListener = {  
    QueryInterface: function(aIID)  
    {  
        if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||  
            aIID.equals(Components.interfaces.nsISupportsWeakReference) ||  
            aIID.equals(Components.interfaces.nsISupports))
            return this;
        throw Components.results.NS_NOINTERFACE;  
    },  
    

    //
    // the URL in the address bar has changed 
    // 
    //
    onLocationChange: function(aProgress, aRequest, aURI)  
    {  
        // reset flag - we're checking a new page
        graceonyoutube.needToCheckYouTubeUsername = false;

        // we're only interested in pages on YouTube
        if (aURI.host == "www.youtube.com")
        {
            try 
            {
                var myURL = aURI.QueryInterface(Components.interfaces.nsIURL);

                //
                // videos with a content rating get redirected via 
                //   a 'verify your age' page. 
                // if we're on that page, we're definitely going to
                //   an unsuitable video, so we don't bother 
                //   checking anything else
                //
                if (myURL.fileName == "verify_age")
                {
                    graceonyoutube.videoNotAllowed("http://" + 
                                                   aURI.host + 
                                                   "/" + 
                                                   aURI.path);
                }
                //
                // YouTube user profile pages
                //
                else if (myURL.directory == "/user/")
                {
                    // get the username - if it is not in the 
                    //   whitelist, this page is not allowed
                    if (!graceonyoutube.checkUser(myURL.fileName))
                    {
                        graceonyoutube.videoNotAllowed("http://" + 
                                                       aURI.host + 
                                                       "/" + 
                                                       aURI.path);
                    }
                }
                //
                // page for a single YouTube video
                //
                else if (myURL.fileName == "watch")
                {
                    // is this video on the whitelist?
                    var videoId = graceonyoutube.getQueryParameter(myURL.query, "v");
                    var isKnownVideo = graceonyoutube.checkVideo(videoId);
                    
                    // if the specific video is not on the whitelist, then
                    //  we need to check if the creator is on the user's 
                    //  whitelist
                    if (isKnownVideo == false)
                    {
                        // we can't do this immediately - the user's name
                        //  is in the body of the page that won't have 
                        //  loaded yet
                        // we set a flag so that once the page body has
                        //  finished loading, we will check the username
                        graceonyoutube.needToCheckYouTubeUsername = true;
                    }
                }
            }
            catch(e) 
            {
                // the URI is not an URL
            }
        }
    },  
    
    onStateChange: function(a, b, c, d) {},  
    onProgressChange: function(a, b, c, d, e, f) {},  
    onStatusChange: function(a, b, c, d) {},  
    onSecurityChange: function(a, b, c) {}  
};  

var graceonyoutube = {

    // -------------------------------------------------------
    //     WHITELISTS - shortened for purposes of blog post
    // -------------------------------------------------------
    
    // lower-case - usernames are not case-sensitive
    usersWhitelist : [ "sesamestreet", 
                       "muppetsstudio" ],

    // video IDs are case-sensitive
    videosWhitelist : [ "gyvsQBcG9OU", "cbc3llYjmZ4", "WoGuEaDbGUY",
                        "Z9oYKR1KDEk", "G-qpdNTyMuM", "Z9oYKR1KDEk", 
                        "VDL4N6_MqTA", "9k41NVcW0P0", "-Uib_Tkb_V8", 
                        "dPvHRIHZDUs", "2zL5HyQua6E", "QIeuY9cMauM",
                        "f20BLJGHNXY", "iYS4sR_EMpU", "WLEKEx7MBAQ", 
                        "QIeuY9cMauM", "GnbrXEcBZKQ", "UUYYApNBeAE", 
                        "b0FVxRnA-Kw", "4Plrz69XiYo", "o-jN8n-WBI0", 
                        "4aUMjesfQUw", "11Dzctnnrrg", "IjJOXY8_BmU",
                        "Cdndiv1zCPY", "hF5b2AENE4Q", "JEjk_lnsLU4",
                        "cbc3llYjmZ4", "Cfpk8QEhK1c", "wTD79-o9aVM",
                        "Fy1n2WLtQMc", "RNtqhiAwXVI", "N8zd-rWQbOo",
                        "Zq-EKFLH2Jg", "9TgyNvHotiM", "pjw2JshcIT4",
                        "T6nwXpmREzI", "W7-PKvQWWf4", "9v6FYQRPNDI",
                        "YAEKlkjVzDA", "XK8JzGj1q5k", "rUCs53CpfD4" ],
    
    
    
    // -------------------------------------------------------
    
     
    // if true, we are waiting for the current page to 
    //   download, so that we can find the name of the 
    //   user who created the video, and look for it in 
    //   the users Whitelist
    needToCheckYouTubeUsername : false,


    //
    // prepare the listeners used by the extension
    //
    onLoad: function() 
    {
        gBrowser.addEventListener("DOMContentLoaded",
                                  function(aEvent)
                                  {
                                      // do we need to look at the page content?
                                      //  return immediately if not
                                      if (graceonyoutube.needToCheckYouTubeUsername &&
                                          (aEvent.originalTarget.nodeName == "#document"))
                                      {
                                          var username = graceonyoutube.findUserName();
                                          // can we find the username on the page?
                                          //  if not, we keep waiting
                                          if (username != "")
                                          {
                                              // stop looking at page content
                                              graceonyoutube.needToCheckYouTubeUsername = false;
                                 
                                              if (!graceonyoutube.checkUser(username))
                                              {
                                                  graceonyoutube.videoNotAllowed(aEvent.originalTarget.location.href);
                                              }
                                          }
                                      }
                                  },
                                  false);

        gBrowser.addProgressListener(myExt_urlBarListener,  
                                     Components.interfaces.nsIWebProgress.NOTIFY_LOCATION);
    },


    // 
    // checks if the provided username is in the users whitelist
    // 
    //   note: user names are not case sensitive
    //
    checkUser: function(username) {
        return this.isItemInWhitelist(username.toLowerCase(), this.usersWhitelist);
    },

    // 
    // checks if the provided video id is in the videos whitelist
    // 
    //   note: video ids are case sensitive
    //
    checkVideo: function(videoid) {
        return this.isItemInWhitelist(videoid, this.videosWhitelist);
    },

    //
    // the username on a YouTube video page is contained in the 
    //   second (last of 2) <a> tags with the 
    //   watch-description-username class
    // 
    // we find this string and return it
    // 
    findUserName: function() {
        var usernameTags = gBrowser.contentDocument.evaluate("//a[@class='watch-description-username'][@href]",
                                                             gBrowser.contentDocument,
                                                             null,
                                                             XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
                                                             null);
        if (usernameTags.snapshotLength > 0)
        {
            return usernameTags.snapshotItem(usernameTags.snapshotLength - 1).textContent;
        }

        return "";
    },

    // 
    // get the value of the provided parameter in the given query string
    //  
    //  e.g. input: queryString    =>  mykey1=value1&mykey2=value2&mykey3=value3 
    //              parameterName  =>  mykey2
    //       output:       => value2
    //
    getQueryParameter: function(queryString, parameterName) {
        var keyValuePairs = queryString.split("&");
        for (var keyvalueIdx = 0; keyvalueIdx < keyValuePairs.length; keyvalueIdx++)
        {
            var keyvaluePair = keyValuePairs[keyvalueIdx];
            var params = keyvaluePair.split("=");
            if ((params.length == 2) &&
                (params[0] == parameterName))
            {
                return params[1];
            }
        }
        return "";
    },

    //
    // look for a string in an array of strings
    //   return true if found
    //
    isItemInWhitelist: function (item, whitelist)
    {
        for (var i=0; i < whitelist.length; i++)
        {
            if (whitelist[i] == item)
            {
                return true;
            }
        }
        return false;
    },


    //
    // called if we are not going to allow this page to 
    //   be shown
    // 
    // we offer the chance of writing the URL of the page
    //   to a log file
    // 
    // this is because at some point in the future, I'm 
    //   thinking of having some way to approve/reject
    //   these URLs, dynamically adding the approved 
    //   URLs to the whitelist
    //
    videoNotAllowed: function(url) 
    {
        // first thing we do is leave this page
        gBrowser.goBack();

        // prompt whether we want to record the URL we 
        //   just left
        var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService);

        if (promptService.confirm(window, 
                                  "You are not allowed to watch this", 
                                  "Do you want to ask Mum or Dad to watch this?"))
        {
            // append URL to log file

            url = "\n" + url;

            var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
            file.initWithPath("/home/dale/logs/youtube.requests");

            var fileStream = Components.classes['@mozilla.org/network/file-output-stream;1'].createInstance(Components.interfaces.nsIFileOutputStream);
            fileStream.init(file, 0x02 | 0x08 | 0x10, 0x666, false);  

            var converterStream = Components.classes['@mozilla.org/intl/converter-output-stream;1'].createInstance(Components.interfaces.nsIConverterOutputStream);
            converterStream.init(fileStream, "UTF-8", url.length, Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
            converterStream.writeString(url);
            converterStream.close();
            fileStream.close();
        }
    },


    cleanUp: function() {
        gBrowser.removeProgressListener(myExt_urlBarListener);
    }
};

window.addEventListener("load",   graceonyoutube.onLoad,  false);
window.addEventListener("unload", graceonyoutube.cleanUp, false);

Tags: , , , , ,

One Response to “Making YouTube (very slightly) more child-safe with a Firefox extension”

  1. Ali Khalid says:

    Thanks i was searching for this …as its very embarrassing when you are sitting with kids during surfing adult adds appear.