Skip to content

Commit d45fdb8

Browse files
author
Bryan Mitchell
committed
fix(district): attribute docket uploads by the page's own goDLS case id
The docket report upload previously trusted a pacer_case_id derived from the referrer or cached in per-tab storage. Both values can belong to a different case viewed earlier in the same tab, and when that happened the entire docket sheet of one case was uploaded under another case's id, merging one case's full history into the other's archive docket. Observed in the wild: all ~165 entries of nvd 3:25-cv-00553 were merged into nvd 2:26-cv-00720 (CourtListener docket 72456248), including the other case's parties. A district docket sheet already asserts its own case id: every document link carries it in the goDLS() onclick handler. Tally those ids and attribute the upload by the strict majority, so a stray link to another case's document (legitimate on consolidated/MDL sheets) cannot outvote the sheet itself; a tie falls back to the previous behavior rather than guessing. When the page id wins over a conflicting inherited id, warn on the console and heal the tab's cached case id so later pages in the tab (e.g. attachment menus, which have no case id of their own) inherit the right one. Pages with no goDLS evidence behave exactly as before.
1 parent 8329d69 commit d45fdb8

4 files changed

Lines changed: 192 additions & 7 deletions

File tree

CHANGES.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ Changes:
1111
- None yet
1212

1313
Fixes:
14-
- None yet
14+
- Attribute district docket report uploads by the case id the page's own
15+
goDLS document links assert, instead of a referrer-derived or per-tab
16+
cached case id that can belong to a different case viewed earlier in
17+
the same tab. Prevents one case's docket sheet from being uploaded
18+
under another case's `pacer_case_id` and merged into the wrong archive
19+
docket.
1520

1621
For developers:
1722
- Nothing yet

spec/ContentDelegateSpec.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,16 @@ describe('The ContentDelegate class', function () {
596596
});
597597

598598
describe('when the docket page is not an interstitial page', function () {
599+
const makeLink = (caseId, docId) => {
600+
const a = document.createElement('a');
601+
a.href = `https://ecf.canb.uscourts.gov/doc1/${docId}`;
602+
a.setAttribute(
603+
'onclick',
604+
`goDLS('/doc1/${docId}','${caseId}','5','','','1','','','');` +
605+
'return(false);'
606+
);
607+
return a;
608+
};
599609
beforeEach(function () {
600610
clearDocumentBody();
601611
table = document.createElement('table');
@@ -670,6 +680,131 @@ describe('The ContentDelegate class', function () {
670680
expect(button.length).toBe(1);
671681
});
672682

683+
it('prefers the goDLS case id over a stale tab id', async function () {
684+
// The tab's cached caseId ('531591' in the storage mock)
685+
// belongs to another case; the page's goDLS links must win.
686+
const link = makeLink('177277', '034031424909');
687+
const cd = new ContentDelegate(
688+
tabId,
689+
docketDisplayUrl,
690+
docketDisplayPath,
691+
'canb',
692+
undefined, // no case id derived from url/referrer/inputs
693+
undefined,
694+
[link]
695+
);
696+
dispatchBackgroundNotifier = jasmine.createSpy();
697+
dispatchBackgroundFetch = jasmine
698+
.createSpy()
699+
.and.callFake(fakeBackgroundFetch);
700+
spyOn(history, 'replaceState');
701+
await cd.handleDocketDisplayPage();
702+
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
703+
jasmine.objectContaining({
704+
action: 'upload',
705+
data: jasmine.objectContaining({ pacer_case_id: '177277' }),
706+
})
707+
);
708+
expect(dispatchBackgroundFetch).not.toHaveBeenCalledWith(
709+
jasmine.objectContaining({
710+
data: jasmine.objectContaining({ pacer_case_id: '531591' }),
711+
})
712+
);
713+
// the stale cached id is corrected for later pages in this tab
714+
expect(window.chrome.storage.local.set).toHaveBeenCalledWith(
715+
jasmine.objectContaining({
716+
1234: jasmine.objectContaining({ caseId: '177277' }),
717+
}),
718+
jasmine.any(Function)
719+
);
720+
});
721+
722+
it('overrides a conflicting id with the goDLS id', async function () {
723+
// Same protection when a wrong id arrived at initialization
724+
// (e.g. parsed from the referrer of a previously viewed case).
725+
const link = makeLink('177277', '034031424909');
726+
const cd = new ContentDelegate(
727+
tabId,
728+
docketDisplayUrl,
729+
docketDisplayPath,
730+
'canb',
731+
'531591', // stale id from another case
732+
undefined,
733+
[link]
734+
);
735+
dispatchBackgroundNotifier = jasmine.createSpy();
736+
dispatchBackgroundFetch = jasmine
737+
.createSpy()
738+
.and.callFake(fakeBackgroundFetch);
739+
spyOn(history, 'replaceState');
740+
await cd.handleDocketDisplayPage();
741+
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
742+
jasmine.objectContaining({
743+
action: 'upload',
744+
data: jasmine.objectContaining({ pacer_case_id: '177277' }),
745+
})
746+
);
747+
});
748+
749+
it('uses the majority goDLS id on merged sheets', async function () {
750+
// Consolidated/MDL dockets legitimately link member cases'
751+
// documents — a stray link must not outvote the sheet.
752+
const cd = new ContentDelegate(
753+
tabId,
754+
docketDisplayUrl,
755+
docketDisplayPath,
756+
'canb',
757+
undefined,
758+
undefined,
759+
[
760+
makeLink('177277', '034031424909'),
761+
makeLink('177277', '034031424910'),
762+
makeLink('177277', '034031424911'),
763+
makeLink('999999', '034031424912'), // stray member-case link
764+
]
765+
);
766+
dispatchBackgroundNotifier = jasmine.createSpy();
767+
dispatchBackgroundFetch = jasmine
768+
.createSpy()
769+
.and.callFake(fakeBackgroundFetch);
770+
spyOn(history, 'replaceState');
771+
await cd.handleDocketDisplayPage();
772+
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
773+
jasmine.objectContaining({
774+
action: 'upload',
775+
data: jasmine.objectContaining({ pacer_case_id: '177277' }),
776+
})
777+
);
778+
});
779+
780+
it('falls back when the goDLS ids are tied', async function () {
781+
// Equal votes for two ids: don't guess; keep the old behavior.
782+
const cd = new ContentDelegate(
783+
tabId,
784+
docketDisplayUrl,
785+
docketDisplayPath,
786+
'canb',
787+
'531591',
788+
undefined,
789+
[
790+
makeLink('177277', '034031424909'),
791+
makeLink('888888', '034031424910'),
792+
]
793+
);
794+
dispatchBackgroundNotifier = jasmine.createSpy();
795+
dispatchBackgroundFetch = jasmine
796+
.createSpy()
797+
.and.callFake(fakeBackgroundFetch);
798+
spyOn(history, 'replaceState');
799+
await cd.handleDocketDisplayPage();
800+
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
801+
jasmine.objectContaining({
802+
action: 'upload',
803+
data: jasmine.objectContaining({ pacer_case_id: '531591' }),
804+
})
805+
);
806+
});
807+
673808
it('calls uploadDocket and responds to a negative result', async function () {
674809
const cd = docketDisplayContentDelegate;
675810
dispatchBackgroundNotifier = jasmine.createSpy();

src/content_delegate.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,12 +269,28 @@ ContentDelegate.prototype.handleDocketDisplayPage = async function () {
269269
// check if appellate
270270
// let isAppellate = PACER.isAppellateCourt(this.court);
271271

272-
// if the content_delegate didn't pull the case Id on initialization,
273-
// check the page for a lead case dktrpt url.
274-
const tabStorage = await getItemsFromStorage(this.tabId);
275-
this.pacer_case_id = this.pacer_case_id
276-
? this.pacer_case_id
277-
: tabStorage.caseId;
272+
// Prefer the case id the page asserts about itself (via its goDLS
273+
// links) over the referrer-derived or tab-cached id — either of those
274+
// can belong to a different case viewed earlier in this tab.
275+
const pageCaseId = PACER.getCaseIdFromDocketDisplayLinks(this.links);
276+
if (pageCaseId) {
277+
if (this.pacer_case_id && this.pacer_case_id !== pageCaseId) {
278+
console.warn(
279+
`RECAP: Not attributing this docket to case ${this.pacer_case_id}: ` +
280+
'its own document links say it belongs to case ' +
281+
`${pageCaseId}. Using ${pageCaseId}.`
282+
);
283+
}
284+
this.pacer_case_id = pageCaseId;
285+
// Refresh the cached id so caseless pages downstream (e.g. attachment
286+
// menus) inherit the right one.
287+
await saveCaseIdinTabStorage({ tabId: this.tabId }, pageCaseId);
288+
} else if (!this.pacer_case_id) {
289+
// if the content_delegate didn't pull the case Id on initialization,
290+
// check the tab storage for a lead case dktrpt url.
291+
const tabStorage = await getItemsFromStorage(this.tabId);
292+
this.pacer_case_id = tabStorage.caseId;
293+
}
278294

279295
// If we don't have this.pacer_case_id at this point, punt.
280296
if (!this.pacer_case_id) return;

src/pacer.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,35 @@ let PACER = {
462462
}
463463
},
464464

465+
// Returns the case id a docket report page asserts about itself: the
466+
// strict majority of the de_caseid values in its document links' goDLS()
467+
// handlers. Majority, because consolidated/MDL sheets legitimately link
468+
// member cases' documents. Returns undefined when there is no goDLS
469+
// evidence or the top ids are tied.
470+
getCaseIdFromDocketDisplayLinks: function (links) {
471+
let tally = {};
472+
for (let i = 0; i < links.length; i++) {
473+
if (!PACER.isDoc1Url(links[i].href)) {
474+
continue;
475+
}
476+
let goDLS = PACER.parseGoDLSFunction(links[i].getAttribute('onclick'));
477+
if (goDLS && goDLS.de_caseid && goDLS.de_caseid !== '0') {
478+
tally[goDLS.de_caseid] = (tally[goDLS.de_caseid] || 0) + 1;
479+
}
480+
}
481+
let best;
482+
let tied = false;
483+
for (let caseId of Object.keys(tally)) {
484+
if (best === undefined || tally[caseId] > tally[best]) {
485+
best = caseId;
486+
tied = false;
487+
} else if (tally[caseId] === tally[best]) {
488+
tied = true;
489+
}
490+
}
491+
return tied ? undefined : best;
492+
},
493+
465494
// Given a URL that satisfies isDocketQueryUrl, gets its case number.
466495
getCaseNumberFromUrls: function (urls) {
467496
// Iterate over an array of URLs and get the case number from the

0 commit comments

Comments
 (0)