@internetarchive/bookreader 5.0.0-48-alpha2 → 5.0.0-48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -82,6 +82,8 @@ const initializeBookReader = (brManifest) => {
82
82
  // we expect this at the global level
83
83
  BookReaderJSIAinit(brManifest.data, options);
84
84
 
85
+ const isRestricted = brManifest.data.isRestricted;
86
+ window.dispatchEvent(new CustomEvent('contextmenu', { detail: { isRestricted } }));
85
87
  if (customAutoflipParams.autoflip) {
86
88
  br.autoToggle(customAutoflipParams);
87
89
  }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ # 5.0.0-48
2
+ - Fix: move analytics to sample bucket @iisa
3
+ - Dev: update dependencies (concurrently, jest) @renovate
4
+
1
5
  # 5.0.0-47
2
6
  - Fix: XSS vulnerability in search results @latonv
3
7
  - Dev: Update jQuery to v3 **BREAKING** @cdrini
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/bookreader",
3
- "version": "5.0.0-48-alpha2",
3
+ "version": "5.0.0-48",
4
4
  "description": "The Internet Archive BookReader.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -449,10 +449,10 @@ export class BookNavigator extends LitElement {
449
449
  /** Display an element's context menu */
450
450
  manageContextMenuVisibility(e) {
451
451
  if (window.archive_analytics) {
452
- window.archive_analytics?.send_event_no_sampling(
452
+ window.archive_analytics?.send_event(
453
453
  'BookReader',
454
454
  `contextmenu-${this.bookIsRestricted ? 'restricted' : 'unrestricted'}`,
455
- e.target.classList.value
455
+ e.target?.classList?.value
456
456
  );
457
457
  }
458
458
  if (!this.bookIsRestricted) {
@@ -2,16 +2,46 @@ import { html } from 'lit';
2
2
  import '@internetarchive/icon-dl/icon-dl';
3
3
  import './downloads';
4
4
 
5
+ const menuBase = {
6
+ pdf: {
7
+ type: 'Encrypted Adobe PDF',
8
+ url: '#',
9
+ note: 'PDF files contain high quality images of pages.',
10
+ },
11
+ lcppdf: {
12
+ type: 'Get LCP PDF',
13
+ url: '#',
14
+ note: 'PDF files contain high quality images of pages.',
15
+ },
16
+ lcpepub: {
17
+ type: 'Get LCP ePub',
18
+ url: '#',
19
+ note: 'ePub files are smaller in size, but may contain errors.',
20
+ },
21
+ epub: {
22
+ type: 'Encrypted Adobe ePub',
23
+ url: '#',
24
+ note: 'ePub files are smaller in size, but may contain errors.',
25
+ },
26
+ };
27
+
28
+ const publicMenuBase = {
29
+ pdf: "PDF",
30
+ epub: "ePub",
31
+ lcppdf: "LCP PDF",
32
+ lcpepub: "LCP ePub",
33
+ };
34
+
5
35
  export default class DownloadsProvider {
6
36
 
7
37
  constructor({ bookreader }) {
8
38
  this.icon = html`<ia-icon-dl style="width: var(--iconWidth); height: var(--iconHeight);"></ia-icon-dl>`;
9
- this.label = 'Read offline';
39
+ this.label = 'Downloadable files';
10
40
  this.menuDetails = '';
11
41
  this.downloads = [];
12
42
  this.id = 'downloads';
13
43
  this.component = '';
14
- this.isBookProtected = bookreader?.options?.protected ?? false;
44
+ this.isBookProtected = bookreader?.options?.isProtected || false;
15
45
  }
16
46
 
17
47
  update(downloadTypes) {
@@ -24,21 +54,23 @@ export default class DownloadsProvider {
24
54
  }
25
55
 
26
56
  /**
27
- * Restructures available download type data for the renderer
28
- * Sets global `downloads`
57
+ * Generates Download Menu Info for available types
58
+ * sets global `downloads`
29
59
  * @param availableTypes
30
60
  */
31
61
  computeAvailableTypes(availableTypes = []) {
32
62
  const menuData = availableTypes.reduce((found, incoming = []) => {
33
63
  const [ type = '', link = '' ] = incoming;
34
- if (!type) return found;
35
- let formattedType = type.toLowerCase();
36
- if ((formattedType === 'pdf' || formattedType === 'epub') && this.isBookProtected) {
37
- formattedType = `adobe${formattedType}`;
64
+ const formattedType = type.toLowerCase();
65
+ const downloadOption = menuBase[formattedType] || null;
66
+
67
+ if (downloadOption) {
68
+ const menuButtonText = this.isBookProtected ? menuBase[formattedType].type : publicMenuBase[formattedType];
69
+ const menuInfo = Object.assign({}, downloadOption, { url: link, type: menuButtonText});
70
+ found.push(menuInfo);
38
71
  }
39
- found[formattedType] = link;
40
72
  return found;
41
- }, {});
73
+ }, []);
42
74
 
43
75
  this.downloads = menuData;
44
76
  }
@@ -1,6 +1,5 @@
1
1
  import { css, html, LitElement, nothing } from 'lit';
2
2
  import buttonStyles from '../assets/button-base.js';
3
-
4
3
  export class IABookDownloads extends LitElement {
5
4
  static get properties() {
6
5
  return {
@@ -19,26 +18,6 @@ export class IABookDownloads extends LitElement {
19
18
  this.isBookProtected = false;
20
19
  }
21
20
 
22
- render() {
23
- return html`
24
- ${this.header}
25
- ${this.loanExpiryMessage}
26
- ${this.renderDownloadOptions()}
27
- `;
28
- }
29
-
30
- get header() {
31
- if (!this.renderHeader) {
32
- return nothing;
33
- }
34
- return html`
35
- <header>
36
- <h3>Read offline</h3>
37
- ${this.formatsCount}
38
- </header>
39
- `;
40
- }
41
-
42
21
  get formatsCount() {
43
22
  const count = this.downloads.length;
44
23
  return count ? html`<p>${count} format${count > 1 ? 's' : ''}</p>` : html``;
@@ -51,80 +30,65 @@ export class IABookDownloads extends LitElement {
51
30
  }
52
31
 
53
32
  renderDownloadOptions() {
54
- const downloadOptions = [];
55
- ['pdf', 'epub', 'lcppdf', 'lcpepub', 'adobepdf', 'adobeepub'].forEach(format => {
56
- if (this.downloads[format]) {
57
- downloadOptions.push(this.downloadOption(format, this.downloads[format]));
58
- }
59
- });
60
- return html`
61
- <ul>
62
- ${downloadOptions}
63
- </ul>
64
- `;
65
- }
66
-
67
- downloadOption(format, link) {
68
- if (/^adobe/.test(format)) {
69
- return html`
33
+ return this.downloads.map(option => (
34
+ html`
70
35
  <li>
71
- <a
72
- href="${link}"
73
- data-event-click-tracking="BookReader|Download-${format}"
74
- >${this.menuText[format].linkText}</a>
75
- ${this.menuText[format].message ?? nothing}
36
+ <a class="ia-button link primary" href="${option.url}">Get ${option.type}</a>
37
+ ${option.note ? html`<p>${option.note}</p>` : html``}
76
38
  </li>
77
- `;
39
+ `
40
+ ));
41
+ }
42
+
43
+ /**
44
+ * checks if downloads list contains an LCP option
45
+ * @return {boolean}
46
+ */
47
+ get hasLCPOption() {
48
+ const regex = /^(LCP)/g;
49
+ const lcpAvailable = this.downloads.some(option => option.type?.match(regex));
50
+ return lcpAvailable;
51
+ }
52
+
53
+ get header() {
54
+ if (!this.renderHeader) {
55
+ return nothing;
78
56
  }
79
57
  return html`
80
- <li>
81
- <a class="ia-button link primary"
82
- href="${link}"
83
- data-event-click-tracking="BookReader|Download-${format}"
84
- >${this.menuText[format].linkText}</a>
85
- ${this.menuText[format].message ? html`<p>${this.menuText[format].message}</p>` : nothing}
86
- </li>
58
+ <header>
59
+ <h3>Downloadable files</h3>
60
+ ${this.formatsCount}
61
+ </header>
87
62
  `;
88
63
  }
89
64
 
90
- get menuText() {
91
- return {
92
- pdf: {
93
- linkText: 'Get high-resolution PDF',
94
- message: html``,
95
- },
96
- epub: {
97
- linkText: 'Get text-based ebook',
98
- message: html`Smaller size. May contain some errors.`,
99
- },
100
- lcppdf: {
101
- linkText: 'Get high-resolution PDF',
102
- message: this.lcpNote,
103
- },
104
- lcpepub: {
105
- linkText: 'Get text-based ebook',
106
- message: html`Smaller size. May contain some errors. ${this.lcpNote}`,
107
- },
108
- adobepdf: {
109
- linkText:'Get legacy Adobe DRM PDF version here.',
110
- message: this.adobeNote,
111
- },
112
- adobeepub: {
113
- linkText: 'Get legacy Adobe DRM EPUB version here.',
114
- message: this.adobeNote,
115
- },
116
- };
117
- };
118
-
119
- get lcpNote() {
65
+ get accessProtectedBook() {
120
66
  return html`
121
- Requires having an LCP-compatible e-reader installed like Aldiko Next (<a href="https://apps.apple.com/us/app/aldiko-next/id1476410111">iOS</a>, <a href="https://play.google.com/store/apps/details?id=com.aldiko.android">Android</a>) or <a href="https://www.edrlab.org/software/thorium-reader/">Thorium</a>.
67
+ <p>To access downloaded books, you need Adobe-compliant software on your device. The Internet Archive will administer this loan, but Adobe may also collect some information.</p>
68
+ <a class="ia-button external primary" href="https://www.adobe.com/solutions/ebook/digital-editions/download.html" rel="noopener noreferrer" target="_blank">Install Adobe Digital Editions</a>
122
69
  `;
123
70
  }
124
71
 
125
- get adobeNote() {
72
+ get installSimplyEAldikoThoriumMsg() {
73
+ return html`
74
+ <p>For LCP downloads, make sure you have SimplyE or Aldiko Next installed on mobile or Thorium on desktop.</p>
75
+ <ul>
76
+ <li><a href="https://librarysimplified.org/simplye/" rel="noopener noreferrer nofollow" target="_blank">Install SimplyE</a></li>
77
+ <li><a href="https://www.demarque.com/en-aldiko" rel="noopener noreferrer nofollow" target="_blank">Install Aldiko</a></li>
78
+ <li><a href="https://www.edrlab.org/software/thorium-reader/" rel="noopener noreferrer nofollow" target="_blank">Install Thorium</a></li>
79
+ </ul>
80
+ `;
81
+ }
82
+
83
+ render() {
126
84
  return html`
127
- Requires a compatible e-reader like <a hef="https://www.adobe.com/solutions/ebook/digital-editions.html">Adobe Digital Editions</a>.
85
+ ${this.header}
86
+ ${this.loanExpiryMessage}
87
+ <ul>${this.renderDownloadOptions()}</ul>
88
+ ${this.hasLCPOption
89
+ ? this.installSimplyEAldikoThoriumMsg
90
+ : (this.isBookProtected ? this.accessProtectedBook : nothing)
91
+ }
128
92
  `;
129
93
  }
130
94
 
@@ -104,7 +104,7 @@ export default class VolumesProvider {
104
104
  if (!window.archive_analytics) {
105
105
  return;
106
106
  }
107
- window.archive_analytics?.send_event_no_sampling(
107
+ window.archive_analytics?.send_event(
108
108
  'BookReader',
109
109
  `VolumesSort|${orderBy}`,
110
110
  window.location.path,
File without changes
@@ -46,6 +46,13 @@ window.ResizeObserver = class ResizeObserver {
46
46
  disconnect = sinon.fake()
47
47
  };
48
48
 
49
+ beforeEach(() => {
50
+ window.archive_analytics = {
51
+ send_event_no_sampling: sinon.fake(),
52
+ send_event: sinon.fake()
53
+ };
54
+ });
55
+
49
56
  afterEach(() => {
50
57
  window.br = null;
51
58
  fixtureCleanup();
@@ -516,8 +523,6 @@ describe('<book-navigator>', () => {
516
523
  describe('Handles Restricted Books', () => {
517
524
  describe('contextMenu is prevented when book is restricted', () => {
518
525
  it('watches on `div.BRscreen`', async () => {
519
- window.archive_analytics = { send_event_no_sampling: sinon.fake() };
520
-
521
526
  const el = fixtureSync(container());
522
527
  const brStub = {
523
528
  options: { restricted: true },
@@ -529,9 +534,8 @@ describe('<book-navigator>', () => {
529
534
  const elSpy = sinon.spy(el.manageContextMenuVisibility);
530
535
  await el.elementUpdated;
531
536
 
532
- expect(window.archive_analytics.send_event_no_sampling.called).toEqual(
533
- false
534
- );
537
+ expect(window.archive_analytics.send_event_no_sampling.called).toEqual(false);
538
+ expect(window.archive_analytics.send_event.called).toEqual(false);
535
539
  expect(elSpy.called).toEqual(false);
536
540
 
537
541
  const body = document.querySelector('body');
@@ -550,14 +554,13 @@ describe('<book-navigator>', () => {
550
554
 
551
555
  // analytics fires
552
556
  expect(window.archive_analytics.send_event_no_sampling.called).toEqual(
553
- true
557
+ false
554
558
  );
559
+ expect(window.archive_analytics.send_event.called).toEqual(true);
555
560
  // we prevent default
556
561
  expect(preventDefaultSpy.called).toEqual(true);
557
562
  });
558
563
  it('watches on `img.BRpageimage`', async () => {
559
- window.archive_analytics = { send_event_no_sampling: sinon.fake() };
560
-
561
564
  const el = fixtureSync(container());
562
565
  const brStub = {
563
566
  options: { restricted: true },
@@ -568,9 +571,8 @@ describe('<book-navigator>', () => {
568
571
 
569
572
  await el.elementUpdated;
570
573
 
571
- expect(window.archive_analytics.send_event_no_sampling.called).toEqual(
572
- false
573
- );
574
+ expect(window.archive_analytics.send_event_no_sampling.called).toEqual(false);
575
+ expect(window.archive_analytics.send_event.called).toEqual(false);
574
576
 
575
577
  const body = document.querySelector('body');
576
578
  // const element stub for img.BRpageimage
@@ -586,14 +588,13 @@ describe('<book-navigator>', () => {
586
588
  imgBRpageimage.dispatchEvent(contextMenuEvent);
587
589
 
588
590
  // analytics fires
589
- expect(window.archive_analytics.send_event_no_sampling.called).toEqual(true);
591
+ expect(window.archive_analytics.send_event_no_sampling.called).toEqual(false);
592
+ expect(window.archive_analytics.send_event.called).toEqual(true);
590
593
  // we prevent default
591
594
  expect(preventDefaultSpy.called).toEqual(true);
592
595
  });
593
596
  });
594
597
  it('Allows unrestricted books access to context menu', async () => {
595
- window.archive_analytics = { send_event_no_sampling: sinon.fake() };
596
-
597
598
  const el = fixtureSync(container());
598
599
  const brStub = {
599
600
  options: { restricted: false },
@@ -607,6 +608,8 @@ describe('<book-navigator>', () => {
607
608
  expect(window.archive_analytics.send_event_no_sampling.called).toEqual(
608
609
  false
609
610
  );
611
+ expect(window.archive_analytics.send_event.called).toEqual(false);
612
+
610
613
 
611
614
  const body = document.querySelector('body');
612
615
  // const element stub for img.BRpageimage
@@ -622,7 +625,8 @@ describe('<book-navigator>', () => {
622
625
  imgBRpageimage.dispatchEvent(contextMenuEvent);
623
626
 
624
627
  // analytics fires
625
- expect(window.archive_analytics.send_event_no_sampling.called).toEqual(true);
628
+ expect(window.archive_analytics.send_event_no_sampling.called).toEqual(false);
629
+ expect(window.archive_analytics.send_event.called).toEqual(true);
626
630
  // we do not prevent default
627
631
  expect(preventDefaultSpy.called).toEqual(false);
628
632
  });