From 99125cc8e31097b0006770e570a69b430303c317 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Thu, 11 Jun 2026 10:32:53 +0200 Subject: [PATCH] Add autosave draft system for partage form with HTMX-based session persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New fragment endpoint POST/GET /partage/fragments/draft.php: saves all form fields to PHP session, excludes file/csrf/slug fields GET returns JSON for JS hydration on page load rotates both global CSRF and share CSRF tokens in sync - form.php accepts optional $formExtraAttrs and $showAutosaveStatus: allows injecting HTMX attributes and 'Brouillon enregistré' indicator - renderShareLinkForm adds hx-post with change/input debounce trigger, loads autosave-handler.js, hydrate fields from draft on page load - Draft cleared on successful form submission in handleShareLinkSubmission - autosave-handler.js now also updates share_link_token hidden input when rotating CSRF token (partage form uses both csrf_token and share_link_token) - Added .autosave-status CSS to form.css (was admin.css-only) - Updated fragment routing to accept GET requests (needed for draft hydration) --- .php-cs-fixer.cache | 2 +- .php-cs-fixer.dist.php | 2 +- .phpunit.result.cache | 2 +- app/public/assets/js/app/acces-password.js | 48 ++- app/public/assets/js/app/access-request.js | 148 +++---- app/public/assets/js/app/admin-logs.js | 114 +++--- app/public/assets/js/app/autosave-handler.js | 96 ++--- .../assets/js/app/beforeunload-guard.js | 33 +- app/public/assets/js/app/clipboard.js | 53 +-- .../assets/js/app/file-upload-filepond.js | 306 +++++++++------ app/public/assets/js/app/jury-autocomplete.js | 240 ++++++------ app/public/assets/js/app/pill-search.js | 286 +++++++------- app/public/assets/js/app/upload-progress.js | 360 +++++++++--------- .../filepond-plugin-file-validate-size.min.js | 116 +++++- .../filepond-plugin-file-validate-type.min.js | 111 +++++- ...epond-plugin-image-exif-orientation.min.js | 90 ++++- app/public/partage/fragments/draft.php | 98 +++++ app/src/AppLogger.php | 9 +- app/src/Controllers/ExportController.php | 2 +- app/src/Controllers/TfeController.php | 12 +- app/src/Controllers/ThesisEditController.php | 2 +- app/src/Database.php | 2 +- app/src/FilepondHandler.php | 2 +- app/src/Logger.php | 4 +- app/storage/schema.sql | 2 + app/templates/partials/form/form.php | 12 +- biome.json | 3 +- tests/TestDatabase.php | 2 +- tests/phpunit/AutofocusFieldForErrorTest.php | 2 +- tests/phpunit/DatabaseExtendedTest.php | 10 +- tests/phpunit/PureLogicTest.php | 19 +- tests/phpunit/TfeControllerOgTest.php | 2 +- tests/phpunit/ThesisEditValidationTest.php | 4 +- 33 files changed, 1388 insertions(+), 806 deletions(-) create mode 100644 app/public/partage/fragments/draft.php diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache index 49335f4..759ba04 100644 --- a/.php-cs-fixer.cache +++ b/.php-cs-fixer.cache @@ -1 +1 @@ -{"php":"8.5.6","version":"3.95.1","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_anonymous_functions":false,"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"sort_algorithm":"alpha"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":true},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"clean_namespace":true,"no_unset_cast":true,"assign_null_coalescing_to_coalesce_equal":true,"normalize_index_brace":true,"heredoc_indentation":true,"no_whitespace_before_comma_in_array":{"after_heredoc":true},"trailing_comma_in_multiline":true,"list_syntax":true,"ternary_to_null_coalescing":true,"array_syntax":{"syntax":"short"},"no_unused_imports":true,"single_quote":true},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"app\/src\/SystemCache.php":"4ead28637fa3a9281bdad42cdb9e00c2","app\/src\/AdminAuth.php":"14677d94cd04b7d455c43e74f7ec5d14","app\/tests\/Security\/SecurityTest.php":"ff9996affd0ca42095d34e0ac0650050","app\/tests\/Integration\/SearchTest.php":"29aba0f9da3c115fb5d2722576815361","app\/tests\/Unit\/DatabaseTest.php":"2dd249e28ac632ba222a8cead3ae16a1","app\/tests\/Unit\/RateLimitTest.php":"4fc2ffe64d2c889835ef4fcd2e89d835","app\/src\/Controllers\/FileAccessController.php":"9665edaa0ab1fd7c94b9a3d9fad95c5f","app\/src\/Controllers\/LiveReloadController.php":"e2ff21e7155e769b2684a51accf1699d","app\/src\/Parsedown.php":"d98c00dfbbb11933a86407ee9cf9215d","app\/src\/AppLogger.php":"139735566a1cc21d64eacc5b63de1d3c","app\/src\/DuplicateThesisException.php":"52abe5f40ef48cfbfd44c119d91309e9","app\/src\/RateLimit.php":"2e1df734570cb3eb584682bed33a2636","app\/src\/Audit.php":"6f795cbdf5d81b0ae337dd2df5084f75","app\/src\/Crypto.php":"e72e65eeaf5b6fa8e41ba4501643440b","app\/src\/FragmentRenderer.php":"52083e1f2ff98f01e074a93bf6765366","app\/src\/ErrorHandler.php":"56d3dc0af9ce6b5ef07291419073a6b1","app\/src\/EmailObfuscator.php":"ff946c10add222870223b9626990e75c","app\/src\/FilepondHandler.php":"7ba1547b7cb8daeecd69f32566d6a755","app\/src\/Controllers\/validate-file-fragment-shared.php":"d572b9564076e22b6a3f9baddf7bdb46","app\/tests\/run-tests.php":"860d3aa6d8f15ee90c80fef5e5d50c7c","app\/src\/Controllers\/HomeController.php":"8e9a29c622c8c37191ea2bdb40c5f3bf","app\/src\/Controllers\/ThesisCreateController.php":"02958bdd91e11e23e1597070a382012c","app\/src\/Dispatcher.php":"5d7ca0543b942857e5ef909aface040b","app\/src\/App.php":"cc568bc6c2453d35638ae64836c443da","app\/src\/AdminLogger.php":"b3e80ea6375d19f40e5da126ee8397b7","app\/src\/ShareLink.php":"7fc43ea0264f7e2a8562abd0ef8c2ecd","app\/src\/StudentEmail.php":"06b51a435ff86462f7311f58dfe03c34","app\/src\/Controllers\/ExportController.php":"f33ac38ed4014c0236534c8529b92f1c","app\/src\/Controllers\/TfeController.php":"d080380d1612d8365d7987ca536c31c7","app\/src\/Controllers\/SystemController.php":"4e25f8cd33ff44d5a9a6c615b000d6cb","app\/src\/Controllers\/SearchController.php":"2daaca037c8df9353888f8093120e36e","app\/src\/Controllers\/MediaController.php":"4b1412723d5a77bb8f75ad825b4ef333","app\/src\/Controllers\/ThesisEditController.php":"ee75f8510c40add9365966187262e251","app\/src\/Controllers\/ThesisFileHandler.php":"01af0cfcc89e048868351805b760c13b","app\/src\/Database.php":"37a008372ca85a45fa78651cd88975ec","app\/tests\/Unit\/ErrorHandlerTest.php":"a8f89fea425a0c0d358a865aeca8dd11","app\/tests\/Unit\/ShareLinkTest.php":"1c4eccb8d46f15a3b4f3938b52a70e60","app\/tests\/Unit\/FormSaveTest.php":"72fe083f6df01d588e4a097e2cb36cd6","app\/tests\/Unit\/PureLogicTest.php":"de6e0c5535c6ef83c4e566bd07320c9e","app\/src\/Controllers\/LicenceController.php":"b0947402e3cdfeef49a4cca6f0e703c8","app\/src\/Controllers\/AboutController.php":"4c67bf2c4aa6d7d69b0f5168d13029de","app\/src\/PeerTubeService.php":"ea89db249ebc9b9d0dd6695ae4ec6b98","app\/src\/SmtpRelay.php":"58b659727976e5bafc99830b0ca6cb33"}} \ No newline at end of file +{"php":"8.5.7","version":"3.95.1","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_anonymous_functions":false,"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"sort_algorithm":"alpha"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":true},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"clean_namespace":true,"no_unset_cast":true,"assign_null_coalescing_to_coalesce_equal":true,"normalize_index_brace":true,"heredoc_indentation":true,"no_whitespace_before_comma_in_array":{"after_heredoc":true},"trailing_comma_in_multiline":true,"list_syntax":true,"ternary_to_null_coalescing":true,"array_syntax":{"syntax":"short"},"no_unused_imports":true,"single_quote":true},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"app\/src\/Dispatcher.php":"e233ceb92678bffe6bb343174c7c58b5","app\/src\/App.php":"cc568bc6c2453d35638ae64836c443da","app\/src\/FragmentRenderer.php":"52083e1f2ff98f01e074a93bf6765366","app\/src\/AppLogger.php":"fe8435f658a74e88d4ffa6bbc83895d6","app\/src\/DuplicateThesisException.php":"52abe5f40ef48cfbfd44c119d91309e9","app\/src\/RateLimit.php":"2e1df734570cb3eb584682bed33a2636","app\/src\/ErrorHandler.php":"e9407d408af265ae3d043f63a00d3724","app\/src\/AdminLogger.php":"5d5078ebaba66c1492cbcb36ac57ea9d","app\/src\/SystemCache.php":"4ead28637fa3a9281bdad42cdb9e00c2","app\/src\/SmtpRelay.php":"58b659727976e5bafc99830b0ca6cb33","app\/src\/AdminAuth.php":"14677d94cd04b7d455c43e74f7ec5d14","app\/src\/ShareLink.php":"75bedcc493e4d684e28e306a7339442d","app\/src\/Audit.php":"aec9fbbe8d8eefa8fc657884e8df9b42","app\/src\/StudentEmail.php":"344309b5b3703b34e97cdeca38de7644","app\/src\/Crypto.php":"e72e65eeaf5b6fa8e41ba4501643440b","tests\/phpunit\/ThesisCreateValidationTest.php":"533fa660b77c6b2d616481d1b66c8967","tests\/phpunit\/ErrorHandlerTest.php":"2005e176a105511fc19e853fb4799456","tests\/phpunit\/SearchControllerTest.php":"4b3de46664ede0d065f93d6d7fdee145","tests\/phpunit\/RateLimitExtendedTest.php":"a5e76ed126b2d760436bffb6f1fad0a0","tests\/phpunit\/TfeControllerOgTest.php":"53eefeea18c8d25aec47d59a9c7565a3","tests\/phpunit\/PureLogicTest.php":"2aeaf72d083345356c3308760b355463","tests\/phpunit\/ThesisEditValidationTest.php":"06761c5109cc12fb0f17046b4bcd0814","tests\/phpunit\/ShareLinkExtendedTest.php":"778d8cb9052714f36466d17ed8bf40e9","tests\/phpunit\/EmailObfuscatorTest.php":"ba6fb4078a7e5459749b1402e5558cfb","tests\/phpunit\/DatabaseExtendedTest.php":"f1717a8b1a98bb899d3e1bb275569320","tests\/phpunit\/SystemControllerHelpersTest.php":"d3fc100e6576fbf98517be3a55cad4d6","tests\/phpunit\/CryptoTest.php":"4b4882a940fca3a2a169ef7941f1f413","tests\/phpunit\/AutofocusFieldForErrorTest.php":"402a1258c9adafca7622eb2715162c84","tests\/phpunit\/StudentEmailTest.php":"0fa0d2b1abd68b645e3287b234efb043","tests\/bootstrap.php":"f7b19885b338519f4439e9153ee008aa","tests\/TestDatabase.php":"7b8301cbfadb1a3c02ccfcef8edc1115","app\/src\/Controllers\/LicenceController.php":"b0947402e3cdfeef49a4cca6f0e703c8","app\/src\/Controllers\/FileAccessController.php":"9665edaa0ab1fd7c94b9a3d9fad95c5f","app\/src\/Controllers\/MediaController.php":"4b1412723d5a77bb8f75ad825b4ef333","app\/src\/Controllers\/HomeController.php":"68aa0a9cc3bfc0f844490f73e080ca09","app\/src\/Controllers\/AboutController.php":"0122be8eed5cf58bf4fb7fac8d87fecd","app\/src\/Controllers\/LiveReloadController.php":"e2ff21e7155e769b2684a51accf1699d","app\/src\/Controllers\/ThesisEditController.php":"8e59e77016039daf82af8130901b5174","app\/src\/Controllers\/ThesisCreateController.php":"d4c07d985325561225d434c169f8e1a3","app\/src\/Controllers\/CharteController.php":"dfba582f99c3f1f091527d6fd16d04cd","app\/src\/PeerTubeService.php":"da3708d8d7e591dcb3503bceeb9611cc","app\/src\/EmailObfuscator.php":"ff946c10add222870223b9626990e75c","app\/src\/Database.php":"567144cf47e5cc9b7d81bd0b492b2563","app\/src\/Logger.php":"97e8d68cbddef1c99e447952a59fb8b0","app\/src\/FilepondHandler.php":"d71786912f389aca8129b80cfc1d84c1","app\/src\/Controllers\/ExportController.php":"3cc3b3e6bede289e093a1d935bfce793","app\/src\/Controllers\/TfeController.php":"ba2df88adb1b4f0331cf88ace3642598","app\/src\/Controllers\/SystemController.php":"a68816afc71e34004df7103c73e17596","app\/src\/Controllers\/validate-file-fragment-shared.php":"d572b9564076e22b6a3f9baddf7bdb46","app\/src\/Controllers\/SearchController.php":"3aa83d57550050f657b00d1652546f88","app\/src\/Controllers\/ThesisFileHandler.php":"ca411226ac6722067002c0f5c686a47c"}} \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 0be22dd..3357497 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -19,7 +19,7 @@ return (new Config()) ->setFinder( (new Finder()) ->in(__DIR__ . '/app/src') - ->in(__DIR__ . '/app/tests') + ->in(__DIR__ . '/tests') ->name('*.php') ) ; diff --git a/.phpunit.result.cache b/.phpunit.result.cache index bfe0958..aa1a093 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"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},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0.003,"CryptoTest::testEncryptDecryptWithUnicode":0.001,"CryptoTest::testEncryptDecryptMultiline":0,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0.001,"CryptoTest::testIsEncryptedRejectsPlaintext":0.001,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0.001,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0.001,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0.001,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0.001,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0.001,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0.001,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0,"EmailObfuscatorTest::testMultipleEmailsInOneString":0,"EmailObfuscatorTest::testEmailWithPlusSign":0,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0.001,"StudentEmailTest::testBuildHtmlContainsKeyFields":0,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0,"StudentEmailTest::testBuildHtmlContainsLabelFields":0,"SystemControllerHelpersTest::testHumanBytesZero":0.001,"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.001,"SystemControllerHelpersTest::testLogLineClassError":0.001,"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,"SystemControllerHelpersTest::testStatusLabelActive":0,"SystemControllerHelpersTest::testStatusLabelInactive":0,"SystemControllerHelpersTest::testStatusLabelFailed":0,"SystemControllerHelpersTest::testStatusLabelWarn":0,"SystemControllerHelpersTest::testStatusLabelUnknown":0,"SystemControllerHelpersTest::testStatusClassOk":0,"SystemControllerHelpersTest::testStatusClassWarn":0,"SystemControllerHelpersTest::testStatusClassError":0,"SystemControllerHelpersTest::testStatusClassUnknown":0,"TfeControllerOgTest::testBuildOgTagsReturnsAllRequiredKeys":0,"TfeControllerOgTest::testBuildOgTagsTitleIncludesAuthors":0,"TfeControllerOgTest::testBuildOgTagsImageEmptyWhenNoFiles":0,"TfeControllerOgTest::testBuildOgTagsImageFromCover":0,"TfeControllerOgTest::testBuildOgTagsImageFallbackToFirstImage":0,"TfeControllerOgTest::testBuildOgTagsUrlIncludesThesisId":0,"TfeControllerOgTest::testBuildOgTagsPublishedTimeFormatted":0,"TfeControllerOgTest::testBuildOgTagsPublishedTimeEmptyWhenNoYear":0,"TfeControllerOgTest::testBuildMetaDescriptionTruncatesLongSynopsis":0.003,"TfeControllerOgTest::testBuildMetaDescriptionKeepsShortSynopsis":0,"TfeControllerOgTest::testBuildMetaDescriptionEmptySynopsisReturnsDefault":0,"TfeControllerOgTest::testBuildMetaDescriptionStripsHtmlTags":0,"CryptoTest::testEncryptEmptyStringProducesCiphertext":0.001,"DatabaseExtendedTest::testEscapeLikeStringViaSearchConditions":0.001,"DatabaseExtendedTest::testBuildSearchConditionsEmptyParams":0.001,"DatabaseExtendedTest::testBuildSearchConditionsWithQuery":0.001,"DatabaseExtendedTest::testBuildSearchConditionsWithYear":0.001,"DatabaseExtendedTest::testBuildSearchConditionsWithAllFilters":0,"DatabaseExtendedTest::testFindDuplicateThesisExactMatch":0.001,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentTitle":0.001,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentYear":0.001,"DatabaseExtendedTest::testFindDuplicateThesisEmptyAuthorNamesReturnsNull":0.001,"DatabaseExtendedTest::testFindDuplicateThesisEmptyTable":0.001,"DatabaseExtendedTest::testFindDuplicateThesisNearDuplicateByLevenshtein":0.001,"DatabaseExtendedTest::testGenerateThesisIdentifierFirstInYear":0,"DatabaseExtendedTest::testGenerateThesisIdentifierIncrementsCorrectly":0,"DatabaseExtendedTest::testGenerateThesisIdentifierUsesMaxNotCount":0,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsPaths":0,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsEmptyForUnknownIds":0,"DatabaseExtendedTest::testGetCoverPathsForThesesEmptyInputReturnsEmpty":0,"DatabaseExtendedTest::testGetCoverPathsForThesesMultipleTheses":0.001,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":0,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":0,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":0,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":0,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":0.001,"DatabaseExtendedTest::testRenameLanguageUpdatesName":0.009,"DatabaseExtendedTest::testMergeLanguageReassignsTheses":0.001,"DatabaseExtendedTest::testRenameTagUpdatesName":0,"DatabaseExtendedTest::testMergeTagReassignsTheses":0.001,"RateLimitExtendedTest::testCheckKeyCountsPerKey":0.002,"RateLimitExtendedTest::testCheckKeyDoesNotAffectDefaultCheck":0.001,"RateLimitExtendedTest::testGetRemainingDecrements":0,"RateLimitExtendedTest::testGetRemainingAtLimit":0,"RateLimitExtendedTest::testGetRemainingUsesClientIdentifier":0.001,"RateLimitExtendedTest::testCheckUsesConsistentIdentifier":0,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":0,"RateLimitExtendedTest::testGetResetTimeReturnsZeroWhenNoData":0,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":0.001,"RateLimitExtendedTest::testCleanupRemovesOldFiles":0,"ShareLinkExtendedTest::testListActiveReturnsOnlyActiveLinks":0.284,"ShareLinkExtendedTest::testListArchivedReturnsOnlyArchivedLinks":0.284,"ShareLinkExtendedTest::testFindBySlugHit":0.283,"ShareLinkExtendedTest::testFindBySlugMiss":0,"ShareLinkExtendedTest::testSetPasswordAndDecryptRoundTrip":0.281,"ShareLinkExtendedTest::testGetDecryptedPasswordOnNonexistentId":0,"ShareLinkExtendedTest::testUpdateChangesNameAndExpiration":0.283,"ShareLinkExtendedTest::testUpdateOnlyNameLeavesExpirationUnchanged":0.284,"ShareLinkExtendedTest::testUpdateClearsExpiration":0.283,"ShareLinkExtendedTest::testCreateWithLockedYear":0.29,"ShareLinkExtendedTest::testCreateWithInvalidLockedYearRejected":0.281,"ShareLinkExtendedTest::testUpdateLockedYear":0.28,"ShareLinkExtendedTest::testUpdateClearLockedYear":0.283,"ShareLinkExtendedTest::testIncrementUsage":0.284,"ShareLinkExtendedTest::testCreateDefaultsToTfeWhenInvalidObjet":0.281,"ShareLinkExtendedTest::testCreateAcceptsValidObjet":0.279,"RateLimitExtendedTest::testGetRemainingStartsAtMax":0,"RateLimitExtendedTest::testCheckDecrementsRemainingForSameIp":0.001,"RateLimitExtendedTest::testCheckAndCheckKeyAreIndependent":0,"RateLimitExtendedTest::testMultipleChecksFromSameClient":0,"AutofocusFieldForErrorTest::testCreateAutofocusTitle":0.004,"AutofocusFieldForErrorTest::testCreateAutofocusAuthors":0,"AutofocusFieldForErrorTest::testCreateAutofocusSynopsis":0,"AutofocusFieldForErrorTest::testCreateAutofocusYear":0,"AutofocusFieldForErrorTest::testCreateAutofocusOrientation":0,"AutofocusFieldForErrorTest::testCreateAutofocusAP":0,"AutofocusFieldForErrorTest::testCreateAutofocusFinality":0,"AutofocusFieldForErrorTest::testCreateAutofocusLanguages":0,"AutofocusFieldForErrorTest::testCreateAutofocusPromoteur":0,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurInterne":0,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurExterne":0,"AutofocusFieldForErrorTest::testCreateAutofocusFormats":0,"AutofocusFieldForErrorTest::testCreateAutofocusLicense":0,"AutofocusFieldForErrorTest::testCreateAutofocusUrl":0,"AutofocusFieldForErrorTest::testCreateAutofocusTags":0,"AutofocusFieldForErrorTest::testCreateAutofocusUnknownErrorReturnsNull":0,"AutofocusFieldForErrorTest::testEditAutofocusTitle":0.001,"AutofocusFieldForErrorTest::testEditAutofocusYear":0,"AutofocusFieldForErrorTest::testEditAutofocusSynopsis":0,"AutofocusFieldForErrorTest::testEditAutofocusAuthors":0,"AutofocusFieldForErrorTest::testEditAutofocusUnknownErrorReturnsNull":0,"AutofocusFieldForErrorTest::testCreateDoesNotLeakEditFieldNames":0,"ThesisCreateValidationTest::testValidSubmissionReturnsCleanedData":0,"ThesisCreateValidationTest::testMissingTitleThrowsException":0.001,"ThesisCreateValidationTest::testMissingAuthorsThrowsException":0,"ThesisCreateValidationTest::testMissingSynopsisThrowsException":0,"ThesisCreateValidationTest::testMissingOrientationInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingAPProgramInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingFinalityInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testInvalidYearFormatRejected":0,"ThesisCreateValidationTest::testYearZeroRejected":0,"ThesisCreateValidationTest::testYearBefore2000Rejected":0,"ThesisCreateValidationTest::testFarFutureYearRejected":0.001,"ThesisCreateValidationTest::testCurrentYearAccepted":0.001,"ThesisCreateValidationTest::testMalformedUrlRejected":0,"ThesisCreateValidationTest::testValidUrlAccepted":0,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":0,"ThesisCreateValidationTest::testMaxTenKeywordsEnforced":0,"ThesisCreateValidationTest::testXssPayloadStrippedFromTitle":0,"ThesisCreateValidationTest::testHtmlInSynopsisStripped":0,"ThesisCreateValidationTest::testMultipleAuthorsAreSorted":0,"ThesisCreateValidationTest::testMissingPromoteurInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingLecteurInterneInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingLanguagesInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingFormatsInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingLicenseWithLibreAccessThrowsException":0,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":0.011,"ThesisEditValidationTest::testLoadThrowsOnUnknownId":0.001,"ThesisEditValidationTest::testLoadThrowsOnInvalidId":0,"ThesisEditValidationTest::testLoadThrowsOnNegativeId":0,"ThesisEditValidationTest::testCollectJuryMembersEmptyInput":0,"ThesisEditValidationTest::testCollectJuryMembersSinglePromoteur":0,"ThesisEditValidationTest::testCollectJuryMembersPromoteurUlb":0,"ThesisEditValidationTest::testCollectJuryMembersLecteurs":0,"ThesisEditValidationTest::testCollectJuryMembersDeduplicatesEmptyStrings":0,"ThesisEditValidationTest::testCollectJuryMembersScalarPromoteurAccepted":0,"ThesisEditValidationTest::testHandleWebsiteUrlStoresValidUrl":0.001,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsInvalidUrl":0.001,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsEmptyUrl":0,"ThesisEditValidationTest::testHandleWebsiteUrlNormalisesHttp":0.001,"ErrorHandlerTest::testFkThesesTableMentionsAllPossibleFields":0.001,"ErrorHandlerTest::testFkApPrograms":0,"ErrorHandlerTest::testFkFinalityTypes":0,"ErrorHandlerTest::testFkThesisLanguages":0,"ErrorHandlerTest::testFkThesisFormats":0,"ErrorHandlerTest::testFkThesisTags":0,"ErrorHandlerTest::testFkThesisSupervisors":0,"ErrorHandlerTest::testFkAccessTypes":0,"ErrorHandlerTest::testFkLicenseTypes":0,"ErrorHandlerTest::testFkAuthors":0,"ErrorHandlerTest::testFkQuotedTableName":0.001,"ErrorHandlerTest::testFkQuotedLanguages":0,"ErrorHandlerTest::testFkQuotedFormatTypes":0,"ErrorHandlerTest::testFkReferencesTags":0,"ErrorHandlerTest::testFkReferencesOrientations":0,"ErrorHandlerTest::testFkUnknownTableGenericFallback":0,"ErrorHandlerTest::testFkEmptyMessageGenericFallback":0,"ErrorHandlerTest::testUniqueConstraint":0,"ErrorHandlerTest::testNotNullConstraint":0,"ErrorHandlerTest::testGenericPdoError":0,"ErrorHandlerTest::testDuplicateThesisExceptionPassesThrough":0,"ErrorHandlerTest::testValidationExceptionPassesThrough":0,"ErrorHandlerTest::testGenericExceptionPassesThrough":0,"ErrorHandlerTest::testTypeErrorReturnsGeneric":0,"ErrorHandlerTest::testLogWithContext":0.001,"ErrorHandlerTest::testLogWithNullValues":0,"ErrorHandlerTest::testLogWithEmptyExtra":0,"ErrorHandlerTest::testFkQuotedColumnNames":0,"ErrorHandlerTest::testFkUpdateStatement":0,"ErrorHandlerTest::testFkWithReferencesAndInsert":0,"PureLogicTest::testSplitJuryByRoleAllRoles":0.001,"PureLogicTest::testSplitJuryByRoleEmptyNameSkipped":0,"PureLogicTest::testSplitJuryByRoleEmptyJury":0,"PureLogicTest::testCollectCaptionPathsVttByMime":0,"PureLogicTest::testCollectCaptionPathsVttByExtension":0,"PureLogicTest::testCollectCaptionPathsNoVttReturnsEmpty":0,"PureLogicTest::testDetectFileTypeByMime":0,"PureLogicTest::testDetectFileTypeByExtensionFallback":0,"SearchControllerTest::testHandleSearchReturnsCoverMapKey":0.003,"SearchControllerTest::testCoverMapContainsKnownThesis":0.002}} \ No newline at end of file +{"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},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0.022,"CryptoTest::testEncryptDecryptWithUnicode":0.002,"CryptoTest::testEncryptDecryptMultiline":0.002,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0.002,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0.002,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0.002,"CryptoTest::testIsEncryptedRejectsPlaintext":0.002,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0.002,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0.002,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0.002,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0.002,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0.001,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0.002,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0.002,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0.016,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0.002,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0.002,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0.002,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0.002,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0.002,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0.002,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0.002,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0.002,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0.002,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0.001,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0.002,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0.003,"EmailObfuscatorTest::testMultipleEmailsInOneString":0.003,"EmailObfuscatorTest::testEmailWithPlusSign":0.002,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0.027,"StudentEmailTest::testBuildHtmlContainsKeyFields":0.002,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0.003,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0.002,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0.002,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0.002,"StudentEmailTest::testBuildHtmlContainsLabelFields":0.002,"SystemControllerHelpersTest::testHumanBytesZero":0.122,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0.002,"SystemControllerHelpersTest::testHumanBytesOneKB":0.002,"SystemControllerHelpersTest::testHumanBytesOneMB":0.002,"SystemControllerHelpersTest::testHumanBytesOneGB":0.002,"SystemControllerHelpersTest::testHumanBytes1523MB":0.003,"SystemControllerHelpersTest::testHumanBytes2500GB":0.002,"SystemControllerHelpersTest::testDiskColorBelowWarning":0.002,"SystemControllerHelpersTest::testDiskColorWarning":0.002,"SystemControllerHelpersTest::testDiskColorCritical":0.002,"SystemControllerHelpersTest::testLogLineClassCrit":0.002,"SystemControllerHelpersTest::testLogLineClassError":0.002,"SystemControllerHelpersTest::testLogLineClassWarn":0.002,"SystemControllerHelpersTest::testLogLineClassNotice":0.003,"SystemControllerHelpersTest::testLogLineClassHttp500":0.003,"SystemControllerHelpersTest::testLogLineClassHttp300":0.002,"SystemControllerHelpersTest::testLogLineClassDefault":0.003,"SystemControllerHelpersTest::testNginxLineClassComment":0.003,"SystemControllerHelpersTest::testNginxLineClassBlock":0.004,"SystemControllerHelpersTest::testNginxLineClassDirective":0.003,"SystemControllerHelpersTest::testStatusLabelActive":0.002,"SystemControllerHelpersTest::testStatusLabelInactive":0.002,"SystemControllerHelpersTest::testStatusLabelFailed":0.002,"SystemControllerHelpersTest::testStatusLabelWarn":0.002,"SystemControllerHelpersTest::testStatusLabelUnknown":0.002,"SystemControllerHelpersTest::testStatusClassOk":0.004,"SystemControllerHelpersTest::testStatusClassWarn":0.002,"SystemControllerHelpersTest::testStatusClassError":0.002,"SystemControllerHelpersTest::testStatusClassUnknown":0.002,"TfeControllerOgTest::testBuildOgTagsReturnsAllRequiredKeys":0.003,"TfeControllerOgTest::testBuildOgTagsTitleIncludesAuthors":0.003,"TfeControllerOgTest::testBuildOgTagsImageEmptyWhenNoFiles":0.002,"TfeControllerOgTest::testBuildOgTagsImageFromCover":0.002,"TfeControllerOgTest::testBuildOgTagsImageFallbackToFirstImage":0.002,"TfeControllerOgTest::testBuildOgTagsUrlIncludesThesisId":0.003,"TfeControllerOgTest::testBuildOgTagsPublishedTimeFormatted":0.002,"TfeControllerOgTest::testBuildOgTagsPublishedTimeEmptyWhenNoYear":0.002,"TfeControllerOgTest::testBuildMetaDescriptionTruncatesLongSynopsis":0.002,"TfeControllerOgTest::testBuildMetaDescriptionKeepsShortSynopsis":0.002,"TfeControllerOgTest::testBuildMetaDescriptionEmptySynopsisReturnsDefault":0.003,"TfeControllerOgTest::testBuildMetaDescriptionStripsHtmlTags":0.003,"CryptoTest::testEncryptEmptyStringProducesCiphertext":0.002,"DatabaseExtendedTest::testEscapeLikeStringViaSearchConditions":0.002,"DatabaseExtendedTest::testBuildSearchConditionsEmptyParams":0.002,"DatabaseExtendedTest::testBuildSearchConditionsWithQuery":0.002,"DatabaseExtendedTest::testBuildSearchConditionsWithYear":0.002,"DatabaseExtendedTest::testBuildSearchConditionsWithAllFilters":0.002,"DatabaseExtendedTest::testFindDuplicateThesisExactMatch":0.002,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentTitle":0.003,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentYear":0.002,"DatabaseExtendedTest::testFindDuplicateThesisEmptyAuthorNamesReturnsNull":0.002,"DatabaseExtendedTest::testFindDuplicateThesisEmptyTable":0.002,"DatabaseExtendedTest::testFindDuplicateThesisNearDuplicateByLevenshtein":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierFirstInYear":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierIncrementsCorrectly":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierUsesMaxNotCount":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsPaths":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsEmptyForUnknownIds":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesEmptyInputReturnsEmpty":0.003,"DatabaseExtendedTest::testGetCoverPathsForThesesMultipleTheses":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":0.002,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":0.002,"DatabaseExtendedTest::testRenameLanguageUpdatesName":0.034,"DatabaseExtendedTest::testMergeLanguageReassignsTheses":0.003,"DatabaseExtendedTest::testRenameTagUpdatesName":0.003,"DatabaseExtendedTest::testMergeTagReassignsTheses":0.003,"RateLimitExtendedTest::testCheckKeyCountsPerKey":0.031,"RateLimitExtendedTest::testCheckKeyDoesNotAffectDefaultCheck":0.002,"RateLimitExtendedTest::testGetRemainingDecrements":0,"RateLimitExtendedTest::testGetRemainingAtLimit":0,"RateLimitExtendedTest::testGetRemainingUsesClientIdentifier":0.001,"RateLimitExtendedTest::testCheckUsesConsistentIdentifier":0,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":0.002,"RateLimitExtendedTest::testGetResetTimeReturnsZeroWhenNoData":0.002,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":0.002,"RateLimitExtendedTest::testCleanupRemovesOldFiles":0.002,"ShareLinkExtendedTest::testListActiveReturnsOnlyActiveLinks":0.328,"ShareLinkExtendedTest::testListArchivedReturnsOnlyArchivedLinks":0.286,"ShareLinkExtendedTest::testFindBySlugHit":0.292,"ShareLinkExtendedTest::testFindBySlugMiss":0.003,"ShareLinkExtendedTest::testSetPasswordAndDecryptRoundTrip":0.28,"ShareLinkExtendedTest::testGetDecryptedPasswordOnNonexistentId":0.004,"ShareLinkExtendedTest::testUpdateChangesNameAndExpiration":0.279,"ShareLinkExtendedTest::testUpdateOnlyNameLeavesExpirationUnchanged":0.278,"ShareLinkExtendedTest::testUpdateClearsExpiration":0.281,"ShareLinkExtendedTest::testCreateWithLockedYear":0.278,"ShareLinkExtendedTest::testCreateWithInvalidLockedYearRejected":0.278,"ShareLinkExtendedTest::testUpdateLockedYear":0.278,"ShareLinkExtendedTest::testUpdateClearLockedYear":0.278,"ShareLinkExtendedTest::testIncrementUsage":0.285,"ShareLinkExtendedTest::testCreateDefaultsToTfeWhenInvalidObjet":0.294,"ShareLinkExtendedTest::testCreateAcceptsValidObjet":0.282,"RateLimitExtendedTest::testGetRemainingStartsAtMax":0.002,"RateLimitExtendedTest::testCheckDecrementsRemainingForSameIp":0.002,"RateLimitExtendedTest::testCheckAndCheckKeyAreIndependent":0.002,"RateLimitExtendedTest::testMultipleChecksFromSameClient":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusTitle":0.921,"AutofocusFieldForErrorTest::testCreateAutofocusAuthors":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusSynopsis":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusYear":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusOrientation":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusAP":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusFinality":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLanguages":0.001,"AutofocusFieldForErrorTest::testCreateAutofocusPromoteur":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurInterne":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurExterne":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusFormats":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLicense":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusUrl":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusTags":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusUnknownErrorReturnsNull":0.002,"AutofocusFieldForErrorTest::testEditAutofocusTitle":0.134,"AutofocusFieldForErrorTest::testEditAutofocusYear":0.002,"AutofocusFieldForErrorTest::testEditAutofocusSynopsis":0.001,"AutofocusFieldForErrorTest::testEditAutofocusAuthors":0.001,"AutofocusFieldForErrorTest::testEditAutofocusUnknownErrorReturnsNull":0.002,"AutofocusFieldForErrorTest::testCreateDoesNotLeakEditFieldNames":0.002,"ThesisCreateValidationTest::testValidSubmissionReturnsCleanedData":0.002,"ThesisCreateValidationTest::testMissingTitleThrowsException":0.003,"ThesisCreateValidationTest::testMissingAuthorsThrowsException":0.004,"ThesisCreateValidationTest::testMissingSynopsisThrowsException":0.002,"ThesisCreateValidationTest::testMissingOrientationInNonAdminModeThrowsException":0.002,"ThesisCreateValidationTest::testMissingAPProgramInNonAdminModeThrowsException":0.004,"ThesisCreateValidationTest::testMissingFinalityInNonAdminModeThrowsException":0.004,"ThesisCreateValidationTest::testInvalidYearFormatRejected":0.002,"ThesisCreateValidationTest::testYearZeroRejected":0.003,"ThesisCreateValidationTest::testYearBefore2000Rejected":0.002,"ThesisCreateValidationTest::testFarFutureYearRejected":0.002,"ThesisCreateValidationTest::testCurrentYearAccepted":0.002,"ThesisCreateValidationTest::testMalformedUrlRejected":0.004,"ThesisCreateValidationTest::testValidUrlAccepted":0.002,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":0.002,"ThesisCreateValidationTest::testMaxTenKeywordsEnforced":0.002,"ThesisCreateValidationTest::testXssPayloadStrippedFromTitle":0.003,"ThesisCreateValidationTest::testHtmlInSynopsisStripped":0.002,"ThesisCreateValidationTest::testMultipleAuthorsAreSorted":0.002,"ThesisCreateValidationTest::testMissingPromoteurInNonAdminModeThrowsException":0.002,"ThesisCreateValidationTest::testMissingLecteurInterneInNonAdminModeThrowsException":0.002,"ThesisCreateValidationTest::testMissingLanguagesInNonAdminModeThrowsException":0.003,"ThesisCreateValidationTest::testMissingFormatsInNonAdminModeThrowsException":0.003,"ThesisCreateValidationTest::testMissingLicenseWithLibreAccessThrowsException":0.003,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":0.007,"ThesisEditValidationTest::testLoadThrowsOnUnknownId":0.003,"ThesisEditValidationTest::testLoadThrowsOnInvalidId":0.002,"ThesisEditValidationTest::testLoadThrowsOnNegativeId":0.004,"ThesisEditValidationTest::testCollectJuryMembersEmptyInput":0.002,"ThesisEditValidationTest::testCollectJuryMembersSinglePromoteur":0.002,"ThesisEditValidationTest::testCollectJuryMembersPromoteurUlb":0.002,"ThesisEditValidationTest::testCollectJuryMembersLecteurs":0.002,"ThesisEditValidationTest::testCollectJuryMembersDeduplicatesEmptyStrings":0.002,"ThesisEditValidationTest::testCollectJuryMembersScalarPromoteurAccepted":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlStoresValidUrl":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsInvalidUrl":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsEmptyUrl":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlNormalisesHttp":0.003,"ErrorHandlerTest::testFkThesesTableMentionsAllPossibleFields":0.025,"ErrorHandlerTest::testFkApPrograms":0.002,"ErrorHandlerTest::testFkFinalityTypes":0.001,"ErrorHandlerTest::testFkThesisLanguages":0.002,"ErrorHandlerTest::testFkThesisFormats":0.002,"ErrorHandlerTest::testFkThesisTags":0.002,"ErrorHandlerTest::testFkThesisSupervisors":0.002,"ErrorHandlerTest::testFkAccessTypes":0.001,"ErrorHandlerTest::testFkLicenseTypes":0.002,"ErrorHandlerTest::testFkAuthors":0.002,"ErrorHandlerTest::testFkQuotedTableName":0.001,"ErrorHandlerTest::testFkQuotedLanguages":0.001,"ErrorHandlerTest::testFkQuotedFormatTypes":0.002,"ErrorHandlerTest::testFkReferencesTags":0.002,"ErrorHandlerTest::testFkReferencesOrientations":0.002,"ErrorHandlerTest::testFkUnknownTableGenericFallback":0.002,"ErrorHandlerTest::testFkEmptyMessageGenericFallback":0.002,"ErrorHandlerTest::testUniqueConstraint":0.002,"ErrorHandlerTest::testNotNullConstraint":0.002,"ErrorHandlerTest::testGenericPdoError":0.002,"ErrorHandlerTest::testDuplicateThesisExceptionPassesThrough":0.005,"ErrorHandlerTest::testValidationExceptionPassesThrough":0.002,"ErrorHandlerTest::testGenericExceptionPassesThrough":0.002,"ErrorHandlerTest::testTypeErrorReturnsGeneric":0.002,"ErrorHandlerTest::testLogWithContext":0.002,"ErrorHandlerTest::testLogWithNullValues":0.002,"ErrorHandlerTest::testLogWithEmptyExtra":0.002,"ErrorHandlerTest::testFkQuotedColumnNames":0.002,"ErrorHandlerTest::testFkUpdateStatement":0.001,"ErrorHandlerTest::testFkWithReferencesAndInsert":0.002,"PureLogicTest::testSplitJuryByRoleAllRoles":0.042,"PureLogicTest::testSplitJuryByRoleEmptyNameSkipped":0.002,"PureLogicTest::testSplitJuryByRoleEmptyJury":0.002,"PureLogicTest::testCollectCaptionPathsVttByMime":0.002,"PureLogicTest::testCollectCaptionPathsVttByExtension":0.002,"PureLogicTest::testCollectCaptionPathsNoVttReturnsEmpty":0.002,"PureLogicTest::testDetectFileTypeByMime":0.002,"PureLogicTest::testDetectFileTypeByExtensionFallback":0.002,"SearchControllerTest::testHandleSearchReturnsCoverMapKey":0.057,"SearchControllerTest::testCoverMapContainsKnownThesis":0.005}} \ No newline at end of file diff --git a/app/public/assets/js/app/acces-password.js b/app/public/assets/js/app/acces-password.js index 440743b..e1f2121 100644 --- a/app/public/assets/js/app/acces-password.js +++ b/app/public/assets/js/app/acces-password.js @@ -6,25 +6,31 @@ * * Provides visual feedback on the originating button. */ -(function () { - 'use strict'; - - window.copyTextToClipboard = function (text) { - if (!text) return; - navigator.clipboard.writeText(text).then(function () { - var btn = window.event && window.event.target ? window.event.target.closest('button') : null; - if (btn) { - var origTitle = btn.getAttribute('title') || ''; - var origHTML = btn.innerHTML; - btn.setAttribute('title', '\u2713 Copi\u00e9'); - btn.innerHTML = '\u2713'; - setTimeout(function () { - btn.setAttribute('title', origTitle); - btn.innerHTML = origHTML; - }, 1200); - } - }).catch(function () { - // Clipboard write failed — silently ignore - }); - }; +(() => { + window.copyTextToClipboard = (text) => { + var btn; + var origTitle; + var origHTML; + if (!text) return; + navigator.clipboard + .writeText(text) + .then(() => { + btn = window.event?.target + ? window.event.target.closest("button") + : null; + if (btn) { + origTitle = btn.getAttribute("title") || ""; + origHTML = btn.innerHTML; + btn.setAttribute("title", "\u2713 Copi\u00e9"); + btn.innerHTML = "\u2713"; + setTimeout(() => { + btn.setAttribute("title", origTitle); + btn.innerHTML = origHTML; + }, 1200); + } + }) + .catch(() => { + // Clipboard write failed — silently ignore + }); + }; })(); diff --git a/app/public/assets/js/app/access-request.js b/app/public/assets/js/app/access-request.js index 471f0f3..4d84d7c 100644 --- a/app/public/assets/js/app/access-request.js +++ b/app/public/assets/js/app/access-request.js @@ -12,86 +12,90 @@ * #access-justification — justification textarea * #access-request-message — message display div */ -(function () { - 'use strict'; +(() => { + var form = document.getElementById("access-request-form"); + if (!form) return; - var form = document.getElementById('access-request-form'); - if (!form) return; + var emailInput = document.getElementById("access-email"); + var justificationContainer = document.getElementById( + "justification-container", + ); + var justificationInput = document.getElementById("access-justification"); + var messageDiv = document.getElementById("access-request-message"); - var emailInput = document.getElementById('access-email'); - var justificationContainer = document.getElementById('justification-container'); - var justificationInput = document.getElementById('access-justification'); - var messageDiv = document.getElementById('access-request-message'); + if (!emailInput || !messageDiv) return; - if (!emailInput || !messageDiv) return; + // Show/hide justification based on email domain + emailInput.addEventListener("input", function () { + var email = this.value.trim().toLowerCase(); + var isErg = email.endsWith("@erg.school") || email.endsWith("@erg.be"); + if (justificationContainer) + justificationContainer.style.display = isErg ? "none" : "block"; + if (justificationInput) justificationInput.required = !isErg; + }); - // Show/hide justification based on email domain - emailInput.addEventListener('input', function () { - var email = this.value.trim().toLowerCase(); - var isErg = email.endsWith('@erg.school') || email.endsWith('@erg.be'); - if (justificationContainer) justificationContainer.style.display = isErg ? 'none' : 'block'; - if (justificationInput) justificationInput.required = !isErg; - }); + function showRetryPrompt(rejectedEmail, serverMessage) { + messageDiv.style.display = "block"; + messageDiv.className = "tfe-access-message tfe-access-error"; + messageDiv.innerHTML = + "Adresse e-mail introuvable sur le serveur de l'ERG.
" + + "" + + serverMessage.replace(/

" + + "Corrigez votre adresse e-mail et réessayez."; + emailInput.value = rejectedEmail; + emailInput.classList.add("input-error"); + emailInput.focus(); + emailInput.select(); + emailInput.addEventListener("input", function clearError() { + emailInput.classList.remove("input-error"); + emailInput.removeEventListener("input", clearError); + }); + } - function showRetryPrompt(rejectedEmail, serverMessage) { - messageDiv.style.display = 'block'; - messageDiv.className = 'tfe-access-message tfe-access-error'; - messageDiv.innerHTML = - 'Adresse e-mail introuvable sur le serveur de l\'ERG.
' + - '' + serverMessage.replace(/

' + - 'Corrigez votre adresse e-mail et réessayez.'; - emailInput.value = rejectedEmail; - emailInput.classList.add('input-error'); - emailInput.focus(); - emailInput.select(); - emailInput.addEventListener('input', function clearError() { - emailInput.classList.remove('input-error'); - emailInput.removeEventListener('input', clearError); - }); - } + form.addEventListener("submit", (e) => { + e.preventDefault(); - form.addEventListener('submit', function (e) { - e.preventDefault(); + var submitBtn = form.querySelector('button[type="submit"]'); + submitBtn.disabled = true; + submitBtn.textContent = "Envoi en cours..."; + messageDiv.style.display = "none"; - var submitBtn = form.querySelector('button[type="submit"]'); - submitBtn.disabled = true; - submitBtn.textContent = 'Envoi en cours...'; - messageDiv.style.display = 'none'; + var submittedEmail = emailInput.value.trim(); + var formData = new FormData(form); + formData.append("thesis_id", form.getAttribute("data-thesis-id")); - var submittedEmail = emailInput.value.trim(); - var formData = new FormData(form); - formData.append('thesis_id', form.getAttribute('data-thesis-id')); + fetch("/request-access", { + method: "POST", + body: formData, + }) + .then((response) => response.json()) + .then((data) => { + submitBtn.disabled = false; + submitBtn.textContent = "Demander l'accès"; - fetch('/request-access', { - method: 'POST', - body: formData - }) - .then(function (response) { return response.json(); }) - .then(function (data) { - submitBtn.disabled = false; - submitBtn.textContent = 'Demander l\'accès'; + if (data.status === "recipient_rejected") { + showRetryPrompt(submittedEmail, data.message); + return; + } - if (data.status === 'recipient_rejected') { - showRetryPrompt(submittedEmail, data.message); - return; - } - - messageDiv.style.display = 'block'; - if (data.success) { - messageDiv.className = 'tfe-access-message tfe-access-success'; - messageDiv.textContent = data.message; - form.reset(); - } else { - messageDiv.className = 'tfe-access-message tfe-access-error'; - messageDiv.textContent = data.message || 'Une erreur est survenue. Veuillez réessayer.'; - } - }) - .catch(function () { - submitBtn.disabled = false; - submitBtn.textContent = 'Demander l\'accès'; - messageDiv.style.display = 'block'; - messageDiv.className = 'tfe-access-message tfe-access-error'; - messageDiv.textContent = 'Erreur de connexion. Veuillez réessayer.'; - }); - }); + messageDiv.style.display = "block"; + if (data.success) { + messageDiv.className = "tfe-access-message tfe-access-success"; + messageDiv.textContent = data.message; + form.reset(); + } else { + messageDiv.className = "tfe-access-message tfe-access-error"; + messageDiv.textContent = + data.message || "Une erreur est survenue. Veuillez réessayer."; + } + }) + .catch(() => { + submitBtn.disabled = false; + submitBtn.textContent = "Demander l'accès"; + messageDiv.style.display = "block"; + messageDiv.className = "tfe-access-message tfe-access-error"; + messageDiv.textContent = "Erreur de connexion. Veuillez réessayer."; + }); + }); })(); diff --git a/app/public/assets/js/app/admin-logs.js b/app/public/assets/js/app/admin-logs.js index 1c84231..5fdfddf 100644 --- a/app/public/assets/js/app/admin-logs.js +++ b/app/public/assets/js/app/admin-logs.js @@ -5,60 +5,66 @@ * - copyLogContent(btn) — copy visible log lines to clipboard * - HTMX afterSwap handler to update active tab class on #sys-tab-panel */ -(function () { - 'use strict'; +(() => { + window.copyLogContent = (btn) => { + var logOut = document.querySelector("#log-output"); + if (!logOut) return; + var text = Array.from(logOut.querySelectorAll(".log-line")) + .map((el) => el.textContent) + .join("\n"); + if (navigator.clipboard?.writeText) { + navigator.clipboard.writeText(text).then(() => { + btn.textContent = "\u2713 Copi\u00e9"; + btn.classList.add("copied"); + setTimeout(() => { + btn.textContent = "Copier"; + btn.classList.remove("copied"); + }, 2000); + }); + } else { + window._fallbackCopy(text, btn); + } + }; - window.copyLogContent = function (btn) { - var logOut = document.querySelector('#log-output'); - if (!logOut) return; - var text = Array.from(logOut.querySelectorAll('.log-line')) - .map(function (el) { return el.textContent; }).join('\n'); - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(text).then(function () { - btn.textContent = '\u2713 Copi\u00e9'; - btn.classList.add('copied'); - setTimeout(function () { btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000); - }); - } else { - window._fallbackCopy(text, btn); - } - }; + window._fallbackCopy = (text, btn) => { + var ta = document.createElement("textarea"); + ta.value = text; + ta.style.cssText = "position:fixed;opacity:0"; + document.body.appendChild(ta); + ta.select(); + try { + document.execCommand("copy"); + btn.textContent = "\u2713 Copi\u00e9"; + btn.classList.add("copied"); + setTimeout(() => { + btn.textContent = "Copier"; + btn.classList.remove("copied"); + }, 2000); + } catch (_e) {} + document.body.removeChild(ta); + }; - window._fallbackCopy = function (text, btn) { - var ta = document.createElement('textarea'); - ta.value = text; - ta.style.cssText = 'position:fixed;opacity:0'; - document.body.appendChild(ta); - ta.select(); - try { - document.execCommand('copy'); - btn.textContent = '\u2713 Copi\u00e9'; - btn.classList.add('copied'); - setTimeout(function () { btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000); - } catch (e) {} - document.body.removeChild(ta); - }; - - // Update active tab class after each HTMX swap on #sys-tab-panel - document.body.addEventListener('htmx:afterSwap', function (evt) { - if (!(evt.detail.target && evt.detail.target.id === 'sys-tab-panel')) return; - var rc = evt.detail.requestConfig; - var tab = null; - // Tab clicks carry ?tab=… in the path - var qIdx = rc.path.indexOf('?'); - if (qIdx !== -1) { - tab = new URLSearchParams(rc.path.substring(qIdx + 1)).get('tab'); - } - // Line-count form sends tab via hx-vals in parameters - if (!tab && rc.parameters && rc.parameters.tab) { - tab = rc.parameters.tab; - } - if (!tab) return; - document.querySelectorAll('.sys-tabs .sys-tab').forEach(function (a) { - var isActive = a.getAttribute('data-tab') === tab; - a.classList.toggle('active', isActive); - if (isActive) a.setAttribute('aria-current', 'page'); - else a.removeAttribute('aria-current'); - }); - }); + // Update active tab class after each HTMX swap on #sys-tab-panel + document.body.addEventListener("htmx:afterSwap", (evt) => { + if (!(evt.detail.target && evt.detail.target.id === "sys-tab-panel")) + return; + var rc = evt.detail.requestConfig; + var tab = null; + // Tab clicks carry ?tab=… in the path + var qIdx = rc.path.indexOf("?"); + if (qIdx !== -1) { + tab = new URLSearchParams(rc.path.substring(qIdx + 1)).get("tab"); + } + // Line-count form sends tab via hx-vals in parameters + if (!tab && rc.parameters && rc.parameters.tab) { + tab = rc.parameters.tab; + } + if (!tab) return; + document.querySelectorAll(".sys-tabs .sys-tab").forEach((a) => { + var isActive = a.getAttribute("data-tab") === tab; + a.classList.toggle("active", isActive); + if (isActive) a.setAttribute("aria-current", "page"); + else a.removeAttribute("aria-current"); + }); + }); })(); diff --git a/app/public/assets/js/app/autosave-handler.js b/app/public/assets/js/app/autosave-handler.js index 33257f5..546ecfc 100644 --- a/app/public/assets/js/app/autosave-handler.js +++ b/app/public/assets/js/app/autosave-handler.js @@ -5,58 +5,62 @@ * parse errors instead of silently swallowing them (unlike the * old autosave.js .catch(() => {}) pattern). */ -function handleAutosaveResponse(event) { - const form = event.target.closest("form"); - const status = form ? form.querySelector("[data-autosave-status]") : null; +function _handleAutosaveResponse(event) { + const form = event.target.closest("form"); + const status = form ? form.querySelector("[data-autosave-status]") : null; - if (!event.detail.successful) { - if (status) { - status.textContent = "Erreur !"; - status.className = "autosave-status autosave-status--error"; - } - return; - } + if (!event.detail.successful) { + if (status) { + status.textContent = "Erreur !"; + status.className = "autosave-status autosave-status--error"; + } + return; + } - try { - const data = JSON.parse(event.detail.xhr.responseText); + try { + const data = JSON.parse(event.detail.xhr.responseText); - // Rotate CSRF token in both the form and the meta tag - if (data.csrf_token) { - const csrfInput = form.querySelector('input[name="csrf_token"]'); - if (csrfInput) csrfInput.value = data.csrf_token; - const meta = document.querySelector('meta[name="csrf-token"]'); - if (meta) meta.content = data.csrf_token; - } + // Rotate CSRF token in the form, any share_link_token fields, and the meta tag + if (data.csrf_token) { + const csrfInput = form.querySelector('input[name="csrf_token"]'); + if (csrfInput) csrfInput.value = data.csrf_token; + const shareTokenInput = form.querySelector( + 'input[name="share_link_token"]', + ); + if (shareTokenInput) shareTokenInput.value = data.csrf_token; + const meta = document.querySelector('meta[name="csrf-token"]'); + if (meta) meta.content = data.csrf_token; + } - if (status) { - if (data.success) { - status.textContent = "Enregistré ✓"; - status.className = "autosave-status autosave-status--saved"; - } else { - status.textContent = "Erreur !"; - status.className = "autosave-status autosave-status--error"; - } - } - } catch { - // JSON parse failed (e.g. PHP warning in output) — surface it - if (status) { - status.textContent = "Erreur !"; - status.className = "autosave-status autosave-status--error"; - } - console.warn( - "Autosave: could not parse response", - event.detail.xhr.responseText, - ); - } + if (status) { + if (data.success) { + status.textContent = "Enregistré ✓"; + status.className = "autosave-status autosave-status--saved"; + } else { + status.textContent = "Erreur !"; + status.className = "autosave-status autosave-status--error"; + } + } + } catch { + // JSON parse failed (e.g. PHP warning in output) — surface it + if (status) { + status.textContent = "Erreur !"; + status.className = "autosave-status autosave-status--error"; + } + console.warn( + "Autosave: could not parse response", + event.detail.xhr.responseText, + ); + } } // Show saving indicator while request is in flight document.body.addEventListener("htmx:beforeRequest", (e) => { - const el = e.target; - if (!el) return; - const status = el.querySelector("[data-autosave-status]"); - if (status) { - status.textContent = "Enregistrement…"; - status.className = "autosave-status autosave-status--saving"; - } + const el = e.target; + if (!el) return; + const status = el.querySelector("[data-autosave-status]"); + if (status) { + status.textContent = "Enregistrement…"; + status.className = "autosave-status autosave-status--saving"; + } }); diff --git a/app/public/assets/js/app/beforeunload-guard.js b/app/public/assets/js/app/beforeunload-guard.js index c5a7455..db4d3e2 100644 --- a/app/public/assets/js/app/beforeunload-guard.js +++ b/app/public/assets/js/app/beforeunload-guard.js @@ -6,20 +6,27 @@ * No effect when JavaScript is unavailable (form posts normally). */ (() => { - const forms = document.querySelectorAll('form[data-beforeunload-guard]'); - if (!forms.length) return; + const forms = document.querySelectorAll("form[data-beforeunload-guard]"); + if (!forms.length) return; - let dirty = false; + let dirty = false; - for (const form of forms) { - form.addEventListener('input', () => { dirty = true; }); - form.addEventListener('change', () => { dirty = true; }); - form.addEventListener('submit', () => { dirty = false; window.__xamxamDirty = false; }); - } + for (const form of forms) { + form.addEventListener("input", () => { + dirty = true; + }); + form.addEventListener("change", () => { + dirty = true; + }); + form.addEventListener("submit", () => { + dirty = false; + window.__xamxamDirty = false; + }); + } - window.addEventListener('beforeunload', (e) => { - if (dirty || window.__xamxamDirty) { - e.preventDefault(); - } - }); + window.addEventListener("beforeunload", (e) => { + if (dirty || window.__xamxamDirty) { + e.preventDefault(); + } + }); })(); diff --git a/app/public/assets/js/app/clipboard.js b/app/public/assets/js/app/clipboard.js index 27a9101..1ae4453 100644 --- a/app/public/assets/js/app/clipboard.js +++ b/app/public/assets/js/app/clipboard.js @@ -8,31 +8,32 @@ * Or with a custom selector pattern: * */ -(function () { - 'use strict'; +(() => { + window.copyUrl = (id) => { + var input = document.getElementById(`url-${id}`); + if (input) { + window.copyUrlFrom(input); + } + }; - window.copyUrl = function (id) { - var input = document.getElementById('url-' + id); - if (input) { - window.copyUrlFrom(input); - } - }; - - window.copyUrlFrom = function (sourceEl) { - var text = sourceEl.value || sourceEl.textContent || ''; - if (!text) return; - navigator.clipboard.writeText(text).then(function () { - var btn = window.event && window.event.target ? window.event.target.closest('button') : null; - if (btn) { - var origTitle = btn.getAttribute('title') || ''; - var origText = btn.textContent; - btn.setAttribute('title', '\u2713 Copi\u00e9'); - btn.textContent = '\u2713 Copi\u00e9'; - setTimeout(function () { - btn.setAttribute('title', origTitle); - btn.textContent = origText; - }, 1200); - } - }); - }; + window.copyUrlFrom = (sourceEl) => { + var text = sourceEl.value || sourceEl.textContent || ""; + var btn; + var origTitle; + var origText; + if (!text) return; + navigator.clipboard.writeText(text).then(() => { + btn = window.event?.target ? window.event.target.closest("button") : null; + if (btn) { + origTitle = btn.getAttribute("title") || ""; + origText = btn.textContent; + btn.setAttribute("title", "\u2713 Copi\u00e9"); + btn.textContent = "\u2713 Copi\u00e9"; + setTimeout(() => { + btn.setAttribute("title", origTitle); + btn.textContent = origText; + }, 1200); + } + }); + }; })(); diff --git a/app/public/assets/js/app/file-upload-filepond.js b/app/public/assets/js/app/file-upload-filepond.js index c58a358..c3d9740 100644 --- a/app/public/assets/js/app/file-upload-filepond.js +++ b/app/public/assets/js/app/file-upload-filepond.js @@ -56,8 +56,8 @@ // so numeric literals are required (string suffixes like "1GB" become // parseInt("1GB") = 1 byte inside the plugin). perExtensionMaxSize: { - pdf: 104857600, // 100 MB - mp4: 8589934592, // 8 GB + pdf: 104857600, // 100 MB + mp4: 8589934592, // 8 GB webm: 8589934592, ogv: 8589934592, mov: 8589934592, @@ -123,7 +123,7 @@ */ function parseSize(str) { // Already a number (bytes) — pass through - if (typeof str === 'number') return str; + if (typeof str === "number") return str; var m = str.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$/i); if (!m) return 0; var val = parseFloat(m[1]); @@ -187,7 +187,7 @@ var oldHidden = form.querySelectorAll( `input[name='queue_file[${queueType}][]'][data-filepond-id]`, ); - for (var h = 0; h < oldHidden.length; h++) { + for (let h = 0; h < oldHidden.length; h++) { oldHidden[h].remove(); } @@ -195,13 +195,13 @@ // Create hidden inputs per file: queue_file[][] = serverId var ids = []; - for (var i = 0; i < files.length; i++) { - var f = files[i]; + for (let i = 0; i < files.length; i++) { + const f = files[i]; // Only include files that have been uploaded and have a serverId - var id = f.serverId || null; + const id = f.serverId || null; if (id) { ids.push(id); - var hidden = document.createElement("input"); + const hidden = document.createElement("input"); hidden.type = "hidden"; hidden.name = `queue_file[${queueType}][]`; hidden.value = id; @@ -212,7 +212,7 @@ // Create order input if (ids.length > 0) { - var orderInput = document.createElement("input"); + const orderInput = document.createElement("input"); orderInput.type = "hidden"; orderInput.name = `queue_order[${queueType}]`; orderInput.value = ids.join("|"); @@ -257,8 +257,11 @@ // return a distinguishable error marker instead of a valid serverId. // Throwing here crashes FilePond internally (no try/catch in the wrapper). if (id.length > 64 || /[<>\n\r]/.test(id)) { - console.error("[filepond] process onload | unexpected response | body=" + id.substring(0, 200)); - return "__error__" + id.substring(0, 32); + console.error( + "[filepond] process onload | unexpected response | body=" + + id.substring(0, 200), + ); + return `__error__${id.substring(0, 32)}`; } console.log(`[filepond] process onload | serverId=${id}`); return id; // file_id stored as serverId @@ -266,8 +269,13 @@ onerror: (response) => { // response is the raw XHR response text (string), not an XHR object. // Log it and return a human-readable error message. - var body = typeof response === 'string' ? response : (response && response.body ? response.body : String(response || '')); - console.error("[filepond] process onerror | body=" + body); + var body = + typeof response === "string" + ? response + : response?.body + ? response.body + : String(response || ""); + console.error(`[filepond] process onerror | body=${body}`); return body || "Erreur lors du téléversement."; }, }, @@ -280,7 +288,7 @@ console.log("[filepond] revert OK"); }, onerror: (r) => { - var body = typeof r === 'string' ? r : (r && r.body ? r.body : ''); + var body = typeof r === "string" ? r : r?.body ? r.body : ""; console.error(`[filepond] revert ERROR | body=${body || r}`); }, }, @@ -293,8 +301,13 @@ return response; }, onerror: (response) => { - var body = typeof response === 'string' ? response : (response && response.body ? response.body : String(response || '')); - console.error("[filepond] load onerror | body=" + body); + var body = + typeof response === "string" + ? response + : response?.body + ? response.body + : String(response || ""); + console.error(`[filepond] load onerror | body=${body}`); // Return a descriptive error — FilePond will fire an error event. return body || "Fichier introuvable."; }, @@ -311,7 +324,12 @@ body: source, }) .then((r) => { - console.log("[filepond] revert (from remove) response | ok=" + r.ok + " | status=" + r.status); + console.log( + "[filepond] revert (from remove) response | ok=" + + r.ok + + " | status=" + + r.status, + ); r.ok ? load() : error("Erreur suppression"); }) .catch((e) => { @@ -403,7 +421,7 @@ var f = item.file; var ext = getExt(f.name); if (ext && perExtMax[ext]) { - var limit = parseSize(perExtMax[ext]); + const limit = parseSize(perExtMax[ext]); if (limit > 0 && f.size > limit) { return false; } @@ -503,20 +521,20 @@ if (pond) { try { // Remove order/hidden inputs before destroying - var form = input.closest("form"); + const form = input.closest("form"); if (form) { - var queueType = input.dataset.queueType || null; + const queueType = input.dataset.queueType || null; if (queueType) { - var orderInput = form.querySelector( + const orderInput = form.querySelector( `input[name='queue_order[${queueType}]']`, ); if (orderInput) orderInput.remove(); - var hiddenInputs = form.querySelectorAll( + const hiddenInputs = form.querySelectorAll( "input[name='queue_file[" + queueType + "][]'][data-filepond-id]", ); - for (var h = 0; h < hiddenInputs.length; h++) { + for (let h = 0; h < hiddenInputs.length; h++) { hiddenInputs[h].remove(); } } @@ -524,14 +542,16 @@ // Abort any in-flight uploads before destroying to prevent // FilePond internal crashes when XHR callbacks fire on a // torn-down instance ("can't access property main"). - var files = pond.getFiles(); - for (var i = 0; i < files.length; i++) { - var f = files[i]; + const files = pond.getFiles(); + for (let i = 0; i < files.length; i++) { + const f = files[i]; if (f.status === 4 || f.status === 2 || f.status === 3) { // FileStatus: PROCESSING=4, PROCESSING_QUEUED=2, PROCESSING=4 // (FilePond 4.x internal: 4 = processing) // Abort processing to avoid stale XHR callbacks - try { pond.removeFile(f); } catch (_abort) {} + try { + pond.removeFile(f); + } catch (_abort) {} } } pond.destroy(); @@ -612,13 +632,15 @@ setTimeout(tryRegisterHtmx, 50); return; } - console.log('[filepond] htmx detected, registering swap listeners'); + console.log("[filepond] htmx detected, registering swap listeners"); window.htmx.on("htmx:beforeSwap", onHtmxBeforeSwap); window.htmx.on("htmx:afterSwap", () => { enableFilepondMode(); _xamxamFilepondReady = false; window.XamxamInitFilePonds(); - setTimeout(() => { _xamxamFilepondReady = true; }, 0); + setTimeout(() => { + _xamxamFilepondReady = true; + }, 0); }); } // ── Enable filepond_mode hidden input (no-JS safety) ──────────────── @@ -627,7 +649,7 @@ // will handle uploads asynchronously. function enableFilepondMode() { var inputs = document.querySelectorAll("input[name='filepond_mode']"); - for (var i = 0; i < inputs.length; i++) { + for (let i = 0; i < inputs.length; i++) { inputs[i].disabled = false; inputs[i].value = "1"; } @@ -675,48 +697,81 @@ * The file browser is loaded inside #relink-modal-body via HTMX. */ window.XamxamRelinkFile = (el) => { - var li = el.closest('.file-browser-entry'); - console.log('[relink] XamxamRelinkFile called | el=', el, '| li=', li); + var li = el.closest(".file-browser-entry"); + console.log("[relink] XamxamRelinkFile called | el=", el, "| li=", li); if (!li) return; var ctx = window.__xamxamRelinkCtx || {}; - var thesisId = ctx.thesisId; + var thesisId = ctx.thesisId; var queueType = ctx.queueType; var filePath = li.dataset.filePath; var fileName = li.dataset.fileName; var fileSize = parseInt(li.dataset.fileSize, 10) || 0; - var ext = li.dataset.fileExt || ''; + var ext = li.dataset.fileExt || ""; - console.log('[relink] data | thesisId=' + thesisId + ' | queueType=' + queueType + ' | filePath=' + filePath + ' | fileName=' + fileName + ' | ext=' + ext); + console.log( + "[relink] data | thesisId=" + + thesisId + + " | queueType=" + + queueType + + " | filePath=" + + filePath + + " | fileName=" + + fileName + + " | ext=" + + ext, + ); if (!filePath || !thesisId || !queueType) { - console.error('[relink] missing data', { filePath, thesisId, queueType }); + console.error("[relink] missing data", { filePath, thesisId, queueType }); return; } // Determine MIME from extension var mimeMap = { - pdf: 'application/pdf', - jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', gif: 'image/gif', - mp4: 'video/mp4', webm: 'video/webm', ogv: 'video/ogg', mov: 'video/quicktime', - mp3: 'audio/mpeg', ogg: 'audio/ogg', wav: 'audio/wav', flac: 'audio/flac', aac: 'audio/aac', m4a: 'audio/mp4', - vtt: 'text/vtt', - zip: 'application/zip', tar: 'application/x-tar', gz: 'application/gzip', + pdf: "application/pdf", + jpg: "image/jpeg", + jpeg: "image/jpeg", + png: "image/png", + webp: "image/webp", + gif: "image/gif", + mp4: "video/mp4", + webm: "video/webm", + ogv: "video/ogg", + mov: "video/quicktime", + mp3: "audio/mpeg", + ogg: "audio/ogg", + wav: "audio/wav", + flac: "audio/flac", + aac: "audio/aac", + m4a: "audio/mp4", + vtt: "text/vtt", + zip: "application/zip", + tar: "application/x-tar", + gz: "application/gzip", }; - var mimeType = mimeMap[ext] || 'application/octet-stream'; + var mimeType = mimeMap[ext] || "application/octet-stream"; - var csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; - console.log('[relink] csrfToken=' + (csrfToken ? csrfToken.substring(0, 8) + '...' : 'MISSING')); + var csrfToken = + document + .querySelector('meta[name="csrf-token"]') + ?.getAttribute("content") || ""; + console.log( + "[relink] csrfToken=" + + (csrfToken ? `${csrfToken.substring(0, 8)}...` : "MISSING"), + ); - var bodyEl = document.getElementById('relink-modal-body'); - if (bodyEl) bodyEl.innerHTML = '

Reliage en cours…

'; + var bodyEl = document.getElementById("relink-modal-body"); + if (bodyEl) + bodyEl.innerHTML = + '

Reliage en cours…

'; - fetch('/admin/actions/filepond/relink.php', { - method: 'POST', + fetch("/admin/actions/filepond/relink.php", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'X-CSRF-Token': csrfToken, + "Content-Type": "application/json", + "X-CSRF-Token": csrfToken, }, body: JSON.stringify({ thesis_id: parseInt(thesisId, 10), @@ -727,71 +782,102 @@ queue_type: queueType, }), }) - .then(r => r.json().then(data => ({ ok: r.ok, status: r.status, data }))) - .then(({ ok, status, data }) => { - if (!ok || (data && data.ok === false)) { - var msg = (data && data.error) ? data.error : (typeof data === 'string' ? data : 'Erreur ' + status); - if (bodyEl) bodyEl.innerHTML = `

Erreur : ${msg}

`; - return; - } - console.log('[relink] success | new_id=' + data.id); - - // Add the new file to the FilePond pool, then close the modal. - // If the DOM was replaced (e.g. live-reload), refresh the - // form fragment via HTMX so the server re-renders the pools - // with the newly-linked file included. - var input = document.querySelector(`.tfe-file-picker[data-queue-type="${queueType}"]`); - console.log('[relink] looking for input | selector=' + `.tfe-file-picker[data-queue-type="${queueType}"]` + ' | found=' + !!input); - var closeAndRefresh = function() { - var modal = document.getElementById('relink-modal'); - if (modal) modal.close(); - // Re-fetch the fichiers fragment from the server so the - // newly-linked file appears in the FilePond pools. - var block = document.getElementById('format-fichiers-block'); - if (block && window.htmx) { - var url = '/admin/fragments/fichiers.php'; - if (window.__xamxamRelinkCtx && window.__xamxamRelinkCtx.thesisId) { - url += '?_thesis_id=' + encodeURIComponent(window.__xamxamRelinkCtx.thesisId); - } - htmx.ajax('GET', url, { - target: '#format-fichiers-block', - swap: 'outerHTML' - }); + .then((r) => + r.json().then((data) => ({ ok: r.ok, status: r.status, data })), + ) + .then(({ ok, status, data }) => { + if (!ok || (data && data.ok === false)) { + const msg = data?.error + ? data.error + : typeof data === "string" + ? data + : `Erreur ${status}`; + if (bodyEl) + bodyEl.innerHTML = `

Erreur : ${msg}

`; + return; } - }; - if (input) { - var pond = FilePond.find(input); - console.log('[relink] looking for pond | found=' + !!pond); - if (pond) { - pond.addFile(String(data.id), { - type: 'limbo', - file: { - name: fileName, - size: fileSize, - type: mimeType + console.log(`[relink] success | new_id=${data.id}`); + + // Add the new file to the FilePond pool, then close the modal. + // If the DOM was replaced (e.g. live-reload), refresh the + // form fragment via HTMX so the server re-renders the pools + // with the newly-linked file included. + var input = document.querySelector( + `.tfe-file-picker[data-queue-type="${queueType}"]`, + ); + console.log( + "[relink] looking for input | selector=" + + `.tfe-file-picker[data-queue-type="${queueType}"]` + + " | found=" + + !!input, + ); + var closeAndRefresh = () => { + var modal = document.getElementById("relink-modal"); + if (modal) modal.close(); + // Re-fetch the fichiers fragment from the server so the + // newly-linked file appears in the FilePond pools. + var block = document.getElementById("format-fichiers-block"); + if (block && window.htmx) { + let url = "/admin/fragments/fichiers.php"; + if (window.__xamxamRelinkCtx?.thesisId) { + url += + "?_thesis_id=" + + encodeURIComponent(window.__xamxamRelinkCtx.thesisId); } - }).then(function() { - console.log('[relink] addFile resolved | source=' + String(data.id) + ' | queueType=' + queueType); + htmx.ajax("GET", url, { + target: "#format-fichiers-block", + swap: "outerHTML", + }); + } + }; + if (input) { + const pond = FilePond.find(input); + console.log(`[relink] looking for pond | found=${!!pond}`); + if (pond) { + pond + .addFile(String(data.id), { + type: "limbo", + file: { + name: fileName, + size: fileSize, + type: mimeType, + }, + }) + .then(() => { + console.log( + "[relink] addFile resolved | source=" + + String(data.id) + + " | queueType=" + + queueType, + ); + closeAndRefresh(); + }) + .catch((err) => { + console.error("[relink] addFile rejected", err); + closeAndRefresh(); + }); + } else { + console.error( + "[relink] FilePond.find returned null for input", + input, + ); closeAndRefresh(); - }).catch(function(err) { - console.error('[relink] addFile rejected', err); - closeAndRefresh(); - }); + } } else { - console.error('[relink] FilePond.find returned null for input', input); + console.warn( + "[relink] input not found, page may have reloaded | queueType=" + + queueType, + ); closeAndRefresh(); } - } else { - console.warn('[relink] input not found, page may have reloaded | queueType=' + queueType); - closeAndRefresh(); - } - // Mark form dirty - window.__xamxamDirty = true; - }) - .catch(err => { - console.error('[relink] fetch error', err); - if (bodyEl) bodyEl.innerHTML = '

Erreur réseau.

'; - }); + // Mark form dirty + window.__xamxamDirty = true; + }) + .catch((err) => { + console.error("[relink] fetch error", err); + if (bodyEl) + bodyEl.innerHTML = '

Erreur réseau.

'; + }); }; })(); diff --git a/app/public/assets/js/app/jury-autocomplete.js b/app/public/assets/js/app/jury-autocomplete.js index 0b78271..dd13590 100644 --- a/app/public/assets/js/app/jury-autocomplete.js +++ b/app/public/assets/js/app/jury-autocomplete.js @@ -11,130 +11,142 @@ * data-jury-hx-post — HTMX endpoint URL (required) * data-jury-hx-target — CSS selector for the shared dropdown (optional) */ -(function () { - 'use strict'; +(() => { + function initAll() { + document + .querySelectorAll( + "[data-jury-autocomplete]:not([data-jury-autocomplete-initialized])", + ) + .forEach((fieldset) => { + fieldset.setAttribute("data-jury-autocomplete-initialized", "1"); + initFieldset(fieldset); + }); + } - function initAll() { - document.querySelectorAll('[data-jury-autocomplete]:not([data-jury-autocomplete-initialized])').forEach(function (fieldset) { - fieldset.setAttribute('data-jury-autocomplete-initialized', '1'); - initFieldset(fieldset); - }); - } + document.addEventListener("DOMContentLoaded", initAll); + document.body.addEventListener("htmx:afterSwap", initAll); - document.addEventListener('DOMContentLoaded', initAll); - document.body.addEventListener('htmx:afterSwap', initAll); + function initFieldset(fieldset) { + var list; + var activeInput; + var selectedIdx; + var debounceTimer; - function initFieldset(fieldset) { - var hxPost = fieldset.getAttribute('data-jury-hx-post') || '/admin/fragments/pill-search.php'; - var role = fieldset.getAttribute('data-jury-role') || ''; - var dropdown = fieldset.querySelector('.jury-suggestions'); - if (!dropdown) { - dropdown = document.createElement('div'); - dropdown.className = 'jury-suggestions tag-search-suggestions'; - dropdown.setAttribute('role', 'listbox'); - // Insert after the list container - var list = fieldset.querySelector('.admin-jury-list'); - if (list) { - list.insertAdjacentElement('afterend', dropdown); - } else { - fieldset.appendChild(dropdown); - } - } + var hxPost = + fieldset.getAttribute("data-jury-hx-post") || + "/admin/fragments/pill-search.php"; + var role = fieldset.getAttribute("data-jury-role") || ""; + var dropdown = fieldset.querySelector(".jury-suggestions"); + if (!dropdown) { + dropdown = document.createElement("div"); + dropdown.className = "jury-suggestions tag-search-suggestions"; + dropdown.setAttribute("role", "listbox"); + // Insert after the list container + list = fieldset.querySelector(".admin-jury-list"); + if (list) { + list.insertAdjacentElement("afterend", dropdown); + } else { + fieldset.appendChild(dropdown); + } + } - var activeInput = null; - var selectedIdx = -1; - var debounceTimer = null; + // Click on suggestion → fill the active input + dropdown.addEventListener("click", (e) => { + var btn = e.target.closest(".tag-search-item"); + if (!btn) return; + var name = (btn.getAttribute("data-tag-name") || "").trim(); + if (!name || !activeInput) return; + activeInput.value = btn.classList.contains("tag-search-item--create") + ? activeInput.value.trim() + : name; + dropdown.innerHTML = ""; + selectedIdx = -1; + activeInput.focus(); + }); - // Click on suggestion → fill the active input - dropdown.addEventListener('click', function (e) { - var btn = e.target.closest('.tag-search-item'); - if (!btn) return; - var name = (btn.getAttribute('data-tag-name') || '').trim(); - if (!name || !activeInput) return; - activeInput.value = btn.classList.contains('tag-search-item--create') - ? activeInput.value.trim() - : name; - dropdown.innerHTML = ''; - selectedIdx = -1; - activeInput.focus(); - }); + // Highlighting helper + function highlight(idx) { + var items = dropdown.querySelectorAll(".tag-search-item"); + for (let i = 0; i < items.length; i++) { + items[i].classList.toggle("tag-search-item--highlight", i === idx); + } + } - // Highlighting helper - function highlight(idx) { - var items = dropdown.querySelectorAll('.tag-search-item'); - for (var i = 0; i < items.length; i++) { - items[i].classList.toggle('tag-search-item--highlight', i === idx); - } - } + fieldset.addEventListener("input", (e) => { + var inp = e.target.closest('input[type="text"]'); + if (!inp) return; - fieldset.addEventListener('input', function (e) { - var inp = e.target.closest('input[type="text"]'); - if (!inp) return; + activeInput = inp; + var q = inp.value.trim(); - activeInput = inp; - var q = inp.value.trim(); + // Build the hx-include query — include hidden type=supervisor + var _typeInput = fieldset.querySelector( + 'input[name="type"][value="supervisor"]', + ); - // Build the hx-include query — include hidden type=supervisor - var typeInput = fieldset.querySelector('input[name="type"][value="supervisor"]'); - var includeSelector = typeInput ? '[name="type"][value="supervisor"]' : ''; + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + if (q === "") { + dropdown.innerHTML = ""; + selectedIdx = -1; + return; + } - if (debounceTimer) clearTimeout(debounceTimer); - debounceTimer = setTimeout(function () { - if (q === '') { - dropdown.innerHTML = ''; - selectedIdx = -1; - return; - } + // Manual HTMX POST + var xhr = new XMLHttpRequest(); + xhr.open("POST", hxPost); + xhr.setRequestHeader( + "Content-Type", + "application/x-www-form-urlencoded", + ); + xhr.setRequestHeader("HX-Request", "true"); + xhr.onload = () => { + if (xhr.status === 200) { + dropdown.innerHTML = xhr.responseText; + selectedIdx = -1; + } + }; + var params = + "type=supervisor&q=" + + encodeURIComponent(q) + + (role ? `&role=${encodeURIComponent(role)}` : ""); + xhr.send(params); + }, 200); + }); - // Manual HTMX POST - var xhr = new XMLHttpRequest(); - xhr.open('POST', hxPost); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.setRequestHeader('HX-Request', 'true'); - xhr.onload = function () { - if (xhr.status === 200) { - dropdown.innerHTML = xhr.responseText; - selectedIdx = -1; - } - }; - var params = 'type=supervisor&q=' + encodeURIComponent(q) + (role ? '&role=' + encodeURIComponent(role) : ''); - xhr.send(params); - }, 200); - }); + // Keyboard navigation + fieldset.addEventListener("keydown", (e) => { + var items = dropdown.querySelectorAll(".tag-search-item"); + if (e.key === "ArrowDown" || e.key === "ArrowUp") { + if (items.length === 0) return; + e.preventDefault(); + if (e.key === "ArrowDown") { + selectedIdx = (selectedIdx + 1) % items.length; + } else { + selectedIdx = selectedIdx <= 0 ? items.length - 1 : selectedIdx - 1; + } + highlight(selectedIdx); + } else if (e.key === "Enter") { + if (items.length > 0 && dropdown.innerHTML !== "") { + e.preventDefault(); + if (selectedIdx >= 0 && selectedIdx < items.length) { + items[selectedIdx].click(); + } else { + items[0].click(); + } + } + } else if (e.key === "Escape") { + dropdown.innerHTML = ""; + selectedIdx = -1; + } + }); - // Keyboard navigation - fieldset.addEventListener('keydown', function (e) { - var items = dropdown.querySelectorAll('.tag-search-item'); - if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { - if (items.length === 0) return; - e.preventDefault(); - if (e.key === 'ArrowDown') { - selectedIdx = (selectedIdx + 1) % items.length; - } else { - selectedIdx = selectedIdx <= 0 ? items.length - 1 : selectedIdx - 1; - } - highlight(selectedIdx); - } else if (e.key === 'Enter') { - if (items.length > 0 && dropdown.innerHTML !== '') { - e.preventDefault(); - if (selectedIdx >= 0 && selectedIdx < items.length) { - items[selectedIdx].click(); - } else { - items[0].click(); - } - } - } else if (e.key === 'Escape') { - dropdown.innerHTML = ''; - selectedIdx = -1; - } - }); - - // Close dropdown on outside click - document.addEventListener('click', function (e) { - if (!fieldset.contains(e.target)) { - dropdown.innerHTML = ''; - selectedIdx = -1; - } - }); - } + // Close dropdown on outside click + document.addEventListener("click", (e) => { + if (!fieldset.contains(e.target)) { + dropdown.innerHTML = ""; + selectedIdx = -1; + } + }); + } })(); diff --git a/app/public/assets/js/app/pill-search.js b/app/public/assets/js/app/pill-search.js index a116487..ddccdcd 100644 --- a/app/public/assets/js/app/pill-search.js +++ b/app/public/assets/js/app/pill-search.js @@ -20,152 +20,172 @@ * data-pill-required → si "1", active l'affichage du minimum * data-pill-role → "tag" (lowercase) ou "lang" (ucfirst) */ -(function () { - 'use strict'; +(() => { + function initAll() { + document + .querySelectorAll( + "[data-pill-search]:not([data-pill-search-initialized])", + ) + .forEach((container) => { + container.setAttribute("data-pill-search-initialized", "1"); + initPillSearch(container); + }); + } - function initAll() { - document.querySelectorAll('[data-pill-search]:not([data-pill-search-initialized])').forEach(function (container) { - container.setAttribute('data-pill-search-initialized', '1'); - initPillSearch(container); - }); - } + document.addEventListener("DOMContentLoaded", initAll); + document.body.addEventListener("htmx:afterSwap", initAll); - document.addEventListener('DOMContentLoaded', initAll); - document.body.addEventListener('htmx:afterSwap', initAll); + function initPillSearch(container) { + var pills = container.querySelector(".tag-search-pills"); + var search = container.querySelector(".tag-search-input"); + var dropdown = container.querySelector(".tag-search-suggestions"); + var countEl = container.querySelector(".tag-search-count"); + var counter = container.querySelector(".tag-search-counter"); + var maxTags = parseInt(container.getAttribute("data-pill-max"), 10) || 10; + var minTags = parseInt(container.getAttribute("data-pill-min"), 10) || 0; + var required = container.getAttribute("data-pill-required") === "1"; + var inputName = container.getAttribute("data-pill-name") || "tag"; + var _role = container.getAttribute("data-pill-role") || "tag"; + var selectedIdx = -1; - function initPillSearch(container) { - var pills = container.querySelector('.tag-search-pills'); - var search = container.querySelector('.tag-search-input'); - var dropdown = container.querySelector('.tag-search-suggestions'); - var countEl = container.querySelector('.tag-search-count'); - var counter = container.querySelector('.tag-search-counter'); - var maxTags = parseInt(container.getAttribute('data-pill-max')) || 10; - var minTags = parseInt(container.getAttribute('data-pill-min')) || 0; - var required = container.getAttribute('data-pill-required') === '1'; - var inputName = container.getAttribute('data-pill-name') || 'tag'; - var role = container.getAttribute('data-pill-role') || 'tag'; - var selectedIdx = -1; + if (!pills || !search || !dropdown) return; - if (!pills || !search || !dropdown) return; + function normalize(name) { + return name.trim().replace(/\s+/g, " ").toLowerCase(); + } - function normalize(name) { - return name.trim().replace(/\s+/g, ' ').toLowerCase(); - } + function pillAlreadyExists(name) { + var norm = normalize(name); + var existing = pills.querySelectorAll(".tag-pill-name"); + for (let i = 0; i < existing.length; i++) { + if (normalize(existing[i].textContent) === norm) return true; + } + return false; + } - function pillAlreadyExists(name) { - var norm = normalize(name); - var existing = pills.querySelectorAll('.tag-pill-name'); - for (var i = 0; i < existing.length; i++) { - if (normalize(existing[i].textContent) === norm) return true; - } - return false; - } + function updateCount() { + var n = pills.querySelectorAll(".tag-pill").length; + var suffix = required ? ` (min ${minTags})` : ""; + if (countEl) countEl.textContent = `${n}/${maxTags}${suffix}`; + if (counter) counter.style.display = n > 0 || required ? "" : "none"; + if (countEl && required) { + countEl.style.color = + n < minTags ? "var(--text-danger)" : "var(--accent)"; + } - function updateCount() { - var n = pills.querySelectorAll('.tag-pill').length; - var suffix = required ? ' (min ' + minTags + ')' : ''; - if (countEl) countEl.textContent = n + '/' + maxTags + suffix; - if (counter) counter.style.display = (n > 0 || required) ? '' : 'none'; - if (countEl && required) { - countEl.style.color = n < minTags ? 'var(--text-danger)' : 'var(--accent)'; - } + var wrap = container.querySelector(".tag-search-input-wrap"); + var maxMsg = container.querySelector(".tag-search-max-msg"); + if (n >= maxTags) { + if (wrap) wrap.style.display = "none"; + if (maxMsg) maxMsg.style.display = ""; + } else { + if (wrap) { + wrap.style.display = ""; + if (search) search.style.display = ""; + } + if (maxMsg) maxMsg.style.display = "none"; + } + } - var wrap = container.querySelector('.tag-search-input-wrap'); - var maxMsg = container.querySelector('.tag-search-max-msg'); - if (n >= maxTags) { - if (wrap) wrap.style.display = 'none'; - if (maxMsg) maxMsg.style.display = ''; - } else { - if (wrap) { wrap.style.display = ''; if (search) search.style.display = ''; } - if (maxMsg) maxMsg.style.display = 'none'; - } - } + pills.addEventListener("click", (e) => { + var btn = e.target.closest(".tag-pill-remove"); + if (!btn) return; + var pill = btn.closest(".tag-pill"); + pill.remove(); + updateCount(); + var wrap = container.querySelector(".tag-search-input-wrap"); + var inp = container.querySelector(".tag-search-input"); + if (wrap && inp) { + wrap.style.display = ""; + inp.style.display = ""; + } + }); - pills.addEventListener('click', function (e) { - var btn = e.target.closest('.tag-pill-remove'); - if (!btn) return; - var pill = btn.closest('.tag-pill'); - pill.remove(); - updateCount(); - var wrap = container.querySelector('.tag-search-input-wrap'); - var inp = container.querySelector('.tag-search-input'); - if (wrap && inp) { wrap.style.display = ''; inp.style.display = ''; } - }); + function highlight(idx) { + var items = dropdown.querySelectorAll(".tag-search-item"); + for (let i = 0; i < items.length; i++) { + items[i].classList.toggle("tag-search-item--highlight", i === idx); + } + } - function highlight(idx) { - var items = dropdown.querySelectorAll('.tag-search-item'); - for (var i = 0; i < items.length; i++) { - items[i].classList.toggle('tag-search-item--highlight', i === idx); - } - } + function selectPill(btn) { + var name = normalize(btn.getAttribute("data-tag-name") || ""); + if (!name) return; + if (pillAlreadyExists(name)) return; + if (pills.querySelectorAll(".tag-pill").length >= maxTags) return; - function selectPill(btn) { - var name = normalize(btn.getAttribute('data-tag-name') || ''); - if (!name) return; - if (pillAlreadyExists(name)) return; - if ((pills.querySelectorAll('.tag-pill').length) >= maxTags) return; + var escaped = htmlEscape(name); + var pill = document.createElement("span"); + pill.className = "tag-pill"; + pill.innerHTML = + '' + + '' + + escaped + + "" + + '"; + pills.appendChild(pill); + updateCount(); + search.value = ""; + dropdown.innerHTML = ""; + selectedIdx = -1; + search.focus(); + } - var escaped = htmlEscape(name); - var pill = document.createElement('span'); - pill.className = 'tag-pill'; - pill.innerHTML = '' - + '' + escaped + '' - + ''; - pills.appendChild(pill); - updateCount(); - search.value = ''; - dropdown.innerHTML = ''; - selectedIdx = -1; - search.focus(); - } + dropdown.addEventListener("click", (e) => { + var btn = e.target.closest(".tag-search-item"); + if (!btn) return; + selectPill(btn); + }); - dropdown.addEventListener('click', function (e) { - var btn = e.target.closest('.tag-search-item'); - if (!btn) return; - selectPill(btn); - }); + search.addEventListener("keydown", (e) => { + var items = dropdown.querySelectorAll(".tag-search-item"); + if (e.key === "ArrowDown" || e.key === "ArrowUp") { + e.preventDefault(); + if (items.length === 0) return; + if (e.key === "ArrowDown") { + selectedIdx = (selectedIdx + 1) % items.length; + } else { + selectedIdx = selectedIdx <= 0 ? items.length - 1 : selectedIdx - 1; + } + highlight(selectedIdx); + } else if (e.key === "Enter") { + if (items.length > 0) { + e.preventDefault(); + if (selectedIdx >= 0 && selectedIdx < items.length) { + selectPill(items[selectedIdx]); + } else { + selectPill(items[0]); + } + } + } else if (e.key === "Escape") { + dropdown.innerHTML = ""; + selectedIdx = -1; + } + }); - search.addEventListener('keydown', function (e) { - var items = dropdown.querySelectorAll('.tag-search-item'); - if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { - e.preventDefault(); - if (items.length === 0) return; - if (e.key === 'ArrowDown') { - selectedIdx = (selectedIdx + 1) % items.length; - } else { - selectedIdx = selectedIdx <= 0 ? items.length - 1 : selectedIdx - 1; - } - highlight(selectedIdx); - } else if (e.key === 'Enter') { - if (items.length > 0) { - e.preventDefault(); - if (selectedIdx >= 0 && selectedIdx < items.length) { - selectPill(items[selectedIdx]); - } else { - selectPill(items[0]); - } - } - } else if (e.key === 'Escape') { - dropdown.innerHTML = ''; - selectedIdx = -1; - } - }); + search.addEventListener("blur", () => { + setTimeout(() => { + if (!dropdown.contains(document.activeElement)) { + dropdown.innerHTML = ""; + selectedIdx = -1; + } + }, 150); + }); - search.addEventListener('blur', function () { - setTimeout(function () { - if (!dropdown.contains(document.activeElement)) { - dropdown.innerHTML = ''; - selectedIdx = -1; - } - }, 150); - }); - - function htmlEscape(str) { - var el = document.createElement('span'); - el.textContent = str; - return el.innerHTML; - } - } + function htmlEscape(str) { + var el = document.createElement("span"); + el.textContent = str; + return el.innerHTML; + } + } })(); diff --git a/app/public/assets/js/app/upload-progress.js b/app/public/assets/js/app/upload-progress.js index ce04227..1488d24 100644 --- a/app/public/assets/js/app/upload-progress.js +++ b/app/public/assets/js/app/upload-progress.js @@ -11,203 +11,211 @@ * 100% : response received — "Téléversé avec succès", then redirect */ (() => { - 'use strict'; + const FORMS = document.querySelectorAll("form[data-upload-progress]"); + if (!FORMS.length) return; - const FORMS = document.querySelectorAll('form[data-upload-progress]'); - if (!FORMS.length) return; + const POLL_INTERVAL = 400; + const UPLOAD_CAP = 25; + const PROCESSING_MAX = 99; + const SUCCESS_DELAY = 800; - const POLL_INTERVAL = 400; - const UPLOAD_CAP = 25; - const PROCESSING_MAX = 99; - const SUCCESS_DELAY = 800; + for (const form of FORMS) { + const progressWrap = form.querySelector("#upload-progress-wrap"); + const progressBar = form.querySelector("#upload-progress-bar"); + const progressLabel = form.querySelector("#upload-progress-label"); + const progressFile = form.querySelector("#upload-progress-file"); + const submitBtn = form.querySelector('button[type="submit"]'); + const tokenInput = form.querySelector('input[name="progress_token"]'); - for (const form of FORMS) { - const progressWrap = form.querySelector('#upload-progress-wrap'); - const progressBar = form.querySelector('#upload-progress-bar'); - const progressLabel = form.querySelector('#upload-progress-label'); - const progressFile = form.querySelector('#upload-progress-file'); - const submitBtn = form.querySelector('button[type="submit"]'); - const tokenInput = form.querySelector('input[name="progress_token"]'); + if (!progressBar || !progressWrap) continue; - if (!progressBar || !progressWrap) continue; + function collectFileNames() { + const names = []; + // Check raw elements (non-FilePond) + const inputs = form.querySelectorAll('input[type="file"]'); + for (const fi of inputs) { + if (fi.files) { + for (const f of fi.files) { + if (f.name) names.push(f.name); + } + } + } + // Read processed file names from FilePond instances (async mode) + if (typeof FilePond !== "undefined") { + const pondInputs = form.querySelectorAll(".tfe-file-picker"); + for (const pi of pondInputs) { + const pond = FilePond.find(pi); + if (pond) { + const pondFiles = pond.getFiles(); + for (const pf of pondFiles) { + // Only count successfully uploaded files (have serverId) + if (pf.serverId) { + const name = pf.filename || pf.file?.name || pf.serverId; + if (name) names.push(name); + } + } + } + } + } + return names; + } - function collectFileNames() { - const names = []; - // Check raw elements (non-FilePond) - const inputs = form.querySelectorAll('input[type="file"]'); - for (const fi of inputs) { - if (fi.files) { - for (const f of fi.files) { - if (f.name) names.push(f.name); - } - } - } - // Read processed file names from FilePond instances (async mode) - if (typeof FilePond !== 'undefined') { - const pondInputs = form.querySelectorAll('.tfe-file-picker'); - for (const pi of pondInputs) { - const pond = FilePond.find(pi); - if (pond) { - const pondFiles = pond.getFiles(); - for (const pf of pondFiles) { - // Only count successfully uploaded files (have serverId) - if (pf.serverId) { - const name = pf.filename || (pf.file && pf.file.name) || pf.serverId; - if (name) names.push(name); - } - } - } - } - } - return names; - } + form.addEventListener("submit", (e) => { + // ── Guard: block submit if any FilePond item is still uploading ── + if (typeof FilePond !== "undefined") { + let stillUploading = false; + const pondInputs = form.querySelectorAll(".tfe-file-picker"); + for (const pi of pondInputs) { + const pond = FilePond.find(pi); + if (pond) { + const pondFiles = pond.getFiles(); + for (const pf of pondFiles) { + if ( + pf.status === FilePond.FileStatus.PROCESSING || + pf.status === FilePond.FileStatus.IDLE + ) { + stillUploading = true; + break; + } + } + } + if (stillUploading) break; + } + if (stillUploading) { + e.preventDefault(); + progressLabel.textContent = + "Veuillez attendre la fin du téléversement…"; + progressWrap.style.display = ""; + return; + } + } - form.addEventListener('submit', function (e) { - // ── Guard: block submit if any FilePond item is still uploading ── - if (typeof FilePond !== 'undefined') { - let stillUploading = false; - const pondInputs = form.querySelectorAll('.tfe-file-picker'); - for (const pi of pondInputs) { - const pond = FilePond.find(pi); - if (pond) { - const pondFiles = pond.getFiles(); - for (const pf of pondFiles) { - if (pf.status === FilePond.FileStatus.PROCESSING || - pf.status === FilePond.FileStatus.IDLE) { - stillUploading = true; - break; - } - } - } - if (stillUploading) break; - } - if (stillUploading) { - e.preventDefault(); - progressLabel.textContent = 'Veuillez attendre la fin du téléversement…'; - progressWrap.style.display = ''; - return; - } - } + const fileNames = collectFileNames(); + if (!fileNames.length) return; - const fileNames = collectFileNames(); - if (!fileNames.length) return; + e.preventDefault(); - e.preventDefault(); + const token = tokenInput ? tokenInput.value : ""; - const token = tokenInput ? tokenInput.value : ''; + progressWrap.style.display = ""; + progressBar.value = 0; + progressBar.removeAttribute("data-complete"); + progressLabel.textContent = "Téléversement en cours…"; + progressFile.textContent = + fileNames.length === 1 ? fileNames[0] : `${fileNames.length} fichiers`; + if (submitBtn) submitBtn.disabled = true; - progressWrap.style.display = ''; - progressBar.value = 0; - progressBar.removeAttribute('data-complete'); - progressLabel.textContent = 'Téléversement en cours…'; - progressFile.textContent = fileNames.length === 1 - ? fileNames[0] - : fileNames.length + ' fichiers'; - if (submitBtn) submitBtn.disabled = true; + const fd = new FormData(form); + const xhr = new XMLHttpRequest(); - const fd = new FormData(form); - const xhr = new XMLHttpRequest(); + let _uploadDone = false; + let lastUploadPct = 0; + let pollingTimer = null; - let uploadDone = false; - let lastUploadPct = 0; - let pollingTimer = null; + /** Poll server-side progress */ + function startPolling() { + if (pollingTimer || !token) return; + progressLabel.textContent = "Traitement en cours…"; + pollingTimer = setInterval(() => { + fetch( + "/admin/actions/upload-progress.php?token=" + + encodeURIComponent(token), + ) + .then((r) => r.json()) + .then((data) => { + if (data?.stage && data.stage !== "upload") { + const pct = Math.min( + PROCESSING_MAX, + Math.max(UPLOAD_CAP, data.pct || UPLOAD_CAP), + ); + progressBar.value = pct; + if (data.file) { + progressFile.textContent = data.file; + } + } + }) + .catch(() => { + /* ignore poll errors */ + }); + }, POLL_INTERVAL); + } - /** Poll server-side progress */ - function startPolling() { - if (pollingTimer || !token) return; - progressLabel.textContent = 'Traitement en cours…'; - pollingTimer = setInterval(function () { - fetch('/admin/actions/upload-progress.php?token=' + encodeURIComponent(token)) - .then(function (r) { return r.json(); }) - .then(function (data) { - if (data && data.stage && data.stage !== 'upload') { - const pct = Math.min(PROCESSING_MAX, Math.max(UPLOAD_CAP, data.pct || UPLOAD_CAP)); - progressBar.value = pct; - if (data.file) { - progressFile.textContent = data.file; - } - } - }) - .catch(function () { /* ignore poll errors */ }); - }, POLL_INTERVAL); - } + function stopPolling() { + if (pollingTimer) { + clearInterval(pollingTimer); + pollingTimer = null; + } + } - function stopPolling() { - if (pollingTimer) { - clearInterval(pollingTimer); - pollingTimer = null; - } - } + function finishSuccess() { + stopPolling(); + progressBar.value = 100; + progressBar.setAttribute("data-complete", ""); + progressLabel.textContent = "Téléversé avec succès"; + progressFile.textContent = ""; + } - function finishSuccess() { - stopPolling(); - progressBar.value = 100; - progressBar.setAttribute('data-complete', ''); - progressLabel.textContent = 'Téléversé avec succès'; - progressFile.textContent = ''; - } + // ── Upload phase (0% → UPLOAD_CAP) ── + xhr.upload.addEventListener("progress", (evt) => { + if (evt.lengthComputable) { + const rawPct = Math.round((evt.loaded / evt.total) * 100); + const scaled = Math.round((rawPct / 100) * UPLOAD_CAP); + if (scaled > lastUploadPct) { + lastUploadPct = scaled; + progressBar.value = scaled; + } + } + }); - // ── Upload phase (0% → UPLOAD_CAP) ── - xhr.upload.addEventListener('progress', function (evt) { - if (evt.lengthComputable) { - const rawPct = Math.round((evt.loaded / evt.total) * 100); - const scaled = Math.round((rawPct / 100) * UPLOAD_CAP); - if (scaled > lastUploadPct) { - lastUploadPct = scaled; - progressBar.value = scaled; - } - } - }); + xhr.upload.addEventListener("loadend", () => { + _uploadDone = true; + progressBar.value = UPLOAD_CAP; + startPolling(); + }); - xhr.upload.addEventListener('loadend', function () { - uploadDone = true; - progressBar.value = UPLOAD_CAP; - startPolling(); - }); + // ── Response handling ── + xhr.addEventListener("readystatechange", () => { + if (xhr.readyState !== XMLHttpRequest.DONE) return; - // ── Response handling ── - xhr.addEventListener('readystatechange', function () { - if (xhr.readyState !== XMLHttpRequest.DONE) return; + stopPolling(); - stopPolling(); + if (xhr.status >= 200 && xhr.status < 300) { + finishSuccess(); - if (xhr.status >= 200 && xhr.status < 300) { - finishSuccess(); + setTimeout(() => { + const finalUrl = xhr.responseURL || ""; + if (finalUrl && finalUrl !== form.action) { + window.location.href = finalUrl; + } else { + document.open(); + document.write(xhr.responseText); + document.close(); + } + }, SUCCESS_DELAY); + } else { + progressLabel.textContent = "Erreur"; + progressFile.textContent = "Échec du téléversement"; + document.open(); + document.write(xhr.responseText); + document.close(); + } + }); - setTimeout(function () { - const finalUrl = xhr.responseURL || ''; - if (finalUrl && finalUrl !== form.action) { - window.location.href = finalUrl; - } else { - document.open(); - document.write(xhr.responseText); - document.close(); - } - }, SUCCESS_DELAY); - } else { - progressLabel.textContent = 'Erreur'; - progressFile.textContent = 'Échec du téléversement'; - document.open(); - document.write(xhr.responseText); - document.close(); - } - }); + xhr.addEventListener("error", () => { + stopPolling(); + progressLabel.textContent = "Erreur réseau"; + progressFile.textContent = ""; + if (submitBtn) submitBtn.disabled = false; + }); - xhr.addEventListener('error', function () { - stopPolling(); - progressLabel.textContent = 'Erreur réseau'; - progressFile.textContent = ''; - if (submitBtn) submitBtn.disabled = false; - }); + xhr.addEventListener("abort", () => { + stopPolling(); + progressWrap.style.display = "none"; + if (submitBtn) submitBtn.disabled = false; + }); - xhr.addEventListener('abort', function () { - stopPolling(); - progressWrap.style.display = 'none'; - if (submitBtn) submitBtn.disabled = false; - }); - - xhr.open('POST', form.action, true); - xhr.send(fd); - }); - } + xhr.open("POST", form.action, true); + xhr.send(fd); + }); + } })(); diff --git a/app/public/assets/js/vendor/filepond-plugin-file-validate-size.min.js b/app/public/assets/js/vendor/filepond-plugin-file-validate-size.min.js index 17fcb9d..611df58 100644 --- a/app/public/assets/js/vendor/filepond-plugin-file-validate-size.min.js +++ b/app/public/assets/js/vendor/filepond-plugin-file-validate-size.min.js @@ -6,4 +6,118 @@ /* eslint-disable */ -!function(e,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(e=e||self).FilePondPluginFileValidateSize=i()}(this,function(){"use strict";var e=function(e){var i=e.addFilter,E=e.utils,l=E.Type,_=E.replaceInString,n=E.toNaturalFileSize;return i("ALLOW_HOPPER_ITEM",function(e,i){var E=i.query;if(!E("GET_ALLOW_FILE_SIZE_VALIDATION"))return!0;var l=E("GET_MAX_FILE_SIZE");if(null!==l&&e.size>l)return!1;var _=E("GET_MIN_FILE_SIZE");return!(null!==_&&e.size<_)}),i("LOAD_FILE",function(e,i){var E=i.query;return new Promise(function(i,l){if(!E("GET_ALLOW_FILE_SIZE_VALIDATION"))return i(e);var I=E("GET_FILE_VALIDATE_SIZE_FILTER");if(I&&!I(e))return i(e);var t=E("GET_MAX_FILE_SIZE");if(null!==t&&e.size>t)l({status:{main:E("GET_LABEL_MAX_FILE_SIZE_EXCEEDED"),sub:_(E("GET_LABEL_MAX_FILE_SIZE"),{filesize:n(t,".",E("GET_FILE_SIZE_BASE"),E("GET_FILE_SIZE_LABELS",E))})}});else{var L=E("GET_MIN_FILE_SIZE");if(null!==L&&e.sizea)return void l({status:{main:E("GET_LABEL_MAX_TOTAL_FILE_SIZE_EXCEEDED"),sub:_(E("GET_LABEL_MAX_TOTAL_FILE_SIZE"),{filesize:n(a,".",E("GET_FILE_SIZE_BASE"),E("GET_FILE_SIZE_LABELS",E))})}});i(e)}}})}),{options:{allowFileSizeValidation:[!0,l.BOOLEAN],maxFileSize:[null,l.INT],minFileSize:[null,l.INT],maxTotalFileSize:[null,l.INT],fileValidateSizeFilter:[null,l.FUNCTION],labelMinFileSizeExceeded:["File is too small",l.STRING],labelMinFileSize:["Minimum file size is {filesize}",l.STRING],labelMaxFileSizeExceeded:["File is too large",l.STRING],labelMaxFileSize:["Maximum file size is {filesize}",l.STRING],labelMaxTotalFileSizeExceeded:["Maximum total size exceeded",l.STRING],labelMaxTotalFileSize:["Maximum total file size is {filesize}",l.STRING]}}};return"undefined"!=typeof window&&void 0!==window.document&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:e})),e}); +!((e, i) => { + "object" == typeof exports && "undefined" != typeof module + ? (module.exports = i()) + : "function" == typeof define && define.amd + ? define(i) + : ((e = e || self).FilePondPluginFileValidateSize = i()); +})(this, () => { + var e = (e) => { + var i = e.addFilter, + E = e.utils, + l = E.Type, + _ = E.replaceInString, + n = E.toNaturalFileSize; + return ( + i("ALLOW_HOPPER_ITEM", (e, i) => { + var E = i.query; + if (!E("GET_ALLOW_FILE_SIZE_VALIDATION")) return !0; + var l = E("GET_MAX_FILE_SIZE"); + if (null !== l && e.size > l) return !1; + var _ = E("GET_MIN_FILE_SIZE"); + return !(null !== _ && e.size < _); + }), + i("LOAD_FILE", (e, i) => { + var E = i.query; + return new Promise((i, l) => { + if (!E("GET_ALLOW_FILE_SIZE_VALIDATION")) return i(e); + var I = E("GET_FILE_VALIDATE_SIZE_FILTER"); + if (I && !I(e)) return i(e); + var t = E("GET_MAX_FILE_SIZE"); + if (null !== t && e.size > t) + l({ + status: { + main: E("GET_LABEL_MAX_FILE_SIZE_EXCEEDED"), + sub: _(E("GET_LABEL_MAX_FILE_SIZE"), { + filesize: n( + t, + ".", + E("GET_FILE_SIZE_BASE"), + E("GET_FILE_SIZE_LABELS", E), + ), + }), + }, + }); + else { + var L = E("GET_MIN_FILE_SIZE"); + if (null !== L && e.size < L) + l({ + status: { + main: E("GET_LABEL_MIN_FILE_SIZE_EXCEEDED"), + sub: _(E("GET_LABEL_MIN_FILE_SIZE"), { + filesize: n( + L, + ".", + E("GET_FILE_SIZE_BASE"), + E("GET_FILE_SIZE_LABELS", E), + ), + }), + }, + }); + else { + var a = E("GET_MAX_TOTAL_FILE_SIZE"); + if (null !== a) + if ( + E("GET_ACTIVE_ITEMS").reduce((e, i) => e + i.fileSize, 0) > a + ) + return void l({ + status: { + main: E("GET_LABEL_MAX_TOTAL_FILE_SIZE_EXCEEDED"), + sub: _(E("GET_LABEL_MAX_TOTAL_FILE_SIZE"), { + filesize: n( + a, + ".", + E("GET_FILE_SIZE_BASE"), + E("GET_FILE_SIZE_LABELS", E), + ), + }), + }, + }); + i(e); + } + } + }); + }), + { + options: { + allowFileSizeValidation: [!0, l.BOOLEAN], + maxFileSize: [null, l.INT], + minFileSize: [null, l.INT], + maxTotalFileSize: [null, l.INT], + fileValidateSizeFilter: [null, l.FUNCTION], + labelMinFileSizeExceeded: ["File is too small", l.STRING], + labelMinFileSize: ["Minimum file size is {filesize}", l.STRING], + labelMaxFileSizeExceeded: ["File is too large", l.STRING], + labelMaxFileSize: ["Maximum file size is {filesize}", l.STRING], + labelMaxTotalFileSizeExceeded: [ + "Maximum total size exceeded", + l.STRING, + ], + labelMaxTotalFileSize: [ + "Maximum total file size is {filesize}", + l.STRING, + ], + }, + } + ); + }; + return ( + "undefined" != typeof window && + void 0 !== window.document && + document.dispatchEvent( + new CustomEvent("FilePond:pluginloaded", { detail: e }), + ), + e + ); +}); diff --git a/app/public/assets/js/vendor/filepond-plugin-file-validate-type.min.js b/app/public/assets/js/vendor/filepond-plugin-file-validate-type.min.js index f2cb360..bf463ec 100644 --- a/app/public/assets/js/vendor/filepond-plugin-file-validate-type.min.js +++ b/app/public/assets/js/vendor/filepond-plugin-file-validate-type.min.js @@ -6,4 +6,113 @@ /* eslint-disable */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).FilePondPluginFileValidateType=t()}(this,function(){"use strict";var e=function(e){var t=e.addFilter,n=e.utils,i=n.Type,T=n.isString,E=n.replaceInString,l=n.guesstimateMimeType,o=n.getExtensionFromFilename,r=n.getFilenameFromURL,u=function(e,t){return e.some(function(e){return/\*$/.test(e)?(n=e,(/^[^/]+/.exec(t)||[]).pop()===n.slice(0,-2)):e===t;var n})},a=function(e,t,n){if(0===t.length)return!0;var i=function(e){var t="";if(T(e)){var n=r(e),i=o(n);i&&(t=l(i))}else t=e.type;return t}(e);return n?new Promise(function(T,E){n(e,i).then(function(e){u(t,e)?T():E()}).catch(E)}):u(t,i)};return t("SET_ATTRIBUTE_TO_OPTION_MAP",function(e){return Object.assign(e,{accept:"acceptedFileTypes"})}),t("ALLOW_HOPPER_ITEM",function(e,t){var n=t.query;return!n("GET_ALLOW_FILE_TYPE_VALIDATION")||a(e,n("GET_ACCEPTED_FILE_TYPES"))}),t("LOAD_FILE",function(e,t){var n=t.query;return new Promise(function(t,i){if(n("GET_ALLOW_FILE_TYPE_VALIDATION")){var T=n("GET_ACCEPTED_FILE_TYPES"),l=n("GET_FILE_VALIDATE_TYPE_DETECT_TYPE"),o=a(e,T,l),r=function(){var e,t=T.map((e=n("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES_MAP"),function(t){return null!==e[t]&&(e[t]||t)})).filter(function(e){return!1!==e}),l=t.filter(function(e,n){return t.indexOf(e)===n});i({status:{main:n("GET_LABEL_FILE_TYPE_NOT_ALLOWED"),sub:E(n("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES"),{allTypes:l.join(", "),allButLastType:l.slice(0,-1).join(", "),lastType:l[l.length-1]})}})};if("boolean"==typeof o)return o?t(e):r();o.then(function(){t(e)}).catch(r)}else t(e)})}),{options:{allowFileTypeValidation:[!0,i.BOOLEAN],acceptedFileTypes:[[],i.ARRAY],labelFileTypeNotAllowed:["File is of invalid type",i.STRING],fileValidateTypeLabelExpectedTypes:["Expects {allButLastType} or {lastType}",i.STRING],fileValidateTypeLabelExpectedTypesMap:[{},i.OBJECT],fileValidateTypeDetectType:[null,i.FUNCTION]}}};return"undefined"!=typeof window&&void 0!==window.document&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:e})),e}); +!((e, t) => { + "object" == typeof exports && "undefined" != typeof module + ? (module.exports = t()) + : "function" == typeof define && define.amd + ? define(t) + : ((e = e || self).FilePondPluginFileValidateType = t()); +})(this, () => { + var e = (e) => { + var t = e.addFilter, + n = e.utils, + i = n.Type, + T = n.isString, + E = n.replaceInString, + l = n.guesstimateMimeType, + o = n.getExtensionFromFilename, + r = n.getFilenameFromURL, + u = (e, t) => + e.some((e) => + /\*$/.test(e) + ? ((n = e), (/^[^/]+/.exec(t) || []).pop() === n.slice(0, -2)) + : e === t, + ), + a = (e, t, n) => { + if (0 === t.length) return !0; + var i = ((e) => { + var t = ""; + if (T(e)) { + var n = r(e), + i = o(n); + i && (t = l(i)); + } else t = e.type; + return t; + })(e); + return n + ? new Promise((T, E) => { + n(e, i) + .then((e) => { + u(t, e) ? T() : E(); + }) + .catch(E); + }) + : u(t, i); + }; + return ( + t("SET_ATTRIBUTE_TO_OPTION_MAP", (e) => + Object.assign(e, { accept: "acceptedFileTypes" }), + ), + t("ALLOW_HOPPER_ITEM", (e, t) => { + var n = t.query; + return ( + !n("GET_ALLOW_FILE_TYPE_VALIDATION") || + a(e, n("GET_ACCEPTED_FILE_TYPES")) + ); + }), + t("LOAD_FILE", (e, t) => { + var n = t.query; + return new Promise((t, i) => { + if (n("GET_ALLOW_FILE_TYPE_VALIDATION")) { + var T = n("GET_ACCEPTED_FILE_TYPES"), + l = n("GET_FILE_VALIDATE_TYPE_DETECT_TYPE"), + o = a(e, T, l), + r = () => { + var e, + t = T.map( + ((e = n("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES_MAP")), + (t) => null !== e[t] && (e[t] || t)), + ).filter((e) => !1 !== e), + l = t.filter((e, n) => t.indexOf(e) === n); + i({ + status: { + main: n("GET_LABEL_FILE_TYPE_NOT_ALLOWED"), + sub: E(n("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES"), { + allTypes: l.join(", "), + allButLastType: l.slice(0, -1).join(", "), + lastType: l[l.length - 1], + }), + }, + }); + }; + if ("boolean" == typeof o) return o ? t(e) : r(); + o.then(() => { + t(e); + }).catch(r); + } else t(e); + }); + }), + { + options: { + allowFileTypeValidation: [!0, i.BOOLEAN], + acceptedFileTypes: [[], i.ARRAY], + labelFileTypeNotAllowed: ["File is of invalid type", i.STRING], + fileValidateTypeLabelExpectedTypes: [ + "Expects {allButLastType} or {lastType}", + i.STRING, + ], + fileValidateTypeLabelExpectedTypesMap: [{}, i.OBJECT], + fileValidateTypeDetectType: [null, i.FUNCTION], + }, + } + ); + }; + return ( + "undefined" != typeof window && + void 0 !== window.document && + document.dispatchEvent( + new CustomEvent("FilePond:pluginloaded", { detail: e }), + ), + e + ); +}); diff --git a/app/public/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js b/app/public/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js index 90cc07f..ccfe14e 100644 --- a/app/public/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js +++ b/app/public/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js @@ -6,4 +6,92 @@ /* eslint-disable */ -!function(A,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(A=A||self).FilePondPluginImageExifOrientation=e()}(this,function(){"use strict";var A=65496,e=65505,n=1165519206,t=18761,i=274,r=65280,o=function(A,e){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return A.getUint16(e,n)},a=function(A,e){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return A.getUint32(e,n)},u="undefined"!=typeof window&&void 0!==window.document,d=void 0,f=u?new Image:{};f.onload=function(){return d=f.naturalWidth>f.naturalHeight},f.src="data:image/jpg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QA6RXhpZgAATU0AKgAAAAgAAwESAAMAAAABAAYAAAEoAAMAAAABAAIAAAITAAMAAAABAAEAAAAAAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wAALCAABAAIBASIA/8QAJgABAAAAAAAAAAAAAAAAAAAAAxABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAAPwBH/9k=";var l=function(u){var f=u.addFilter,l=u.utils,c=l.Type,g=l.isFile;return f("DID_LOAD_ITEM",function(u,f){var l=f.query;return new Promise(function(f,c){var s=u.file;if(!(g(s)&&function(A){return/^image\/jpeg/.test(A.type)}(s)&&l("GET_ALLOW_IMAGE_EXIF_ORIENTATION")&&d))return f(u);(function(u){return new Promise(function(d,f){var l=new FileReader;l.onload=function(u){var f=new DataView(u.target.result);if(o(f,0)===A){for(var l=f.byteLength,c=2;c { + "object" == typeof exports && "undefined" != typeof module + ? (module.exports = e()) + : "function" == typeof define && define.amd + ? define(e) + : ((A = A || self).FilePondPluginImageExifOrientation = e()); +})(this, () => { + var A = 65496, + e = 65505, + n = 1165519206, + t = 18761, + i = 274, + r = 65280, + o = function (A, e) { + var n = arguments.length > 2 && void 0 !== arguments[2] && arguments[2]; + return A.getUint16(e, n); + }, + a = function (A, e) { + var n = arguments.length > 2 && void 0 !== arguments[2] && arguments[2]; + return A.getUint32(e, n); + }, + u = "undefined" != typeof window && void 0 !== window.document, + d = void 0, + f = u ? new Image() : {}; + (f.onload = () => (d = f.naturalWidth > f.naturalHeight)), + (f.src = + "data:image/jpg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QA6RXhpZgAATU0AKgAAAAgAAwESAAMAAAABAAYAAAEoAAMAAAABAAIAAAITAAMAAAABAAEAAAAAAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wAALCAABAAIBASIA/8QAJgABAAAAAAAAAAAAAAAAAAAAAxABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAAPwBH/9k="); + var l = (u) => { + var f = u.addFilter, + l = u.utils, + c = l.Type, + g = l.isFile; + return ( + f("DID_LOAD_ITEM", (u, f) => { + var l = f.query; + return new Promise((f, c) => { + var s = u.file; + if ( + !( + g(s) && + ((A) => /^image\/jpeg/.test(A.type))(s) && + l("GET_ALLOW_IMAGE_EXIF_ORIENTATION") && + d + ) + ) + return f(u); + ((u) => + new Promise((d, f) => { + var l = new FileReader(); + (l.onload = (u) => { + var f = new DataView(u.target.result); + if (o(f, 0) === A) { + for (var l = f.byteLength, c = 2; c < l; ) { + var g = o(f, c); + if (((c += 2), g === e)) { + if (a(f, (c += 2)) !== n) break; + var s = o(f, (c += 6)) === t; + c += a(f, c + 4, s); + var v = o(f, c, s); + c += 2; + for (var w = 0; w < v; w++) + if (o(f, c + 12 * w, s) === i) + return void d(o(f, c + 12 * w + 8, s)); + } else { + if ((g & r) !== r) break; + c += o(f, c); + } + } + d(-1); + } else d(-1); + }), + l.readAsArrayBuffer(u.slice(0, 65536)); + }))(s).then((A) => { + u.setMetadata("exif", { orientation: A }), f(u); + }); + }); + }), + { options: { allowImageExifOrientation: [!0, c.BOOLEAN] } } + ); + }; + return ( + "undefined" != typeof window && + void 0 !== window.document && + document.dispatchEvent( + new CustomEvent("FilePond:pluginloaded", { detail: l }), + ), + l + ); +}); diff --git a/app/public/partage/fragments/draft.php b/app/public/partage/fragments/draft.php new file mode 100644 index 0000000..9de578f --- /dev/null +++ b/app/public/partage/fragments/draft.php @@ -0,0 +1,98 @@ + 'Token de sécurité invalide.']); + exit; + } +} + +// ── Slug validation ───────────────────────────────────────────────────── +$slug = $_GET['slug'] ?? ($_POST['slug'] ?? ''); +if (!preg_match('#^\d{8}-[A-Z0-9+/]{8}$#', $slug)) { + http_response_code(400); + header('Content-Type: application/json'); + echo json_encode(['error' => 'Slug invalide.']); + exit; +} + +// Draft storage key +$draftKey = 'partage_draft_' . $slug; + +// ── POST: save all form fields ────────────────────────────────────────── +if ($method === 'POST') { + // Fields that should never be persisted as drafts + $excludePrefixes = [ + 'csrf_token', 'share_link_token', 'share_password', + 'filepond_mode', 'queue_file', 'filepond_', + ]; + $excludeExact = ['slug', 'couverture', 'note_intention', 'files', 'annexes', + 'peertube_video', 'peertube_audio', 'cover_remove', + 'go', 'MAX_FILE_SIZE']; + + $draft = []; + foreach ($_POST as $key => $value) { + // Skip excluded fields + if (in_array($key, $excludeExact, true)) continue; + $skip = false; + foreach ($excludePrefixes as $prefix) { + if (str_starts_with($key, $prefix)) { $skip = true; break; } + } + if ($skip) continue; + + // Skip empty values (but keep '0' as valid) + if ($value === '' || $value === null || (is_array($value) && count($value) === 0)) { + continue; + } + + $draft[$key] = $value; + } + + $_SESSION[$draftKey] = $draft; + + // Rotate CSRF after mutation — keep share CSRF in sync + $newToken = bin2hex(random_bytes(32)); + $_SESSION['csrf_token'] = $newToken; + $_SESSION['share_csrf_' . $slug] = $newToken; + + header('Content-Type: application/json'); + echo json_encode([ + 'success' => true, + 'csrf_token' => $newToken, + ]); + exit; +} + +// ── GET: return draft fields for hydration ────────────────────────────── +header('Content-Type: application/json'); +$draft = $_SESSION[$draftKey] ?? []; +echo json_encode([ + 'success' => true, + 'draft' => $draft, +]); +exit; diff --git a/app/src/AppLogger.php b/app/src/AppLogger.php index 73453fd..391d582 100644 --- a/app/src/AppLogger.php +++ b/app/src/AppLogger.php @@ -8,15 +8,8 @@ */ class AppLogger { - private string $logDir; - private string $logFile; - - public function __construct(?string $logDir = null) + public function __construct() { - $this->logDir = $logDir ?? (defined('STORAGE_ROOT') ? STORAGE_ROOT . '/logs' : __DIR__ . '/../storage/logs'); - - // Keep for backward compat — actual file I/O is now handled by Monolog via Logger::get('app') - $this->logFile = $this->logDir . '/form-submissions.log'; } /** diff --git a/app/src/Controllers/ExportController.php b/app/src/Controllers/ExportController.php index 10a510c..1a115ab 100644 --- a/app/src/Controllers/ExportController.php +++ b/app/src/Controllers/ExportController.php @@ -175,7 +175,7 @@ class ExportController $ptInstanceUrl = $this->getPeerTubeInstanceUrl(); } $tid = (int) $f['thesis_id']; - $peertubeLinks[$tid] = $peertubeLinks[$tid] ?? ['dirname' => '', 'links' => []]; + $peertubeLinks[$tid] ??= ['dirname' => '', 'links' => []]; $peertubeLinks[$tid]['links'][] = [ 'uuid' => $uuid, 'url' => $ptInstanceUrl !== '' ? rtrim($ptInstanceUrl, '/') . '/videos/watch/' . $uuid : '', diff --git a/app/src/Controllers/TfeController.php b/app/src/Controllers/TfeController.php index a71e688..e8117e7 100644 --- a/app/src/Controllers/TfeController.php +++ b/app/src/Controllers/TfeController.php @@ -107,10 +107,14 @@ class TfeController . ' – XAMXAM'; // Editable messages - $restrictedMessage = $this->db->getSetting('tfe_restricted_message', - 'Les fichiers attachés à ce TFE sont réservés aux utilisateur·ices autorisé·es.'); - $forbiddenMessage = $this->db->getSetting('tfe_forbidden_message', - "Ce TFE n'est pas disponible en ligne."); + $restrictedMessage = $this->db->getSetting( + 'tfe_restricted_message', + 'Les fichiers attachés à ce TFE sont réservés aux utilisateur·ices autorisé·es.' + ); + $forbiddenMessage = $this->db->getSetting( + 'tfe_forbidden_message', + "Ce TFE n'est pas disponible en ligne." + ); return [ // Core data diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php index b38121d..a304b8c 100644 --- a/app/src/Controllers/ThesisEditController.php +++ b/app/src/Controllers/ThesisEditController.php @@ -104,7 +104,7 @@ class ThesisEditController $licenseTypes = $this->db->getAllLicenseTypes(); $enabledAccessTypes = $this->db->getEnabledFormAccessTypes(); - $rawRow = $this->db->getThesisRawFields($thesisId); + $rawRow = $this->db->getThesisRawFields($thesisId) ?? []; $currentLicenseId = $rawRow['license_id'] ?? null; $currentAccessTypeId = $rawRow['access_type_id'] ?? null; $currentContextNote = $rawRow['context_note'] ?? ''; diff --git a/app/src/Database.php b/app/src/Database.php index 62515e2..5c0c8ae 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -2016,7 +2016,7 @@ class Database * Return the raw FK fields not exposed through v_theses_full string columns. * Returns ['license_id', 'access_type_id', 'context_note'] or null if not found. * - * @return array{license_id:int|null,access_type_id:int|null,context_note:string}|null + * @return array{license_id:int|null,access_type_id:int|null,context_note:string,contact_visible:int|null}|null */ public function getThesisRawFields(int $thesisId): ?array { diff --git a/app/src/FilepondHandler.php b/app/src/FilepondHandler.php index a185d04..4132f58 100644 --- a/app/src/FilepondHandler.php +++ b/app/src/FilepondHandler.php @@ -146,7 +146,7 @@ class FilepondHandler // Track temp file in session so it survives page reloads if (session_status() === PHP_SESSION_ACTIVE) { - $_SESSION['filepond_tmp'][$queueType] = $_SESSION['filepond_tmp'][$queueType] ?? []; + $_SESSION['filepond_tmp'][$queueType] ??= []; $_SESSION['filepond_tmp'][$queueType][] = $fileId; } diff --git a/app/src/Logger.php b/app/src/Logger.php index b441389..e0287cd 100644 --- a/app/src/Logger.php +++ b/app/src/Logger.php @@ -1,8 +1,8 @@ tag + * bool $showAutosaveStatus — render the "Brouillon enregistré" status indicator + * * Website: * string $existingWebsiteUrl * string $existingWebsiteLabel @@ -56,6 +60,8 @@ // ── Defaults ────────────────────────────────────────────────────────────────── $mode = $mode ?? 'add'; +$formExtraAttrs = $formExtraAttrs ?? ''; +$showAutosaveStatus = $showAutosaveStatus ?? false; // In admin add/edit, no field is required (admins can save partial records) $adminMode = ($mode === 'add' || $mode === 'edit'); $formAction = $formAction ?? ''; @@ -146,7 +152,7 @@ $errorFieldName = $errorFieldName ?? null; -
+ > @@ -540,6 +546,10 @@ if ($filesMode === 'add'): ?> + +
+ +