nodebb-plugin-pdf-secure2 1.4.3 → 1.5.0

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.
@@ -888,6 +888,84 @@
888
888
  border: 1px solid rgba(220, 38, 38, 0.25);
889
889
  }
890
890
 
891
+ /* AI message action buttons (copy, regenerate) */
892
+ .chatMsgActions {
893
+ display: flex;
894
+ gap: 8px;
895
+ margin-top: 6px;
896
+ opacity: 0;
897
+ transition: opacity 0.15s;
898
+ }
899
+ .chatMsg.ai:hover .chatMsgActions { opacity: 1; }
900
+ .chatMsgAction {
901
+ background: none;
902
+ border: none;
903
+ color: var(--text-secondary);
904
+ font-size: 11px;
905
+ cursor: pointer;
906
+ display: flex;
907
+ align-items: center;
908
+ gap: 3px;
909
+ padding: 2px 4px;
910
+ border-radius: 4px;
911
+ transition: color 0.15s, background 0.15s;
912
+ }
913
+ .chatMsgAction:hover { color: var(--text-primary); background: rgba(255,255,255,0.08); }
914
+ .chatMsgAction.copied { color: #4ade80; }
915
+ .chatMsgAction svg { width: 12px; height: 12px; fill: currentColor; }
916
+
917
+ /* AI badge timestamp */
918
+ .aiBadgeTime { font-weight: 400; opacity: 0.6; }
919
+
920
+ /* Scroll to bottom button */
921
+ .chatScrollDown {
922
+ position: absolute;
923
+ bottom: 80px;
924
+ left: 50%;
925
+ transform: translateX(-50%);
926
+ width: 32px; height: 32px;
927
+ border-radius: 50%;
928
+ background: var(--bg-secondary);
929
+ border: 1px solid var(--border-color);
930
+ color: var(--text-primary);
931
+ font-size: 16px;
932
+ cursor: pointer;
933
+ opacity: 0;
934
+ pointer-events: none;
935
+ transition: opacity 0.2s;
936
+ z-index: 5;
937
+ display: flex;
938
+ align-items: center;
939
+ justify-content: center;
940
+ }
941
+ .chatScrollDown:hover { background: var(--bg-tertiary); }
942
+ .chatScrollDown.visible { opacity: 1; pointer-events: auto; }
943
+
944
+ /* Blockquote in AI messages */
945
+ .chatMsg blockquote {
946
+ border-left: 3px solid var(--accent);
947
+ margin: 6px 0;
948
+ padding: 4px 10px;
949
+ color: var(--text-secondary);
950
+ font-style: italic;
951
+ background: rgba(255,255,255,0.03);
952
+ border-radius: 0 4px 4px 0;
953
+ }
954
+ body[data-tier="vip"] .chatMsg blockquote { border-left-color: #ffd700; }
955
+
956
+ /* Table overflow scroll hint */
957
+ .chatMsg .tableWrap.scrollable::after {
958
+ content: '';
959
+ position: absolute;
960
+ top: 0; right: 0; bottom: 0;
961
+ width: 20px;
962
+ background: linear-gradient(to right, transparent, var(--bg-tertiary));
963
+ pointer-events: none;
964
+ }
965
+
966
+ /* Loading elapsed timer */
967
+ .chatLoadingElapsed { font-size: 10px; color: var(--text-secondary); opacity: 0.5; margin-left: 4px; }
968
+
891
969
  /* Injection warning banner */
892
970
  .chatInjectionWarning {
893
971
  background: rgba(255, 170, 0, 0.12);
@@ -1004,6 +1082,30 @@
1004
1082
  }
1005
1083
  body[data-tier="vip"] .vipTipsBanner { display: block; }
1006
1084
 
1085
+ /* Detail mode toggle */
1086
+ .vipTipsRow { display: flex; justify-content: space-between; align-items: center; }
1087
+ .detailToggle { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
1088
+ .detailToggle input { display: none; }
1089
+ .detailToggleSlider {
1090
+ width: 28px; height: 16px;
1091
+ background: rgba(255,255,255,0.15);
1092
+ border-radius: 8px;
1093
+ position: relative;
1094
+ transition: background 0.2s;
1095
+ }
1096
+ .detailToggleSlider::after {
1097
+ content: '';
1098
+ position: absolute; left: 2px; top: 2px;
1099
+ width: 12px; height: 12px;
1100
+ background: var(--text-secondary);
1101
+ border-radius: 50%;
1102
+ transition: transform 0.2s, background 0.2s;
1103
+ }
1104
+ .detailToggle input:checked + .detailToggleSlider { background: rgba(255,215,0,0.3); }
1105
+ .detailToggle input:checked + .detailToggleSlider::after { transform: translateX(12px); background: #ffd700; }
1106
+ .detailToggleText { font-size: 10px; color: var(--text-secondary); font-weight: 500; transition: color 0.2s; }
1107
+ .detailToggle input:checked ~ .detailToggleText { color: #ffd700; }
1108
+
1007
1109
  /* Suggestion chips */
1008
1110
  .chatSuggestions {
1009
1111
  display: flex;
@@ -3011,7 +3113,14 @@
3011
3113
  <button class="closeBtn" id="closeChatSidebar">&times;</button>
3012
3114
  </div>
3013
3115
  <div class="vipTipsBanner" id="vipTipsBanner">
3014
- <div class="vipTipsLabel">VIP — Benden isteyebileceklerin:</div>
3116
+ <div class="vipTipsRow">
3117
+ <div class="vipTipsLabel">VIP — Benden isteyebileceklerin:</div>
3118
+ <label class="detailToggle" id="detailToggleLabel">
3119
+ <input type="checkbox" id="detailToggle">
3120
+ <span class="detailToggleSlider"></span>
3121
+ <span class="detailToggleText">Detaylı</span>
3122
+ </label>
3123
+ </div>
3015
3124
  <div class="vipTipsItems">
3016
3125
  <span class="vipTip">detayli aciklama</span>
3017
3126
  <span class="vipTip">ornek ver</span>
@@ -3021,6 +3130,7 @@
3021
3130
  </div>
3022
3131
  </div>
3023
3132
  <div id="chatMessages"></div>
3133
+ <button id="chatScrollDown" class="chatScrollDown">&#x2193;</button>
3024
3134
  <div class="chatCharCount" id="chatCharCount"></div>
3025
3135
  <div class="chatQuotaBar" id="chatQuotaBar">
3026
3136
  <div class="chatQuotaTrack"><div class="chatQuotaFill" id="chatQuotaFill"></div></div>
@@ -3089,6 +3199,18 @@
3089
3199
  return originalToBlob.apply(this, arguments);
3090
3200
  };
3091
3201
 
3202
+ // Block getImageData for PDF page canvases (prevents raw pixel extraction)
3203
+ const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
3204
+ CanvasRenderingContext2D.prototype.getImageData = function () {
3205
+ if (this.canvas && this.canvas.closest && this.canvas.closest('.page') && this.canvas.closest('#viewerContainer')) {
3206
+ // Return blank ImageData of requested size
3207
+ var w = arguments[2] || 1;
3208
+ var h = arguments[3] || 1;
3209
+ return new ImageData(w, h);
3210
+ }
3211
+ return originalGetImageData.apply(this, arguments);
3212
+ };
3213
+
3092
3214
  pdfjsLib.GlobalWorkerOptions.workerSrc = '';
3093
3215
 
3094
3216
  // State - now private, not accessible from console
@@ -3651,38 +3773,9 @@
3651
3773
  }
3652
3774
 
3653
3775
  try {
3654
- // ============================================
3655
- // SPA CACHE - Check if parent has cached buffer
3656
- // ============================================
3657
3776
  let pdfBuffer = null;
3658
3777
 
3659
- if (window.parent && window.parent !== window) {
3660
- // Request cached buffer from parent
3661
- const cachePromise = new Promise((resolve) => {
3662
- const handler = (event) => {
3663
- if (event.data && event.data.type === 'pdf-secure-cache-response' && event.data.filename === config.filename) {
3664
- window.removeEventListener('message', handler);
3665
- resolve(event.data.buffer);
3666
- }
3667
- };
3668
- window.addEventListener('message', handler);
3669
-
3670
- // Timeout after 100ms
3671
- setTimeout(() => {
3672
- window.removeEventListener('message', handler);
3673
- resolve(null);
3674
- }, 100);
3675
-
3676
- window.parent.postMessage({ type: 'pdf-secure-cache-request', filename: config.filename }, window.location.origin);
3677
- });
3678
-
3679
- pdfBuffer = await cachePromise;
3680
- if (pdfBuffer) {
3681
- }
3682
- }
3683
-
3684
- // If no cache, fetch from server
3685
- if (!pdfBuffer) {
3778
+ {
3686
3779
  // Nonce and key are embedded in HTML config (not fetched from API)
3687
3780
  const nonce = config.nonce;
3688
3781
  const decryptKey = config.dk;
@@ -3698,23 +3791,12 @@
3698
3791
 
3699
3792
  const encodedBuffer = await pdfRes.arrayBuffer();
3700
3793
 
3701
- // Decrypt AES-256-GCM encrypted data
3702
- if (decryptKey && decryptIv) {
3703
- pdfBuffer = await aesGcmDecode(encodedBuffer, decryptKey, decryptIv);
3704
- } else {
3705
- pdfBuffer = encodedBuffer;
3794
+ // Decrypt AES-256-GCM encrypted data (refuse to load without encryption)
3795
+ if (!decryptKey || !decryptIv) {
3796
+ throw new Error('Missing encryption parameters');
3706
3797
  }
3798
+ pdfBuffer = await aesGcmDecode(encodedBuffer, decryptKey, decryptIv);
3707
3799
 
3708
- // Send buffer to parent for caching (premium only - non-premium must not leak decoded buffer)
3709
- if ((_cfg.isPremium !== false || _cfg.isLite) && window.parent && window.parent !== window) {
3710
- // Clone buffer for parent (we keep original)
3711
- const bufferCopy = pdfBuffer.slice(0);
3712
- window.parent.postMessage({
3713
- type: 'pdf-secure-buffer',
3714
- filename: config.filename,
3715
- buffer: bufferCopy
3716
- }, window.location.origin, [bufferCopy]); // Transferable
3717
- }
3718
3800
  }
3719
3801
 
3720
3802
 
@@ -3758,6 +3840,18 @@
3758
3840
  };
3759
3841
  }
3760
3842
 
3843
+ // Security: Wipe decryption keys from config to prevent memory inspection
3844
+ if (config) {
3845
+ config.dk = null;
3846
+ config.iv = null;
3847
+ config.nonce = null;
3848
+ }
3849
+ if (_cfg) {
3850
+ _cfg.dk = null;
3851
+ _cfg.iv = null;
3852
+ _cfg.nonce = null;
3853
+ }
3854
+
3761
3855
 
3762
3856
  } catch (err) {
3763
3857
 
@@ -3962,6 +4056,17 @@
3962
4056
  const chatIsPremium = _cfg && _cfg.isPremium;
3963
4057
  const chatIsVip = _cfg && _cfg.isVip;
3964
4058
 
4059
+ // Detail mode toggle (VIP only)
4060
+ var chatDetailMode = false;
4061
+ var detailToggleEl = document.getElementById('detailToggle');
4062
+ if (chatIsVip && detailToggleEl) {
4063
+ try { chatDetailMode = localStorage.getItem('pdfChat_detailMode') === 'true'; detailToggleEl.checked = chatDetailMode; } catch (e) { /* localStorage unavailable */ }
4064
+ detailToggleEl.addEventListener('change', function () {
4065
+ chatDetailMode = detailToggleEl.checked;
4066
+ try { localStorage.setItem('pdfChat_detailMode', String(chatDetailMode)); } catch (e) { /* ignore */ }
4067
+ });
4068
+ }
4069
+
3965
4070
  // Show chat button if chatEnabled (visible to all users, not just premium)
3966
4071
  if (_cfg && _cfg.chatEnabled) {
3967
4072
  chatBtnEl.style.display = '';
@@ -4053,6 +4158,18 @@
4053
4158
 
4054
4159
  chatBtnEl.onclick = toggleChat;
4055
4160
 
4161
+ // Scroll-to-bottom button
4162
+ var chatScrollDownBtn = document.getElementById('chatScrollDown');
4163
+ if (chatScrollDownBtn) {
4164
+ chatMessagesEl.addEventListener('scroll', function () {
4165
+ var atBottom = chatMessagesEl.scrollHeight - chatMessagesEl.scrollTop - chatMessagesEl.clientHeight < 60;
4166
+ chatScrollDownBtn.classList.toggle('visible', !atBottom);
4167
+ });
4168
+ chatScrollDownBtn.onclick = function () {
4169
+ chatMessagesEl.scrollTo({ top: chatMessagesEl.scrollHeight, behavior: 'smooth' });
4170
+ };
4171
+ }
4172
+
4056
4173
  // VIP tip chips — click to populate chat input
4057
4174
  var vipTipChips = document.querySelectorAll('.vipTip');
4058
4175
  var vipTipPrompts = {
@@ -4067,7 +4184,7 @@
4067
4184
  var prompt = vipTipPrompts[chip.textContent.trim()];
4068
4185
  if (prompt && chatInputEl) {
4069
4186
  chatInputEl.value = prompt;
4070
- chatInputEl.focus();
4187
+ sendChatMessage();
4071
4188
  }
4072
4189
  };
4073
4190
  });
@@ -4172,6 +4289,17 @@
4172
4289
  i++; continue;
4173
4290
  }
4174
4291
 
4292
+ // Blockquote: > text
4293
+ if (trimmed.startsWith('> ')) {
4294
+ flushPara();
4295
+ var bq = document.createElement('blockquote');
4296
+ var bqTokens = tokenizeInline(trimmed.slice(2));
4297
+ for (var t = 0; t < bqTokens.length; t++) bq.appendChild(bqTokens[t]);
4298
+ parent.appendChild(bq);
4299
+ hasOutput = true;
4300
+ i++; continue;
4301
+ }
4302
+
4175
4303
  // Table: lines containing | with a separator row
4176
4304
  if (trimmed.includes('|') && i + 1 < lines.length) {
4177
4305
  var tLines = [];
@@ -4189,6 +4317,14 @@
4189
4317
  wrap.appendChild(table);
4190
4318
  wrap.appendChild(createCopyBtn(tableToText(table)));
4191
4319
  parent.appendChild(wrap);
4320
+ // Detect horizontal overflow for scroll hint
4321
+ requestAnimationFrame(function () {
4322
+ if (wrap.scrollWidth > wrap.clientWidth) wrap.classList.add('scrollable');
4323
+ });
4324
+ wrap.addEventListener('scroll', function () {
4325
+ var atEnd = wrap.scrollLeft + wrap.clientWidth >= wrap.scrollWidth - 5;
4326
+ wrap.classList.toggle('scrollable', !atEnd);
4327
+ });
4192
4328
  hasOutput = true;
4193
4329
  i = j; continue;
4194
4330
  }
@@ -4451,6 +4587,18 @@
4451
4587
  continue;
4452
4588
  }
4453
4589
  }
4590
+ // Strikethrough: ~~...~~
4591
+ if (line[i] === '~' && line[i + 1] === '~') {
4592
+ var endS = line.indexOf('~~', i + 2);
4593
+ if (endS !== -1 && endS - i < 5002) {
4594
+ flushBuf();
4595
+ var del = document.createElement('del');
4596
+ del.textContent = line.slice(i + 2, endS);
4597
+ nodes.push(del);
4598
+ i = endS + 2;
4599
+ continue;
4600
+ }
4601
+ }
4454
4602
  buf += line[i];
4455
4603
  i++;
4456
4604
  }
@@ -4472,6 +4620,11 @@
4472
4620
  svg.appendChild(path);
4473
4621
  badge.appendChild(svg);
4474
4622
  badge.appendChild(document.createTextNode(' AI'));
4623
+ var ts = document.createElement('span');
4624
+ ts.className = 'aiBadgeTime';
4625
+ var now = new Date();
4626
+ ts.textContent = ' \u2022 ' + now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
4627
+ badge.appendChild(ts);
4475
4628
  return badge;
4476
4629
  }
4477
4630
 
@@ -4487,6 +4640,32 @@
4487
4640
  }
4488
4641
  msg.appendChild(createAiBadge());
4489
4642
  msg.appendChild(renderMarkdownSafe(text));
4643
+
4644
+ // Action buttons (regenerate)
4645
+ var actions = document.createElement('div');
4646
+ actions.className = 'chatMsgActions';
4647
+
4648
+ var regenBtn = document.createElement('button');
4649
+ regenBtn.className = 'chatMsgAction';
4650
+ regenBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> Tekrar sor';
4651
+ regenBtn.onclick = function () {
4652
+ if (chatHistory.length >= 2) {
4653
+ var lastQ = chatHistory[chatHistory.length - 2];
4654
+ if (lastQ.role === 'user') {
4655
+ chatHistory.pop();
4656
+ chatHistory.pop();
4657
+ var msgs = chatMessagesEl.querySelectorAll('.chatMsg');
4658
+ if (msgs.length >= 2) {
4659
+ msgs[msgs.length - 1].remove();
4660
+ msgs[msgs.length - 2].remove();
4661
+ }
4662
+ chatInputEl.value = lastQ.text;
4663
+ sendChatMessage();
4664
+ }
4665
+ }
4666
+ };
4667
+ actions.appendChild(regenBtn);
4668
+ msg.appendChild(actions);
4490
4669
  } else if (role === 'user') {
4491
4670
  var now = new Date();
4492
4671
  var timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
@@ -4504,18 +4683,34 @@
4504
4683
  }
4505
4684
 
4506
4685
  function showChatLoading() {
4507
- const msg = document.createElement('div');
4686
+ var loadText = chatDetailMode ? 'Detayl\u0131 analiz haz\u0131rlan\u0131yor' : 'D\u00fc\u015f\u00fcn\u00fcyor';
4687
+ var msg = document.createElement('div');
4508
4688
  msg.className = 'chatMsg ai chatLoading';
4509
4689
  msg.id = 'chatLoadingMsg';
4510
- msg.innerHTML = '<span class="chatLoadingText">Düşünüyor</span><span class="chatLoadingDots"><span>.</span><span>.</span><span>.</span></span>';
4690
+ msg.innerHTML = '<span class="chatLoadingText">' + loadText + '</span><span class="chatLoadingDots"><span>.</span><span>.</span><span>.</span></span>';
4691
+ var elapsed = document.createElement('span');
4692
+ elapsed.className = 'chatLoadingElapsed';
4693
+ elapsed.style.display = 'none';
4694
+ msg.appendChild(elapsed);
4695
+ var startTime = Date.now();
4696
+ msg._timer = setInterval(function () {
4697
+ var secs = Math.floor((Date.now() - startTime) / 1000);
4698
+ if (secs >= 5) {
4699
+ elapsed.style.display = '';
4700
+ elapsed.textContent = '(' + secs + 's)';
4701
+ }
4702
+ }, 1000);
4511
4703
  chatMessagesEl.appendChild(msg);
4512
4704
  chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
4513
4705
  return msg;
4514
4706
  }
4515
4707
 
4516
4708
  function removeChatLoading() {
4517
- const el = document.getElementById('chatLoadingMsg');
4518
- if (el) el.remove();
4709
+ var el = document.getElementById('chatLoadingMsg');
4710
+ if (el) {
4711
+ if (el._timer) clearInterval(el._timer);
4712
+ el.remove();
4713
+ }
4519
4714
  }
4520
4715
 
4521
4716
  // Quota usage bar
@@ -4587,7 +4782,7 @@
4587
4782
  var existingChips = suggestions.querySelectorAll('.chatSuggestionChip');
4588
4783
  existingChips.forEach(function (c) { c.classList.add('loading'); });
4589
4784
 
4590
- fetch((_cfg.relativePath || '') + '/api/v3/plugins/pdf-secure/suggestions?filename=' + encodeURIComponent(_cfg.filename), {
4785
+ fetch((_cfg.relativePath || '') + '/api/v3/plugins/pdf-secure/suggestions?filename=' + encodeURIComponent(_cfg.filename) + '&tid=' + encodeURIComponent(_cfg.tid || 0), {
4591
4786
  headers: { 'x-csrf-token': _cfg.csrfToken || '' },
4592
4787
  })
4593
4788
  .then(function (r) { return r.json(); })
@@ -4626,9 +4821,14 @@
4626
4821
  suggestions.appendChild(chip);
4627
4822
  });
4628
4823
 
4629
- // Trigger AI suggestions on hover/focus of the suggestions area (lazy)
4630
- suggestions.addEventListener('mouseenter', loadAiSuggestions, { once: true });
4631
- suggestions.addEventListener('focusin', loadAiSuggestions, { once: true });
4824
+ // VIP: auto-load context-aware suggestions immediately
4825
+ // Premium: lazy-load on hover/focus (avoids unnecessary API calls)
4826
+ if (chatIsVip) {
4827
+ setTimeout(loadAiSuggestions, 0);
4828
+ } else {
4829
+ suggestions.addEventListener('mouseenter', loadAiSuggestions, { once: true });
4830
+ suggestions.addEventListener('focusin', loadAiSuggestions, { once: true });
4831
+ }
4632
4832
 
4633
4833
  chatMessagesEl.appendChild(suggestions);
4634
4834
  chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
@@ -4813,8 +5013,10 @@
4813
5013
  },
4814
5014
  body: JSON.stringify({
4815
5015
  filename: _cfg.filename,
5016
+ tid: _cfg.tid || 0,
4816
5017
  question: question,
4817
5018
  history: chatHistory,
5019
+ detailMode: chatDetailMode,
4818
5020
  }),
4819
5021
  });
4820
5022