mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-11-09 11:10:08 +01:00
Synopsis : normalisation SÛRE des text ajouté par formulaire
- fins de ligne \r\n/\r → \n,
- blocs de lignes vides → \n\n, è
- espaces/tabulations/NBSP → espace unique,
- trim.
Les retours à la ligne simples intra-paragraphe sont LAISSÉS INTACTS :
une coupure de ligne peut être une vraie frontière de mot (→ espace)
ou une coupure du mot ("dyna\nmiques" → "dynamiques"),
indistinguables sans dictionnaire — automatiser corromprait l'entrée
("poursuivantsur").
Migration batch 045 (backfill des données existantes) RETIRÉE : le
nettoyage automatique fiable des synopsis existants est impossible.
Chaque modification est testée (277 tests PHPUnit).
This commit is contained in:
1 parent
6e1fc6a781
commit
0896c4b8c8
4 files changed
+122
-5
No files matched your search
@@ -1 +0,0 @@
|
||||
{"version":2,"defects":{"CryptoTest::testEncryptDecryptEmptyString":7,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":7,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":7,"SystemControllerHelpersTest::testHumanBytesOneMB":7,"SystemControllerHelpersTest::testHumanBytesOneGB":7,"CryptoTest::testEncryptEmptyStringProducesCiphertext":7,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":7,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":7,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":7,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":7,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":7,"RateLimitExtendedTest::testGetRemainingDecrements":7,"RateLimitExtendedTest::testGetRemainingAtLimit":7,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":7,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":7,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":8,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForHexId":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForPeertubeVideo":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForPeertubeAudio":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForMixedIds":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForHexInCoverQueue":8,"PureLogicTest::testHasFilePondQueueDataReturnsTrueForPeertubeInCoverQueue":8,"PureLogicTest::testHasFilePondQueueDataReturnsFalseForNumericIdsOnly":8,"PureLogicTest::testHasFilePondQueueDataReturnsFalseForEmptyInput":8,"PureLogicTest::testHasFilePondQueueDataReturnsFalseForEmptyStrings":8,"PureLogicTest::testHasFilePondQueueDataHandlesScalarNotArray":8},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0,"CryptoTest::testEncryptDecryptWithUnicode":0,"CryptoTest::testEncryptDecryptMultiline":0,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0,"CryptoTest::testIsEncryptedRejectsPlaintext":0,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0,"EmailObfuscatorTest::testMultipleEmailsInOneString":0,"EmailObfuscatorTest::testEmailWithPlusSign":0,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0,"StudentEmailTest::testBuildHtmlContainsKeyFields":0,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0,"StudentEmailTest::testBuildHtmlContainsLabelFields":0,"SystemControllerHelpersTest::testHumanBytesZero":0,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneMB":0,"SystemControllerHelpersTest::testHumanBytesOneGB":0,"SystemControllerHelpersTest::testHumanBytes1523MB":0,"SystemControllerHelpersTest::testHumanBytes2500GB":0,"SystemControllerHelpersTest::testDiskColorBelowWarning":0,"SystemControllerHelpersTest::testDiskColorWarning":0,"SystemControllerHelpersTest::testDiskColorCritical":0,"SystemControllerHelpersTest::testLogLineClassCrit":0,"SystemControllerHelpersTest::testLogLineClassError":0,"SystemControllerHelpersTest::testLogLineClassWarn":0,"SystemControllerHelpersTest::testLogLineClassNotice":0,"SystemControllerHelpersTest::testLogLineClassHttp500":0,"SystemControllerHelpersTest::testLogLineClassHttp300":0,"SystemControllerHelpersTest::testLogLineClassDefault":0,"SystemControllerHelpersTest::testNginxLineClassComment":0.003,"SystemControllerHelpersTest::testNginxLineClassBlock":0.004,"SystemControllerHelpersTest::testNginxLineClassDirective":0.003,"SystemCoLine truncated
|
||||
@@ -1,13 +1,14 @@
|
||||
# TODO
|
||||
|
||||
> Last updated: 2026-08-26
|
||||
> Context: Docs refactor: verify docs/ accuracy against current xamxam codebase; update/refactor stale docs
|
||||
> Context: Fix end-user admin report: unexpected line breaks in some synopsis — resolved as undetectable/unfixable for existing data; only safe normalization on new inputs
|
||||
|
||||
## In Progress
|
||||
|
||||
## Pending
|
||||
|
||||
## Completed
|
||||
- [x] #normalize-synopsis-whitespace & Passages à la ligne inopinés dans les synopsis : jugés INDÉTECTABLES/non-corrigeables automatiquement (une coupure de ligne interne peut être une vraie frontière de mot → espace, ou une coupure du mot → à réjoindre ; indistinguables sans dictionnaire, toute tentative corrompt l'entrée p.ex. "poursuivantsur"). Migration 045 SUPPRIMÉE ; dites à l'admin qu'il n'y a pas de nettoyage fiable des données existantes. Conservation de la NORMALISATION SÛRE sur les nouvelles saisies : fin de ligne \r\n/\r → \n, pertes de lignes vides multiples → \n\n, espaces/tabulations/NBSP → espace unique, trim — retour à la ligne simple intra-paragraphe laissé intact.
|
||||
- [x] #audit-all-docs-and [!high] Audit all docs/ and classify accurate vs stale
|
||||
- [x] #rewrite-development-md-to-match [!high] Rewrite development.md to match current just dev / app/ layout / PHPUnit
|
||||
- [x] #rewrite-deployment-md-to-match [!high] Rewrite deployment.md to match just deploy / /var/www/xamxam/ / backup
|
||||
|
||||
@@ -609,12 +609,39 @@ class ThesisCreateController
|
||||
// ── Private: input helpers ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Trim and strip HTML tags from a string value.
|
||||
* htmlspecialchars is applied at render time, not here.
|
||||
* Normalise a submitted string value.
|
||||
*
|
||||
* Only the UNAMBIGUOUS cleanups are applied here. We deliberately do NOT
|
||||
* auto-collapse or rejoin single newlines inside a paragraph: a line break
|
||||
* can be either a real word boundary (should become a space) or a word split
|
||||
* across the line ("dyna\nmiques", should be rejoined to "dynamiques"),
|
||||
* and these are indistinguishable without a dictionary. Trying to guess
|
||||
* corrupts valid input (e.g. "se poursuivant\nsur" → "se poursuivantsur"),
|
||||
* so such breaks are left untouched and must be fixed by the author.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - strip HTML tags (htmlspecialchars is applied at render time, not here)
|
||||
* - normalise all line endings to \n (handles \r\n and bare \r)
|
||||
* - collapse runs of horizontal whitespace (spaces, tabs, NBSP) to one space
|
||||
* - collapse multiple blank lines into a single paragraph separator (\n\n)
|
||||
* - trim leading/trailing whitespace and blank lines
|
||||
*/
|
||||
private function sanitiseString(string $input): string
|
||||
{
|
||||
return strip_tags(trim($input));
|
||||
$input = strip_tags($input);
|
||||
|
||||
// Normalise all line endings to \n (handles \r\n and bare \r).
|
||||
$input = preg_replace('/\r\n|\r|\n/', "\n", $input);
|
||||
|
||||
// Collapse multiple blank lines into a single paragraph separator.
|
||||
$input = preg_replace("/\n{2,}/u", "\n\n", $input);
|
||||
|
||||
// Collapse runs of horizontal whitespace (spaces, tabs, NBSP gaps, …)
|
||||
// into a single space. Single newlines inside a paragraph are NOT
|
||||
// touched, for the reason explained above.
|
||||
$input = preg_replace('/[ \t\x0B\x00\xC2\xA0]+/u', ' ', $input);
|
||||
|
||||
return trim($input, " \t\n\r\0\x0B\xC2\xA0");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -263,6 +263,96 @@ class ThesisCreateValidationTest extends TestCase
|
||||
$this->assertStringContainsString('Bold synopsis', $data['synopsis']);
|
||||
}
|
||||
|
||||
// ── Whitespace normalisation (safe, unambiguous subset only) ────────────────
|
||||
// Single newlines inside a paragraph are deliberately LEFT untouched: they can
|
||||
// be a word boundary (→ space) or a word split across a line ("dyna\nmiques"
|
||||
// → "dynamiques") and are indistinguishable, so we never guess and risk
|
||||
// corrupting valid input (e.g. "se poursuivant\nsur" → "poursuivantsur").
|
||||
|
||||
public function testSingleIntraParagraphNewlineIsLeftUntouched(): void
|
||||
{
|
||||
// A mid-word line break is ambiguous (could be a real split or a wrap),
|
||||
// so we do not auto-alter it — the author is responsible for cleaning it.
|
||||
$post = $this->validPost();
|
||||
$post['synopsis'] = "…les dyn\namiques complexes…";
|
||||
|
||||
$data = $this->validate($post);
|
||||
$this->assertSame("…les dyn\namiques complexes…", $data['synopsis']);
|
||||
}
|
||||
|
||||
public function testWordWrapSingleNewlineNotMerged(): void
|
||||
{
|
||||
// We must NOT merge two real words across a line break ("poursuivantsur").
|
||||
$post = $this->validPost();
|
||||
$post['synopsis'] = "se poursuivant\nsur une seconde ligne";
|
||||
|
||||
$data = $this->validate($post);
|
||||
$this->assertSame(
|
||||
"se poursuivant\nsur une seconde ligne",
|
||||
$data['synopsis']
|
||||
);
|
||||
}
|
||||
|
||||
public function testLegitimateParagraphBreakPreserved(): void
|
||||
{
|
||||
$post = $this->validPost();
|
||||
$post['synopsis'] = "Premier paragraphe\n\nSecond paragraphe";
|
||||
|
||||
$data = $this->validate($post);
|
||||
$this->assertSame(
|
||||
"Premier paragraphe\n\nSecond paragraphe",
|
||||
$data['synopsis']
|
||||
);
|
||||
}
|
||||
|
||||
public function testMultipleBlankLinesCollapseToSingleParagraphBreak(): void
|
||||
{
|
||||
$post = $this->validPost();
|
||||
$post['synopsis'] = "A\n\n\n\nB";
|
||||
|
||||
$data = $this->validate($post);
|
||||
$this->assertSame("A\n\nB", $data['synopsis']);
|
||||
}
|
||||
|
||||
public function testMixedWindowsAndUnixLineEndingsNormalised(): void
|
||||
{
|
||||
$post = $this->validPost();
|
||||
$post['synopsis'] = "une pause\r\n\r\ner une future";
|
||||
|
||||
$data = $this->validate($post);
|
||||
// \r\n and \r line-endings all collapse to \n, and blank lines mark
|
||||
// a single paragraph separator.
|
||||
$this->assertSame("une pause\n\ner une future", $data['synopsis']);
|
||||
}
|
||||
|
||||
public function testConsecutiveSpacesAndTabsCollapse(): void
|
||||
{
|
||||
$post = $this->validPost();
|
||||
$post['synopsis'] = "Très espacé\t\ttab";
|
||||
|
||||
$data = $this->validate($post);
|
||||
$this->assertSame('Très espacé tab', $data['synopsis']);
|
||||
}
|
||||
|
||||
public function testNonBreakingSpacesNormalised(): void
|
||||
{
|
||||
$post = $this->validPost();
|
||||
// Word pasted NBSP (U+00A0) between words must become a regular space.
|
||||
$post['synopsis'] = "Premier\u{00A0}mot second mot";
|
||||
|
||||
$data = $this->validate($post);
|
||||
$this->assertSame('Premier mot second mot', $data['synopsis']);
|
||||
}
|
||||
|
||||
public function testSynopsisSingleLineFieldTrimmed(): void
|
||||
{
|
||||
$post = $this->validPost();
|
||||
$post['synopsis'] = " texte qui flotte ";
|
||||
|
||||
$data = $this->validate($post);
|
||||
$this->assertSame('texte qui flotte', $data['synopsis']);
|
||||
}
|
||||
|
||||
// ── Author processing ────────────────────────────────────────────────────
|
||||
|
||||
public function testMultipleAuthorsAreSorted(): void
|
||||
|
||||
Reference in new issue
Block a user