From 95ee40fa8cdca9cff0aba033a4352fc0621a9583 Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Mon, 5 Mar 2018 19:06:55 +0100 Subject: Add generic click behavior script Configureable. Clicks elements matching one (or more) CSS selectors once or multiple times. Currently supported: Facebook, Twitter, Disqus (embedded iframe) --- crocoite/behavior.py | 15 +++--- crocoite/data/click.js | 111 ++++++++++++++++++++++++++++++++++++++ crocoite/data/per-site/twitter.js | 30 ----------- 3 files changed, 119 insertions(+), 37 deletions(-) create mode 100644 crocoite/data/click.js delete mode 100644 crocoite/data/per-site/twitter.js (limited to 'crocoite') diff --git a/crocoite/behavior.py b/crocoite/behavior.py index e4ec93b..26841aa 100644 --- a/crocoite/behavior.py +++ b/crocoite/behavior.py @@ -222,17 +222,18 @@ class Screenshot (Behavior): 'X-Chrome-Viewport': viewport}) writer.write_record (record) -### Site-specific scripts ### +class Click (JsOnload): + """ Generic link clicking """ + + name = 'click' + scriptPath = 'click.js' -class Twitter (HostnameFilter, JsOnload): - name = 'twitter' - scriptPath = 'per-site/twitter.js' - hostname = ['com', 'twitter'] +### Site-specific scripts ### # available behavior scripts. Order matters, move those modifying the page # towards the end of available -generic = [Scroll, EmulateScreenMetrics] -perSite = [Twitter] +generic = [Scroll, EmulateScreenMetrics, Click] +perSite = [] available = generic + perSite + [Screenshot, DomSnapshot] availableNames = set (map (lambda x: x.name, available)) diff --git a/crocoite/data/click.js b/crocoite/data/click.js new file mode 100644 index 0000000..7013487 --- /dev/null +++ b/crocoite/data/click.js @@ -0,0 +1,111 @@ +/* Generic clicking + * + * We can’t just click every clickable object, since there may be side-effects + * like navigating to a different location. Thus whitelist known elements. + */ + +(function(){ +const selectorFlag = Object.freeze ({ + none: 0, + multi: 1, /* click item multiple times */ +}); +const sites = Object.freeze ([ + { + hostname: /^www\.facebook\.com$/i, + selector: [ + /* show more comments */ + {s: 'a.UFIPagerLink[role=button]', flags: selectorFlag.none}, + /* show nested comments*/ + {s: 'a.UFICommentLink[role=button]', flags: selectorFlag.none}, + ], + }, { + hostname: /^twitter\.com$/i, + selector: [ + /* expand threads */ + {s: 'a.ThreadedConversation-moreRepliesLink', flags: selectorFlag.none}, + /* show hidden profiles */ + {s: 'button.ProfileWarningTimeline-button', flags: selectorFlag.none}, + /* show hidden/sensitive media */ + {s: 'button.Tombstone-action.js-display-this-media', flags: selectorFlag.none}, + ], + }, { + hostname: /^disqus\.com$/i, + selector: [ + /* load more comments */ + {s: 'a.load-more__button', flags: selectorFlag.multi}, + ], + } + ]); + +/* pick selectors matching current location */ +let hostname = document.location.hostname; +let selector = []; +for (let s of sites) { + if (s.hostname.test (hostname)) { + selector = selector.concat (s.selector); + } +} + +function makeClickEvent () { + return new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); +} + +/* throttle clicking */ +let queue = []; +function click () { + let o = queue.shift (); + if (o !== undefined) { + o.dispatchEvent (makeClickEvent ()); + } + return queue.length > 0; +} + +/* Element is visible if itself and all of its parents are + */ +function isVisible (o) { + if (o === null || !(o instanceof Element)) { + return true; + } + let style = window.getComputedStyle (o); + if ('parentNode' in o) { + return style.display !== 'none' && isVisible (o.parentNode); + } else { + return style.display !== 'none'; + } +} + +/* Elements are considered clickable if they are a) visible and b) not + * disabled + */ +function isClickable (o) { + return !o.hasAttribute ('disabled') && isVisible (o); +} + +/* some sites don’t remove/replace the element immediately, so keep track of + * which ones we already clicked */ +let have = new Set (); +function discover () { + for (let s of selector) { + let obj = document.querySelectorAll (s.s); + for (let o of obj) { + if (!have.has (o) && isClickable (o)) { + queue.push (o); + if (!(s.flags & selectorFlag.multi)) { + have.add (o); + } + } + } + } + if (queue.length > 0) { + window.setInterval (click, 50); + } + return true; +} + +/* XXX: can we use a mutation observer instead? */ +window.setInterval (discover, 1000); +}()); diff --git a/crocoite/data/per-site/twitter.js b/crocoite/data/per-site/twitter.js deleted file mode 100644 index 2773f64..0000000 --- a/crocoite/data/per-site/twitter.js +++ /dev/null @@ -1,30 +0,0 @@ -/* Fixups for twitter: - * - Some accounts are hidden behind a “suspicious activity” message, click - * that. - * - Click “more replies” buttons periodically (as they popup when scrolling) - */ -(function(){ -function makeClickEvent () { - return new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); -} -function expandThread () { - let links = document.querySelectorAll('a.ThreadedConversation-moreRepliesLink'); - for (let i = 0; i < links.length; i++) { - links[i].dispatchEvent (makeClickEvent ()); - } - return true; -} -function showProfile () { - var show = document.querySelector ("button.ProfileWarningTimeline-button"); - if (show) { - show.dispatchEvent (makeClickEvent ()); - } -} -window.addEventListener("load", showProfile); -/* XXX: can we use a mutation observer instead? */ -window.setInterval (expandThread, 1000); -}()); -- cgit v1.2.3