@@ -6,6 +6,7 @@ const _ = require('lodash')
6
6
const Joi = require ( 'joi' )
7
7
const uuid = require ( 'uuid/v4' )
8
8
const config = require ( 'config' )
9
+ const xss = require ( 'xss' )
9
10
const helper = require ( '../common/helper' )
10
11
const logger = require ( '../common/logger' )
11
12
const errors = require ( '../common/errors' )
@@ -109,6 +110,15 @@ async function searchChallenges (currentUser, criteria) {
109
110
const page = criteria . page || 1
110
111
const perPage = criteria . perPage || 20
111
112
const boolQuery = [ ]
113
+ const matchPhraseKeys = [
114
+ 'id' ,
115
+ 'timelineTemplateId' ,
116
+ 'projectId' ,
117
+ 'legacyId' ,
118
+ 'status' ,
119
+ 'createdBy' ,
120
+ 'updatedBy'
121
+ ]
112
122
113
123
const includedTrackIds = _ . isArray ( criteria . trackIds ) ? criteria . trackIds : [ ]
114
124
@@ -151,18 +161,24 @@ async function searchChallenges (currentUser, criteria) {
151
161
includedTrackIds . push ( criteria . trackId )
152
162
}
153
163
154
- _ . forIn ( _ . omit ( criteria , [ 'types' , 'tracks' , 'typeIds' , 'trackIds' , 'type' , 'name' , 'trackId' , 'typeId' , 'description' , 'page' , 'perPage' , 'tag' ,
155
- 'group' , 'groups' , 'memberId' , 'ids' , 'createdDateStart' , 'createdDateEnd' , 'updatedDateStart' , 'updatedDateEnd' , 'startDateStart' , 'startDateEnd' , 'endDateStart' , 'endDateEnd' ,
156
- 'tags' , 'registrationStartDateStart' , 'registrationStartDateEnd' , 'currentPhaseName' , 'submissionStartDateStart' , 'submissionStartDateEnd' ,
157
- 'registrationEndDateStart' , 'registrationEndDateEnd' , 'submissionEndDateStart' , 'submissionEndDateEnd' , 'includeAllEvents' , 'events' ,
158
- 'forumId' , 'track' , 'reviewType' , 'confidentialityType' , 'directProjectId' , 'sortBy' , 'sortOrder' , 'isLightweight' , 'isTask' , 'taskIsAssigned' , 'taskMemberId' ] ) , ( value , key ) => {
164
+ _ . forIn ( _ . pick ( criteria , matchPhraseKeys ) , ( value , key ) => {
159
165
if ( ! _ . isUndefined ( value ) ) {
160
166
const filter = { match_phrase : { } }
161
167
filter . match_phrase [ key ] = value
162
168
boolQuery . push ( filter )
163
169
}
164
170
} )
165
171
172
+ _ . forEach ( _ . keys ( criteria ) , ( key ) => {
173
+ if ( _ . toString ( key ) . indexOf ( 'meta.' ) > - 1 ) {
174
+ // Parse and use metadata key
175
+ if ( ! _ . isUndefined ( criteria [ key ] ) ) {
176
+ const metaKey = key . split ( 'meta.' ) [ 1 ]
177
+ boolQuery . push ( { match_phrase : { [ `metadata.${ metaKey } ` ] : criteria [ key ] } } )
178
+ }
179
+ }
180
+ } )
181
+
166
182
if ( includedTypeIds . length > 0 ) {
167
183
boolQuery . push ( {
168
184
bool : {
@@ -180,10 +196,10 @@ async function searchChallenges (currentUser, criteria) {
180
196
}
181
197
182
198
if ( criteria . name ) {
183
- boolQuery . push ( { match : { name : `. *${ criteria . name } . *` } } )
199
+ boolQuery . push ( { wildcard : { name : `*${ _ . toLower ( criteria . name ) } *` } } )
184
200
}
185
201
if ( criteria . description ) {
186
- boolQuery . push ( { match : { description : `. *${ criteria . name } . *` } } )
202
+ boolQuery . push ( { wildcard : { description : `*${ _ . toLower ( criteria . description ) } *` } } )
187
203
}
188
204
if ( criteria . forumId ) {
189
205
boolQuery . push ( { match_phrase : { 'legacy.forumId' : criteria . forumId } } )
@@ -259,31 +275,23 @@ async function searchChallenges (currentUser, criteria) {
259
275
260
276
const mustQuery = [ ]
261
277
262
- const shouldQuery = [ ]
278
+ const groupsQuery = [ ]
263
279
264
280
// logger.debug(`Tags: ${criteria.tags}`)
265
281
if ( criteria . tags ) {
266
- if ( criteria . includeAllTags ) {
267
- for ( const tag of criteria . tags ) {
268
- boolQuery . push ( { match_phrase : { tags : tag } } )
269
- }
270
- } else {
271
- for ( const tag of criteria . tags ) {
272
- shouldQuery . push ( { match : { tags : tag } } )
282
+ boolQuery . push ( {
283
+ bool : {
284
+ [ criteria . includeAllTags ? 'must' : 'should' ] : _ . map ( criteria . tags , t => ( { match_phrase : { tags : t } } ) )
273
285
}
274
- }
286
+ } )
275
287
}
276
288
277
289
if ( criteria . events ) {
278
- if ( criteria . includeAllEvents ) {
279
- for ( const e of criteria . events ) {
280
- boolQuery . push ( { match_phrase : { 'events.key' : e } } )
281
- }
282
- } else {
283
- for ( const e of criteria . events ) {
284
- shouldQuery . push ( { match : { 'events.key' : e } } )
290
+ boolQuery . push ( {
291
+ bool : {
292
+ [ criteria . includeAllEvents ? 'must' : 'should' ] : _ . map ( criteria . events , e => ( { match_phrase : { 'events.key' : e } } ) )
285
293
}
286
- }
294
+ } )
287
295
}
288
296
289
297
const mustNotQuery = [ ]
@@ -337,21 +345,23 @@ async function searchChallenges (currentUser, criteria) {
337
345
} else if ( ! currentUser . isMachine && ! helper . hasAdminRole ( currentUser ) ) {
338
346
// If the user is not M2M and is not an admin, return public + challenges from groups the user can access
339
347
_ . each ( accessibleGroups , ( g ) => {
340
- shouldQuery . push ( { match_phrase : { groups : g } } )
348
+ groupsQuery . push ( { match_phrase : { groups : g } } )
341
349
} )
342
350
// include public challenges
343
- shouldQuery . push ( { bool : { must_not : { exists : { field : 'groups' } } } } )
351
+ groupsQuery . push ( { bool : { must_not : { exists : { field : 'groups' } } } } )
344
352
}
345
353
} else {
346
354
_ . each ( groupsToFilter , ( g ) => {
347
- shouldQuery . push ( { match_phrase : { groups : g } } )
355
+ groupsQuery . push ( { match_phrase : { groups : g } } )
348
356
} )
349
357
}
350
358
351
359
if ( criteria . ids ) {
352
- for ( const id of criteria . ids ) {
353
- shouldQuery . push ( { match_phrase : { _id : id } } )
354
- }
360
+ boolQuery . push ( {
361
+ bool : {
362
+ should : _ . map ( criteria . ids , id => ( { match_phrase : { _id : id } } ) )
363
+ }
364
+ } )
355
365
}
356
366
357
367
const accessQuery = [ ]
@@ -367,6 +377,8 @@ async function searchChallenges (currentUser, criteria) {
367
377
memberChallengeIds = await helper . listChallengesByMember ( criteria . memberId )
368
378
// logger.error(`response ${JSON.stringify(ids)}`)
369
379
accessQuery . push ( { terms : { _id : memberChallengeIds } } )
380
+ } else if ( currentUser && ! helper . hasAdminRole ( currentUser ) && ! _ . get ( currentUser , 'isMachine' , false ) ) {
381
+ memberChallengeIds = await helper . listChallengesByMember ( currentUser . userId )
370
382
}
371
383
372
384
if ( accessQuery . length > 0 ) {
@@ -407,7 +419,7 @@ async function searchChallenges (currentUser, criteria) {
407
419
mustQuery . push ( {
408
420
bool : {
409
421
should : [
410
- ...( _ . get ( memberChallengeIds , 'length' , 0 ) > 0 ? [ { terms : { _id : memberChallengeIds } } ] : [ ] ) ,
422
+ ...( _ . get ( memberChallengeIds , 'length' , 0 ) > 0 ? [ { bool : { should : [ { terms : { _id : memberChallengeIds } } ] } } ] : [ ] ) ,
411
423
{ bool : { must_not : { exists : { field : 'task.isTask' } } } } ,
412
424
{ match_phrase : { 'task.isTask' : false } } ,
413
425
{
@@ -428,10 +440,10 @@ async function searchChallenges (currentUser, criteria) {
428
440
} )
429
441
}
430
442
431
- if ( shouldQuery . length > 0 ) {
443
+ if ( groupsQuery . length > 0 ) {
432
444
mustQuery . push ( {
433
445
bool : {
434
- should : shouldQuery
446
+ should : groupsQuery
435
447
}
436
448
} )
437
449
}
@@ -594,7 +606,7 @@ searchChallenges.schema = {
594
606
taskMemberId : Joi . string ( ) ,
595
607
events : Joi . array ( ) . items ( Joi . number ( ) ) ,
596
608
includeAllEvents : Joi . boolean ( ) . default ( true )
597
- } )
609
+ } ) . unknown ( true )
598
610
}
599
611
600
612
/**
@@ -731,6 +743,8 @@ async function populatePhases (phases, startDate, timelineTemplateId) {
731
743
* @returns {Object } the created challenge
732
744
*/
733
745
async function createChallenge ( currentUser , challenge , userToken ) {
746
+ challenge . name = xss ( challenge . name )
747
+ challenge . description = xss ( challenge . description )
734
748
if ( challenge . status === constants . challengeStatuses . Active ) {
735
749
throw new errors . BadRequestError ( 'You cannot create an Active challenge. Please create a Draft challenge and then change the status to Active.' )
736
750
}
@@ -743,6 +757,13 @@ async function createChallenge (currentUser, challenge, userToken) {
743
757
}
744
758
if ( _ . isUndefined ( _ . get ( challenge , 'task.memberId' ) ) ) {
745
759
_ . set ( challenge , 'task.memberId' , null )
760
+ } else {
761
+ throw new errors . BadRequestError ( `Cannot assign a member before the challenge gets created.` )
762
+ }
763
+ }
764
+ if ( challenge . discussions && challenge . discussions . length > 0 ) {
765
+ for ( let i = 0 ; i < challenge . discussions . length ; i += 1 ) {
766
+ challenge . discussions [ i ] . id = uuid ( )
746
767
}
747
768
}
748
769
if ( challenge . phases && challenge . phases . length > 0 ) {
@@ -782,6 +803,11 @@ async function createChallenge (currentUser, challenge, userToken) {
782
803
// this will need to be updated to associate project terms with a roleId
783
804
challenge . terms = await helper . validateChallengeTerms ( challenge . terms || [ ] )
784
805
806
+ // default the descriptionFormat
807
+ if ( ! challenge . descriptionFormat ) {
808
+ challenge . descriptionFormat = 'markdown'
809
+ }
810
+
785
811
if ( challenge . phases && challenge . phases . length > 0 ) {
786
812
challenge . endDate = helper . calculateChallengeEndDate ( challenge )
787
813
}
@@ -866,6 +892,13 @@ createChallenge.schema = {
866
892
name : Joi . string ( ) ,
867
893
key : Joi . string ( )
868
894
} ) ) ,
895
+ discussions : Joi . array ( ) . items ( Joi . object ( ) . keys ( {
896
+ name : Joi . string ( ) . required ( ) ,
897
+ type : Joi . string ( ) . required ( ) . valid ( _ . values ( constants . DiscussionTypes ) ) ,
898
+ provider : Joi . string ( ) . required ( ) ,
899
+ url : Joi . string ( ) ,
900
+ options : Joi . array ( ) . items ( Joi . object ( ) )
901
+ } ) ) ,
869
902
prizeSets : Joi . array ( ) . items ( Joi . object ( ) . keys ( {
870
903
type : Joi . string ( ) . valid ( _ . values ( constants . prizeSetTypes ) ) . required ( ) ,
871
904
description : Joi . string ( ) ,
@@ -1122,6 +1155,11 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
1122
1155
throw new errors . BadRequestError ( 'You cannot activate the challenge as it has not been created on legacy yet. Please try again later or contact support.' )
1123
1156
}
1124
1157
billingAccountId = await helper . getProjectBillingAccount ( _ . get ( challenge , 'legacy.directProjectId' ) )
1158
+ // if activating a challenge, the challenge must have a billing account id
1159
+ if ( ( ! billingAccountId || billingAccountId === null ) &&
1160
+ challenge . status === constants . challengeStatuses . Draft ) {
1161
+ throw new errors . BadRequestError ( 'Cannot Activate this project, it has no active billing accounts.' )
1162
+ }
1125
1163
}
1126
1164
if ( data . status === constants . challengeStatuses . Completed ) {
1127
1165
if ( challenge . status !== constants . challengeStatuses . Active ) {
@@ -1159,8 +1197,30 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
1159
1197
newAttachments = await helper . getByIds ( 'Attachment' , data . attachmentIds || [ ] )
1160
1198
}
1161
1199
1162
- if ( ! currentUser . isMachine && ! helper . hasAdminRole ( currentUser ) && challenge . createdBy . toLowerCase ( ) !== currentUser . handle . toLowerCase ( ) ) {
1163
- throw new errors . ForbiddenError ( `Only M2M, admin or challenge's copilot can perform modification.` )
1200
+ const userHasFullAccess = await helper . userHasFullAccess ( challengeId , currentUser . userId )
1201
+ if ( ! currentUser . isMachine && ! helper . hasAdminRole ( currentUser ) && challenge . createdBy . toLowerCase ( ) !== currentUser . handle . toLowerCase ( ) && ! userHasFullAccess ) {
1202
+ throw new errors . ForbiddenError ( `Only M2M, admin, challenge's copilot or users with full access can perform modification.` )
1203
+ }
1204
+
1205
+ // Only M2M can update url and options of discussions
1206
+ if ( data . discussions && data . discussions . length > 0 ) {
1207
+ for ( let i = 0 ; i < data . discussions . length ; i += 1 ) {
1208
+ if ( _ . isUndefined ( data . discussions [ i ] . id ) ) {
1209
+ data . discussions [ i ] . id = uuid ( )
1210
+ if ( ! currentUser . isMachine ) {
1211
+ _ . unset ( data . discussions , 'url' )
1212
+ _ . unset ( data . discussions , 'options' )
1213
+ }
1214
+ } else if ( ! currentUser . isMachine ) {
1215
+ const existingDiscussion = _ . find ( _ . get ( challenge , 'discussions' , [ ] ) , d => d . id === data . discussions [ i ] . id )
1216
+ if ( existingDiscussion ) {
1217
+ _ . assign ( data . discussions [ i ] , _ . pick ( existingDiscussion , [ 'url' , 'options' ] ) )
1218
+ } else {
1219
+ _ . unset ( data . discussions , 'url' )
1220
+ _ . unset ( data . discussions , 'options' )
1221
+ }
1222
+ }
1223
+ }
1164
1224
}
1165
1225
1166
1226
// Validate the challenge terms
@@ -1211,7 +1271,15 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
1211
1271
}
1212
1272
1213
1273
if ( data . phases || data . startDate ) {
1214
- const newPhases = data . phases || challenge . phases
1274
+ if ( data . phases && data . phases . length > 0 ) {
1275
+ for ( let i = 0 ; i < challenge . phases . length ; i += 1 ) {
1276
+ const updatedPhaseInfo = _ . find ( data . phases , p => p . phaseId === challenge . phases [ i ] . phaseId )
1277
+ if ( updatedPhaseInfo ) {
1278
+ _ . extend ( challenge . phases [ i ] , updatedPhaseInfo )
1279
+ }
1280
+ }
1281
+ }
1282
+ const newPhases = challenge . phases
1215
1283
const newStartDate = data . startDate || challenge . startDate
1216
1284
1217
1285
await helper . validatePhases ( newPhases )
@@ -1419,6 +1487,27 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
1419
1487
data . winners = null
1420
1488
}
1421
1489
1490
+ const { track, type } = await validateChallengeData ( _ . pick ( challenge , [ 'trackId' , 'typeId' ] ) )
1491
+
1492
+ // Only m2m tokens are allowed to modify the `task.*` information on a challenge
1493
+ if ( ! _ . isUndefined ( _ . get ( data , 'task' ) ) && ! currentUser . isMachine ) {
1494
+ if ( ! _ . isUndefined ( _ . get ( challenge , 'task' ) ) ) {
1495
+ data . task = challenge . task
1496
+ } else {
1497
+ delete data . task
1498
+ }
1499
+ }
1500
+
1501
+ if ( _ . get ( type , 'isTask' ) ) {
1502
+ if ( ! _ . isEmpty ( _ . get ( data , 'task.memberId' ) ) ) {
1503
+ const challengeResources = await helper . getChallengeResources ( challengeId )
1504
+ const registrants = _ . filter ( challengeResources , r => r . roleId === config . SUBMITTER_ROLE_ID )
1505
+ if ( ! _ . find ( registrants , r => _ . toString ( r . memberId ) === _ . toString ( _ . get ( data , 'task.memberId' ) ) ) ) {
1506
+ throw new errors . BadRequestError ( `Member ${ _ . get ( data , 'task.memberId' ) } is not a submitter resource of challenge ${ challengeId } ` )
1507
+ }
1508
+ }
1509
+ }
1510
+
1422
1511
logger . debug ( `Challenge.update id: ${ challengeId } Details: ${ JSON . stringify ( updateDetails ) } ` )
1423
1512
await models . Challenge . update ( { id : challengeId } , updateDetails )
1424
1513
@@ -1459,7 +1548,6 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
1459
1548
}
1460
1549
1461
1550
// Populate challenge.track and challenge.type based on the track/type IDs
1462
- const { track, type } = await validateChallengeData ( _ . pick ( challenge , [ 'trackId' , 'typeId' ] ) )
1463
1551
1464
1552
if ( track ) {
1465
1553
challenge . track = track . name
@@ -1515,6 +1603,12 @@ function sanitizeChallenge (challenge) {
1515
1603
'attachmentIds' ,
1516
1604
'groups'
1517
1605
] )
1606
+ if ( ! _ . isUndefined ( sanitized . name ) ) {
1607
+ sanitized . name = xss ( sanitized . name )
1608
+ }
1609
+ if ( ! _ . isUndefined ( sanitized . description ) ) {
1610
+ sanitized . description = xss ( sanitized . description )
1611
+ }
1518
1612
if ( challenge . legacy ) {
1519
1613
sanitized . legacy = _ . pick ( challenge . legacy , [
1520
1614
'track' ,
@@ -1546,6 +1640,9 @@ function sanitizeChallenge (challenge) {
1546
1640
if ( challenge . winners ) {
1547
1641
sanitized . winners = _ . map ( challenge . winners , winner => _ . pick ( winner , [ 'userId' , 'handle' , 'placement' ] ) )
1548
1642
}
1643
+ if ( challenge . discussions ) {
1644
+ sanitized . discussions = _ . map ( challenge . discussions , discussion => _ . pick ( discussion , [ 'id' , 'provider' , 'name' , 'type' , 'url' , 'options' ] ) )
1645
+ }
1549
1646
if ( challenge . terms ) {
1550
1647
sanitized . terms = _ . map ( challenge . terms , term => _ . pick ( term , [ 'id' , 'roleId' ] ) )
1551
1648
}
@@ -1614,6 +1711,13 @@ fullyUpdateChallenge.schema = {
1614
1711
name : Joi . string ( ) ,
1615
1712
key : Joi . string ( )
1616
1713
} ) . unknown ( true ) ) ,
1714
+ discussions : Joi . array ( ) . items ( Joi . object ( ) . keys ( {
1715
+ name : Joi . string ( ) . required ( ) ,
1716
+ type : Joi . string ( ) . required ( ) . valid ( _ . values ( constants . DiscussionTypes ) ) ,
1717
+ provider : Joi . string ( ) . required ( ) ,
1718
+ url : Joi . string ( ) ,
1719
+ options : Joi . array ( ) . items ( Joi . object ( ) )
1720
+ } ) ) ,
1617
1721
tags : Joi . array ( ) . items ( Joi . string ( ) . required ( ) ) , // tag names
1618
1722
projectId : Joi . number ( ) . integer ( ) . positive ( ) . required ( ) ,
1619
1723
legacyId : Joi . number ( ) . integer ( ) . positive ( ) ,
@@ -1686,6 +1790,13 @@ partiallyUpdateChallenge.schema = {
1686
1790
name : Joi . string ( ) ,
1687
1791
key : Joi . string ( )
1688
1792
} ) . unknown ( true ) ) ,
1793
+ discussions : Joi . array ( ) . items ( Joi . object ( ) . keys ( {
1794
+ name : Joi . string ( ) . required ( ) ,
1795
+ type : Joi . string ( ) . required ( ) . valid ( _ . values ( constants . DiscussionTypes ) ) ,
1796
+ provider : Joi . string ( ) . required ( ) ,
1797
+ url : Joi . string ( ) ,
1798
+ options : Joi . array ( ) . items ( Joi . object ( ) )
1799
+ } ) ) ,
1689
1800
startDate : Joi . date ( ) ,
1690
1801
prizeSets : Joi . array ( ) . items ( Joi . object ( ) . keys ( {
1691
1802
type : Joi . string ( ) . valid ( _ . values ( constants . prizeSetTypes ) ) . required ( ) ,
0 commit comments