1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

All: Fixes #5051: Fixed error that could prevent a revision from being created, and that would prevent the revision service from processing the rest of the notes

This commit is contained in:
Laurent Cozic 2021-06-20 11:19:59 +01:00
parent e79f965e5d
commit 097e49d797
10 changed files with 1099 additions and 155 deletions

View File

@ -968,6 +968,9 @@ packages/lib/models/ResourceLocalState.js.map
packages/lib/models/Revision.d.ts
packages/lib/models/Revision.js
packages/lib/models/Revision.js.map
packages/lib/models/Revision.test.d.ts
packages/lib/models/Revision.test.js
packages/lib/models/Revision.test.js.map
packages/lib/models/Search.d.ts
packages/lib/models/Search.js
packages/lib/models/Search.js.map
@ -1088,6 +1091,9 @@ packages/lib/services/ResourceService.test.js.map
packages/lib/services/RevisionService.d.ts
packages/lib/services/RevisionService.js
packages/lib/services/RevisionService.js.map
packages/lib/services/RevisionService.test.d.ts
packages/lib/services/RevisionService.test.js
packages/lib/services/RevisionService.test.js.map
packages/lib/services/SettingUtils.d.ts
packages/lib/services/SettingUtils.js
packages/lib/services/SettingUtils.js.map

6
.gitignore vendored
View File

@ -954,6 +954,9 @@ packages/lib/models/ResourceLocalState.js.map
packages/lib/models/Revision.d.ts
packages/lib/models/Revision.js
packages/lib/models/Revision.js.map
packages/lib/models/Revision.test.d.ts
packages/lib/models/Revision.test.js
packages/lib/models/Revision.test.js.map
packages/lib/models/Search.d.ts
packages/lib/models/Search.js
packages/lib/models/Search.js.map
@ -1074,6 +1077,9 @@ packages/lib/services/ResourceService.test.js.map
packages/lib/services/RevisionService.d.ts
packages/lib/services/RevisionService.js
packages/lib/services/RevisionService.js.map
packages/lib/services/RevisionService.test.d.ts
packages/lib/services/RevisionService.test.js
packages/lib/services/RevisionService.test.js.map
packages/lib/services/SettingUtils.d.ts
packages/lib/services/SettingUtils.js
packages/lib/services/SettingUtils.js.map

View File

@ -0,0 +1,742 @@
# Reserved Strings
#
# Strings which may be used elsewhere in code
undefined
undef
null
NULL
(null)
nil
NIL
true
false
True
False
TRUE
FALSE
None
hasOwnProperty
then
constructor
\
\\
# Numeric Strings
#
# Strings which can be interpreted as numeric
0
1
1.00
$1.00
1/2
1E2
1E02
1E+02
-1
-1.00
-$1.00
-1/2
-1E2
-1E02
-1E+02
1/0
0/0
-2147483648/-1
-9223372036854775808/-1
-0
-0.0
+0
+0.0
0.00
0..0
.
0.0.0
0,00
0,,0
,
0,0,0
0.0/0
1.0/0.0
0.0/0.0
1,0/0,0
0,0/0,0
--1
-
-.
-,
999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
NaN
Infinity
-Infinity
INF
1#INF
-1#IND
1#QNAN
1#SNAN
1#IND
0x0
0xffffffff
0xffffffffffffffff
0xabad1dea
123456789012345678901234567890123456789
1,000.00
1 000.00
1'000.00
1,000,000.00
1 000 000.00
1'000'000.00
1.000,00
1 000,00
1'000,00
1.000.000,00
1 000 000,00
1'000'000,00
01000
08
09
2.2250738585072011e-308
# Special Characters
#
# ASCII punctuation. All of these characters may need to be escaped in some
# contexts. Divided into three groups based on (US-layout) keyboard position.
,./;'[]\-=
<>?:"{}|_+
!@#$%^&*()`~
# Non-whitespace C0 controls: U+0001 through U+0008, U+000E through U+001F,
# and U+007F (DEL)
# Often forbidden to appear in various text-based file formats (e.g. XML),
# or reused for internal delimiters on the theory that they should never
# appear in input.
# The next line may appear to be blank or mojibake in some viewers.

# Non-whitespace C1 controls: U+0080 through U+0084 and U+0086 through U+009F.
# Commonly misinterpreted as additional graphic characters.
# The next line may appear to be blank, mojibake, or dingbats in some viewers.
€‚ƒ„†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ
# Whitespace: all of the characters with category Zs, Zl, or Zp (in Unicode
# version 8.0.0), plus U+0009 (HT), U+000B (VT), U+000C (FF), U+0085 (NEL),
# and U+200B (ZERO WIDTH SPACE), which are in the C categories but are often
# treated as whitespace in some contexts.
# This file unfortunately cannot express strings containing
# U+0000, U+000A, or U+000D (NUL, LF, CR).
# The next line may appear to be blank or mojibake in some viewers.
# The next line may be flagged for "trailing whitespace" in some viewers.
…             ​

   
# Unicode additional control characters: all of the characters with
# general category Cf (in Unicode 8.0.0).
# The next line may appear to be blank or mojibake in some viewers.
­؀؁؂؃؄؅؜۝܏᠎​‌‍‎‏‪‫‬‭‮⁠⁡⁢⁣⁤⁦⁧⁨⁩𑂽𛲠𛲡𛲢𛲣𝅳𝅴𝅵𝅶𝅷𝅸𝅹𝅺󠀁󠀠󠀡󠀢󠀣󠀤󠀥󠀦󠀧󠀨󠀩󠀪󠀫󠀬󠀭󠀮󠀯󠀰󠀱󠀲󠀳󠀴󠀵󠀶󠀷󠀸󠀹󠀺󠀻󠀼󠀽󠀾󠀿󠁀󠁁󠁂󠁃󠁄󠁅󠁆󠁇󠁈󠁉󠁊󠁋󠁌󠁍󠁎󠁏󠁐󠁑󠁒󠁓󠁔󠁕󠁖󠁗󠁘󠁙󠁚󠁛󠁜󠁝󠁞󠁟󠁠󠁡󠁢󠁣󠁤󠁥󠁦󠁧󠁨󠁩󠁪󠁫󠁬󠁭󠁮󠁯󠁰󠁱󠁲󠁳󠁴󠁵󠁶󠁷󠁸󠁹󠁺󠁻󠁼󠁽󠁾󠁿
# "Byte order marks", U+FEFF and U+FFFE, each on its own line.
# The next two lines may appear to be blank or mojibake in some viewers.

# Unicode Symbols
#
# Strings which contain common unicode symbols (e.g. smart quotes)
Ω≈ç√∫˜µ≤≥÷
åß∂ƒ©˙∆˚¬…æ
œ∑´®†¥¨ˆøπ“‘
¡™£¢∞§¶•ªº–≠
¸˛Ç◊ı˜Â¯˘¿
ÅÍÎÏ˝ÓÔÒÚÆ☃
Œ„´‰ˇÁ¨ˆØ∏”’
`⁄€‹›fifl‡°·‚—±
⅛⅜⅝⅞
ЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя
٠١٢٣٤٥٦٧٨٩
# Unicode Subscript/Superscript/Accents
#
# Strings which contain unicode subscripts/superscripts; can cause rendering issues
⁰⁴⁵
₀₁₂
⁰⁴⁵₀₁₂
ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็
# Quotation Marks
#
# Strings which contain misplaced quotation marks; can cause encoding errors
'
"
''
""
'"'
"''''"'"
"'"'"''''"
<foo val=“bar” />
<foo val=“bar” />
<foo val=”bar“ />
<foo val=`bar' />
# Two-Byte Characters
#
# Strings which contain two-byte characters: can cause rendering issues or character-length issues
田中さんにあげて下さい
パーティーへ行かないか
和製漢語
部落格
사회과학원 어학연구소
찦차를 타고 온 펲시맨과 쑛다리 똠방각하
社會科學院語學研究所
울란바토르
𠜎𠜱𠝹𠱓𠱸𠲖𠳏
# Strings which contain two-byte letters: can cause issues with naïve UTF-16 capitalizers which think that 16 bits == 1 character
𐐜 𐐔𐐇𐐝𐐀𐐡𐐇𐐓 𐐙𐐊𐐡𐐝𐐓/𐐝𐐇𐐗𐐊𐐤𐐔 𐐒𐐋𐐗 𐐒𐐌 𐐜 𐐡𐐀𐐖𐐇𐐤𐐓𐐝 𐐱𐑂 𐑄 𐐔𐐇𐐝𐐀𐐡𐐇𐐓 𐐏𐐆𐐅𐐤𐐆𐐚𐐊𐐡𐐝𐐆𐐓𐐆
# Special Unicode Characters Union
#
# A super string recommended by VMware Inc. Globalization Team: can effectively cause rendering issues or character-length issues to validate product globalization readiness.
#
# 表 CJK_UNIFIED_IDEOGRAPHS (U+8868)
# ポ KATAKANA LETTER PO (U+30DD)
# あ HIRAGANA LETTER A (U+3042)
# A LATIN CAPITAL LETTER A (U+0041)
# 鷗 CJK_UNIFIED_IDEOGRAPHS (U+9DD7)
# Œ LATIN SMALL LIGATURE OE (U+0153)
# é LATIN SMALL LETTER E WITH ACUTE (U+00E9)
# B FULLWIDTH LATIN CAPITAL LETTER B (U+FF22)
# 逍 CJK_UNIFIED_IDEOGRAPHS (U+900D)
# Ü LATIN SMALL LETTER U WITH DIAERESIS (U+00FC)
# ß LATIN SMALL LETTER SHARP S (U+00DF)
# ª FEMININE ORDINAL INDICATOR (U+00AA)
# ą LATIN SMALL LETTER A WITH OGONEK (U+0105)
# ñ LATIN SMALL LETTER N WITH TILDE (U+00F1)
# 丂 CJK_UNIFIED_IDEOGRAPHS (U+4E02)
# 㐀 CJK Ideograph Extension A, First (U+3400)
# 𠀀 CJK Ideograph Extension B, First (U+20000)
表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀
# Changing length when lowercased
#
# Characters which increase in length (2 to 3 bytes) when lowercased
# Credit: https://twitter.com/jifa/status/625776454479970304
Ⱥ
Ⱦ
# Japanese Emoticons
#
# Strings which consists of Japanese-style emoticons which are popular on the web
ヽ༼ຈل͜ຈ༽ノ ヽ༼ຈل͜ຈ༽ノ
(。◕ ∀ ◕。)
`ィ(´∀`∩
__ロ(,_,*)
・( ̄∀ ̄)・:*:
゚・✿ヾ╲(。◕‿◕。)╱✿・゚
,。・:*:・゜’( ☻ ω ☻ )。・:*:・゜’
(╯°□°)╯︵ ┻━┻)
(ノಥ益ಥ)ノ ┻━┻
┬─┬ノ( º _ ºノ)
( ͡° ͜ʖ ͡°)
¯\_(ツ)_/¯
# Emoji
#
# Strings which contain Emoji; should be the same behavior as two-byte characters, but not always
😍
👩🏽
👨‍🦰 👨🏿‍🦰 👨‍🦱 👨🏿‍🦱 🦹🏿‍♂️
👾 🙇 💁 🙅 🙆 🙋 🙎 🙍
🐵 🙈 🙉 🙊
❤️ 💔 💌 💕 💞 💓 💗 💖 💘 💝 💟 💜 💛 💚 💙
✋🏿 💪🏿 👐🏿 🙌🏿 👏🏿 🙏🏿
👨‍👩‍👦 👨‍👩‍👧‍👦 👨‍👨‍👦 👩‍👩‍👧 👨‍👦 👨‍👧‍👦 👩‍👦 👩‍👧‍👦
🚾 🆒 🆓 🆕 🆖 🆗 🆙 🏧
0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟
# Regional Indicator Symbols
#
# Regional Indicator Symbols can be displayed differently across
# fonts, and have a number of special behaviors
🇺🇸🇷🇺🇸 🇦🇫🇦🇲🇸
🇺🇸🇷🇺🇸🇦🇫🇦🇲
🇺🇸🇷🇺🇸🇦
# Unicode Numbers
#
# Strings which contain unicode numbers; if the code is localized, it should see the input as numeric
123
١٢٣
# Right-To-Left Strings
#
# Strings which contain text that should be rendered RTL if possible (e.g. Arabic, Hebrew)
ثم نفس سقطت وبالتحديد،, جزيرتي باستخدام أن دنو. إذ هنا؟ الستار وتنصيب كان. أهّل ايطاليا، بريطانيا-فرنسا قد أخذ. سليمان، إتفاقية بين ما, يذكر الحدود أي بعد, معاملة بولندا، الإطلاق عل إيو.
בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ
הָיְתָהtestالصفحات التّحول
مُنَاقَشَةُ سُبُلِ اِسْتِخْدَامِ اللُّغَةِ فِي النُّظُمِ الْقَائِمَةِ وَفِيم يَخُصَّ التَّطْبِيقَاتُ الْحاسُوبِيَّةُ،
الكل في المجمو عة (5)
# Ogham Text
#
# The only unicode alphabet to use a space which isn't empty but should still act like a space.
᚛ᚄᚓᚐᚋᚒᚄ ᚑᚄᚂᚑᚏᚅ᚜
᚛                 ᚜
# Trick Unicode
#
# Strings which contain unicode with unusual properties (e.g. Right-to-left override) (c.f. http://www.unicode.org/charts/PDF/U2000.pdf)
‪‪test‪
‫test‫

test

test⁠test‫
⁦test⁧
# Zalgo Text
#
# Strings which contain "corrupted" text. The corruption will not appear in non-HTML text, however. (via http://www.eeemo.net)
Ṱ̺̺̕o͞ ̷i̲̬͇̪͙n̝̗͕v̟̜̘̦͟o̶̙̰̠kè͚̮̺̪̹̱̤ ̖t̝͕̳̣̻̪͞h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳ ̞̥̱̳̭r̛̗̘e͙p͠r̼̞̻̭̗e̺̠̣͟s̘͇̳͍̝͉e͉̥̯̞̲͚̬͜ǹ̬͎͎̟̖͇̤t͍̬̤͓̼̭͘ͅi̪̱n͠g̴͉ ͏͉ͅc̬̟h͡a̫̻̯͘o̫̟̖͍̙̝͉s̗̦̲.̨̹͈̣
̡͓̞ͅI̗̘̦͝n͇͇͙v̮̫ok̲̫̙͈i̖͙̭̹̠̞n̡̻̮̣̺g̲͈͙̭͙̬͎ ̰t͔̦h̞̲e̢̤ ͍̬̲͖f̴̘͕̣è͖ẹ̥̩l͖͔͚i͓͚̦͠n͖͍̗͓̳̮g͍ ̨o͚̪͡f̘̣̬ ̖̘͖̟͙̮c҉͔̫͖͓͇͖ͅh̵̤̣͚͔á̗̼͕ͅo̼̣̥s̱͈̺̖̦̻͢.̛̖̞̠̫̰
̗̺͖̹̯͓Ṯ̤͍̥͇͈h̲́e͏͓̼̗̙̼̣͔ ͇̜̱̠͓͍ͅN͕͠e̗̱z̘̝̜̺͙p̤̺̹͍̯͚e̠̻̠͜r̨̤͍̺̖͔̖̖d̠̟̭̬̝͟i̦͖̩͓͔̤a̠̗̬͉̙n͚͜ ̻̞̰͚ͅh̵͉i̳̞v̢͇ḙ͎͟-҉̭̩̼͔m̤̭̫i͕͇̝̦n̗͙ḍ̟ ̯̲͕͞ǫ̟̯̰̲͙̻̝f ̪̰̰̗̖̭̘͘c̦͍̲̞͍̩̙ḥ͚a̮͎̟̙͜ơ̩̹͎s̤.̝̝ ҉Z̡̖̜͖̰̣͉̜a͖̰͙̬͡l̲̫̳͍̩g̡̟̼̱͚̞̬ͅo̗͜.̟
̦H̬̤̗̤͝e͜ ̜̥̝̻͍̟́w̕h̖̯͓o̝͙̖͎̱̮ ҉̺̙̞̟͈W̷̼̭a̺̪͍į͈͕̭͙̯̜t̶̼̮s̘͙͖̕ ̠̫̠B̻͍͙͉̳ͅe̵h̵̬͇̫͙i̹͓̳̳̮͎̫̕n͟d̴̪̜̖ ̰͉̩͇͙̲͞ͅT͖̼͓̪͢h͏͓̮̻e̬̝̟ͅ ̤̹̝W͙̞̝͔͇͝ͅa͏͓͔̹̼̣l̴͔̰̤̟͔ḽ̫.͕
Z̮̞̠͙͔ͅḀ̗̞͈̻̗Ḷ͙͎̯̹̞͓G̻O̭̗̮
# Unicode Upsidedown
#
# Strings which contain unicode with an "upsidedown" effect (via http://www.upsidedowntext.com)
˙ɐnbᴉlɐ ɐuƃɐɯ ǝɹolop ʇǝ ǝɹoqɐl ʇn ʇunpᴉpᴉɔuᴉ ɹodɯǝʇ poɯsnᴉǝ op pǝs 'ʇᴉlǝ ƃuᴉɔsᴉdᴉpɐ ɹnʇǝʇɔǝsuoɔ 'ʇǝɯɐ ʇᴉs ɹolop ɯnsdᴉ ɯǝɹo˥
00˙Ɩ$-
# Unicode font
#
# Strings which contain bold/italic/etc. versions of normal characters
The quick brown fox jumps over the lazy dog
𝐓𝐡𝐞 𝐪𝐮𝐢𝐜𝐤 𝐛𝐫𝐨𝐰𝐧 𝐟𝐨𝐱 𝐣𝐮𝐦𝐩𝐬 𝐨𝐯𝐞𝐫 𝐭𝐡𝐞 𝐥𝐚𝐳𝐲 𝐝𝐨𝐠
𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌
𝑻𝒉𝒆 𝒒𝒖𝒊𝒄𝒌 𝒃𝒓𝒐𝒘𝒏 𝒇𝒐𝒙 𝒋𝒖𝒎𝒑𝒔 𝒐𝒗𝒆𝒓 𝒕𝒉𝒆 𝒍𝒂𝒛𝒚 𝒅𝒐𝒈
𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁 𝓳𝓾𝓶𝓹𝓼 𝓸𝓿𝓮𝓻 𝓽𝓱𝓮 𝓵𝓪𝔃𝔂 𝓭𝓸𝓰
𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘
𝚃𝚑𝚎 𝚚𝚞𝚒𝚌𝚔 𝚋𝚛𝚘𝚠𝚗 𝚏𝚘𝚡 𝚓𝚞𝚖𝚙𝚜 𝚘𝚟𝚎𝚛 𝚝𝚑𝚎 𝚕𝚊𝚣𝚢 𝚍𝚘𝚐
⒯⒣⒠ ⒬⒰⒤⒞⒦ ⒝⒭⒪⒲⒩ ⒡⒪⒳ ⒥⒰⒨⒫⒮ ⒪⒱⒠⒭ ⒯⒣⒠ ⒧⒜⒵⒴ ⒟⒪⒢
# Script Injection
#
# Strings which attempt to invoke a benign script injection; shows vulnerability to XSS
<script>alert(0)</script>
&lt;script&gt;alert(&#39;1&#39;);&lt;/script&gt;
<img src=x onerror=alert(2) />
<svg><script>123<1>alert(3)</script>
"><script>alert(4)</script>
'><script>alert(5)</script>
><script>alert(6)</script>
</script><script>alert(7)</script>
< / script >< script >alert(8)< / script >
 onfocus=JaVaSCript:alert(9) autofocus
" onfocus=JaVaSCript:alert(10) autofocus
' onfocus=JaVaSCript:alert(11) autofocus
<script>alert(12)</script>
<sc<script>ript>alert(13)</sc</script>ript>
--><script>alert(14)</script>
";alert(15);t="
';alert(16);t='
JavaSCript:alert(17)
;alert(18);
src=JaVaSCript:prompt(19)
"><script>alert(20);</script x="
'><script>alert(21);</script x='
><script>alert(22);</script x=
" autofocus onkeyup="javascript:alert(23)
' autofocus onkeyup='javascript:alert(24)
<script\x20type="text/javascript">javascript:alert(25);</script>
<script\x3Etype="text/javascript">javascript:alert(26);</script>
<script\x0Dtype="text/javascript">javascript:alert(27);</script>
<script\x09type="text/javascript">javascript:alert(28);</script>
<script\x0Ctype="text/javascript">javascript:alert(29);</script>
<script\x2Ftype="text/javascript">javascript:alert(30);</script>
<script\x0Atype="text/javascript">javascript:alert(31);</script>
'`"><\x3Cscript>javascript:alert(32)</script>
'`"><\x00script>javascript:alert(33)</script>
ABC<div style="x\x3Aexpression(javascript:alert(34)">DEF
ABC<div style="x:expression\x5C(javascript:alert(35)">DEF
ABC<div style="x:expression\x00(javascript:alert(36)">DEF
ABC<div style="x:exp\x00ression(javascript:alert(37)">DEF
ABC<div style="x:exp\x5Cression(javascript:alert(38)">DEF
ABC<div style="x:\x0Aexpression(javascript:alert(39)">DEF
ABC<div style="x:\x09expression(javascript:alert(40)">DEF
ABC<div style="x:\xE3\x80\x80expression(javascript:alert(41)">DEF
ABC<div style="x:\xE2\x80\x84expression(javascript:alert(42)">DEF
ABC<div style="x:\xC2\xA0expression(javascript:alert(43)">DEF
ABC<div style="x:\xE2\x80\x80expression(javascript:alert(44)">DEF
ABC<div style="x:\xE2\x80\x8Aexpression(javascript:alert(45)">DEF
ABC<div style="x:\x0Dexpression(javascript:alert(46)">DEF
ABC<div style="x:\x0Cexpression(javascript:alert(47)">DEF
ABC<div style="x:\xE2\x80\x87expression(javascript:alert(48)">DEF
ABC<div style="x:\xEF\xBB\xBFexpression(javascript:alert(49)">DEF
ABC<div style="x:\x20expression(javascript:alert(50)">DEF
ABC<div style="x:\xE2\x80\x88expression(javascript:alert(51)">DEF
ABC<div style="x:\x00expression(javascript:alert(52)">DEF
ABC<div style="x:\xE2\x80\x8Bexpression(javascript:alert(53)">DEF
ABC<div style="x:\xE2\x80\x86expression(javascript:alert(54)">DEF
ABC<div style="x:\xE2\x80\x85expression(javascript:alert(55)">DEF
ABC<div style="x:\xE2\x80\x82expression(javascript:alert(56)">DEF
ABC<div style="x:\x0Bexpression(javascript:alert(57)">DEF
ABC<div style="x:\xE2\x80\x81expression(javascript:alert(58)">DEF
ABC<div style="x:\xE2\x80\x83expression(javascript:alert(59)">DEF
ABC<div style="x:\xE2\x80\x89expression(javascript:alert(60)">DEF
<a href="\x0Bjavascript:javascript:alert(61)" id="fuzzelement1">test</a>
<a href="\x0Fjavascript:javascript:alert(62)" id="fuzzelement1">test</a>
<a href="\xC2\xA0javascript:javascript:alert(63)" id="fuzzelement1">test</a>
<a href="\x05javascript:javascript:alert(64)" id="fuzzelement1">test</a>
<a href="\xE1\xA0\x8Ejavascript:javascript:alert(65)" id="fuzzelement1">test</a>
<a href="\x18javascript:javascript:alert(66)" id="fuzzelement1">test</a>
<a href="\x11javascript:javascript:alert(67)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x88javascript:javascript:alert(68)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x89javascript:javascript:alert(69)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x80javascript:javascript:alert(70)" id="fuzzelement1">test</a>
<a href="\x17javascript:javascript:alert(71)" id="fuzzelement1">test</a>
<a href="\x03javascript:javascript:alert(72)" id="fuzzelement1">test</a>
<a href="\x0Ejavascript:javascript:alert(73)" id="fuzzelement1">test</a>
<a href="\x1Ajavascript:javascript:alert(74)" id="fuzzelement1">test</a>
<a href="\x00javascript:javascript:alert(75)" id="fuzzelement1">test</a>
<a href="\x10javascript:javascript:alert(76)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x82javascript:javascript:alert(77)" id="fuzzelement1">test</a>
<a href="\x20javascript:javascript:alert(78)" id="fuzzelement1">test</a>
<a href="\x13javascript:javascript:alert(79)" id="fuzzelement1">test</a>
<a href="\x09javascript:javascript:alert(80)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x8Ajavascript:javascript:alert(81)" id="fuzzelement1">test</a>
<a href="\x14javascript:javascript:alert(82)" id="fuzzelement1">test</a>
<a href="\x19javascript:javascript:alert(83)" id="fuzzelement1">test</a>
<a href="\xE2\x80\xAFjavascript:javascript:alert(84)" id="fuzzelement1">test</a>
<a href="\x1Fjavascript:javascript:alert(85)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x81javascript:javascript:alert(86)" id="fuzzelement1">test</a>
<a href="\x1Djavascript:javascript:alert(87)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x87javascript:javascript:alert(88)" id="fuzzelement1">test</a>
<a href="\x07javascript:javascript:alert(89)" id="fuzzelement1">test</a>
<a href="\xE1\x9A\x80javascript:javascript:alert(90)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x83javascript:javascript:alert(91)" id="fuzzelement1">test</a>
<a href="\x04javascript:javascript:alert(92)" id="fuzzelement1">test</a>
<a href="\x01javascript:javascript:alert(93)" id="fuzzelement1">test</a>
<a href="\x08javascript:javascript:alert(94)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x84javascript:javascript:alert(95)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x86javascript:javascript:alert(96)" id="fuzzelement1">test</a>
<a href="\xE3\x80\x80javascript:javascript:alert(97)" id="fuzzelement1">test</a>
<a href="\x12javascript:javascript:alert(98)" id="fuzzelement1">test</a>
<a href="\x0Djavascript:javascript:alert(99)" id="fuzzelement1">test</a>
<a href="\x0Ajavascript:javascript:alert(100)" id="fuzzelement1">test</a>
<a href="\x0Cjavascript:javascript:alert(101)" id="fuzzelement1">test</a>
<a href="\x15javascript:javascript:alert(102)" id="fuzzelement1">test</a>
<a href="\xE2\x80\xA8javascript:javascript:alert(103)" id="fuzzelement1">test</a>
<a href="\x16javascript:javascript:alert(104)" id="fuzzelement1">test</a>
<a href="\x02javascript:javascript:alert(105)" id="fuzzelement1">test</a>
<a href="\x1Bjavascript:javascript:alert(106)" id="fuzzelement1">test</a>
<a href="\x06javascript:javascript:alert(107)" id="fuzzelement1">test</a>
<a href="\xE2\x80\xA9javascript:javascript:alert(108)" id="fuzzelement1">test</a>
<a href="\xE2\x80\x85javascript:javascript:alert(109)" id="fuzzelement1">test</a>
<a href="\x1Ejavascript:javascript:alert(110)" id="fuzzelement1">test</a>
<a href="\xE2\x81\x9Fjavascript:javascript:alert(111)" id="fuzzelement1">test</a>
<a href="\x1Cjavascript:javascript:alert(112)" id="fuzzelement1">test</a>
<a href="javascript\x00:javascript:alert(113)" id="fuzzelement1">test</a>
<a href="javascript\x3A:javascript:alert(114)" id="fuzzelement1">test</a>
<a href="javascript\x09:javascript:alert(115)" id="fuzzelement1">test</a>
<a href="javascript\x0D:javascript:alert(116)" id="fuzzelement1">test</a>
<a href="javascript\x0A:javascript:alert(117)" id="fuzzelement1">test</a>
`"'><img src=xxx:x \x0Aonerror=javascript:alert(118)>
`"'><img src=xxx:x \x22onerror=javascript:alert(119)>
`"'><img src=xxx:x \x0Bonerror=javascript:alert(120)>
`"'><img src=xxx:x \x0Donerror=javascript:alert(121)>
`"'><img src=xxx:x \x2Fonerror=javascript:alert(122)>
`"'><img src=xxx:x \x09onerror=javascript:alert(123)>
`"'><img src=xxx:x \x0Conerror=javascript:alert(124)>
`"'><img src=xxx:x \x00onerror=javascript:alert(125)>
`"'><img src=xxx:x \x27onerror=javascript:alert(126)>
`"'><img src=xxx:x \x20onerror=javascript:alert(127)>
"`'><script>\x3Bjavascript:alert(128)</script>
"`'><script>\x0Djavascript:alert(129)</script>
"`'><script>\xEF\xBB\xBFjavascript:alert(130)</script>
"`'><script>\xE2\x80\x81javascript:alert(131)</script>
"`'><script>\xE2\x80\x84javascript:alert(132)</script>
"`'><script>\xE3\x80\x80javascript:alert(133)</script>
"`'><script>\x09javascript:alert(134)</script>
"`'><script>\xE2\x80\x89javascript:alert(135)</script>
"`'><script>\xE2\x80\x85javascript:alert(136)</script>
"`'><script>\xE2\x80\x88javascript:alert(137)</script>
"`'><script>\x00javascript:alert(138)</script>
"`'><script>\xE2\x80\xA8javascript:alert(139)</script>
"`'><script>\xE2\x80\x8Ajavascript:alert(140)</script>
"`'><script>\xE1\x9A\x80javascript:alert(141)</script>
"`'><script>\x0Cjavascript:alert(142)</script>
"`'><script>\x2Bjavascript:alert(143)</script>
"`'><script>\xF0\x90\x96\x9Ajavascript:alert(144)</script>
"`'><script>-javascript:alert(145)</script>
"`'><script>\x0Ajavascript:alert(146)</script>
"`'><script>\xE2\x80\xAFjavascript:alert(147)</script>
"`'><script>\x7Ejavascript:alert(148)</script>
"`'><script>\xE2\x80\x87javascript:alert(149)</script>
"`'><script>\xE2\x81\x9Fjavascript:alert(150)</script>
"`'><script>\xE2\x80\xA9javascript:alert(151)</script>
"`'><script>\xC2\x85javascript:alert(152)</script>
"`'><script>\xEF\xBF\xAEjavascript:alert(153)</script>
"`'><script>\xE2\x80\x83javascript:alert(154)</script>
"`'><script>\xE2\x80\x8Bjavascript:alert(155)</script>
"`'><script>\xEF\xBF\xBEjavascript:alert(156)</script>
"`'><script>\xE2\x80\x80javascript:alert(157)</script>
"`'><script>\x21javascript:alert(158)</script>
"`'><script>\xE2\x80\x82javascript:alert(159)</script>
"`'><script>\xE2\x80\x86javascript:alert(160)</script>
"`'><script>\xE1\xA0\x8Ejavascript:alert(161)</script>
"`'><script>\x0Bjavascript:alert(162)</script>
"`'><script>\x20javascript:alert(163)</script>
"`'><script>\xC2\xA0javascript:alert(164)</script>
<img \x00src=x onerror="alert(165)">
<img \x47src=x onerror="javascript:alert(166)">
<img \x11src=x onerror="javascript:alert(167)">
<img \x12src=x onerror="javascript:alert(168)">
<img\x47src=x onerror="javascript:alert(169)">
<img\x10src=x onerror="javascript:alert(170)">
<img\x13src=x onerror="javascript:alert(171)">
<img\x32src=x onerror="javascript:alert(172)">
<img\x47src=x onerror="javascript:alert(173)">
<img\x11src=x onerror="javascript:alert(174)">
<img \x47src=x onerror="javascript:alert(175)">
<img \x34src=x onerror="javascript:alert(176)">
<img \x39src=x onerror="javascript:alert(177)">
<img \x00src=x onerror="javascript:alert(178)">
<img src\x09=x onerror="javascript:alert(179)">
<img src\x10=x onerror="javascript:alert(180)">
<img src\x13=x onerror="javascript:alert(181)">
<img src\x32=x onerror="javascript:alert(182)">
<img src\x12=x onerror="javascript:alert(183)">
<img src\x11=x onerror="javascript:alert(184)">
<img src\x00=x onerror="javascript:alert(185)">
<img src\x47=x onerror="javascript:alert(186)">
<img src=x\x09onerror="javascript:alert(187)">
<img src=x\x10onerror="javascript:alert(188)">
<img src=x\x11onerror="javascript:alert(189)">
<img src=x\x12onerror="javascript:alert(190)">
<img src=x\x13onerror="javascript:alert(191)">
<img[a][b][c]src[d]=x[e]onerror=[f]"alert(192)">
<img src=x onerror=\x09"javascript:alert(193)">
<img src=x onerror=\x10"javascript:alert(194)">
<img src=x onerror=\x11"javascript:alert(195)">
<img src=x onerror=\x12"javascript:alert(196)">
<img src=x onerror=\x32"javascript:alert(197)">
<img src=x onerror=\x00"javascript:alert(198)">
<a href=java&#1&#2&#3&#4&#5&#6&#7&#8&#11&#12script:javascript:alert(199)>XXX</a>
<img src="x` `<script>javascript:alert(200)</script>"` `>
<img src onerror /" '"= alt=javascript:alert(201)//">
<title onpropertychange=javascript:alert(202)></title><title title=>
<a href=http://foo.bar/#x=`y></a><img alt="`><img src=x:x onerror=javascript:alert(203)></a>">
<!--[if]><script>javascript:alert(204)</script -->
<!--[if<img src=x onerror=javascript:alert(205)//]> -->
<script src="/\%(jscript)s"></script>
<script src="\\%(jscript)s"></script>
<IMG """><SCRIPT>alert("206")</SCRIPT>">
<IMG SRC=javascript:alert(String.fromCharCode(50,48,55))>
<IMG SRC=# onmouseover="alert('208')">
<IMG SRC= onmouseover="alert('209')">
<IMG onmouseover="alert('210')">
<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#50;&#49;&#49;&#39;&#41;>
<IMG SRC=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000050&#0000049&#0000050&#0000039&#0000041>
<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x32&#x31&#x33&#x27&#x29>
<IMG SRC="jav   ascript:alert('214');">
<IMG SRC="jav&#x09;ascript:alert('215');">
<IMG SRC="jav&#x0A;ascript:alert('216');">
<IMG SRC="jav&#x0D;ascript:alert('217');">
perl -e 'print "<IMG SRC=java\0script:alert(\"218\")>";' > out
<IMG SRC=" &#14;  javascript:alert('219');">
<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>
<BODY onload!#$%&()*~+-_.,:;?@[/|\]^`=alert("220")>
<SCRIPT/SRC="http://ha.ckers.org/xss.js"></SCRIPT>
<<SCRIPT>alert("221");//<</SCRIPT>
<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >
<SCRIPT SRC=//ha.ckers.org/.j>
<IMG SRC="javascript:alert('222')"
<iframe src=http://ha.ckers.org/scriptlet.html <
\";alert('223');//
<u oncopy=alert()> Copy me</u>
<i onwheel=alert(224)> Scroll over me </i>
<plaintext>
http://a/%%30%30
</textarea><script>alert(225)</script>
# SQL Injection
#
# Strings which can cause a SQL injection if inputs are not sanitized
1;DROP TABLE users
1'; DROP TABLE users-- 1
' OR 1=1 -- 1
' OR '1'='1
'; EXEC sp_MSForEachTable 'DROP TABLE ?'; --
%
_
# Server Code Injection
#
# Strings which can cause user to run code on server as a privileged user (c.f. https://news.ycombinator.com/item?id=7665153)
-
--
--version
--help
$USER
/dev/null; touch /tmp/blns.fail ; echo
`touch /tmp/blns.fail`
$(touch /tmp/blns.fail)
@{[system "touch /tmp/blns.fail"]}
# Command Injection (Ruby)
#
# Strings which can call system commands within Ruby/Rails applications
eval("puts 'hello world'")
System("ls -al /")
`ls -al /`
Kernel.exec("ls -al /")
Kernel.exit(1)
%x('ls -al /')
# XXE Injection (XML)
#
# String which can reveal system files when parsed by a badly configured XML parser
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [ <!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo>
# Unwanted Interpolation
#
# Strings which can be accidentally expanded into different strings if evaluated in the wrong context, e.g. used as a printf format string or via Perl or shell eval. Might expose sensitive data from the program doing the interpolation, or might just represent the wrong string.
$HOME
$ENV{'HOME'}
%d
%s%s%s%s%s
{0}
%*.*s
%@
%n
File:///
# File Inclusion
#
# Strings which can cause user to pull in files that should not be a part of a web server
../../../../../../../../../../../etc/passwd%00
../../../../../../../../../../../etc/hosts
# Known CVEs and Vulnerabilities
#
# Strings that test for known vulnerabilities
() { 0; }; touch /tmp/blns.shellshock1.fail;
() { _; } >_[$($())] { touch /tmp/blns.shellshock2.fail; }
<<< %s(un='%s') = %u
+++ATH0
# MSDOS/Windows Special Filenames
#
# Strings which are reserved characters in MSDOS/Windows
CON
PRN
AUX
CLOCK$
NUL
A:
ZZ:
COM1
LPT1
LPT2
LPT3
COM2
COM3
COM4
# IRC specific strings
#
# Strings that may occur on IRC clients that make security products freak out
DCC SEND STARTKEYLOGGER 0 0 0
# Scunthorpe Problem
#
# Innocuous strings which may be blocked by profanity filters (https://en.wikipedia.org/wiki/Scunthorpe_problem)
Scunthorpe General Hospital
Penistone Community Church
Lightwater Country Park
Jimmy Clitheroe
Horniman Museum
shitake mushrooms
RomansInSussex.co.uk
http://www.cum.qc.ca/
Craig Cockburn, Software Specialist
Linda Callahan
Dr. Herman I. Libshitz
magna cum laude
Super Bowl XXX
medieval erection of parapets
evaluate
mocha
expression
Arsenal canal
classic
Tyson Gay
Dick Van Dyke
basement
# Human injection
#
# Strings which may cause human to reinterpret worldview
If you're reading this, you've been in a coma for almost 20 years now. We're trying a new technique. We don't know where this message will end up in your dream, but we hope it works. Please wake up, we miss you.
# Terminal escape codes
#
# Strings which punish the fools who use cat/type on this file
Roses are red, violets are blue. Hope you enjoy terminal hue
But now...for my greatest trick...
The quick brown fox... [Beeeep]
# iOS Vulnerabilities
#
# Strings which crashed iMessage in various versions of iOS
Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗
🏳0🌈️
జ్ఞ‌ా
# Persian special characters
#
# This is a four characters string which includes Persian special characters (گچپژ)
گچپژ
# jinja2 injection
#
# first one is supposed to raise "MemoryError" exception
# second, obviously, prints contents of /etc/passwd
{% print 'x' * 64 * 1024**3 %}
{{ "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read() }}

View File

@ -633,7 +633,7 @@ export default class Note extends BaseItem {
return n.updated_time < date;
}
static async save(o: NoteEntity, options: any = null) {
public static async save(o: NoteEntity, options: any = null): Promise<NoteEntity> {
const isNew = this.isNew(o, options);
// If true, this is a provisional note - it will be saved permanently

View File

@ -1,92 +0,0 @@
const { setupDatabaseAndSynchronizer, switchClient } = require('../testing/test-utils.js');
const Note = require('../models/Note').default;
const Revision = require('../models/Revision').default;
describe('models_Revision', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should create patches of text and apply it', (async () => {
const note1 = await Note.save({ body: 'my note\nsecond line' });
const patch = Revision.createTextPatch(note1.body, 'my new note\nsecond line');
const merged = Revision.applyTextPatch(note1.body, patch);
expect(merged).toBe('my new note\nsecond line');
}));
it('should create patches of objects and apply it', (async () => {
const oldObject = {
one: '123',
two: '456',
three: '789',
};
const newObject = {
one: '123',
three: '999',
};
const patch = Revision.createObjectPatch(oldObject, newObject);
const merged = Revision.applyObjectPatch(oldObject, patch);
expect(JSON.stringify(merged)).toBe(JSON.stringify(newObject));
}));
it('should move target revision to the top', (async () => {
const revs = [
{ id: '123' },
{ id: '456' },
{ id: '789' },
];
let newRevs;
newRevs = Revision.moveRevisionToTop({ id: '456' }, revs);
expect(newRevs[0].id).toBe('123');
expect(newRevs[1].id).toBe('789');
expect(newRevs[2].id).toBe('456');
newRevs = Revision.moveRevisionToTop({ id: '789' }, revs);
expect(newRevs[0].id).toBe('123');
expect(newRevs[1].id).toBe('456');
expect(newRevs[2].id).toBe('789');
}));
it('should create patch stats', (async () => {
const tests = [
{
patch: `@@ -625,16 +625,48 @@
rrupted download
+%0A- %5B %5D Fix mobile screen options`,
expected: [-0, +32],
},
{
patch: `@@ -564,17 +564,17 @@
ages%0A- %5B
-
+x
%5D Check `,
expected: [-1, +1],
},
{
patch: `@@ -1022,56 +1022,415 @@
.%0A%0A#
- How to view a note history%0A%0AWhile all the apps
+%C2%A0How does it work?%0A%0AAll the apps save a version of the modified notes every 10 minutes.
%0A%0A# `,
expected: [-(19 + 27 + 2), 17 + 67 + 4],
},
];
for (const test of tests) {
const stats = Revision.patchStats(test.patch);
expect(stats.removed).toBe(-test.expected[0]);
expect(stats.added).toBe(test.expected[1]);
}
}));
});

View File

@ -0,0 +1,194 @@
import { expectNotThrow, naughtyStrings, setupDatabaseAndSynchronizer, switchClient } from '../testing/test-utils';
import Note from '../models/Note';
import Revision from '../models/Revision';
describe('models/Revision', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should create patches of text and apply it', (async () => {
const note1 = await Note.save({ body: 'my note\nsecond line' });
const patch = Revision.createTextPatch(note1.body, 'my new note\nsecond line');
const merged = Revision.applyTextPatch(note1.body, patch);
expect(merged).toBe('my new note\nsecond line');
}));
it('should check if it is an empty revision', async () => {
const testCases = [
[false, {
title_diff: '',
body_diff: '',
metadata_diff: '{"new":{"id":"aaa"},"deleted":[]}',
}],
[true, {
title_diff: '',
body_diff: '',
metadata_diff: '',
}],
[true, {
title_diff: '[]',
body_diff: '',
metadata_diff: '{"new":{},"deleted":[]}',
}],
[true, {
title_diff: '',
body_diff: '[]',
metadata_diff: '{"new":{},"deleted":[]}',
}],
[false, {
title_diff: '[{"diffs":[[1,"hello"]],"start1":0,"start2":0,"length1":0,"length2":5}]',
body_diff: '[]',
metadata_diff: '{"new":{},"deleted":[]}',
}],
];
for (const t of testCases) {
const [expected, input] = t;
expect(Revision.isEmptyRevision(input as any)).toBe(expected);
}
});
it('should not fail to create revisions on naughty strings', (async () => {
// Previously this pattern would fail:
// - Create a patch between an empty string and smileys
// - Use that patch on the empty string to get back the smileys
// - Create a patch between those smileys and new smileys
// https://github.com/JackuB/diff-match-patch/issues/22
const nss = await naughtyStrings();
// First confirm that it indeed fails with the legacy approach.
let errorCount = 0;
for (let i = 0; i < nss.length - 1; i++) {
const ns1 = nss[i];
const ns2 = nss[i + 1];
try {
const patchText = Revision.createTextPatchLegacy('', ns1);
const patchedText = Revision.applyTextPatchLegacy('', patchText);
Revision.createTextPatchLegacy(patchedText, ns2);
} catch (error) {
errorCount++;
}
}
expect(errorCount).toBe(10);
// Now feed the naughty list again but using the new approach. In that
// case it should work fine.
await expectNotThrow(async () => {
for (let i = 0; i < nss.length - 1; i++) {
const ns1 = nss[i];
const ns2 = nss[i + 1];
const patchText = Revision.createTextPatch('', ns1);
const patchedText = Revision.applyTextPatch('', patchText);
Revision.createTextPatch(patchedText, ns2);
}
});
}));
it('should successfully handle legacy patches', async () => {
// The code should handle applying a series of new style patches and
// legacy patches, and the correct text should be recovered at the end.
const changes = [
'',
'one',
'one three',
'one two three',
];
const patches = [
Revision.createTextPatch(changes[0], changes[1]),
Revision.createTextPatchLegacy(changes[1], changes[2]),
Revision.createTextPatch(changes[2], changes[3]),
];
// Sanity check - verify that the patches are as expected
expect(patches[0].substr(0, 2)).toBe('[{'); // New
expect(patches[1].substr(0, 2)).toBe('@@'); // Legacy
expect(patches[2].substr(0, 2)).toBe('[{'); // New
let finalString = Revision.applyTextPatch(changes[0], patches[0]);
finalString = Revision.applyTextPatch(finalString, patches[1]);
finalString = Revision.applyTextPatch(finalString, patches[2]);
expect(finalString).toBe('one two three');
});
it('should create patches of objects and apply it', (async () => {
const oldObject = {
one: '123',
two: '456',
three: '789',
};
const newObject = {
one: '123',
three: '999',
};
const patch = Revision.createObjectPatch(oldObject, newObject);
const merged = Revision.applyObjectPatch(oldObject, patch);
expect(JSON.stringify(merged)).toBe(JSON.stringify(newObject));
}));
it('should move target revision to the top', (async () => {
const revs = [
{ id: '123' },
{ id: '456' },
{ id: '789' },
];
let newRevs;
newRevs = Revision.moveRevisionToTop({ id: '456' }, revs);
expect(newRevs[0].id).toBe('123');
expect(newRevs[1].id).toBe('789');
expect(newRevs[2].id).toBe('456');
newRevs = Revision.moveRevisionToTop({ id: '789' }, revs);
expect(newRevs[0].id).toBe('123');
expect(newRevs[1].id).toBe('456');
expect(newRevs[2].id).toBe('789');
}));
it('should create patch stats', (async () => {
const tests = [
{
patch: `@@ -625,16 +625,48 @@
rrupted download
+%0A- %5B %5D Fix mobile screen options`,
expected: [-0, +32],
},
{
patch: `@@ -564,17 +564,17 @@
ages%0A- %5B
-
+x
%5D Check `,
expected: [-1, +1],
},
{
patch: `@@ -1022,56 +1022,415 @@
.%0A%0A#
- How to view a note history%0A%0AWhile all the apps
+%C2%A0How does it work?%0A%0AAll the apps save a version of the modified notes every 10 minutes.
%0A%0A# `,
expected: [-(19 + 27 + 2), 17 + 67 + 4],
},
];
for (const test of tests) {
const stats = Revision.patchStats(test.patch);
expect(stats.removed).toBe(-test.expected[0]);
expect(stats.added).toBe(test.expected[1]);
}
}));
});

View File

@ -17,17 +17,54 @@ export default class Revision extends BaseItem {
return BaseModel.TYPE_REVISION;
}
static createTextPatch(oldText: string, newText: string) {
public static createTextPatchLegacy(oldText: string, newText: string): string {
return dmp.patch_toText(dmp.patch_make(oldText, newText));
}
static applyTextPatch(text: string, patch: string) {
public static createTextPatch(oldText: string, newText: string): string {
return JSON.stringify(dmp.patch_make(oldText, newText));
}
public static applyTextPatchLegacy(text: string, patch: string): string {
patch = dmp.patch_fromText(patch);
const result = dmp.patch_apply(patch, text);
if (!result || !result.length) throw new Error('Could not apply patch');
return result[0];
}
private static isLegacyPatch(patch: string): boolean {
return patch && patch.indexOf('@@') === 0;
}
private static isNewPatch(patch: string): boolean {
if (!patch) return true;
return patch.indexOf('[{') === 0;
}
public static applyTextPatch(text: string, patch: string): string {
if (this.isLegacyPatch(patch)) {
return this.applyTextPatchLegacy(text, patch);
} else {
const result = dmp.patch_apply(JSON.parse(patch), text);
if (!result || !result.length) throw new Error('Could not apply patch');
return result[0];
}
}
public static isEmptyRevision(rev: RevisionEntity): boolean {
if (this.isLegacyPatch(rev.title_diff) && rev.title_diff) return false;
if (this.isLegacyPatch(rev.body_diff) && rev.body_diff) return false;
if (this.isNewPatch(rev.title_diff) && rev.title_diff && rev.title_diff !== '[]') return false;
if (this.isNewPatch(rev.body_diff) && rev.body_diff && rev.body_diff !== '[]') return false;
const md = rev.metadata_diff ? JSON.parse(rev.metadata_diff) : {};
if (md.new && Object.keys(md.new).length) return false;
if (md.deleted && Object.keys(md.deleted).length) return false;
return true;
}
static createObjectPatch(oldObject: any, newObject: any) {
if (!oldObject) oldObject = {};

View File

@ -1,13 +1,11 @@
/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars, prefer-const */
const time = require('../time').default;
const { revisionService, setupDatabaseAndSynchronizer, switchClient } = require('../testing/test-utils.js');
const Setting = require('../models/Setting').default;
const Note = require('../models/Note').default;
const ItemChange = require('../models/ItemChange').default;
const Revision = require('../models/Revision').default;
const BaseModel = require('../BaseModel').default;
const RevisionService = require('../services/RevisionService').default;
import time from '../time';
import { revisionService, setupDatabaseAndSynchronizer, switchClient } from '../testing/test-utils';
import Setting from '../models/Setting';
import Note from '../models/Note';
import ItemChange from '../models/ItemChange';
import Revision from '../models/Revision';
import BaseModel from '../BaseModel';
import RevisionService from '../services/RevisionService';
describe('services_Revision', function() {
@ -25,7 +23,7 @@ describe('services_Revision', function() {
await service.collectRevisions();
await Note.save({ id: n1_v1.id, title: 'hello', author: 'testing' });
await service.collectRevisions();
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'hello welcome', author: '' });
await Note.save({ id: n1_v1.id, title: 'hello welcome', author: '' });
await service.collectRevisions();
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1_v1.id);
@ -49,6 +47,46 @@ describe('services_Revision', function() {
expect(revisions2.length).toBe(0);
}));
// ----------------------------------------------------------------------
// This is to verify that the revision service continues processing
// revisions even when it fails on one note. However, now that the
// diff-match-patch bug is fixed, it's not possible to create notes that
// would make the process fail. Keeping the test anyway in case such case
// comes up again.
// ----------------------------------------------------------------------
// it('should handle corrupted strings', (async () => {
// const service = new RevisionService();
// // Silence the logger because the revision service is going to print
// // errors.
// // Logger.globalLogger.enabled = false;
// const n1 = await Note.save({ body: '' });
// await service.collectRevisions();
// await Note.save({ id: n1.id, body: naughtyStrings[152] }); // REV 1
// await service.collectRevisions();
// await Note.save({ id: n1.id, body: naughtyStrings[153] }); // FAIL (Should have been REV 2)
// await service.collectRevisions();
// // Because it fails, only one revision was generated. The second was skipped.
// expect((await Revision.all()).length).toBe(1);
// // From this point, note 1 will always fail because of a
// // diff-match-patch bug:
// // https://github.com/JackuB/diff-match-patch/issues/22
// // It will throw "URI malformed". But it shouldn't prevent other notes
// // from getting revisions.
// const n2 = await Note.save({ body: '' });
// await service.collectRevisions();
// await Note.save({ id: n2.id, body: 'valid' }); // REV 2
// await service.collectRevisions();
// expect((await Revision.all()).length).toBe(2);
// Logger.globalLogger.enabled = true;
// }));
it('should delete old revisions (1 note, 2 rev)', (async () => {
const service = new RevisionService();
@ -59,7 +97,7 @@ describe('services_Revision', function() {
const time_v1 = Date.now();
await time.msleep(100);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await service.collectRevisions();
expect((await Revision.allByType(BaseModel.TYPE_NOTE, n1_v1.id)).length).toBe(2);
@ -81,12 +119,12 @@ describe('services_Revision', function() {
const time_v1 = Date.now();
await time.msleep(100);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'one two' });
await Note.save({ id: n1_v1.id, title: 'one two' });
await service.collectRevisions();
const time_v2 = Date.now();
await time.msleep(100);
const n1_v3 = await Note.save({ id: n1_v1.id, title: 'one two three' });
await Note.save({ id: n1_v1.id, title: 'one two three' });
await service.collectRevisions();
{
@ -124,8 +162,8 @@ describe('services_Revision', function() {
const time_n2_v1 = Date.now();
await time.msleep(100);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'note 1 (v2)' });
const n2_v2 = await Note.save({ id: n2_v1.id, title: 'note 2 (v2)' });
await Note.save({ id: n1_v1.id, title: 'note 1 (v2)' });
await Note.save({ id: n2_v1.id, title: 'note 2 (v2)' });
await service.collectRevisions();
expect((await Revision.all()).length).toBe(4);
@ -167,9 +205,9 @@ describe('services_Revision', function() {
const noteId = n1_v1.id;
const rev1 = await service.createNoteRevision_(n1_v1);
const n1_v2 = await Note.save({ id: noteId, title: 'hello Paul' });
const rev2 = await service.createNoteRevision_(n1_v2, rev1.id);
await service.createNoteRevision_(n1_v2, rev1.id);
const n1_v3 = await Note.save({ id: noteId, title: 'hello John' });
const rev3 = await service.createNoteRevision_(n1_v3, rev1.id);
await service.createNoteRevision_(n1_v3, rev1.id);
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, noteId);
expect(revisions.length).toBe(3);
@ -311,7 +349,7 @@ describe('services_Revision', function() {
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1
await time.sleep(0.1);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await revisionService().collectRevisions(); // REV 2
await time.sleep(0.1);
@ -340,7 +378,7 @@ describe('services_Revision', function() {
const timeRev1 = Date.now();
await time.msleep(100);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await revisionService().collectRevisions(); // REV 2
expect((await Revision.all()).length).toBe(2);
@ -364,7 +402,7 @@ describe('services_Revision', function() {
const timeRev1 = Date.now();
await time.msleep(100);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await revisionService().collectRevisions(); // REV 2
expect((await Revision.all()).length).toBe(2);
@ -385,11 +423,11 @@ describe('services_Revision', function() {
it('should not create a revision if the note has not changed', (async () => {
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1
expect((await Revision.all()).length).toBe(1);
const n1_v2 = await Note.save({ id: n1_v0.id, title: 'hello' });
await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // Note has not changed (except its timestamp) so don't create a revision
expect((await Revision.all()).length).toBe(1);
}));
@ -399,12 +437,12 @@ describe('services_Revision', function() {
// places so make sure it is saved correctly with the revision
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1
expect((await Revision.all()).length).toBe(1);
const userUpdatedTime = Date.now() - 1000 * 60 * 60;
const n1_v2 = await Note.save({ id: n1_v0.id, title: 'hello', updated_time: Date.now(), user_updated_time: userUpdatedTime }, { autoTimestamp: false });
await Note.save({ id: n1_v0.id, title: 'hello', updated_time: Date.now(), user_updated_time: userUpdatedTime }, { autoTimestamp: false });
await revisionService().collectRevisions(); // Only the user timestamp has changed, but that needs to be saved
const revisions = await Revision.all();
@ -416,20 +454,20 @@ describe('services_Revision', function() {
it('should not create a revision if there is already a recent one', (async () => {
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1
const timeRev1 = Date.now();
await time.sleep(2);
const timeRev2 = Date.now();
const n1_v2 = await Note.save({ id: n1_v0.id, title: 'hello 2' });
await Note.save({ id: n1_v0.id, title: 'hello 2' });
await revisionService().collectRevisions(); // REV 2
expect((await Revision.all()).length).toBe(2);
const interval = Date.now() - timeRev1 + 1;
Setting.setValue('revisionService.intervalBetweenRevisions', interval);
const n1_v3 = await Note.save({ id: n1_v0.id, title: 'hello 3' });
await Note.save({ id: n1_v0.id, title: 'hello 3' });
await revisionService().collectRevisions(); // No rev because time since last rev is less than the required 'interval between revisions'
expect(Date.now() - interval < timeRev2).toBe(true); // check the computer is not too slow for this test
expect((await Revision.all()).length).toBe(2);

View File

@ -9,10 +9,13 @@ import shim from '../shim';
import BaseService from './BaseService';
import { _ } from '../locale';
import { ItemChangeEntity, NoteEntity, RevisionEntity } from './database/types';
import Logger from '../Logger';
const { substrWithEllipsis } = require('../string-utils');
const { sprintf } = require('sprintf-js');
const { wrapError } = require('../errorUtils');
const logger = Logger.create('RevisionService');
export default class RevisionService extends BaseService {
public static instance_: RevisionService;
@ -60,18 +63,7 @@ export default class RevisionService extends BaseService {
return md;
}
isEmptyRevision_(rev: RevisionEntity) {
if (rev.title_diff) return false;
if (rev.body_diff) return false;
const md = JSON.parse(rev.metadata_diff);
if (md.new && Object.keys(md.new).length) return false;
if (md.deleted && Object.keys(md.deleted).length) return false;
return true;
}
async createNoteRevision_(note: NoteEntity, parentRevId: string = null) {
public async createNoteRevision_(note: NoteEntity, parentRevId: string = null): Promise<RevisionEntity> {
try {
const parentRev = parentRevId ? await Revision.load(parentRevId) : await Revision.latestRevision(BaseModel.TYPE_NOTE, note.id);
@ -100,7 +92,7 @@ export default class RevisionService extends BaseService {
output.metadata_diff = Revision.createObjectPatch(merged.metadata, noteMd);
}
if (this.isEmptyRevision_(output)) return null;
if (Revision.isEmptyRevision(output)) return null;
return Revision.save(output);
} catch (error) {
@ -109,7 +101,7 @@ export default class RevisionService extends BaseService {
}
}
async collectRevisions() {
public async collectRevisions() {
if (this.isCollecting_) return;
this.isCollecting_ = true;
@ -153,11 +145,11 @@ export default class RevisionService extends BaseService {
if (oldNote && oldNote.updated_time < this.oldNoteCutOffDate_()) {
// This is where we save the original version of this old note
const rev = await this.createNoteRevision_(oldNote);
if (rev) this.logger().debug(sprintf('RevisionService::collectRevisions: Saved revision %s (old note)', rev.id));
if (rev) logger.debug(sprintf('RevisionService::collectRevisions: Saved revision %s (old note)', rev.id));
}
const rev = await this.createNoteRevision_(note);
if (rev) this.logger().debug(sprintf('RevisionService::collectRevisions: Saved revision %s (Last rev was more than %d ms ago)', rev.id, Setting.value('revisionService.intervalBetweenRevisions')));
if (rev) logger.debug(sprintf('RevisionService::collectRevisions: Saved revision %s (Last rev was more than %d ms ago)', rev.id, Setting.value('revisionService.intervalBetweenRevisions')));
doneNoteIds.push(noteId);
this.isOldNotesCache_[noteId] = false;
}
@ -168,7 +160,7 @@ export default class RevisionService extends BaseService {
const revExists = await Revision.revisionExists(BaseModel.TYPE_NOTE, note.id, note.updated_time);
if (!revExists) {
const rev = await this.createNoteRevision_(note);
if (rev) this.logger().debug(sprintf('RevisionService::collectRevisions: Saved revision %s (for deleted note)', rev.id));
if (rev) logger.debug(sprintf('RevisionService::collectRevisions: Saved revision %s (for deleted note)', rev.id));
}
doneNoteIds.push(noteId);
}
@ -181,9 +173,15 @@ export default class RevisionService extends BaseService {
// One or more revisions are encrypted - stop processing for now
// and these revisions will be processed next time the revision
// collector runs.
this.logger().info('RevisionService::collectRevisions: One or more revision was encrypted. Processing was stopped but will resume later when the revision is decrypted.', error);
logger.info('RevisionService::collectRevisions: One or more revision was encrypted. Processing was stopped but will resume later when the revision is decrypted.', error);
} else {
this.logger().error('RevisionService::collectRevisions:', error);
// Note that, for now, if any revision creation fails, the whole
// process fails. This is on purpose because if we keep on
// processing, whatever caused the error will be in the past
// changes (before revisionService.lastProcessedChangeId) and
// will never be processed again. Now that the diff-match-patch
// issue is fixed, there should be no such error anyway.
logger.error('RevisionService::collectRevisions:', error);
}
}
@ -192,7 +190,7 @@ export default class RevisionService extends BaseService {
this.isCollecting_ = false;
this.logger().info(`RevisionService::collectRevisions: Created revisions for ${doneNoteIds.length} notes`);
logger.info(`RevisionService::collectRevisions: Created revisions for ${doneNoteIds.length} notes`);
}
async deleteOldRevisions(ttl: number) {
@ -266,23 +264,23 @@ export default class RevisionService extends BaseService {
this.maintenanceCalls_.push(true);
try {
const startTime = Date.now();
this.logger().info('RevisionService::maintenance: Starting...');
logger.info('RevisionService::maintenance: Starting...');
if (!Setting.value('revisionService.enabled')) {
this.logger().info('RevisionService::maintenance: Service is disabled');
logger.info('RevisionService::maintenance: Service is disabled');
// We do as if we had processed all the latest changes so that they can be cleaned up
// later on by ItemChangeUtils.deleteProcessedChanges().
Setting.setValue('revisionService.lastProcessedChangeId', await ItemChange.lastChangeId());
await this.deleteOldRevisions(Setting.value('revisionService.ttlDays') * 24 * 60 * 60 * 1000);
} else {
this.logger().info('RevisionService::maintenance: Service is enabled');
logger.info('RevisionService::maintenance: Service is enabled');
await this.collectRevisions();
await this.deleteOldRevisions(Setting.value('revisionService.ttlDays') * 24 * 60 * 60 * 1000);
this.logger().info(`RevisionService::maintenance: Done in ${Date.now() - startTime}ms`);
logger.info(`RevisionService::maintenance: Done in ${Date.now() - startTime}ms`);
}
} catch (error) {
this.logger().error('RevisionService::maintenance:', error);
logger.error('RevisionService::maintenance:', error);
} finally {
this.maintenanceCalls_.pop();
}
@ -294,7 +292,7 @@ export default class RevisionService extends BaseService {
if (collectRevisionInterval === null) collectRevisionInterval = 1000 * 60 * 10;
this.logger().info(`RevisionService::runInBackground: Starting background service with revision collection interval ${collectRevisionInterval}`);
logger.info(`RevisionService::runInBackground: Starting background service with revision collection interval ${collectRevisionInterval}`);
this.maintenanceTimer1_ = shim.setTimeout(() => {
void this.maintenance();

View File

@ -17,7 +17,7 @@ import FileApiDriverJoplinServer from '../file-api-driver-joplinServer';
import OneDriveApi from '../onedrive-api';
import SyncTargetOneDrive from '../SyncTargetOneDrive';
import JoplinDatabase from '../JoplinDatabase';
const fs = require('fs-extra');
import * as fs from 'fs-extra';
const { DatabaseDriverNode } = require('../database-driver-node.js');
import Folder from '../models/Folder';
import Note from '../models/Note';
@ -101,8 +101,8 @@ const supportDir = `${oldTestDir}/support`;
const dataDir = `${oldTestDir}/test data/${suiteName_}`;
const profileDir = `${dataDir}/profile`;
fs.mkdirpSync(logDir, 0o755);
fs.mkdirpSync(baseTempDir, 0o755);
fs.mkdirpSync(logDir);
fs.mkdirpSync(baseTempDir);
fs.mkdirpSync(dataDir);
fs.mkdirpSync(profileDir);
@ -392,10 +392,10 @@ async function setupDatabaseAndSynchronizer(id: number, options: any = null) {
DecryptionWorker.instance_ = null;
await fs.remove(resourceDir(id));
await fs.mkdirp(resourceDir(id), 0o755);
await fs.mkdirp(resourceDir(id));
await fs.remove(pluginDir(id));
await fs.mkdirp(pluginDir(id), 0o755);
await fs.mkdirp(pluginDir(id));
if (!synchronizers_[id]) {
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
@ -512,7 +512,7 @@ async function initFileApi() {
let fileApi = null;
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
fs.removeSync(syncDir);
fs.mkdirpSync(syncDir, 0o755);
fs.mkdirpSync(syncDir);
fileApi = new FileApi(syncDir, new FileApiDriverLocal());
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('memory')) {
fileApi = new FileApi('/root', new FileApiDriverMemory());
@ -788,6 +788,21 @@ async function waitForFolderCount(count: number) {
}
}
let naughtyStrings_: string[] = null;
export async function naughtyStrings() {
if (naughtyStrings_) return naughtyStrings_;
const t = await fs.readFile(`${supportDir}/big-list-of-naughty-strings.txt`, 'utf8');
const lines = t.split('\n');
naughtyStrings_ = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
if (trimmed.indexOf('#') === 0) continue;
naughtyStrings_.push(line);
}
return naughtyStrings_;
}
// TODO: Update for Jest
// function mockDate(year, month, day, tick) {