Compare commits
6 Commits
iblogs-boo
...
cc1e853f89
| Author | SHA1 | Date | |
|---|---|---|---|
| cc1e853f89 | |||
| 0282ab594e | |||
| 8437d62394 | |||
| 4fced60a83 | |||
| 33fcd0d81f | |||
| f72e2d0936 |
@@ -2,6 +2,7 @@ FROM dunglas/frankenphp:1-php8.5
|
|||||||
|
|
||||||
# System Setup
|
# System Setup
|
||||||
RUN install-php-extensions mongodb zip
|
RUN install-php-extensions mongodb zip
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ARG USER=iblogs
|
ARG USER=iblogs
|
||||||
RUN useradd ${USER} && \
|
RUN useradd ${USER} && \
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"ext-mongodb": "*",
|
"ext-mongodb": "*",
|
||||||
"ext-uri": "*",
|
"ext-uri": "*",
|
||||||
"ext-zlib": "*",
|
"ext-zlib": "*",
|
||||||
"indifferentketchup/codex": "^0.2.0",
|
"indifferentketchup/codex": "^0.3.0",
|
||||||
"mongodb/mongodb": "2.1.2"
|
"mongodb/mongodb": "2.1.2"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
|||||||
8
composer.lock
generated
8
composer.lock
generated
@@ -4,15 +4,15 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "821a03243eb4b751e38ca3f8f063dd3e",
|
"content-hash": "c970170e823f1c31130ee1eec742a090",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "indifferentketchup/codex",
|
"name": "indifferentketchup/codex",
|
||||||
"version": "v0.2.0",
|
"version": "v0.3.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.indifferentketchup.com/indifferentketchup/ik-codex",
|
"url": "https://git.indifferentketchup.com/indifferentketchup/ik-codex",
|
||||||
"reference": "2bd4fe6189c21be5b1fb03e8ac23b1a3c01d747c"
|
"reference": "656142dbf8979da7d5f06908e5dd53afa1b5e56d"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.4"
|
"php": ">=8.4"
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Generic PHP log parsing and analysis framework.",
|
"description": "Generic PHP log parsing and analysis framework.",
|
||||||
"time": "2026-05-01T22:08:43+00:00"
|
"time": "2026-05-06T19:04:37+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mongodb/mongodb",
|
"name": "mongodb/mongodb",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ services:
|
|||||||
- IBLOGS_WORKER_REQUESTS=1
|
- IBLOGS_WORKER_REQUESTS=1
|
||||||
- FRANKENPHP_WORKERS=4
|
- FRANKENPHP_WORKERS=4
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "4217:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ../:/app
|
- ../:/app
|
||||||
- ./dev.ini:/usr/local/etc/php/conf.d/dev.ini
|
- ./dev.ini:/usr/local/etc/php/conf.d/dev.ini
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ abstract class Filter implements \JsonSerializable
|
|||||||
new TrimFilter(),
|
new TrimFilter(),
|
||||||
new LimitBytesFilter(),
|
new LimitBytesFilter(),
|
||||||
new LimitLinesFilter(),
|
new LimitLinesFilter(),
|
||||||
new IPv4Filter(),
|
new ProjectZomboidRedactorFilter(),
|
||||||
new IPv6Filter(),
|
|
||||||
new UsernameFilter(),
|
new UsernameFilter(),
|
||||||
new AccessTokenFilter(),
|
new AccessTokenFilter(),
|
||||||
];
|
];
|
||||||
|
|||||||
45
src/Filter/ProjectZomboidRedactorFilter.php
Normal file
45
src/Filter/ProjectZomboidRedactorFilter.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Iblogs\Filter;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Util\ProjectZomboid\ProjectZomboidRedactor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save-time wrapper that delegates to codex's ProjectZomboidRedactor.
|
||||||
|
*
|
||||||
|
* Codex owns the canonical Project Zomboid PII patterns (Steam IDs, player
|
||||||
|
* names, world coordinates, plus IPv4 / IPv6 addresses with the v0.3.0
|
||||||
|
* release). This filter is the single point at which PZ-shaped PII is
|
||||||
|
* scrubbed on save; it replaces the previous IPv4Filter + IPv6Filter
|
||||||
|
* stage (whose IP-only matches left port suffixes intact) and adds the
|
||||||
|
* PZ-specific Steam ID, player-name, and coordinate redaction the generic
|
||||||
|
* filters never touched.
|
||||||
|
*
|
||||||
|
* Codex's IPv4 / IPv6 regexes are generic and apply to non-PZ pastes too;
|
||||||
|
* the PZ-specific regexes (Steam ID, player name, coords) mostly no-op on
|
||||||
|
* non-PZ content because they rely on PZ-specific anchors (`76561198`,
|
||||||
|
* the Steam-ID placeholder, `Combat:` / `Safety:` prefixes, `at` / `[`
|
||||||
|
* coord wrappers + trailing PvP verbs).
|
||||||
|
*
|
||||||
|
* Patterns are encapsulated inside the codex redactor and are not exposed
|
||||||
|
* to the client-side preview JS (`getData()` returns an empty array).
|
||||||
|
* Server-side redaction on save is the privacy guarantee; the preview is
|
||||||
|
* only a UX hint for users about what gets scrubbed.
|
||||||
|
*/
|
||||||
|
class ProjectZomboidRedactorFilter extends Filter
|
||||||
|
{
|
||||||
|
public function getType(): FilterType
|
||||||
|
{
|
||||||
|
return FilterType::REGEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getData(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filter(string $data): string
|
||||||
|
{
|
||||||
|
return new ProjectZomboidRedactor()->redact($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,27 @@ const fileSelectButton = document.getElementById('paste-select-file');
|
|||||||
const pasteClipboardButton = document.getElementById('paste-clipboard');
|
const pasteClipboardButton = document.getElementById('paste-clipboard');
|
||||||
const pasteError = document.getElementById('paste-error');
|
const pasteError = document.getElementById('paste-error');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Large-paste buffering. <textarea> rendering is the bottleneck once content
|
||||||
|
* crosses a few hundred KB; the browser locks up while it lays out millions
|
||||||
|
* of characters. For pastes / file loads above the threshold, we hold the
|
||||||
|
* full content in `bufferedContent` and only render a small preview into the
|
||||||
|
* textarea. The save path uses `bufferedContent` when set.
|
||||||
|
*/
|
||||||
|
const PASTE_BUFFER_THRESHOLD_BYTES = 256 * 1024; // 256 KB
|
||||||
|
const PASTE_PREVIEW_LINES = 50;
|
||||||
|
let bufferedContent = null;
|
||||||
|
|
||||||
pasteArea.focus();
|
pasteArea.focus();
|
||||||
pasteArea.addEventListener('input', reevaluateContentStatus);
|
pasteArea.addEventListener('input', () => {
|
||||||
|
// User-typed input invalidates the buffer (the textarea is now the
|
||||||
|
// source of truth). The buffer is only set programmatically, so a
|
||||||
|
// user-driven `input` event always means edit-from-preview.
|
||||||
|
if (bufferedContent !== null) {
|
||||||
|
clearBuffer();
|
||||||
|
}
|
||||||
|
reevaluateContentStatus();
|
||||||
|
});
|
||||||
pasteArea.addEventListener('paste', handlePasteEvent);
|
pasteArea.addEventListener('paste', handlePasteEvent);
|
||||||
pasteSaveButtons.forEach(button => button.addEventListener('click', sendLog));
|
pasteSaveButtons.forEach(button => button.addEventListener('click', sendLog));
|
||||||
fileSelectButton.addEventListener('click', selectLogFile);
|
fileSelectButton.addEventListener('click', selectLogFile);
|
||||||
@@ -39,7 +58,7 @@ async function sendLog() {
|
|||||||
pasteSaveButtons.forEach(button => button.classList.add("btn-working"));
|
pasteSaveButtons.forEach(button => button.classList.add("btn-working"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let log = pasteArea.value;
|
let log = bufferedContent ?? pasteArea.value;
|
||||||
log = applyFilters(log);
|
log = applyFilters(log);
|
||||||
|
|
||||||
const bodyData = {
|
const bodyData = {
|
||||||
@@ -149,13 +168,56 @@ async function pasteFromClipboard() {
|
|||||||
showError("Clipboard is empty.");
|
showError("Clipboard is empty.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pasteArea.value = content;
|
loadContent(content);
|
||||||
reevaluateContentStatus();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError("Clipboard is empty or not accessible.");
|
showError("Clipboard is empty or not accessible.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Single entry point for "place this string in the editor."
|
||||||
|
* Routes large content into the buffer + preview; small content goes into
|
||||||
|
* the textarea normally so the user can keep editing it.
|
||||||
|
*/
|
||||||
|
function loadContent(text) {
|
||||||
|
if (text.length > PASTE_BUFFER_THRESHOLD_BYTES) {
|
||||||
|
loadIntoBuffer(text);
|
||||||
|
} else {
|
||||||
|
clearBuffer();
|
||||||
|
pasteArea.value = text;
|
||||||
|
reevaluateContentStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadIntoBuffer(text) {
|
||||||
|
bufferedContent = text;
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const sizeMb = (text.length / 1024 / 1024).toFixed(2);
|
||||||
|
if (lines.length <= PASTE_PREVIEW_LINES) {
|
||||||
|
pasteArea.value = text;
|
||||||
|
} else {
|
||||||
|
const preview = lines.slice(0, PASTE_PREVIEW_LINES).join('\n');
|
||||||
|
const remaining = lines.length - PASTE_PREVIEW_LINES;
|
||||||
|
pasteArea.value =
|
||||||
|
`[Large paste buffered: ${lines.length.toLocaleString()} lines, ${sizeMb} MB.\n` +
|
||||||
|
` Full content uploads on Save. Edit this textarea to clear the buffer.]\n` +
|
||||||
|
`\n` +
|
||||||
|
`--- preview: first ${PASTE_PREVIEW_LINES} of ${lines.length.toLocaleString()} lines ---\n` +
|
||||||
|
preview +
|
||||||
|
`\n--- ${remaining.toLocaleString()} more lines hidden ---\n`;
|
||||||
|
}
|
||||||
|
pasteArea.readOnly = true;
|
||||||
|
reevaluateContentStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearBuffer() {
|
||||||
|
if (bufferedContent === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bufferedContent = null;
|
||||||
|
pasteArea.readOnly = false;
|
||||||
|
}
|
||||||
|
|
||||||
function reevaluateContentStatus() {
|
function reevaluateContentStatus() {
|
||||||
clearError();
|
clearError();
|
||||||
if (pasteArea.value.length > 0) {
|
if (pasteArea.value.length > 0) {
|
||||||
@@ -184,6 +246,14 @@ async function handlePasteEvent(e) {
|
|||||||
if (e.clipboardData?.files?.length > 0) {
|
if (e.clipboardData?.files?.length > 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await loadFileContents(e.clipboardData.files[0]);
|
await loadFileContents(e.clipboardData.files[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Intercept large text pastes before the browser commits them to the
|
||||||
|
// textarea — assigning multi-MB strings to a <textarea> freezes the page.
|
||||||
|
const text = e.clipboardData?.getData('text');
|
||||||
|
if (text && text.length > PASTE_BUFFER_THRESHOLD_BYTES) {
|
||||||
|
e.preventDefault();
|
||||||
|
loadIntoBuffer(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,8 +290,7 @@ async function loadFileContents(file) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pasteArea.value = new TextDecoder().decode(content);
|
loadContent(new TextDecoder().decode(content));
|
||||||
reevaluateContentStatus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectLogFile() {
|
function selectLogFile() {
|
||||||
@@ -355,8 +424,7 @@ async function handleDropEvent(e) {
|
|||||||
let files = e.dataTransfer.files;
|
let files = e.dataTransfer.files;
|
||||||
if (files.length !== 1) {
|
if (files.length !== 1) {
|
||||||
if (Array.from(e.dataTransfer.types).includes('text/plain')) {
|
if (Array.from(e.dataTransfer.types).includes('text/plain')) {
|
||||||
pasteArea.value = e.dataTransfer.getData('text/plain');
|
loadContent(e.dataTransfer.getData('text/plain'));
|
||||||
reevaluateContentStatus();
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user