Le code source de l’application web headless recorder est disponible. Ci-dessous celui des sections enregistreur d’événements et contrôleur d’interfaces utilisateur :
Code JavaScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | import eventsToRecord from '../code-generator/dom-events-to-record' import UIController from './UIController' import actions from '../models/extension-ui-actions' import ctrl from '../models/extension-control-messages' import finder from '@medv/finder' const DEFAULT_MOUSE_CURSOR = 'default' export default class EventRecorder { constructor () { this._boundedMessageListener = null this._eventLog = [] this._previousEvent = null this._dataAttribute = null this._uiController = null this._screenShotMode = false this._isTopFrame = (window.location === window.parent.location) this._isRecordingClicks = true } boot () { // We need to check the existence of chrome for testing purposes if (chrome.storage && chrome.storage.local) { chrome.storage.local.get(['options'], ({options}) => { const { dataAttribute } = options ? options.code : {} if (dataAttribute) { this._dataAttribute = dataAttribute } this._initializeRecorder() }) } else { this._initializeRecorder() } } _initializeRecorder () { const events = Object.values(eventsToRecord) if (!window.pptRecorderAddedControlListeners) { this._addAllListeners(events) this._boundedMessageListener = this._boundedMessageListener || this._handleBackgroundMessage.bind(this) chrome.runtime.onMessage.addListener(this._boundedMessageListener) window.pptRecorderAddedControlListeners = true } if (!window.document.pptRecorderAddedControlListeners && chrome.runtime && chrome.runtime.onMessage) { window.document.pptRecorderAddedControlListeners = true } if (this._isTopFrame) { this._sendMessage({ control: ctrl.EVENT_RECORDER_STARTED }) this._sendMessage({ control: ctrl.GET_CURRENT_URL, href: window.location.href }) this._sendMessage({ control: ctrl.GET_VIEWPORT_SIZE, coordinates: { width: window.innerWidth, height: window.innerHeight } }) console.debug('Puppeteer Recorder in-page EventRecorder started') } } _handleBackgroundMessage (msg, sender, sendResponse) { console.debug('content-script: message from background', msg) if (msg && msg.action) { switch (msg.action) { case actions.TOGGLE_SCREENSHOT_MODE: this._handleScreenshotMode(false) break case actions.TOGGLE_SCREENSHOT_CLIPPED_MODE: this._handleScreenshotMode(true) break default: } } } _addAllListeners (events) { const boundedRecordEvent = this._recordEvent.bind(this) events.forEach(type => { window.addEventListener(type, boundedRecordEvent, true) }) } _sendMessage (msg) { // filter messages based on enabled / disabled features if (msg.action === 'click' && !this._isRecordingClicks) return try { // poor man's way of detecting whether this script was injected by an actual extension, or is loaded for // testing purposes if (chrome.runtime && chrome.runtime.onMessage) { chrome.runtime.sendMessage(msg) } else { this._eventLog.push(msg) } } catch (err) { console.debug('caught error', err) } } _recordEvent (e) { if (this._previousEvent && this._previousEvent.timeStamp === e.timeStamp) return this._previousEvent = e // we explicitly catch any errors and swallow them, as none node-type events are also ingested. // for these events we cannot generate selectors, which is OK try { const optimizedMinLength = (e.target.id) ? 2 : 10 // if the target has an id, use that instead of multiple other selectors const selector = this._dataAttribute ? finder(e.target, {seedMinLength: 5, optimizedMinLength: optimizedMinLength, attr: (name, _value) => name === this._dataAttribute}) : finder(e.target, {seedMinLength: 5, optimizedMinLength: optimizedMinLength}) const msg = { selector: selector, value: e.target.value, tagName: e.target.tagName, action: e.type, keyCode: e.keyCode ? e.keyCode : null, href: e.target.href ? e.target.href : null, coordinates: EventRecorder._getCoordinates(e) } this._sendMessage(msg) } catch (e) {} } _getEventLog () { return this._eventLog } _clearEventLog () { this._eventLog = [] } _handleScreenshotMode (isClipped) { this._disableClickRecording() this._uiController = new UIController({ showSelector: isClipped }) this._screenShotMode = !this._screenShotMode document.body.style.cursor = 'crosshair' console.debug('screenshot mode:', this._screenShotMode) if (this._screenShotMode) { this._uiController.showSelector() } else { this._uiController.hideSelector() } this._uiController.on('click', event => { this._screenShotMode = false document.body.style.cursor = DEFAULT_MOUSE_CURSOR this._sendMessage({ control: ctrl.GET_SCREENSHOT, value: event.clip }) this._enableClickRecording() }) } _disableClickRecording () { this._isRecordingClicks = false } _enableClickRecording () { this._isRecordingClicks = true } static _getCoordinates (evt) { const eventsWithCoordinates = { mouseup: true, mousedown: true, mousemove: true, mouseover: true } return eventsWithCoordinates[evt.type] ? { x: evt.clientX, y: evt.clientY } : null } static _formatDataSelector (element, attribute) { return `[${attribute}="${element.getAttribute(attribute)}"]` } } |
Code JavaScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | import EventEmitter from 'events' const BORDER_THICKNESS = 3 const defaults = { showSelector: false } class UIController extends EventEmitter { constructor (options) { options = Object.assign({}, defaults, options) super() this._overlay = null this._selector = null this._element = null this._dimensions = {} this._showSelector = options.showSelector this._boundeMouseMove = this._mousemove.bind(this) this._boundeMouseUp = this._mouseup.bind(this) } showSelector () { console.debug('UIController:show') if (!this._overlay) { this._overlay = document.createElement('div') this._overlay.className = 'headlessRecorderOverlay' this._overlay.style.position = 'fixed' this._overlay.style.top = '0px' this._overlay.style.left = '0px' this._overlay.style.width = '100%' this._overlay.style.height = '100%' this._overlay.style.pointerEvents = 'none' if (this._showSelector) { this._selector = document.createElement('div') this._selector.className = 'headlessRecorderOutline' this._selector.style.position = 'fixed' this._selector.style.border = BORDER_THICKNESS + 'px solid rgba(69,200,241,0.8)' this._selector.style.borderRadius = '3px' this._overlay.appendChild(this._selector) } } if (!this._overlay.parentNode) { document.body.appendChild(this._overlay) document.body.addEventListener('mousemove', this._boundeMouseMove, false) document.body.addEventListener('click', this._boundeMouseUp, false) } } hideSelector () { console.debug('UIController:hide') if (this._overlay) { document.body.removeChild(this._overlay) } this._overlay = this._selector = this._element = null this._dimensions = {} } _mousemove (e) { if (this._element !== e.target) { this._element = e.target this._dimensions.top = -window.scrollY this._dimensions.left = -window.scrollX let elem = e.target while (elem && elem !== document.body) { this._dimensions.top += elem.offsetTop this._dimensions.left += elem.offsetLeft elem = elem.offsetParent } this._dimensions.width = this._element.offsetWidth this._dimensions.height = this._element.offsetHeight if (this._selector) { this._selector.style.top = (this._dimensions.top - BORDER_THICKNESS) + 'px' this._selector.style.left = (this._dimensions.left - BORDER_THICKNESS) + 'px' this._selector.style.width = this._dimensions.width + 'px' this._selector.style.height = this._dimensions.height + 'px' console.debug(`top: ${this._selector.style.top}, left: ${this._selector.style.left}, width: ${this._selector.style.width}, height: ${this._selector.style.height}`) } } } _mouseup (e) { this._overlay.style.backgroundColor = 'white' setTimeout(() => { this._overlay.style.backgroundColor = 'none' this._cleanup() let clip = null if (this._selector) { clip = { x: this._selector.style.left, y: this._selector.style.top, width: this._selector.style.width, height: this._selector.style.height } } this.emit('click', { clip, raw: e }) }, 100) } _cleanup () { document.body.removeEventListener('mousemove', this._boundeMouseMove, false) document.body.removeEventListener('mouseup', this._boundeMouseUp, false) document.body.removeChild(this._overlay) } } module.exports = UIController |
En matière d’open source, l’un des principes de base est d’ouvrir l’accès d’une base de code à des tiers pour qu’ils puissent en faire une copie et introduire des modifications en fonction de la nouvelle orientation recherchée. Les contraintes additionnelles sont spécifiques à chaque type de licence. C’est grosso modo ce sur quoi Amazon s’est appuyé pour créer un fork du projet Headless recorder sachant que ce dernier est publié sous une licence permissive : Apache version 2.0. L’entreprise l’a ensuite lancé comme son propre service, ce que rappelle l’auteur de l’application Headless recorder tout en rappelant quelles sont les obligations d’Amazon…
La situation n’est pas sans faire penser à celle du système de gestion de base de données clé-valeur scalable – Redis. Le cœur du projet est publié sous licence permissive BSD. Au troisième trimestre de l’année 2018, l’équipe Redis a procédé à une reformulation des licences de certains de ses modules complémentaires, bloquant de fait leur utilisation par des tiers offrant des services commerciaux basés sur Redis. L’entreprise visait les gros fournisseurs de cloud computing : Google, Amazon, Microsoft, IBM.
« Les fournisseurs de services cloud ont à plusieurs reprises tiré profit de projets open source réussis et les ont reconditionnés en offres de services propriétaires compétitives. Les fournisseurs de cloud computing contribuent très peu (voire pas du tout) à ces projets open source. Au lieu de cela, ils utilisent leur nature monopolistique pour en tirer des centaines de millions de dollars de revenus. Ce comportement porte préjudice aux communautés open source et a mis en faillite certaines des entreprises à pied d’œuvre dans la sphère », avait lancé l’équipe Redis.
Le responsable de ces questions chez Amazon a fait une sortie pour indiquer qu’il n’avait pas été mis au courant et est en train d’étudier la question. « Je vous suis reconnaissant de votre aide dans ce domaine. AWS utilise beaucoup de logiciels libres et nous y contribuons beaucoup. Mais l'open source est en fin de compte une communauté d'utilisateurs et je pense personnellement que nous aurions pu faire mieux ici. Je vais voir avec vous comment nous pouvons mieux soutenir l'excellent travail que vous faites avec headless recorder », a-t-il ajouté.
Source : Twitter, annonce du lancement du service par Amazon
Et vous ?
Qui d'Amazon ou des responsables du projet Headless recorder est le plus à blâmer dans cette situation ?
Le projet Headless recorder ne bénéficie-t-il pas quelque part d'une meilleure visibilité au travers d'Amazon ?
voir aussi :
la rubrique Cloud computing