diff --git a/README.md b/README.md index 8fbd1e3..5903905 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,156 @@ -iw -== +Wiki. Kind of. +============== + +Handles more like a client application, replication, authenticaton and acl systems. Target group is organization internal usage, writing notes and such. + +Features: +* GSSAPI/Kerberos authentication +* LDAP backend for user & group information +* Three level ACL system for content, with inheritation support +* Simple macros (TOC, include children pages) +* ExtJS based user interface +* Non-destructive versioning data model +* Replication to other instances + +[Mandatory screenshot][1] + +Installation +============ + +Prerequisities +* PostgreSQL +* Working Kerberos setup +* LDAP server +* Golang +* Memcached recommended + +Starting up +----------- + +This should pull the required packages + +``` +go get github.com/mikkolehtisalo/iw +``` + +Change app.secret from app.conf + +``` +app.secret=OWd2Zg7A5wWomHFWkMNIwGNvS7qCAbGY8NKrSADg50bAaSU4hXemJTslFVV3Ah3Q +``` + +Database +-------- + +Create the user, database, and import the schema + +``` +createuser wiki +createdb -U wiki --owner=wiki wiki +psql wiki wiki +\i db/schema.sql +``` + +Then review app.conf + +``` +# DB +db.name=wiki +db.user=wiki +db.password=password +``` + +LDAP +---- + +Create bind account, and add photo;binary for users' avatar images. + +Review app.conf, and fill up the LDAP server information, filters and the attributes +``` +# ldap +ldap.server=freeipa.localdomain +ldap.port=389 +ldap.user_base=cn=users,cn=accounts,dc=localdomain +ldap.user_filter=(&(uid=*)(objectClass=inetUser)) +ldap.user_uid_attr=uid +ldap.user_cn_attr=cn +ldap.user_photo_attr=photo;binary +ldap.user_group_attr=memberOf +ldap.group_filter=(&(cn=*)(objectClass=groupOfNames)) +ldap.group_cn_attr=cn +ldap.group_dn_attr=dn +ldap.user=uid=admin,cn=users,cn=accounts,dc=localdomain +ldap.passwd=password +ldap.group_base=cn=groups,cn=accounts,dc=localdomain +ldap.group_regexp=cn=([^,]+) +``` + +Kerberos +-------- + +Make sure /etc/krb5.conf is sane, for example + +``` +[logging] + default = FILE:/var/log/krb5libs.log + kdc = FILE:/var/log/krb5kdc.log + admin_server = FILE:/var/log/kadmind.log + +[libdefaults] + default_realm = LOCALDOMAIN + dns_lookup_realm = false + dns_lookup_kdc = true + rdns = false + ticket_lifetime = 24h + forwardable = yes + allow_weak_crypto = false + +[realms] + LOCALDOMAIN = { + kdc = freeipa.localdomain:88 + master_kdc = freeipa.localdomain:88 + admin_server = freeipa.localdomain:749 + default_domain = localdomain + pkinit_anchors = FILE:/etc/ipa/ca.crt +} + +[domain_realm] + .localdomain = LOCALDOMAIN + localdomain = LOCALDOMAIN + +[dbmodules] + LOCALDOMAIN = { + db_library = ipadb.so + } + +``` + +Create the keytab + +``` +# Create the principal on kdc +kadmin +addprinc -randkey HTTP/dev.localdomain@LOCALDOMAIN + +# Add it to the server's /etc/krb5.keytab +kadmin +ktadd HTTP/dev.localdomain@LOCALDOMAIN +``` + +Replication +----------- + +Replication directory contains client and server. They are very KISS, but working. Build them. + +Accompany with server/client.json file for settings. Create the mentioned CA certificate, and certificates for all servers. Configure valid peers, and other settings. Start the servers and clients. + +All logging goes to syslog. + +The good and the bad +==================== + +Most parts are pretty solid. Would need test(s/ing) to get rid of some corner case bugs, and proper documentation. + +It's not a ready product, but wouldn't take much to polish it. + +[1]:http://screenshot.org/ diff --git a/app/controllers/activity.go b/app/controllers/activity.go new file mode 100644 index 0000000..a7a963a --- /dev/null +++ b/app/controllers/activity.go @@ -0,0 +1,23 @@ +package controllers + +import ( + "github.com/revel/revel" + "iw/app/models" + "github.com/mikkolehtisalo/revel/acl" +) + +type Activities struct { + *revel.Controller +} + +// READ +func (a Activities) Read() revel.Result { + user := a.Session["username"] + revel.TRACE.Printf("Activities Read() user: %+v", user) + + activities := models.GetActivities() + filtered := acl.Filter(a.Args, []string{"read", "admin","write"}, activities, false) + + revel.TRACE.Printf("Activities Read() returning: %+v", filtered) + return a.RenderJson(filtered) +} diff --git a/app/controllers/app.go b/app/controllers/app.go new file mode 100644 index 0000000..e76d76b --- /dev/null +++ b/app/controllers/app.go @@ -0,0 +1,11 @@ +package controllers + +import "github.com/revel/revel" + +type App struct { + *revel.Controller +} + +func (c App) Index() revel.Result { + return c.Render() +} diff --git a/app/controllers/attachment.go b/app/controllers/attachment.go new file mode 100644 index 0000000..4531762 --- /dev/null +++ b/app/controllers/attachment.go @@ -0,0 +1,148 @@ +package controllers + +import ( + "github.com/revel/revel" + . "github.com/mikkolehtisalo/revel/common" + "iw/app/models" + "encoding/json" + "github.com/mikkolehtisalo/revel/acl" + "encoding/base64" + //"fmt" + "time" + "bytes" +) + +type Attachments struct { + *revel.Controller +} + +// CREATE +func (a Attachments) Create(wiki string, attachment string) revel.Result { + revel.TRACE.Printf("Attachments Create() wiki: %+v, attachment: %+v", wiki, attachment) + + // Make sure the ids at least look like one + if (!IsUUID(wiki)) || (!IsUUID(attachment)) { + revel.ERROR.Printf("Garbage attachment %+v/%+v received from %+v", wiki, attachment, a.Session["username"]) + return a.RenderText("{\"success\":false}") + } + + // Make sure the attachment doesn't pre-exist + exists_test := models.GetAttachment(wiki, attachment) + if exists_test.Wiki_id == wiki && exists_test.Attachment_id == attachment { + revel.ERROR.Printf("Attempt to rewrite pre-existing attachment %+v/%+v by user %+v", wiki, attachment, a.Session["username"]) + return a.RenderText("{\"success\":false}") + } + + // Make sure the wiki exists! + wiki_exists := models.GetWiki(wiki) + if wiki_exists.Wiki_id != wiki { + revel.ERROR.Printf("Attempt to add attachment to non-existent wiki: %+v/%+v", wiki, attachment) + return a.RenderText("{\"success\":false}") + } + + // Decode attachment from json input + var new_att models.Attachment + decoder := json.NewDecoder(a.Request.Body) + err := decoder.Decode(&new_att) + if err != nil { + revel.ERROR.Printf("Unable to parse attachment %+v/%+v: %+v", wiki, attachment, err) + return a.RenderText("{\"success\":false}") + } + + // ID fields must match! + if (new_att.Wiki_id != wiki) || (new_att.Attachment_id != attachment) { + revel.ERROR.Printf("Attachment id mismatch %+v/%+v != %+v/%+v from user %+v", wiki, attachment, new_att.Wiki_id, new_att.Attachment_id, a.Session["username"]) + return a.RenderText("{\"success\":false}") + } + + // Validate fields + new_att.Validate(a.Validation) + if a.Validation.HasErrors() { + revel.ERROR.Printf("Validation errors: %+v", a.Validation.ErrorMap()) + return a.RenderText("{\"success\":false}") + } + + // Make sure the user has rights to create attachment + filtered := acl.Filter(a.Args, []string{"admin","write"}, []models.Attachment{new_att}, true) + if len(filtered) != 1 { + revel.ERROR.Printf("Attempt to create attachment %+v/%+v without access rights: %+v, user: %+v", wiki, attachment, new_att.Filename, a.Session["username"]) + return a.RenderText("{\"success\":false}") + } + + new_att.Create_user = a.Session["username"] + new_att.Status = "ACTIVE" + new_att.Save(true) + + return a.RenderText("{\"success\":true}") +} + +// READ +func (a Attachments) Read(wiki string) revel.Result { + revel.TRACE.Printf("Attachments Read(): %+v", wiki) + + // Make sure the id looks like one + if (!IsUUID(wiki)) { + revel.ERROR.Printf("Garbage wiki %+v received from %+v", wiki, a.Session["username"]) + return a.RenderText("{\"success\":false}") + } + + attachments := models.GetAttachments(wiki) + // Filter by acls... + filtered := acl.Filter(a.Args, []string{"read", "write", "admin"}, attachments, true) + + revel.TRACE.Printf("Attachments Read() returning %+v", filtered) + return a.RenderJson(filtered) +} + +// Serve direct links! +func (a Attachments) Serve(wiki string, attachment string) revel.Result { + revel.TRACE.Printf("Attachments Serve(): %+v, attachment: %+v", wiki, attachment) + + // Make sure the ids at least look like one + if (!IsUUID(wiki)) || (!IsUUID(attachment)) { + revel.ERROR.Printf("Garbage attachment %+v/%+v received from %+v", wiki, attachment, a.Session["username"]) + return a.RenderText("{\"success\":false}") + } + + att := models.GetAttachment(wiki, attachment) + + // Make sure the user has rights to read the attachment + filtered := acl.Filter(a.Args, []string{"read", "admin","write"}, []models.Attachment{att}, true) + if len(filtered) != 1 { + revel.ERROR.Printf("Attempt to read attachment %+v/%+v without access rights: %+v, user: %+v", wiki, attachment, att.Filename, a.Session["username"]) + return a.RenderText("{\"success\":false}") + } + + data, err := base64.StdEncoding.DecodeString(att.Attachment) + if err != nil { + revel.ERROR.Printf("Serve(): Unable to base64 decode attachment! %+v", err) + } + + return a.RenderBinary(bytes.NewReader(data), att.Filename, "inline", time.Now()) +} + +// DELETE +func (a Attachments) Delete(wiki string, attachment string) revel.Result { + revel.TRACE.Printf("Attachments Delete(): %+v, attachment: %+v", wiki, attachment) + + // Make sure the ids at least look like one + if (!IsUUID(wiki)) || (!IsUUID(attachment)) { + revel.ERROR.Printf("Garbage attachment %+v/%+v received from %+v", wiki, attachment, a.Session["username"]) + return a.RenderText("{\"success\":false}") + } + + att := models.GetAttachment(wiki, attachment) + + // Make sure the user has rights to delete the attachment + filtered := acl.Filter(a.Args, []string{"admin","write"}, []models.Attachment{att}, true) + if len(filtered) != 1 { + revel.ERROR.Printf("Attempt to delete attachment %+v/%+v without access rights: %+v, user: %+v", wiki, attachment, att.Filename, a.Session["username"]) + return a.RenderText("{\"success\":false}") + } + + att.Create_user = a.Session["username"] + att.Status = "DELETED" + att.Save(true) + + return a.RenderText("{\"success\":true}") +} \ No newline at end of file diff --git a/app/controllers/contentfield.go b/app/controllers/contentfield.go new file mode 100644 index 0000000..d149da4 --- /dev/null +++ b/app/controllers/contentfield.go @@ -0,0 +1,91 @@ +package controllers + +import ( + "github.com/revel/revel" + . "github.com/mikkolehtisalo/revel/common" + "iw/app/models" + "github.com/mikkolehtisalo/revel/acl" + "encoding/json" +) + +type ContentFields struct { + *revel.Controller +} + +// READ +func (c ContentFields) Read(wiki string, page string) revel.Result { + revel.TRACE.Printf("ContentFields Read() wiki: %+v, page: %+v", wiki, page) + + // Make sure the ids at least look like one + if (!IsUUID(wiki)) || (!IsUUID(page)) { + revel.ERROR.Printf("Garbage contentfield %+v/%+v received from %+v", wiki, page, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Get the contentfield + cf := models.GetContent(wiki, page, c.Args) + // Check the ACL + filtered := acl.Filter(c.Args, []string{"read", "write", "admin"}, []models.ContentField{cf}, true) + + if len(filtered) < 1 { + revel.ERROR.Printf("Unable to read content field! wiki: %+v, page: %+v, user: %+v", wiki, page, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + return c.RenderJson(filtered[0]) +} + +// UPDATE +func (c ContentFields) Update(wiki string, page string) revel.Result { + + revel.TRACE.Printf("ContentFields Update() wiki: %+v, page: %+v", wiki, page) + + // Make sure the ids at least look like one + if (!IsUUID(wiki)) || (!IsUUID(page)) { + revel.ERROR.Printf("Garbage contentfield %+v/%+v received from %+v", wiki, page, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Make sure the contentfield pre-exists + old_cf := models.GetContent(wiki, page, c.Args) + if old_cf.Wiki_id != wiki && old_cf.Contentfield_id != page { + revel.ERROR.Printf("Attempt to update non-existing contentfield %+v/%+v by user %+v", wiki, page, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Decode contentfield from json input + var new_cf models.ContentField + decoder := json.NewDecoder(c.Request.Body) + err := decoder.Decode(&new_cf) + if err != nil { + revel.ERROR.Printf("Unable to parse contentfield %+v/%+v: %+v", wiki, page, err) + return c.RenderText("{\"success\":false}") + } + + // ID fields must match! + if (new_cf.Wiki_id != wiki) || (new_cf.Contentfield_id != page) { + revel.ERROR.Printf("Contentfield id mismatch %+v/%+v != %+v/%+v from user %+v", wiki, page, new_cf.Wiki_id, new_cf.Contentfield_id, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Validate fields + new_cf.Validate(c.Validation) + if c.Validation.HasErrors() { + revel.ERROR.Printf("Validation errors: %+v", c.Validation.ErrorMap()) + return c.RenderText("{\"success\":false}") + } + + // Make sure the user has rights to the page + filtered := acl.Filter(c.Args, []string{"admin","write"}, []models.ContentField{new_cf}, true) + if len(filtered) != 1 { + revel.ERROR.Printf("Attempt to update contentfield %+v/%+v without access rights! user: %+v", wiki, page, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + new_cf.Create_user = c.Session["username"] + new_cf.Status = "ACTIVE" + new_cf.Save(true) + + return c.RenderText("{\"success\":true}") +} + diff --git a/app/controllers/favoritewiki.go b/app/controllers/favoritewiki.go new file mode 100644 index 0000000..1203b9b --- /dev/null +++ b/app/controllers/favoritewiki.go @@ -0,0 +1,69 @@ +package controllers + +import ( + "github.com/revel/revel" + "iw/app/models" + "github.com/mikkolehtisalo/revel/common" +) + +type FavoriteWikis struct { + *revel.Controller +} + +// CREATE +func (c FavoriteWikis) Create(wiki string) revel.Result { + revel.TRACE.Printf("FavoriteWikis Create(): %+v", wiki) + + // Make sure the id at least looks like one + if !common.IsUUID(wiki) { + revel.ERROR.Printf("Garbage favorite %+v create received from %+v", wiki, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Already exists? + if models.IsFavoriteWiki(wiki, c.Session["username"]) { + revel.ERROR.Printf("Wiki %+v already favorite!", wiki) + return c.RenderText("{\"success\":false}") + } + + fav := models.FavoriteWiki{} + fav.Username = c.Session["username"] + fav.Wiki_id = wiki + fav.Status = "ACTIVE" + fav.Save() + + return c.RenderText("{\"success\":true}") +} + +// READ +func (c FavoriteWikis) Read() revel.Result { + revel.TRACE.Printf("FavoriteWikis List()") + return c.RenderJson(models.ListFavoriteWikis(c.Session["username"])) +} + + +// DELETE +func (c FavoriteWikis) Delete(wiki string) revel.Result { + revel.TRACE.Printf("FavoriteWikis Delete(): %+v", wiki) + + // Make sure the id at least looks like one + if !common.IsUUID(wiki) { + revel.ERROR.Printf("Garbage favorite %+v delete received from %+v", wiki, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Already exists? + if !models.IsFavoriteWiki(wiki, c.Session["username"]) { + revel.ERROR.Printf("Wiki %+v not favorite!", wiki) + return c.RenderText("{\"success\":false}") + } + + fav := models.FavoriteWiki{} + fav.Username = c.Session["username"] + fav.Wiki_id = wiki + fav.Status = "DELETED" + fav.Save() + + return c.RenderText("{\"success\":true}") +} + diff --git a/app/controllers/lock.go b/app/controllers/lock.go new file mode 100644 index 0000000..675144c --- /dev/null +++ b/app/controllers/lock.go @@ -0,0 +1,87 @@ +package controllers + +import ( + "github.com/revel/revel" + "iw/app/models" + //"encoding/json" + //"strings" + //"regexp" + . "github.com/mikkolehtisalo/revel/common" + "github.com/mikkolehtisalo/revel/ldapuserdetails" + //"github.com/mikkolehtisalo/revel/acl" +) + +type Locks struct { + *revel.Controller +} + +// CREATE +func (l Locks) Create(wiki string, target string) revel.Result { + revel.TRACE.Printf("Locks Create() wiki: %+v, target: %+v", wiki, target) + + // Make sure the ids at least look like one + if (!IsUUID(wiki)) || (!IsUUID(target)) { + revel.ERROR.Printf("Garbage lock %+v/%+v received from %+v", wiki, target, l.Session["username"]) + return l.RenderText("{\"success\":false}") + } + + // Make sure the lock doesn't pre-exist + exists_test := models.GetLock(wiki, target) + if exists_test.Wiki_id == wiki && exists_test.Target_id == target { + revel.ERROR.Printf("Attempt to rewrite pre-existing lock %+v/%+v by user %+v", wiki, target, l.Session["username"]) + return l.RenderText("{\"success\":false}") + } + + // Make sure the wiki exists! + wiki_exists := models.GetWiki(wiki) + if wiki_exists.Wiki_id != wiki { + revel.ERROR.Printf("Attempt to add lock to non-existent wiki: %+v/%+v", wiki, target) + return l.RenderText("{\"success\":false}") + } + + // We could check for rights for page, but as obeying lock is non-forcing convenience function, meh + + lock := models.Lock{} + lock.Target_id = target + lock.Wiki_id = wiki + lock.Username = l.Session["username"] + dets := l.Args["user_details"].(ldapuserdetails.User_details) + lock.Realname = dets.Visiblename + lock.Save() + + return l.RenderText("{\"success\":true}") +} + +// READ +func (l Locks) Read(wiki string, target string) revel.Result { + revel.TRACE.Printf("Locks Read() wiki: %+v, target: %+v", wiki, target) + + // Make sure the ids at least look like one + if (!IsUUID(wiki)) || (!IsUUID(target)) { + revel.ERROR.Printf("Garbage lock %+v/%+v received from %+v", wiki, target, l.Session["username"]) + return l.RenderText("{\"success\":false}") + } + + // Get the lock + lock := models.GetLock(wiki, target) + + return l.RenderJson(lock) +} + +// DELETE +func (l Locks) Delete(wiki string, target string) revel.Result { + revel.TRACE.Printf("Locks Delete() wiki: %+v, target: %+v", wiki, target) + + // Make sure the ids at least look like one + if (!IsUUID(wiki)) || (!IsUUID(target)) { + revel.ERROR.Printf("Garbage lock %+v/%+v received from %+v", wiki, target, l.Session["username"]) + return l.RenderText("{\"success\":false}") + } + + // Get the lock + lock := models.GetLock(wiki, target) + lock.Delete() + + return l.RenderText("{\"success\":true}") + +} \ No newline at end of file diff --git a/app/controllers/page.go b/app/controllers/page.go new file mode 100644 index 0000000..01a6052 --- /dev/null +++ b/app/controllers/page.go @@ -0,0 +1,194 @@ +package controllers + +import ( + "github.com/revel/revel" + "iw/app/models" + "encoding/json" + "strings" + "regexp" + . "github.com/mikkolehtisalo/revel/common" + "github.com/mikkolehtisalo/revel/acl" +) + +type Pages struct { + *revel.Controller +} + +// CREATE +func (c Pages) Create(wiki string, page string) revel.Result { + revel.TRACE.Printf("Pages Create() wiki: %+v, page: %+v", wiki, page) + + // Make sure the ids at least look like one + if (!IsUUID(wiki)) || (!IsUUID(page)) { + revel.ERROR.Printf("Garbage page %+v/%+v received from %+v", wiki, page, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Make sure the page doesn't pre-exist + exists_test := models.GetPage(wiki, page) + if exists_test.Wiki_id == wiki && exists_test.Page_id == page { + revel.ERROR.Printf("Attempt to rewrite pre-existing page %+v/%+v by user %+v", wiki, page, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Make sure the wiki exists! + wiki_exists := models.GetWiki(wiki) + if wiki_exists.Wiki_id != wiki { + revel.ERROR.Printf("Attempt to add page to non-existent wiki: %+v/%+v", wiki, page) + return c.RenderText("{\"success\":false}") + } + + // Decode page from json input + var new_page models.Page + decoder := json.NewDecoder(c.Request.Body) + err := decoder.Decode(&new_page) + if err != nil { + revel.ERROR.Printf("Unable to parse page %+v/%+v: %+v", wiki, page, err) + return c.RenderText("{\"success\":false}") + } + + // ID fields must match! + if (new_page.Wiki_id != wiki) || (new_page.Page_id != page) { + revel.ERROR.Printf("Page id mismatch %+v/%+v != %+v/%+v from user %+v", wiki, page, new_page.Wiki_id, new_page.Page_id, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Validate fields + new_page.Validate(c.Validation) + if c.Validation.HasErrors() { + revel.ERROR.Printf("Validation errors: %+v", c.Validation.ErrorMap()) + return c.RenderText("{\"success\":false}") + } + + // Make sure the user has rights to create page here + filtered := acl.Filter(c.Args, []string{"admin","write"}, []models.Page{new_page}, true) + if len(filtered) != 1 { + revel.ERROR.Printf("Attempt to create page %+v/%+v without access rights: %+v, user: %+v", wiki, page, new_page.Title, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + new_page.Create_user = c.Session["username"] + new_page.Status = "ACTIVE" + new_page.Save(true) + + // Create corresponding contentfield! + cf := models.ContentField{} + cf.Contentfield_id = new_page.Page_id + cf.Wiki_id = new_page.Wiki_id + cf.Content = "
Add new content here!
" + cf.Status = "ACTIVE" + cf.Create_user = c.Session["username"] + cf.Save(false) + + return c.RenderText("{\"success\":true}") +} + +// READ +func (c Pages) Read() revel.Result { + node := c.Params.Values.Get("node") + revel.TRACE.Printf("Pages Read() node: %+v", node) + + // Check that the input parameter is at least roughly valid + re := regexp.MustCompile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})*") + if !re.MatchString(node) { + revel.TRACE.Printf("Pages List() invalid node: %+v", node) + return c.RenderText("{\"success\":false}") + } + + // This won't blow up thanks to previous + wiki := strings.Split(node, "/")[0] + page := strings.Split(node, "/")[1] + + // Get the list of pages + pages := models.ListPages(wiki, page) + // Filter using the ACL system + filtered := acl.Filter(c.Args, []string{"read","write","admin"}, pages, true) + revel.TRACE.Printf("Returning: %+v", filtered) + + return c.RenderJson(filtered) +} + +// UPDATE +// Not DRY, but this logic might still change: revise later... +func (c Pages) Update(wiki string, page string) revel.Result { + revel.TRACE.Printf("Pages Update() wiki: %+v, page: %+v", wiki, page) + + // Make sure the ids at least look like one + if (!IsUUID(wiki)) || (!IsUUID(page)) { + revel.ERROR.Printf("Garbage page %+v/%+v received from %+v", wiki, page, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Make sure the page pre-exists + // Also the wiki probably exists if the page exists + old_page := models.GetPage(wiki, page) + if old_page.Wiki_id != wiki && old_page.Page_id != page { + revel.ERROR.Printf("Attempt to update non-existing page %+v/%+v by user %+v", wiki, page, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Decode page from json input + var new_page models.Page + decoder := json.NewDecoder(c.Request.Body) + err := decoder.Decode(&new_page) + if err != nil { + revel.ERROR.Printf("Unable to parse page %+v/%+v: %+v", wiki, page, err) + return c.RenderText("{\"success\":false}") + } + + // ID fields must match! + if (new_page.Wiki_id != wiki) || (new_page.Page_id != page) { + revel.ERROR.Printf("Page id mismatch %+v/%+v != %+v/%+v from user %+v", wiki, page, new_page.Wiki_id, new_page.Page_id, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Validate fields + new_page.Validate(c.Validation) + if c.Validation.HasErrors() { + revel.ERROR.Printf("Validation errors: %+v", c.Validation.ErrorMap()) + return c.RenderText("{\"success\":false}") + } + + // Make sure the user has rights to the page + filtered := acl.Filter(c.Args, []string{"admin","write"}, []models.Page{new_page}, true) + if len(filtered) != 1 { + revel.ERROR.Printf("Attempt to create page %+v/%+v without access rights: %+v, user: %+v", wiki, page, new_page.Title, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + new_page.Create_user = c.Session["username"] + new_page.Status = "ACTIVE" + new_page.Save(true) + + return c.RenderText("{\"success\":true}") +} + + +// DELETE +func (c Pages) Delete(wiki string, page string) revel.Result { + revel.TRACE.Printf("Pages Delete() wiki: %+v, page: %+v", wiki, page) + + // If the parameters don't seem valid, bail out + if (!IsUUID(wiki)) || (!IsUUID(page)) { + revel.ERROR.Printf("Pages Delete() invalid IDs! wiki: %+v, page: %+v", wiki, page) + return c.RenderText("{\"success\":false}") + } + + p := models.GetPage(wiki, page) + if p.Wiki_id == "" { + revel.ERROR.Printf("Pages Delete() Attempt to delete non-existing page! user: %+v wiki %+v page %+v", c.Session["username"], wiki, page) + return c.RenderText("{\"success\":false}") + } + + // Write or admin is enough to delete + filtered := acl.Filter(c.Args, []string{"write", "admin"}, []models.Page{p}, true) + if len(filtered) < 1 { + revel.ERROR.Printf("Pages Delete() insufficient rights! user: %+v wiki: %+v, page: %+v", c.Session["username"], wiki, page) + return c.RenderText("{\"success\":false}") + } + + // Also handles children and contentfields + models.DeletePage(wiki, page, c.Session["username"], true) + + return c.RenderText("{\"success\":true}") +} \ No newline at end of file diff --git a/app/controllers/useravatar.go b/app/controllers/useravatar.go new file mode 100644 index 0000000..b0fbbac --- /dev/null +++ b/app/controllers/useravatar.go @@ -0,0 +1,40 @@ +package controllers + +import ( + "github.com/revel/revel" + "bytes" + "github.com/mikkolehtisalo/revel/ldapuserdetails" + "time" + "regexp" + "github.com/revel/revel/cache" + "fmt" +) + +type UserAvatars struct { + *revel.Controller +} + +func (u UserAvatars) Read(user string) revel.Result { + revel.TRACE.Printf("UserAvatars Read() user:%+v", user) + var avatar []byte + + err := cache.Get(fmt.Sprintf("avatar:%s", user), &avatar) + + if err != nil { + // Not in cache, generate + re := regexp.MustCompile("^(\\w*)\\.(jpeg|jpg|png)") + + if !re.MatchString(user) { + revel.TRACE.Printf("UserAvatars Read() invalid user: %+v", user) + return u.RenderText("{\"success\":false}") + } + + username := re.FindStringSubmatch(user)[1] + dets := ldapuserdetails.Get_user_details(username) + avatar = dets.Photo + go cache.Set(fmt.Sprintf("avatar:%s", user), avatar, cache.DEFAULT) + } + + return u.RenderBinary(bytes.NewReader(avatar), user, "inline", time.Now()) + +} diff --git a/app/controllers/usergroupsearch.go b/app/controllers/usergroupsearch.go new file mode 100644 index 0000000..64bcee0 --- /dev/null +++ b/app/controllers/usergroupsearch.go @@ -0,0 +1,15 @@ +package controllers + +import "github.com/revel/revel" +import "iw/app/models" +//import "fmt" + +type UserGroupSearch struct { + *revel.Controller +} + +func (c UserGroupSearch) List() revel.Result { + query := c.Params.Query["query"][0] + return c.RenderJson(models.ListUserGroupSearchItems(query)) +} + diff --git a/app/controllers/wiki.go b/app/controllers/wiki.go new file mode 100644 index 0000000..29a1c28 --- /dev/null +++ b/app/controllers/wiki.go @@ -0,0 +1,147 @@ +package controllers + +import ( + "github.com/revel/revel" + "iw/app/models" + "encoding/json" + . "github.com/mikkolehtisalo/revel/common" + "github.com/mikkolehtisalo/revel/acl" +) + +type Wikis struct { + *revel.Controller +} + +// CREATE +func (c Wikis) Create(wiki string) revel.Result { + revel.TRACE.Printf("Wikis Create(): %+v", wiki) + + // Make sure the id at least looks like one + if !IsUUID(wiki) { + revel.ERROR.Printf("Garbage wiki %+v received from %+v", wiki, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Make sure the wiki doesn't pre-exist + exists_test := models.GetWiki(wiki) + if exists_test.Wiki_id == wiki { + revel.ERROR.Printf("Attempt to rewrite pre-existing wiki %+v by user %+v", wiki, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Decode wiki from input json + var new_wiki models.Wiki + decoder := json.NewDecoder(c.Request.Body) + err := decoder.Decode(&new_wiki) + if err != nil { + revel.ERROR.Printf("Unable to parse wiki %+v: %+v", wiki, err) + return c.RenderText("{\"success\":false}") + } + + // ID fields must match! + if new_wiki.Wiki_id != wiki { + revel.ERROR.Printf("Wiki id mismatch %+v != %+v", new_wiki.Wiki_id, wiki) + return c.RenderText("{\"success\":false}") + } + + // Validate fields + new_wiki.Validate(c.Validation) + if c.Validation.HasErrors() { + revel.ERROR.Printf("Validation errors parsing wiki %+v: %+v", wiki, c.Validation.ErrorMap()) + return c.RenderText("{\"success\":false}") + } + + // Make user the author has admin access right by default + AddUserToACLList(c.Session["username"], &new_wiki.Adminacl) + + // Save the wiki + new_wiki.Create_user = c.Session["username"] + new_wiki.Status = "ACTIVE" + new_wiki.Save(true) + + revel.INFO.Printf("User %+v created wiki %+v: %+v", c.Session["username"], new_wiki.Wiki_id, new_wiki.Title ) + return c.RenderText("{\"success\":true}") +} + +// READ +func (c Wikis) Read() revel.Result { + revel.TRACE.Printf("Wikis Read()") + + wikis := models.ListWikis() + filtered := acl.Filter(c.Args, []string{"read","write","admin"}, wikis, false) + + revel.TRACE.Printf("Wikis Read() returning: %+v", filtered) + return c.RenderJson(filtered) +} + + +// UPDATE +func (c Wikis) Update(wiki string) revel.Result { + revel.TRACE.Printf("Wikis Update(): %s", wiki) + + // Make sure the id at least looks like one + if !IsUUID(wiki) { + revel.ERROR.Printf("Garbage wiki %+v received from %+v", wiki, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Make sure the wiki exists + exists_test := models.GetWiki(wiki) + if exists_test.Wiki_id != wiki { + revel.ERROR.Printf("Attempt to update non-existing wiki %+v by user %+v", wiki, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + // Decode the wiki from input + var new_wiki models.Wiki + decoder := json.NewDecoder(c.Request.Body) + err := decoder.Decode(&new_wiki) + if err != nil { + revel.ERROR.Printf("Unable to parse wiki %+v: %+v", wiki, err) + return c.RenderText("{\"success\":false}") + } + + // ID fields must match! + if new_wiki.Wiki_id != wiki { + revel.ERROR.Printf("Wiki id mismatch %+v != %+v", new_wiki.Wiki_id, wiki) + return c.RenderText("{\"success\":false}") + } + + // Validate fields + new_wiki.Validate(c.Validation) + if c.Validation.HasErrors() { + revel.ERROR.Printf("Validation errors: %+v", c.Validation.ErrorMap()) + return c.RenderText("{\"success\":false}") + } + + // Make sure the user has rights to modify the wiki + filtered := acl.Filter(c.Args, []string{"admin","write"}, []models.Wiki{exists_test}, false) + if len(filtered) != 1 { + revel.ERROR.Printf("Attempt to update wiki without access rights: %+v: %+v, user: %+v", exists_test.Wiki_id, exists_test.Title, c.Session["username"]) + return c.RenderText("{\"success\":false}") + } + + new_wiki.Status = "ACTIVE" + new_wiki.Create_user = c.Session["username"] + new_wiki.Save(true) + + revel.INFO.Printf("User %+v updated wiki %+v: %+v", c.Session["username"], new_wiki.Wiki_id, new_wiki.Title ) + + return c.RenderText("{\"success\":true}") +} + +// DELETE +func (c Wikis) Delete(wiki string) revel.Result { + revel.TRACE.Printf("Wikis Delete(): %s", wiki) + wi := models.GetWiki(wiki) + filtered := acl.Filter(c.Args, []string{"admin"}, []models.Wiki{wi}, false) + + // Delete everything that survived filtering + for _, w := range filtered { + // Will also do other house cleaning + w.(models.Wiki).Delete(c.Session["username"]) + revel.INFO.Printf("User %+v deleted wiki %+v: %+v", c.Session["username"], w.(models.Wiki).Wiki_id, w.(models.Wiki).Title) + } + + return c.RenderText("{\"success\":true}") +} diff --git a/app/init.go b/app/init.go new file mode 100644 index 0000000..f4ddbce --- /dev/null +++ b/app/init.go @@ -0,0 +1,45 @@ +package app + +import "github.com/revel/revel" +import "github.com/mikkolehtisalo/revel/cachesession" +import "github.com/mikkolehtisalo/revel/gssserver" +import "github.com/mikkolehtisalo/revel/ldapuserdetails" +import "github.com/cbonello/revel-csrf" + +func init() { + // Filters is the default set of global filters. + revel.Filters = []revel.Filter{ + revel.PanicFilter, // Recover from panics and display an error page instead. + revel.RouterFilter, // Use the routing table to select the right Action + revel.FilterConfiguringFilter, // A hook for adding or removing per-Action filters. + revel.ParamsFilter, // Parse parameters into Controller.Params. + cachesession.CacheSessionFilter, // Use cache based session implementation. + gssserver.GSSServerFilter, // GSSAPI authentication + ldapuserdetails.UserDetailsLoadFilter, // Load user details from LDAP + revel.FlashFilter, // Restore and write the flash cookie. + csrf.CSRFFilter, // CSRF prevention. + revel.ValidationFilter, // Restore kept validation errors and save new ones from cookie. + revel.I18nFilter, // Resolve the requested language + HeaderFilter, // Add some security based headers + revel.InterceptorFilter, // Run interceptors around the action. + revel.CompressFilter, // Compress the result. + revel.ActionInvoker, // Invoke the action. + } + + // register startup functions with OnAppStart + // ( order dependent ) + // revel.OnAppStart(InitDB()) + // revel.OnAppStart(FillCache()) +} + +// TODO turn this into revel.HeaderFilter +// should probably also have a filter for CSRF +// not sure if it can go in the same filter or not +var HeaderFilter = func(c *revel.Controller, fc []revel.Filter) { + // Add some common security headers + c.Response.Out.Header().Add("X-Frame-Options", "SAMEORIGIN") + c.Response.Out.Header().Add("X-XSS-Protection", "1; mode=block") + c.Response.Out.Header().Add("X-Content-Type-Options", "nosniff") + + fc[0](c, fc[1:]) // Execute the next filter stage. +} diff --git a/app/models/activity.go b/app/models/activity.go new file mode 100644 index 0000000..f01cce0 --- /dev/null +++ b/app/models/activity.go @@ -0,0 +1,245 @@ +package models + +import ( + "time" + "github.com/revel/revel" + "github.com/twinj/uuid" + "github.com/mikkolehtisalo/revel/ldapuserdetails" + "github.com/mikkolehtisalo/revel/acl" + "regexp" + "strings" +) + +type Activity struct { + Activity_id string + Timestamp time.Time + User_id string + User_name string + Activity_type string + Target_type string + Target_title string + Target_id string + MatchedPermissions []string + // For the "flattened" acls... + Readacl string + Writeacl string + Adminacl string +} + +// Oldest: 1 month +func GetActivities() []Activity { + revel.TRACE.Printf("GetActivities()") + activities := []Activity{} + db := get_db() + defer db.Close() + + err := db.Select(&activities, "select * from activities where timestamp > now() - interval '1 month' order by timestamp desc") + + if err != nil { + revel.ERROR.Printf("GetActivities() err: %+v", err) + } + + revel.TRACE.Printf("GetActivities() returning: %+v", activities) + return activities +} + +func GetActivity(activity_id string) Activity { + revel.TRACE.Printf("GetActivity() id: %+v", activity_id) + activities := []Activity{} + activity := Activity{} + db := get_db() + defer db.Close() + + err := db.Select(&activities, "select * from activities where activity_id=uuid_in($1)", activity_id) + + if err != nil { + revel.ERROR.Printf("Unable to get activity %+v: %+v", activity_id, err) + } + + if len(activities) > 0 { + activity = activities[0] + } + + revel.TRACE.Printf("GetActivity() returning: %+v", activity) + + return activity +} + +// Is there a similar activity within one minute range? +func ActivityExists(a Activity) bool { + exists := false + + activities := []Activity{} + db := get_db() + defer db.Close() + + err := db.Select(&activities, "select * from activities where $1 - timestamp < interval '1 minute' and user_id=$2 and activity_type=$3 and target_type=$4 and target_id=$5", + a.Timestamp, a.User_id, a.Activity_type, a.Target_type, a.Target_id) + + if err != nil { + revel.ERROR.Printf("ActivityExists() err: %+v", err) + } + + if len(activities) > 0 { + exists = true + } + + + return exists +} + +// Flatten the inherited ACL structure, for performance reasons +// If the ACLs of the parent items are edited, the ACLs of activity will not change.. +func update_acl(a *Activity) { + filterable := acl.Get_filterable([]Activity{*a}) + acls := acl.GetACLEntry("activity:" + a.Activity_id, filterable[0], true) + + // Remove the duplicates from previous (it will generate them) and flatten the result + var read, write, admin []string + for _, item := range acls.ACLs { + if item.Permission == "read" && item.Principal != "" { + read = append(read, item.Principal) + } + if item.Permission == "write" && item.Principal != "" { + write = append(write, item.Principal) + } + if item.Permission == "admin" && item.Principal != "" { + admin = append(admin, item.Principal) + } + } + + a.Readacl = strings.Join(read, ",") + a.Writeacl = strings.Join(write, ",") + a.Adminacl = strings.Join(admin, ",") +} + +// Flattens the ACL before saving +func (a *Activity) Save() { + revel.TRACE.Printf("Activity Save(): %+v", a) + + db := get_db() + defer db.Close() + + update_acl(a) + + _, err := db.Exec("insert into activities(activity_id, timestamp, user_id, user_name, activity_type, target_type, target_title, target_id, readacl, writeacl, adminacl) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", + a.Activity_id, a.Timestamp, a.User_id, a.User_name, a.Activity_type, a.Target_type, a.Target_title, a.Target_id, a.Readacl, a.Writeacl, a.Adminacl) + + if err != nil { + revel.ERROR.Printf("Activity Save(): error %+v", err) + } + +} + +func SaveActivity(target interface{}) { + revel.TRACE.Printf("target: %+v", target) + a := Activity{} + a.Activity_id = uuid.NewV4().String() + + switch target.(type) { + case *Wiki: + w := target.(*Wiki) + a.Timestamp = w.Modified + a.User_id = w.Create_user + dets := ldapuserdetails.Get_user_details(w.Create_user) + a.User_name = dets.Visiblename + a.Activity_type = w.Status + a.Target_type = "WIKI" + a.Target_title = w.Title + a.Target_id = w.Wiki_id + if !ActivityExists(a) { + a.Save() + } + case *Page: + p := target.(*Page) + a.Timestamp = p.Modified + a.User_id = p.Create_user + dets := ldapuserdetails.Get_user_details(p.Create_user) + a.User_name = dets.Visiblename + a.Activity_type = p.Status + a.Target_type = "PAGE" + a.Target_title = p.Title + + // We have to save the parent node for treeStore, or "" if it's on first level.... + parent := "" + if p.Depth > 0 { + split := strings.Split(p.Path, "/") + parent = split[len(split)-2] + } + a.Target_id = p.Wiki_id + "/" +parent + "/" + p.Page_id + + if !ActivityExists(a) { + a.Save() + } + case *Attachment: + x := target.(*Attachment) + a.Timestamp = x.Modified + a.User_id = x.Create_user + dets := ldapuserdetails.Get_user_details(x.Create_user) + a.User_name = dets.Visiblename + a.Activity_type = x.Status + a.Target_type = "ATTACHMENT" + a.Target_title = x.Filename + a.Target_id = x.Wiki_id + "/" + x.Attachment_id + if !ActivityExists(a) { + a.Save() + } + } +} + +// ACL stuff + +// Never inherit the parent! +func (a Activity) BuildACLInheritation() bool { + return true +} + +// Activity + activity_id +func (a Activity) BuildACLReference() string { + return "activity:" + a.Activity_id +} + +// No parent.. +func (a Activity) BuildACLParent() string { + return strings.ToLower(a.Target_type) + ":" + a.Target_id +} + +// Set the matched permissions to a variable +func (a Activity) SetMatched(permissions []string) interface{} { + a.MatchedPermissions = permissions + return a +} + +func (a Activity) BuildACLEntry(reference string) acl.ACLEntry { + revel.TRACE.Printf("BuildACLEntry() %+v", reference) + var entry acl.ACLEntry + + if reference != ("activity:"+a.Activity_id) { + + if strings.Index(reference, "page") == 0 { + // We are not working on this copy, get from database + re := regexp.MustCompile("page:([^/]*)/([^/]*)/(.*)") + m := re.FindStringSubmatch(reference) + wref := m[1] + pref := m[3] + pa := GetPage(wref, pref) + entry = entry_helper(pa.Readacl, pa.Writeacl, pa.Adminacl, reference, pa) + } + + if strings.Index(reference, "wiki") == 0 { + // This must be wiki! + re := regexp.MustCompile("wiki:(.*)") + ref := re.FindStringSubmatch(reference)[1] + wi := GetWiki(ref) + entry = entry_helper(wi.Readacl, wi.Writeacl, wi.Adminacl, reference, wi) + } + + } else { + // It's exactly the originating item! + entry = entry_helper(a.Readacl, a.Writeacl, a.Adminacl, reference, a) + } + + revel.TRACE.Printf("BuildACLEntry() returning %+v", entry) + + return entry +} diff --git a/app/models/attachment.go b/app/models/attachment.go new file mode 100644 index 0000000..8c6091e --- /dev/null +++ b/app/models/attachment.go @@ -0,0 +1,180 @@ +package models + +import ( + "time" + "github.com/revel/revel" + "github.com/mikkolehtisalo/revel/acl" + "regexp" + "strings" +) + +type Attachment struct { + Attachment_id string + Wiki_id string + // pg driver works probably better with BASE64 encoding instead of handling bytea hex string + //Attachment []byte `json:",omitempty"` + Attachment string + Mime string + Filename string + Create_user string + Modified time.Time + MatchedPermissions []string + Readacl string + Writeacl string + Adminacl string + Status string +} + +func (a *Attachment) Validate(v *revel.Validation) { + // Required fields + v.Required(a.Attachment_id) + v.Required(a.Wiki_id) + v.Required(a.Attachment) + v.Required(a.Filename) + v.Required(a.Modified) + + // Match against regexp patterns + v.Match(a.Wiki_id, regexp.MustCompile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")).Message("Wiki_id not UUID?") + v.Match(a.Attachment_id, regexp.MustCompile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")).Message("Wiki_id not UUID?") + + // Validate the Attachment field + re := regexp.MustCompile("^data:([^;]*);base64,(.*)$") + v.Match(a.Attachment, re).Message("Invalid attachment data!") + + // Fix the content & mime filed + if re.MatchString(a.Attachment) { + parts := re.FindStringSubmatch(a.Attachment) + a.Mime = parts[1] + a.Attachment = parts[2] + + } +} + +// Sets the modified timestamp +func (a *Attachment) Save(save_activity bool) { + revel.TRACE.Printf("Attachment Save(): %+v", a) + + db := get_db() + defer db.Close() + a.Modified = time.Now() + + _, err := db.Exec("insert into attachments(wiki_id, attachment_id, attachment, mime, filename, create_user, modified, status) values ($1, $2, $3, $4, $5, $6, $7, $8)", + a.Wiki_id, a.Attachment_id, a.Attachment, a.Mime, a.Filename, a.Create_user, a.Modified, a.Status) + + if err != nil { + revel.ERROR.Printf("Attachment Save(): error %+v", err) + } + + if save_activity { + SaveActivity(a) + } +} + +func GetAttachment(wiki string, attachment string) Attachment { + revel.TRACE.Printf("GetAttachment() wiki:%+v, attachment:%+v", wiki, attachment) + attachments := []Attachment{} + att := Attachment{} + db := get_db() + defer db.Close() + + err := db.Select(&attachments, "select * from attachments a1 where a1.wiki_id=uuid_in($1) and a1.status='ACTIVE' and a1.attachment_id=uuid_in($2) and not exists (select * from attachments a2 where a1.wiki_id=a2.wiki_id and a1.attachment_id=a2.attachment_id and a2.modified>a1.modified)", wiki, attachment) + + if err != nil { + revel.ERROR.Printf("GetAttachment(): error %+v", err) + } + + if len(attachments)>0 { + att = attachments[0] + } + + revel.TRACE.Printf("GetAttachment() returning %+v", att) + return att +} + +func GetAttachments(wiki string) []Attachment { + revel.TRACE.Printf("GetAttachments() wiki: %+v", wiki) + attachments := []Attachment{} + db := get_db() + defer db.Close() + + err := db.Select(&attachments, "select * from attachments a1 where a1.wiki_id=uuid_in($1) and a1.status='ACTIVE' and not exists (select * from attachments a2 where a1.wiki_id=a2.wiki_id and a1.attachment_id=a2.attachment_id and a2.modified>a1.modified)", wiki) + + if err != nil { + revel.ERROR.Printf("GetAttachments(): error %+v", err) + } + + revel.TRACE.Printf("GetAttachments() returning: %+v", attachments) + return attachments +} + +func DeleteAttachments(wiki string, user string) { + revel.TRACE.Printf("DeleteAttachments() wiki: %+v, user: %+v", wiki, user) + attachments := []Attachment{} + db := get_db() + defer db.Close() + + err := db.Select(&attachments, "select * from attachments a1 where a1.wiki_id=uuid_in($1) and a1.status='ACTIVE' and not exists (select * from attachments a2 where a1.wiki_id=a2.wiki_id and a1.attachment_id=a2.attachment_id and a2.modified>a1.modified)", wiki) + + if err != nil { + revel.ERROR.Printf("DeleteAttachments(): error %+v", err) + } + + for _, item := range attachments { + item.Create_user = user + item.Status = "DELETED" + item.Save(false) + } +} + + +// ACL stuff + +// Build ACL entry for reference +func (a Attachment) BuildACLEntry(reference string) acl.ACLEntry { + revel.TRACE.Printf("BuildACLEntry() %+v", reference) + var entry acl.ACLEntry + + if reference != ("attachment:"+a.Wiki_id+"/"+a.Attachment_id) { + if strings.Index(reference, "attachment") == 0 { + // We are not working on this copy, get from database + re := regexp.MustCompile("attachment:([^/]*)/(.*)") + m := re.FindStringSubmatch(reference) + wref := m[1] + aref := m[2] + aa := GetAttachment(wref, aref) + entry = entry_helper(aa.Readacl, aa.Writeacl, aa.Adminacl, reference, aa) + } else { + // This must be wiki! + re := regexp.MustCompile("wiki:(.*)") + ref := re.FindStringSubmatch(reference)[1] + wi := GetWiki(ref) + entry = entry_helper(wi.Readacl, wi.Writeacl, wi.Adminacl, reference, wi) + } + } else { + // It's exactly the originating item! + entry = entry_helper(a.Readacl, a.Writeacl, a.Adminacl, reference, a) + } + + return entry +} + +// Set the matched permissions to a variable +func (a Attachment) SetMatched(permissions []string) interface{} { + a.MatchedPermissions = permissions + return a +} + +// Building parent information +func (a Attachment) BuildACLParent() string { + return "wiki:"+a.Wiki_id +} + +// Wiki+attachment ids... +func (a Attachment) BuildACLReference() string { + return "attachment:"+a.Wiki_id+"/"+a.Attachment_id +} + +// Set on data +func (a Attachment) BuildACLInheritation() bool { + return true +} diff --git a/app/models/common.go b/app/models/common.go new file mode 100644 index 0000000..8e39762 --- /dev/null +++ b/app/models/common.go @@ -0,0 +1,69 @@ +package models + +import ( + "github.com/revel/revel" + "fmt" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "github.com/mikkolehtisalo/revel/acl" + "strings" +) + +var ( + db_user string + db_password string + db_name string +) + +// Initialize settings from app.conf +func init() { + revel.OnAppStart(func() { + var ok bool + if db_user, ok = revel.Config.String("db.user"); !ok { + panic(fmt.Errorf("Unable to read db.user")) + } + if db_password, ok = revel.Config.String("db.password"); !ok { + panic(fmt.Errorf("Unable to read db.password")) + } + if db_name , ok = revel.Config.String("db.name"); !ok { + panic(fmt.Errorf("Unable to read db.name")) + } + }) +} + +func checkError(err error, s string) { + // Syslogger + //logger, _ := syslog.New(syslog.LOG_ERR, "SyncServer") + //defer logger.Close() + + if err != nil { + //logger.Err(fmt.Sprintf("%s: %s", s, err)) + panic(fmt.Sprintf("%s: %s", s, err)) + } +} + +// Open new database connection +func get_db() *sqlx.DB { + connstring := fmt.Sprintf("user=%s password='%s' dbname=%s sslmode=disable", db_user, db_password, db_name) + db, err := sqlx.Open("postgres", connstring) + checkError(err, "sqlx.Open") + return db +} + +// Common helper for building acl entries specific to this application +func entry_helper(read string, write string, admin string, reference string, tgt acl.Filterable) acl.ACLEntry { + entry := acl.ACLEntry{} + + read_acl := acl.BuildPermissionACLs("read", strings.Split(read, ",")) + write_acl := acl.BuildPermissionACLs("write", strings.Split(write, ",")) + admin_acl := acl.BuildPermissionACLs("admin", strings.Split(admin, ",")) + acls := append(read_acl, write_acl...) + acls = append(acls, admin_acl...) + entry.ACLs = acls + + entry.ObjReference = reference + entry.Inheritation = tgt.BuildACLInheritation() + entry.Parent = tgt.BuildACLParent() + + return entry +} diff --git a/app/models/contentfield.go b/app/models/contentfield.go new file mode 100644 index 0000000..1beea10 --- /dev/null +++ b/app/models/contentfield.go @@ -0,0 +1,279 @@ +package models + +import ( + "github.com/revel/revel" + "github.com/mikkolehtisalo/revel/deXSS" + "time" + "regexp" + "github.com/mikkolehtisalo/revel/acl" + "strings" + "fmt" +) + +var ( + allowed map[string]string +) + +func init() { + allowed = make(map[string]string) + // This is actually what most basic editing functions of CKEditor require + allowed["p"] = "class,id" + allowed["div"] = "class,id" + allowed["h1"] = "class,id" + allowed["h2"] = "class,id" + allowed["h3"] = "class,id" + allowed["ul"] = "class,id" + allowed["li"] = "class,id" + allowed["a"] = "class,id,href,rel" + allowed["img"] = "class,id,src,alt,hspace,vspace,width,height" + allowed["span"] = "class,id,style" +} + +type ContentField struct { + Contentfield_id string + Wiki_id string + Content string + Contentwithmacros string + Modified time.Time + Status string + Create_user string + MatchedPermissions []string + // Not really used, but required for handling the acls + Readacl string + Writeacl string + Adminacl string +} + +func (c *ContentField) Validate(v *revel.Validation) { + // Required fields + v.Required(c.Contentfield_id) + v.Required(c.Wiki_id) + + // Match against regexp patterns + v.Match(c.Contentfield_id, regexp.MustCompile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")).Message("Contentfield_id not UUID?") + v.Match(c.Wiki_id, regexp.MustCompile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")).Message("Wiki_id not UUID?") + + // Clean the HTML + c.Content = deXSS.FilterHTML(c.Content, allowed, true) +} + +func toc_page(p Page, mode string, co map[string]interface {}) string { + toc := "" + + level := p.Depth + 1 + children := Get_children(p) + filtered := acl.Filter(co, []string{"read", "write", "admin"}, children, true) + var link string + + for _, child := range filtered { + x := child.(Page) + if mode == "anchor" { + link = "" + x.Title + "" + } else { + parent := "" + if x.Depth > 0 { + split := strings.Split(x.Path, "/") + parent = split[len(split)-2] + } + link = "" + x.Title + "" + } + + toc = toc + "::toc_anchor(.*)$") + toc_link_regexp := regexp.MustCompile("(?m)^
::toc_link(.*)$") + children_regexp := regexp.MustCompile("(?m)^
::children(.*)$") + + if toc_anchor_regexp.MatchString(c.Content) { + revel.TRACE.Printf("TOC anchor macro found!") + toc := c.build_toc("anchor", co) + c.Contentwithmacros = toc_anchor_regexp.ReplaceAllString(c.Contentwithmacros, toc) + } + + if toc_link_regexp.MatchString(c.Content) { + revel.TRACE.Printf("TOC link macro found!") + toc := c.build_toc("link", co) + c.Contentwithmacros = toc_link_regexp.ReplaceAllString(c.Contentwithmacros, toc) + } + + if children_regexp.MatchString(c.Content) { + revel.TRACE.Printf("Include children macro found!") + include := c.build_include(co) + c.Contentwithmacros = children_regexp.ReplaceAllString(c.Contentwithmacros, include) + } +} + +// Updates modified time +func (c *ContentField) Save(save_activity bool) { + revel.TRACE.Printf("ContentField Save(): %+v", c) + db := get_db() + defer db.Close() + + c.Modified = time.Now() + + _, err := db.Exec("insert into contentfields(contentfield_id, wiki_id, content, modified, status, create_user) values ($1, $2, $3, $4, $5, $6)", + c.Contentfield_id, c.Wiki_id, c.Content, c.Modified, c.Status, c.Create_user ) + + if err != nil { + revel.ERROR.Printf("ContentField Save(): failed with %+v", err) + } + + if save_activity { + // Save the activity just if this was page anyways + p := GetPageAllStatuses(c.Wiki_id, c.Contentfield_id) + SaveActivity(&p) + } +} + +func DeleteContentFields(wiki_id string, user string) { + revel.TRACE.Printf("DeleteContentFields(): wiki: %+v, user: %+v", wiki_id, user) + + contents := []ContentField{} + db := get_db() + defer db.Close() + + err := db.Select(&contents, "select * from contentfields c1 where wiki_id=uuid_in($1) and status='ACTIVE' and not exists (select * from contentfields c2 where c1.contentfield_id=c2.contentfield_id and c1.wiki_id=c2.wiki_id and c2.modified>c1.modified)", + wiki_id) + + if err != nil { + revel.ERROR.Printf("Unable to get contents %+v: %+v", wiki_id, err) + } + + for _, item := range contents { + item.Status = "DELETED" + item.Create_user = user + item.Save(false) + } +} + +func GetContent(wiki_id string, content_id string, c map[string]interface {}) ContentField { + revel.TRACE.Printf("GetContent() wiki: %+v, content: %+v", wiki_id, content_id) + contents := []ContentField{} + content := ContentField{} + db := get_db() + defer db.Close() + + err := db.Select(&contents, "select * from contentfields c1 where contentfield_id=uuid_in($1) and wiki_id=uuid_in($2) and status='ACTIVE' and not exists (select * from contentfields c2 where c1.contentfield_id=c2.contentfield_id and c1.wiki_id=c2.wiki_id and c2.modified>c1.modified)", + content_id, wiki_id) + + if err != nil { + revel.ERROR.Printf("Unable to get content %+v/%+v: %+v", wiki_id, content_id, err) + } + + if len(contents)>0 { + content = contents[0] + } + + content.run_macros(c) + + revel.TRACE.Printf("GetContent() returning: %+v", content) + return content +} + + +// ACL stuff + +// Build ACL entry for reference +func (c ContentField) BuildACLEntry(reference string) acl.ACLEntry { + revel.TRACE.Printf("BuildACLEntry() %+v", reference) + var entry acl.ACLEntry + + if reference != ("cf:"+c.Wiki_id+"/"+c.Contentfield_id) { + + if strings.Index(reference, "page") == 0 { + // We are not working on this copy, get from database + re := regexp.MustCompile("page:([^/]*)/(.*)") + m := re.FindStringSubmatch(reference) + wref := m[1] + pref := m[2] + pa := GetPage(wref, pref) + entry = entry_helper(pa.Readacl, pa.Writeacl, pa.Adminacl, reference, pa) + } + + if strings.Index(reference, "wiki") == 0 { + // This must be wiki! + re := regexp.MustCompile("wiki:(.*)") + ref := re.FindStringSubmatch(reference)[1] + wi := GetWiki(ref) + entry = entry_helper(wi.Readacl, wi.Writeacl, wi.Adminacl, reference, wi) + } + + } else { + // It's exactly the originating item! + entry = entry_helper(c.Readacl, c.Writeacl, c.Adminacl, reference, c) + } + + revel.TRACE.Printf("BuildACLEntry() returning %+v", entry) + + return entry +} + +// Set the matched permissions to a variable +func (c ContentField) SetMatched(permissions []string) interface{} { + c.MatchedPermissions = permissions + return c +} + +// Building parent information +func (c ContentField) BuildACLParent() string { + return "page:" + c.Wiki_id + "/" + c.Contentfield_id +} + +// Wiki+contentfield ids... +func (c ContentField) BuildACLReference() string { + return "cf:"+c.Wiki_id+"/"+c.Contentfield_id +} + +// Always inherit the parent! +func (c ContentField) BuildACLInheritation() bool { + return true +} diff --git a/app/models/favoritewiki.go b/app/models/favoritewiki.go new file mode 100644 index 0000000..6938d40 --- /dev/null +++ b/app/models/favoritewiki.go @@ -0,0 +1,73 @@ +package models + +import ( + "time" + "github.com/revel/revel" +) + +type FavoriteWiki struct { + Username string + Wiki_id string + Modified time.Time + Status string +} + +// Sets modified time +func (f FavoriteWiki) Save() { + revel.TRACE.Printf("FavoriteWiki Save(): %+v", f) + db := get_db() + defer db.Close() + + _, err := db.Exec("insert into favoritewikis(username, wiki_id, modified, status) values ($1, $2, $3, $4)", f.Username, f.Wiki_id, time.Now(), f.Status) + + if err != nil { + revel.ERROR.Printf("FavoriteWiki Save(): error %+v", err) + } +} + +// List all favorites of the user +func ListFavoriteWikis(user string) []FavoriteWiki { + revel.TRACE.Printf("ListFavoriteWikis(): %+v", user) + favorites := []FavoriteWiki{} + db := get_db() + defer db.Close() + + err := db.Select(&favorites, "select * from favoritewikis w1 where username=$1 and status='ACTIVE' and not exists (select * from favoritewikis w2 where w1.wiki_id=w2.wiki_id and w1.username=w2.username and w2.modified>w1.modified)", user) + if err != nil { + revel.ERROR.Printf("ListFavoriteWikis(): error %+v", err) + } + + revel.TRACE.Printf("ListFavoriteWikis() returning: %+v", favorites) + return favorites +} + +func DeleteFavorites(wiki string) { + revel.TRACE.Printf("DeleteFavorites(): %+v", wiki) + favorites := []FavoriteWiki{} + db := get_db() + defer db.Close() + + err := db.Select(&favorites, "select * from favoritewikis w1 where wiki_id=uuid_in($1) and status='ACTIVE' and not exists (select * from favoritewikis w2 where w1.wiki_id=w2.wiki_id and w1.username=w2.username and w2.modified>w1.modified)", wiki) + if err != nil { + revel.ERROR.Printf("DeleteFavorites(): error %+v", err) + } + + // Delete! + for _, item := range favorites { + item.Status = "DELETED" + item.Save() + } +} + +// Is the wiki of user already favorited? +// No point in optimizing probably, won't get called very often +func IsFavoriteWiki(wiki string, user string) bool { + fav := false + favs := ListFavoriteWikis(user) + for _, item := range favs { + if item.Wiki_id==wiki { + fav = true + } + } + return fav +} diff --git a/app/models/lock.go b/app/models/lock.go new file mode 100644 index 0000000..f17ec43 --- /dev/null +++ b/app/models/lock.go @@ -0,0 +1,57 @@ +package models + +import ( + "time" + "github.com/revel/revel" +) + +type Lock struct { + Target_id string + Wiki_id string + Username string + Realname string + Modified time.Time +} + +// Sets modified time +func (l Lock) Save() { + revel.TRACE.Printf("Lock Save(): %+v", l) + db := get_db() + defer db.Close() + + _, err := db.Exec("insert into locks(wiki_id, target_id, username, realname, modified) values ($1, $2, $3, $4, $5)", + l.Wiki_id, l.Target_id, l.Username, l.Realname, time.Now()) + + if err != nil { + revel.ERROR.Printf("Lock Save(): failed with %+v", err) + } +} + +// Deletes by target and wiki id +func (l Lock) Delete() { + revel.TRACE.Printf("Lock Delete(): %+v", l) + db := get_db() + defer db.Close() + + _, err := db.Exec("delete from locks where wiki_id=uuid_in($1) and target_id=uuid_in($2)", + l.Wiki_id, l.Target_id) + + if err != nil { + revel.ERROR.Printf("Lock Delete(): failed with %+v", err) + } +} + +func GetLock(wiki string, target string) Lock { + revel.TRACE.Printf("GetLock(): target: %+v wiki: %+v", target, wiki) + locks := []Lock{} + lock := Lock{} + db := get_db() + defer db.Close() + + db.Select(&locks, "select * from locks where wiki_id=uuid_in($1) and target_id=uuid_in($2)", + wiki, target) + if len(locks)>0 { + lock = locks[0] + } + return lock +} diff --git a/app/models/page.go b/app/models/page.go new file mode 100644 index 0000000..9389768 --- /dev/null +++ b/app/models/page.go @@ -0,0 +1,266 @@ +package models + +import ( + "time" + "github.com/revel/revel" + "github.com/mikkolehtisalo/revel/acl" + "regexp" + "html" + "strings" + "fmt" + //. "github.com/mikkolehtisalo/revel/common" +) + +type Page struct { + Page_id string + Wiki_id string + Path string + Title string + Create_user string + Readacl string + Writeacl string + Adminacl string + Stopinheritation bool + Index int + Depth int + Status string + Modified time.Time + MatchedPermissions []string + Loaded bool `json:"loaded"` +} + +// Sets modified time +// Updates depth automatically +func (p *Page) Save(save_activity bool) { + revel.TRACE.Printf("Page Save(): %+v", p) + db := get_db() + defer db.Close() + + // Update depth + update_depth(p) + p.Modified = time.Now() + + _, err := db.Exec("insert into pages(page_id, wiki_id, path, title, create_user, readacl, writeacl, adminacl, stopinheritation, index, depth, modified, status) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", + p.Page_id, p.Wiki_id, p.Path, p.Title, p.Create_user, p.Readacl, p.Writeacl, p.Adminacl, p.Stopinheritation, p.Index, p.Depth, p.Modified, p.Status ) + + if err != nil { + revel.ERROR.Printf("Page Save(): failed with %+v", err) + } + + if save_activity { + SaveActivity(p) + } +} + +// Wiki id, one page id, path +// The pages in path must either exist in database or match given single id (in case target is not already in database) +func valid_path(w string, me string, p string) bool { + valid := true + split_path := strings.Split(p, "/") + + for _, part := range split_path { + x := GetPage(w, part) + if part != me && x.Page_id != part { + valid = false + } + } + + return valid +} + +func (p *Page) Validate(v *revel.Validation) { + // Required fields + v.Required(p.Page_id) + v.Required(p.Wiki_id) + v.Required(p.Path) + v.Required(p.Title) + + // Match against regexp patterns + v.Match(p.Wiki_id, regexp.MustCompile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")).Message("Wiki_id not UUID?") + v.Match(p.Page_id, regexp.MustCompile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")).Message("Page_id not UUID?") + + // Escape HTML from the fields that might be rendered to users + p.Title = html.EscapeString(p.Title) + + // Validate Path + v.Required(valid_path(p.Wiki_id, p.Page_id, p.Path)).Message(fmt.Sprintf("Path is probably invalid: %+v", p.Path)) +} + +func has_children(page Page) bool { + result := false + pages := Get_children(page) + if len(pages) > 0 { + result = true + } + + return result +} + +// Only the next level +func Get_children(page Page) []Page { + revel.TRACE.Printf("Page Get_children(): %+v", page) + pages := []Page{} + db := get_db() + defer db.Close() + + path := page.Path + "/%" + db.Select(&pages, "select * from pages p1 where p1.status='ACTIVE' and not exists (select * from pages p2 where p1.wiki_id=p2.wiki_id and p1.page_id=p2.page_id and p2.modified>p1.modified) and p1.path like $1 and p1.depth=$2 order by index", path, page.Depth + 1) + + return pages +} + +// Used only from Wiki.Delete() +func DeletePages(wiki string, user string) { + revel.TRACE.Printf("Page DeletePages(): wiki %+v, user %+v", wiki, user) + pages := []Page{} + db := get_db() + defer db.Close() + + db.Select(&pages, "select * from pages p1 where p1.status='ACTIVE' and p1.wiki_id=uuid_in($1) and not exists (select * from pages p2 where p1.wiki_id=p2.wiki_id and p1.page_id=p2.page_id and p2.modified>p1.modified)", + wiki) + + for _, page := range pages { + DeletePage(wiki, page.Page_id, user, false) + } +} + +func ListPages(wiki_id string, node string) []Page { + revel.TRACE.Printf("ListPages() wiki_id: %+v, node: %+v", wiki_id, node) + pages := []Page{} + db := get_db() + defer db.Close() + + if len(node) > 0 { + // Get the parent, and find what's under it - if anything + p := GetPage(wiki_id, node) + path := p.Path + "/%" + depth := p.Depth + 1 + db.Select(&pages, "select * from pages p1 where p1.status='ACTIVE' and not exists (select * from pages p2 where p1.wiki_id=p2.wiki_id and p1.page_id=p2.page_id and p2.modified>p1.modified) and p1.path like $1 and p1.depth=$2", path, depth) + } else { + db.Select(&pages, "select * from pages p1 where p1.status='ACTIVE' and p1.wiki_id=uuid_in($1) and p1.depth=0 and not exists (select * from pages p2 where p1.wiki_id=p2.wiki_id and p1.page_id=p2.page_id and p2.modified>p1.modified)", wiki_id) + } + + // Update children status + for x, _ := range pages { + pages[x].Loaded = !has_children(pages[x]) + revel.TRACE.Printf("Page %+v children: %+v", pages[x], has_children(pages[x])) + } + + revel.TRACE.Printf("ListPages returning %+v", pages) + return pages +} + +func DeletePage(wiki_id string, page_id string, user string, save_activity bool) { + revel.TRACE.Printf("DeletePage() wiki: %+v, page: %+v, user: %+v", wiki_id, page_id, user) + + page := GetPage(wiki_id, page_id) + pages := Get_children(page) + // For looping + pages = append(pages, page) + + for _, p := range pages { + if p.Wiki_id != "" { + p.Status = "DELETED" + p.Create_user = user + p.Save(save_activity) + + // Delete ContentField too + cf := GetContent(p.Wiki_id, p.Page_id, nil) + cf.Status = "DELETED" + cf.Create_user = user + cf.Save(save_activity) + } + } +} + +func GetPage(wiki_id string, page_id string) Page { + revel.TRACE.Printf("Page GetPage(): page: %+v wiki: %+v", page_id, wiki_id) + pages := []Page{} + page := Page{} + db := get_db() + defer db.Close() + + db.Select(&pages, "select * from pages p1 where p1.wiki_id=uuid_in($1) and p1.page_id=uuid_in($2) and p1.status='ACTIVE' and not exists (select * from pages p2 where p1.wiki_id=p2.wiki_id and p1.page_id=p2.page_id and p2.modified>p1.modified)", + wiki_id, page_id) + if len(pages)>0 { + page = pages[0] + } + return page +} + +func GetPageAllStatuses(wiki_id string, page_id string) Page { + revel.TRACE.Printf("Page GetPageAllStatuses(): page: %+v wiki: %+v", page_id, wiki_id) + pages := []Page{} + page := Page{} + db := get_db() + defer db.Close() + + db.Select(&pages, "select * from pages p1 where p1.wiki_id=uuid_in($1) and p1.page_id=uuid_in($2) and not exists (select * from pages p2 where p1.wiki_id=p2.wiki_id and p1.page_id=p2.page_id and p2.modified>p1.modified)", + wiki_id, page_id) + if len(pages)>0 { + page = pages[0] + } + return page +} + +func update_depth(page *Page) { + split := strings.Split(page.Path, "/") + page.Depth = len(split) - 1 +} + +// ACL stuff + +// Build ACL entry for reference +func (p Page) BuildACLEntry(reference string) acl.ACLEntry { + revel.TRACE.Printf("BuildACLEntry() %+v", reference) + var entry acl.ACLEntry + + if reference != ("page:"+p.Wiki_id+"/"+p.Page_id) { + if strings.Index(reference, "page") == 0 { + // We are not working on this copy, get from database + re := regexp.MustCompile("page:([^/]*)/(.*)") + m := re.FindStringSubmatch(reference) + wref := m[1] + pref := m[2] + pa := GetPage(wref, pref) + entry = entry_helper(pa.Readacl, pa.Writeacl, pa.Adminacl, reference, pa) + } else { + // This must be wiki! + re := regexp.MustCompile("wiki:(.*)") + ref := re.FindStringSubmatch(reference)[1] + wi := GetWiki(ref) + entry = entry_helper(wi.Readacl, wi.Writeacl, wi.Adminacl, reference, wi) + } + } else { + // It's exactly the originating item! + entry = entry_helper(p.Readacl, p.Writeacl, p.Adminacl, reference, p) + } + + return entry +} + +// Set the matched permissions to a variable +func (p Page) SetMatched(permissions []string) interface{} { + p.MatchedPermissions = permissions + return p +} + +// Building parent information +func (p Page) BuildACLParent() string { + if p.Depth==0 { + return "wiki:"+p.Wiki_id + } else { + pslice := strings.Split(p.Path, "/") + return "page:"+p.Wiki_id + "/" + pslice[len(pslice)-2] + } +} + +// Wiki+page ids... +func (p Page) BuildACLReference() string { + return "page:"+p.Wiki_id+"/"+p.Page_id +} + +// Set on data +func (p Page) BuildACLInheritation() bool { + return !p.Stopinheritation +} diff --git a/app/models/usergroupsearchitem.go b/app/models/usergroupsearchitem.go new file mode 100644 index 0000000..9934d13 --- /dev/null +++ b/app/models/usergroupsearchitem.go @@ -0,0 +1,79 @@ +package models + +import ( + "github.com/mikkolehtisalo/revel/ldapuserdetails" + "fmt" + "strings" + "github.com/revel/revel" + //"time" + // _ "github.com/lib/pq" + //"github.com/jmoiron/sqlx" +) + +var ( + ldap_user_filter string = "&(objectClass=*)" + ldap_user_uid_attr string = "*" + ldap_user_cn_attr string = "*" + ldap_user_base string = "dc=*,dc=*" + ldap_group_filter string = "&(objectClass=*)" + ldap_group_cn_attr string = "*" + ldap_group_dn_attr string = "*" + ldap_group_base string = "dc=*,dc=*" +) + +func get_c_str(name string) string { + if tmp, ok := revel.Config.String(name); !ok { + panic(fmt.Errorf("%s invalid", name)) + } else { + return tmp + } +} + +func init() { + revel.OnAppStart(func() { + ldap_user_filter = get_c_str("ldap.user_filter") + ldap_user_base = get_c_str("ldap.user_base") + ldap_user_uid_attr = get_c_str("ldap.user_uid_attr") + ldap_user_cn_attr = get_c_str("ldap.user_cn_attr") + ldap_group_filter = get_c_str("ldap.group_filter") + ldap_group_base = get_c_str("ldap.group_base") + ldap_group_cn_attr = get_c_str("ldap.group_cn_attr") + ldap_group_dn_attr = get_c_str("ldap.group_dn_attr") + }) +} + + +type UserGroupSearchItem struct { + Id string + Name string + Type string +} + +func ListUserGroupSearchItems(query string) []UserGroupSearchItem { + items := []UserGroupSearchItem{} + l := ldapuserdetails.Get_connection() + defer l.Close() + + sru := ldapuserdetails.QueryLdap(ldap_user_base, strings.Replace(ldap_user_filter, "*", + fmt.Sprintf("*%s*",query), -1), []string{ldap_user_uid_attr, ldap_user_cn_attr}) + srg := ldapuserdetails.QueryLdap(ldap_group_base, strings.Replace(ldap_group_filter, "*", + fmt.Sprintf("*%s*",query), -1), []string{ldap_group_cn_attr, ldap_group_dn_attr}) + + for _, user := range sru.Entries { + item := UserGroupSearchItem { + Id: fmt.Sprintf("u:%s",user.GetAttributeValue(ldap_user_uid_attr)), + Name: fmt.Sprintf("%s (u:%s)", user.GetAttributeValue(ldap_user_cn_attr), user.GetAttributeValue(ldap_user_uid_attr)), + Type: "user"} + items = append(items, item) + } + for _, group := range srg.Entries { + item := UserGroupSearchItem { + Id: fmt.Sprintf("g:%s", group.GetAttributeValue(ldap_group_cn_attr)), + Name: fmt.Sprintf("%s (g:%s)", group.GetAttributeValue(ldap_group_cn_attr), group.GetAttributeValue(ldap_group_cn_attr)), + Type: "group"} + items = append(items, item) + } + + return items +} + diff --git a/app/models/wiki.go b/app/models/wiki.go new file mode 100644 index 0000000..d208016 --- /dev/null +++ b/app/models/wiki.go @@ -0,0 +1,160 @@ +package models + +import ( + "time" + "github.com/mikkolehtisalo/revel/acl" + "strings" + "github.com/revel/revel" + "regexp" + "html" +) + +type Wiki struct { + Wiki_id string + Title string + Description string + Create_user string + Readacl string + Writeacl string + Adminacl string + Status string + Modified time.Time + MatchedPermissions []string + Favorite bool +} + +// Sets the modified timestamp +func (w *Wiki) Save(save_activity bool) { + revel.TRACE.Printf("Wiki Save(): %+v", w) + + db := get_db() + defer db.Close() + w.Modified = time.Now() + + _, err := db.Exec("insert into wikis(wiki_id, title, description, create_user, readacl, writeacl, adminacl, status, modified) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + w.Wiki_id, w.Title, w.Description, w.Create_user, w.Readacl, w.Writeacl, w.Adminacl, w.Status, w.Modified) + + if err != nil { + revel.ERROR.Printf("Wiki Save(): error %+v", err) + } + + if save_activity { + SaveActivity(w) + } +} + +func (w *Wiki) Validate(v *revel.Validation) { + // Required fields + v.Required(w.Wiki_id) + v.Required(w.Title) + + // Match against regexp patterns + v.Match(w.Wiki_id, regexp.MustCompile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")).Message("Not UUID?") + + // Escape HTML from the fields that might be rendered to users + w.Description = html.EscapeString(w.Description) + w.Title = html.EscapeString(w.Title) + w.Readacl = html.EscapeString(w.Readacl) + w.Writeacl = html.EscapeString(w.Writeacl) + w.Adminacl = html.EscapeString(w.Adminacl) +} + +//All ACTIVE Wikis +func ListWikis() []Wiki { + revel.TRACE.Printf("ListWikis()") + + wikis := []Wiki{} + db := get_db() + defer db.Close() + + err := db.Select(&wikis, "select * from wikis w1 where not exists (select * from wikis w2 where w2.modified>w1.modified and w1.wiki_id=w2.wiki_id) and status='ACTIVE'") + if err != nil { + revel.ERROR.Printf("ListWikis() err: %+v", err) + } + + revel.TRACE.Printf("ListWikis() returning %+v", wikis) + return wikis +} + +//Newest ACTIVE version of wiki +func GetWiki(id string) Wiki { + revel.TRACE.Printf("GetWiki() %+v", id) + wikis := []Wiki{} + wiki := Wiki{} + db := get_db() + defer db.Close() + + err := db.Select(&wikis, "select * from wikis w1 where w1.wiki_id=uuid_in($1) and w1.status='ACTIVE' and not exists (select * from wikis w2 where w1.wiki_id=w2.wiki_id and w2.modified>w1.modified)", id) + if err != nil { + revel.ERROR.Printf("GetWiki(): error %+v", err) + } + + if len(wikis)>0 { + wiki = wikis[0] + } + + revel.TRACE.Printf("GetWiki() returning %+v", wiki) + return wiki +} + +// Goodbye, wiki! +func (w Wiki) Delete(user string) { + revel.TRACE.Printf("Wiki Delete() user: %+v", user) + w.Status = "DELETED" + w.Create_user = user + w.Save(true) + DeletePages(w.Wiki_id, user) + DeleteContentFields(w.Wiki_id, user) + DeleteAttachments(w.Wiki_id, user) + DeleteFavorites(w.Wiki_id) +} + +// ACL stuff +// --------- + +// Build ACL entry for reference +func (w Wiki) BuildACLEntry(reference string) acl.ACLEntry { + entry := acl.ACLEntry{} + + tgt := w + if reference != ("wiki:"+w.Wiki_id) { + // We are not working on this copy, get from database + re := regexp.MustCompile("wiki:(.*)") + ref := re.FindStringSubmatch(reference)[1] + tgt = GetWiki(ref) + } + + // Build the ACL from tgt + read_acl := acl.BuildPermissionACLs("read", strings.Split(tgt.Readacl, ",")) + write_acl := acl.BuildPermissionACLs("write", strings.Split(tgt.Writeacl, ",")) + admin_acl := acl.BuildPermissionACLs("admin", strings.Split(tgt.Adminacl, ",")) + acls := append(read_acl, write_acl...) + acls = append(acls, admin_acl...) + entry.ObjReference = reference + entry.ACLs = acls + entry.Inheritation = tgt.BuildACLInheritation() + entry.Parent = tgt.BuildACLParent() + + return entry +} + +// Set the matched permissions to a variable +func (w Wiki) SetMatched(permissions []string) interface{} { + w.MatchedPermissions = permissions + return w +} + +// Just append type to the id +func (w Wiki) BuildACLReference() string { + return "wiki:"+w.Wiki_id +} + +// No wiki inherits ACL +func (w Wiki) BuildACLInheritation() bool { + return false +} + +// No wiki has parent +func (w Wiki) BuildACLParent() string { + return "" +} \ No newline at end of file diff --git a/app/routes/routes.go b/app/routes/routes.go new file mode 100644 index 0000000..c332c45 --- /dev/null +++ b/app/routes/routes.go @@ -0,0 +1,341 @@ +// GENERATED CODE - DO NOT EDIT +package routes + +import "github.com/revel/revel" + + +type tWikis struct {} +var Wikis tWikis + + +func (_ tWikis) Create( + wiki string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + return revel.MainRouter.Reverse("Wikis.Create", args).Url +} + +func (_ tWikis) Read( + ) string { + args := make(map[string]string) + + return revel.MainRouter.Reverse("Wikis.Read", args).Url +} + +func (_ tWikis) Update( + wiki string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + return revel.MainRouter.Reverse("Wikis.Update", args).Url +} + +func (_ tWikis) Delete( + wiki string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + return revel.MainRouter.Reverse("Wikis.Delete", args).Url +} + + +type tFavoriteWikis struct {} +var FavoriteWikis tFavoriteWikis + + +func (_ tFavoriteWikis) Create( + wiki string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + return revel.MainRouter.Reverse("FavoriteWikis.Create", args).Url +} + +func (_ tFavoriteWikis) Read( + ) string { + args := make(map[string]string) + + return revel.MainRouter.Reverse("FavoriteWikis.Read", args).Url +} + +func (_ tFavoriteWikis) Delete( + wiki string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + return revel.MainRouter.Reverse("FavoriteWikis.Delete", args).Url +} + + +type tUserAvatars struct {} +var UserAvatars tUserAvatars + + +func (_ tUserAvatars) Read( + user string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "user", user) + return revel.MainRouter.Reverse("UserAvatars.Read", args).Url +} + + +type tUserGroupSearch struct {} +var UserGroupSearch tUserGroupSearch + + +func (_ tUserGroupSearch) List( + ) string { + args := make(map[string]string) + + return revel.MainRouter.Reverse("UserGroupSearch.List", args).Url +} + + +type tLocks struct {} +var Locks tLocks + + +func (_ tLocks) Create( + wiki string, + target string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + revel.Unbind(args, "target", target) + return revel.MainRouter.Reverse("Locks.Create", args).Url +} + +func (_ tLocks) Read( + wiki string, + target string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + revel.Unbind(args, "target", target) + return revel.MainRouter.Reverse("Locks.Read", args).Url +} + +func (_ tLocks) Delete( + wiki string, + target string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + revel.Unbind(args, "target", target) + return revel.MainRouter.Reverse("Locks.Delete", args).Url +} + + +type tPages struct {} +var Pages tPages + + +func (_ tPages) Create( + wiki string, + page string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + revel.Unbind(args, "page", page) + return revel.MainRouter.Reverse("Pages.Create", args).Url +} + +func (_ tPages) Read( + ) string { + args := make(map[string]string) + + return revel.MainRouter.Reverse("Pages.Read", args).Url +} + +func (_ tPages) Update( + wiki string, + page string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + revel.Unbind(args, "page", page) + return revel.MainRouter.Reverse("Pages.Update", args).Url +} + +func (_ tPages) Delete( + wiki string, + page string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + revel.Unbind(args, "page", page) + return revel.MainRouter.Reverse("Pages.Delete", args).Url +} + + +type tAttachments struct {} +var Attachments tAttachments + + +func (_ tAttachments) Create( + wiki string, + attachment string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + revel.Unbind(args, "attachment", attachment) + return revel.MainRouter.Reverse("Attachments.Create", args).Url +} + +func (_ tAttachments) Read( + wiki string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + return revel.MainRouter.Reverse("Attachments.Read", args).Url +} + +func (_ tAttachments) Serve( + wiki string, + attachment string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + revel.Unbind(args, "attachment", attachment) + return revel.MainRouter.Reverse("Attachments.Serve", args).Url +} + +func (_ tAttachments) Delete( + wiki string, + attachment string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + revel.Unbind(args, "attachment", attachment) + return revel.MainRouter.Reverse("Attachments.Delete", args).Url +} + + +type tApp struct {} +var App tApp + + +func (_ tApp) Index( + ) string { + args := make(map[string]string) + + return revel.MainRouter.Reverse("App.Index", args).Url +} + + +type tContentFields struct {} +var ContentFields tContentFields + + +func (_ tContentFields) Read( + wiki string, + page string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + revel.Unbind(args, "page", page) + return revel.MainRouter.Reverse("ContentFields.Read", args).Url +} + +func (_ tContentFields) Update( + wiki string, + page string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "wiki", wiki) + revel.Unbind(args, "page", page) + return revel.MainRouter.Reverse("ContentFields.Update", args).Url +} + + +type tActivities struct {} +var Activities tActivities + + +func (_ tActivities) Read( + ) string { + args := make(map[string]string) + + return revel.MainRouter.Reverse("Activities.Read", args).Url +} + + +type tTestRunner struct {} +var TestRunner tTestRunner + + +func (_ tTestRunner) Index( + ) string { + args := make(map[string]string) + + return revel.MainRouter.Reverse("TestRunner.Index", args).Url +} + +func (_ tTestRunner) Run( + suite string, + test string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "suite", suite) + revel.Unbind(args, "test", test) + return revel.MainRouter.Reverse("TestRunner.Run", args).Url +} + +func (_ tTestRunner) List( + ) string { + args := make(map[string]string) + + return revel.MainRouter.Reverse("TestRunner.List", args).Url +} + + +type tStatic struct {} +var Static tStatic + + +func (_ tStatic) Serve( + prefix string, + filepath string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "prefix", prefix) + revel.Unbind(args, "filepath", filepath) + return revel.MainRouter.Reverse("Static.Serve", args).Url +} + +func (_ tStatic) ServeModule( + moduleName string, + prefix string, + filepath string, + ) string { + args := make(map[string]string) + + revel.Unbind(args, "moduleName", moduleName) + revel.Unbind(args, "prefix", prefix) + revel.Unbind(args, "filepath", filepath) + return revel.MainRouter.Reverse("Static.ServeModule", args).Url +} + + diff --git a/app/tmp/main.go b/app/tmp/main.go new file mode 100644 index 0000000..0f35f37 --- /dev/null +++ b/app/tmp/main.go @@ -0,0 +1,375 @@ +// GENERATED CODE - DO NOT EDIT +package main + +import ( + "flag" + "reflect" + "github.com/revel/revel" + _ "github.com/mikkolehtisalo/iw/app" + controllers "github.com/mikkolehtisalo/iw/app/controllers" + _ "github.com/mikkolehtisalo/iw/app/models" + tests "github.com/mikkolehtisalo/iw/tests" + controllers1 "github.com/revel/revel/modules/static/app/controllers" + _ "github.com/revel/revel/modules/testrunner/app" + controllers0 "github.com/revel/revel/modules/testrunner/app/controllers" +) + +var ( + runMode *string = flag.String("runMode", "", "Run mode.") + port *int = flag.Int("port", 0, "By default, read from app.conf") + importPath *string = flag.String("importPath", "", "Go Import Path for the app.") + srcPath *string = flag.String("srcPath", "", "Path to the source root.") + + // So compiler won't complain if the generated code doesn't reference reflect package... + _ = reflect.Invalid +) + +func main() { + flag.Parse() + revel.Init(*runMode, *importPath, *srcPath) + revel.INFO.Println("Running revel server") + + revel.RegisterController((*controllers.Wikis)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "Create", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Read", + Args: []*revel.MethodArg{ + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Update", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Delete", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + + }) + + revel.RegisterController((*controllers.FavoriteWikis)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "Create", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Read", + Args: []*revel.MethodArg{ + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Delete", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + + }) + + revel.RegisterController((*controllers.UserAvatars)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "Read", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "user", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + + }) + + revel.RegisterController((*controllers.UserGroupSearch)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "List", + Args: []*revel.MethodArg{ + }, + RenderArgNames: map[int][]string{ + }, + }, + + }) + + revel.RegisterController((*controllers.Locks)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "Create", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "target", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Read", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "target", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Delete", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "target", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + + }) + + revel.RegisterController((*controllers.Pages)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "Create", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "page", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Read", + Args: []*revel.MethodArg{ + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Update", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "page", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Delete", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "page", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + + }) + + revel.RegisterController((*controllers.Attachments)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "Create", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "attachment", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Read", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Serve", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "attachment", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Delete", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "attachment", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + + }) + + revel.RegisterController((*controllers.App)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "Index", + Args: []*revel.MethodArg{ + }, + RenderArgNames: map[int][]string{ + 10: []string{ + }, + }, + }, + + }) + + revel.RegisterController((*controllers.ContentFields)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "Read", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "page", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "Update", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "wiki", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "page", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + + }) + + revel.RegisterController((*controllers.Activities)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "Read", + Args: []*revel.MethodArg{ + }, + RenderArgNames: map[int][]string{ + }, + }, + + }) + + revel.RegisterController((*controllers0.TestRunner)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "Index", + Args: []*revel.MethodArg{ + }, + RenderArgNames: map[int][]string{ + 46: []string{ + "testSuites", + }, + }, + }, + &revel.MethodType{ + Name: "Run", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "suite", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "test", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + 69: []string{ + "error", + }, + }, + }, + &revel.MethodType{ + Name: "List", + Args: []*revel.MethodArg{ + }, + RenderArgNames: map[int][]string{ + }, + }, + + }) + + revel.RegisterController((*controllers1.Static)(nil), + []*revel.MethodType{ + &revel.MethodType{ + Name: "Serve", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "prefix", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "filepath", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + &revel.MethodType{ + Name: "ServeModule", + Args: []*revel.MethodArg{ + &revel.MethodArg{Name: "moduleName", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "prefix", Type: reflect.TypeOf((*string)(nil)) }, + &revel.MethodArg{Name: "filepath", Type: reflect.TypeOf((*string)(nil)) }, + }, + RenderArgNames: map[int][]string{ + }, + }, + + }) + + revel.DefaultValidationKeys = map[string]map[int]string{ + "github.com/mikkolehtisalo/iw/app/models.(*Attachment).Validate": { + 30: "a.Attachment_id", + 31: "a.Wiki_id", + 32: "a.Attachment", + 33: "a.Filename", + 34: "a.Modified", + 37: "a.Wiki_id", + 38: "a.Attachment_id", + 42: "a.Attachment", + }, + "github.com/mikkolehtisalo/iw/app/models.(*ContentField).Validate": { + 49: "c.Contentfield_id", + 50: "c.Wiki_id", + 53: "c.Contentfield_id", + 54: "c.Wiki_id", + }, + "github.com/mikkolehtisalo/iw/app/models.(*Page).Validate": { + 73: "p.Page_id", + 74: "p.Wiki_id", + 75: "p.Path", + 76: "p.Title", + 79: "p.Wiki_id", + 80: "p.Page_id", + }, + "github.com/mikkolehtisalo/iw/app/models.(*Wiki).Validate": { + 48: "w.Wiki_id", + 49: "w.Title", + 52: "w.Wiki_id", + }, + } + revel.TestSuites = []interface{}{ + (*tests.AppTest)(nil), + } + + revel.Run(*port) +} diff --git a/app/views/App/Index.html b/app/views/App/Index.html new file mode 100644 index 0000000..b444654 --- /dev/null +++ b/app/views/App/Index.html @@ -0,0 +1,14 @@ + +
++ {{.Description}} +
+ {{end}} +{{end}} + + diff --git a/app/views/errors/500.html b/app/views/errors/500.html new file mode 100644 index 0000000..0cef4de --- /dev/null +++ b/app/views/errors/500.html @@ -0,0 +1,16 @@ + + + ++ This exception has been logged. +
+ {{end}} + + diff --git a/app/views/flash.html b/app/views/flash.html new file mode 100644 index 0000000..9c9ade9 --- /dev/null +++ b/app/views/flash.html @@ -0,0 +1,18 @@ +{{if .flash.success}} +