161 lines
5.3 KiB
Go
161 lines
5.3 KiB
Go
package api
|
||
|
||
import (
|
||
"errors"
|
||
"log/slog"
|
||
"net/http"
|
||
"strings"
|
||
"unicode/utf8"
|
||
|
||
"dana/internal/storage"
|
||
)
|
||
|
||
// Старые адреса Bitrix, которых нет на новом сайте. Правила подобраны по картам сайта
|
||
// и инфоблокам старой базы: удалённый врач ведёт в список специальности, позиция прайса —
|
||
// в её раздел, лицензия — на страницу лицензий и т. д. Нечего предложить — честный 404.
|
||
|
||
// legacyTarget возвращает адрес, куда перенаправить старую ссылку, или "".
|
||
func (a *API) legacyTarget(r *http.Request, parts []string) string {
|
||
ctx := r.Context()
|
||
if len(parts) == 0 {
|
||
return ""
|
||
}
|
||
sectionPage := map[string]string{
|
||
"licenses": "/company/licenses/", "docs": "/legaldocs/", "nadzor": "/company/nadzor/",
|
||
"partners": "/company/partners/", "vacancy": "/company/vacancy/", "reviews": "/reviews/",
|
||
"history": "/company/", "requisites": "/legaldocs/",
|
||
}
|
||
switch parts[0] {
|
||
case "company":
|
||
if len(parts) >= 2 && parts[1] == "staff" {
|
||
return a.legacyDoctor(r, parts)
|
||
}
|
||
if len(parts) >= 2 && sectionPage[parts[1]] != "" {
|
||
return sectionPage[parts[1]]
|
||
}
|
||
return "/company/"
|
||
case "price":
|
||
return a.legacyPrice(r, parts)
|
||
case "services":
|
||
if len(parts) >= 2 {
|
||
if _, err := a.store.ServiceByPath(ctx, parts[1]); err == nil {
|
||
return "/services/" + parts[1] + "/"
|
||
}
|
||
}
|
||
return "/services/"
|
||
case "checkup":
|
||
return "/checkup/"
|
||
case "news", "articles":
|
||
return "/" + parts[0] + "/"
|
||
case "contacts":
|
||
return "/contacts/"
|
||
case "info":
|
||
if len(parts) == 1 || parts[1] == "faq" {
|
||
return "/info/faq/"
|
||
}
|
||
case "patients":
|
||
// страница «Пациенту» перенесена скрытой; пока клиника её не включила — ближайший раздел
|
||
return "/info/faq/"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// legacyDoctor: /company/staff/<раздел>/<врач>/. Врач мог сменить специальность — тогда
|
||
// ведём на его текущий адрес; адреса со старыми хвостами («-det», «_uro», «-») — к тому же врачу.
|
||
func (a *API) legacyDoctor(r *http.Request, parts []string) string {
|
||
ctx := r.Context()
|
||
if len(parts) >= 4 {
|
||
slug := parts[3]
|
||
candidates := []string{slug, strings.TrimRight(slug, "-"), slug + "-"}
|
||
for _, suffix := range []string{"-det", "_uro", "_det"} {
|
||
if strings.HasSuffix(slug, suffix) {
|
||
candidates = append(candidates, strings.TrimSuffix(slug, suffix))
|
||
}
|
||
}
|
||
for _, c := range candidates {
|
||
if d, err := a.store.DoctorBySlug(ctx, c); err == nil {
|
||
return doctorWebPath(*d)
|
||
}
|
||
}
|
||
}
|
||
if len(parts) >= 3 {
|
||
if ok, _ := a.knownContentRoute(r, parts[:3]); ok {
|
||
return "/company/staff/" + parts[2] + "/"
|
||
}
|
||
}
|
||
return "/company/staff/"
|
||
}
|
||
|
||
// legacyPrice: страницы отдельных позиций прайса и старые вложенные разделы
|
||
// (/price/analizy/allergologiya/…) — в ближайший раздел, который есть на новом сайте.
|
||
func (a *API) legacyPrice(r *http.Request, parts []string) string {
|
||
ctx := r.Context()
|
||
for i := len(parts) - 1; i >= 1; i-- {
|
||
p := parts[i]
|
||
if strings.HasPrefix(p, "chek-ap-") {
|
||
if _, err := a.store.CheckupBySlug(ctx, p); err == nil {
|
||
return "/checkup/" + p + "/"
|
||
}
|
||
}
|
||
if ok, _ := a.knownContentRoute(r, []string{"price", p}); ok {
|
||
return "/price/" + p + "/"
|
||
}
|
||
}
|
||
return "/price/"
|
||
}
|
||
|
||
// notFound отвечает на адрес, которого нет: ручной редирект из админки, правило для
|
||
// старых адресов или 404 с записью в журнал.
|
||
func (a *API) notFound(w http.ResponseWriter, r *http.Request, pagePath string, parts []string) {
|
||
if a.store == nil { // юнит-тесты сайта без базы
|
||
a.website404(w, r)
|
||
return
|
||
}
|
||
// файлы (/index.php) хранятся без слэша на конце, разделы — со слэшем
|
||
for _, p := range []string{r.URL.Path, pagePath} {
|
||
if to, code, err := a.store.RedirectFor(r.Context(), p); err == nil {
|
||
redirectTo(w, r, to, code)
|
||
return
|
||
} else if !errors.Is(err, storage.ErrNotFound) {
|
||
slog.Error("redirect lookup", "err", err)
|
||
}
|
||
if p == pagePath {
|
||
break
|
||
}
|
||
}
|
||
if to := a.legacyTarget(r, parts); to != "" && to != pagePath {
|
||
redirectTo(w, r, to, http.StatusMovedPermanently)
|
||
return
|
||
}
|
||
logged := pagePath
|
||
if last := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]; strings.Contains(last, ".") {
|
||
logged = r.URL.Path // файл, а не раздел: без добавленного слэша
|
||
}
|
||
if len(logged) <= 300 && utf8.ValidString(logged) {
|
||
if err := a.store.LogNotFound(r.Context(), logged, truncate(r.Referer(), 300)); err != nil {
|
||
slog.Error("log not found", "err", err)
|
||
}
|
||
}
|
||
a.website404(w, r)
|
||
}
|
||
|
||
func redirectTo(w http.ResponseWriter, r *http.Request, to string, code int) {
|
||
if code != http.StatusFound {
|
||
code = http.StatusMovedPermanently
|
||
}
|
||
if r.URL.RawQuery != "" && !strings.Contains(to, "?") {
|
||
to += "?" + r.URL.RawQuery
|
||
}
|
||
http.Redirect(w, r, to, code)
|
||
}
|
||
|
||
func truncate(s string, n int) string {
|
||
if len(s) <= n {
|
||
return s
|
||
}
|
||
for n > 0 && !utf8.RuneStart(s[n]) {
|
||
n--
|
||
}
|
||
return s[:n]
|
||
}
|