2017-08-22 10:53:26 +03:00
import Controller from '@ember/controller' ;
2017-05-29 21:50:03 +03:00
import Ember from 'ember' ;
import boundOneWay from 'ghost-admin/utils/bound-one-way' ;
import isNumber from 'ghost-admin/utils/isNumber' ;
2018-03-19 20:56:09 +03:00
import validator from 'npm:validator' ;
2017-11-10 17:19:20 +03:00
import windowProxy from 'ghost-admin/utils/window-proxy' ;
2017-08-22 10:53:26 +03:00
import { alias , and , not , or , readOnly } from '@ember/object/computed' ;
import { computed } from '@ember/object' ;
import { htmlSafe } from '@ember/string' ;
import { isArray as isEmberArray } from '@ember/array' ;
import { run } from '@ember/runloop' ;
2017-10-30 12:38:01 +03:00
import { inject as service } from '@ember/service' ;
2016-08-11 09:58:38 +03:00
import { task , taskGroup } from 'ember-concurrency' ;
2017-01-04 18:52:00 +03:00
// ember-cli-shims doesn't export this
const { Handlebars } = Ember ;
2016-04-25 12:54:36 +03:00
export default Controller . extend ( {
2018-01-11 20:43:23 +03:00
ajax : service ( ) ,
config : service ( ) ,
dropdown : service ( ) ,
ghostPaths : service ( ) ,
notifications : service ( ) ,
session : service ( ) ,
slugGenerator : service ( ) ,
2017-10-31 18:27:25 +03:00
leaveSettingsTransition : null ,
dirtyAttributes : false ,
2015-11-18 13:50:48 +03:00
showDeleteUserModal : false ,
2017-03-08 21:21:35 +03:00
showSuspendUserModal : false ,
2015-11-18 13:50:48 +03:00
showTransferOwnerModal : false ,
showUploadCoverModal : false ,
showUplaodImageModal : false ,
2016-03-03 11:52:27 +03:00
_scratchFacebook : null ,
_scratchTwitter : null ,
2014-07-31 08:25:42 +04:00
2018-01-11 20:43:23 +03:00
saveHandlers : taskGroup ( ) . enqueue ( ) ,
2015-10-28 14:36:45 +03:00
user : alias ( 'model' ) ,
2015-11-18 13:50:48 +03:00
currentUser : alias ( 'session.user' ) ,
2014-07-01 19:58:26 +04:00
2018-01-11 01:57:43 +03:00
email : readOnly ( 'user.email' ) ,
slugValue : boundOneWay ( 'user.slug' ) ,
2014-07-02 07:44:39 +04:00
2015-10-28 14:36:45 +03:00
canAssignRoles : or ( 'currentUser.isAdmin' , 'currentUser.isOwner' ) ,
2017-09-04 22:17:04 +03:00
canChangeEmail : not ( 'isAdminUserOnOwnerProfile' ) ,
canChangePassword : not ( 'isAdminUserOnOwnerProfile' ) ,
2015-10-28 14:36:45 +03:00
canMakeOwner : and ( 'currentUser.isOwner' , 'isNotOwnProfile' , 'user.isAdmin' ) ,
2017-09-04 22:17:04 +03:00
isAdminUserOnOwnerProfile : and ( 'currentUser.isAdmin' , 'user.isOwner' ) ,
isNotOwnersProfile : not ( 'user.isOwner' ) ,
2015-10-28 14:36:45 +03:00
rolesDropdownIsVisible : and ( 'isNotOwnProfile' , 'canAssignRoles' , 'isNotOwnersProfile' ) ,
2015-11-18 13:50:48 +03:00
userActionsAreVisible : or ( 'deleteUserActionIsVisible' , 'canMakeOwner' ) ,
2018-01-11 20:43:23 +03:00
isNotOwnProfile : not ( 'isOwnProfile' ) ,
2017-03-03 16:24:43 +03:00
isOwnProfile : computed ( 'user.id' , 'currentUser.id' , function ( ) {
return this . get ( 'user.id' ) === this . get ( 'currentUser.id' ) ;
} ) ,
2015-05-26 05:10:50 +03:00
2015-10-28 14:36:45 +03:00
deleteUserActionIsVisible : computed ( 'currentUser' , 'canAssignRoles' , 'user' , function ( ) {
2016-11-14 16:16:51 +03:00
if ( ( this . get ( 'canAssignRoles' ) && this . get ( 'isNotOwnProfile' ) && ! this . get ( 'user.isOwner' ) )
|| ( this . get ( 'currentUser.isEditor' ) && ( this . get ( 'isNotOwnProfile' )
2018-02-07 12:42:46 +03:00
|| this . get ( 'user.isAuthorOrContributor' ) ) ) ) {
2015-06-13 17:34:09 +03:00
return true ;
}
2014-07-30 05:57:19 +04:00
} ) ,
2014-03-23 06:31:45 +04:00
2015-06-13 17:34:09 +03:00
// duplicated in gh-user-active -- find a better home and consolidate?
2015-10-28 14:36:45 +03:00
userDefault : computed ( 'ghostPaths' , function ( ) {
2018-03-19 18:31:35 +03:00
let defaultImage = '/img/user-image.png' ;
return ` ${ this . get ( 'ghostPaths.assetRoot' ) } ${ defaultImage } ` ;
2014-07-30 05:57:19 +04:00
} ) ,
2015-05-26 05:10:50 +03:00
2017-04-24 20:22:39 +03:00
userImageBackground : computed ( 'user.profileImage' , 'userDefault' , function ( ) {
let url = this . get ( 'user.profileImage' ) || this . get ( 'userDefault' ) ;
2017-01-04 18:52:00 +03:00
let safeUrl = Handlebars . Utils . escapeExpression ( url ) ;
2015-05-26 05:10:50 +03:00
2017-01-04 18:52:00 +03:00
return htmlSafe ( ` background-image: url( ${ safeUrl } ) ` ) ;
2014-07-30 05:57:19 +04:00
} ) ,
2015-06-13 17:34:09 +03:00
// end duplicated
2014-07-16 21:12:45 +04:00
2015-10-28 14:36:45 +03:00
coverDefault : computed ( 'ghostPaths' , function ( ) {
2018-03-19 18:31:35 +03:00
let defaultCover = '/img/user-cover.png' ;
return ` ${ this . get ( 'ghostPaths.assetRoot' ) } ${ defaultCover } ` ;
2014-07-30 05:57:19 +04:00
} ) ,
2014-07-02 07:44:39 +04:00
2017-04-24 20:22:39 +03:00
coverImageBackground : computed ( 'user.coverImage' , 'coverDefault' , function ( ) {
let url = this . get ( 'user.coverImage' ) || this . get ( 'coverDefault' ) ;
2017-01-04 18:52:00 +03:00
let safeUrl = Handlebars . Utils . escapeExpression ( url ) ;
2015-06-13 17:34:09 +03:00
2017-01-04 18:52:00 +03:00
return htmlSafe ( ` background-image: url( ${ safeUrl } ) ` ) ;
2015-06-13 17:34:09 +03:00
} ) ,
2014-07-16 21:12:45 +04:00
2015-10-28 14:36:45 +03:00
coverTitle : computed ( 'user.name' , function ( ) {
return ` ${ this . get ( 'user.name' ) } 's Cover Image ` ;
2014-07-30 05:57:19 +04:00
} ) ,
2014-07-31 08:25:42 +04:00
2015-10-28 14:36:45 +03:00
roles : computed ( function ( ) {
2015-09-03 14:06:50 +03:00
return this . store . query ( 'role' , { permissions : 'assign' } ) ;
2015-07-09 20:10:00 +03:00
} ) ,
2016-08-11 09:58:38 +03:00
actions : {
changeRole ( newRole ) {
2017-10-31 18:27:25 +03:00
this . get ( 'user' ) . set ( 'role' , newRole ) ;
this . set ( 'dirtyAttributes' , true ) ;
2014-03-23 06:31:45 +04:00
} ,
2015-11-18 13:50:48 +03:00
deleteUser ( ) {
return this . _deleteUser ( ) . then ( ( ) => {
this . _deleteUserSuccess ( ) ;
} , ( ) => {
this . _deleteUserFailure ( ) ;
} ) ;
} ,
toggleDeleteUserModal ( ) {
if ( this . get ( 'deleteUserActionIsVisible' ) ) {
this . toggleProperty ( 'showDeleteUserModal' ) ;
}
} ,
2017-03-08 21:21:35 +03:00
suspendUser ( ) {
2017-10-31 18:27:25 +03:00
this . get ( 'user' ) . set ( 'status' , 'inactive' ) ;
2017-03-08 21:21:35 +03:00
return this . get ( 'save' ) . perform ( ) ;
} ,
toggleSuspendUserModal ( ) {
if ( this . get ( 'deleteUserActionIsVisible' ) ) {
this . toggleProperty ( 'showSuspendUserModal' ) ;
}
} ,
unsuspendUser ( ) {
2017-10-31 18:27:25 +03:00
this . get ( 'user' ) . set ( 'status' , 'active' ) ;
2017-03-08 21:21:35 +03:00
return this . get ( 'save' ) . perform ( ) ;
} ,
toggleUnsuspendUserModal ( ) {
if ( this . get ( 'deleteUserActionIsVisible' ) ) {
this . toggleProperty ( 'showUnsuspendUserModal' ) ;
}
} ,
2016-03-03 11:52:27 +03:00
validateFacebookUrl ( ) {
let newUrl = this . get ( '_scratchFacebook' ) ;
let oldUrl = this . get ( 'user.facebook' ) ;
let errMessage = '' ;
2017-11-10 19:38:30 +03:00
// reset errors and validation
this . get ( 'user.errors' ) . remove ( 'facebook' ) ;
this . get ( 'user.hasValidated' ) . removeObject ( 'facebook' ) ;
2016-05-17 21:14:14 +03:00
if ( newUrl === '' ) {
2016-03-03 11:52:27 +03:00
// Clear out the Facebook url
this . set ( 'user.facebook' , '' ) ;
return ;
}
2016-05-17 21:14:14 +03:00
// _scratchFacebook will be null unless the user has input something
if ( ! newUrl ) {
newUrl = oldUrl ;
}
2017-11-10 19:38:30 +03:00
try {
// strip any facebook URLs out
newUrl = newUrl . replace ( /(https?:\/\/)?(www\.)?facebook\.com/i , '' ) ;
2016-03-03 11:52:27 +03:00
2017-11-10 19:38:30 +03:00
// don't allow any non-facebook urls
if ( newUrl . match ( /^(http|\/\/)/i ) ) {
throw 'invalid url' ;
2016-05-16 21:16:40 +03:00
}
2016-03-03 11:52:27 +03:00
2017-11-10 19:38:30 +03:00
// strip leading / if we have one then concat to full facebook URL
newUrl = newUrl . replace ( /^\// , '' ) ;
newUrl = ` https://www.facebook.com/ ${ newUrl } ` ;
2016-03-03 11:52:27 +03:00
2017-11-10 19:38:30 +03:00
// don't allow URL if it's not valid
if ( ! validator . isURL ( newUrl ) ) {
throw 'invalid url' ;
2016-03-03 11:52:27 +03:00
}
2016-05-16 21:16:40 +03:00
2017-10-31 18:27:25 +03:00
this . set ( 'user.facebook' , '' ) ;
run . schedule ( 'afterRender' , this , function ( ) {
this . set ( 'user.facebook' , newUrl ) ;
2016-05-16 21:16:40 +03:00
} ) ;
2017-11-10 19:38:30 +03:00
} catch ( e ) {
if ( e === 'invalid url' ) {
errMessage = 'The URL must be in a format like '
+ 'https://www.facebook.com/yourPage' ;
this . get ( 'user.errors' ) . add ( 'facebook' , errMessage ) ;
return ;
}
throw e ;
} finally {
2016-05-16 21:16:40 +03:00
this . get ( 'user.hasValidated' ) . pushObject ( 'facebook' ) ;
2016-03-03 11:52:27 +03:00
}
} ,
validateTwitterUrl ( ) {
let newUrl = this . get ( '_scratchTwitter' ) ;
let oldUrl = this . get ( 'user.twitter' ) ;
let errMessage = '' ;
2017-11-10 19:38:30 +03:00
// reset errors and validation
this . get ( 'user.errors' ) . remove ( 'twitter' ) ;
this . get ( 'user.hasValidated' ) . removeObject ( 'twitter' ) ;
2016-05-17 21:14:14 +03:00
if ( newUrl === '' ) {
2016-03-03 11:52:27 +03:00
// Clear out the Twitter url
this . set ( 'user.twitter' , '' ) ;
return ;
}
2016-05-17 21:14:14 +03:00
// _scratchTwitter will be null unless the user has input something
if ( ! newUrl ) {
newUrl = oldUrl ;
}
2017-09-11 10:56:11 +03:00
if ( newUrl . match ( /(?:twitter\.com\/)(\S+)/ ) || newUrl . match ( /([a-z\d.]+)/i ) ) {
2016-05-16 21:16:40 +03:00
let username = [ ] ;
2016-03-03 11:52:27 +03:00
2016-05-16 21:16:40 +03:00
if ( newUrl . match ( /(?:twitter\.com\/)(\S+)/ ) ) {
2016-11-14 16:16:51 +03:00
[ , username ] = newUrl . match ( /(?:twitter\.com\/)(\S+)/ ) ;
2016-05-16 21:16:40 +03:00
} else {
[ username ] = newUrl . match ( /([^/]+)\/?$/mi ) ;
}
2016-03-03 11:52:27 +03:00
2016-05-16 21:16:40 +03:00
// check if username starts with http or www and show error if so
2017-09-11 10:56:11 +03:00
if ( username . match ( /^(http|www)|(\/)/ ) || ! username . match ( /^[a-z\d._]{1,15}$/mi ) ) {
errMessage = ! username . match ( /^[a-z\d._]{1,15}$/mi ) ? 'Your Username is not a valid Twitter Username' : 'The URL must be in a format like https://twitter.com/yourUsername' ;
2016-03-03 11:52:27 +03:00
this . get ( 'user.errors' ) . add ( 'twitter' , errMessage ) ;
this . get ( 'user.hasValidated' ) . pushObject ( 'twitter' ) ;
return ;
}
2016-05-16 21:16:40 +03:00
newUrl = ` https://twitter.com/ ${ username } ` ;
this . get ( 'user.hasValidated' ) . pushObject ( 'twitter' ) ;
2017-10-31 18:27:25 +03:00
this . set ( 'user.twitter' , '' ) ;
run . schedule ( 'afterRender' , this , function ( ) {
this . set ( 'user.twitter' , newUrl ) ;
2016-05-16 21:16:40 +03:00
} ) ;
} else {
2016-11-14 16:16:51 +03:00
errMessage = 'The URL must be in a format like '
+ 'https://twitter.com/yourUsername' ;
2016-05-16 21:16:40 +03:00
this . get ( 'user.errors' ) . add ( 'twitter' , errMessage ) ;
this . get ( 'user.hasValidated' ) . pushObject ( 'twitter' ) ;
return ;
2016-03-03 11:52:27 +03:00
}
} ,
2015-11-18 13:50:48 +03:00
transferOwnership ( ) {
let user = this . get ( 'user' ) ;
let url = this . get ( 'ghostPaths.url' ) . api ( 'users' , 'owner' ) ;
this . get ( 'dropdown' ) . closeDropdowns ( ) ;
2016-01-18 18:37:14 +03:00
return this . get ( 'ajax' ) . put ( url , {
2015-11-18 13:50:48 +03:00
data : {
owner : [ {
id : user . get ( 'id' )
} ]
}
} ) . then ( ( response ) => {
// manually update the roles for the users that just changed roles
// because store.pushPayload is not working with embedded relations
2016-07-06 22:47:30 +03:00
if ( response && isEmberArray ( response . users ) ) {
2015-11-18 13:50:48 +03:00
response . users . forEach ( ( userJSON ) => {
let user = this . store . peekRecord ( 'user' , userJSON . id ) ;
let role = this . store . peekRecord ( 'role' , userJSON . roles [ 0 ] . id ) ;
user . set ( 'role' , role ) ;
} ) ;
}
this . get ( 'notifications' ) . showAlert ( ` Ownership successfully transferred to ${ user . get ( 'name' ) } ` , { type : 'success' , key : 'owner.transfer.success' } ) ;
} ) . catch ( ( error ) => {
this . get ( 'notifications' ) . showAPIError ( error , { key : 'owner.transfer' } ) ;
} ) ;
} ,
2017-10-31 18:27:25 +03:00
toggleLeaveSettingsModal ( transition ) {
let leaveTransition = this . get ( 'leaveSettingsTransition' ) ;
if ( ! transition && this . get ( 'showLeaveSettingsModal' ) ) {
this . set ( 'leaveSettingsTransition' , null ) ;
this . set ( 'showLeaveSettingsModal' , false ) ;
return ;
}
if ( ! leaveTransition || transition . targetName === leaveTransition . targetName ) {
this . set ( 'leaveSettingsTransition' , transition ) ;
// if a save is running, wait for it to finish then transition
if ( this . get ( 'saveHandlers.isRunning' ) ) {
return this . get ( 'saveHandlers.last' ) . then ( ( ) => {
transition . retry ( ) ;
} ) ;
}
// we genuinely have unsaved data, show the modal
this . set ( 'showLeaveSettingsModal' , true ) ;
}
} ,
leaveSettings ( ) {
let transition = this . get ( 'leaveSettingsTransition' ) ;
let user = this . get ( 'user' ) ;
if ( ! transition ) {
this . get ( 'notifications' ) . showAlert ( 'Sorry, there was an error in the application. Please let the Ghost team know what happened.' , { type : 'error' } ) ;
return ;
}
2018-01-11 01:57:43 +03:00
// roll back changes on user props
2017-10-31 18:27:25 +03:00
user . rollbackAttributes ( ) ;
// roll back the slugValue property
if ( this . get ( 'dirtyAttributes' ) ) {
this . set ( 'slugValue' , user . get ( 'slug' ) ) ;
this . set ( 'dirtyAttributes' , false ) ;
}
return transition . retry ( ) ;
} ,
2015-11-18 13:50:48 +03:00
toggleTransferOwnerModal ( ) {
if ( this . get ( 'canMakeOwner' ) ) {
this . toggleProperty ( 'showTransferOwnerModal' ) ;
}
} ,
toggleUploadCoverModal ( ) {
this . toggleProperty ( 'showUploadCoverModal' ) ;
} ,
toggleUploadImageModal ( ) {
this . toggleProperty ( 'showUploadImageModal' ) ;
2016-06-14 14:46:24 +03:00
} ,
// TODO: remove those mutation actions once we have better
// inline validations that auto-clear errors on input
updatePassword ( password ) {
this . set ( 'user.password' , password ) ;
this . get ( 'user.hasValidated' ) . removeObject ( 'password' ) ;
this . get ( 'user.errors' ) . remove ( 'password' ) ;
} ,
updateNewPassword ( password ) {
this . set ( 'user.newPassword' , password ) ;
this . get ( 'user.hasValidated' ) . removeObject ( 'newPassword' ) ;
this . get ( 'user.errors' ) . remove ( 'newPassword' ) ;
} ,
updateNe2Password ( password ) {
this . set ( 'user.ne2Password' , password ) ;
this . get ( 'user.hasValidated' ) . removeObject ( 'ne2Password' ) ;
this . get ( 'user.errors' ) . remove ( 'ne2Password' ) ;
2014-03-23 06:31:45 +04:00
}
2018-01-11 20:43:23 +03:00
} ,
_deleteUser ( ) {
if ( this . get ( 'deleteUserActionIsVisible' ) ) {
let user = this . get ( 'user' ) ;
return user . destroyRecord ( ) ;
}
} ,
_deleteUserSuccess ( ) {
this . get ( 'notifications' ) . closeAlerts ( 'user.delete' ) ;
this . store . unloadAll ( 'post' ) ;
this . transitionToRoute ( 'team' ) ;
} ,
_deleteUserFailure ( ) {
this . get ( 'notifications' ) . showAlert ( 'The user could not be deleted. Please try again.' , { type : 'error' , key : 'user.delete.failed' } ) ;
} ,
updateSlug : task ( function * ( newSlug ) {
let slug = this . get ( 'user.slug' ) ;
newSlug = newSlug || slug ;
newSlug = newSlug . trim ( ) ;
// Ignore unchanged slugs or candidate slugs that are empty
if ( ! newSlug || slug === newSlug ) {
this . set ( 'slugValue' , slug ) ;
return true ;
}
let serverSlug = yield this . get ( 'slugGenerator' ) . generateSlug ( 'user' , newSlug ) ;
// If after getting the sanitized and unique slug back from the API
// we end up with a slug that matches the existing slug, abort the change
if ( serverSlug === slug ) {
return true ;
}
// Because the server transforms the candidate slug by stripping
// certain characters and appending a number onto the end of slugs
// to enforce uniqueness, there are cases where we can get back a
// candidate slug that is a duplicate of the original except for
// the trailing incrementor (e.g., this-is-a-slug and this-is-a-slug-2)
// get the last token out of the slug candidate and see if it's a number
let slugTokens = serverSlug . split ( '-' ) ;
let check = Number ( slugTokens . pop ( ) ) ;
// if the candidate slug is the same as the existing slug except
// for the incrementor then the existing slug should be used
if ( isNumber ( check ) && check > 0 ) {
if ( slug === slugTokens . join ( '-' ) && serverSlug !== newSlug ) {
this . set ( 'slugValue' , slug ) ;
return true ;
}
}
this . set ( 'slugValue' , serverSlug ) ;
this . set ( 'dirtyAttributes' , true ) ;
return true ;
} ) . group ( 'saveHandlers' ) ,
save : task ( function * ( ) {
let user = this . get ( 'user' ) ;
let slugValue = this . get ( 'slugValue' ) ;
let slugChanged ;
if ( user . get ( 'slug' ) !== slugValue ) {
slugChanged = true ;
user . set ( 'slug' , slugValue ) ;
}
try {
let currentPath ,
newPath ;
user = yield user . save ( { format : false } ) ;
// If the user's slug has changed, change the URL and replace
// the history so refresh and back button still work
if ( slugChanged ) {
currentPath = window . location . hash ;
newPath = currentPath . split ( '/' ) ;
newPath [ newPath . length - 1 ] = user . get ( 'slug' ) ;
newPath = newPath . join ( '/' ) ;
windowProxy . replaceState ( { path : newPath } , '' , newPath ) ;
}
this . set ( 'dirtyAttributes' , false ) ;
this . get ( 'notifications' ) . closeAlerts ( 'user.update' ) ;
return user ;
} catch ( error ) {
// validation engine returns undefined so we have to check
// before treating the failure as an API error
if ( error ) {
this . get ( 'notifications' ) . showAPIError ( error , { key : 'user.update' } ) ;
}
}
} ) . group ( 'saveHandlers' )
2014-03-23 06:31:45 +04:00
} ) ;