Salut à tous,
J’ai quelques nouvelles informations pour vous et j’ai trouvé une solution à ce problème.
Aujourd’hui, j’ai creusé à nouveau ce bug et je pense avoir trouvé plus d’informations.
En examinant le fichier cmp2ui-fr.js de Quantcast, j’ai pu identifier où le bug se produit, c’est dans cette fonction (nous n’avons que la version minifiée) :
function(t){for(var n in t){t[n].status=e;}}
Comme vous pouvez le voir, cette fonction utilise une boucle for..in, et la variable t est un tableau. Nous l’avons déjà expliqué : Ember.js étend le tableau natif de JavaScript. Il semble que l’une des modifications soit l’ajout d’une entrée _super.
L’entrée _super pointe vers une fonction ROOT(), qui semble faire référence à _utils.ROOT dans Ember.js (cela peut sembler familier à certains d’entre vous, mais pas à moi ^^). Cette fonction ROOT() n’est pas extensible.
Apparemment, cette propriété _super est considérée comme énumérable, ce qui signifie que lors de l’utilisation d’une boucle for..in sur un tableau, l’entrée _super est traitée comme une entrée « normale » (contrairement, par exemple, à values, bind, valueOf, c’est-à-dire pratiquement toutes les fonctions d’un objet tableau).
Je pense que c’est un bug d’Ember.js et non un comportement souhaitable.
J’ai donc pu rendre le bug très reproductible. Pour cela, créez simplement un tableau d’objets comme ceci :
var objs = [{'key':'val'},{'key':'val'}];
Ensuite, créez une fonction en mode strict, qui itère sur un tableau et définit une nouvelle propriété pour chaque entrée :
var tst_func = function (objs){'use strict';for(var i in objs){objs[i].newproperty = true }};
Enfin, appelez simplement cette fonction sur le tableau d’objets :
tst_func(objs);
Vous devriez obtenir une erreur : Uncaught TypeError: can't define property "newproperty": Function is not extensible.
En y réfléchissant, je pense que le bug est très probablement déjà présent dans la nature et n’est pas spécifiquement lié à Quantcast. Fondamentalement, quiconque essaie d’utiliser for..in s’expose à un comportement incohérent ou à un bug critique.
Pour moi, ce n’est vraiment pas à Discourse ou à Quantcast de résoudre cela, mais clairement à Ember.js. Cependant, cela ne change pas le fait que nous devons trouver une solution tant que le bug n’est pas corrigé dans Ember.js ^^.
La bonne nouvelle, c’est que je pense avoir trouvé un moyen de le corriger.
Une méthode consiste à ajouter une ligne if (!objs.hasOwnProperty(i)) {continue}; dans chaque boucle for..in (probablement aussi for each, for of, etc.). Cela ne modifie pas le comportement étrange de _super dans les tableaux, mais empêche localement d’y accéder. Cela signifie évidemment que cela ne fonctionne pas pour tout script externe que nous ne contrôlons pas, comme mon cas d’utilisation particulier avec Quantcast.
Une autre méthode, que je pense être la voie à suivre, consiste à modifier le prototype du tableau en JavaScript (nous remplaçons donc en quelque sorte la propre surcharge d’Ember.js ^^) pour rendre _super non énumérable. Pour cela, nous devons exécuter cette ligne JS APRÈS que Ember.js ait été chargé et évalué :
//Rendre _super non énumérable pour éviter le bug entre Ember.js et for..in
Object.defineProperty(Array.prototype, '_super', {'enumerable': false});
La deuxième méthode permet toujours l’utilisation directe de _super, comme prévu, tout en empêchant son apparition dans les boucles. Cependant, je ne peux pas garantir que ce comportement étrange n’est pas utilisé par une fonction interne d’Ember.js ou un plugin externe.
En examinant le fichier _ember_jquery-189e46ebcb33594b835e782fd1ce916ec750bc0cf980ebc4fb7796649161a18d.js, j’ai remarqué certaines lignes spécifiques à Discourse, comme :
var ALIASES = {
"ember-addons/ember-computed-decorators":
"discourse-common/utils/decorators",
"discourse/lib/raw-templates": "discourse-common/lib/raw-templates",
"preload-store": "discourse/lib/preload-store",
"fixtures/user_fixtures": "discourse/tests/fixtures/user-fixtures",
};
var ALIAS_PREPEND = {
fixtures: "discourse/tests/",
helpers: "discourse/tests/",
};
Peut-être pourrions-nous ajouter notre ligne Object.defineProperty(Array.prototype, '_super', {'enumerable': false}); ici ?
Pour l’instant, j’ai corrigé le bug en ajoutant les lignes suivantes :
//Rendre _super non énumérable pour éviter le bug entre Ember.js et for..in
Object.defineProperty(Array.prototype, ‘_super’, {‘enumerable’: false});
Avant d’appeler le script choice.js de Quantcast.
Je pense que quelqu’un pourrait corriger le bug localement en créant un fichier ‘fix_ember.js’ contenant ces deux lignes, en le servant statiquement via un proxy inverse (par exemple Nginx), puis en ajoutant une ligne <script src="/fix_emmber.js"></script> dans le pied de page du thème via la personnalisation. Écrire le script directement au lieu de créer un lien ne fonctionne pas à cause de l’extraction des scripts dans Discourse (voir Custom javascript in <head> disappear).
J’espère que ce sujet aidera d’autres personnes. J’ouvrirai un ticket auprès d’Ember.js demain pour vérifier s’il s’agit d’un bug ou d’un comportement très étrange mais délibéré.
Je vous tiendrai informés pour voir si nous devons inclure une correction dans Discourse.
PS : Un grand merci à Angus Croll de Extending JavaScript Natives – JavaScript, JavaScript…, son article m’a beaucoup aidé !