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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  });