diff --git a/conferenceadmin.go b/conferenceadmin.go index 2cf8511..0efcf93 100644 --- a/conferenceadmin.go +++ b/conferenceadmin.go @@ -865,3 +865,39 @@ func ManageConferenceList(ctxt ui.AmContext) (string, any) { ctxt.SetFrameTitle("Manage Conference List") return "framed", "manage_conflist.jet" } + +/* ManageDeleteConference handles the deletion of a conference from the management menu. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + */ +func ManageDeleteConference(ctxt ui.AmContext) (string, any) { + comm := ctxt.CurrentCommunity() + conf := ctxt.GetScratch("currentConference").(*database.Conference) + myLevel := ctxt.GetScratch("levelInConference").(uint16) + if !conf.TestPermission("Conference.Delete", myLevel) { + return "error", ENOPERM + } + + // Load the message box, and, if we have a valid "yes," then perform the delete + mbox, err := ui.AmLoadMessageBox("deleteConf") + if err != nil { + return "error", err + } + if mbox.Validate(ctxt, "yes") { + err := conf.Delete(ctxt.Ctx(), comm, ctxt.CurrentUser(), ctxt.RemoteIP(), ampool) + if err != nil { + return "error", err + } + return "redirect", fmt.Sprintf("/comm/%s/manage_conf", ctxt.CurrentCommunity().Alias) + } + + // Set up to display the message box. + mbox.SetMessage(fmt.Sprintf(`You are about to delete the conference "%s" + from the "%s" community!`, conf.Name, comm.Name)) + mbox.SetLink("no", fmt.Sprintf("/comm/%s/manage_conf", ctxt.CurrentCommunity().Alias)) + mbox.SetLink("yes", fmt.Sprintf("/comm/%s/manage_conf/del/%s", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"))) + return mbox.Render(ctxt) +} diff --git a/database/conference.go b/database/conference.go index 14c2c32..b3db49c 100644 --- a/database/conference.go +++ b/database/conference.go @@ -813,6 +813,131 @@ func (c *Conference) Stats(ctx context.Context) (int, int, error) { return ntopic, npost, err } +// backgroundPurgeConference purges out all the conference information in the background. +func backgroundPurgeConference(ctx context.Context, confid int32) error { + // Purge out auxiliary conference tables first. + tx := amdb.MustBegin() + _, err := tx.ExecContext(ctx, "DELETE FROM confmember WHERE confid = ?", confid) + if err != nil { + log.Warnf("backgroundPurgeConference(%d): failed purging confmember: %v", confid, err) + } + _, err = tx.ExecContext(ctx, "DELETE FROM confsettings WHERE confid = ?", confid) + if err != nil { + log.Warnf("backgroundPurgeConference(%d): failed purging confsettings: %v", confid, err) + } + _, err = tx.ExecContext(ctx, "DELETE FROM confhotlist WHERE confid = ?", confid) + if err != nil { + log.Warnf("backgroundPurgeConference(%d): failed purging confhotlist: %v", confid, err) + } + _, err = tx.ExecContext(ctx, "DELETE FROM propconf WHERE confid = ?", confid) + if err != nil { + log.Warnf("backgroundPurgeConference(%d): failed purging propconf: %v", confid, err) + } + _, err = tx.ExecContext(ctx, "DELETE FROM confcustom WHERE confid = ?", confid) + if err != nil { + log.Warnf("backgroundPurgeConference(%d): failed purging confcustom: %v", confid, err) + } + err = tx.Commit() + if err != nil { + tx.Rollback() + return err + } + + // Get all topic IDs in this conference. + var topicIds []int32 + err = amdb.SelectContext(ctx, &topicIds, "SELECT topicid FROM topics WHERE confid = ?", confid) + if err != nil { + return err + } + + // Erase each topic in turn by calling two of the "delete topic" internal functions. + for _, topicId := range topicIds { + tx := amdb.MustBegin() + err = eraseTopicRecords(ctx, tx, topicId) + if err == nil { + err = tx.Commit() + } + if err != nil { + tx.Rollback() + return err + } + err = backgroundPurgeTopic(ctx, topicId) + if err != nil { + return err + } + } + return nil +} + +// Delete unlinks this conference from the community, deleting it entirely if the last link is gone. +func (c *Conference) Delete(ctx context.Context, comm *Community, u *User, ipaddr string, background *util.WorkerPool) error { + success := false + tx := amdb.MustBegin() + defer func() { + if !success { + tx.Rollback() + } + }() + getConferenceMutex.Lock() + defer getConferenceMutex.Unlock() + + // any references to conference other than this community? + row := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM commtoconf WHERE confid = ? AND commid <> ?", c.ConfId, comm.Id) + refCount := 0 + err := row.Scan(&refCount) + if err != nil { + return err + } + + // break the link with the community + if _, err = tx.ExecContext(ctx, "DELETE FROM commtoconf WHERE commid = ? AND confid = ?", comm.Id, c.ConfId); err != nil { + return err + } + + if refCount == 0 { + // We have to delete all the conference core data now. + _, err = tx.ExecContext(ctx, "DELETE FROM confs WHERE confid = ?", c.ConfId) + if err == nil { + _, err = tx.ExecContext(ctx, "DELETE FROM confalias WHERE confid = ?", c.ConfId) + } + } + if err != nil { + return err + } + + if err = tx.Commit(); err != nil { + return err + } + success = true + + if refCount == 0 { + // kick the conference out of the cache + conferenceCache.Remove(c.ConfId) + + // add an audit record + AmStoreAudit(AmNewAudit(AuditConferenceDelete, u.Uid, ipaddr, fmt.Sprintf("confid=%d", c.ConfId))) + + // set up a background job to purge the rest of the data + confid := c.ConfId + background.Submit(func(ctx context.Context) { + start := time.Now() + // Just dump the whole conference property cache + getConferencePropMutex.Lock() + conferencePropCache.Purge() + getConferencePropMutex.Unlock() + + // purge the conference data + err := backgroundPurgeConference(ctx, confid) + if err != nil { + log.Errorf("Conference purge(#%d) background job failed: %v", confid, err) + } + dur := time.Since(start) + log.Infof("Conference.Delete task completed in %v", dur) + }) + } + return nil +} + /* AmGetConference returns a conference given its ID. * Parameters: * ctx - Standard Go context value. diff --git a/database/topic.go b/database/topic.go index a4f15e3..40824b4 100644 --- a/database/topic.go +++ b/database/topic.go @@ -478,10 +478,13 @@ func (t *Topic) Delete(ctx context.Context, u *User, ipaddr string, background * // Spin off a background task to finish deleting this topic. myTopicId := t.TopicId background.Submit(func(ctx context.Context) { + start := time.Now() err := backgroundPurgeTopic(ctx, myTopicId) if err != nil { log.Errorf("backgroundTopicPurge FAILED with %v", err) } + dur := time.Since(start) + log.Infof("Topic.Delete task completed in %v", dur) }) return nil diff --git a/email/message.go b/email/message.go index 1781935..fadd792 100644 --- a/email/message.go +++ b/email/message.go @@ -126,10 +126,13 @@ func (m *amMessage) Send() { * The new Message. */ func AmNewEmailMessage(sender int32, ip string) Message { - rc := freeMessages.Get().(*amMessage) - if rc == nil { + var rc *amMessage + tmp := freeMessages.Get() + if tmp == nil { rc = &amMessage{to: make([]string, 0), cc: make([]string, 0), bcc: make([]string, 0), headers: make(map[string]string), vars: make(jet.VarMap)} + } else { + rc = tmp.(*amMessage) } rc.uid = sender rc.ip = ip diff --git a/main.go b/main.go index 8cdb6ac..732c0d6 100644 --- a/main.go +++ b/main.go @@ -118,8 +118,10 @@ func setupEcho() *echo.Echo { // conference group commGroup.GET("/create_conf", ui.AmWrap(CreateConferenceForm)) commGroup.POST("/create_conf", ui.AmWrap(CreateConference)) - commGroup.GET("/manage_conf", ui.AmWrap(ManageConferenceList)) + commGroup.GET("/manage_conf", ui.AmWrap(ManageConferenceList), ui.ValidateConference) + commGroup.GET("/manage_conf/del/:confid", ui.AmWrap(ManageDeleteConference), ui.ValidateConference, ui.SetConference) commGroup.GET("/conf", ui.AmWrap(Conferences), ui.ValidateConference) + confGroup := commGroup.Group("/conf/:confid", ui.ValidateConference, ui.SetConference) confGroup.GET("", ui.AmWrap(Topics)) confGroup.GET("/new_topic", ui.AmWrap(NewTopicForm)) @@ -145,6 +147,7 @@ func setupEcho() *echo.Echo { confGroup.GET("/invite", ui.AmWrap(InviteToConference)) confGroup.GET("/r/:topic", ui.AmWrap(ReadPosts), ui.SetTopic) confGroup.POST("/r/:topic", ui.AmWrap(PostInTopic), ui.SetTopic) + opsGroup := confGroup.Group("/op/:topic", ui.SetTopic) opsGroup.GET("/find", ui.AmWrap(FindPostsPageTopic)) opsGroup.POST("/find", ui.AmWrap(FindPostsTopic)) diff --git a/ui/amcontext.go b/ui/amcontext.go index cbbfe56..5e2ecae 100644 --- a/ui/amcontext.go +++ b/ui/amcontext.go @@ -524,14 +524,17 @@ var amContextRecycleBin chan *amContext * Standard Go error status. */ func newContext(ctxt echo.Context) (*amContext, error) { - rc := freeContext.Get().(*amContext) - if rc == nil { + var rc *amContext + tmp := freeContext.Get() + if tmp == nil { rc = &amContext{ rendervars: make(jet.VarMap), frameTitle: "", frameMeta: make(map[int]map[string]string), outputType: "", } + } else { + rc = tmp.(*amContext) } var err error diff --git a/ui/messagedefs.yaml b/ui/messagedefs.yaml index 71f38f9..5b8c3eb 100644 --- a/ui/messagedefs.yaml +++ b/ui/messagedefs.yaml @@ -55,3 +55,29 @@ messagedefs: tone: "green" icon: "✗" text: "No, Cancel" + - id: "deleteConf" + title: "Delete Conference" + tone: "red" + destructive: true + message: "You are about to delete a conference!" + warningIcon: "💣" + warningLines: + - text: "Warning: This action cannot be undone!" + bold: true + - text: "Deleting this conference will permanently remove it and all its topics from the system." + bold: false + - text: "Hundreds or even thousands of conference posts will be lost forever." + bold: false + buttons: + - id: "yes" + link: "placeholder" + confirm: true + tone: "red" + icon: "✓" + text: "Yes, Delete It" + - id: "no" + link: "placeholder" + confirm: false + tone: "green" + icon: "✗" + text: "No, Cancel" diff --git a/ui/views/manage_conflist.jet b/ui/views/manage_conflist.jet index 3aff616..8e611bb 100644 --- a/ui/views/manage_conflist.jet +++ b/ui/views/manage_conflist.jet @@ -70,7 +70,7 @@ {{ end }} -