From bf3a3bce33877651e88b089ef7b92a727e0d8bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D0=BC=D0=B0=D0=BD=20=D0=9F=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=BE=D0=BF=D0=BE=D0=B2?= Date: Sun, 10 Oct 2021 07:05:15 +0200 Subject: [PATCH 1/9] Translated using Weblate (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 81.3% (210 of 258 strings) Co-authored-by: Роман Прокопов Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/ru/ Translation: I Hate Money/I Hate Money --- .../translations/ru/LC_MESSAGES/messages.mo | Bin 23816 -> 22596 bytes .../translations/ru/LC_MESSAGES/messages.po | 17 ++++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/ihatemoney/translations/ru/LC_MESSAGES/messages.mo b/ihatemoney/translations/ru/LC_MESSAGES/messages.mo index c9c234e0d8ecb18ce1c9ac876750bc0d4fd42330..c343e8df3da8496ed666db116c18cb6151a71241 100644 GIT binary patch delta 4828 zcmY+`2~##(D2)}={KdzyOUNm3hfnwWSRZU5iAOHTU^AHRFwy!Y<<2G*W( zT_|^X{}LIz)o@%VG2~c?G0`q#MuzLGG3CvT38H=!k5WG#X-q86YGF(>EX8Qtj)`~_ zx!l~wbo>}cVYA1LnSxU=39FF%z2fhGLLDm=t?{07g+C zg(+Bw^xbU1R=5KjV>uqdYMhL7_%H%5VJPjJTNKpbU$G&6iFDf3p=KnQR`lRF)B`&q z&ob$#>qlTPj>R@O0d@TXWGrSWGHGT5Hp1N)h6ga1_Dux^_2>ku;mfE8eQNz4+ft8g zZ45uA8^2O2Y!cLF`VA%x_+nz2O}SuCs9jOi0ar<)JRLLub`HC2Wp1)wqgFYnGVUVq#Cu97g00vXUxN|P@8!GjcRX<_EOM^ z>8J}Ap&BT$ZbmiyCaNPxP!0G|o3I*n-$&LvNE_xpYKdC3bMB8u4KT^pyQ5~xJD7sj zWDIhTnTQ(sVtc__)C0Go*0{pfucEI10@a~V)=_&Y0yXkjYY$XIIjH-`qeeat8MxQ1 zrl1G-P(9pP~L7EZwj*aauLjVZ$t+=gAaNPFc1R^UU_Oq8<$)Uo$a zulp4Yp?!0Mg4XU+?1E8*bh%(AN(G*c4?iQnaV*e(R@?~OHr>~ z8ET4;qXu>swO49U9r)UQ{-6`{AHs=X-Z^!oG17*KKs_MSo*#l?)N_&5F?qJW81K3m z1sJWgAKy|G!cAj>AqCd-pE5PGwZuD|I;XJ6?o3j+F zP$PNAdIGg4E+eaFK0_^8Xm?}yZZXM7pG*#FW}n5$cnmdz&C;Cv61)^Nb;Ge2T`I)$2__HEwpOKF`BET#0Jv zPpAR?9W|gj)Lv-SR|97KdsEN_6EF;?VJObUz*HkY#>+1~unJiva|QLfKE&A=&Z|;{ z%kT;O8@AEss3mUQ-iJFsJG=C z)LRri&{-lkwxm7`qcI=#eX$tTv30ioHhOj9Bn4hca|2((N2uLgGRRr$gQ!h)17q=? zt%qkh5A1-tJ`1%83sEz=#GYS`dP~Y|{VHnj)MYXMk5fqG9nmK2i@I?d>h+nA8o>^G z-jB?-Icq=v5jE1pA^dlP{ZLEeLETq|DR>UGbpJ%{frr-kpFs1QbiOA%7(;z4w!;e44Ax@cb;J_tX`_r; zg_X#Q#-`>ebjo;84OF2v-96OX@B?aBHyGn|Xbx(mEAZ#|7JiFCW1TOSA5cq?_A{q} z*{Ff+#}EcuDXE`f2Xmd*T%w@Y?GZM{ka5lzMkMOSWYp(_uoQD~JJ#TOEE?~;HQ(Y4 z>Z2z(OXf!|F*lLfHc3nyFN#@&TDl61*86{jf~M*Y#$nTm&Zg>yn)+;Pijz>gy9hht za$DbPt-$7-e;@T4-#`r@^=apiO)cd#@(fu))S3RYZ}{9SCm#~FLtqpGQAdWuPrv6- z`lYQEA}_%I9a@s#+A}k)^uwGbM+wUwIObZtO(9>JM#1ZQd?qggx0E+mR_`Daj=p$#$Y+mV@~XdY|Rj2^B~gNgywg{Y1xn z(vS33g<~&yoiruy5+B)0dJ*0036z{a-6{qXkAHECt1kbLmX$8QE2_fft8D$8KP$RZkbhOoKU_`R3lv;Ogyf*Y)Fu{;>4+LB8~i z2m2>vhWJYdTn`F9>8?5B+mjvTA3L~5gT}jS%4=8DoUW;=-QXLP+1h_%Xh)YXKKs~~ zoOQnHoKC*eIR(Cbx&8g?a>qvc?oE#KP0sJ^+m`S4RpobXR#9_`8#mRK)>PK6;sJ9D KI{02MnD<}G20+RH delta 5891 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 bm-*Ey)|>w~m(|m{H?31AL*;=Fr!Dv|bg0js diff --git a/ihatemoney/translations/ru/LC_MESSAGES/messages.po b/ihatemoney/translations/ru/LC_MESSAGES/messages.po index 0b307d94..24845770 100644 --- a/ihatemoney/translations/ru/LC_MESSAGES/messages.po +++ b/ihatemoney/translations/ru/LC_MESSAGES/messages.po @@ -1,19 +1,19 @@ - msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-17 17:31+0200\n" -"PO-Revision-Date: 2021-05-10 11:33+0000\n" -"Last-Translator: Vsevolod \n" +"PO-Revision-Date: 2021-09-20 12:38+0000\n" +"Last-Translator: Роман Прокопов \n" +"Language-Team: Russian \n" "Language: ru\n" -"Language-Team: Russian \n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " -"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.9-dev\n" "Generated-By: Babel 2.9.0\n" msgid "" @@ -26,9 +26,8 @@ msgstr "" msgid "Project name" msgstr "Имя проекта" -#, fuzzy msgid "New private code" -msgstr "Приватный код" +msgstr "Новый приватный код" msgid "Enter a new code if you want to change it" msgstr "" From 6044947a9046598259fb5c4e17ad6865df0d6b8f Mon Sep 17 00:00:00 2001 From: Christian H Date: Sun, 10 Oct 2021 07:05:15 +0200 Subject: [PATCH 2/9] Translated using Weblate (German) Currently translated at 98.4% (254 of 258 strings) Co-authored-by: Christian H Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/de/ Translation: I Hate Money/I Hate Money --- .../translations/de/LC_MESSAGES/messages.mo | Bin 22080 -> 22202 bytes .../translations/de/LC_MESSAGES/messages.po | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ihatemoney/translations/de/LC_MESSAGES/messages.mo b/ihatemoney/translations/de/LC_MESSAGES/messages.mo index 27d5ffa303635846021e1241465ad7a0c3818874..0c14afc68e246d976edf3e5963370df6ba1ee2f6 100644 GIT binary patch delta 5498 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 diff --git a/ihatemoney/translations/de/LC_MESSAGES/messages.po b/ihatemoney/translations/de/LC_MESSAGES/messages.po index 46144d0d..60be7506 100644 --- a/ihatemoney/translations/de/LC_MESSAGES/messages.po +++ b/ihatemoney/translations/de/LC_MESSAGES/messages.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-17 17:31+0200\n" -"PO-Revision-Date: 2021-08-19 20:34+0000\n" -"Last-Translator: corny \n" +"PO-Revision-Date: 2021-09-23 19:36+0000\n" +"Last-Translator: Christian H. \n" "Language-Team: German \n" "Language: de\n" @@ -12,7 +12,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.8-dev\n" +"X-Generator: Weblate 4.9-dev\n" "Generated-By: Babel 2.9.0\n" msgid "" @@ -74,7 +74,7 @@ msgstr "" "wähle eine andere Kennung" msgid "Enter private code to confirm deletion" -msgstr "" +msgstr "Geben Sie Ihren privaten Code ein, um die Löschung zu bestätigen" msgid "Unknown error" msgstr "Unbekannter Fehler" From 1c1f1ae8936f5e83aeecb4cb1eed1f47f4dee6de Mon Sep 17 00:00:00 2001 From: phlostically Date: Sun, 10 Oct 2021 07:05:16 +0200 Subject: [PATCH 3/9] Translated using Weblate (Esperanto) Currently translated at 86.8% (224 of 258 strings) Co-authored-by: phlostically Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/eo/ Translation: I Hate Money/I Hate Money --- .../translations/eo/LC_MESSAGES/messages.mo | Bin 19241 -> 18925 bytes .../translations/eo/LC_MESSAGES/messages.po | 36 ++++++++---------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/ihatemoney/translations/eo/LC_MESSAGES/messages.mo b/ihatemoney/translations/eo/LC_MESSAGES/messages.mo index 5be5c4a502915aff5ca0ccc7165d2140d342e0e0..cc3133f3aa10cbb3fabefb98957fd43f0b1e61bf 100644 GIT binary patch delta 5625 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 I>C)l90%SxrrT_o{ diff --git a/ihatemoney/translations/eo/LC_MESSAGES/messages.po b/ihatemoney/translations/eo/LC_MESSAGES/messages.po index c9c0d740..02e59259 100644 --- a/ihatemoney/translations/eo/LC_MESSAGES/messages.po +++ b/ihatemoney/translations/eo/LC_MESSAGES/messages.po @@ -1,18 +1,18 @@ - msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-17 17:31+0200\n" -"PO-Revision-Date: 2021-07-10 15:34+0000\n" +"PO-Revision-Date: 2021-10-01 20:35+0000\n" "Last-Translator: phlostically \n" +"Language-Team: Esperanto \n" "Language: eo\n" -"Language-Team: Esperanto \n" -"Plural-Forms: nplurals=2; plural=n != 1\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.9-dev\n" "Generated-By: Babel 2.9.0\n" msgid "" @@ -76,13 +76,11 @@ msgstr "" msgid "Enter private code to confirm deletion" msgstr "" -#, fuzzy msgid "Unknown error" -msgstr "Nekonata projekto" +msgstr "Nekonata eraro" -#, fuzzy msgid "Invalid private code." -msgstr "Nova privata kodo" +msgstr "Nevalida privata kodo." msgid "Get in" msgstr "Eniri" @@ -175,30 +173,30 @@ msgstr "La retpoŝta adreso %(email)s ne validas" #. List with two items only msgid "{dual_object_0} and {dual_object_1}" -msgstr "" +msgstr "{dual_object_0} kaj {dual_object_1}" #. Last two items of a list with more than 3 items msgid "{previous_object}, and {end_object}" -msgstr "" +msgstr "{previous_object} kaj {end_object}" #. Two items in a middle of a list with more than 5 objects msgid "{previous_object}, {next_object}" -msgstr "" +msgstr "{previous_object}, {next_object}" #. First two items of a list with more than 3 items msgid "{start_object}, {next_object}" -msgstr "" +msgstr "{start_object}, {next_object}" msgid "No Currency" msgstr "Neniu valuto" #. Form error with only one error msgid "{prefix}: {error}" -msgstr "" +msgstr "{prefix}: {error}" #. Form error with a list of errors msgid "{prefix}:
{errors}" -msgstr "" +msgstr "{prefix}:
{errors}" msgid "Too many failed login attempts, please retry later." msgstr "Tro da malsukcesaj provoj de salutado; bonvolu reprovi poste." @@ -396,16 +394,14 @@ msgstr "Elŝuti programon por poŝaparato" msgid "Get it on" msgstr "Elŝuti ĝin ĉe" -#, fuzzy msgid "Are you sure?" -msgstr "ĉu vi certas?" +msgstr "Ĉu vi certas?" msgid "Edit project" msgstr "Redakti projekton" -#, fuzzy msgid "Delete project" -msgstr "Redakti projekton" +msgstr "Forviŝi projekton" msgid "Import JSON" msgstr "Enporti JSON-dosieron" @@ -453,7 +449,7 @@ msgid "Everyone" msgstr "Ĉiuj" msgid "No one" -msgstr "" +msgstr "Neniu" msgid "More options" msgstr "" From 879936a4c48d3340c1fb1918e1c912eec2e32638 Mon Sep 17 00:00:00 2001 From: "Frank.wu" Date: Sun, 10 Oct 2021 07:05:16 +0200 Subject: [PATCH 4/9] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (258 of 258 strings) Co-authored-by: Frank.wu Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/zh_Hans/ Translation: I Hate Money/I Hate Money --- .../zh_Hans/LC_MESSAGES/messages.mo | Bin 16974 -> 20341 bytes .../zh_Hans/LC_MESSAGES/messages.po | 160 ++++++++---------- 2 files changed, 74 insertions(+), 86 deletions(-) diff --git a/ihatemoney/translations/zh_Hans/LC_MESSAGES/messages.mo b/ihatemoney/translations/zh_Hans/LC_MESSAGES/messages.mo index b2ff6719449b931948dd28ad57101061161f6ffb..969a2e233e378158b385ce57a77a90cdcde1dfd6 100644 GIT binary patch literal 20341 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 T?_IyZ;T1|hvOWB+pWyx**tBWZ delta 6023 zcmZA433L=i0><$gITQnOhC46>ArLu20J#w`kwXFH&|MXDNCt=`8B78J5hrp)}S! z0`{T?I%)k3V`%@3tid$mU{~ynb#MZX#c8PVPaqd>&SGD@ios+mEm{~;8-1t=Gf^uK z+V*nPm2E+7X(4JSj-m#W%N9&JST9{1^7dc(y~gBn9>UfNjsj+iA~3UFc5KLP}!Te^pM= z5sMd41OI>;;CJN5RBsh&$6`m?9kBt9Mct~&sEMapbFK4H3toZRkzJ@;`6_Bh&IGB9 zq4F{6DUauJ^>Fk>wMU~)d<-?glh(PYTa}Mmzy{mD1vSwtsE4uy_5O3#%gAP#Z&9}@ z80C!&R3CN4v9{e2wN-slw`3^tV@C6$9hr{Wf#s<4wxI^zkLrILb?>jB`u~l3Uo>AH zT3~l%+@R@CMGspNYHP+?r=UKDb5K{l2sP14)WBO%J9PxLQ}3b%zJj`t3e@@Eq8`#; zQS&t9K2tTZI6%0Hi@Miw z7>9#!AWlUsU?*zfCy-B`xrAQkH$PJ0$3(X`W-#`{WSooqnA5!I_}8edtjFlO=bcek zoQU1AAL^M1pmrn=xd!tBreg_m+fADeky|+!gSwJQRJ2tyaSG1In)nTBrQc%>tiz)( z8=^iX&8%%PydWGxeY7Dgj(>jJJ^3!R??w`ti@aK z1nP}vtrszh_7&7bU)%ogQSbj9HQ`O22-Zd(BU2aEpNOu9UBCet%h%;P`X3K=jojN` zQ1`BNLge8}K)w=866#7*P*3d?)RyMs06dL+>l#nD$SsUPU17Yno9*w7dM1*sf`e*Y5@&;7&8i+q1sbX=LN7A&O_a@Qq)2(Swq$ztfpt={dF+> z`@a#DM!X>owZ;8W6OTttG#m90<)J26f|_8r^&m#mK8?D^mr&I&LY(HrhWO+3h&ikfIKYJk;P4+~H`whzOf64ZGYtyfVK{R8#BT5ON3hnla6 zZO8Xw|8J+G8yy(kbJ)D^8koxj`m7o)~GY1?mFKeYYVP&-r^ zq@t~?&WBnPw6=D_rnLK@7B&W>aT036S=Obfi8fmcQRD2j9hph;iY7jf+M0`~zXv`?ZDrKJh)uBx?RGd02VgT?i7~hrbs^`hSMed*KcmJUbywv0 zc+@=8urBkP8LHqy)P$>1Z`h7}_n6(7jGrT)DARUOr2k=5dpT;rGpHSV54Dg=>wi$= zHX0nU4QjqF7}P`qspywXGHQ#4qXv8wb>d{yc{8nZtqZJ6tt(OEtVjJe-DLaEpvF0G z+n23h4QBr}@Yi%`!td;XUv2v){+iH$wNX1!4|QG=dQe|JD(btj?6D2quAQWvZNH6E z$$63!?h8i#o_~eqfpNhWMRSL)pM4w?jy(*;&yD09*-Ms@ zp`?iPB=JP$CNhV7O70;Q#7o+eH%OxPzcH0HWIOqssB9&VlG}*ND)KTJPd+3n)yYX! zD0!qNnMN)X72Vn!%U3j7k|VZrF;3A*d_$VGWF1jy9l<o7LYg92p9JMc`EzJbP_`b5|yjuE%F?Bn52`lM5PP)AY3zM1g<6% z$RMKfEIC02kWR!QDt{r56PMH_cVEBPQ8aoJmG{Xua*8pSJ=Q!WAY~?hMS>_y!iM}Z<+xew+3#WUlW5u;w-|ulov>Dbk zc~*`;E7O18LUk(GN>T>Kl> z?`Dvkv_NL(Bnb)Z_~XFD0Y$}e1FL(SI)g?;yF1oY&EHk!91kro3~ek4 ztvXVExYT)YPzPt>piiAygY)CuosRp$G7~Du3#}=tEP0u^|F7}dwopO7lRTuGGk3^C z&c{RUtL3hkSAKYTd1+o$n^k_ejMJRz!%sPH4ZpJwE3VkMpF!CLw_ukEtvy(^w!~DFt*~KhE;hhxjxM0dqH7PzNkOCbxiZFliTx2)8JHq3LM zEpQS>HLg}(wp`EL(8$J{P<|d8$*zSCFAVSUiM&urnF&RR?3MZBCYjK>;}ttfjl1}{ O(6a69k8$RW8uC9>9n2p9 diff --git a/ihatemoney/translations/zh_Hans/LC_MESSAGES/messages.po b/ihatemoney/translations/zh_Hans/LC_MESSAGES/messages.po index 26bb5077..3a790c3f 100644 --- a/ihatemoney/translations/zh_Hans/LC_MESSAGES/messages.po +++ b/ihatemoney/translations/zh_Hans/LC_MESSAGES/messages.po @@ -1,19 +1,18 @@ - msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-17 17:31+0200\n" -"PO-Revision-Date: 2020-10-12 04:47+0000\n" -"Last-Translator: Jwen921 \n" +"PO-Revision-Date: 2021-10-10 05:05+0000\n" +"Last-Translator: Frank.wu \n" +"Language-Team: Chinese (Simplified) \n" "Language: zh_Hans\n" -"Language-Team: Chinese (Simplified) " -"" -"\n" -"Plural-Forms: nplurals=1; plural=0\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.9-dev\n" "Generated-By: Babel 2.9.0\n" msgid "" @@ -24,12 +23,11 @@ msgstr "金额或符号无效。仅限数字与+-*/符号。" msgid "Project name" msgstr "账目名称" -#, fuzzy msgid "New private code" -msgstr "共享密钥" +msgstr "新的私人代码" msgid "Enter a new code if you want to change it" -msgstr "" +msgstr "如要更改,请输入新代码" msgid "Email" msgstr "邮箱" @@ -46,7 +44,7 @@ msgstr "默认货币" msgid "" "This project cannot be set to 'no currency' because it contains bills in " "multiple currencies." -msgstr "" +msgstr "此项目不能设置为“无货币”,因为它包含多种货币的账单。" msgid "Import previously exported JSON file" msgstr "导入之前的JSON 文件" @@ -70,15 +68,13 @@ msgid "" msgstr "账目(“%(project)s”)已存在,请选择一个新名称" msgid "Enter private code to confirm deletion" -msgstr "" +msgstr "请输入专用代码以确认删除" -#, fuzzy msgid "Unknown error" -msgstr "未知项目" +msgstr "未知错误" -#, fuzzy msgid "Invalid private code." -msgstr "共享密钥" +msgstr "无效的私人代码。" msgid "Get in" msgstr "进入" @@ -171,40 +167,40 @@ msgstr "此邮箱%(email)s不存在" #. List with two items only msgid "{dual_object_0} and {dual_object_1}" -msgstr "" +msgstr "{dual_object_0} 和 {dual_object_1}" #. Last two items of a list with more than 3 items msgid "{previous_object}, and {end_object}" -msgstr "" +msgstr "{previous_object} 和 {end_object}" #. Two items in a middle of a list with more than 5 objects msgid "{previous_object}, {next_object}" -msgstr "" +msgstr "{previous_object},{next_object}" #. First two items of a list with more than 3 items msgid "{start_object}, {next_object}" -msgstr "" +msgstr "{start_object}, {next_object}" msgid "No Currency" -msgstr "没有货币" +msgstr "无货币" #. Form error with only one error msgid "{prefix}: {error}" -msgstr "" +msgstr "{prefix}: {error}" #. Form error with a list of errors msgid "{prefix}:
{errors}" -msgstr "" +msgstr "{prefix}:
{errors}" msgid "Too many failed login attempts, please retry later." -msgstr "输入错误太多次了,请稍后重试。" +msgstr "登录失败次数过多,请稍后重试。" #, python-format msgid "This admin password is not the right one. Only %(num)d attempts left." -msgstr "管理密码有误,只剩 %(num)d次尝试机会" +msgstr "管理密码有误,只剩 %(num)d次尝试机会。" msgid "You either provided a bad token or no project identifier." -msgstr "你输入了错误的符号或没有项目标识符。" +msgstr "你输入了错误的令牌或没有项目标识符。" msgid "This private code is not the right one" msgstr "专用码不正确" @@ -241,7 +237,7 @@ msgid "Unknown project" msgstr "未知项目" msgid "Password successfully reset." -msgstr "密码重置成功" +msgstr "密码重置成功。" msgid "Project successfully uploaded" msgstr "项目成功上传" @@ -253,7 +249,7 @@ msgid "Project successfully deleted" msgstr "项目成功删除" msgid "Error deleting project" -msgstr "" +msgstr "删除项目时出错" #, python-format msgid "You have been invited to share your expenses for %(project)s" @@ -273,20 +269,20 @@ msgid "%(member)s has been added" msgstr "已添加%(member)s" msgid "Error activating member" -msgstr "" +msgstr "激活成员时出错" #, python-format msgid "%(name)s is part of this project again" msgstr "%(name)s 已经在项目里了" msgid "Error removing member" -msgstr "" +msgstr "删除成员时出错" #, python-format msgid "" "User '%(name)s' has been deactivated. It will still appear in the users " "list until its balance becomes zero." -msgstr "用户 '%(name)s'已被暂停,在余额为0之前会继续显示在用户列表里" +msgstr "用户 '%(name)s'已被暂停,在余额为0之前会继续显示在用户列表里。" #, python-format msgid "User '%(name)s' has been removed" @@ -300,7 +296,7 @@ msgid "The bill has been added" msgstr "帐单已添加" msgid "Error deleting bill" -msgstr "" +msgstr "删除账单时出错" msgid "The bill has been deleted" msgstr "账单已删除" @@ -308,26 +304,23 @@ msgstr "账单已删除" msgid "The bill has been modified" msgstr "帐单已修改" -#, fuzzy msgid "Error deleting project history" -msgstr "启用项目历史" +msgstr "删除项目历史记录时出错" -#, fuzzy msgid "Deleted project history." -msgstr "启用项目历史" +msgstr "已删除的项目历史记录。" -#, fuzzy msgid "Error deleting recorded IP addresses" -msgstr "删除已储存的IP地址" +msgstr "删除记录的IP地址时出错" msgid "Deleted recorded IP addresses in project history." -msgstr "" +msgstr "删除项目历史记录中的 IP 地址。" msgid "Sorry, we were unable to find the page you've asked for." -msgstr "对不起,未找到该页面" +msgstr "抱歉,我们无法找到您要求的页面。" msgid "The best thing to do is probably to get back to the main page." -msgstr "最好的办法是返回主页" +msgstr "最好的办法是返回主页。" msgid "Back to the list" msgstr "返回列表" @@ -375,26 +368,22 @@ msgid "show" msgstr "显示" msgid "The Dashboard is currently deactivated." -msgstr "操作面板失效" +msgstr "操作面板失效。" -#, fuzzy msgid "Download Mobile Application" -msgstr "手机软件" +msgstr "下载移动应用程序" -#, fuzzy msgid "Get it on" -msgstr "进入" +msgstr "获取" -#, fuzzy msgid "Are you sure?" -msgstr "确定?" +msgstr "是否确定?" msgid "Edit project" msgstr "编辑项目" -#, fuzzy msgid "Delete project" -msgstr "编辑项目" +msgstr "删除项目" msgid "Import JSON" msgstr "导入json文件" @@ -430,7 +419,7 @@ msgid "Edit the project" msgstr "编辑项目" msgid "This will remove all bills and participants in this project!" -msgstr "" +msgstr "这将删除此项目的所有账单和参与者!" msgid "Edit this bill" msgstr "编辑帐单" @@ -442,10 +431,10 @@ msgid "Everyone" msgstr "每个人" msgid "No one" -msgstr "" +msgstr "无人" msgid "More options" -msgstr "" +msgstr "更多选项" msgid "Add participant" msgstr "添加参与人" @@ -485,11 +474,11 @@ msgstr "历史设置改变" #, python-format msgid "Bill %(name)s: %(property_name)s changed from %(before)s to %(after)s" -msgstr "" +msgstr "账单 %(name)s: %(property_name)s 从 %(before)s 改为 %(after)s" #, python-format msgid "Bill %(name)s: %(property_name)s changed to %(after)s" -msgstr "" +msgstr "账单 %(name)s: %(property_name)s 改为 %(after)s" msgid "Confirm Remove IP Adresses" msgstr "确认移除IP地址" @@ -503,7 +492,6 @@ msgstr "" "你确定要删除此项目里所有的IP地址吗?\n" "项目其他内容不受影响,此操作不可撤回。" -#, fuzzy msgid "Confirm deletion" msgstr "确认删除" @@ -520,11 +508,11 @@ msgstr "确定删除此项目所有记录?此操作不可撤回。" #, python-format msgid "Bill %(name)s: added %(owers_list_str)s to owers list" -msgstr "" +msgstr "帐单 %(name)s:将 %(owers_list_str)s 添加到所有者列表" #, python-format msgid "Bill %(name)s: removed %(owers_list_str)s from owers list" -msgstr "" +msgstr "账单 %(name)s:从所有者列表中删除了 %(owers_list_str)s" #, python-format msgid "" @@ -590,51 +578,51 @@ msgstr "IP地址记录可在设置里禁用" msgid "From IP" msgstr "从IP" -#, fuzzy, python-format +#, python-format msgid "Project %(name)s added" -msgstr "账目名称" +msgstr "项目 %(name)s 已添加" -#, fuzzy, python-format +#, python-format msgid "Bill %(name)s added" -msgstr "帐单已添加" +msgstr "帐单 %(name)s 已添加" #, python-format msgid "Participant %(name)s added" -msgstr "" +msgstr "成员 %(name)s 已添加" msgid "Project private code changed" msgstr "项目专用码已更改" -#, fuzzy, python-format +#, python-format msgid "Project renamed to %(new_project_name)s" -msgstr "项目的标识符是%(project)s" +msgstr "项目的标识符是 %(new_project_name)s" -#, fuzzy, python-format +#, python-format msgid "Project contact email changed to %(new_email)s" -msgstr "项目联系邮箱更改为" +msgstr "项目联系邮箱更改为 %(new_email)s" msgid "Project settings modified" msgstr "项目设置已修改" #, python-format msgid "Participant %(name)s deactivated" -msgstr "" +msgstr "成员 %(name)s 已停用" #, python-format msgid "Participant %(name)s reactivated" -msgstr "" +msgstr "成员 %(name)s 被重新激活" #, python-format msgid "Participant %(name)s renamed to %(new_name)s" -msgstr "" +msgstr "成员 %(name)s 重命名为 %(new_name)s" #, python-format msgid "Bill %(name)s renamed to %(new_description)s" -msgstr "" +msgstr "账单 %(name)s 更名为 %(new_description)s" #, python-format msgid "Participant %(name)s: weight changed from %(old_weight)s to %(new_weight)s" -msgstr "" +msgstr "成员 %(name)s:权重从%(old_weight)s变为%(new_weight)s" msgid "Amount" msgstr "数量" @@ -643,33 +631,33 @@ msgstr "数量" msgid "Amount in %(currency)s" msgstr "%(currency)s的数量是" -#, fuzzy, python-format +#, python-format msgid "Bill %(name)s modified" -msgstr "帐单已修改" +msgstr "帐单 %(name)s 已修改" #, python-format msgid "Participant %(name)s modified" -msgstr "" +msgstr "成员 %(name)s 已修改" -#, fuzzy, python-format +#, python-format msgid "Bill %(name)s removed" -msgstr "用户 '%(name)s'已被移除" +msgstr "账单 %(name)s 已被移除" -#, fuzzy, python-format +#, python-format msgid "Participant %(name)s removed" -msgstr "用户 '%(name)s'已被移除" +msgstr "用户 %(name)s 已被移除" -#, fuzzy, python-format +#, python-format msgid "Project %(name)s changed in an unknown way" -msgstr "未知的改变" +msgstr "项目 %(name)s 以未知方式更改" -#, fuzzy, python-format +#, python-format msgid "Bill %(name)s changed in an unknown way" -msgstr "未知的改变" +msgstr "账单 %(name)s 以一种未知的方式更改" -#, fuzzy, python-format +#, python-format msgid "Participant %(name)s changed in an unknown way" -msgstr "未知的改变" +msgstr "成员 %(name)s 以未知方式更改" msgid "Nothing to list" msgstr "无列表" @@ -815,7 +803,7 @@ msgid "No bills" msgstr "没有账单" msgid "Nothing to list yet." -msgstr "没有列表" +msgstr "没有列表。" msgid "You probably want to" msgstr "你想要" From c13c4c7e3cf1cdacc0549d6d92a04bdaa45b42af Mon Sep 17 00:00:00 2001 From: Baptiste Jonglez Date: Sun, 10 Oct 2021 11:56:28 +0200 Subject: [PATCH 5/9] Fix duplicate entry in changelog --- CHANGELOG.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 44270012..89515862 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,7 +37,6 @@ Added - Add sorting, pagination, and searching to the admin dashboard (#538) - Add Project History page that records all changes (#553) - Add token-based authentication to the API (#504) -- Add translations for Hindi, Portuguese (Brazil), Tamil - Add illustrations as a showcase, currently only for French (#544) - Add a page for downloading mobile application (#688) - Add translations for Greek, Esperanto, Italian, Japanese, Portuguese and Swedish From bbe00ebb579918cf622236817dfa14f1e7b6f0b9 Mon Sep 17 00:00:00 2001 From: Glandos Date: Sun, 10 Oct 2021 14:43:40 +0200 Subject: [PATCH 6/9] Include project code into project authentication token (#802) Fix #780 This a breaking change, the API for authentication is different, as it now requires `project_id`. Token is generated with only the project_id (so it's shorter than before), and signature is done by mixing password with secret key. Thus, it expires on every project code change. --- ihatemoney/api/common.py | 4 +- ihatemoney/models.py | 52 ++++++++++++++------ ihatemoney/templates/invitation_mail.en.j2 | 2 +- ihatemoney/templates/invitation_mail.fr.j2 | 2 +- ihatemoney/templates/password_reminder.en.j2 | 2 +- ihatemoney/templates/password_reminder.fr.j2 | 2 +- ihatemoney/templates/send_invites.html | 4 +- ihatemoney/tests/api_test.py | 4 +- ihatemoney/tests/budget_test.py | 43 ++++++++++++++++ ihatemoney/tests/common/help_functions.py | 11 +++++ ihatemoney/web.py | 18 ++++--- 11 files changed, 114 insertions(+), 30 deletions(-) diff --git a/ihatemoney/api/common.py b/ihatemoney/api/common.py index ede76e46..fa097dec 100644 --- a/ihatemoney/api/common.py +++ b/ihatemoney/api/common.py @@ -35,7 +35,9 @@ def need_auth(f): auth_token = auth_header.split(" ")[1] except IndexError: abort(401) - project_id = Project.verify_token(auth_token, token_type="non_timed_token") + project_id = Project.verify_token( + auth_token, token_type="auth", project_id=project_id + ) if auth_token and project_id: project = Project.query.get(project_id) if project: diff --git a/ihatemoney/models.py b/ihatemoney/models.py index 68d1fd84..b3d1cba2 100644 --- a/ihatemoney/models.py +++ b/ihatemoney/models.py @@ -7,8 +7,8 @@ from flask_sqlalchemy import BaseQuery, SQLAlchemy from itsdangerous import ( BadSignature, SignatureExpired, - TimedJSONWebSignatureSerializer, URLSafeSerializer, + URLSafeTimedSerializer, ) import sqlalchemy from sqlalchemy import orm @@ -339,41 +339,61 @@ class Project(db.Model): db.session.delete(self) db.session.commit() - def generate_token(self, expiration=0): + def generate_token(self, token_type="auth"): """Generate a timed and serialized JsonWebToken - :param expiration: Token expiration time (in seconds) + :param token_type: Either "auth" for authentication (invalidated when project code changed), + or "reset" for password reset (invalidated after expiration) """ - if expiration: - serializer = TimedJSONWebSignatureSerializer( - current_app.config["SECRET_KEY"], expiration + + if token_type == "reset": + serializer = URLSafeTimedSerializer( + current_app.config["SECRET_KEY"], salt=token_type ) - token = serializer.dumps({"project_id": self.id}).decode("utf-8") + token = serializer.dumps([self.id]) else: - serializer = URLSafeSerializer(current_app.config["SECRET_KEY"]) - token = serializer.dumps({"project_id": self.id}) + serializer = URLSafeSerializer( + current_app.config["SECRET_KEY"] + self.password, salt=token_type + ) + token = serializer.dumps([self.id]) + return token @staticmethod - def verify_token(token, token_type="timed_token"): + def verify_token(token, token_type="auth", project_id=None, max_age=3600): """Return the project id associated to the provided token, None if the provided token is expired or not valid. :param token: Serialized TimedJsonWebToken + :param token_type: Either "auth" for authentication (invalidated when project code changed), + or "reset" for password reset (invalidated after expiration) + :param project_id: Project ID. Used for token_type "auth" to use the password as serializer + secret key. + :param max_age: Token expiration time (in seconds). Only used with token_type "reset" """ - if token_type == "timed_token": - serializer = TimedJSONWebSignatureSerializer( - current_app.config["SECRET_KEY"] + loads_kwargs = {} + if token_type == "reset": + serializer = URLSafeTimedSerializer( + current_app.config["SECRET_KEY"], salt=token_type ) + loads_kwargs["max_age"] = max_age else: - serializer = URLSafeSerializer(current_app.config["SECRET_KEY"]) + project = Project.query.get(project_id) if project_id is not None else None + password = project.password if project is not None else "" + serializer = URLSafeSerializer( + current_app.config["SECRET_KEY"] + password, salt=token_type + ) try: - data = serializer.loads(token) + data = serializer.loads(token, **loads_kwargs) except SignatureExpired: return None except BadSignature: return None - return data["project_id"] + + data_project = data[0] if isinstance(data, list) else None + return ( + data_project if project_id is None or data_project == project_id else None + ) def __str__(self): return self.name diff --git a/ihatemoney/templates/invitation_mail.en.j2 b/ihatemoney/templates/invitation_mail.en.j2 index 79fcc427..2b3157b1 100644 --- a/ihatemoney/templates/invitation_mail.en.j2 +++ b/ihatemoney/templates/invitation_mail.en.j2 @@ -4,7 +4,7 @@ Someone using the email address {{ g.project.contact_email }} invited you to sha It's as simple as saying what did you pay for, for whom, and how much did it cost you, we are caring about the rest. -You can log in using this link: {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}. +You can log in using this link: {{ url_for(".authenticate", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}. Once logged-in, you can use the following link which is easier to remember: {{ url_for(".list_bills", _external=True) }} If your cookie gets deleted or if you log out, you will need to log back in using the first link. diff --git a/ihatemoney/templates/invitation_mail.fr.j2 b/ihatemoney/templates/invitation_mail.fr.j2 index e57d7035..d095cfdb 100644 --- a/ihatemoney/templates/invitation_mail.fr.j2 +++ b/ihatemoney/templates/invitation_mail.fr.j2 @@ -4,7 +4,7 @@ Quelqu'un dont l'adresse email est {{ g.project.contact_email }} vous a invité Il suffit de renseigner qui a payé pour quoi, pour qui, combien ça a coûté, et on s’occupe du reste. -Vous pouvez vous connecter grâce à ce lien : {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}. +Vous pouvez vous connecter grâce à ce lien : {{ url_for(".authenticate", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}. Une fois connecté, vous pourrez utiliser le lien suivant qui est plus facile à mémoriser : {{ url_for(".list_bills", _external=True) }} Si vous êtes déconnecté volontairement ou non, vous devrez utiliser à nouveau le premier lien. diff --git a/ihatemoney/templates/password_reminder.en.j2 b/ihatemoney/templates/password_reminder.en.j2 index c6543546..845ff790 100644 --- a/ihatemoney/templates/password_reminder.en.j2 +++ b/ihatemoney/templates/password_reminder.en.j2 @@ -1,7 +1,7 @@ Hi, You requested to reset the password of the following project: "{{ project.name }}". -You can reset it here: {{ url_for(".reset_password", _external=True, token=project.generate_token(expiration=3600)) }}. +You can reset it here: {{ url_for(".reset_password", _external=True, token=project.generate_token(token_type="reset")) }}. This link is only valid for one hour. Hope this helps, diff --git a/ihatemoney/templates/password_reminder.fr.j2 b/ihatemoney/templates/password_reminder.fr.j2 index 17c52c4d..4603a963 100644 --- a/ihatemoney/templates/password_reminder.fr.j2 +++ b/ihatemoney/templates/password_reminder.fr.j2 @@ -1,7 +1,7 @@ Salut, Vous avez demandé à réinitialiser le mot de passe du projet suivant : "{{ project.name }}". -Vous pouvez le réinitialiser ici : {{ url_for(".reset_password", _external=True, token=project.generate_token(expiration=3600)) }}. +Vous pouvez le réinitialiser ici : {{ url_for(".reset_password", _external=True, token=project.generate_token(token_type="reset")) }}. Ce lien est seulement valide pendant 1 heure. Faites-en bon usage ! diff --git a/ihatemoney/templates/send_invites.html b/ihatemoney/templates/send_invites.html index 53492c85..8b73b175 100644 --- a/ihatemoney/templates/send_invites.html +++ b/ihatemoney/templates/send_invites.html @@ -21,8 +21,8 @@ {{ _("You can directly share the following link via your prefered medium") }}
- - {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }} + + {{ url_for(".authenticate", _external=True, project_id=g.project.id, token=g.project.generate_token()) }} diff --git a/ihatemoney/tests/api_test.py b/ihatemoney/tests/api_test.py index 41f5ab2d..83d5aa2a 100644 --- a/ihatemoney/tests/api_test.py +++ b/ihatemoney/tests/api_test.py @@ -213,7 +213,9 @@ class APITestCase(IhatemoneyTestCase): "/api/projects/raclette/token", headers=self.get_auth("raclette") ) decoded_resp = json.loads(resp.data.decode("utf-8")) - resp = self.client.get("/authenticate?token={}".format(decoded_resp["token"])) + resp = self.client.get( + f"/authenticate?token={decoded_resp['token']}&project_id=raclette" + ) # Test that we are redirected. self.assertEqual(302, resp.status_code) diff --git a/ihatemoney/tests/budget_test.py b/ihatemoney/tests/budget_test.py index 75a2dc35..af33197a 100644 --- a/ihatemoney/tests/budget_test.py +++ b/ihatemoney/tests/budget_test.py @@ -4,6 +4,7 @@ import json import re from time import sleep import unittest +from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from flask import session import pytest @@ -11,6 +12,7 @@ from werkzeug.security import check_password_hash, generate_password_hash from ihatemoney import models from ihatemoney.currency_convertor import CurrencyConverter +from ihatemoney.tests.common.help_functions import extract_link from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase from ihatemoney.versioning import LoggingMode @@ -87,11 +89,52 @@ class BudgetTestCase(IhatemoneyTestCase): ) # Test empty and invalid tokens self.client.get("/exit") + # Use another project_id + parsed_url = urlparse(url) + query = parse_qs(parsed_url.query) + query["project_id"] = "invalid" + resp = self.client.get( + urlunparse(parsed_url._replace(query=urlencode(query, doseq=True))) + ) + assert "You either provided a bad token" in resp.data.decode("utf-8") + resp = self.client.get("/authenticate") self.assertIn("You either provided a bad token", resp.data.decode("utf-8")) resp = self.client.get("/authenticate?token=token") self.assertIn("You either provided a bad token", resp.data.decode("utf-8")) + def test_invite_code_invalidation(self): + """Test that invitation link expire after code change""" + self.login("raclette") + self.post_project("raclette") + response = self.client.get("/raclette/invite").data.decode("utf-8") + link = extract_link(response, "share the following link") + + self.client.get("/exit") + response = self.client.get(link) + # Link is valid + assert response.status_code == 302 + + # Change password to invalidate token + # Other data are required, but useless for the test + response = self.client.post( + "/raclette/edit", + data={ + "name": "raclette", + "contact_email": "zorglub@notmyidea.org", + "password": "didoudida", + "default_currency": "XXX", + }, + follow_redirects=True, + ) + assert response.status_code == 200 + assert "alert-danger" not in response.data.decode("utf-8") + + self.client.get("/exit") + response = self.client.get(link, follow_redirects=True) + # Link is invalid + self.assertIn("You either provided a bad token", response.data.decode("utf-8")) + def test_password_reminder(self): # test that it is possible to have an email containing the password of a # project in case people forget it (and it happens!) diff --git a/ihatemoney/tests/common/help_functions.py b/ihatemoney/tests/common/help_functions.py index e9c4dcd1..5a401059 100644 --- a/ihatemoney/tests/common/help_functions.py +++ b/ihatemoney/tests/common/help_functions.py @@ -1,5 +1,16 @@ +from markupsafe import Markup + + def em_surround(string, regex_escape=False): if regex_escape: return r'%s<\/em>' % string else: return '%s' % string + + +def extract_link(data, start_prefix): + base_index = data.find(start_prefix) + start = data.find('href="', base_index) + 6 + end = data.find('">', base_index) + link = Markup(data[start:end]).unescape() + return link diff --git a/ihatemoney/web.py b/ihatemoney/web.py index 712d2b0e..5af15e08 100644 --- a/ihatemoney/web.py +++ b/ihatemoney/web.py @@ -199,15 +199,21 @@ def admin(): def authenticate(project_id=None): """Authentication form""" form = AuthenticationForm() + + if not form.id.data and request.args.get("project_id"): + form.id.data = request.args["project_id"] + project_id = form.id.data # Try to get project_id from token first token = request.args.get("token") if token: - project_id = Project.verify_token(token, token_type="non_timed_token") - token_auth = True + verified_project_id = Project.verify_token( + token, token_type="auth", project_id=project_id + ) + if verified_project_id == project_id: + token_auth = True + else: + project_id = None else: - if not form.id.data and request.args.get("project_id"): - form.id.data = request.args["project_id"] - project_id = form.id.data token_auth = False if project_id is None: # User doesn't provide project identifier or a valid token @@ -381,7 +387,7 @@ def reset_password(): return render_template( "reset_password.html", form=form, error=_("No token provided") ) - project_id = Project.verify_token(token) + project_id = Project.verify_token(token, token_type="reset") if not project_id: return render_template( "reset_password.html", form=form, error=_("Invalid token") From c1ef4033b6fde6181a30ab76b53f699cfe1eb7c4 Mon Sep 17 00:00:00 2001 From: Baptiste Jonglez Date: Sun, 10 Oct 2021 18:34:24 +0200 Subject: [PATCH 7/9] Better document configuration files location and override --- docs/configuration.rst | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index b0733a8a..29ab34f7 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -13,6 +13,21 @@ To know defaults on your deployed instance, simply look at your "Production values" are the recommended values for use in production. +Configuration files +------------------- + +By default, Ihatemoney loads its configuration from ``/etc/ihatemoney/ihatemoney.cfg``. + +If you need to load the configuration from a custom path, you can define the +``IHATEMONEY_SETTINGS_FILE_PATH`` environment variable with the path to the configuration +file. +For instance :: + + export IHATEMONEY_SETTINGS_FILE_PATH="/path/to/your/conf/file.cfg" + +The path should be absolute. A relative path will be interpreted as being +inside ``/etc/ihatemoney/``. + `SQLALCHEMY_DATABASE_URI` ------------------------- @@ -142,12 +157,3 @@ possible to configure it to act differently, thanks to the great * **MAIL_PASSWORD** : default **None** * **DEFAULT_MAIL_SENDER** : default **None** -Using an alternate settings path --------------------------------- - -You can put your settings file where you want, and pass its path to the -application using the ``IHATEMONEY_SETTINGS_FILE_PATH`` environment variable. - -For instance :: - - export IHATEMONEY_SETTINGS_FILE_PATH="/path/to/your/conf/file.cfg" From 7554842b1f3ae4497669d0ae2eb1e715e9125f91 Mon Sep 17 00:00:00 2001 From: zorun Date: Sun, 10 Oct 2021 18:39:03 +0200 Subject: [PATCH 8/9] Add URL validation to external link to prevent XSS (#846) Co-authored-by: Baptiste Jonglez --- ihatemoney/forms.py | 3 ++- ihatemoney/tests/budget_test.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py index fe1bdcc7..180619c7 100644 --- a/ihatemoney/forms.py +++ b/ihatemoney/forms.py @@ -13,6 +13,7 @@ from wtforms.fields.core import Label, SelectField, SelectMultipleField from wtforms.fields.html5 import DateField, DecimalField, URLField from wtforms.fields.simple import BooleanField, PasswordField, StringField, SubmitField from wtforms.validators import ( + URL, DataRequired, Email, EqualTo, @@ -292,7 +293,7 @@ class BillForm(FlaskForm): original_currency = SelectField(_("Currency"), validators=[DataRequired()]) external_link = URLField( _("External link"), - validators=[Optional()], + validators=[Optional(), URL()], description=_("A link to an external document, related to this bill"), ) payed_for = SelectMultipleField( diff --git a/ihatemoney/tests/budget_test.py b/ihatemoney/tests/budget_test.py index af33197a..1539ece7 100644 --- a/ihatemoney/tests/budget_test.py +++ b/ihatemoney/tests/budget_test.py @@ -675,6 +675,35 @@ class BudgetTestCase(IhatemoneyTestCase): bill = models.Bill.query.filter(models.Bill.date == "2011-08-01")[0] self.assertEqual(bill.amount, 25.02) + # add a bill with a valid external link + self.client.post( + "/raclette/add", + data={ + "date": "2015-05-05", + "what": "fromage à raclette", + "payer": members_ids[0], + "payed_for": members_ids, + "amount": "42", + "external_link": "https://example.com/fromage", + }, + ) + bill = models.Bill.query.filter(models.Bill.date == "2015-05-05")[0] + self.assertEqual(bill.external_link, "https://example.com/fromage") + + # add a bill with an invalid external link + resp = self.client.post( + "/raclette/add", + data={ + "date": "2015-05-06", + "what": "mauvais fromage à raclette", + "payer": members_ids[0], + "payed_for": members_ids, + "amount": "42000", + "external_link": "javascript:alert('Tu bluffes, Martoni.')", + }, + ) + self.assertIn("Invalid URL", resp.data.decode("utf-8")) + def test_weighted_balance(self): self.post_project("raclette") From e626a1cbea766ce15d528e7379208435aa5cad4f Mon Sep 17 00:00:00 2001 From: Baptiste Jonglez Date: Sun, 10 Oct 2021 17:45:56 +0200 Subject: [PATCH 9/9] Implement security best practices using Flask-Talisman --- CHANGELOG.rst | 2 ++ conf/entrypoint.sh | 1 + docs/configuration.rst | 15 +++++++++++++++ docs/contributing.rst | 12 ++++++++++-- docs/upgrade.rst | 11 +++++++++++ ihatemoney/conf-templates/ihatemoney.cfg.j2 | 4 ++++ ihatemoney/default_settings.py | 1 + ihatemoney/run.py | 19 +++++++++++++++++++ setup.cfg | 1 + 9 files changed, 64 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 89515862..326b851d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ This document describes changes between each past release. Breaking changes ---------------- +- Enable session cookie security by default (#845) - Drop support for Python 2 (#483) - Drop support for Python 3.5 (#571) - Drop support for MySQL (#743) @@ -25,6 +26,7 @@ Security - Add CSRF validation on destructive actions (#796) - Ask for private code to delete project or project history (#796) +- Add headers to mitigate Clickjacking, XSS, and other attacks: `X-Frame-Options`, `X-XSS-Protection`, `X-Content-Type-Options`, `Content-Security-Policy`, `Referrer-Policy` (#845) Added ----- diff --git a/conf/entrypoint.sh b/conf/entrypoint.sh index dfe5e12c..e66ad7c8 100755 --- a/conf/entrypoint.sh +++ b/conf/entrypoint.sh @@ -21,6 +21,7 @@ ADMIN_PASSWORD = '$ADMIN_PASSWORD' ALLOW_PUBLIC_PROJECT_CREATION = $ALLOW_PUBLIC_PROJECT_CREATION ACTIVATE_ADMIN_DASHBOARD = $ACTIVATE_ADMIN_DASHBOARD BABEL_DEFAULT_TIMEZONE = "$BABEL_DEFAULT_TIMEZONE" +SESSION_COOKIE_SECURE = $SESSION_COOKIE_SECURE EOF # Start gunicorn without forking diff --git a/docs/configuration.rst b/docs/configuration.rst index 29ab34f7..d29ef9f1 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -64,6 +64,21 @@ of the secret key could easily access any project and bypass the private code ve - **Production value:** `ihatemoney conf-example ihatemoney.cfg` sets it to something random, which is good. +`SESSION_COOKIE_SECURE` +----------------------- + +A boolean that controls whether the session cookie will be marked "secure". +If this is the case, browsers will refuse to send the session cookie over plain HTTP. + +- **Default value:** ``True`` +- **Production value:** ``True`` if you run your service over HTTPS, ``False`` if you run + your service over plain HTTP. + +Note: this setting is actually interpreted by Flask, see the +`Flask documentation`_ for details. + +.. _Flask documentation: https://flask.palletsprojects.com/en/2.0.x/config/#SESSION_COOKIE_SECURE + `MAIL_DEFAULT_SENDER` --------------------- diff --git a/docs/contributing.rst b/docs/contributing.rst index 80174b76..5d597b25 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -104,12 +104,20 @@ You can create a ``settings.cfg`` file, with the following content:: DEBUG = True SQLACHEMY_ECHO = DEBUG -You can also set the `TESTING` flag to `True` so no mails are sent -(and no exception is raised) while you're on development mode. Then before running the application, declare its path with :: export IHATEMONEY_SETTINGS_FILE_PATH="$(pwd)/settings.cfg" +You can also set the ``TESTING`` flag to ``True`` so no mails are sent +(and no exception is raised) while you're on development mode. + +In some cases, you may need to disable secure cookies by setting +``SESSION_COOKIE_SECURE`` to ``False``. This is needed if you +access your dev server over the network: with the default value +of ``SESSION_COOKIE_SECURE``, the browser will refuse to send +the session cookie over insecure HTTP, so many features of Ihatemoney +won't work (project login, language change, etc). + .. _contributing-developer: Contributing as a developer diff --git a/docs/upgrade.rst b/docs/upgrade.rst index ec846324..53185413 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -65,6 +65,17 @@ If so, pick the ``pip`` commands to use in the relevant section(s) of Then follow :ref:`general-procedure` from step 1. in order to complete the update. +Disable session cookie security if running over plain HTTP +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. note:: If you are running Ihatemoney over HTTPS, no special action is required. + +Session cookies are now marked "secure" by default to increase security. + +If you run Ihatemoney over plain HTTP, you need to explicitly disable this security +feature by setting ``SESSION_COOKIE_SECURE`` to ``False``, see :ref:`configuration`. + + Switch to MariaDB >= 10.3.2 instead of MySQL ++++++++++++++++++++++++++++++++++++++++++++ diff --git a/ihatemoney/conf-templates/ihatemoney.cfg.j2 b/ihatemoney/conf-templates/ihatemoney.cfg.j2 index 0188c6b1..13a8e9f5 100644 --- a/ihatemoney/conf-templates/ihatemoney.cfg.j2 +++ b/ihatemoney/conf-templates/ihatemoney.cfg.j2 @@ -38,3 +38,7 @@ ACTIVATE_ADMIN_DASHBOARD = False # You can change the timezone used to display time. By default it will be #derived from the server OS. #BABEL_DEFAULT_TIMEZONE = "Europe/Paris" + +# Enable secure cookies. Requires HTTPS. Disable if you run your ihatemoney +# service over plain HTTP. +SESSION_COOKIE_SECURE = True diff --git a/ihatemoney/default_settings.py b/ihatemoney/default_settings.py index 9050bbeb..96795a01 100644 --- a/ihatemoney/default_settings.py +++ b/ihatemoney/default_settings.py @@ -8,6 +8,7 @@ ACTIVATE_DEMO_PROJECT = True ADMIN_PASSWORD = "" ALLOW_PUBLIC_PROJECT_CREATION = True ACTIVATE_ADMIN_DASHBOARD = False +SESSION_COOKIE_SECURE = True SUPPORTED_LANGUAGES = [ "de", "el", diff --git a/ihatemoney/run.py b/ihatemoney/run.py index c8fc5b25..cea6f93e 100644 --- a/ihatemoney/run.py +++ b/ihatemoney/run.py @@ -7,6 +7,7 @@ from flask import Flask, g, render_template, request, session from flask_babel import Babel, format_currency from flask_mail import Mail from flask_migrate import Migrate, stamp, upgrade +from flask_talisman import Talisman from jinja2 import pass_context from markupsafe import Markup import pytz @@ -126,6 +127,24 @@ def create_app( instance_relative_config=instance_relative_config, ) + # If we need to load external JS/CSS/image resources, it needs to be added here, see + # https://github.com/wntrblm/flask-talisman#content-security-policy + csp = { + "default-src": ["'self'"], + # We have several inline javascript scripts :( + "script-src": ["'self'", "'unsafe-inline'"], + "object-src": "'none'", + } + + Talisman( + app, + # Forcing HTTPS is the job of a reverse proxy + force_https=False, + # This is handled separately through the SESSION_COOKIE_SECURE Flask setting + session_cookie_secure=False, + content_security_policy=csp, + ) + # If a configuration object is passed, use it. Otherwise try to find one. load_configuration(app, configuration) app.wsgi_app = PrefixedWSGI(app) diff --git a/setup.cfg b/setup.cfg index 3bbf6f27..f58c1619 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ install_requires = Flask-Migrate>=2.5.3,<4 # Not following semantic versioning (e.g. https://github.com/miguelgrinberg/flask-migrate/commit/1af28ba273de6c88544623b8dc02dd539340294b) Flask-RESTful>=0.3.9,<1 Flask-SQLAlchemy>=2.4,<3 + Flask-Talisman>=0.8,<1 Flask-WTF>=0.14.3,<1 WTForms>=2.3.1,<2.4 Flask>=2,<3