Insérez du code dans le contexte de la page à l'aide d'un script de contenu

480

J'apprends à créer des extensions Chrome. Je viens de commencer à en développer un pour capturer les événements YouTube. Je veux l'utiliser avec YouTube Flash Player (plus tard, je vais essayer de le rendre compatible avec HTML5).

manifest.json:

{
    "name": "MyExtension",
    "version": "1.0",
    "description": "Gotta catch Youtube events!",
    "permissions": ["tabs", "http://*/*"],
    "content_scripts" : [{
        "matches" : [ "www.youtube.com/*"],
        "js" : ["myScript.js"]
    }]
}

myScript.js:

function state() { console.log("State Changed!"); }
var player = document.getElementById("movie_player");
player.addEventListener("onStateChange", "state");
console.log("Started!");

Le problème est que la console me donne le "Démarré!" , mais il n'y a pas de "Changement d'état!" lorsque je joue / met en pause des vidéos YouTube.

Lorsque ce code est mis dans la console, cela a fonctionné. Qu'est-ce que je fais mal?

André Alves
la source
14
essayez de supprimer les guillemets autour du nom de votre fonction:player.addEventListener("onStateChange", state);
Eduardo
2
Il est également à noter que lors de l'écriture de correspondances, n'oubliez pas d'inclure https://ou http://, cela www.youtube.com/*ne vous laisserait pas emballer l'extension et lancerait une erreur de séparateur de schéma manquant
Nilay Vishwakarma

Réponses:

875

Les scripts de contenu sont exécutés dans un environnement "monde isolé" . Vous devez injecter votre state()méthode dans la page elle-même.

Lorsque vous souhaitez utiliser l'une des chrome.*API du script, vous devez implémenter un gestionnaire d'événements spécial, comme décrit dans cette réponse: Extension Chrome - récupérer le message d'origine de Gmail .

Sinon, si vous n'avez pas à utiliser d' chrome.*API, je vous recommande fortement d'injecter tout votre code JS dans la page via l'ajout d'une <script>balise:

Table des matières

  • Méthode 1: injecter un autre fichier
  • Méthode 2: injecter du code incorporé
  • Méthode 2b: utilisation d'une fonction
  • Méthode 3: utiliser un événement en ligne
  • Valeurs dynamiques dans le code injecté

Méthode 1: injecter un autre fichier

C'est la méthode la plus simple / la meilleure lorsque vous avez beaucoup de code. Incluez votre code JS réel dans un fichier au sein de votre extension, par exemple script.js. Ensuite, laissez votre script de contenu être le suivant (expliqué ici: Javascript personnalisé "Raccourci d'application" Google Chome ):

var s = document.createElement('script');
// TODO: add "script.js" to web_accessible_resources in manifest.json
s.src = chrome.runtime.getURL('script.js');
s.onload = function() {
    this.remove();
};
(document.head || document.documentElement).appendChild(s);

Remarque: Si vous utilisez cette méthode, le script.jsfichier injecté doit être ajouté à la "web_accessible_resources"section ( exemple ). Si vous ne le faites pas, Chrome refusera de charger votre script et affichera l'erreur suivante dans la console:

Refuser la charge de chrome-extension: // [EXTENSIONID] /script.js. Les ressources doivent être répertoriées dans la clé de manifeste web_accessible_resources afin d'être chargées par des pages en dehors de l'extension.

Méthode 2: injecter du code incorporé

Cette méthode est utile lorsque vous souhaitez exécuter rapidement un petit morceau de code. (Voir aussi: Comment désactiver les raccourcis Facebook avec l'extension Chrome? ).

var actualCode = `// Code here.
// If you want to use a variable, use $ and curly braces.
// For example, to use a fixed random number:
var someFixedRandomValue = ${ Math.random() };
// NOTE: Do not insert unsafe variables in this way, see below
// at "Dynamic values in the injected code"
`;

var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

Remarque: les littéraux de modèle ne sont pris en charge que dans Chrome 41 et versions ultérieures. Si vous souhaitez que l'extension fonctionne dans Chrome 40-, utilisez:

var actualCode = ['/* Code here. Example: */' + 'alert(0);',
                  '// Beware! This array have to be joined',
                  '// using a newline. Otherwise, missing semicolons',
                  '// or single-line comments (//) will mess up your',
                  '// code ----->'].join('\n');

Méthode 2b: utilisation d'une fonction

Pour un gros morceau de code, il n'est pas possible de citer la chaîne. Au lieu d'utiliser un tableau, une fonction peut être utilisée et stringifiée:

var actualCode = '(' + function() {
    // All code is executed in a local scope.
    // For example, the following does NOT overwrite the global `alert` method
    var alert = null;
    // To overwrite a global variable, prefix `window`:
    window.alert = null;
} + ')();';
var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

Cette méthode fonctionne, car l' +opérateur sur les chaînes et une fonction convertit tous les objets en chaîne. Si vous avez l'intention d'utiliser le code plus d'une fois, il est sage de créer une fonction pour éviter la répétition du code. Une implémentation pourrait ressembler à:

function injectScript(func) {
    var actualCode = '(' + func + ')();'
    ...
}
injectScript(function() {
   alert("Injected script");
});

Remarque: Étant donné que la fonction est sérialisée, la portée d'origine et toutes les propriétés liées sont perdues!

var scriptToInject = function() {
    console.log(typeof scriptToInject);
};
injectScript(scriptToInject);
// Console output:  "undefined"

Méthode 3: utiliser un événement en ligne

Parfois, vous voulez exécuter du code immédiatement, par exemple pour exécuter du code avant la création de l' <head>élément. Cela peut être fait en insérant une <script>balise avec textContent(voir méthode 2 / 2b).

Une alternative, mais non recommandée, consiste à utiliser des événements en ligne. Il n'est pas recommandé car si la page définit une stratégie de sécurité du contenu qui interdit les scripts en ligne, les écouteurs d'événements en ligne sont bloqués. Les scripts en ligne injectés par l'extension, en revanche, continuent de s'exécuter. Si vous souhaitez toujours utiliser des événements en ligne, voici comment:

var actualCode = '// Some code example \n' + 
                 'console.log(document.documentElement.outerHTML);';

document.documentElement.setAttribute('onreset', actualCode);
document.documentElement.dispatchEvent(new CustomEvent('reset'));
document.documentElement.removeAttribute('onreset');

Remarque: Cette méthode suppose qu'aucun autre écouteur d'événement global ne gère l' resetévénement. Si tel est le cas, vous pouvez également choisir l'un des autres événements mondiaux. Ouvrez simplement la console JavaScript (F12), saisissez document.documentElement.onet sélectionnez les événements disponibles.

Valeurs dynamiques dans le code injecté

Parfois, vous devez passer une variable arbitraire à la fonction injectée. Par exemple:

var GREETING = "Hi, I'm ";
var NAME = "Rob";
var scriptToInject = function() {
    alert(GREETING + NAME);
};

Pour injecter ce code, vous devez passer les variables comme arguments à la fonction anonyme. Assurez-vous de l'implémenter correctement! Les éléments suivants ne fonctionneront pas :

var scriptToInject = function (GREETING, NAME) { ... };
var actualCode = '(' + scriptToInject + ')(' + GREETING + ',' + NAME + ')';
// The previous will work for numbers and booleans, but not strings.
// To see why, have a look at the resulting string:
var actualCode = "(function(GREETING, NAME) {...})(Hi, I'm ,Rob)";
//                                                 ^^^^^^^^ ^^^ No string literals!

La solution est d'utiliser JSON.stringifyavant de passer l'argument. Exemple:

var actualCode = '(' + function(greeting, name) { ...
} + ')(' + JSON.stringify(GREETING) + ',' + JSON.stringify(NAME) + ')';

Si vous avez de nombreuses variables, il vaut la peine de les utiliser JSON.stringifyune fois, pour améliorer la lisibilité, comme suit:

...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';
Rob W
la source
83
Cette réponse devrait faire partie des documents officiels. Les documents officiels doivent être expédiés de la manière recommandée -> 3 façons de faire la même chose ... Faux?
Mars Robertson
7
@ Qantas94Heavy Le CSP de l'extension n'affecte pas les scripts de contenu. Seul le CSP de la page est pertinent. La méthode 1 peut être bloquée en utilisant une script-srcdirective qui exclut l'origine de l'extension, la méthode 2 peut être bloquée en utilisant un CSP qui exclut "unsafe-inline" `.
Rob W
3
Quelqu'un m'a demandé pourquoi je supprimais la balise de script à l'aide de script.parentNode.removeChild(script);. Je le fais parce que j'aime nettoyer mon bordel. Lorsqu'un script en ligne est inséré dans le document, il est immédiatement exécuté et la <script>balise peut être supprimée en toute sécurité.
Rob W
9
Autre méthode: utilisez location.href = "javascript: alert('yeah')";n'importe où dans votre script de contenu. Il est plus facile pour les courts extraits de code et peut également accéder aux objets JS de la page.
Métoule
3
@ChrisP Soyez prudent avec l'utilisation javascript:. Le code s'étendant sur plusieurs lignes peut ne pas fonctionner comme prévu. Une ligne commentaire ( //) tronque le reste, donc ce échouera: location.href = 'javascript:// Do something <newline> alert(0);';. Cela peut être contourné en vous assurant que vous utilisez des commentaires sur plusieurs lignes. Une autre chose à laquelle il faut faire attention est que le résultat de l'expression soit nul. javascript:window.x = 'some variable';entraînera le déchargement du document et sera remplacé par l'expression «une variable». S'il est utilisé correctement, c'est en effet une alternative intéressante à <script>.
Rob W
61

La seule chose manquant cachée à l'excellente réponse de Rob W est de savoir comment communiquer entre le script de page injecté et le script de contenu.

Côté réception (votre script de contenu ou le script de page injecté), ajoutez un écouteur d'événements:

document.addEventListener('yourCustomEvent', function (e) {
  var data = e.detail;
  console.log('received', data);
});

Du côté initiateur (script de contenu ou script de page injecté) envoyez l'événement:

var data = {
  allowedTypes: 'those supported by structured cloning, see the list below',
  inShort: 'no DOM elements or classes/functions',
};

document.dispatchEvent(new CustomEvent('yourCustomEvent', { detail: data }));

Remarques:

  • La messagerie DOM utilise un algorithme de clonage structuré, qui ne peut transférer que certains types de données en plus des valeurs primitives. Il ne peut pas envoyer d'instances de classe, de fonctions ou d'éléments DOM.
  • Dans Firefox, pour envoyer un objet (c'est-à-dire pas une valeur primitive) du script de contenu au contexte de la page, vous devez le cloner explicitement dans la cible en utilisant cloneInto(une fonction intégrée), sinon il échouera avec une erreur de violation de sécurité .

    document.dispatchEvent(new CustomEvent('yourCustomEvent', {
      detail: cloneInto(data, document.defaultView),
    }));
laktak
la source
J'ai en fait lié au code et à l'explication à la deuxième ligne de ma réponse, à stackoverflow.com/questions/9602022/… .
Rob W
1
Avez-vous une référence pour votre méthode mise à jour (par exemple un rapport de bogue ou un cas de test?) Le CustomEventconstructeur remplace l' document.createEventAPI obsolète .
Rob W
Pour moi, 'dispatchEvent (new CustomEvent ...' a fonctionné. J'ai Chrome 33. De plus, cela n'a pas fonctionné auparavant parce que j'ai écrit addEventListener après avoir injecté le code js.
jscripter
Faites très attention à ce que vous transmettez comme 2e paramètre au CustomEventconstructeur. J'ai connu 2 revers très déroutants: 1. simplement mettre des guillemets simples autour des «détails» a fait perplexe la valeur nulllorsqu'ils sont reçus par l'auditeur de mon script de contenu. 2. Plus important encore, pour une raison quelconque, je devais le faire, JSON.parse(JSON.stringify(myData))sinon cela deviendrait aussi null. Compte tenu de cela, il me semble que l'affirmation suivante du développeur Chromium - que l'algorithme du "clone structuré" est utilisé automatiquement - n'est pas vraie. bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk
Je pense que la manière officielle est d'utiliser window.postMessage: developer.chrome.com/extensions/…
Enrique
9

J'ai également rencontré le problème de la commande des scripts chargés, qui a été résolu par le chargement séquentiel des scripts. Le chargement est basé sur la réponse de Rob W .

function scriptFromFile(file) {
    var script = document.createElement("script");
    script.src = chrome.extension.getURL(file);
    return script;
}

function scriptFromSource(source) {
    var script = document.createElement("script");
    script.textContent = source;
    return script;
}

function inject(scripts) {
    if (scripts.length === 0)
        return;
    var otherScripts = scripts.slice(1);
    var script = scripts[0];
    var onload = function() {
        script.parentNode.removeChild(script);
        inject(otherScripts);
    };
    if (script.src != "") {
        script.onload = onload;
        document.head.appendChild(script);
    } else {
        document.head.appendChild(script);
        onload();
    }
}

L'exemple d'utilisation serait:

var formulaImageUrl = chrome.extension.getURL("formula.png");
var codeImageUrl = chrome.extension.getURL("code.png");

inject([
    scriptFromSource("var formulaImageUrl = '" + formulaImageUrl + "';"),
    scriptFromSource("var codeImageUrl = '" + codeImageUrl + "';"),
    scriptFromFile("EqEditor/eq_editor-lite-17.js"),
    scriptFromFile("EqEditor/eq_config.js"),
    scriptFromFile("highlight/highlight.pack.js"),
    scriptFromFile("injected.js")
]);

En fait, je suis un peu nouveau pour JS, alors n'hésitez pas à me cingler vers les meilleures façons.

Dmitry Ginzburg
la source
3
Cette façon d'insérer des scripts n'est pas agréable, car vous polluez l'espace de noms de la page Web. Si la page Web utilise une variable appelée formulaImageUrlou codeImageUrl, vous détruisez efficacement la fonctionnalité de la page. Si vous souhaitez transmettre une variable à la page Web, je suggère d'attacher les données à l'élément de script ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) et d'utiliser par exemple (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();dans le script pour accéder aux données.
Rob W
@RobW merci pour votre note, bien qu'il s'agisse davantage de l'échantillon. Pouvez-vous préciser pourquoi je devrais utiliser l'IFEF au lieu de simplement l'obtenir dataset?
Dmitry Ginzburg
4
document.currentScriptpointe uniquement sur la balise de script pendant son exécution. Si vous souhaitez accéder à la balise de script et / ou à ses attributs / propriétés (par exemple dataset), vous devez la stocker dans une variable. Nous avons besoin d'un IIFE pour obtenir une fermeture pour stocker cette variable sans polluer l'espace de noms global.
Rob W
@RobW excellent! Mais ne pouvons-nous pas simplement utiliser un nom de variable, qui ne croiserait guère avec l'existant. Est-ce simplement non idiomatique ou nous pouvons avoir d'autres problèmes avec cela?
Dmitry Ginzburg
2
Vous pourriez, mais le coût d'utilisation d'un IIFE est négligeable, donc je ne vois pas de raison de préférer la pollution de l'espace de noms à un IIFE. J'apprécie le fait que je ne briserai pas la page Web des autres d'une manière ou d'une autre et la possibilité d'utiliser des noms de variables courts. Un autre avantage de l'utilisation d'un IIFE est que vous pouvez quitter le script plus tôt si vous le souhaitez ( return;).
Rob W
6

dans le script de contenu, j'ajoute une balise de script à la tête qui lie un gestionnaire «onmessage», à l'intérieur du gestionnaire que j'utilise, eval pour exécuter le code. Dans le script de contenu de stand, j'utilise également le gestionnaire onmessage, donc je reçois une communication bidirectionnelle. Documents Google Chrome

//Content Script

var pmsgUrl = chrome.extension.getURL('pmListener.js');
$("head").first().append("<script src='"+pmsgUrl+"' type='text/javascript'></script>");


//Listening to messages from DOM
window.addEventListener("message", function(event) {
  console.log('CS :: message in from DOM', event);
  if(event.data.hasOwnProperty('cmdClient')) {
    var obj = JSON.parse(event.data.cmdClient);
    DoSomthingInContentScript(obj);
 }
});

pmListener.js est un écouteur d'URL de message

//pmListener.js

//Listen to messages from Content Script and Execute Them
window.addEventListener("message", function (msg) {
  console.log("im in REAL DOM");
  if (msg.data.cmnd) {
    eval(msg.data.cmnd);
  }
});

console.log("injected To Real Dom");

De cette façon, je peux avoir une communication bidirectionnelle entre CS et Real Dom. Son très utile par exemple si vous avez besoin d'écouter des événements de webscoket, ou à des variables ou des événements en mémoire.

doron aviguy
la source
1

Vous pouvez utiliser une fonction utilitaire que j'ai créée dans le but d'exécuter du code dans le contexte de la page et de récupérer la valeur renvoyée.

Cela se fait en sérialisant une fonction dans une chaîne et en l'injectant dans la page Web.

L'utilitaire est disponible ici sur GitHub .

Exemples d'utilisation -



// Some code that exists only in the page context -
window.someProperty = 'property';
function someFunction(name = 'test') {
    return new Promise(res => setTimeout(()=>res('resolved ' + name), 1200));
}
/////////////////

// Content script examples -

await runInPageContext(() => someProperty); // returns 'property'

await runInPageContext(() => someFunction()); // returns 'resolved test'

await runInPageContext(async (name) => someFunction(name), 'with name' ); // 'resolved with name'

await runInPageContext(async (...args) => someFunction(...args), 'with spread operator and rest parameters' ); // returns 'resolved with spread operator and rest parameters'

await runInPageContext({
    func: (name) => someFunction(name),
    args: ['with params object'],
    doc: document,
    timeout: 10000
} ); // returns 'resolved with params object'

Arik
la source
0

Si vous souhaitez injecter une fonction pure, au lieu de texte, vous pouvez utiliser cette méthode:

function inject(){
    document.body.style.backgroundColor = 'blue';
}

// this includes the function as text and the barentheses make it run itself.
var actualCode = "("+inject+")()"; 

document.documentElement.setAttribute('onreset', actualCode);
document.documentElement.dispatchEvent(new CustomEvent('reset'));
document.documentElement.removeAttribute('onreset');

Et vous pouvez passer des paramètres (malheureusement aucun objet et aucun tableau ne peut être stratifié) aux fonctions. Ajoutez-le dans les baréthèses, comme ceci:

function inject(color){
    document.body.style.backgroundColor = color;
}

// this includes the function as text and the barentheses make it run itself.
var color = 'yellow';
var actualCode = "("+inject+")("+color+")"; 

Tarmo Saluste
la source
C'est assez cool ... mais la deuxième version, avec une variable pour la couleur, ne fonctionne pas pour moi ... je suis "non reconnu" et le code génère une erreur ... ne le voit pas comme une variable.
11