2017-06-02 00:02:03 +03:00
import $ from 'jquery' ;
2016-06-30 13:21:47 +03:00
import Controller from 'ember-controller' ;
2017-02-21 22:04:50 +03:00
import computed from 'ember-computed' ;
2016-06-30 13:21:47 +03:00
import injectService from 'ember-service/inject' ;
import observer from 'ember-metal/observer' ;
2016-05-24 15:06:59 +03:00
import randomPassword from 'ghost-admin/utils/random-password' ;
2017-05-29 21:50:03 +03:00
import run from 'ember-runloop' ;
2017-05-23 11:50:04 +03:00
import {
2017-05-29 21:50:03 +03:00
IMAGE _EXTENSIONS ,
IMAGE _MIME _TYPES
2017-05-23 11:50:04 +03:00
} from 'ghost-admin/components/gh-image-uploader' ;
2017-05-29 21:50:03 +03:00
import { task } from 'ember-concurrency' ;
2015-05-11 18:35:55 +03:00
2017-03-08 16:55:35 +03:00
export default Controller . extend ( {
2016-06-30 13:21:47 +03:00
config : injectService ( ) ,
2016-08-17 18:01:46 +03:00
ghostPaths : injectService ( ) ,
notifications : injectService ( ) ,
session : injectService ( ) ,
2015-06-13 17:34:09 +03:00
2017-05-23 11:50:04 +03:00
availableTimezones : null ,
2017-01-26 14:17:34 +03:00
iconExtensions : [ 'ico' , 'png' ] ,
2017-05-23 11:50:04 +03:00
iconMimeTypes : 'image/png,image/x-icon' ,
imageExtensions : IMAGE _EXTENSIONS ,
imageMimeTypes : IMAGE _MIME _TYPES ,
_scratchFacebook : null ,
_scratchTwitter : null ,
2017-01-26 14:17:34 +03:00
2015-10-28 14:36:45 +03:00
isDatedPermalinks : computed ( 'model.permalinks' , {
set ( key , value ) {
2014-12-30 05:11:24 +03:00
this . set ( 'model.permalinks' , value ? '/:year/:month/:day/:slug/' : '/:slug/' ) ;
2014-03-21 06:55:32 +04:00
2015-10-28 14:36:45 +03:00
let slugForm = this . get ( 'model.permalinks' ) ;
2015-06-03 05:56:42 +03:00
return slugForm !== '/:slug/' ;
} ,
2015-10-28 14:36:45 +03:00
get ( ) {
let slugForm = this . get ( 'model.permalinks' ) ;
2014-03-21 06:55:32 +04:00
2015-06-03 05:56:42 +03:00
return slugForm !== '/:slug/' ;
}
2014-07-30 05:57:19 +04:00
} ) ,
2014-03-21 06:55:32 +04:00
2015-10-28 14:36:45 +03:00
generatePassword : observer ( 'model.isPrivate' , function ( ) {
2015-09-02 12:01:20 +03:00
this . get ( 'model.errors' ) . remove ( 'password' ) ;
2015-09-03 14:06:50 +03:00
if ( this . get ( 'model.isPrivate' ) && this . get ( 'model.hasDirtyAttributes' ) ) {
2015-05-11 18:35:55 +03:00
this . get ( 'model' ) . set ( 'password' , randomPassword ( ) ) ;
}
} ) ,
2016-08-17 18:01:46 +03:00
_deleteTheme ( ) {
2017-02-21 21:28:44 +03:00
let theme = this . get ( 'store' ) . peekRecord ( 'theme' , this . get ( 'themeToDelete' ) . name ) ;
2016-08-17 18:01:46 +03:00
if ( ! theme ) {
return ;
}
2017-02-21 21:28:44 +03:00
return theme . destroyRecord ( ) . catch ( ( error ) => {
2016-08-17 18:01:46 +03:00
this . get ( 'notifications' ) . showAPIError ( error ) ;
} ) ;
} ,
2017-03-08 16:55:35 +03:00
save : task ( function * ( ) {
2015-10-28 14:36:45 +03:00
let notifications = this . get ( 'notifications' ) ;
let config = this . get ( 'config' ) ;
2017-03-08 16:55:35 +03:00
try {
let model = yield this . get ( 'model' ) . save ( ) ;
2015-08-10 18:43:49 +03:00
config . set ( 'blogTitle' , model . get ( 'title' ) ) ;
2014-03-21 06:55:32 +04:00
2016-05-16 03:41:28 +03:00
// this forces the document title to recompute after
// a blog title change
this . send ( 'collectTitleTokens' , [ ] ) ;
2015-08-10 18:43:49 +03:00
return model ;
2017-03-08 16:55:35 +03:00
} catch ( error ) {
2015-08-10 18:43:49 +03:00
if ( error ) {
2015-10-07 17:44:23 +03:00
notifications . showAPIError ( error , { key : 'settings.save' } ) ;
2015-08-10 18:43:49 +03:00
}
2016-03-03 11:52:27 +03:00
throw error ;
2017-03-08 16:55:35 +03:00
}
} ) ,
2014-06-24 10:33:24 +04:00
2015-08-10 18:43:49 +03:00
actions : {
2017-05-18 13:48:37 +03:00
save ( ) {
this . get ( 'save' ) . perform ( ) ;
} ,
Timezones: Always use the timezone of blog setting
closes TryGhost/Ghost#6406
follow-up PR of #2
- adds a `timeZone` Service to provide the offset (=timezone reg. moment-timezone) of the users blog settings
- `gh-datetime-input` will read the offset of the timezone now and adjust the `publishedAt` date with it. This is the date which will be shown in the PSM 'Publish Date' field. When the user writes a new date/time, the offset is considered and will be deducted again before saving it to the model. This way, we always work with a UTC publish date except for this input field.
- gets `availableTimezones` from `configuration/timezones` API endpoint
- adds a `moment-utc` transform on all date attr (`createdAt`, `updatedAt`, `publishedAt`, `unsubscribedAt` and `lastLogin`) to only work with UTC times on serverside
- when switching the timezone in the select box, the user will be shown the local time of the selected timezone
- `createdAt`-property in `gh-user-invited` returns now `moment(createdAt).fromNow()` as `createdAt` is a moment date already
- added clock service to show actual time ticking below select box
- default timezone is '(GMT) Greenwich Mean Time : Dublin, Edinburgh, London'
- if no timezone is saved in the settings yet, the default value will be used
- shows the local time in 'Publish Date' in PSM by default, until user overwrites it
- adds dependency `moment-timezone 0.5.4` to `bower.json`
---------
**Tests:**
- sets except for clock service in test env
- adds fixtures to mirage
- adds `service.ajax` and `service:ghostPaths` to navigation-test.js
- adds unit test for `gh-format-timeago` helper
- updates acceptance test `general-setting`
- adds acceptance test for `editor`
- adds integration tests for `services/config` and `services/time-zone`
---------
**Todos:**
- [ ] Integration tests: ~~`services/config`~~, ~~`services/time-zone`~~, `components/gh-datetime-input`
- [x] Acceptance test: `editor`
- [ ] Unit tests: `utils/date-formatting`
- [ ] write issue for renaming date properties (e. g. `createdAt` to `createdAtUTC`) and translate those for server side with serializers
2016-02-02 10:04:40 +03:00
setTimezone ( timezone ) {
this . set ( 'model.activeTimezone' , timezone . name ) ;
} ,
2016-08-17 18:01:46 +03:00
2017-05-23 11:50:04 +03:00
removeImage ( image ) {
// setting `null` here will error as the server treats it as "null"
this . get ( 'model' ) . set ( image , '' ) ;
2015-11-18 13:50:48 +03:00
} ,
2017-05-23 11:50:04 +03:00
/ * *
* Opens a file selection dialog - Triggered by "Upload Image" buttons ,
* searches for the hidden file input within the . gh - setting element
* containing the clicked button then simulates a click
* @ param { MouseEvent } event - MouseEvent fired by the button click
* /
triggerFileDialog ( event ) {
2017-06-02 00:02:03 +03:00
let fileInput = $ ( event . target )
2017-05-23 11:50:04 +03:00
. closest ( '.gh-setting' )
2017-06-02 00:02:03 +03:00
. find ( 'input[type="file"]' ) ;
2017-05-23 11:50:04 +03:00
2017-06-02 00:02:03 +03:00
if ( fileInput . length > 0 ) {
2017-05-23 11:50:04 +03:00
// reset file input value before clicking so that the same image
// can be selected again
fileInput . value = '' ;
// simulate click to open file dialog
2017-06-02 00:02:03 +03:00
// using jQuery because IE11 doesn't support MouseEvent
$ ( fileInput ) . click ( ) ;
2017-05-23 11:50:04 +03:00
}
2016-03-03 11:52:27 +03:00
} ,
2017-05-23 11:50:04 +03:00
/ * *
* Fired after an image upload completes
* @ param { string } property - Property name to be set on ` this.model `
* @ param { UploadResult [ ] } results - Array of UploadResult objects
* @ return { string } The URL that was set on ` this.model.property `
* /
imageUploaded ( property , results ) {
if ( results [ 0 ] ) {
2017-06-20 12:54:27 +03:00
// Note: We have to reset the file input after upload, otherwise you can't upload the same image again
// See https://github.com/thefrontside/emberx-file-input/blob/master/addon/components/x-file-input.js#L37
// See https://github.com/TryGhost/Ghost/issues/8545
$ ( '.x-file--input' ) . val ( '' ) ;
2017-05-23 11:50:04 +03:00
return this . get ( 'model' ) . set ( property , results [ 0 ] . url ) ;
}
2017-01-26 14:17:34 +03:00
} ,
2016-03-03 11:52:27 +03:00
validateFacebookUrl ( ) {
let newUrl = this . get ( '_scratchFacebook' ) ;
let oldUrl = this . get ( 'model.facebook' ) ;
let errMessage = '' ;
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 ( 'model.facebook' , '' ) ;
this . get ( 'model.errors' ) . remove ( 'facebook' ) ;
return ;
}
2016-05-17 21:14:14 +03:00
// _scratchFacebook will be null unless the user has input something
if ( ! newUrl ) {
newUrl = oldUrl ;
}
2016-03-03 11:52:27 +03:00
// If new url didn't change, exit
if ( newUrl === oldUrl ) {
2016-05-16 21:16:40 +03:00
this . get ( 'model.errors' ) . remove ( 'facebook' ) ;
2016-03-03 11:52:27 +03:00
return ;
}
2016-05-16 21:16:40 +03:00
if ( newUrl . match ( /(?:facebook\.com\/)(\S+)/ ) || newUrl . match ( /([a-z\d\.]+)/i ) ) {
let username = [ ] ;
2016-03-03 11:52:27 +03:00
2016-05-16 21:16:40 +03:00
if ( newUrl . match ( /(?:facebook\.com\/)(\S+)/ ) ) {
2016-11-14 16:16:51 +03:00
[ , username ] = newUrl . match ( /(?:facebook\.com\/)(\S+)/ ) ;
2016-05-16 21:16:40 +03:00
} else {
2016-11-14 16:16:51 +03:00
[ , username ] = newUrl . match ( /(?:https\:\/\/|http\:\/\/)?(?:www\.)?(?:\w+\.\w+\/+)?(\S+)/mi ) ;
2016-05-16 21:16:40 +03:00
}
2016-03-03 11:52:27 +03:00
2016-05-16 21:16:40 +03:00
// check if we have a /page/username or without
if ( username . match ( /^(?:\/)?(pages?\/\S+)/mi ) ) {
// we got a page url, now save the username without the / in the beginning
2016-11-14 16:16:51 +03:00
[ , username ] = username . match ( /^(?:\/)?(pages?\/\S+)/mi ) ;
2017-05-15 21:26:34 +03:00
} else if ( username . match ( /^(http|www)|(\/)/ ) || ! username . match ( /^([a-z\d\.]{1,50})$/mi ) ) {
errMessage = ! username . match ( /^([a-z\d\.]{1,50})$/mi ) ? 'Your Page name is not a valid Facebook Page name' : 'The URL must be in a format like https://www.facebook.com/yourPage' ;
2016-03-03 11:52:27 +03:00
this . get ( 'model.errors' ) . add ( 'facebook' , errMessage ) ;
this . get ( 'model.hasValidated' ) . pushObject ( 'facebook' ) ;
return ;
}
2016-05-16 21:16:40 +03:00
newUrl = ` https://www.facebook.com/ ${ username } ` ;
this . set ( 'model.facebook' , newUrl ) ;
this . get ( 'model.errors' ) . remove ( 'facebook' ) ;
this . get ( 'model.hasValidated' ) . pushObject ( 'facebook' ) ;
// User input is validated
2017-03-08 16:55:35 +03:00
return this . get ( 'save' ) . perform ( ) . then ( ( ) => {
2016-05-16 21:16:40 +03:00
this . set ( 'model.facebook' , '' ) ;
run . schedule ( 'afterRender' , this , function ( ) {
this . set ( 'model.facebook' , newUrl ) ;
} ) ;
} ) ;
} else {
2016-11-14 16:16:51 +03:00
errMessage = 'The URL must be in a format like '
+ 'https://www.facebook.com/yourPage' ;
2016-05-16 21:16:40 +03:00
this . get ( 'model.errors' ) . add ( 'facebook' , errMessage ) ;
this . get ( 'model.hasValidated' ) . pushObject ( 'facebook' ) ;
return ;
2016-03-03 11:52:27 +03:00
}
} ,
validateTwitterUrl ( ) {
let newUrl = this . get ( '_scratchTwitter' ) ;
let oldUrl = this . get ( 'model.twitter' ) ;
let errMessage = '' ;
2016-05-17 21:14:14 +03:00
if ( newUrl === '' ) {
// Clear out the Twitter url
2016-03-03 11:52:27 +03:00
this . set ( 'model.twitter' , '' ) ;
this . get ( 'model.errors' ) . remove ( 'twitter' ) ;
return ;
}
2016-05-17 21:14:14 +03:00
// _scratchTwitter will be null unless the user has input something
if ( ! newUrl ) {
newUrl = oldUrl ;
}
2016-03-03 11:52:27 +03:00
// If new url didn't change, exit
if ( newUrl === oldUrl ) {
2016-05-16 21:16:40 +03:00
this . get ( 'model.errors' ) . remove ( 'twitter' ) ;
2016-03-03 11:52:27 +03:00
return ;
}
2016-05-16 21:16:40 +03:00
if ( newUrl . match ( /(?:twitter\.com\/)(\S+)/ ) || newUrl . match ( /([a-z\d\.]+)/i ) ) {
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
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 ( 'model.errors' ) . add ( 'twitter' , errMessage ) ;
this . get ( 'model.hasValidated' ) . pushObject ( 'twitter' ) ;
return ;
}
2016-05-16 21:16:40 +03:00
newUrl = ` https://twitter.com/ ${ username } ` ;
this . set ( 'model.twitter' , newUrl ) ;
this . get ( 'model.errors' ) . remove ( 'twitter' ) ;
this . get ( 'model.hasValidated' ) . pushObject ( 'twitter' ) ;
// User input is validated
2017-03-08 16:55:35 +03:00
return this . get ( 'save' ) . perform ( ) . then ( ( ) => {
2016-05-16 21:16:40 +03:00
this . set ( 'model.twitter' , '' ) ;
run . schedule ( 'afterRender' , this , function ( ) {
this . set ( 'model.twitter' , newUrl ) ;
} ) ;
} ) ;
} 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 ( 'model.errors' ) . add ( 'twitter' , errMessage ) ;
this . get ( 'model.hasValidated' ) . pushObject ( 'twitter' ) ;
return ;
2016-03-03 11:52:27 +03:00
}
2014-08-19 02:56:28 +04:00
}
2014-03-21 06:55:32 +04:00
}
} ) ;