I can't figure out how Ember determines if it should update or create a record. I would assume its based on the ID or on the Store entry, but it seems to be something else. The code example clarifies:
// this returns the user without making an api call
currentUser.get('store').find('user_detail', '49')
// this returns 49
currentUser.get('id')
// this returns true
currentUser.get('store').hasRecordForId('user_detail', 49)
// this issues a create to api/userDetails instead
// of updating /api/userDetails/49
currentUser.save()
// maybe this is a lead, not the 48 at the end
currentUser.toString()
// <EmberApp.UserDetail:ember461:48>
// it looks as though currentState is involved here
// http://emberjs.com/api/data/classes/DS.RootState.html
currentUser.currentState
// returns root.loaded.created.uncommitted
currentUser.get('currentState.stateName');
// also isNew is wrong and returns true
currentUser.get('isNew');
Let me explain why I have this issue. My app has a current user. If you logout I update the current user. So I set Ember.currentUser.setProperties(newUserData). I update the currentUser object so that ember automatically triggers updates throughout my app. If I would replace the currentUser Ember.currentUser = newUser; Nothing would update. If I cant solve the above problem an alternative solution for the swapping of the user object would also work.
This is how I handle the global user state
container.register('user:current', Ember.currentUser);
// and handle updates via Ember.currentUser.setProperties()
application.inject('controller', 'user', 'user:current');
application.inject('route', 'user', 'user:current');
A proper solution would replace Ember.currentUser, however doing that doesnt trigger updates.
A new model will have the isNew and isDirty properties set to true, an existing record that needs to be updated will only have isDirty set to true.
I'd recommend pushing your user one level deeper and not storing it on the Ember namespace, that way you can set it from anywhere else, yet still inject it during injection
var users = Em.Object.create({
current: currentUser
});
container.register('users:current', users, {instantiate: false});
// and handle updates via Ember.currentUser.setProperties()
application.inject('controller', 'users', 'users:current');
application.inject('route', 'users', 'users:current');
Then from any controller you can access/watch it on users.current, yet you can also set it using this.users.set('current', newUser) which would effect anyone watching that property on any controller or route.
Example: http://emberjs.jsbin.com/OxIDiVU/1145/edit
Additionally a lot of things you are doing are async calls and should use the promise pattern for viewing properties etc.
Related
I am trying to understand more in depth the difference between filter and item access control.
Basically I understand that Item access control is, sort of, higher order check and will run before the GraphQL filter.
My question is, if I am doing a filter on a specific field while updating, for instance a groupID or something like this, do I need to do the same check in Item Access Control?
This will cause an extra database query that will be part of the filter.
Any thoughts on that?
The TL;DR answer...
if I am doing a filter on a specific field [..] do I need to do the same check in Item Access Control?
No, you only need to apply the restriction in one place or the other.
Generally speaking, if you can describe the restriction using filter access control (ie. as a graphQL-style filter, with the args provided) then that's the best place to do it. But, if your access control needs to behave differently based on values in the current item or the specific changes being made, item access control may be required.
Background
Access control in Keystone can be a little hard to get your head around but it's actually very powerful and the design has good reasons behind it. Let me attempt to clarify:
Filter access control is applied by adding conditions to the queries run against the database.
Imagine a content system with lists for users and posts. Users can author a post but some posts are also editable by everyone. The Post list config might have something like this:
// ..
access: {
filter: {
update: () => ({ isEditable: { equals: true } }),
}
},
// ..
What that's effectively doing is adding a condition to all update queries run for this list. So if you update a post like this:
mutation {
updatePost(where: { id: "123"}, data: { title: "Best Pizza" }) {
id name
}
}
The SQL that runs might look like this:
update "Post"
set title = 'Best Pizza'
where id = 234 and "isEditable" = true;
Note the isEditable condition that's automatically added by the update filter. This is pretty powerful in some ways but also has its limits – filter access control functions can only return GraphQL-style filters which prevents them from operating on things like virtual fields, which can't be filtered on (as they don't exist in the database). They also can't apply different filters depending on the item's current values or the specific updates being performed.
Filter access control functions can access the current session, so can do things like this:
filter: {
// If the current user is an admin don't apply the usual filter for editability
update: (session) => {
return session.isAdmin ? {} : { isEditable: { equals: true } };
},
}
But you couldn't do something like this, referencing the current item data:
filter: {
// ⚠️ this is broken; filter access control functions don't receive the current item ⚠️
// The current user can update any post they authored, regardless of the isEditable flag
update: (session, item) => {
return item.author === session.itemId ? {} : { isEditable: { equals: true } };
},
}
The benefit of filter access control is it doesn't force Keystone to read an item before an operation occurs; the filter is effectively added to the operation itself. This can makes them more efficient for the DB but does limit them somewhat. Note that things like hooks may also cause an item to be read before an operation is performed so this performance difference isn't always evident.
Item access control is applied in the application layer, by evaluating the JS function supplied against the existing item and/or the new data supplied.
This makes them a lot more powerful in some respects. You can, for example, implement the previous use case, where authors are allowed to update their own posts, like this:
item: {
// The current user can update any post they authored, regardless of the isEditable flag
update: (session, item) => {
return item.author === session.itemId || item.isEditable;
},
}
Or add further restrictions based on the specific updates being made, by referencing the inputData argument.
So item access control is arguably more powerful but they can have significant performance implications – not so much for mutations which are likely to be performed in small quantities, but definitely for read operations. In fact, Keystone won't let you define item access control for read operations. If you stop and think about this, you might see why – doing so would require reading all items in the list out of the DB and running the access control function against each one, every time a list was read. As such, the items accessible can only be restricted using filter access control.
Tip: If you think you need item access control for reads, consider putting the relevant business logic in a resolveInput hook that flattens stores the relevant values as fields, then referencing those fields using filter access control.
Hope that helps
I have a many to many relationship table with payload (additional field) coming from .Net WebAPI that I have modelled in ember-data. When I add a record into this table/relationship ember is creating an additional record that is held in memory until the user performs a browser page refresh. My models are:
// student.js
export default DS.Model.extend({
name: DS.attr('string'),
studentsClasses: DS.hasMany('student-class')
})
// class.js
export default DS.Model.extend({
desc: DS.attr('string'),
studentsClasses: DS.hasMany('student-class')
})
// student-class
export default DS.Model.extend({
studentId: DS.attr('string'),
student: DS.belongsTo('student'),
class: DS.belongsTo('class'),
grade: DS.attr('number') // payload
})
Here is the code I use to create and add the many to many record.
let newRecord = this.get('store').createRecord('student-class');
newRecord.studentId = 1;
newRecord.grade = 3;
class.get('studentsClasses').pushObject(newRecord);
The new record gets created and added and everything looks good on screen, until I come back to the same page and there is an extra record in the class.studentClasses array.
Any idea why ember-data is creating an extra record in memory and how I can stop it doing it please?
Thanks
As you said, ember-data keeps records in memory. And you must keep in mind that ember-data will not remove those records by it own. It can only be removed from memory by yourself, page refresh or replaced by new payload if has same id property. You can observe that behavior by using ember debug plugin for browsers like chrome and firefox.
In your case, you've created a new record by store.createRecord(). In this moment, it had added this record to your memory already, and it was pushed to your class record. If you didn't save these models successfully, it will keep in a status called 'dirty', and if you never clean your store memory (using something like store.unloadRecord() which has some side effects, or remove this unsaved new record from your related model), the next time you use store.findRecord() to find a record, useless you force record to be reloaded like store.findRecord('class', 1, {reload: true}), it will use the existing data in your memory as first priority.
So my suggestion for this is to force reload this class model when entering this class page.
This seems like a simple thing and should be part of the base code for Yii, but I can't find a solution anywhere. Here is my scenario.
1) User updates their record (use beforesave to set a cache value, changes with each new save, php unique())
public function beforeSave()
{
Yii::app()->cache->set('userupdate'.$this->id,uniqid());
return parent::beforeSave();
}
2) User data is cached using the cache value in step one as a dependency in the loadModel function of the model.
$model=Users::model()->cache(1800, $dependency)->findByPk($id);
3) User views a page that calls to retrieve their data. Yii evaluates the request to see if the cached valued from step 1 has changed, if it has not pull from cache, if it has pull from db.
While reading this page (http://www.yiiframework.com/doc/guide/1.1/en/caching.data) it has that function if a file date changes, but not one for it a variable changes. Any help in this matter would be great as I am at a loss of how to implement this.
NOTE: I need to use cache to hold the variable as I'm running multiple instances of my application and they need shared over each server and all users (thus session won't work).
After fighting with this I found the solution, don't feel it's completely pretty, but it does work. Any feedback on a cleaner way is much appreciated.
$cache = Yii::app()->cache;
$key1 = 'userupdate'.$id; //main cache value
$key2 = '2userupdate'.$id; //will equal main cache when query is cached
$cache1 = $cache['userupdate'.$id];
$cache2 = $cache['2userupdate'.$id];
$dependency = new CExpressionDependency("Yii::app()->cache->get('$key1') == Yii::app()->cache->get('$key2')");
$model=Users::model()->cache(1800,$dependency)->findByPk($id);
if($cache1 != $cache2)
$cache['2userupdate'.$id] = $cache['userupdate'.$id];
One of the dependency options is CExpressionDependency. You could compare the currently cached beforeSave value to the value you get from the loadModel call.
I have added a column in my wikidatabase in the user table called approved_account.
The standard value on that column is 0 (zero).
I would like to add an exception when a user tries to log in to the wiki, such that
if approved_account = 0 then the login attempt is denied.
Does anyone know how and where I should place that if statement?
Edit: I've come this far.
I am using the AbortLogin hook, since I need to verify if my statement is true every time a user tries to log in.
However, my code won't let anyone in. It blocks all login attempts, even if I have the correct value in the approved_account field.
Can anyone help me fix this?
<?php
/**
* Prevent a user from accessing this file directly and provide a helpful
* message explaining how to install this extension.
*/
if ( !defined( 'MEDIAWIKI' ) ) {
echo <<<EOT
To install the Test extension, put the following line in your LocalSettings.php file:
require_once( "$IP/extensions/approvedaccount.php" );
EOT;
exit( 1 );
}
// Extension credits that will show up on Special:Version
$wgExtensionCredits['parserhook'][] = array(
'name' => 'Approved Account extension',
'description' => 'Prevent login',
'author' => 'Me',
'url' => 'http://www.mediawiki.org/wiki/Extension:approvedaccount'
);
$wgHooks['AbortLogin'][] = 'approvedaccount::onAbortLogin';
class approvedaccount
{
public static function onAbortLogin( $user, $password, &$retval ) {
global $wgOut, $wgUser;
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select(
'user', // $table
array( 'user_name', 'approved_account' ), // $vars (columns of the table)
'user_name = "'.$wgUser.'"', // $conds
__METHOD__, // $fname = 'Database::select',
array( 'ORDER BY' => 'user_name ASC' ) // $options = array()
);
$output = '';
foreach( $res as $row ) {
$output .= 'Användarnamn: ' . $row->user_name . ' , Approved Account: ' . $row->approved_account . ".";
}
if ($row->approved_account = "1"){
//$this->loadDefaults();
// return false;
header("Location: http://hbg-whirlpool.emea.stream.corp/index.php?title=Special:UserLogout&returnto=Main+Page");
exit(); // you need to exit after a Location header is sent
}
}
}
You could do this with a simple AuthPlugin, overriding the strictUserAuth() method to return true for users that match the condition.
However, I suspect you're approaching this problem the wrong way. Why not just define a new user group, say, approved, and then add the corresponding record to the user_groups table for approved users? You won't be able to prevent unapproved users from logging in, but you can prevent them from making edits by only granting the edit permission to the approved group, like this:
$wgGroupPermissions['*']['edit'] = false;
$wgGroupPermissions['user']['edit'] = false;
$wgGroupPermissions['approved']['edit'] = true;
(If you wanted, you could even revoke the read permission from unapproved users too, but please read the warnings about restricting read access in MediaWiki first.)
Edit: I see a couple of problems with your AbortLogin hook.
Doing a 301 redirect and an exit() in the middle of the hook is probably not a very good idea. Sure, it probably will abort the login, but that's not really how the hook is meant to be used. Rather, you should just have the hook function return false to indicate that the login should be aborted or true to proceed with the normal login checks.
In any case, you're doing the exit() when the approved_account column is 1, which is presumably exactly when you don't want to abort the login.
...or, rather, you're doing the exit() always, because you used the assignment operator = instead of the comparison operator == in the condition, causing it to be always true. (Don't worry, that's a common bug in PHP and other C-like languages. One way to avoid is to get in the habit of using "Yoda conditionals" like 1 == $row->approved_account, which will produce an error if you leave out one =, since you can't assign to 1.)
Also, concatenating a User object with a string probably won't produce anything meaningful; and, even if it did, there would be an SQL injection vulnerability there. And besides, the hook parameters already include a User object, so you should use that instead of the global $wgUser (which might be stale during login anyway).
I admit that some of this stuff is really poorly documented. Besides the AbortLogin docs, I'd suggest looking at the general MediaWiki hook documentation, as well as the actual way the hook is called from SpecialUserlogin.php. For the database access, I'd also point you to the database wrapper function docs; unfortunately, the method documentation pages are giving 404 errors right now, so you'd again need to look directly in the source for the documentation.
Anyway, I'd rewrite your hook like this:
public static function onAbortLogin( $user, $password, &$retval, &$msg ) {
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow(
'user',
'approved_account',
array( 'user_id' => $user->getID() ),
__METHOD__
);
if ( !$row || !$row->approved_account ) {
$retval = LoginForm::ABORTED; // actually the default, but let's be sure
$msg = 'login-abort-not-approved'; // optional: custom error message
return false;
}
else {
// account is approved, return true to proceed with other login checks
return true;
}
}
If you want the custom message, you'll also need to create the page MediaWiki:login-abort-not-approved on your wiki. (If you wanted to turn this into a proper MediaWiki extension, you could provide a default message in an i18n file, but that's probably overkill here.)
Edit 2: Yes, you can add as many hooks as you want in an extension. (In fact, you don't even need an extension, it's perfectly fine to define simple site-specific hooks directly in LocalSettings.php if you want.) I think something like this could work for an AddNewAccount hook to log the user out, although I must note that I haven't actually tested this:
public static function onAddNewAccount( $user, $byEmail ) {
global $wgUser;
// try to log out the new user only if they're actually logged in
if ( $user->getName() == $wgUser->getName() ) $user->logout();
return true;
}
The if clause is there because the AddNewAccount is also called when a user creates a new account while logged in to a pre-existing account, in which case logging them out from their original account would be an unwelcome surprise. (Technically, just if ( $user == $wgUser ) ought to suffice, but explicitly comparing the usernames rather than the object references seems safer.)
Note that logging the new user out at that point kind of yanks the carpet out from under the new user creation code, so some unusual things may happen. For example, I suspect that the user creation log may actually end up saying something like "NewUserName created the new user account NewUserName", and the "Account successfully created" page may temporarily show the user as logged in, even though they're actually not.
It would be much cleaner to somehow avoid the auto-login behavior in the first place, but I don't see any obvious way to do that without patching SpecialUserlogin.php: the only check that determines whether the new user is automatically logged in is if ( $this->getUser()->isAnon() ), which only checks whether a user is already logged in. Even faking that somehow (which would be an ugly kluge in itself) doesn't really seem practical, as far as I can tell.
If you don't mind patching the MediaWiki core, though, just replacing that condition with if ( false ) (or if ( false && $this->getUser()->isAnon() ), if you want to keep it self-documenting) should do the trick. Note that you could still keep the AddNewAccount hook as a backup, in case you forget to reapply the patch after upgrading or something.
I'm developing a WCF Data Service with self tracking entities and I want to prevent clients from inserting duplicated content. Whenever they POST data without providing a value for the data key, I have to execute some logic to determine whether that data is already present inside my database or not. I've written a Change interceptor like this:
[ChangeInterceptor("MyEntity")]
public void OnChangeEntity(MyEntity item, UpdateOperations operations){
if (operations == UpdateOperations.Add)
{
// Here I search the database to see if a matching record exists.
// If a record is found, I'd like to use its ID and basically change an insertion
// into an update.
item.EntityID = existingEntityID;
item.MarkAsModified();
}
}
However, this is not working. The existingEntityID is ignored and, as a result, the record is always inserted, never updated. Is it even possible to do? Thanks in advance.
Hooray! I managed to do it.
item.EntityID = existingEntityID;
this.CurrentDataSource.ObjectStateManager.ChangeObjectState(item, EntityState.Modified);
I had to change the object state elsewhere, ie. by calling .ChangeObjectState of the ObjectStateManager, which is a property of the underlying EntityContext. I was mislead by the .MarkAsModified() method which, at this point, I'm not sure what it does.