Compare commits

..
1 Commits
Author SHA1 Message Date
Kristian WindandClaude Opus 5 2bd8e48aba fix(notes): en note med tre prikker i titlen blev smidt væk ved importen
Release / Build and publish image (push) Failing after 9m54s
`skipFile` afviste ethvert navn, der indeholdt "..", for at stoppe en zip i at
skrive uden for sin egen mappe. Det er den rigtige idé rettet mod det forkerte:
det beskriver ikke en sti, der klatrer ud af arkivet, men ethvert filnavn med to
prikker ved siden af hinanden. En ellipse i en overskrift er almindelig, og noten
"Så blev det endelig jul! Stay tuned... ☕️" blev derfor aldrig oprettet.

Lydløst, oven i købet: filen blev sprunget over, før den blev talt, så svaret
sagde det samme, som hvis den var kommet med. Én note ud af tolv hundrede, og den
blev kun fundet ved at tælle efter, hvor mange der kom ud i den anden ende — 1140
hvor der skulle stå 1141.

Nu læses ".." som et stiled, ikke som to tegn, og en Markdown-fil, filteret
afviser, tælles som sprunget over. En import, der taber noget, skal sige det.

Og move-ikonet i sidebjælken er væk. ⠿ sad i den to pixels brede rende til
venstre for teksten og lå oven i rækkens egen kant — idéen var rigtig, pladsen
var det ikke. Elementet bliver: det er dét, der ejer trækket, og gjorde man i
stedet hele rækken draggable, ville alt indeni også blive det, og en opgave
trukket hen på I dag holdt op med at lande. Nu er håndtaget usynligt og lidt
bredere, og hånden ved hover er hele forklaringen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 00:04:08 +02:00
3 changed files with 91 additions and 16 deletions
+37 -4
View File
@@ -353,7 +353,16 @@ func (s *Server) handleImportNotes(w http.ResponseWriter, r *http.Request) {
for _, f := range zr.File {
name := f.Name
if f.FileInfo().IsDir() || skipFile(name) || !strings.EqualFold(path.Ext(name), ".md") {
if f.FileInfo().IsDir() || !strings.EqualFold(path.Ext(name), ".md") {
continue
}
// Counted, not passed over in silence. A Markdown file the filter refuses is
// a note that did not arrive, and the caller is told the same number either
// way unless it is said here — which is how one note went missing without
// anything looking wrong.
if skipFile(name) {
s.log.Warn("import skipped a note file", "file", name)
skipped++
continue
}
@@ -409,10 +418,34 @@ func (s *Server) handleImportNotes(w http.ResponseWriter, r *http.Request) {
// A zip made on a Mac carries a __MACOSX copy of every file, and importing those
// would double a whole library with each copy full of binary rubbish.
func skipFile(name string) bool {
return strings.HasPrefix(name, "__MACOSX/") ||
if strings.HasPrefix(name, "__MACOSX/") ||
strings.HasPrefix(path.Base(name), "._") ||
strings.HasPrefix(path.Base(name), ".") ||
strings.Contains(name, "..")
strings.HasPrefix(path.Base(name), ".") {
return true
}
// Traversal, read as path segments rather than as a substring.
//
// This was `strings.Contains(name, "..")`, which is the right idea aimed at the
// wrong thing: it does not describe a path that climbs out of the archive, it
// describes any name with two dots next to each other anywhere in it. A note
// called "Så blev det endelig jul! Stay tuned... ☕️" was therefore dropped —
// silently, and without even counting as skipped, so the number the import
// reported still looked right. One note in twelve hundred, found only by
// counting what came out.
//
// An ellipsis in a title is ordinary. A segment that *is* "..", or a path that
// starts at the root, is not — and those are what a zip uses to write outside
// the folder it was unpacked into.
if strings.HasPrefix(name, "/") {
return true
}
for _, part := range strings.Split(name, "/") {
if part == ".." {
return true
}
}
return false
}
// attachToNote records the files a note's text points at and rewrites the text to
@@ -0,0 +1,38 @@
package httpapi
import "testing"
// En titel med tre prikker i er ikke et forsøg på at skrive uden for mappen.
//
// `strings.Contains(name, "..")` var den rigtige idé rettet mod det forkerte:
// den beskriver ikke en sti, der klatrer ud af arkivet, men ethvert navn med to
// prikker ved siden af hinanden. Noten "Så blev det endelig jul! Stay tuned... ☕️"
// blev derfor droppet lydløst — én ud af tolv hundrede, og kun fundet ved at
// tælle efter, hvor mange der kom ud.
func TestSkipFileTakesEllipsesButRefusesTraversal(t *testing.T) {
keep := []string{
"Så blev det endelig jul! Stay tuned... ☕️.md",
"Møde 12.8..md",
"vedhaeftninger/1-2-Pasted Graphic.png",
"noter/Referat....md",
}
for _, name := range keep {
if skipFile(name) {
t.Errorf("skipFile(%q) = true, men det er et almindeligt filnavn", name)
}
}
drop := []string{
"../uden for.md",
"noter/../../etc/passwd",
"/etc/passwd",
"__MACOSX/._noget",
".skjult.md",
"noter/._AppleDouble.md",
}
for _, name := range drop {
if !skipFile(name) {
t.Errorf("skipFile(%q) = false, men den skal afvises", name)
}
}
}
+16 -12
View File
@@ -508,14 +508,22 @@
sidebar is already a drop target for tasks — a task dragged onto I dag
stopped landing, because the row had claimed the gesture. A grip claims
nothing until it is grabbed. -->
<!-- Håndtaget har ingen prik i sig længere.
⠿ sad i den to pixels brede rende til venstre for teksten og lå oven i
rækkens egen kant — idéen var rigtig, pladsen var det ikke. Men elementet
bliver: det er dét, der ejer trækket, og gjorde man i stedet hele rækken
draggable, ville alt indeni også blive det — og en opgave trukket hen på
I dag holdt op med at lande, fordi rækken havde taget gesten. Nu er
håndtaget usynligt og lidt bredere, og hånden, der kommer ved hover,
er hele forklaringen. -->
{#snippet grip(key)}
<span
class="grip"
draggable="true"
ondragstart={(e) => onNavDragStart(e, key)}
ondragend={clearNavDrag}
aria-hidden="true">⠿</span
>
aria-hidden="true"
></span>
{/snippet}
{#snippet projectRow(project, sortable)}
@@ -1004,19 +1012,15 @@
so nothing moves when it appears. */
.grip {
position: absolute;
left: -2px;
width: 12px;
text-align: center;
font-size: 10px;
line-height: 1;
color: var(--ink-faint);
left: 0;
top: 0;
bottom: 0;
width: 14px;
cursor: grab;
opacity: 0;
transition: opacity var(--fast) var(--ease);
}
.navrow:hover .grip {
opacity: 1;
.grip:active {
cursor: grabbing;
}
.navrow > :global(a) {