1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 13:19:43 +02:00

detect broken links

This commit is contained in:
Harvey Kandola 2016-10-27 15:44:40 -07:00
parent ad716a23ba
commit 16b7fd45d7
11 changed files with 148 additions and 108 deletions

View file

@ -71,21 +71,26 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
let self = this; let self = this;
$("a[data-documize='true']").off('click').on('click', function() { $("a[data-documize='true']").off('click').on('click', function() {
let link = links.getLinkObject(this); let link = links.getLinkObject(self.get('meta.outboundLinks'), this);
// local link? exists? // local link? exists?
if (link.linkType === "section" && link.documentId === doc.get('id')) { if (link.linkType === "section" && link.documentId === doc.get('id')) {
let exists = self.get('pages').findBy('id', link.targetId); let exists = self.get('pages').findBy('id', link.targetId);
if (_.isUndefined(exists) || link.orphan) { if (_.isUndefined(exists)) {
self.showNotification('Broken link!'); link.orphan = true;
return false;
} else { } else {
self.attrs.gotoPage(link.targetId); self.attrs.gotoPage(link.targetId);
return false; return false;
} }
} }
if (link.orphan) {
$(this).addClass('broken-link');
self.showNotification('Broken link!');
return false;
}
links.linkClick(doc, link); links.linkClick(doc, link);
return false; return false;
}); });

View file

@ -18,7 +18,7 @@
onAttachmentUpload=(action 'onAttachmentUpload') onAttachmentUpload=(action 'onAttachmentUpload')
onDocumentDelete=(action 'onDocumentDelete')}} onDocumentDelete=(action 'onDocumentDelete')}}
{{document/document-view document=model pages=pages attachments=attachments folder=folder folders=folders {{document/document-view document=model meta=meta pages=pages attachments=attachments folder=folder folders=folders
isEditor=isEditor isEditor=isEditor
gotoPage=(action 'gotoPage') gotoPage=(action 'gotoPage')
onAttachmentDeleted=(action 'onAttachmentDeleted') onAttachmentDeleted=(action 'onAttachmentDeleted')

View file

@ -50,7 +50,6 @@ export default Ember.Service.extend({
}); });
}, },
buildLink(link) { buildLink(link) {
let result = ""; let result = "";
let href = ""; let href = "";
@ -59,21 +58,21 @@ export default Ember.Service.extend({
if (link.linkType === "section" || link.linkType === "document") { if (link.linkType === "section" || link.linkType === "document") {
href = `/link/${link.linkType}/${link.id}`; href = `/link/${link.linkType}/${link.id}`;
result = `<a data-documize='true' data-link-space-id='${link.folderId}' data-link-id='${link.id}' data-link-document-id='${link.documentId}' data-link-target-id='${link.targetId}' data-link-type='${link.linkType}' href='${href}'>${link.title}</a>`; result = `<a data-documize='true' data-link-space-id='${link.folderId}' data-link-id='${link.id}' data-link-target-document-id='${link.documentId}' data-link-target-id='${link.targetId}' data-link-type='${link.linkType}' href='${href}'>${link.title}</a>`;
} }
if (link.linkType === "file") { if (link.linkType === "file") {
href = `${endpoint}/public/attachments/${orgId}/${link.targetId}`; href = `${endpoint}/public/attachments/${orgId}/${link.targetId}`;
result = `<a data-documize='true' data-link-space-id='${link.folderId}' data-link-id='${link.id}' data-link-document-id='${link.documentId}' data-link-target-id='${link.targetId}' data-link-type='${link.linkType}' href='${href}'>${link.title}</a>`; result = `<a data-documize='true' data-link-space-id='${link.folderId}' data-link-id='${link.id}' data-link-target-document-id='${link.documentId}' data-link-target-id='${link.targetId}' data-link-type='${link.linkType}' href='${href}'>${link.title}</a>`;
} }
return result; return result;
}, },
getLinkObject(a) { getLinkObject(outboundLinks, a) {
let link = { let link = {
linkId: a.attributes["data-link-id"].value, linkId: a.attributes["data-link-id"].value,
linkType: a.attributes["data-link-type"].value, linkType: a.attributes["data-link-type"].value,
documentId: a.attributes["data-link-document-id"].value, documentId: a.attributes["data-link-target-document-id"].value,
folderId: a.attributes["data-link-space-id"].value, folderId: a.attributes["data-link-space-id"].value,
targetId: a.attributes["data-link-target-id"].value, targetId: a.attributes["data-link-target-id"].value,
url: a.attributes["href"].value, url: a.attributes["href"].value,
@ -82,6 +81,15 @@ export default Ember.Service.extend({
link.orphan = _.isEmpty(link.linkId) || _.isEmpty(link.documentId) || _.isEmpty(link.folderId) || _.isEmpty(link.targetId); link.orphan = _.isEmpty(link.linkId) || _.isEmpty(link.documentId) || _.isEmpty(link.folderId) || _.isEmpty(link.targetId);
// we check latest state of link using database data
let existing = outboundLinks.findBy('id', link.linkId);
if (_.isUndefined(existing)) {
link.orphan = true;
} else {
link.orphan = existing.orphan;
}
return link; return link;
}, },
@ -119,21 +127,12 @@ export default Ember.Service.extend({
}); });
/* /*
when attachment deleted:
The link id's get ZERO'd in Page.Body whenever: mark as orphan references where link.documentid = document.refId
- doc is moved to different space
- doc is deleted (set to ZERO and marked as orphan)
- page is deleted (set to ZERO and marked as orphan)
- page is moved to different doc (update data-document-id attribute value)
- attachment is deleted (remove HREF)
link/section/{documentId}/{sectionId}:
- if ZERO id show notification
- store previous positions -- localStorage, dropdown menu?
Markdown editor support
permission checks: permission checks:
can view space can view space
can view document can view document
Markdown editor support
*/ */

View file

@ -132,6 +132,11 @@ a {
} }
} }
a.broken-link {
color: $color-red;
text-decoration: line-through;
}
a.alt { a.alt {
color: $color-blue; color: $color-blue;
text-decoration: none; text-decoration: none;

View file

@ -222,8 +222,10 @@ func (p *PageMeta) SetDefaults() {
// DocumentMeta details who viewed the document. // DocumentMeta details who viewed the document.
type DocumentMeta struct { type DocumentMeta struct {
Viewers []DocumentMetaViewer `json:"viewers"` Viewers []DocumentMetaViewer `json:"viewers"`
Editors []DocumentMetaEditor `json:"editors"` Editors []DocumentMetaEditor `json:"editors"`
InboundLinks []Link `json:"inboundLinks"`
OutboundLinks []Link `json:"outboundLinks"`
} }
// DocumentMetaViewer contains the "view" metatdata content. // DocumentMetaViewer contains the "view" metatdata content.
@ -346,14 +348,15 @@ type SitemapDocument struct {
// Link defines a reference between a section and another document/section/attachment. // Link defines a reference between a section and another document/section/attachment.
type Link struct { type Link struct {
BaseEntity BaseEntity
OrgID string `json:"orgId"` OrgID string `json:"orgId"`
FolderID string `json:"folderId"` FolderID string `json:"folderId"`
UserID string `json:"userId"` UserID string `json:"userId"`
LinkType string `json:"linkType"` LinkType string `json:"linkType"`
SourceID string `json:"sourceId"` SourceDocumentID string `json:"sourceDocumentId"`
DocumentID string `json:"documentId"` SourcePageID string `json:"sourcePageId"`
TargetID string `json:"targetId"` TargetDocumentID string `json:"targetDocumentId"`
Orphan bool `json:"orphan"` TargetPageID string `json:"targetPageId"`
Orphan bool `json:"orphan"`
} }
// LinkCandidate defines a potential link to a document/section/attachment. // LinkCandidate defines a potential link to a document/section/attachment.

View file

@ -107,6 +107,13 @@ func (p *Persister) GetDocumentMeta(id string) (meta entity.DocumentMeta, err er
return return
} }
meta.OutboundLinks, err = p.GetDocumentOutboundLinks(id)
if err != nil {
log.Error(fmt.Sprintf("Unable to execute GetDocumentOutboundLinks for document %s", id), err)
return
}
return return
} }
@ -400,6 +407,18 @@ func (p *Persister) DeleteDocument(documentID string) (rows int64, err error) {
return return
} }
// Mark references to this document as orphaned
err = p.MarkOrphanDocumentLink(documentID)
if err != nil {
return
}
// Remove all references from this document
_, err = p.DeleteSourceDocumentLinks(documentID)
if err != nil {
return
}
p.Base.Audit(p.Context, "delete-document", documentID, "") p.Base.Audit(p.Context, "delete-document", documentID, "")
return p.Base.DeleteConstrained(p.Context.Transaction, "document", p.Context.OrgID, documentID) return p.Base.DeleteConstrained(p.Context.Transaction, "document", p.Context.OrgID, documentID)

View file

@ -24,11 +24,10 @@ import (
// AddContentLink inserts wiki-link into the store. // AddContentLink inserts wiki-link into the store.
// These links exist when content references another document or content. // These links exist when content references another document or content.
func (p *Persister) AddContentLink(l entity.Link) (err error) { func (p *Persister) AddContentLink(l entity.Link) (err error) {
l.UserID = p.Context.UserID
l.Created = time.Now().UTC() l.Created = time.Now().UTC()
l.Revised = time.Now().UTC() l.Revised = time.Now().UTC()
stmt, err := p.Context.Transaction.Preparex("INSERT INTO link (refid, orgid, folderid, userid, sourceid, documentid, targetid, linktype, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") stmt, err := p.Context.Transaction.Preparex("INSERT INTO link (refid, orgid, folderid, userid, sourcedocumentid, sourcepageid, targetdocumentid, targetpageid, linktype, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
defer utility.Close(stmt) defer utility.Close(stmt)
if err != nil { if err != nil {
@ -36,7 +35,7 @@ func (p *Persister) AddContentLink(l entity.Link) (err error) {
return return
} }
_, err = stmt.Exec(l.RefID, l.OrgID, l.FolderID, l.UserID, l.SourceID, l.DocumentID, l.TargetID, l.LinkType, l.Created, l.Revised) _, err = stmt.Exec(l.RefID, l.OrgID, l.FolderID, l.UserID, l.SourceDocumentID, l.SourcePageID, l.TargetDocumentID, l.TargetPageID, l.LinkType, l.Created, l.Revised)
if err != nil { if err != nil {
log.Error("Unable to execute insert for link", err) log.Error("Unable to execute insert for link", err)
@ -181,80 +180,78 @@ func (p *Persister) SearchLinkCandidates(keywords string) (docs []entity.LinkCan
return return
} }
// GetReferencedLinks returns all links that the specified section is referencing. // GetDocumentOutboundLinks returns outbound links for specified document.
// func (p *Persister) GetReferencedLinks(sectionID string) (links []entity.Link, err error) { func (p *Persister) GetDocumentOutboundLinks(documentID string) (links []entity.Link, err error) {
// err = nil err = nil
//
// sql := "SELECT id,refid,orgid,folderid,userid,sourceid,documentid,targetid,linktype,orphan,created,revised from link WHERE orgid=? AND sourceid=?"
//
// err = Db.Select(&links, sql, p.Context.OrgID, sectionID)
//
// if err != nil {
// log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err)
// return
// }
//
// return
// }
//
// // GetContentLinksForSection returns all links that are linking to the specified section.
// func (p *Persister) GetContentLinksForSection(sectionID string) (links []entity.Link, err error) {
// err = nil
//
// sql := "SELECT id,refid,orgid,folderid,userid,sourceid,documentid,targetid,linktype,orphan,created,revised from link WHERE orgid=? AND sectionid=?"
//
// err = Db.Select(&links, sql, p.Context.OrgID, sectionID)
//
// if err != nil {
// log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err)
// return
// }
//
// return
// }
//
// // GetContentLinksForDocument returns all links that are linking to the specified document.
// func (p *Persister) GetContentLinksForDocument(documentID string) (links []entity.Link, err error) {
// err = nil
//
// sql := "SELECT id,refid,orgid,folderid,userid,sourceid,documentid,targetid,linktype,orphan,created,revised from link WHERE orgid=? AND documentid=?"
//
// err = Db.Select(&links, sql, p.Context.OrgID, documentID)
//
// if err != nil {
// log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err)
// return
// }
//
// return
// }
// MarkOrphanContentLink marks the link record as being invalid. err = Db.Select(&links,
func (p *Persister) MarkOrphanContentLink(l entity.Link) (err error) { `select l.refid, l.orgid, l.folderid, l.userid, l.sourcedocumentid, l.sourcepageid, l.targetdocumentid, l.targetpageid, l.linktype, l.orphan, l.created, l.revised
l.Orphan = true FROM link l
l.Revised = time.Now().UTC() WHERE l.orgid=? AND l.sourcedocumentid=?`,
p.Context.OrgID,
stmt, err := p.Context.Transaction.PrepareNamed("UPDATE link SET orphan=1 revised=:revised WHERE orgid=:orgid AND refid=:refid") documentID)
defer utility.Close(stmt)
if err != nil { if err != nil {
log.Error(fmt.Sprintf("Unable to prepare update for link %s", l.RefID), err)
return return
} }
_, err = stmt.Exec(&l) if len(links) == 0 {
links = []entity.Link{}
}
return
}
// MarkOrphanDocumentLink marks all link records referencing specified document.
func (p *Persister) MarkOrphanDocumentLink(documentID string) (err error) {
revised := time.Now().UTC()
stmt, err := p.Context.Transaction.Preparex("UPDATE link SET orphan=1, revised=? WHERE linktype='document' AND orgid=? AND targetdocumentid=?")
if err != nil {
return
}
defer utility.Close(stmt)
_, err = stmt.Exec(revised, p.Context.OrgID, documentID)
if err != nil { if err != nil {
log.Error(fmt.Sprintf("Unable to execute update for link %s", l.RefID), err)
return return
} }
return return
} }
// DeleteSourceLinks removes saved links for given source. // MarkOrphanPageLink marks all link records referencing specified page.
func (p *Persister) DeleteSourceLinks(sourceID string) (rows int64, err error) { func (p *Persister) MarkOrphanPageLink(pageID string) (err error) {
return p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM link WHERE orgid=\"%s\" AND sourceid=\"%s\"", p.Context.OrgID, sourceID)) revised := time.Now().UTC()
stmt, err := p.Context.Transaction.Preparex("UPDATE link SET orphan=1, revised=? WHERE linktype='section' AND orgid=? AND targetpageid=?")
if err != nil {
return
}
defer utility.Close(stmt)
_, err = stmt.Exec(revised, p.Context.OrgID, pageID)
if err != nil {
return
}
return
}
// DeleteSourcePageLinks removes saved links for given source.
func (p *Persister) DeleteSourcePageLinks(pageID string) (rows int64, err error) {
return p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM link WHERE orgid=\"%s\" AND sourcepageid=\"%s\"", p.Context.OrgID, pageID))
}
// DeleteSourceDocumentLinks removes saved links for given document.
func (p *Persister) DeleteSourceDocumentLinks(documentID string) (rows int64, err error) {
return p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM link WHERE orgid=\"%s\" AND sourcedocumentid=\"%s\"", p.Context.OrgID, documentID))
} }
// DeleteLink removes saved link from the store. // DeleteLink removes saved link from the store.

View file

@ -291,14 +291,18 @@ func (p *Persister) UpdatePage(page entity.Page, refID, userID string, skipRevis
links := util.GetContentLinks(page.Body) links := util.GetContentLinks(page.Body)
// delete previous content links for this page // delete previous content links for this page
_, _ = p.DeleteSourceLinks(page.RefID) _, _ = p.DeleteSourcePageLinks(page.RefID)
// save latest content links for this page // save latest content links for this page
for _, link := range links { for _, link := range links {
link.Orphan = false
link.OrgID = p.Context.OrgID link.OrgID = p.Context.OrgID
link.UserID = p.Context.UserID link.UserID = p.Context.UserID
link.SourceID = page.RefID link.SourceDocumentID = page.DocumentID
link.Orphan = false
if link.LinkType == "section" {
link.SourcePageID = page.RefID
}
err := p.AddContentLink(link) err := p.AddContentLink(link)
@ -399,6 +403,12 @@ func (p *Persister) DeletePage(documentID, pageID string) (rows int64, err error
_, err = p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM pagemeta WHERE orgid='%s' AND pageid='%s'", p.Context.OrgID, pageID)) _, err = p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM pagemeta WHERE orgid='%s' AND pageid='%s'", p.Context.OrgID, pageID))
_, err = searches.Delete(&databaseRequest{OrgID: p.Context.OrgID}, documentID, pageID) _, err = searches.Delete(&databaseRequest{OrgID: p.Context.OrgID}, documentID, pageID)
// delete content links from this page
_, err = p.DeleteSourcePageLinks(pageID)
// mark as orphan links to this page
err = p.MarkOrphanPageLink(pageID)
p.Base.Audit(p.Context, "remove-page", documentID, pageID) p.Base.Audit(p.Context, "remove-page", documentID, pageID)
} }

View file

@ -62,10 +62,10 @@ func getLink(t html.Token) (ok bool, link entity.Link) {
link.RefID = strings.TrimSpace(a.Val) link.RefID = strings.TrimSpace(a.Val)
case "data-link-space-id": case "data-link-space-id":
link.FolderID = strings.TrimSpace(a.Val) link.FolderID = strings.TrimSpace(a.Val)
case "data-link-document-id": case "data-link-target-document-id":
link.DocumentID = strings.TrimSpace(a.Val) link.TargetDocumentID = strings.TrimSpace(a.Val)
case "data-link-target-id": case "data-link-target-id":
link.TargetID = strings.TrimSpace(a.Val) link.TargetPageID = strings.TrimSpace(a.Val)
case "data-link-type": case "data-link-type":
link.LinkType = strings.TrimSpace(a.Val) link.LinkType = strings.TrimSpace(a.Val)
} }

View file

@ -321,10 +321,11 @@ CREATE TABLE IF NOT EXISTS `link` (
`orgid` CHAR(16) NOT NULL COLLATE utf8_bin, `orgid` CHAR(16) NOT NULL COLLATE utf8_bin,
`folderid` CHAR(16) NOT NULL COLLATE utf8_bin, `folderid` CHAR(16) NOT NULL COLLATE utf8_bin,
`userid` CHAR(16) NOT NULL COLLATE utf8_bin, `userid` CHAR(16) NOT NULL COLLATE utf8_bin,
`sourceid` CHAR(16) NOT NULL COLLATE utf8_bin, `sourcedocumentid` CHAR(16) NOT NULL COLLATE utf8_bin,
`sourcepageid` CHAR(16) NOT NULL COLLATE utf8_bin,
`linktype` CHAR(16) NOT NULL COLLATE utf8_bin, `linktype` CHAR(16) NOT NULL COLLATE utf8_bin,
`documentid` CHAR(16) NOT NULL COLLATE utf8_bin, `targetdocumentid` CHAR(16) NOT NULL COLLATE utf8_bin,
`targetid` CHAR(16) DEFAULT '' COLLATE utf8_bin, `targetpageid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`orphan` BOOL NOT NULL DEFAULT 0, `orphan` BOOL NOT NULL DEFAULT 0,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

View file

@ -7,10 +7,11 @@ CREATE TABLE IF NOT EXISTS `link` (
`orgid` CHAR(16) NOT NULL COLLATE utf8_bin, `orgid` CHAR(16) NOT NULL COLLATE utf8_bin,
`folderid` CHAR(16) NOT NULL COLLATE utf8_bin, `folderid` CHAR(16) NOT NULL COLLATE utf8_bin,
`userid` CHAR(16) NOT NULL COLLATE utf8_bin, `userid` CHAR(16) NOT NULL COLLATE utf8_bin,
`sourceid` CHAR(16) NOT NULL COLLATE utf8_bin, `sourcedocumentid` CHAR(16) NOT NULL COLLATE utf8_bin,
`sourcepageid` CHAR(16) NOT NULL COLLATE utf8_bin,
`linktype` CHAR(16) NOT NULL COLLATE utf8_bin, `linktype` CHAR(16) NOT NULL COLLATE utf8_bin,
`documentid` CHAR(16) NOT NULL COLLATE utf8_bin, `targetdocumentid` CHAR(16) NOT NULL COLLATE utf8_bin,
`targetid` CHAR(16) DEFAULT '' COLLATE utf8_bin, `targetpageid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`orphan` BOOL NOT NULL DEFAULT 0, `orphan` BOOL NOT NULL DEFAULT 0,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,