diff --git a/package.json b/package.json index 1622cf7..07b3836 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,10 @@ "packageManager": "pnpm@10.5.2", "dependencies": { "better-sqlite3": "^11.9.1", + "dompurify": "^3.2.4", "express": "^4.21.2", + "jsdom": "^26.0.0", + "marked": "^15.0.7", "mysql": "^2.18.1" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9604306..498707d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,18 @@ importers: better-sqlite3: specifier: ^11.9.1 version: 11.9.1 + dompurify: + specifier: ^3.2.4 + version: 3.2.4 express: specifier: ^4.21.2 version: 4.21.2 + jsdom: + specifier: ^26.0.0 + version: 26.0.0 + marked: + specifier: ^15.0.7 + version: 15.0.7 mysql: specifier: ^2.18.1 version: 2.18.1 @@ -24,10 +33,48 @@ importers: packages: + '@asamuzakjp/css-color@3.1.1': + resolution: {integrity: sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==} + + '@csstools/color-helpers@5.0.2': + resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.2': + resolution: {integrity: sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-color-parser@3.0.8': + resolution: {integrity: sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -35,6 +82,9 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -90,6 +140,10 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -111,6 +165,14 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cssstyle@4.3.0: + resolution: {integrity: sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==} + engines: {node: '>=18'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -128,6 +190,9 @@ packages: supports-color: optional: true + decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -136,6 +201,10 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -148,6 +217,9 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + dompurify@3.2.4: + resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -166,6 +238,10 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -178,6 +254,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -204,6 +284,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -250,18 +334,38 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -294,9 +398,29 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + jsdom@26.0.0: + resolution: {integrity: sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + marked@15.0.7: + resolution: {integrity: sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==} + engines: {node: '>= 18'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -368,6 +492,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + nwsapi@2.2.20: + resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -379,6 +506,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -408,6 +538,10 @@ packages: pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -435,6 +569,9 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -444,6 +581,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + semver@7.7.1: resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} @@ -508,6 +649,9 @@ packages: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tar-fs@2.1.2: resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} @@ -515,6 +659,13 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tldts-core@6.1.85: + resolution: {integrity: sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==} + + tldts@6.1.85: + resolution: {integrity: sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -527,6 +678,14 @@ packages: resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} hasBin: true + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.0: + resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==} + engines: {node: '>=18'} + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -552,16 +711,88 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.1: + resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + snapshots: + '@asamuzakjp/css-color@3.1.1': + dependencies: + '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + lru-cache: 10.4.3 + + '@csstools/color-helpers@5.0.2': {} + + '@csstools/css-calc@2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-color-parser@3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/color-helpers': 5.0.2 + '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-tokenizer@3.0.3': {} + + '@types/trusted-types@2.0.7': + optional: true + accepts@1.3.8: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 + agent-base@7.1.3: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -569,6 +800,8 @@ snapshots: array-flatten@1.1.1: {} + asynckit@0.4.0: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -649,6 +882,10 @@ snapshots: chownr@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} content-disposition@0.5.4: @@ -663,6 +900,16 @@ snapshots: core-util-is@1.0.3: {} + cssstyle@4.3.0: + dependencies: + '@asamuzakjp/css-color': 3.1.1 + rrweb-cssom: 0.8.0 + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + debug@2.6.9: dependencies: ms: 2.0.0 @@ -673,18 +920,26 @@ snapshots: optionalDependencies: supports-color: 5.5.0 + decimal.js@10.5.0: {} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 deep-extend@0.6.0: {} + delayed-stream@1.0.0: {} + depd@2.0.0: {} destroy@1.2.0: {} detect-libc@2.0.3: {} + dompurify@3.2.4: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -701,6 +956,8 @@ snapshots: dependencies: once: 1.4.0 + entities@4.5.0: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -709,6 +966,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + escape-html@1.0.3: {} etag@1.8.1: {} @@ -769,6 +1033,13 @@ snapshots: transitivePeerDependencies: - supports-color + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -810,10 +1081,18 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -822,10 +1101,28 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore-by-default@1.0.1: {} @@ -848,8 +1145,42 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + isarray@1.0.0: {} + jsdom@26.0.0: + dependencies: + cssstyle: 4.3.0 + data-urls: 5.0.0 + decimal.js: 10.5.0 + form-data: 4.0.2 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.20 + parse5: 7.2.1 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + lru-cache@10.4.3: {} + + marked@15.0.7: {} + math-intrinsics@1.1.0: {} media-typer@0.3.0: {} @@ -910,6 +1241,8 @@ snapshots: normalize-path@3.0.0: {} + nwsapi@2.2.20: {} + object-inspect@1.13.4: {} on-finished@2.4.1: @@ -920,6 +1253,10 @@ snapshots: dependencies: wrappy: 1.0.2 + parse5@7.2.1: + dependencies: + entities: 4.5.0 + parseurl@1.3.3: {} path-to-regexp@0.1.12: {} @@ -955,6 +1292,8 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 + punycode@2.3.1: {} + qs@6.13.0: dependencies: side-channel: 1.1.0 @@ -995,12 +1334,18 @@ snapshots: dependencies: picomatch: 2.3.1 + rrweb-cssom@0.8.0: {} + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + semver@7.7.1: {} send@0.19.0: @@ -1090,6 +1435,8 @@ snapshots: dependencies: has-flag: 3.0.0 + symbol-tree@3.2.4: {} + tar-fs@2.1.2: dependencies: chownr: 1.1.4 @@ -1105,6 +1452,12 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tldts-core@6.1.85: {} + + tldts@6.1.85: + dependencies: + tldts-core: 6.1.85 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -1113,6 +1466,14 @@ snapshots: touch@3.1.1: {} + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.85 + + tr46@5.1.0: + dependencies: + punycode: 2.3.1 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 @@ -1132,4 +1493,27 @@ snapshots: vary@1.1.2: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.0 + webidl-conversions: 7.0.0 + wrappy@1.0.2: {} + + ws@8.18.1: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} diff --git a/src/controllers/mods.js b/src/controllers/mods.js index 33cd5f5..63bc80c 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -24,13 +24,24 @@ async function getModByName(req, res) { async function createMod(req, res) { try { - const status = await mod_service.createMod(req.body); - res.status(status); + await mod_service.createMod(req.body); + res.sendStatus(200); } catch (error) { - console.error("Cannot create mod:", error.message); + console.error("ERROR: Couldn't create mod:", error.message); + handleError(error, req, res, null); + } +} + +async function deleteMod(req, res) { + try { + await mod_service.deleteMod(req.params.name); + return res.sendStatus(200); + } catch (error) { + console.error("ERROR: Couldn't delete mod " + req.params.name + ":", error.message); handleError(error, req, res, null); } } -module.exports = { getAllMods, getModByName, createMod }; \ No newline at end of file + +module.exports = { getAllMods, getModByName, createMod, deleteMod }; \ No newline at end of file diff --git a/src/database/sqlite.js b/src/database/sqlite.js index 5e40299..7070f5d 100644 --- a/src/database/sqlite.js +++ b/src/database/sqlite.js @@ -1,4 +1,3 @@ -// TODO promisify const sqlite = require("better-sqlite3"); class SQLiteDatabase { diff --git a/src/models/mod.js b/src/models/mod.js index 1e3e83b..f64cc2d 100644 --- a/src/models/mod.js +++ b/src/models/mod.js @@ -1,8 +1,8 @@ const { getDatabase } = require('../database/index'); +const AppError = require('../utils/appError'); const db = getDatabase(); async function getAllMods() { - console.debug("Calling model"); return db.query("SELECT * FROM mods"); } @@ -26,25 +26,35 @@ async function exists(name) { return db.exists("mods", "Name", name); } -// --- WIP --- - async function createMod(mod_data) { - console.warn("WARNING: using a WIP function : createMod (models/mods.js)"); - const { name, displayName, author, versions, otherInfos } = mod_data; - return db.prepare("INSERT INTO mods (Name, DisplayName, Author, Versions, OtherInfos) VALUES (?, ?, ?, ?, ?)", [name, displayName, author, versions, otherInfos]); -} -async function updateMod(mod_data) { - console.log("WARNING: using a WIP function : updateMod (models/mods.js)"); - throw new Error("Not implemented"); - // const { name, description } = mod_data; - // return db.query("INSERT INTO mods (name, description) VALUES (?, ?)", [name, description]); + const { name, displayName, author, versions, otherInfos } = mod_data; + const { description, links, tags, screenshots, license, changelogs, counts } = otherInfos; + + await db.prepare("INSERT INTO mods (Name, DisplayName, Author, Versions) \ + VALUES (?, ?, ?, ?)", [name, displayName, author, versions]); + // db.prepare("INSERT INTO modsDescription (Name, Description, Links, Tags, Screenshots, License, Changelogs, Counts) \ + // VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [name, description, links, tags, screenshots, license, changelogs, counts]); + return; } async function deleteMod(name) { console.log("WARNING: using a WIP function : deleteMod (models/mods.js)"); - return db.query("DELETE FROM mods WHERE name = ?", [name]); + db.prepare("DELETE FROM mods WHERE Name = ?", [name]); + // db.prepare("DELETE FROM modsDescription WHERE Name = ?", [name]); + return; +} + +// --- WIP --- + +async function updateMod(mod_data) { + console.log("WARNING: using a WIP function : updateMod (models/mods.js)"); + throw new AppError(501, "Not implemented"); + // const { name, description } = mod_data; + // return db.query("INSERT INTO mods (name, description) VALUES (?, ?)", [name, description]); } -module.exports = { getAllMods, getModByName, createMod, exists } \ No newline at end of file + + +module.exports = { getAllMods, getModByName, createMod, deleteMod, exists } \ No newline at end of file diff --git a/src/routes/mods.js b/src/routes/mods.js index f62e531..4254161 100644 --- a/src/routes/mods.js +++ b/src/routes/mods.js @@ -22,4 +22,12 @@ router.get("/:name", async (req,res) => { controller.getModByName(req, res); }); +// +router.delete("/:name", async (req,res) => { + const name = req.params.name; + console.debug("Deleting mod " + name) + controller.deleteMod(req, res); +}); + + module.exports = router; \ No newline at end of file diff --git a/src/services/modService.js b/src/services/modService.js index a960367..b1611d5 100644 --- a/src/services/modService.js +++ b/src/services/modService.js @@ -1,7 +1,8 @@ -const handleError = require("../middleware/errors"); const model = require("../models/mod"); const AppError = require("../utils/appError"); -const { validateModData } = require("../utils/validation"); +const { validateModData } = require("../utils/validate"); +const { mdToHtml } = require("../utils/convert"); +const { sanitizeModData } = require("../utils/sanitize"); async function getAllMods() { return model.getAllMods(); @@ -11,10 +12,7 @@ async function getModByName(name) { return model.getModByName(name); } -async function createMod(mod_data) { - // throw new AppError(501, "Not implemented"); - - // console.debug("Received body: ", JSON.stringify(mod_data)); +async function createMod(mod_data) { // Check body validity await validateModData(mod_data); @@ -22,8 +20,33 @@ async function createMod(mod_data) { // Check authenticity //TODO no auth provider + // Convert + mod_data.otherInfos.description = await mdToHtml(mod_data.otherInfos.description); + mod_data.otherInfos.changelogs = await mdToHtml(mod_data.otherInfos.changelogs); + + // Sanitize + await sanitizeModData(mod_data); + + + console.debug("Passed validity checks"); - return model.createMod(mod_data); + model.createMod(mod_data); + return; } -module.exports = { getAllMods, getModByName, createMod }; \ No newline at end of file +async function deleteMod(name) { + + // Check existence + const exists = await model.exists(name); + if (!exists) { + throw new AppError(404, "Not found: Cannot find mod with this name"); + } + + // Check authenticity + //TODO no auth provider + + model.deleteMod(name); + return; +} + +module.exports = { getAllMods, getModByName, createMod, deleteMod }; \ No newline at end of file diff --git a/src/utils/convert.js b/src/utils/convert.js new file mode 100644 index 0000000..39a56a9 --- /dev/null +++ b/src/utils/convert.js @@ -0,0 +1,11 @@ +const marked = require("marked"); + +async function mdToHtml(md_content) { + if (md_content) { + return marked.parse(md_content); + } else { + return ""; + } +} + +module.exports = { mdToHtml }; \ No newline at end of file diff --git a/src/utils/sanitize.js b/src/utils/sanitize.js new file mode 100644 index 0000000..f6b99d1 --- /dev/null +++ b/src/utils/sanitize.js @@ -0,0 +1,18 @@ +const createDOMPurify = require("dompurify"); +const { JSDOM } = require("jsdom"); + +// Initialize +const window = new JSDOM("").window; +const DOMPurify = createDOMPurify(window); + +async function sanitizeText(text) { + return DOMPurify.sanitize(text); +} + +async function sanitizeModData(mod_data) { + mod_data.displayName = await sanitizeText(mod_data.displayName); + mod_data.otherInfos.description = await sanitizeText(mod_data.otherInfos.description); + mod_data.otherInfos.changelogs = await sanitizeText(mod_data.otherInfos.changelogs); +} + +module.exports = { sanitizeText, sanitizeModData }; \ No newline at end of file diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 0000000..dface71 --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,49 @@ +const mod_model = require("../models/mod"); +const AppError = require("./appError"); + +async function validateModData(mod_data) { + //TODO WIP + // Check fields existence + const not_null = mod_data && + Object.keys(mod_data).length == 5 && + mod_data.name && + mod_data.displayName && + mod_data.author && + mod_data.versions != null; + + // mod_data.otherInfos != null && + // Object.keys(mod_data.otherInfos).length == 0 && + // mod_data.otherInfos.description != null && + // mod_data.otherInfos.links != null && + // mod_data.otherInfos.tags != null && + // mod_data.otherInfos.screenshots != null && + // mod_data.otherInfos.license != null && + // mod_data.otherInfos.changelogs != null; + + if (!not_null) { + console.debug("Item is missing expected fields:", mod_data); + throw new AppError(400, "Bad request: missing expected fields"); + } + + // Check fields format (check if sanitized) + const is_valid_name = /^[a-zA-Z0-9_]+$/.test(mod_data.name); + const is_valid_displayName = true; + // const is_valid_displayName = /^[a-zA-Z0-9_]+$/.test(mod_data.name); // Temporary + // const + + const is_valid = is_valid_name && is_valid_displayName; + if (!is_valid) { + console.debug("Fields are not following the expected formats"); + throw new AppError(400, "Bad request: The provided fields don't match the expected format"); + } + + // Check if mod already exists + const exists = await mod_model.exists(mod_data.name); + if (exists) { + console.debug("Error: Item already exists"); + throw new AppError(403, "Forbidden: Content with this name already exists"); + } +} + + +module.exports = { validateModData } \ No newline at end of file diff --git a/src/utils/validation.js b/src/utils/validation.js deleted file mode 100644 index 0e9bdda..0000000 --- a/src/utils/validation.js +++ /dev/null @@ -1,26 +0,0 @@ -const mod_model = require("../models/mod"); -const AppError = require("./appError"); - -async function validateModData(mod_data) { - // WIP - const not_null = mod_data != null; - mod_data.name && - mod_data.displayName && - mod_data.author && - mod_data.versions && - mod_data.otherInfos; - - if (!not_null) { - console.debug("Item is missing expected fields:", mod_data); - throw new AppError(400, "Bad request: missing expected fields"); - } - - const exists = await mod_model.exists(mod_data.name); - if (exists) { - console.debug("Error: Item already exists"); - throw new AppError(403, "Forbidden: Content with this name already exists"); - } -} - - -module.exports = { validateModData } \ No newline at end of file