package controllers import ( "context" "strconv" "git.golem15.com/golem15/sm-seo-plugin/classes" "git.golem15.com/golem15/sm-seo-plugin/models" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/pact" "gorm.io/gorm" ) // RedirectsControllerID is the redirects screen (UI-SPEC C8). const RedirectsControllerID = "golem15.seo.redirects" // redirectsAdminController lists and edits redirects (D-16 to D-19). The // loop refusal, the chain notice and history come from the plugin's own // field-less form extension on this controller, which runs inside the // save transaction once the row is written. type redirectsAdminController struct { app func() *backpack.App } func (redirectsAdminController) ID() string { return RedirectsControllerID } func (redirectsAdminController) ModelName() string { return `Golem15\SEO\Models\Redirect` } func (redirectsAdminController) ConfigDir() string { return "controllers/redirects" } func (redirectsAdminController) RequiredPermissions() []string { return []string{PermissionManage} } // NewRecord is a redirect with the form's defaults, so a save that omits // an option keeps its default. func (redirectsAdminController) NewRecord() any { r := models.NewRedirect() return &r } // FormBeforeCreate normalises the redirect and refuses an invalid source, // target or regex, and a source and match another redirect has. func (c redirectsAdminController) FormBeforeCreate(ctx context.Context, model any) error { return c.check(ctx, model) } // FormBeforeUpdate checks an update like a create. func (c redirectsAdminController) FormBeforeUpdate(ctx context.Context, model any) error { return c.check(ctx, model) } func (c redirectsAdminController) check(ctx context.Context, model any) error { r, ok := model.(*models.Redirect) if !ok || r == nil { return nil } r.Normalize() db, err := database(ctx, c.app) if err != nil { return err } return problemsError(ctx, translator(c.app), r.Problems(db)) } // FormAfterDelete records the deleted redirect in history inside the // delete's transaction. func (c redirectsAdminController) FormAfterDelete(ctx context.Context, model any) error { r, ok := model.(*models.Redirect) if !ok || r == nil { return nil } svc, err := classes.NewService(appOf(c.app)) if err != nil { return err } db, err := database(ctx, c.app) if err != nil { return err } return svc.Record(ctx, db, classes.Change{ Kind: models.SubjectRedirect, SubjectID: strconv.FormatUint(uint64(r.ID), 10), What: svc.Phrase("golem15.seo::lang.history.what_redirect", map[string]string{"url": r.Source}), Before: classes.RedirectValues(*r), }) } // ListRowStates marks a chain (UI-SPEC C8): a redirect whose target is // another redirect's source, labelled redirects.state_chain. func (redirectsAdminController) ListRowStates(ctx context.Context, db *gorm.DB, records []any) ([][]pact.RowState, error) { var targets []string for _, record := range records { if r, ok := record.(*models.Redirect); ok && r != nil && r.Target != "" { targets = append(targets, r.Target) } } sources := map[string]bool{} if len(targets) > 0 { var rows []string if err := db.WithContext(ctx).Model(&models.Redirect{}).Where("source IN ?", targets).Pluck("source", &rows).Error; err != nil { return nil, err } for _, s := range rows { sources[s] = true } } out := make([][]pact.RowState, len(records)) for i, record := range records { if r, ok := record.(*models.Redirect); ok && r != nil && r.Target != "" && sources[r.Target] { out[i] = []pact.RowState{pact.RowStateNegative} } } return out, nil } // matchLabels and originLabels are the phrase keys of the match kinds and // origins. var ( matchLabels = map[string]string{ models.RedirectExact: "golem15.seo::lang.redirects.match_exact", models.RedirectPattern: "golem15.seo::lang.redirects.match_pattern", models.RedirectRegex: "golem15.seo::lang.redirects.match_regex", } originLabels = map[string]string{ models.OriginManual: "golem15.seo::lang.redirects.origin_manual", models.OriginSlug: "golem15.seo::lang.redirects.origin_slug", models.OriginImport: "golem15.seo::lang.redirects.origin_import", models.OriginMCP: "golem15.seo::lang.redirects.origin_mcp", models.OriginNotFound: "golem15.seo::lang.redirects.origin_notfound", } ) // FilterOptions serves the choices of the status, match, origin and chain // filters. func (redirectsAdminController) FilterOptions(scope string) []pact.Option { switch scope { case models.FilterRedirectStatus: return []pact.Option{ {Value: "301", Label: "golem15.seo::lang.redirects.status_301"}, {Value: "302", Label: "golem15.seo::lang.redirects.status_302"}, {Value: "410", Label: "golem15.seo::lang.redirects.status_410"}, } case models.FilterRedirectMatch: out := make([]pact.Option, 0, len(matchLabels)) for _, m := range []string{models.RedirectExact, models.RedirectPattern, models.RedirectRegex} { out = append(out, pact.Option{Value: m, Label: matchLabels[m]}) } return out case models.FilterRedirectOrigin: out := make([]pact.Option, 0, len(models.RedirectOrigins)) for _, o := range models.RedirectOrigins { out = append(out, pact.Option{Value: o, Label: originLabels[o]}) } return out case models.FilterRedirectChain: return []pact.Option{ {Value: models.ChainOnly, Label: "golem15.seo::lang.redirects.filter_chain_only"}, {Value: models.ChainHide, Label: "golem15.seo::lang.redirects.filter_chain_hide"}, } } return nil } // ListExtendRecords shows the match and the origin of the list's rows as // their labels in the request locale. The rows are list values only and // never saved; the columns still sort by the stored values. func (c redirectsAdminController) ListExtendRecords(ctx context.Context, _ *gorm.DB, records []any) error { tr := translator(c.app) for _, record := range records { r, ok := record.(*models.Redirect) if !ok || r == nil { continue } if key, ok := matchLabels[r.Match]; ok { r.Match = Phrase(ctx, tr, key, nil) } if key, ok := originLabels[r.Origin]; ok { r.Origin = Phrase(ctx, tr, key, nil) } } return nil } var ( _ pact.AdminController = redirectsAdminController{} _ pact.AdminPermissioned = redirectsAdminController{} _ pact.AdminRecordSource = redirectsAdminController{} _ pact.FormBeforeCreate = redirectsAdminController{} _ pact.FormBeforeUpdate = redirectsAdminController{} _ pact.FormAfterDelete = redirectsAdminController{} _ pact.ListRowStates = redirectsAdminController{} _ pact.FilterOptions = redirectsAdminController{} _ pact.ListExtendRecords = redirectsAdminController{} )