{"id":1550,"date":"2010-11-16T22:49:26","date_gmt":"2010-11-16T22:49:26","guid":{"rendered":"http:\/\/dalelane.co.uk\/blog\/?p=1550"},"modified":"2010-11-17T00:14:04","modified_gmt":"2010-11-17T00:14:04","slug":"making-youtube-very-slightly-more-child-safe-with-a-firefox-extension","status":"publish","type":"post","link":"https:\/\/dalelane.co.uk\/blog\/?p=1550","title":{"rendered":"Making YouTube (very slightly) more child-safe with a Firefox extension"},"content":{"rendered":"<p><a href=\"http:\/\/www.flickr.com\/photos\/dalelane\/5183219852\/\" title=\"Kids stuff on YouTube by dalelane, on Flickr\"><img loading=\"lazy\" decoding=\"async\" src=\"http:\/\/farm2.static.flickr.com\/1013\/5183219852_a0199f4e5b_m.jpg\" align=\"right\" hspace=\"10\" vspace=\"10\" style=\"border: thin black solid\" width=\"240\" height=\"194\" alt=\"Kids stuff on YouTube\" \/><\/a>Our six year old daughter, <a href=\"http:\/\/picasaweb.google.com\/dale.lane\" target=\"_blank\">Grace<\/a>, has lost interest in kids TV recently &#8211; she&#8217;s discovered the joys of YouTube!<\/p>\n<p>She can happily spend a half-hour sat in front of the TV on Firefox (<em>our TV set-up is a <a href=\"http:\/\/dalelane.co.uk\/blog\/?p=1228\">Linux-based media centre<\/a>, so it&#8217;s proper Firefox with a keyboard and mouse<\/em>) clicking from video to video.<\/p>\n<p>I&#8217;m fine with this. It&#8217;s good: she&#8217;s getting more familiar with how to use a web browser, getting used to starting the browser, typing &#8220;youtube&#8221; into the address bar, using the search box to search for what she wants, using the &#8216;Back&#8217; button to go back to the search results if it&#8217;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. <\/p>\n<p>But&#8230;<\/p>\n<p>Well, she&#8217;s six. Not every video on YouTube is suitable for her. I&#8217;m not just talking about the stuff for over-18s. I don&#8217;t even want her to come across stuff with, for example, more swearing and violence &#8211; such as stuff that you might be happy to show a 12 year old. <\/p>\n<p>The real solution to this is what we do now &#8211; she&#8217;s doing this in the sitting room on the TV, while we&#8217;re in the room watching stuff with her. I&#8217;m not saying I want to give her a laptop, send her up to her room, and say &#8220;here&#8217;s YouTube &#8211; off you go, have fun!&#8221;. <\/p>\n<p>Even so, I wanted something to help out a little. <\/p>\n<p><!--more--><strong>Existing solutions<\/strong><\/p>\n<p><a href=\"http:\/\/www.flickr.com\/photos\/dalelane\/5182630597\/\" title=\"kidzui by dalelane, on Flickr\"><img loading=\"lazy\" decoding=\"async\" align=\"left\" hspace=\"10\" vspace=\"10\" style=\"border: thin black solid\"  src=\"http:\/\/farm5.static.flickr.com\/4128\/5182630597_6e494f264f_m.jpg\" width=\"240\" height=\"150\" alt=\"kidzui\" \/><\/a><a href=\"http:\/\/twitter.com\/#!\/laurakalbag\/status\/4596486943809536\" target=\"_blank\">Laura suggested<\/a> <a href=\"http:\/\/www.kidzui.com\/\" target=\"_blank\">kidzui<\/a> &#8211; a web browser with a simple, kid-friendly UI that will only visit pre-approved websites, games and videos. <\/p>\n<p>It&#8217;s really very cool. I&#8217;ve installed it on my laptop, and will definitely let Grace have a go with it &#8211; I think she&#8217;ll like it. It is something I think I&#8217;ll be able to leave her to play with without having to worry.<\/p>\n<p>But, it has two issues:<\/p>\n<ol>\n<li>It&#8217;s Windows-only &#8211; and the media centre computer we have runs Ubuntu. She&#8217;ll only be able to use it on my laptop.<\/li>\n<li>It&#8217;s a really, very simple UI. This is nice, but I also want Grace to get used to using &#8220;real&#8221; browsers<\/li>\n<\/ol>\n<p>So, it&#8217;s not enough. And I thought I could help plug the gap with a simple Firefox extension.<\/p>\n<p><strong>A Firefox extension &#8211; the features<\/strong><\/p>\n<p>I wrote something that lets me maintain a white-list of videos it&#8217;s okay for her to watch. This is a combination of:<\/p>\n<ul>\n<li>specific videos (identified by video id &#8211; e.g. <a href=\"http:\/\/www.youtube.com\/watch?v=WoGuEaDbGUY\" target=\"_blank\">&#8220;WoGuEaDbGUY&#8221; &#8211; a &#8216;Button Moon&#8217; episode<\/a>)<\/li>\n<li>user names &#8211; where anything uploaded by them is okay (e.g. <a href=\"http:\/\/www.youtube.com\/user\/SesameStreet\" target=\"_blank\">SesameStreet<\/a> or <a href=\"http:\/\/www.youtube.com\/user\/MuppetsStudio\" target=\"_blank\">MuppetsStudio<\/a>)<\/li>\n<\/ul>\n<p>If she tries to watch something that hasn&#8217;t been approved by video ID or user, it will:<\/p>\n<ol>\n<li>prevent it<\/li>\n<li>display a message saying she&#8217;s not allowed to watch that<\/li>\n<li>display a Yes\/No prompt asking if she&#8217;d like to be able to watch that<\/li>\n<\/ol>\n<p>The idea of the prompt is that if she clicks Yes, it&#8217;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.<\/p>\n<p><strong>Making a quick Firefox extension<\/strong><\/p>\n<p>First step, use <a href=\"https:\/\/addons.mozilla.org\/en-US\/developers\/tools\/builder\" target=\"_blank\">Add-on Builder<\/a> to make a skeleton Firefox add-on. <\/p>\n<p>Then I started hacking around with the overlay.js file &#8211; reacting to page loaded events, and comparing the URL with a couple of white-lists.<\/p>\n<p>I&#8217;ve put the source at the bottom of this post, in case it&#8217;s useful to anyone.<\/p>\n<p>I didn&#8217;t get around to automating the approvals bit yet&#8230; 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.<\/p>\n<p><strong>Pointing out the obvious flaws<\/strong><\/p>\n<p>This isn&#8217;t foolproof. It&#8217;s not meant to be &#8211; it&#8217;s something to stop her accidentally clicking through to an unsuitable video if I have to go into another room for a minute. It&#8217;s not meant to be a replacement for parental supervision. <\/p>\n<p>There&#8217;s a ton of stuff on the Internet that&#8217;s unsuitable for kids that isn&#8217;t on youtube.com &#8211; and this script ignores that. But, again, the idea is to stop her unwittingly clicking on a &#8216;Suggested Videos&#8217; link for an unsuitable video. It&#8217;s not NetNanny.<\/p>\n<p>It&#8217;s not rocket science to get around it &#8211; she could disable the Add-on! But&#8230; she&#8217;s six. So I&#8217;ve got a little while before she&#8217;ll work out that yet \ud83d\ude42<\/p>\n<p>A manually maintained white-list isn&#8217;t very scalable. But &#8211; with only one child making requests to add stuff to it, at times when I&#8217;m either in the room or just next door, and with an automated way for me to add items &#8211; it doesn&#8217;t need to be scalable.<\/p>\n<p>It&#8217;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? \ud83d\ude42<\/p>\n<p><strong>The source<\/strong><\/p>\n<pre style=\"border: thin solid silver; background-color: #eeeeee; padding: 0.7em; font-size: 1.1em; overflow: auto;\">\/\/\r\n\/\/   Grace on YouTube\r\n\/\/      \r\n\/\/        16-Nov-2010\r\n\/\/\r\n\/\/       a quick-and-dirty hack of a Firefox extension that \r\n\/\/        will react to any attempts to visit pages on youtube.com\r\n\/\/       \r\n\/\/       it will prevent any attempts to watch a video that isn't\r\n\/\/        on the extension's whitelist\r\n\/\/ \r\n\/\/       videos can be added to the whitelist:\r\n\/\/         - individually, by their unique video id\r\n\/\/         - by user - user who uploaded\/created them\r\n\/\/\r\n\/\/       http:\/\/dalelane.co.uk\/blog\/?p=1550    \r\n\/\/\r\n\/\/\r\nvar myExt_urlBarListener = {  \r\n    QueryInterface: function(aIID)  \r\n    {  \r\n        if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||  \r\n            aIID.equals(Components.interfaces.nsISupportsWeakReference) ||  \r\n            aIID.equals(Components.interfaces.nsISupports))\r\n            return this;\r\n        throw Components.results.NS_NOINTERFACE;  \r\n    },  \r\n    \r\n\r\n    \/\/\r\n    \/\/ the URL in the address bar has changed \r\n    \/\/ \r\n    \/\/\r\n    onLocationChange: function(aProgress, aRequest, aURI)  \r\n    {  \r\n        \/\/ reset flag - we're checking a new page\r\n        graceonyoutube.needToCheckYouTubeUsername = false;\r\n\r\n        \/\/ we're only interested in pages on YouTube\r\n        if (aURI.host == \"www.youtube.com\")\r\n        {\r\n            try \r\n            {\r\n                var myURL = aURI.QueryInterface(Components.interfaces.nsIURL);\r\n\r\n                \/\/\r\n                \/\/ videos with a content rating get redirected via \r\n                \/\/   a 'verify your age' page. \r\n                \/\/ if we're on that page, we're definitely going to\r\n                \/\/   an unsuitable video, so we don't bother \r\n                \/\/   checking anything else\r\n                \/\/\r\n                if (myURL.fileName == \"verify_age\")\r\n                {\r\n                    graceonyoutube.videoNotAllowed(\"http:\/\/\" + \r\n                                                   aURI.host + \r\n                                                   \"\/\" + \r\n                                                   aURI.path);\r\n                }\r\n                \/\/\r\n                \/\/ YouTube user profile pages\r\n                \/\/\r\n                else if (myURL.directory == \"\/user\/\")\r\n                {\r\n                    \/\/ get the username - if it is not in the \r\n                    \/\/   whitelist, this page is not allowed\r\n                    if (!graceonyoutube.checkUser(myURL.fileName))\r\n                    {\r\n                        graceonyoutube.videoNotAllowed(\"http:\/\/\" + \r\n                                                       aURI.host + \r\n                                                       \"\/\" + \r\n                                                       aURI.path);\r\n                    }\r\n                }\r\n                \/\/\r\n                \/\/ page for a single YouTube video\r\n                \/\/\r\n                else if (myURL.fileName == \"watch\")\r\n                {\r\n                    \/\/ is this video on the whitelist?\r\n                    var videoId = graceonyoutube.getQueryParameter(myURL.query, \"v\");\r\n                    var isKnownVideo = graceonyoutube.checkVideo(videoId);\r\n                    \r\n                    \/\/ if the specific video is not on the whitelist, then\r\n                    \/\/  we need to check if the creator is on the user's \r\n                    \/\/  whitelist\r\n                    if (isKnownVideo == false)\r\n                    {\r\n                        \/\/ we can't do this immediately - the user's name\r\n                        \/\/  is in the body of the page that won't have \r\n                        \/\/  loaded yet\r\n                        \/\/ we set a flag so that once the page body has\r\n                        \/\/  finished loading, we will check the username\r\n                        graceonyoutube.needToCheckYouTubeUsername = true;\r\n                    }\r\n                }\r\n            }\r\n            catch(e) \r\n            {\r\n                \/\/ the URI is not an URL\r\n            }\r\n        }\r\n    },  \r\n    \r\n    onStateChange: function(a, b, c, d) {},  \r\n    onProgressChange: function(a, b, c, d, e, f) {},  \r\n    onStatusChange: function(a, b, c, d) {},  \r\n    onSecurityChange: function(a, b, c) {}  \r\n};  \r\n\r\nvar graceonyoutube = {\r\n\r\n    \/\/ -------------------------------------------------------\r\n    \/\/     WHITELISTS - shortened for purposes of blog post\r\n    \/\/ -------------------------------------------------------\r\n    \r\n    \/\/ lower-case - usernames are not case-sensitive\r\n    usersWhitelist : [ \"sesamestreet\", \r\n                       \"muppetsstudio\" ],\r\n\r\n    \/\/ video IDs are case-sensitive\r\n    videosWhitelist : [ \"gyvsQBcG9OU\", \"cbc3llYjmZ4\", \"WoGuEaDbGUY\",\r\n                        \"Z9oYKR1KDEk\", \"G-qpdNTyMuM\", \"Z9oYKR1KDEk\", \r\n                        \"VDL4N6_MqTA\", \"9k41NVcW0P0\", \"-Uib_Tkb_V8\", \r\n                        \"dPvHRIHZDUs\", \"2zL5HyQua6E\", \"QIeuY9cMauM\",\r\n                        \"f20BLJGHNXY\", \"iYS4sR_EMpU\", \"WLEKEx7MBAQ\", \r\n                        \"QIeuY9cMauM\", \"GnbrXEcBZKQ\", \"UUYYApNBeAE\", \r\n                        \"b0FVxRnA-Kw\", \"4Plrz69XiYo\", \"o-jN8n-WBI0\", \r\n                        \"4aUMjesfQUw\", \"11Dzctnnrrg\", \"IjJOXY8_BmU\",\r\n                        \"Cdndiv1zCPY\", \"hF5b2AENE4Q\", \"JEjk_lnsLU4\",\r\n                        \"cbc3llYjmZ4\", \"Cfpk8QEhK1c\", \"wTD79-o9aVM\",\r\n                        \"Fy1n2WLtQMc\", \"RNtqhiAwXVI\", \"N8zd-rWQbOo\",\r\n                        \"Zq-EKFLH2Jg\", \"9TgyNvHotiM\", \"pjw2JshcIT4\",\r\n                        \"T6nwXpmREzI\", \"W7-PKvQWWf4\", \"9v6FYQRPNDI\",\r\n                        \"YAEKlkjVzDA\", \"XK8JzGj1q5k\", \"rUCs53CpfD4\" ],\r\n    \r\n    \r\n    \r\n    \/\/ -------------------------------------------------------\r\n    \r\n     \r\n    \/\/ if true, we are waiting for the current page to \r\n    \/\/   download, so that we can find the name of the \r\n    \/\/   user who created the video, and look for it in \r\n    \/\/   the users Whitelist\r\n    needToCheckYouTubeUsername : false,\r\n\r\n\r\n    \/\/\r\n    \/\/ prepare the listeners used by the extension\r\n    \/\/\r\n    onLoad: function() \r\n    {\r\n        gBrowser.addEventListener(\"DOMContentLoaded\",\r\n                                  function(aEvent)\r\n                                  {\r\n                                      \/\/ do we need to look at the page content?\r\n                                      \/\/  return immediately if not\r\n                                      if (graceonyoutube.needToCheckYouTubeUsername &&\r\n                                          (aEvent.originalTarget.nodeName == \"#document\"))\r\n                                      {\r\n                                          var username = graceonyoutube.findUserName();\r\n                                          \/\/ can we find the username on the page?\r\n                                          \/\/  if not, we keep waiting\r\n                                          if (username != \"\")\r\n                                          {\r\n                                              \/\/ stop looking at page content\r\n                                              graceonyoutube.needToCheckYouTubeUsername = false;\r\n                                 \r\n                                              if (!graceonyoutube.checkUser(username))\r\n                                              {\r\n                                                  graceonyoutube.videoNotAllowed(aEvent.originalTarget.location.href);\r\n                                              }\r\n                                          }\r\n                                      }\r\n                                  },\r\n                                  false);\r\n\r\n        gBrowser.addProgressListener(myExt_urlBarListener,  \r\n                                     Components.interfaces.nsIWebProgress.NOTIFY_LOCATION);\r\n    },\r\n\r\n\r\n    \/\/ \r\n    \/\/ checks if the provided username is in the users whitelist\r\n    \/\/ \r\n    \/\/   note: user names are not case sensitive\r\n    \/\/\r\n    checkUser: function(username) {\r\n        return this.isItemInWhitelist(username.toLowerCase(), this.usersWhitelist);\r\n    },\r\n\r\n    \/\/ \r\n    \/\/ checks if the provided video id is in the videos whitelist\r\n    \/\/ \r\n    \/\/   note: video ids are case sensitive\r\n    \/\/\r\n    checkVideo: function(videoid) {\r\n        return this.isItemInWhitelist(videoid, this.videosWhitelist);\r\n    },\r\n\r\n    \/\/\r\n    \/\/ the username on a YouTube video page is contained in the \r\n    \/\/   second (last of 2) &lt;a&gt; tags with the \r\n    \/\/   watch-description-username class\r\n    \/\/ \r\n    \/\/ we find this string and return it\r\n    \/\/ \r\n    findUserName: function() {\r\n        var usernameTags = gBrowser.contentDocument.evaluate(\"\/\/a[@class='watch-description-username'][@href]\",\r\n                                                             gBrowser.contentDocument,\r\n                                                             null,\r\n                                                             XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,\r\n                                                             null);\r\n        if (usernameTags.snapshotLength &gt; 0)\r\n        {\r\n            return usernameTags.snapshotItem(usernameTags.snapshotLength - 1).textContent;\r\n        }\r\n\r\n        return \"\";\r\n    },\r\n\r\n    \/\/ \r\n    \/\/ get the value of the provided parameter in the given query string\r\n    \/\/  \r\n    \/\/  e.g. input: queryString    =&gt;  mykey1=value1&mykey2=value2&mykey3=value3 \r\n    \/\/              parameterName  =&gt;  mykey2\r\n    \/\/       output:       =&gt; value2\r\n    \/\/\r\n    getQueryParameter: function(queryString, parameterName) {\r\n        var keyValuePairs = queryString.split(\"&\");\r\n        for (var keyvalueIdx = 0; keyvalueIdx &lt; keyValuePairs.length; keyvalueIdx++)\r\n        {\r\n            var keyvaluePair = keyValuePairs[keyvalueIdx];\r\n            var params = keyvaluePair.split(\"=\");\r\n            if ((params.length == 2) &&\r\n                (params[0] == parameterName))\r\n            {\r\n                return params[1];\r\n            }\r\n        }\r\n        return \"\";\r\n    },\r\n\r\n    \/\/\r\n    \/\/ look for a string in an array of strings\r\n    \/\/   return true if found\r\n    \/\/\r\n    isItemInWhitelist: function (item, whitelist)\r\n    {\r\n        for (var i=0; i &lt; whitelist.length; i++)\r\n        {\r\n            if (whitelist[i] == item)\r\n            {\r\n                return true;\r\n            }\r\n        }\r\n        return false;\r\n    },\r\n\r\n\r\n    \/\/\r\n    \/\/ called if we are not going to allow this page to \r\n    \/\/   be shown\r\n    \/\/ \r\n    \/\/ we offer the chance of writing the URL of the page\r\n    \/\/   to a log file\r\n    \/\/ \r\n    \/\/ this is because at some point in the future, I'm \r\n    \/\/   thinking of having some way to approve\/reject\r\n    \/\/   these URLs, dynamically adding the approved \r\n    \/\/   URLs to the whitelist\r\n    \/\/\r\n    videoNotAllowed: function(url) \r\n    {\r\n        \/\/ first thing we do is leave this page\r\n        gBrowser.goBack();\r\n\r\n        \/\/ prompt whether we want to record the URL we \r\n        \/\/   just left\r\n        var promptService = Components.classes[\"@mozilla.org\/embedcomp\/prompt-service;1\"].getService(Components.interfaces.nsIPromptService);\r\n\r\n        if (promptService.confirm(window, \r\n                                  \"You are not allowed to watch this\", \r\n                                  \"Do you want to ask Mum or Dad to watch this?\"))\r\n        {\r\n            \/\/ append URL to log file\r\n\r\n            url = \"\\n\" + url;\r\n\r\n            var file = Components.classes[\"@mozilla.org\/file\/local;1\"].createInstance(Components.interfaces.nsILocalFile);\r\n            file.initWithPath(\"\/home\/dale\/logs\/youtube.requests\");\r\n\r\n            var fileStream = Components.classes['@mozilla.org\/network\/file-output-stream;1'].createInstance(Components.interfaces.nsIFileOutputStream);\r\n            fileStream.init(file, 0x02 | 0x08 | 0x10, 0x666, false);  \r\n\r\n            var converterStream = Components.classes['@mozilla.org\/intl\/converter-output-stream;1'].createInstance(Components.interfaces.nsIConverterOutputStream);\r\n            converterStream.init(fileStream, \"UTF-8\", url.length, Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);\r\n            converterStream.writeString(url);\r\n            converterStream.close();\r\n            fileStream.close();\r\n        }\r\n    },\r\n\r\n\r\n    cleanUp: function() {\r\n        gBrowser.removeProgressListener(myExt_urlBarListener);\r\n    }\r\n};\r\n\r\nwindow.addEventListener(\"load\",   graceonyoutube.onLoad,  false);\r\nwindow.addEventListener(\"unload\", graceonyoutube.cleanUp, false);<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Our six year old daughter, Grace, has lost interest in kids TV recently &#8211; she&#8217;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&#8217;s proper Firefox with a keyboard and mouse) clicking from video to [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[473,238,152,160,474,67],"class_list":["post-1550","post","type-post","status-publish","format-standard","hentry","category-code","tag-child","tag-children","tag-firefox","tag-javascript","tag-safety","tag-youtube"],"_links":{"self":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts\/1550","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1550"}],"version-history":[{"count":0,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts\/1550\/revisions"}],"wp:attachment":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1550"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1550"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1550"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}