1+ // # Update Checking Service
2+ //
3+ // Makes a request to Ghost.org to check if there is a new version of Ghost available.
4+ // The service is provided in return for users opting in to anonymous usage data collection
5+ // Blog owners can opt-out of update checks by setting 'updateCheck: false' in their config.js
6+ //
7+ // The data collected is as follows:
8+ // - blog id - a hash of the blog hostname, pathname and dbHash, we do not store URL, IP or other identifiable info
9+ // - ghost version
10+ // - node version
11+ // - npm version
12+ // - env - production or development
13+ // - database type - SQLite, MySQL, pg
14+ // - email transport - mail.options.service, or otherwise mail.transport
15+ // - created date - the date the database was created
16+ // - post count - total number of posts
17+ // - user count - total number of users
18+ // - theme - name of the currently active theme
19+ // - apps - names of any active plugins
20+
21+ var crypto = require ( 'crypto' ) ,
22+ exec = require ( 'child_process' ) . exec ,
23+ https = require ( 'https' ) ,
24+ moment = require ( 'moment' ) ,
25+ semver = require ( 'semver' ) ,
26+ when = require ( 'when' ) ,
27+ nodefn = require ( 'when/node/function' ) ,
28+ _ = require ( 'underscore' ) ,
29+ url = require ( 'url' ) ,
30+
31+ api = require ( './api' ) ,
32+ config = require ( './config' ) ,
33+ errors = require ( './errorHandling' ) ,
34+
35+ allowedCheckEnvironments = [ 'development' , 'production' ] ,
36+ checkEndpoint = 'updates.ghost.org' ,
37+ currentVersion ;
38+
39+ function updateCheckError ( error ) {
40+ errors . logError (
41+ error ,
42+ "Checking for updates failed, your blog will continue to function." ,
43+ "If you get this error repeatedly, please seek help from http://ghost.org/forum."
44+ ) ;
45+ }
46+
47+ function updateCheckData ( ) {
48+ var data = { } ,
49+ ops = [ ] ,
50+ mailConfig = config ( ) . mail ;
51+
52+ ops . push ( api . settings . read ( 'dbHash' ) . otherwise ( errors . rejectError ) ) ;
53+ ops . push ( api . settings . read ( 'activeTheme' ) . otherwise ( errors . rejectError ) ) ;
54+ ops . push ( api . settings . read ( 'activePlugins' )
55+ . then ( function ( apps ) {
56+ try {
57+ apps = JSON . parse ( apps . value ) ;
58+ } catch ( e ) {
59+ return errors . rejectError ( e ) ;
60+ }
61+
62+ return _ . reduce ( apps , function ( memo , item ) { return memo === '' ? memo + item : memo + ', ' + item ; } , '' ) ;
63+ } ) . otherwise ( errors . rejectError ) ) ;
64+ ops . push ( api . posts . browse ( ) . otherwise ( errors . rejectError ) ) ;
65+ ops . push ( api . users . browse ( ) . otherwise ( errors . rejectError ) ) ;
66+ ops . push ( nodefn . call ( exec , 'npm -v' ) . otherwise ( errors . rejectError ) ) ;
67+
68+ data . ghost_version = currentVersion ;
69+ data . node_version = process . versions . node ;
70+ data . env = process . env . NODE_ENV ;
71+ data . database_type = require ( './models/base' ) . client ;
72+ data . email_transport = mailConfig . options && mailConfig . options . service ? mailConfig . options . service : mailConfig . transport ;
73+
74+ return when . settle ( ops ) . then ( function ( descriptors ) {
75+ var hash = descriptors [ 0 ] . value ,
76+ theme = descriptors [ 1 ] . value ,
77+ apps = descriptors [ 2 ] . value ,
78+ posts = descriptors [ 3 ] . value ,
79+ users = descriptors [ 4 ] . value ,
80+ npm = descriptors [ 5 ] . value ,
81+ blogUrl = url . parse ( config ( ) . url ) ,
82+ blogId = blogUrl . hostname + blogUrl . pathname . replace ( / \/ / , '' ) + hash . value ;
83+
84+ data . blog_id = crypto . createHash ( 'md5' ) . update ( blogId ) . digest ( 'hex' ) ;
85+ data . theme = theme ? theme . value : '' ;
86+ data . apps = apps || '' ;
87+ data . post_count = posts && posts . total ? posts . total : 0 ;
88+ data . user_count = users && users . length ? users . length : 0 ;
89+ data . blog_created_at = users && users [ 0 ] && users [ 0 ] . created_at ? moment ( users [ 0 ] . created_at ) . unix ( ) : '' ;
90+ data . npm_version = _ . isArray ( npm ) && npm [ 0 ] ? npm [ 0 ] . toString ( ) . replace ( / \n / , '' ) : '' ;
91+
92+ return data ;
93+ } ) . otherwise ( updateCheckError ) ;
94+ }
95+
96+ function updateCheckRequest ( ) {
97+ return updateCheckData ( ) . then ( function ( reqData ) {
98+ var deferred = when . defer ( ) ,
99+ resData = '' ,
100+ headers ,
101+ req ;
102+
103+ reqData = JSON . stringify ( reqData ) ;
104+
105+ headers = {
106+ 'Content-Length' : reqData . length
107+ } ;
108+
109+ req = https . request ( {
110+ hostname : checkEndpoint ,
111+ method : 'POST' ,
112+ headers : headers
113+ } , function ( res ) {
114+ res . on ( 'error' , function ( error ) { deferred . reject ( error ) ; } ) ;
115+ res . on ( 'data' , function ( chunk ) { resData += chunk ; } ) ;
116+ res . on ( 'end' , function ( ) {
117+ try {
118+ resData = JSON . parse ( resData ) ;
119+ deferred . resolve ( resData ) ;
120+ } catch ( e ) {
121+ deferred . reject ( 'Unable to decode update response' ) ;
122+ }
123+ } ) ;
124+ } ) ;
125+
126+ req . write ( reqData ) ;
127+ req . end ( ) ;
128+
129+ req . on ( 'error' , function ( error ) {
130+ deferred . reject ( error ) ;
131+ } ) ;
132+
133+ return deferred . promise ;
134+ } ) ;
135+ }
136+
137+ // ## Update Check Response
138+ // Handles the response from the update check
139+ // Does two things with the information received:
140+ // 1. Updates the time we can next make a check
141+ // 2. Checks if the version in the response is new, and updates the notification setting
142+ function updateCheckResponse ( response ) {
143+ var ops = [ ] ,
144+ displayUpdateNotification = currentVersion && semver . gt ( response . version , currentVersion ) ;
145+
146+ ops . push ( api . settings . edit ( 'nextUpdateCheck' , response . next_check )
147+ . otherwise ( errors . rejectError ) ) ;
148+
149+ ops . push ( api . settings . edit ( 'displayUpdateNotification' , displayUpdateNotification )
150+ . otherwise ( errors . rejectError ) ) ;
151+
152+ return when . settle ( ops ) . then ( function ( descriptors ) {
153+ descriptors . forEach ( function ( d ) {
154+ if ( d . state === 'rejected' ) {
155+ errors . rejectError ( d . reason ) ;
156+ }
157+ } ) ;
158+ return when . resolve ( ) ;
159+ } ) ;
160+ }
161+
162+ function updateCheck ( res ) {
163+ var deferred = when . defer ( ) ;
164+
165+ // The check will not happen if:
166+ // 1. updateCheck is defined as false in config.js
167+ // 2. we've already done a check this session
168+ // 3. we're not in production or development mode
169+ if ( config ( ) . updateCheck === false || _ . indexOf ( allowedCheckEnvironments , process . env . NODE_ENV ) === - 1 ) {
170+ // No update check
171+ deferred . resolve ( ) ;
172+ } else {
173+ api . settings . read ( 'nextUpdateCheck' ) . then ( function ( nextUpdateCheck ) {
174+ if ( nextUpdateCheck && nextUpdateCheck . value && nextUpdateCheck . value > moment ( ) . unix ( ) ) {
175+ // It's not time to check yet
176+ deferred . resolve ( ) ;
177+ } else {
178+ // We need to do a check, store the current version
179+ currentVersion = res . locals . version ;
180+ return updateCheckRequest ( )
181+ . then ( updateCheckResponse )
182+ . otherwise ( updateCheckError ) ;
183+ }
184+ } ) . otherwise ( updateCheckError )
185+ . then ( deferred . resolve ) ;
186+ }
187+
188+ return deferred . promise ;
189+ }
190+
191+ module . exports = updateCheck ;
0 commit comments