apps/posts/handlers_admin.go 17.9 K raw
1
package main
2
3
import (
4
	"archive/zip"
5
	"bytes"
6
	"fmt"
7
	"io"
8
	"net/http"
9
	"net/url"
10
	"strings"
11
12
	"github.com/stevedylandev/andromeda/pkg/auth"
13
)
14
15
const importMaxBytes = 50 * 1024 * 1024
16
const uploadMaxBytes = 10 * 1024 * 1024
17
const bodyLimit = 51 * 1024 * 1024
18
19
func (a *App) loginGet(w http.ResponseWriter, r *http.Request) {
20
	a.renderPage(w, "login.html", loginPageData{Error: r.URL.Query().Get("error")})
21
}
22
23
func (a *App) loginPost(w http.ResponseWriter, r *http.Request) {
24
	if err := r.ParseForm(); err != nil {
25
		http.Redirect(w, r, "/admin/login?error=Bad+request", http.StatusSeeOther)
26
		return
27
	}
28
	if !auth.VerifyPassword(r.FormValue("password"), a.AppPassword) {
29
		http.Redirect(w, r, "/admin/login?error=Invalid+password", http.StatusSeeOther)
30
		return
31
	}
32
	token, err := a.Sessions.Create()
33
	if err != nil {
34
		a.Log.Error("create session", "err", err)
35
		http.Redirect(w, r, "/admin/login?error=Server+error", http.StatusSeeOther)
36
		return
37
	}
38
	a.Sessions.PruneExpired()
39
	http.SetCookie(w, a.Sessions.SessionCookie(token))
40
	http.Redirect(w, r, "/admin", http.StatusSeeOther)
41
}
42
43
func (a *App) logout(w http.ResponseWriter, r *http.Request) {
44
	if c, err := r.Cookie(a.Sessions.CookieName); err == nil && c.Value != "" {
45
		a.Sessions.Delete(c.Value)
46
	}
47
	http.SetCookie(w, a.Sessions.ClearCookie())
48
	http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
49
}
50
51
func (a *App) adminIndex(w http.ResponseWriter, r *http.Request) {
52
	posts, err := getAllPosts(a.DB)
53
	if err != nil {
54
		http.Error(w, "Server error", http.StatusInternalServerError)
55
		return
56
	}
57
	a.renderPage(w, "admin_index.html", adminIndexPageData{Posts: posts})
58
}
59
60
func (a *App) adminNewPost(w http.ResponseWriter, r *http.Request) {
61
	a.renderPage(w, "admin_post_form.html", adminPostFormPageData{Error: r.URL.Query().Get("error")})
62
}
63
64
func (a *App) adminCreatePost(w http.ResponseWriter, r *http.Request) {
65
	if err := r.ParseForm(); err != nil {
66
		http.Redirect(w, r, "/admin/posts/new?error=Bad+request", http.StatusSeeOther)
67
		return
68
	}
69
	attrs := parseAttributes(r.FormValue("attributes"))
70
	title := strings.TrimSpace(attrs.Title)
71
	slug := deriveSlugWith(a, title, strings.TrimSpace(attrs.Slug))
72
	status := "draft"
73
	if r.FormValue("action") == "publish" {
74
		status = "published"
75
	}
76
	lang := "en"
77
	if l := strings.TrimSpace(attrs.Lang); l != "" {
78
		lang = l
79
	}
80
	defaultLocation, err := getSetting(a.DB, "default_location")
81
	if err != nil {
82
		defaultLocation = ""
83
	}
84
	weather := getWeather(defaultLocation)
85
	if newWeather := strings.TrimSpace(attrs.Weather); newWeather != "" {
86
		weather = newWeather
87
	}
88
	pub := strings.TrimSpace(attrs.PublishedDate)
89
	if pub == "" {
90
		pub = nowDatetime()
91
	}
92
	in := PostInput{
93
		Title: optStr(title), Slug: slug, Content: r.FormValue("content"),
94
		Status: status, Alias: optStr(attrs.Alias),
95
		PublishedDate:   &pub,
96
		MetaDescription: optStr(attrs.MetaDescription),
97
		MetaImage:       optStr(attrs.MetaImage),
98
		Lang:            lang, Tags: optStr(attrs.Tags),
99
		Weather: 				 optStr(weather),
100
	}
101
	if _, err := createPost(a.DB, in); err != nil {
102
		a.Log.Error("create post", "err", err)
103
		http.Redirect(w, r, "/admin/posts/new?error=Failed+to+create+post", http.StatusSeeOther)
104
		return
105
	}
106
	http.Redirect(w, r, "/admin", http.StatusSeeOther)
107
}
108
109
func deriveSlugWith(a *App, title, slug string) string {
110
	if slug != "" {
111
		return slug
112
	}
113
	if s := slugify(title); s != "" {
114
		return s
115
	}
116
	id, _ := auth.GenerateShortID(10)
117
	return id
118
}
119
120
func (a *App) adminEditPost(w http.ResponseWriter, r *http.Request) {
121
	shortID := r.PathValue("id")
122
	post, err := getPostByShortID(a.DB, shortID)
123
	if err != nil {
124
		http.Error(w, "Server error", http.StatusInternalServerError)
125
		return
126
	}
127
	if post == nil {
128
		http.Error(w, "Post not found", http.StatusNotFound)
129
		return
130
	}
131
	a.renderPage(w, "admin_post_form.html", adminPostFormPageData{Post: post, Error: r.URL.Query().Get("error")})
132
}
133
134
func (a *App) adminUpdatePost(w http.ResponseWriter, r *http.Request) {
135
	shortID := r.PathValue("id")
136
	if err := r.ParseForm(); err != nil {
137
		http.Redirect(w, r, "/admin/posts/"+shortID+"/edit?error=Bad+request", http.StatusSeeOther)
138
		return
139
	}
140
	attrs := parseAttributes(r.FormValue("attributes"))
141
	title := strings.TrimSpace(attrs.Title)
142
	slug := deriveSlugWith(a, title, strings.TrimSpace(attrs.Slug))
143
	status := "draft"
144
	if r.FormValue("action") == "publish" {
145
		status = "published"
146
	}
147
	lang := "en"
148
	if l := strings.TrimSpace(attrs.Lang); l != "" {
149
		lang = l
150
	}
151
	var pubDate *string
152
	if t := strings.TrimSpace(attrs.PublishedDate); t != "" {
153
		pubDate = &t
154
	}
155
		defaultLocation, err := getSetting(a.DB, "default_location")
156
	if err != nil {
157
		defaultLocation = ""
158
	}
159
	weather := getWeather(defaultLocation)
160
	if newWeather := strings.TrimSpace(attrs.Weather); newWeather != "" {
161
		weather = newWeather
162
	}
163
	in := PostInput{
164
		Title: optStr(title), Slug: slug, Content: r.FormValue("content"),
165
		Status: status, Alias: optStr(attrs.Alias),
166
		PublishedDate:   pubDate,
167
		MetaDescription: optStr(attrs.MetaDescription),
168
		MetaImage:       optStr(attrs.MetaImage),
169
		Lang:            lang, Tags: optStr(attrs.Tags),
170
		Weather:				 optStr(weather),
171
	}
172
	if _, err := updatePost(a.DB, shortID, in); err != nil {
173
		a.Log.Error("update post", "err", err)
174
		http.Redirect(w, r, "/admin/posts/"+shortID+"/edit?error=Failed+to+update", http.StatusSeeOther)
175
		return
176
	}
177
	http.Redirect(w, r, "/admin", http.StatusSeeOther)
178
}
179
180
func (a *App) adminDeletePost(w http.ResponseWriter, r *http.Request) {
181
	_, _ = deletePost(a.DB, r.PathValue("id"))
182
	http.Redirect(w, r, "/admin", http.StatusSeeOther)
183
}
184
185
func (a *App) adminTogglePublish(w http.ResponseWriter, r *http.Request) {
186
	_, _ = togglePostStatus(a.DB, r.PathValue("id"))
187
	http.Redirect(w, r, "/admin", http.StatusSeeOther)
188
}
189
190
func (a *App) adminPages(w http.ResponseWriter, r *http.Request) {
191
	pages, err := getAllPages(a.DB)
192
	if err != nil {
193
		http.Error(w, "Server error", http.StatusInternalServerError)
194
		return
195
	}
196
	a.renderPage(w, "admin_pages.html", adminPagesPageData{Pages: pages})
197
}
198
199
func (a *App) adminNewPage(w http.ResponseWriter, r *http.Request) {
200
	a.renderPage(w, "admin_page_form.html", adminPageFormPageData{Error: r.URL.Query().Get("error")})
201
}
202
203
func (a *App) adminCreatePage(w http.ResponseWriter, r *http.Request) {
204
	if err := r.ParseForm(); err != nil {
205
		http.Redirect(w, r, "/admin/pages/new?error=Bad+request", http.StatusSeeOther)
206
		return
207
	}
208
	attrs := parsePageAttributes(r.FormValue("attributes"))
209
	title := strings.TrimSpace(attrs.Title)
210
	slug := strings.TrimSpace(attrs.Slug)
211
	if title == "" || slug == "" {
212
		http.Redirect(w, r, "/admin/pages/new?error=Title+and+slug+are+required", http.StatusSeeOther)
213
		return
214
	}
215
	if isReservedPageSlug(slug) {
216
		http.Redirect(w, r, "/admin/pages/new?error=That+slug+is+reserved", http.StatusSeeOther)
217
		return
218
	}
219
	if _, err := createPage(a.DB, title, slug, r.FormValue("content"), attrs.IsPublished, 0); err != nil {
220
		a.Log.Error("create page", "err", err)
221
		http.Redirect(w, r, "/admin/pages/new?error=Failed+to+create+page", http.StatusSeeOther)
222
		return
223
	}
224
	http.Redirect(w, r, "/admin/pages", http.StatusSeeOther)
225
}
226
227
func (a *App) adminEditPage(w http.ResponseWriter, r *http.Request) {
228
	page, err := getPageByShortID(a.DB, r.PathValue("id"))
229
	if err != nil {
230
		http.Error(w, "Server error", http.StatusInternalServerError)
231
		return
232
	}
233
	if page == nil {
234
		http.Error(w, "Page not found", http.StatusNotFound)
235
		return
236
	}
237
	a.renderPage(w, "admin_page_form.html", adminPageFormPageData{Page: page, Error: r.URL.Query().Get("error")})
238
}
239
240
func (a *App) adminUpdatePage(w http.ResponseWriter, r *http.Request) {
241
	shortID := r.PathValue("id")
242
	if err := r.ParseForm(); err != nil {
243
		http.Redirect(w, r, "/admin/pages/"+shortID+"/edit?error=Bad+request", http.StatusSeeOther)
244
		return
245
	}
246
	attrs := parsePageAttributes(r.FormValue("attributes"))
247
	title := strings.TrimSpace(attrs.Title)
248
	slug := strings.TrimSpace(attrs.Slug)
249
	if title == "" || slug == "" {
250
		http.Redirect(w, r, "/admin/pages/"+shortID+"/edit?error=Title+and+slug+are+required", http.StatusSeeOther)
251
		return
252
	}
253
	if isReservedPageSlug(slug) {
254
		http.Redirect(w, r, "/admin/pages/"+shortID+"/edit?error=That+slug+is+reserved", http.StatusSeeOther)
255
		return
256
	}
257
	if _, err := updatePage(a.DB, shortID, title, slug, r.FormValue("content"), attrs.IsPublished, 0); err != nil {
258
		a.Log.Error("update page", "err", err)
259
		http.Redirect(w, r, "/admin/pages/"+shortID+"/edit?error=Failed+to+update", http.StatusSeeOther)
260
		return
261
	}
262
	http.Redirect(w, r, "/admin/pages", http.StatusSeeOther)
263
}
264
265
func (a *App) adminDeletePage(w http.ResponseWriter, r *http.Request) {
266
	_ = deletePage(a.DB, r.PathValue("id"))
267
	http.Redirect(w, r, "/admin/pages", http.StatusSeeOther)
268
}
269
270
func (a *App) adminGetSettings(w http.ResponseWriter, r *http.Request) {
271
	get := func(k string) string { v, _ := getSetting(a.DB, k); return v }
272
	defaultCSS, _ := appFS.ReadFile("static/styles.css")
273
	a.renderPage(w, "admin_settings.html", adminSettingsPageData{
274
		BlogTitle:       get("blog_title"),
275
		BlogDescription: get("blog_description"),
276
		IntroContent:    get("intro_content"),
277
		NavLinksRaw:     get("nav_links"),
278
		CustomCSS:       get("custom_css"),
279
		DefaultCSS:      string(defaultCSS),
280
		FaviconURL:      get("favicon_url"),
281
		OGImageURL:      get("og_image_url"),
282
		CustomHeader:    get("custom_header"),
283
		CustomFooter:    get("custom_footer"),
284
		DefaultLocation: get("default_location"),
285
		Success:         r.URL.Query().Get("success") == "true",
286
	})
287
}
288
289
func (a *App) adminPostSettings(w http.ResponseWriter, r *http.Request) {
290
	if err := r.ParseForm(); err != nil {
291
		http.Redirect(w, r, "/admin/settings", http.StatusSeeOther)
292
		return
293
	}
294
	_ = setSetting(a.DB, "blog_title", strings.TrimSpace(r.FormValue("blog_title")))
295
	_ = setSetting(a.DB, "blog_description", strings.TrimSpace(r.FormValue("blog_description")))
296
	_ = setSetting(a.DB, "intro_content", r.FormValue("intro_content"))
297
	_ = setSetting(a.DB, "nav_links", r.FormValue("nav_links"))
298
	_ = setSetting(a.DB, "custom_css", r.FormValue("custom_css"))
299
	_ = setSetting(a.DB, "favicon_url", strings.TrimSpace(r.FormValue("favicon_url")))
300
	_ = setSetting(a.DB, "og_image_url", strings.TrimSpace(r.FormValue("og_image_url")))
301
	_ = setSetting(a.DB, "custom_header", r.FormValue("custom_header"))
302
	_ = setSetting(a.DB, "custom_footer", r.FormValue("custom_footer"))
303
	_ = setSetting(a.DB, "default_location", r.FormValue("default_location"))
304
	http.Redirect(w, r, "/admin/settings?success=true", http.StatusSeeOther)
305
}
306
307
func (a *App) adminFiles(w http.ResponseWriter, r *http.Request) {
308
	files, err := getAllFiles(a.DB)
309
	if err != nil {
310
		http.Error(w, "Server error", http.StatusInternalServerError)
311
		return
312
	}
313
	a.renderPage(w, "admin_files.html", adminFilesPageData{
314
		Files: files, SiteURL: a.SiteURL,
315
		Error:   r.URL.Query().Get("error"),
316
		Success: r.URL.Query().Get("success") == "true",
317
	})
318
}
319
320
func (a *App) adminUploadFile(w http.ResponseWriter, r *http.Request) {
321
	r.Body = http.MaxBytesReader(w, r.Body, bodyLimit)
322
	if err := r.ParseMultipartForm(uploadMaxBytes); err != nil {
323
		http.Redirect(w, r, "/admin/files?error=Failed+to+read+upload", http.StatusSeeOther)
324
		return
325
	}
326
	file, header, err := r.FormFile("file")
327
	if err != nil {
328
		http.Redirect(w, r, "/admin/files?error=No+file+provided", http.StatusSeeOther)
329
		return
330
	}
331
	defer file.Close()
332
	data, err := io.ReadAll(file)
333
	if err != nil {
334
		http.Redirect(w, r, "/admin/files?error=Failed+to+read+upload", http.StatusSeeOther)
335
		return
336
	}
337
	if int64(len(data)) > uploadMaxBytes {
338
		http.Redirect(w, r, "/admin/files?error=File+exceeds+10MB+limit", http.StatusSeeOther)
339
		return
340
	}
341
	originalName := "upload"
342
	contentType := "application/octet-stream"
343
	if header != nil {
344
		originalName = header.Filename
345
		if ct := header.Header.Get("Content-Type"); ct != "" {
346
			contentType = ct
347
		}
348
	}
349
	ext := ""
350
	if i := strings.LastIndex(originalName, "."); i > 0 && i < len(originalName)-1 {
351
		ext = originalName[i+1:]
352
	}
353
	id, _ := auth.GenerateShortID(10)
354
	stored := id
355
	if ext != "" {
356
		stored = id + "." + ext
357
	}
358
	backend := a.Storage
359
	if backend == nil {
360
		http.Redirect(w, r, "/admin/files?error=Storage+not+configured", http.StatusSeeOther)
361
		return
362
	}
363
	if err := backend.Put(r.Context(), stored, contentType, data); err != nil {
364
		a.Log.Error("save file", "backend", backend.Name(), "err", err)
365
		http.Redirect(w, r, "/admin/files?error=Failed+to+save+file", http.StatusSeeOther)
366
		return
367
	}
368
	if _, err := createFile(a.DB, stored, originalName, contentType, int64(len(data)), backend.Name()); err != nil {
369
		a.Log.Error("record file", "err", err)
370
		_ = backend.Delete(r.Context(), stored)
371
		http.Redirect(w, r, "/admin/files?error=Failed+to+record+file", http.StatusSeeOther)
372
		return
373
	}
374
	http.Redirect(w, r, "/admin/files?success=true", http.StatusSeeOther)
375
}
376
377
func (a *App) adminDeleteFile(w http.ResponseWriter, r *http.Request) {
378
	file, err := deleteFile(a.DB, r.PathValue("id"))
379
	if err != nil || file == nil {
380
		http.Redirect(w, r, "/admin/files", http.StatusSeeOther)
381
		return
382
	}
383
	if file.StorageBackend == "r2" && a.Storage != nil && a.Storage.Name() == "r2" {
384
		_ = a.Storage.Delete(r.Context(), file.Filename)
385
	} else {
386
		_ = removeFile(joinPath(a.UploadsDir, file.Filename))
387
	}
388
	http.Redirect(w, r, "/admin/files", http.StatusSeeOther)
389
}
390
391
func (a *App) adminDownloadPosts(w http.ResponseWriter, r *http.Request) {
392
	posts, err := getAllPosts(a.DB)
393
	if err != nil {
394
		http.Error(w, "Server error", http.StatusInternalServerError)
395
		return
396
	}
397
	var buf bytes.Buffer
398
	zw := zip.NewWriter(&buf)
399
	for i := range posts {
400
		f, err := zw.Create(posts[i].Slug + ".md")
401
		if err != nil {
402
			continue
403
		}
404
		_, _ = f.Write([]byte(postToMarkdown(&posts[i])))
405
	}
406
	_ = zw.Close()
407
	w.Header().Set("Content-Type", "application/zip")
408
	w.Header().Set("Content-Disposition", `attachment; filename="posts.zip"`)
409
	_, _ = w.Write(buf.Bytes())
410
}
411
412
func (a *App) adminDownloadUploads(w http.ResponseWriter, r *http.Request) {
413
	files, err := getAllFiles(a.DB)
414
	if err != nil {
415
		http.Error(w, "Server error", http.StatusInternalServerError)
416
		return
417
	}
418
	var buf bytes.Buffer
419
	zw := zip.NewWriter(&buf)
420
	header := &zip.FileHeader{Method: zip.Store}
421
	_ = header
422
	seen := map[string]bool{}
423
	for _, file := range files {
424
		data, err := readFileImpl(joinPath(a.UploadsDir, file.Filename))
425
		if err != nil {
426
			continue
427
		}
428
		name := file.OriginalName
429
		if seen[name] {
430
			name = file.ShortID + "_" + file.OriginalName
431
		}
432
		seen[file.OriginalName] = true
433
		w2, err := zw.CreateHeader(&zip.FileHeader{Name: name, Method: zip.Store})
434
		if err != nil {
435
			continue
436
		}
437
		_, _ = w2.Write(data)
438
	}
439
	_ = zw.Close()
440
	w.Header().Set("Content-Type", "application/zip")
441
	w.Header().Set("Content-Disposition", `attachment; filename="uploads.zip"`)
442
	_, _ = w.Write(buf.Bytes())
443
}
444
445
func (a *App) adminImportForm(w http.ResponseWriter, r *http.Request) {
446
	data := adminImportPageData{Error: r.URL.Query().Get("error")}
447
	if v := r.URL.Query().Get("imported"); v != "" {
448
		var n int
449
		_, _ = fmt.Sscanf(v, "%d", &n)
450
		data.Imported = &n
451
	}
452
	if v := r.URL.Query().Get("skipped"); v != "" {
453
		var n int
454
		_, _ = fmt.Sscanf(v, "%d", &n)
455
		data.Skipped = &n
456
	}
457
	a.renderPage(w, "admin_import.html", data)
458
}
459
460
func (a *App) adminImportPosts(w http.ResponseWriter, r *http.Request) {
461
	r.Body = http.MaxBytesReader(w, r.Body, bodyLimit)
462
	if err := r.ParseMultipartForm(importMaxBytes); err != nil {
463
		http.Redirect(w, r, "/admin/import?error=Failed+to+read+upload", http.StatusSeeOther)
464
		return
465
	}
466
	file, _, err := r.FormFile("zip")
467
	if err != nil {
468
		http.Redirect(w, r, "/admin/import?error=No+zip+provided", http.StatusSeeOther)
469
		return
470
	}
471
	defer file.Close()
472
	data, err := io.ReadAll(file)
473
	if err != nil {
474
		http.Redirect(w, r, "/admin/import?error=Failed+to+read+upload", http.StatusSeeOther)
475
		return
476
	}
477
	if int64(len(data)) > importMaxBytes {
478
		http.Redirect(w, r, "/admin/import?error=Zip+exceeds+50MB+limit", http.StatusSeeOther)
479
		return
480
	}
481
	imported, skipped, err := a.processImportZip(data)
482
	if err != nil {
483
		a.Log.Error("import zip", "err", err)
484
		http.Redirect(w, r, "/admin/import?error=Invalid+zip+archive", http.StatusSeeOther)
485
		return
486
	}
487
	http.Redirect(w, r, fmt.Sprintf("/admin/import?imported=%d&skipped=%d", imported, skipped), http.StatusSeeOther)
488
}
489
490
func (a *App) processImportZip(data []byte) (int, int, error) {
491
	zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
492
	if err != nil {
493
		return 0, 0, err
494
	}
495
	imported, skipped := 0, 0
496
	for _, f := range zr.File {
497
		if f.FileInfo().IsDir() {
498
			continue
499
		}
500
		name := f.Name
501
		if strings.HasPrefix(name, "__MACOSX/") {
502
			continue
503
		}
504
		base := name
505
		if i := strings.LastIndex(name, "/"); i >= 0 {
506
			base = name[i+1:]
507
		}
508
		if base == "" || strings.HasPrefix(base, ".") {
509
			continue
510
		}
511
		low := strings.ToLower(base)
512
		if !strings.HasSuffix(low, ".md") && !strings.HasSuffix(low, ".markdown") {
513
			continue
514
		}
515
		rc, err := f.Open()
516
		if err != nil {
517
			continue
518
		}
519
		raw, err := io.ReadAll(rc)
520
		rc.Close()
521
		if err != nil {
522
			continue
523
		}
524
		if a.importOne(base, string(raw), &imported, &skipped) {
525
			continue
526
		}
527
		skipped++
528
	}
529
	return imported, skipped, nil
530
}
531
532
func (a *App) importOne(basename, raw string, imported, skipped *int) bool {
533
	fm, body := splitFrontmatter(raw)
534
	attrs := parseAttributes(fm)
535
	title := strings.TrimSpace(attrs.Title)
536
	if title == "" {
537
		title = titleFromFilename(basename)
538
	}
539
	slug := deriveSlugWith(a, title, strings.TrimSpace(attrs.Slug))
540
	if slug == "" {
541
		return false
542
	}
543
	if existing, _ := getPostBySlug(a.DB, slug); existing != nil {
544
		*skipped++
545
		return true
546
	}
547
	status := "draft"
548
	if strings.EqualFold(strings.TrimSpace(attrs.Status), "published") {
549
		status = "published"
550
	}
551
	lang := "en"
552
	if l := strings.TrimSpace(attrs.Lang); l != "" {
553
		lang = l
554
	}
555
	pub := strings.TrimSpace(attrs.PublishedDate)
556
	if pub == "" {
557
		pub = nowDatetime()
558
	}
559
	in := PostInput{
560
		Title: optStr(title), Slug: slug, Content: body, Status: status,
561
		Alias:           optStr(attrs.Alias),
562
		PublishedDate:   &pub,
563
		MetaDescription: optStr(attrs.MetaDescription),
564
		MetaImage:       optStr(attrs.MetaImage),
565
		Lang:            lang, Tags: optStr(attrs.Tags),
566
	}
567
	if _, err := createPost(a.DB, in); err != nil {
568
		a.Log.Warn("import insert failed", "slug", slug, "err", err)
569
		return false
570
	}
571
	*imported++
572
	return true
573
}
574
575
var _ = url.QueryEscape