Skip to content

Commit 39a9493

Browse files
authored
Fix: Invalidate draw/highlight annotations without properly structured locations (#9)
- Fix: Create draw threads for controller setup without validation - Update: Cleaning up validation methods
1 parent 31643cd commit 39a9493

File tree

9 files changed

+177
-80
lines changed

9 files changed

+177
-80
lines changed

src/Annotator.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,39 @@ class Annotator extends EventEmitter {
517517
service.removeListener(ANNOTATOR_EVENT.error, this.handleServiceEvents);
518518
}
519519

520+
/**
521+
* Gets thread params for the new annotation thread
522+
*
523+
* @param {Annotation[]} annotations - Annotations in thread
524+
* @param {Object} location - Location object
525+
* @param {string} [type] - Optional annotation type
526+
* @return {AnnotationThread} Created annotation thread
527+
*/
528+
getThreadParams(annotations, location, type) {
529+
const params = {
530+
annotatedElement: this.annotatedElement,
531+
annotations,
532+
annotationService: this.annotationService,
533+
container: this.container,
534+
fileVersionId: this.fileVersionId,
535+
isMobile: this.isMobile,
536+
hasTouch: this.hasTouch,
537+
locale: this.locale,
538+
location,
539+
type,
540+
permissions: this.permissions,
541+
localized: this.localized
542+
};
543+
544+
// Set existing thread ID if created with annotations
545+
if (annotations.length > 0) {
546+
params.threadID = annotations[0].threadID;
547+
params.threadNumber = annotations[0].threadNumber;
548+
}
549+
550+
return params;
551+
}
552+
520553
/**
521554
* Binds custom event listeners for a thread.
522555
*

src/__tests__/annotatorUtil-test.js

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import {
1919
htmlEscape,
2020
repositionCaret,
2121
isPending,
22-
validateThreadParams,
22+
isPointLocationValid,
23+
isHighlightLocationValid,
24+
isDrawLocationValid,
25+
areThreadParamsValid,
2326
eventToLocationHandler,
2427
decodeKeydown,
2528
getHeaders,
@@ -411,23 +414,64 @@ describe('annotatorUtil', () => {
411414
});
412415
});
413416

414-
describe('validateThreadParams()', () => {
417+
describe('isPointLocationValid()', () => {
415418
it('should return false if the thread is null or missing any expected params', () => {
416-
expect(validateThreadParams(null)).to.be.false;
417-
expect(validateThreadParams({ fileVersionId: 123 })).to.be.false;
419+
expect(isPointLocationValid({})).to.be.false;
420+
expect(isPointLocationValid({ x: 1, y: 2 })).to.be.true;
418421
});
422+
});
419423

420-
it('should return true if the thread is has all expected params', () => {
424+
describe('isHighlightLocationValid()', () => {
425+
it('should return false if the thread is null or missing any expected params', () => {
426+
expect(isHighlightLocationValid({})).to.be.false;
427+
expect(isHighlightLocationValid({ quadPoints: {} })).to.be.true;
428+
});
429+
});
430+
431+
describe('isDrawLocationValid()', () => {
432+
it('should return false if the thread is null or missing any expected params', () => {
433+
expect(isDrawLocationValid({})).to.be.false;
434+
expect(isDrawLocationValid({ minX: 1, minY: 1, maxX: 2, maxY: 2 })).to.be.true;
435+
});
436+
});
437+
438+
describe('areThreadParamsValid()', () => {
439+
it('should return false if the thread is null or missing any expected params', () => {
440+
expect(areThreadParamsValid(null)).to.be.false;
441+
expect(areThreadParamsValid({ fileVersionId: 123 })).to.be.false;
442+
});
443+
444+
it('should return false if thread has invalid location', () => {
421445
const threadParams = {
422446
annotatedElement: {},
423447
annotations: [],
424448
annotationService: {},
425449
fileVersionId: 123,
426450
location: {},
451+
locale: 'en-US'
452+
};
453+
454+
threadParams.type = TYPES.point;
455+
expect(areThreadParamsValid(threadParams)).to.be.false;
456+
457+
threadParams.type = TYPES.highlight;
458+
expect(areThreadParamsValid(threadParams)).to.be.false;
459+
460+
threadParams.type = TYPES.draw;
461+
expect(areThreadParamsValid(threadParams)).to.be.false;
462+
});
463+
464+
it('should return true if the thread is has all expected params', () => {
465+
const threadParams = {
466+
annotatedElement: {},
467+
annotations: [],
468+
annotationService: {},
469+
fileVersionId: 123,
470+
location: { x: 1, y: 2 },
427471
locale: 'en-US',
428-
type: 'point'
472+
type: TYPES.point
429473
};
430-
expect(validateThreadParams(threadParams)).to.be.true;
474+
expect(areThreadParamsValid(threadParams)).to.be.true;
431475
});
432476
});
433477

src/annotatorUtil.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,15 +479,52 @@ export function isPending(threadState) {
479479
return PENDING_STATES.indexOf(threadState) > -1;
480480
}
481481

482+
/**
483+
* Checks whether a point annotation thread has the correct location params
484+
*
485+
* @param {Object} location Point annotation thread location object
486+
* @return {boolean} Whether or not the point annotation has the correct location information
487+
*/
488+
export function isPointLocationValid(location) {
489+
return !!(location && location.x && location.y);
490+
}
491+
492+
/**
493+
* Checks whether a highlight annotation thread has the correct location params
494+
*
495+
* @param {Object} location Highlight annotation thread location object
496+
* @return {boolean} Whether or not the highlight annotation has the correct location information
497+
*/
498+
export function isHighlightLocationValid(location) {
499+
return !!(location && location.quadPoints);
500+
}
501+
502+
/**
503+
* Checks whether a draw annotation thread has the correct location params
504+
*
505+
* @param {Object} location Draw annotation thread location object
506+
* @return {boolean} Whether or not the draw annotation has the correct location information
507+
*/
508+
export function isDrawLocationValid(location) {
509+
return !!(location && location.minX && location.minY && location.maxX && location.maxY);
510+
}
511+
482512
/**
483513
* Checks whether annotation thread is valid by checking whether each property
484514
* in THREAD_PARAMS on the specified file object is defined.
485515
*
486516
* @param {Object} thread Annotation thread params to check
487517
* @return {boolean} Whether or not annotation thread has all the required params
488518
*/
489-
export function validateThreadParams(thread) {
519+
export function areThreadParamsValid(thread) {
490520
if (thread) {
521+
if (
522+
(thread.type === TYPES.point && !isPointLocationValid(thread.location)) ||
523+
(isHighlightAnnotation(thread.type) && !isHighlightLocationValid(thread.location)) ||
524+
(thread.type === TYPES.draw && !isDrawLocationValid(thread.location))
525+
) {
526+
return false;
527+
}
491528
return THREAD_PARAMS.every((param) => typeof thread[param] !== 'undefined');
492529
}
493530
return false;

src/doc/DocAnnotator.js

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -322,28 +322,8 @@ class DocAnnotator extends Annotator {
322322
*/
323323
createAnnotationThread(annotations, location, type) {
324324
let thread;
325-
const threadParams = {
326-
annotatedElement: this.annotatedElement,
327-
annotations,
328-
annotationService: this.annotationService,
329-
container: this.container,
330-
fileVersionId: this.fileVersionId,
331-
isMobile: this.isMobile,
332-
hasTouch: this.hasTouch,
333-
locale: this.locale,
334-
location,
335-
type,
336-
permissions: this.permissions,
337-
localized: this.localized
338-
};
339-
340-
// Set existing thread ID if created with annotations
341-
if (annotations.length > 0) {
342-
threadParams.threadID = annotations[0].threadID;
343-
threadParams.threadNumber = annotations[0].threadNumber;
344-
}
345-
346-
if (!annotatorUtil.validateThreadParams(threadParams)) {
325+
const threadParams = this.getThreadParams(annotations, location, type);
326+
if (!annotatorUtil.areThreadParamsValid(threadParams)) {
347327
this.handleValidationError();
348328
return thread;
349329
}
@@ -356,8 +336,8 @@ class DocAnnotator extends Annotator {
356336
thread = new DocPointThread(threadParams);
357337
}
358338

359-
if (!thread && this.notification) {
360-
this.emit(ANNOTATOR_EVENT.error, this.localized.createError);
339+
if (!thread) {
340+
this.emit(ANNOTATOR_EVENT.error, this.localized.loadError);
361341
} else if (thread && (type !== TYPES.draw || location.page)) {
362342
this.addThreadToMap(thread);
363343
}

src/doc/__tests__/DocAnnotator-test.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { CreateEvents } from '../CreateHighlightDialog';
1111
import * as annotatorUtil from '../../annotatorUtil';
1212
import * as docAnnotatorUtil from '../docAnnotatorUtil';
1313
import {
14+
ANNOTATOR_EVENT,
1415
STATES,
1516
TYPES,
1617
CLASS_ANNOTATION_LAYER_HIGHLIGHT,
@@ -52,7 +53,8 @@ describe('doc/DocAnnotator', () => {
5253
locale: 'en-US'
5354
},
5455
localizedStrings: {
55-
anonymousUserName: 'anonymous'
56+
anonymousUserName: 'anonymous',
57+
loadError: 'loaderror'
5658
}
5759
});
5860
annotator.annotatedElement = document.querySelector('.annotated-element');
@@ -355,7 +357,7 @@ describe('doc/DocAnnotator', () => {
355357
beforeEach(() => {
356358
stubs.addThread = sandbox.stub(annotator, 'addThreadToMap');
357359
stubs.setupFunc = AnnotationThread.prototype.setup;
358-
stubs.validateThread = sandbox.stub(annotatorUtil, 'validateThreadParams').returns(true);
360+
stubs.validateThread = sandbox.stub(annotatorUtil, 'areThreadParamsValid').returns(true);
359361
sandbox.stub(annotator, 'handleValidationError');
360362
annotator.notification = {
361363
show: sandbox.stub()
@@ -420,6 +422,13 @@ describe('doc/DocAnnotator', () => {
420422
expect(thread).to.be.empty;
421423
expect(annotator.handleValidationError).to.be.called;
422424
});
425+
426+
it('should emit error and return undefined if thread fails to create', () => {
427+
sandbox.stub(annotator, 'emit');
428+
const thread = annotator.createAnnotationThread([], {}, 'random');
429+
expect(thread).to.be.undefined;
430+
expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.error, annotator.localized.loadError);
431+
});
423432
});
424433

425434
describe('createPlainHighlight()', () => {

src/drawing/DrawingModeController.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import rbush from 'rbush';
22
import AnnotationModeController from '../AnnotationModeController';
33
import annotationsShell from './../annotationsShell.html';
4+
import DocDrawingThread from '../doc/DocDrawingThread';
45
import * as annotatorUtil from '../annotatorUtil';
56
import {
67
CLASS_ANNOTATION_DRAW,
@@ -153,7 +154,8 @@ class DrawingModeController extends AnnotationModeController {
153154
/* eslint-enable require-jsdoc */
154155

155156
// Setup
156-
this.currentThread = this.annotator.createAnnotationThread([], {}, TYPES.draw);
157+
const threadParams = this.annotator.getThreadParams([], {}, TYPES.draw);
158+
this.currentThread = new DocDrawingThread(threadParams);
157159
this.bindCustomListenersOnThread(this.currentThread);
158160

159161
// Get handlers

src/drawing/__tests__/DrawingModeController-test.js

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -135,22 +135,15 @@ describe('drawing/DrawingModeController', () => {
135135
describe('setupHandlers()', () => {
136136
beforeEach(() => {
137137
drawingModeController.annotator = {
138-
createAnnotationThread: sandbox.stub(),
138+
getThreadParams: sandbox.stub().returns({
139+
annotatedElement: document
140+
}),
139141
getLocationFromEvent: sandbox.stub(),
140142
annotatedElement: {}
141143
};
142-
stubs.createThread = drawingModeController.annotator.createAnnotationThread;
144+
stubs.getThreadParams = drawingModeController.annotator.getThreadParams;
143145
stubs.getLocation = drawingModeController.annotator.getLocationFromEvent;
144146
stubs.bindCustomListenersOnThread = sandbox.stub(drawingModeController, 'bindCustomListenersOnThread');
145-
146-
stubs.createThread.returns({
147-
saveAnnotation: () => {},
148-
undo: () => {},
149-
redo: () => {},
150-
handleMove: () => {},
151-
handleStart: () => {},
152-
handleStop: () => {}
153-
});
154147
});
155148

156149
it('should successfully contain draw mode handlers if undo and redo buttons do not exist', () => {
@@ -159,7 +152,7 @@ describe('drawing/DrawingModeController', () => {
159152
drawingModeController.redoButtonEl = undefined;
160153

161154
drawingModeController.setupHandlers();
162-
expect(stubs.createThread).to.be.called;
155+
expect(stubs.getThreadParams).to.be.called;
163156
expect(stubs.bindCustomListenersOnThread).to.be.called;
164157
expect(drawingModeController.handlers.length).to.equal(4);
165158
});
@@ -172,7 +165,7 @@ describe('drawing/DrawingModeController', () => {
172165

173166

174167
drawingModeController.setupHandlers();
175-
expect(stubs.createThread).to.be.called;
168+
expect(stubs.getThreadParams).to.be.called;
176169
expect(stubs.bindCustomListenersOnThread).to.be.called;
177170
expect(drawingModeController.handlers.length).to.equal(7);
178171
});

src/image/ImageAnnotator.js

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ImagePointThread from './ImagePointThread';
44
import * as annotatorUtil from '../annotatorUtil';
55
import * as imageAnnotatorUtil from './imageAnnotatorUtil';
66
import {
7+
TYPES,
78
CLASS_ANNOTATION_POINT_MARKER,
89
SELECTOR_ANNOTATION_BUTTON_POINT,
910
ANNOTATOR_EVENT
@@ -111,33 +112,22 @@ class ImageAnnotator extends Annotator {
111112
fixedLocation.page = 1;
112113
}
113114

114-
const threadParams = {
115-
annotatedElement: this.annotatedElement,
116-
annotations,
117-
annotationService: this.annotationService,
118-
container: this.container,
119-
fileVersionId: this.fileVersionId,
120-
isMobile: this.isMobile,
121-
locale: this.locale,
122-
location: fixedLocation,
123-
type,
124-
permissions: this.permissions,
125-
localized: this.localized
126-
};
127-
128-
if (!annotatorUtil.validateThreadParams(threadParams)) {
115+
const threadParams = this.getThreadParams(annotations, location, type);
116+
if (!annotatorUtil.areThreadParamsValid(threadParams)) {
129117
this.handleValidationError();
130118
return thread;
131119
}
132120

133-
// Set existing thread ID if created with annotations
134-
if (annotations.length > 0) {
135-
threadParams.threadID = annotations[0].threadID;
136-
threadParams.threadNumber = annotations[0].threadNumber;
121+
if (type === TYPES.point) {
122+
thread = new ImagePointThread(threadParams);
123+
}
124+
125+
if (!thread) {
126+
this.emit(ANNOTATOR_EVENT.error, this.localized.loadError);
127+
} else {
128+
this.addThreadToMap(thread);
137129
}
138130

139-
thread = new ImagePointThread(threadParams);
140-
this.addThreadToMap(thread);
141131
return thread;
142132
}
143133

0 commit comments

Comments
 (0)