github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/patcher/patches.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"database/sql"
     6  	"strconv"
     7  	"strings"
     8  	"unicode"
     9  
    10  	meta "github.com/Azareal/Gosora/common/meta"
    11  	qgen "github.com/Azareal/Gosora/query_gen"
    12  )
    13  
    14  type tblColumn = qgen.DBTableColumn
    15  type tC = tblColumn
    16  type tblKey = qgen.DBTableKey
    17  type tK = tblKey
    18  
    19  func init() {
    20  	addPatch(0, patch0)
    21  	addPatch(1, patch1)
    22  	addPatch(2, patch2)
    23  	addPatch(3, patch3)
    24  	addPatch(4, patch4)
    25  	addPatch(5, patch5)
    26  	addPatch(6, patch6)
    27  	addPatch(7, patch7)
    28  	addPatch(8, patch8)
    29  	addPatch(9, patch9)
    30  	addPatch(10, patch10)
    31  	addPatch(11, patch11)
    32  	addPatch(12, patch12)
    33  	addPatch(13, patch13)
    34  	addPatch(14, patch14)
    35  	addPatch(15, patch15)
    36  	addPatch(16, patch16)
    37  	addPatch(17, patch17)
    38  	addPatch(18, patch18)
    39  	addPatch(19, patch19)
    40  	addPatch(20, patch20)
    41  	addPatch(21, patch21)
    42  	addPatch(22, patch22)
    43  	addPatch(23, patch23)
    44  	addPatch(24, patch24)
    45  	addPatch(25, patch25)
    46  	addPatch(26, patch26)
    47  	addPatch(27, patch27)
    48  	addPatch(28, patch28)
    49  	addPatch(29, patch29)
    50  	addPatch(30, patch30)
    51  	addPatch(31, patch31)
    52  	addPatch(32, patch32)
    53  	addPatch(33, patch33)
    54  	addPatch(34, patch34)
    55  	addPatch(35, patch35)
    56  	addPatch(36, patch36)
    57  }
    58  
    59  func bcol(col string, val bool) qgen.DBTableColumn {
    60  	if val {
    61  		return tC{col, "boolean", 0, false, false, "1"}
    62  	}
    63  	return tC{col, "boolean", 0, false, false, "0"}
    64  }
    65  func ccol(col string, size int, sdefault string) qgen.DBTableColumn {
    66  	return tC{col, "varchar", size, false, false, sdefault}
    67  }
    68  
    69  func patch0(scanner *bufio.Scanner) (err error) {
    70  	err = createTable("menus", "", "",
    71  		[]tC{
    72  			{"mid", "int", 0, false, true, ""},
    73  		},
    74  		[]tK{
    75  			{"mid", "primary", "", false},
    76  		},
    77  	)
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	err = createTable("menu_items", "", "",
    83  		[]tC{
    84  			{"miid", "int", 0, false, true, ""},
    85  			{"mid", "int", 0, false, false, ""},
    86  			ccol("name", 200, ""),
    87  			ccol("htmlID", 200, "''"),
    88  			ccol("cssClass", 200, "''"),
    89  			ccol("position", 100, ""),
    90  			ccol("path", 200, "''"),
    91  			ccol("aria", 200, "''"),
    92  			ccol("tooltip", 200, "''"),
    93  			ccol("tmplName", 200, "''"),
    94  			{"order", "int", 0, false, false, "0"},
    95  
    96  			bcol("guestOnly", false),
    97  			bcol("memberOnly", false),
    98  			bcol("staffOnly", false),
    99  			bcol("adminOnly", false),
   100  		},
   101  		[]tK{
   102  			{"miid", "primary", "", false},
   103  		},
   104  	)
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	err = execStmt(qgen.Builder.SimpleInsert("menus", "", ""))
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	var order int
   115  	mOrder := "mid, name, htmlID, cssClass, position, path, aria, tooltip, guestOnly, memberOnly, staffOnly, adminOnly"
   116  	addMenuItem := func(data map[string]interface{}) error {
   117  		cols, values := qgen.InterfaceMapToInsertStrings(data, mOrder)
   118  		err := execStmt(qgen.Builder.SimpleInsert("menu_items", cols+", order", values+","+strconv.Itoa(order)))
   119  		order++
   120  		return err
   121  	}
   122  
   123  	err = addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_forums}", "htmlID": "menu_forums", "position": "left", "path": "/forums/", "aria": "{lang.menu_forums_aria}", "tooltip": "{lang.menu_forums_tooltip}"})
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	err = addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_topics}", "htmlID": "menu_topics", "cssClass": "menu_topics", "position": "left", "path": "/topics/", "aria": "{lang.menu_topics_aria}", "tooltip": "{lang.menu_topics_tooltip}"})
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	err = addMenuItem(map[string]interface{}{"mid": 1, "htmlID": "general_alerts", "cssClass": "menu_alerts", "position": "right", "tmplName": "menu_alerts"})
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	err = addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_account}", "cssClass": "menu_account", "position": "left", "path": "/user/edit/critical/", "aria": "{lang.menu_account_aria}", "tooltip": "{lang.menu_account_tooltip}", "memberOnly": true})
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	err = addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_profile}", "cssClass": "menu_profile", "position": "left", "path": "{me.Link}", "aria": "{lang.menu_profile_aria}", "tooltip": "{lang.menu_profile_tooltip}", "memberOnly": true})
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	err = addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_panel}", "cssClass": "menu_panel menu_account", "position": "left", "path": "/panel/", "aria": "{lang.menu_panel_aria}", "tooltip": "{lang.menu_panel_tooltip}", "memberOnly": true, "staffOnly": true})
   149  	if err != nil {
   150  		return err
   151  	}
   152  
   153  	err = addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_logout}", "cssClass": "menu_logout", "position": "left", "path": "/accounts/logout/?session={me.Session}", "aria": "{lang.menu_logout_aria}", "tooltip": "{lang.menu_logout_tooltip}", "memberOnly": true})
   154  	if err != nil {
   155  		return err
   156  	}
   157  
   158  	err = addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_register}", "cssClass": "menu_register", "position": "left", "path": "/accounts/create/", "aria": "{lang.menu_register_aria}", "tooltip": "{lang.menu_register_tooltip}", "guestOnly": true})
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	err = addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_login}", "cssClass": "menu_login", "position": "left", "path": "/accounts/login/", "aria": "{lang.menu_login_aria}", "tooltip": "{lang.menu_login_tooltip}", "guestOnly": true})
   164  	if err != nil {
   165  		return err
   166  	}
   167  
   168  	return nil
   169  }
   170  
   171  func patch1(scanner *bufio.Scanner) error {
   172  	return renameRoutes(map[string]string{
   173  		"routeAccountEditCriticalSubmit": "routes.AccountEditCriticalSubmit",
   174  		"routeAccountEditAvatar":         "routes.AccountEditAvatar",
   175  		"routeAccountEditAvatarSubmit":   "routes.AccountEditAvatarSubmit",
   176  		"routeAccountEditUsername":       "routes.AccountEditUsername",
   177  		"routeAccountEditUsernameSubmit": "routes.AccountEditUsernameSubmit",
   178  	})
   179  }
   180  
   181  func patch2(scanner *bufio.Scanner) error {
   182  	return renameRoutes(map[string]string{
   183  		"routeLogout":                   "routes.AccountLogout",
   184  		"routeShowAttachment":           "routes.ShowAttachment",
   185  		"routeChangeTheme":              "routes.ChangeTheme",
   186  		"routeProfileReplyCreateSubmit": "routes.ProfileReplyCreateSubmit",
   187  		"routeLikeTopicSubmit":          "routes.LikeTopicSubmit",
   188  		"routeReplyLikeSubmit":          "routes.ReplyLikeSubmit",
   189  		"routeDynamic":                  "routes.DynamicRoute",
   190  		"routeUploads":                  "routes.UploadedFile",
   191  		"BadRoute":                      "routes.BadRoute",
   192  	})
   193  }
   194  
   195  func patch3(scanner *bufio.Scanner) error {
   196  	return createTable("registration_logs", "", "",
   197  		[]tC{
   198  			{"rlid", "int", 0, false, true, ""},
   199  			ccol("username", 100, ""),
   200  			ccol("email", 100, ""),
   201  			ccol("failureReason", 100, ""),
   202  			bcol("success", false), // Did this attempt succeed?
   203  			ccol("ipaddress", 200, ""),
   204  			{"doneAt", "createdAt", 0, false, false, ""},
   205  		},
   206  		[]tK{
   207  			{"rlid", "primary", "", false},
   208  		},
   209  	)
   210  }
   211  
   212  func patch4(scanner *bufio.Scanner) error {
   213  	routes := map[string]string{
   214  		"routeReportSubmit":                      "routes.ReportSubmit",
   215  		"routeAccountEditEmail":                  "routes.AccountEditEmail",
   216  		"routeAccountEditEmailTokenSubmit":       "routes.AccountEditEmailTokenSubmit",
   217  		"routePanelLogsRegs":                     "panel.LogsRegs",
   218  		"routePanelLogsMod":                      "panel.LogsMod",
   219  		"routePanelLogsAdmin":                    "panel.LogsAdmin",
   220  		"routePanelDebug":                        "panel.Debug",
   221  		"routePanelAnalyticsViews":               "panel.AnalyticsViews",
   222  		"routePanelAnalyticsRouteViews":          "panel.AnalyticsRouteViews",
   223  		"routePanelAnalyticsAgentViews":          "panel.AnalyticsAgentViews",
   224  		"routePanelAnalyticsForumViews":          "panel.AnalyticsForumViews",
   225  		"routePanelAnalyticsSystemViews":         "panel.AnalyticsSystemViews",
   226  		"routePanelAnalyticsLanguageViews":       "panel.AnalyticsLanguageViews",
   227  		"routePanelAnalyticsReferrerViews":       "panel.AnalyticsReferrerViews",
   228  		"routePanelAnalyticsTopics":              "panel.AnalyticsTopics",
   229  		"routePanelAnalyticsPosts":               "panel.AnalyticsPosts",
   230  		"routePanelAnalyticsForums":              "panel.AnalyticsForums",
   231  		"routePanelAnalyticsRoutes":              "panel.AnalyticsRoutes",
   232  		"routePanelAnalyticsAgents":              "panel.AnalyticsAgents",
   233  		"routePanelAnalyticsSystems":             "panel.AnalyticsSystems",
   234  		"routePanelAnalyticsLanguages":           "panel.AnalyticsLanguages",
   235  		"routePanelAnalyticsReferrers":           "panel.AnalyticsReferrers",
   236  		"routePanelSettings":                     "panel.Settings",
   237  		"routePanelSettingEdit":                  "panel.SettingEdit",
   238  		"routePanelSettingEditSubmit":            "panel.SettingEditSubmit",
   239  		"routePanelForums":                       "panel.Forums",
   240  		"routePanelForumsCreateSubmit":           "panel.ForumsCreateSubmit",
   241  		"routePanelForumsDelete":                 "panel.ForumsDelete",
   242  		"routePanelForumsDeleteSubmit":           "panel.ForumsDeleteSubmit",
   243  		"routePanelForumsEdit":                   "panel.ForumsEdit",
   244  		"routePanelForumsEditSubmit":             "panel.ForumsEditSubmit",
   245  		"routePanelForumsEditPermsSubmit":        "panel.ForumsEditPermsSubmit",
   246  		"routePanelForumsEditPermsAdvance":       "panel.ForumsEditPermsAdvance",
   247  		"routePanelForumsEditPermsAdvanceSubmit": "panel.ForumsEditPermsAdvanceSubmit",
   248  		"routePanelBackups":                      "panel.Backups",
   249  	}
   250  	e := renameRoutes(routes)
   251  	if e != nil {
   252  		return e
   253  	}
   254  
   255  	e = execStmt(qgen.Builder.SimpleDelete("settings", "name='url_tags'"))
   256  	if e != nil {
   257  		return e
   258  	}
   259  
   260  	return createTable("pages", "utf8mb4", "utf8mb4_general_ci",
   261  		[]tC{
   262  			{"pid", "int", 0, false, true, ""},
   263  			ccol("name", 200, ""),
   264  			ccol("title", 200, ""),
   265  			{"body", "text", 0, false, false, ""},
   266  			{"allowedGroups", "text", 0, false, false, ""},
   267  			{"menuID", "int", 0, false, false, "-1"},
   268  		},
   269  		[]tK{
   270  			{"pid", "primary", "", false},
   271  		},
   272  	)
   273  }
   274  
   275  func patch5(scanner *bufio.Scanner) error {
   276  	routes := map[string]string{
   277  		"routePanelUsers":                  "panel.Users",
   278  		"routePanelUsersEdit":              "panel.UsersEdit",
   279  		"routePanelUsersEditSubmit":        "panel.UsersEditSubmit",
   280  		"routes.AccountEditCritical":       "routes.AccountEditPassword",
   281  		"routes.AccountEditCriticalSubmit": "routes.AccountEditPasswordSubmit",
   282  	}
   283  	e := renameRoutes(routes)
   284  	if e != nil {
   285  		return e
   286  	}
   287  
   288  	e = execStmt(qgen.Builder.SimpleUpdate("menu_items", "path='/user/edit/'", "path='/user/edit/critical/'"))
   289  	if e != nil {
   290  		return e
   291  	}
   292  
   293  	return createTable("users_2fa_keys", "utf8mb4", "utf8mb4_general_ci",
   294  		[]tC{
   295  			{"uid", "int", 0, false, false, ""},
   296  			ccol("secret", 100, ""),
   297  			ccol("scratch1", 50, ""),
   298  			ccol("scratch2", 50, ""),
   299  			ccol("scratch3", 50, ""),
   300  			ccol("scratch4", 50, ""),
   301  			ccol("scratch5", 50, ""),
   302  			ccol("scratch6", 50, ""),
   303  			ccol("scratch7", 50, ""),
   304  			ccol("scratch8", 50, ""),
   305  			{"createdAt", "createdAt", 0, false, false, ""},
   306  		},
   307  		[]tK{
   308  			{"uid", "primary", "", false},
   309  		},
   310  	)
   311  }
   312  
   313  func patch6(scanner *bufio.Scanner) error {
   314  	return execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'rapid_loading','1','bool'"))
   315  }
   316  
   317  func patch7(scanner *bufio.Scanner) error {
   318  	return createTable("users_avatar_queue", "", "",
   319  		[]tC{
   320  			{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
   321  		},
   322  		[]tK{
   323  			{"uid", "primary", "", false},
   324  		},
   325  	)
   326  }
   327  
   328  func renameRoutes(routes map[string]string) error {
   329  	// ! Don't reuse this function blindly, it doesn't escape apostrophes
   330  	replaceTextWhere := func(replaceThis string, withThis string) error {
   331  		return execStmt(qgen.Builder.SimpleUpdate("viewchunks", "route = '"+withThis+"'", "route = '"+replaceThis+"'"))
   332  	}
   333  
   334  	for key, value := range routes {
   335  		e := replaceTextWhere(key, value)
   336  		if e != nil {
   337  			return e
   338  		}
   339  	}
   340  
   341  	return nil
   342  }
   343  
   344  func patch8(scanner *bufio.Scanner) error {
   345  	routes := map[string]string{
   346  		"routePanelWordFilter":                 "panel.WordFilters",
   347  		"routePanelWordFiltersCreateSubmit":    "panel.WordFiltersCreateSubmit",
   348  		"routePanelWordFiltersEdit":            "panel.WordFiltersEdit",
   349  		"routePanelWordFiltersEditSubmit":      "panel.WordFiltersEditSubmit",
   350  		"routePanelWordFiltersDeleteSubmit":    "panel.WordFiltersDeleteSubmit",
   351  		"routePanelPlugins":                    "panel.Plugins",
   352  		"routePanelPluginsActivate":            "panel.PluginsActivate",
   353  		"routePanelPluginsDeactivate":          "panel.PluginsDeactivate",
   354  		"routePanelPluginsInstall":             "panel.PluginsInstall",
   355  		"routePanelGroups":                     "panel.Groups",
   356  		"routePanelGroupsEdit":                 "panel.GroupsEdit",
   357  		"routePanelGroupsEditPerms":            "panel.GroupsEditPerms",
   358  		"routePanelGroupsEditSubmit":           "panel.GroupsEditSubmit",
   359  		"routePanelGroupsEditPermsSubmit":      "panel.GroupsEditPermsSubmit",
   360  		"routePanelGroupsCreateSubmit":         "panel.GroupsCreateSubmit",
   361  		"routePanelThemes":                     "panel.Themes",
   362  		"routePanelThemesSetDefault":           "panel.ThemesSetDefault",
   363  		"routePanelThemesMenus":                "panel.ThemesMenus",
   364  		"routePanelThemesMenusEdit":            "panel.ThemesMenusEdit",
   365  		"routePanelThemesMenuItemEdit":         "panel.ThemesMenuItemEdit",
   366  		"routePanelThemesMenuItemEditSubmit":   "panel.ThemesMenuItemEditSubmit",
   367  		"routePanelThemesMenuItemCreateSubmit": "panel.ThemesMenuItemCreateSubmit",
   368  		"routePanelThemesMenuItemDeleteSubmit": "panel.ThemesMenuItemDeleteSubmit",
   369  		"routePanelThemesMenuItemOrderSubmit":  "panel.ThemesMenuItemOrderSubmit",
   370  		"routePanelDashboard":                  "panel.Dashboard",
   371  	}
   372  	e := renameRoutes(routes)
   373  	if e != nil {
   374  		return e
   375  	}
   376  
   377  	return createTable("updates", "", "",
   378  		[]tC{
   379  			{"dbVersion", "int", 0, false, false, "0"},
   380  		}, nil,
   381  	)
   382  }
   383  
   384  func patch9(scanner *bufio.Scanner) error {
   385  	// Table "updates" might not exist due to the installer, so drop it and remake it if so
   386  	err := patch8(scanner)
   387  	if err != nil {
   388  		return err
   389  	}
   390  
   391  	return createTable("login_logs", "", "",
   392  		[]tC{
   393  			{"lid", "int", 0, false, true, ""},
   394  			{"uid", "int", 0, false, false, ""},
   395  			bcol("success", false), // Did this attempt succeed?
   396  			ccol("ipaddress", 200, ""),
   397  			{"doneAt", "createdAt", 0, false, false, ""},
   398  		},
   399  		[]tK{
   400  			{"lid", "primary", "", false},
   401  		},
   402  	)
   403  }
   404  
   405  var acc = qgen.NewAcc
   406  var itoa = strconv.Itoa
   407  
   408  func patch10(scanner *bufio.Scanner) error {
   409  	err := execStmt(qgen.Builder.AddColumn("topics", tC{"attachCount", "int", 0, false, false, "0"}, nil))
   410  	if err != nil {
   411  		return err
   412  	}
   413  	err = execStmt(qgen.Builder.AddColumn("topics", tC{"lastReplyID", "int", 0, false, false, "0"}, nil))
   414  	if err != nil {
   415  		return err
   416  	}
   417  
   418  	err = acc().Select("topics").Cols("tid").EachInt(func(tid int) error {
   419  		stid := itoa(tid)
   420  		count, err := acc().Count("attachments").Where("originTable = 'topics' and originID=" + stid).Total()
   421  		if err != nil {
   422  			return err
   423  		}
   424  
   425  		hasReply := false
   426  		err = acc().Select("replies").Cols("rid").Where("tid=" + stid).Orderby("rid DESC").Limit("1").EachInt(func(rid int) error {
   427  			hasReply = true
   428  			_, err := acc().Update("topics").Set("lastReplyID=?, attachCount=?").Where("tid="+stid).Exec(rid, count)
   429  			return err
   430  		})
   431  		if err != nil {
   432  			return err
   433  		}
   434  		if !hasReply {
   435  			_, err = acc().Update("topics").Set("attachCount=?").Where("tid=" + stid).Exec(count)
   436  		}
   437  		return err
   438  	})
   439  	if err != nil {
   440  		return err
   441  	}
   442  
   443  	_, err = acc().Insert("updates").Columns("dbVersion").Fields("0").Exec()
   444  	return err
   445  }
   446  
   447  func patch11(scanner *bufio.Scanner) error {
   448  	err := execStmt(qgen.Builder.AddColumn("replies", tC{"attachCount", "int", 0, false, false, "0"}, nil))
   449  	if err != nil {
   450  		return err
   451  	}
   452  
   453  	// Attachments for replies got the topicID rather than the replyID for a while in error, so we want to separate these out
   454  	_, err = acc().Update("attachments").Set("originTable='freplies'").Where("originTable='replies'").Exec()
   455  	if err != nil {
   456  		return err
   457  	}
   458  
   459  	// We could probably do something more efficient, but as there shouldn't be too many sites right now, we can probably cheat a little, otherwise it'll take forever to get things done
   460  	return acc().Select("topics").Cols("tid").EachInt(func(tid int) error {
   461  		stid := itoa(tid)
   462  		count, err := acc().Count("attachments").Where("originTable='topics' and originID=" + stid).Total()
   463  		if err != nil {
   464  			return err
   465  		}
   466  		_, err = acc().Update("topics").Set("attachCount=?").Where("tid=" + stid).Exec(count)
   467  		return err
   468  	})
   469  
   470  	/*return acc().Select("replies").Cols("rid").EachInt(func(rid int) error {
   471  		srid := itoa(rid)
   472  		count, err := acc().Count("attachments").Where("originTable='replies' and originID=" + srid).Total()
   473  		if err != nil {
   474  			return err
   475  		}
   476  		_, err = acc().Update("replies").Set("attachCount = ?").Where("rid=" + srid).Exec(count)
   477  		return err
   478  	})*/
   479  }
   480  
   481  func patch12(scanner *bufio.Scanner) error {
   482  	var e error
   483  	addIndex := func(tbl, iname, colname string) {
   484  		if e != nil {
   485  			return
   486  		}
   487  		/*e = execStmt(qgen.Builder.RemoveIndex(tbl, iname))
   488  		if e != nil {
   489  			return
   490  		}*/
   491  		e = execStmt(qgen.Builder.AddIndex(tbl, iname, colname))
   492  	}
   493  	addIndex("topics", "parentID", "parentID")
   494  	addIndex("replies", "tid", "tid")
   495  	addIndex("polls", "parentID", "parentID")
   496  	addIndex("likes", "targetItem", "targetItem")
   497  	addIndex("emails", "uid", "uid")
   498  	addIndex("attachments", "originID", "originID")
   499  	addIndex("attachments", "path", "path")
   500  	addIndex("activity_stream_matches", "watcher", "watcher")
   501  	return e
   502  }
   503  
   504  func patch13(scanner *bufio.Scanner) error {
   505  	return execStmt(qgen.Builder.AddColumn("widgets", tC{"wid", "int", 0, false, true, ""}, &tK{"wid", "primary", "", false}))
   506  }
   507  
   508  func patch14(scanner *bufio.Scanner) error {
   509  	/*err := execStmt(qgen.Builder.AddKey("topics", "title", tK{"title", "fulltext", "", false}))
   510  	if err != nil {
   511  		return err
   512  	}
   513  	err = execStmt(qgen.Builder.AddKey("topics", "content", tK{"content", "fulltext", "", false}))
   514  	if err != nil {
   515  		return err
   516  	}
   517  	err = execStmt(qgen.Builder.AddKey("replies", "content", tK{"content", "fulltext", "", false}))
   518  	if err != nil {
   519  		return err
   520  	}*/
   521  
   522  	return nil
   523  }
   524  
   525  func patch15(scanner *bufio.Scanner) error {
   526  	return execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'google_site_verify','','html-attribute'"))
   527  }
   528  
   529  func patch16(scanner *bufio.Scanner) error {
   530  	return createTable("password_resets", "", "",
   531  		[]tC{
   532  			ccol("email", 200, ""),
   533  			{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
   534  			ccol("validated", 200, ""),          // Token given once the one-use token is consumed, used to prevent multiple people consuming the same one-use token
   535  			ccol("token", 200, ""),
   536  			{"createdAt", "createdAt", 0, false, false, ""},
   537  		}, nil,
   538  	)
   539  }
   540  
   541  func patch17(scanner *bufio.Scanner) error {
   542  	err := execStmt(qgen.Builder.AddColumn("attachments", ccol("extra", 200, ""), nil))
   543  	if err != nil {
   544  		return err
   545  	}
   546  
   547  	err = acc().Select("topics").Cols("tid,parentID").Where("attachCount > 0").Each(func(rows *sql.Rows) error {
   548  		var tid, parentID int
   549  		err := rows.Scan(&tid, &parentID)
   550  		if err != nil {
   551  			return err
   552  		}
   553  		_, err = acc().Update("attachments").Set("sectionID=?").Where("originTable='topics' AND originID=?").Exec(parentID, tid)
   554  		return err
   555  	})
   556  	if err != nil {
   557  		return err
   558  	}
   559  
   560  	return acc().Select("replies").Cols("rid,tid").Where("attachCount > 0").Each(func(rows *sql.Rows) error {
   561  		var rid, tid, sectionID int
   562  		err := rows.Scan(&rid, &tid)
   563  		if err != nil {
   564  			return err
   565  		}
   566  		err = acc().Select("topics").Cols("parentID").Where("tid=?").QueryRow(tid).Scan(&sectionID)
   567  		if err != nil {
   568  			return err
   569  		}
   570  		_, err = acc().Update("attachments").Set("sectionID=?, extra=?").Where("originTable='replies' AND originID=?").Exec(sectionID, tid, rid)
   571  		return err
   572  	})
   573  }
   574  
   575  func patch18(scanner *bufio.Scanner) error {
   576  	return execStmt(qgen.Builder.AddColumn("forums", tC{"order", "int", 0, false, false, "0"}, nil))
   577  }
   578  
   579  func patch19(scanner *bufio.Scanner) error {
   580  	return createTable("memchunks", "", "",
   581  		[]tC{
   582  			{"count", "int", 0, false, false, "0"},
   583  			{"createdAt", "datetime", 0, false, false, ""},
   584  		}, nil,
   585  	)
   586  }
   587  
   588  func patch20(scanner *bufio.Scanner) error {
   589  	err := acc().Select("activity_stream_matches").Cols("asid").Each(func(rows *sql.Rows) error {
   590  		var asid int
   591  		if e := rows.Scan(&asid); e != nil {
   592  			return e
   593  		}
   594  		e := acc().Select("activity_stream").Cols("asid").Where("asid=?").QueryRow(asid).Scan(&asid)
   595  		if e != sql.ErrNoRows {
   596  			return e
   597  		}
   598  		_, e = acc().Delete("activity_stream_matches").Where("asid=?").Run(asid)
   599  		return e
   600  	})
   601  	if err != nil {
   602  		return err
   603  	}
   604  
   605  	return execStmt(qgen.Builder.AddForeignKey("activity_stream_matches", "asid", "activity_stream", "asid", true))
   606  }
   607  
   608  func patch21(scanner *bufio.Scanner) error {
   609  	err := execStmt(qgen.Builder.AddColumn("memchunks", tC{"stack", "int", 0, false, false, "0"}, nil))
   610  	if err != nil {
   611  		return err
   612  	}
   613  
   614  	err = execStmt(qgen.Builder.AddColumn("memchunks", tC{"heap", "int", 0, false, false, "0"}, nil))
   615  	if err != nil {
   616  		return err
   617  	}
   618  
   619  	err = createTable("meta", "", "",
   620  		[]tC{
   621  			ccol("name", 200, ""),
   622  			ccol("value", 200, ""),
   623  		}, nil,
   624  	)
   625  	if err != nil {
   626  		return err
   627  	}
   628  
   629  	return execStmt(qgen.Builder.AddColumn("activity_stream", tC{"createdAt", "createdAt", 0, false, false, ""}, nil))
   630  }
   631  
   632  func patch22(scanner *bufio.Scanner) error {
   633  	return execStmt(qgen.Builder.AddColumn("forums", ccol("tmpl", 200, "''"), nil))
   634  }
   635  
   636  func patch23(scanner *bufio.Scanner) error {
   637  	err := createTable("conversations", "", "",
   638  		[]tC{
   639  			{"cid", "int", 0, false, true, ""},
   640  			{"createdBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key
   641  			{"createdAt", "createdAt", 0, false, false, ""},
   642  			{"lastReplyAt", "datetime", 0, false, false, ""},
   643  			{"lastReplyBy", "int", 0, false, false, ""},
   644  		},
   645  		[]tK{
   646  			{"cid", "primary", "", false},
   647  		},
   648  	)
   649  	if err != nil {
   650  		return err
   651  	}
   652  
   653  	err = createTable("conversations_posts", "", "",
   654  		[]tC{
   655  			{"pid", "int", 0, false, true, ""},
   656  			{"cid", "int", 0, false, false, ""},
   657  			{"createdBy", "int", 0, false, false, ""},
   658  			ccol("body", 50, ""),
   659  			ccol("post", 50, "''"),
   660  		},
   661  		[]tK{
   662  			{"pid", "primary", "", false},
   663  		},
   664  	)
   665  	if err != nil {
   666  		return err
   667  	}
   668  
   669  	return createTable("conversations_participants", "", "",
   670  		[]tC{
   671  			{"uid", "int", 0, false, false, ""},
   672  			{"cid", "int", 0, false, false, ""},
   673  		}, nil,
   674  	)
   675  }
   676  
   677  func patch24(scanner *bufio.Scanner) error {
   678  	return createTable("users_groups_promotions", "", "",
   679  		[]tC{
   680  			{"pid", "int", 0, false, true, ""},
   681  			{"from_gid", "int", 0, false, false, ""},
   682  			{"to_gid", "int", 0, false, false, ""},
   683  			bcol("two_way", false), // If a user no longer meets the requirements for this promotion then they will be demoted if this flag is set
   684  
   685  			// Requirements
   686  			{"level", "int", 0, false, false, ""},
   687  			{"minTime", "int", 0, false, false, ""}, // How long someone needs to have been in their current group before being promoted
   688  		},
   689  		[]tK{
   690  			{"pid", "primary", "", false},
   691  		},
   692  	)
   693  }
   694  
   695  func patch25(scanner *bufio.Scanner) error {
   696  	return execStmt(qgen.Builder.AddColumn("users_groups_promotions", tC{"posts", "int", 0, false, false, "0"}, nil))
   697  }
   698  
   699  func patch26(scanner *bufio.Scanner) error {
   700  	return createTable("users_blocks", "", "",
   701  		[]tC{
   702  			{"blocker", "int", 0, false, false, ""},
   703  			{"blockedUser", "int", 0, false, false, ""},
   704  		}, nil,
   705  	)
   706  }
   707  
   708  func patch27(scanner *bufio.Scanner) error {
   709  	err := execStmt(qgen.Builder.AddColumn("moderation_logs", tC{"extra", "text", 0, false, false, ""}, nil))
   710  	if err != nil {
   711  		return err
   712  	}
   713  	return execStmt(qgen.Builder.AddColumn("administration_logs", tC{"extra", "text", 0, false, false, ""}, nil))
   714  }
   715  
   716  func patch28(scanner *bufio.Scanner) error {
   717  	return execStmt(qgen.Builder.AddColumn("users", tC{"enable_embeds", "int", 0, false, false, "-1"}, nil))
   718  }
   719  
   720  // The word counter might run into problems with some languages where words aren't as obviously demarcated, I would advise turning it off in those cases, or if it becomes annoying in general, really.
   721  func WordCount(input string) (count int) {
   722  	input = strings.TrimSpace(input)
   723  	if input == "" {
   724  		return 0
   725  	}
   726  
   727  	var inSpace bool
   728  	for _, value := range input {
   729  		if unicode.IsSpace(value) || unicode.IsPunct(value) {
   730  			if !inSpace {
   731  				inSpace = true
   732  			}
   733  		} else if inSpace {
   734  			count++
   735  			inSpace = false
   736  		}
   737  	}
   738  
   739  	return count + 1
   740  }
   741  
   742  func patch29(scanner *bufio.Scanner) error {
   743  	f := func(tbl, idCol string) error {
   744  		return acc().Select(tbl).Cols(idCol + ",content").Each(func(rows *sql.Rows) error {
   745  			var id int
   746  			var content string
   747  			err := rows.Scan(&id, &content)
   748  			if err != nil {
   749  				return err
   750  			}
   751  			_, err = acc().Update(tbl).Set("words=?").Where(idCol+"=?").Exec(WordCount(content), id)
   752  			return err
   753  		})
   754  	}
   755  	err := f("topics", "tid")
   756  	if err != nil {
   757  		return err
   758  	}
   759  	err = f("replies", "rid")
   760  	if err != nil {
   761  		return err
   762  	}
   763  
   764  	meta, err := meta.NewDefaultMetaStore(acc())
   765  	if err != nil {
   766  		return err
   767  	}
   768  	err = meta.Set("sched", "recalc")
   769  	if err != nil {
   770  		return err
   771  	}
   772  
   773  	fixCols := func(tbls ...string) error {
   774  		for _, tbl := range tbls {
   775  			//err := execStmt(qgen.Builder.RenameColumn(tbl, "ipaddress","ip"))
   776  			err := execStmt(qgen.Builder.ChangeColumn(tbl, "ipaddress", ccol("ip", 200, "''")))
   777  			if err != nil {
   778  				return err
   779  			}
   780  			err = execStmt(qgen.Builder.SetDefaultColumn(tbl, "ip", "varchar", ""))
   781  			if err != nil {
   782  				return err
   783  			}
   784  		}
   785  		return nil
   786  	}
   787  	err = fixCols("topics", "replies", "polls_votes", "users_replies")
   788  	if err != nil {
   789  		return err
   790  	}
   791  
   792  	err = execStmt(qgen.Builder.SetDefaultColumn("replies", "lastEdit", "int", "0"))
   793  	if err != nil {
   794  		return err
   795  	}
   796  	err = execStmt(qgen.Builder.SetDefaultColumn("replies", "lastEditBy", "int", "0"))
   797  	if err != nil {
   798  		return err
   799  	}
   800  	err = execStmt(qgen.Builder.SetDefaultColumn("users_replies", "lastEdit", "int", "0"))
   801  	if err != nil {
   802  		return err
   803  	}
   804  	err = execStmt(qgen.Builder.SetDefaultColumn("users_replies", "lastEditBy", "int", "0"))
   805  	if err != nil {
   806  		return err
   807  	}
   808  
   809  	return execStmt(qgen.Builder.AddColumn("activity_stream", tC{"extra", "varchar", 200, false, false, "''"}, nil))
   810  
   811  }
   812  
   813  func patch30(scanner *bufio.Scanner) error {
   814  	e := execStmt(qgen.Builder.AddColumn("users_groups_promotions", tC{"registeredFor", "int", 0, false, false, "0"}, nil))
   815  	if e != nil {
   816  		return e
   817  	}
   818  	return execStmt(qgen.Builder.SetDefaultColumn("users", "last_ip", "varchar", ""))
   819  }
   820  
   821  func patch31(scanner *bufio.Scanner) (e error) {
   822  	addKey := func(tbl, col string, tk qgen.DBTableKey) error {
   823  		/*e := execStmt(qgen.Builder.RemoveIndex(tbl, col))
   824  		if e != nil {
   825  			return e
   826  		}*/
   827  		return execStmt(qgen.Builder.AddKey(tbl, col, tk))
   828  	}
   829  	e = addKey("topics", "title", tK{"title", "fulltext", "", false})
   830  	if e != nil {
   831  		return e
   832  	}
   833  	e = addKey("topics", "content", tK{"content", "fulltext", "", false})
   834  	if e != nil {
   835  		return e
   836  	}
   837  	return addKey("replies", "content", tK{"content", "fulltext", "", false})
   838  }
   839  
   840  func createTable(tbl, charset, collation string, cols []tC, keys []tK) error {
   841  	e := execStmt(qgen.Builder.DropTable(tbl))
   842  	if e != nil {
   843  		return e
   844  	}
   845  	return execStmt(qgen.Builder.CreateTable(tbl, charset, collation, cols, keys))
   846  }
   847  
   848  func patch32(scanner *bufio.Scanner) error {
   849  	return createTable("perfchunks", "", "",
   850  		[]tC{
   851  			{"low", "int", 0, false, false, "0"},
   852  			{"high", "int", 0, false, false, "0"},
   853  			{"avg", "int", 0, false, false, "0"},
   854  			{"createdAt", "datetime", 0, false, false, ""},
   855  		}, nil,
   856  	)
   857  }
   858  
   859  func patch33(scanner *bufio.Scanner) error {
   860  	return execStmt(qgen.Builder.AddColumn("viewchunks", tC{"avg", "int", 0, false, false, "0"}, nil))
   861  }
   862  
   863  func patch34(scanner *bufio.Scanner) error {
   864  	/*err := createTable("tables", "", "",
   865  		[]tC{
   866  			{"id", "int", 0, false, true, ""},
   867  			ccol("name", 200, ""),
   868  		},
   869  		[]tK{
   870  			{"id", "primary", "", false},
   871  			{"name", "unique", "", false},
   872  		},
   873  	)
   874  	if err != nil {
   875  		return err
   876  	}
   877  	insert := func(tbl, cols, fields string) {
   878  		if err != nil {
   879  			return
   880  		}
   881  		err = execStmt(qgen.Builder.SimpleInsert(tbl, cols, fields))
   882  	}
   883  	insert("tables", "name", "forums")
   884  	insert("tables", "name", "topics")
   885  	insert("tables", "name", "replies")
   886  	// ! Hold onto freplies for a while longer
   887  	insert("tables", "name", "freplies")*/
   888  	/*err := execStmt(qgen.Builder.AddColumn("topics", tC{"attachCount", "int", 0, false, false, "0"}, nil))
   889  	if err != nil {
   890  		return err
   891  	}*/
   892  	overwriteColumn := func(tbl, col string, tc qgen.DBTableColumn) error {
   893  		/*e := execStmt(qgen.Builder.DropColumn(tbl, col))
   894  		if e != nil {
   895  			return e
   896  		}*/
   897  		return execStmt(qgen.Builder.AddColumn(tbl, tc, nil))
   898  	}
   899  	err := overwriteColumn("users", "profile_comments", tC{"profile_comments", "int", 0, false, false, "0"})
   900  	if err != nil {
   901  		return err
   902  	}
   903  	err = overwriteColumn("users", "who_can_convo", tC{"who_can_convo", "int", 0, false, false, "0"})
   904  	if err != nil {
   905  		return err
   906  	}
   907  
   908  	setDefault := func(tbl, col, typ, val string) {
   909  		if err != nil {
   910  			return
   911  		}
   912  		err = execStmt(qgen.Builder.SetDefaultColumn(tbl, col, typ, val))
   913  	}
   914  	setDefault("users_groups", "permissions", "text", "{}")
   915  	setDefault("users_groups", "plugin_perms", "text", "{}")
   916  	setDefault("forums_permissions", "permissions", "text", "{}")
   917  	setDefault("topics", "content", "text", "")
   918  	setDefault("topics", "parsed_content", "text", "")
   919  	setDefault("replies", "content", "text", "")
   920  	setDefault("replies", "parsed_content", "text", "")
   921  	//setDefault("revisions", "content", "text", "")
   922  	setDefault("users_replies", "content", "text", "")
   923  	setDefault("users_replies", "parsed_content", "text", "")
   924  	setDefault("pages", "body", "text", "")
   925  	setDefault("pages", "allowedGroups", "text", "")
   926  	setDefault("moderation_logs", "extra", "text", "")
   927  	setDefault("administration_logs", "extra", "text", "")
   928  	if err != nil {
   929  		return err
   930  	}
   931  
   932  	return nil
   933  }
   934  
   935  func patch35(scanner *bufio.Scanner) error {
   936  	e := execStmt(qgen.Builder.AddColumn("topics", tC{"weekEvenViews", "int", 0, false, false, "0"}, nil))
   937  	if e != nil {
   938  		return e
   939  	}
   940  	return execStmt(qgen.Builder.AddColumn("topics", tC{"weekOddViews", "int", 0, false, false, "0"}, nil))
   941  }
   942  
   943  func patch36(scanner *bufio.Scanner) error {
   944  	e := createTable("forums_actions", "utf8mb4", "utf8mb4_general_ci",
   945  		[]tC{
   946  			{"faid", "int", 0, false, true, ""},
   947  			{"fid", "int", 0, false, false, ""},
   948  			bcol("runOnTopicCreation", false),
   949  			{"runDaysAfterTopicCreation", "int", 0, false, false, "0"},
   950  			{"runDaysAfterTopicLastReply", "int", 0, false, false, "0"},
   951  			ccol("action", 50, ""),
   952  			ccol("extra", 200, "''"),
   953  		},
   954  		[]tK{
   955  			{"faid", "primary", "", false},
   956  		},
   957  	)
   958  	if e != nil {
   959  		return e
   960  	}
   961  	return execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type, constraints", "'avatar_visibility','0','list','0-1'"))
   962  }