Context
I have been tasked with fixing a big bug on the menu-edit page, which was caused by a stale element issue, caused by the HTML elements for it being rendered server-side. In my three-day fight against this bug, I got some inspiration from Angular and decided to try to make a menu state that will power everything on the page (adding/removing categories/items, and later, pagination of the modals for the adding)
Some Code
I came up with this IIFE (to be the "controller" of the MVC. Selector modals hit the add methods of this, and delete buttons hit the remove methods of this. Also, this gets passed to template-render function, which is literally the first thing hit when a modal gets popped):
/* all the categories, items, and modifiers that power this page */
var menuState = (function() {
let _categories = {
attached: [],
available: []
}, _items = {
attached: [],
available: []
}, _modifiers = {
attached: [],
available: []
}
function getExposedMethodsFor(obj) {
return {
all : function() { return obj.attached.concat(obj.available) },
attached : function() { return obj.attached },
available : function() { return obj.available }
// ... other methods that work on obj.attached,obj.available
}
}
let categoryExposedMethods = getExposedMethodsFor(_categories)
// other exposer objects
return {
getAllCategories : categoryExposedMethods.all,
getAttachedCategories : categoryExposedMethods.attached,
getAvailableCategories : categoryExposedMethods.available
// the rest of the exposed methods irrelevant to this question at hand
}
})()
OK, so what's the problem?
The problem is that this is false sense of security, it seems. When I try to XSS-test this structure alone, it fails.
I test it with three entities in _categories, all of which are attached, causing
menuState.getAllCategories().length
to return 3 and
menuState.getAvailableCategories().length
to return 0. Good news is that when I tried
menuState.getAllCategories().push('a')
menuState.getAllCategories().length
I still get three.
However, when I go
menuState.getAvailableCategories().push('b')
menuState.getAvailableCategories().length
I get 1, instead of 0 !!
Is there truly a way to lock down the other getters here?! If not, what are my alternatives?
I fixed it with Object.freeze, which I already used for refactoring the "enums" the dev before me wrote when he was working on this project. What it does is fully protect a state from any type of changes, including:
adding properties
deleting properties
modifying properties
re-assigning the object/array being "frozen"
How I use it
In the helper method, I did the following :
attached : function() { return Object.freeze(obj.attached) },
available : function() { return Object.freeze(obj.available) },
This prevents the arrays being changed from those methods, thus shutting down this type of XSS. Also, menuState was declared with const.
Related
What I am trying to do is to switch out an object's property (a string) with a matching (keyed) object from another object where the values are keyed.
So for example...
const dataRefs = {
'idkey1': { title: "Key1", /*...etc... */ },
'idkey2': { title: "Key2", /*...etc... */ },
'idkey3': { title: "Key3", /*...etc... */ },
// ...etc...
};
const pages = [
{ title: "A Page", data: 'idkey1' },
// ...etc...
];
Using the below code I want to switch out pages[n].data with the matching property in dataRefs. So using a forEach on the pages...
pages.forEach(page => page.data = dataRefs[page.data])
Doing this results in page.data property becoming undefined, even though it should match.
If I try to debug by outputting it to console, I get some unusual effect of seeing the undefined only when the code is added after the output....
// This works and does the match exactly as I want it.
pages.forEach(page => console.log("%s: ", page.data, dataRefs[page.data]));
// Output:
// idkey1: undefined
// This however results in bizzare behaviour and ends up with "undefined".
pages.forEach(page => {
// using console.log to see what's going on...
console.log("%s: ", page.data, dataRefs[page.data]);
page.data = dataRefs[page.data];
});
// Output:
// [Object object]: undefined
// Trying this alternative, just in case how the DOM inspector
// might be using references, but still the same issue...
pages.forEach(page => {
console.log(page.data + ": ", dataRefs[page.data]);
page.data = dataRefs[page.data];
});
// Output:
// [Object object]: undefined
Have checked spelling of variables and gone over and over the code trying so many variants but it seems that no matter what I do, calling page.data = dataRefs[page.data] does not work. Would this be some sort of complex race-condition or have I been watching too much Matrix of late?
This is being called in the Component's render() method.
Using Safari 14.1.2, if that helps.
The issue was related with Next.JS. Best guess is that Next.JS was pre-rendering the data, storing it in some JSON cache file and passing that to the component render function....or something like that.
Using the browser's inspector, a breakpoint at the problematic line page.data = dataRefs[page.data] was only ever triggered once, and showed the data had already been manipulated by the function, before it had been called. Which is simply odd. Removing the line, the breakpoint would trigger and the data not converted.
This leads me to believe it to be some sort of NextJS pre-lifecycle thing, possibly as part of the SSG process.
To resolve the issue and move on, I used a check if (page.data instanceof Object) return; to stop it from running twice, which seemed to do the trick. Not ideal, but without a better reason, this will have to suffice. So the code ultimately went like....
pages.forEach(page => {
// skip the loop if the data has already been converted
// could have also used a string check, but both should work for now.
if (page.data instanceof Object) return;
// now change the data if it's still a string referrence
page.data = dataRefs[page.data]));
});
Again, appologies that I don't have the best answer, but this was the only way to resolve it, and since Javascript does not do this normally (have done this sort of thing so many times without issue), it will have to be chalked up to a NextJS/SSG (or some other processor) issue.
Would love to get any NextJS expert's knowledge on how this could happen.
I'm doing a Linked List data structure. The prototype includes a method to pop (delete) the last item from the list which I'm attempting to do by finding the last object, and then setting it to null. It does not seem to work. What does work is setting the reference (the 'pointer') in the previous object to null. I'm still a relative JS OOP newbie, can't get my brain to understand why. The code:
function LinkedList() {
this._rootNode = null;
this._length = 0;
}
LinkedList.prototype = {
push: function(data) {
var newNode = {
data: data,
nextNode: null
};
// initialize this._rootNode or subsequent .nextNode with newNode
this._length++;
},
pop: function() {
var selectedNode, perviousNode;
if ( this._rootNode ) {
if ( this._length > 1 ) {
selectedNode = this._rootNode;
while ( selectedNode.nextNode ) {
previousNode = selectedNode; // <-- shouldn't need this?
selectedNode = selectedNode.nextNode;
}
selectedNode = null; // <-- doesn't delete it
// previousNode.nextNode = null; // <-- works (but feels unnecessary?)
} else {
this._rootNode = null;
}
this._length--;
}
},
// more methods..
};
/* --- Main Prorgam --- */
var list = new LinkedList();
list.push('AAA');
list.push('BBB');
list.pop();
console.log(list._rootNode.nextNode.data); <-- 'BBB' still there
Would appreciate some insight, and any other tips on improving the function. Thanks!
I guess you realize that your push method doesn't work, but you haven't asked about that one.
If you are doing some kind of school project that requires you to write a linked list like this, then by all means, continue. Your issue is that selectedNode is not really "the node itself", it's a reference to it, and you're just setting that reference to null while the previous item's nextNode pointer still refers to it, so you haven't actually removed it from your list. You would actually do so by un-commenting the line setting that pointer to null, which means you also have to leave in the line saving the reference to the previous node.
previousNode.nextNode = null;
You actually don't want to delete the node entirely with pop(), you want to return it. Once you remove the reference to the popped node in your calling function though, it will be the last reference and the object will be made available for garbage collection. This is (to my knowledge) how all traditional OOP languages handle linked lists at the basic level.
Which brings me to my next point, that most OOP languages you'll use these days don't actually require you to work on the basic level. Most of them have libraries that will implement linked lists for you and Javascript in particular essentially implements a linked list-style data structure in its array syntax. To the point where ([1,2,3,4]).pop() evaluates to 4 and ([1,2,3,4]).push(5) evaluates to [1,2,3,4,5]. If you actually need to USE a linked list in a real project, just don't.
I'm doing something pretty standard, I think.
Model:
app.model.Todo = Backbone.Model.extend({
defaults: {
task: ''
, completed: 0
, attachments: []
, note: ''
}
});
Collection:
var Todos = Backbone.Collection.extend({
model: app.model.Todo
, localStorage: new Store('Todos')
, incomplete: function () {
return this.filter(function (todo) {
return !todo.get('completed')
});
}
, complete: function () {
return this.filter(function (todo) {
return todo.get('completed')
});
}
, comparator: function(todo) {
return todo.get('order');
}
});
app.collection.Todos = new Todos();
Then, if I just do:
app.collection.Todos.create({task: 'hi'});
app.collection.Todos.create({task: 'hi'});
The 2nd one never works. I get an infinite loop (too much recursion on Firefox and stack_overflow on Chrome).
I'm really at a loss. I commented out all events as well.
Appears it spins out of control here in backbone:
// Return a copy of the model's `attributes` object.
toJSON: function(options) {
return _.clone(this.attributes);
},
UPDATE: If I add id: 0 or whatever id to the model the error stops, but if I give it a custom ID (i.e. new Date().getTime() the error happens again. It's like whenever I create a unique item it blows up.
UPDATE 2:
var todo = new gator.model.Todo({task: actionbarVal});
gator.collection.Todos.add(todo);
gator.collection.Todos.sync('create', todo);
Doing the above kinda works, and for what I need it for it works, but it's really bad. It's bad because every single time we do a new add and sync it calls toJSON 1 time for every time add and sync has been called on this page load. So, if you add 3 items, you get 6 toJSON calls (1 for the first, 2 for the second, 3 for the third). Also, it's not as clean. I also noticed in the toJSON call in backbone this.attributes with create was correct the first time. The 2nd time it was like this.attributes == backbone or something. Very, very strange. It had all the methods of Backbone. It was as if clone did a deep clone or something.
You have a mismatch between your version of Backbone (v0.9.9) and the version of the localstorage add-on. Be sure to get the latest version of the localstorage add-on from the Backbone repo and it will fix this problem.
I eventually fixed it by reverting back to 0.9.2 of Backbone, thanks to Derick Bailey. My attempts of using the latest localStorage add-on didn't seem to fix it. Maybe I was using a different source? I was using develop of this:
https://github.com/jeromegn/Backbone.localStorage
I've been coding JS for a while, but I've never did anything object oriented. I usually just defined all my variables at the top, and then just used them all. I kept hearing over and over to use OO, but now I can't do what I want and I can't get any help.
Here is a fiddle, along with semi identical code:
http://jsfiddle.net/zDeAJ/1/
var App = {
options: {
/* ------------------------------------
Options (PREFERABLY DONT CHANGE)
--------------------------------------- */
baseDomain : 'google.com',
apiVersion : '/api/v1'
},
state: {
current: App.options.baseDomain + App.options.apiVersion
}
}
So doing App.options.baseDomain (or this.options.baseDomain) won't work for me. What's the usefulness of defining Application level variables if I can't define other application level values based on them? I know this is a vague question but I really don't know what I'm asking... I just have a problem in that what I was easily able to accomplish with just a bunch of variables that held not only settings, but state within my application, is not so easy with my knowledge of Javascript OO patterns....
Edit: Alright, this is specifically what I want to do:
http://i.imgur.com/ak5YD.png
But I wasn't aware of the limitations... so I need a way around it, which sticks as close and elegant as possible to this implementation.
You can think of your approach as creating an "Instance" object called App.
Here's a slightly different approach.
function App () {
// Save a reference to the object
var that = this;
that.options = {
baseDomain: "google.com",
apiVersion: "/api/v1"
};
that.state = {
current: that.options.baseDomain + that.options.apiVersion
};
}
var myApp = new App();
// Write the current state to the screen
document.write(myApp.state.current);
Here's the JSFiddle: http://jsfiddle.net/zDeAJ/1/
Hope this helps!
Your question is quite generic; you should be more specific about what you're trying to accomplish.
That aside, read the documentation on objects;
Taken from w3schools:
With JavaScript you can define and create your own objects.
There are 2 different ways to create a new object:
1. Define and create a direct instance of an object.
2. Use a function to define an object, then create new object instances.
Way 1:
personObj=new Object();
personObj.firstname="John";
personObj.lastname="Doe";
personObj.age=50;
personObj.eyecolor="blue";
Way 2:
function person(firstname,lastname,age,eyecolor)
{
this.firstname=firstname;
this.lastname=lastname;
this.age=age;
this.eyecolor=eyecolor;
this.changeName=changeName;
function changeName(name) {
this.lastname=name;
}
}
Not one of you answered my question.... I thought about it a little... I could use a named function inside of the literal to access it... and if I wanted (not necessary) I could even assign it back to the options object
http://jsfiddle.net/zDeAJ/9/
var App = {
options: {
/* ------------------------------------
Options (PREFERABLY DONT CHANGE)
--------------------------------------- */
baseDomain : 'google.com',
apiVersion : '/api/v1',
blah: ''
},
state: function(){
this.options.blah = this.options.baseDomain + this.options.apiVersion;
}
}
App.state();
console.log(App.options.blah);
JavaScript is an interpreted language. That means your code is evaluated from the inside out or the most inner expression is evaluated and passed to the next outer expression.
In your example the value of options get's evaluated first and next the value of state. The problem is that you can't access the associative array of App before it is fully evaluated, wich is not the case during the evaluation of the value of state.
EDITED
Sorry for not answering correctly. Here is a refined approach from your second:
var App = {
options: {
/* ------------------------------------
Options (PREFERABLY DONT CHANGE)
--------------------------------------- */
baseDomain : 'google.com',
apiVersion : '/api/v1',
blah: ''
},
blah: function(){
return App.options.baseDomain + App.options.apiVersion;
}
}
console.log(App.blah());
You could do the following (in JavaScript, functions are objects):
function App () {
// Save a reference to the object
this.options = {
baseDomain: "google.com",
apiVersion: "/api/v1"
};
this.state = {
current: this.options.baseDomain + this.options.apiVersion
};
}
var myApp = new App();
console.log(myApp);
What I've got is an ASP.NET MasterPage/ContentPage, where the ContentPage utilizes an UpdatePanel with UpdateMode set to "Conditional". This ContentPage is basically a MultiView that has 3 views: Setup, Confirm, and Complete. I've got navigation buttons that when clicked, go to the server, do what they need to do and finally update the UpdatePanel so it can come back. My problem lies in my JavaScript.
I have the following Global object literal named PAGE:
PAGE = {
panel : undefined,
currentView: undefined,
buttons : {
all : undefined,
cont : undefined
},
views : {
setup : {},
confirm : {},
complete : {}
}
};
My PAGE.init() function looks like this:
PAGE.init = function() { console.log("PAGE.init() fired");
this.panel = $('div[id$="Panel_Page"]');
this.currentView = this.panel.find('input[id$="Hidden_CurrentView"]').val();
this.buttons.all = this.panel.find('input[type="submit"]');
this.buttons.cont = this.panel.find('input[id$="Button_Continue"]');
this.buttons.all.click(function() { PAGE.panel.hide(); });
switch (this.currentView) {
case "confirm" : this.views.confirm.init(); break;
case "complete" : this.views.complete.init(); break;
default : this.views.setup.init(); break;
}
};
And last, but not least, it all gets kicked off by:
// Fire events on initial page load.
PAGE.init();
// Fire events for partial postbacks.
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(PAGE.init);
My issue is that when it first fires Page.init() everything is great, however, when you click a button it immediately throws an error of: Uncaught TypeError: Cannot set property 'all' of undefined. Now I've tried to figure this out, but I'm at a loss. It seems as though this happens to ANY nested object literal off of the root of PAGE. The immediate properties like PAGE.panel work just fine, but as soon as you need access to PAGE.buttons.all or PAGE.views.setup, it throws this error. I have never seen this before.
Any ideas out there?
Use a getter for your panel property. Seeing as everything else appears to be a property of panel this should do it - you might be losing the reference to panel during the postback, so instead of getting the object once, get it every time:
get_panel: function() { return $get("Panel_Page"); }
Again, if I just have this.panel = $get(myElement) and then my partial page update destroys the node that was returned to this.panel when I created my object initially, this.panel will become undefined. Use a getter.
Hope that helps - happy coding.
EDIT:
Actually - now that I look at it again, you'll probably want to use getters for all of your properties instead of relying on get_panel().find(..) for currentView, for example, I'd do another getter there:
get_currentView: function() { return $get("Hidden_CurrentView", this.get_panel()); }
B
Instead of using this inside of the PAGE.init() function, I used PAGE and that corrected the issue. Apparently this was not referring to PAGE but rather PAGE.init. Still not sure why, as I do this elsewhere and it works.
For instance, the following use of this works and it's no different than how I use it in my OP:
PAGE.views.setup = {
buttons : {
all : undefined,
cont : undefined
},
init: function() {
var self = this,
buttons = self.buttons;
buttons.all = PAGE.panel.find('input[type="submit"]');
buttons.all.click(function() { alert(this.id + " clicked"); });
}
}