1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-27 20:10:03 +00:00
TiddlyWiki5/core/modules/widgets/link.js
Jermolene 08cfa88249 Fix problem with unsafe use of String.prototype.replace()
We were using `String.prototype.replace()` without addressing the
wrinkle that dollar signs in the replacement string have special
handling. This caused problems in situations where the replacement
string is derived from user input and contains dollar signs.

Fixes #2517
2016-08-06 14:45:33 +01:00

241 lines
8.3 KiB
JavaScript
Executable File

/*\
title: $:/core/modules/widgets/link.js
type: application/javascript
module-type: widget
Link widget
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var Widget = require("$:/core/modules/widgets/widget.js").widget;
var MISSING_LINK_CONFIG_TITLE = "$:/config/MissingLinks";
var LinkWidget = function(parseTreeNode,options) {
this.initialise(parseTreeNode,options);
};
/*
Inherit from the base widget class
*/
LinkWidget.prototype = new Widget();
/*
Render this widget into the DOM
*/
LinkWidget.prototype.render = function(parent,nextSibling) {
// Save the parent dom node
this.parentDomNode = parent;
// Compute our attributes
this.computeAttributes();
// Execute our logic
this.execute();
// Get the value of the tv-wikilinks configuration macro
var wikiLinksMacro = this.getVariable("tv-wikilinks"),
useWikiLinks = wikiLinksMacro ? (wikiLinksMacro.trim() !== "no") : true,
missingLinksEnabled = !(this.hideMissingLinks && this.isMissing && !this.isShadow);
// Render the link if required
if(useWikiLinks && missingLinksEnabled) {
this.renderLink(parent,nextSibling);
} else {
// Just insert the link text
var domNode = this.document.createElement("span");
parent.insertBefore(domNode,nextSibling);
this.renderChildren(domNode,null);
this.domNodes.push(domNode);
}
};
/*
Render this widget into the DOM
*/
LinkWidget.prototype.renderLink = function(parent,nextSibling) {
var self = this;
// Sanitise the specified tag
var tag = this.linkTag;
if($tw.config.htmlUnsafeElements.indexOf(tag) !== -1) {
tag = "a";
}
// Create our element
var domNode = this.document.createElement(tag);
// Assign classes
var classes = [];
if(this.linkClasses) {
classes.push(this.linkClasses);
}
classes.push("tc-tiddlylink");
if(this.isShadow) {
classes.push("tc-tiddlylink-shadow");
}
if(this.isMissing && !this.isShadow) {
classes.push("tc-tiddlylink-missing");
} else {
if(!this.isMissing) {
classes.push("tc-tiddlylink-resolves");
}
}
domNode.setAttribute("class",classes.join(" "));
// Set an href
var wikiLinkTemplateMacro = this.getVariable("tv-wikilink-template"),
wikiLinkTemplate = wikiLinkTemplateMacro ? wikiLinkTemplateMacro.trim() : "#$uri_encoded$",
wikiLinkText = $tw.utils.replaceString(wikiLinkTemplate,"$uri_encoded$",encodeURIComponent(this.to));
wikiLinkText = $tw.utils.replaceString(wikiLinkText,"$uri_doubleencoded$",encodeURIComponent(encodeURIComponent(this.to)));
wikiLinkText = this.getVariable("tv-get-export-link",{params: [{name: "to",value: this.to}],defaultValue: wikiLinkText});
if(tag === "a") {
domNode.setAttribute("href",wikiLinkText);
}
if(this.tabIndex) {
domNode.setAttribute("tabindex",this.tabIndex);
}
// Set the tooltip
// HACK: Performance issues with re-parsing the tooltip prevent us defaulting the tooltip to "<$transclude field='tooltip'><$transclude field='title'/></$transclude>"
var tooltipWikiText = this.tooltip || this.getVariable("tv-wikilink-tooltip");
if(tooltipWikiText) {
var tooltipText = this.wiki.renderText("text/plain","text/vnd.tiddlywiki",tooltipWikiText,{
parseAsInline: true,
variables: {
currentTiddler: this.to
},
parentWidget: this
});
domNode.setAttribute("title",tooltipText);
}
if(this["aria-label"]) {
domNode.setAttribute("aria-label",this["aria-label"]);
}
// Add a click event handler
$tw.utils.addEventListeners(domNode,[
{name: "click", handlerObject: this, handlerMethod: "handleClickEvent"},
]);
if(this.draggable === "yes") {
$tw.utils.addEventListeners(domNode,[
{name: "dragstart", handlerObject: this, handlerMethod: "handleDragStartEvent"},
{name: "dragend", handlerObject: this, handlerMethod: "handleDragEndEvent"}
]);
}
// Insert the link into the DOM and render any children
parent.insertBefore(domNode,nextSibling);
this.renderChildren(domNode,null);
this.domNodes.push(domNode);
};
LinkWidget.prototype.handleClickEvent = function(event) {
// Send the click on its way as a navigate event
var bounds = this.domNodes[0].getBoundingClientRect();
this.dispatchEvent({
type: "tm-navigate",
navigateTo: this.to,
navigateFromTitle: this.getVariable("storyTiddler"),
navigateFromNode: this,
navigateFromClientRect: { top: bounds.top, left: bounds.left, width: bounds.width, right: bounds.right, bottom: bounds.bottom, height: bounds.height
},
navigateSuppressNavigation: event.metaKey || event.ctrlKey || (event.button === 1)
});
if(this.domNodes[0].hasAttribute("href")) {
event.preventDefault();
}
event.stopPropagation();
return false;
};
LinkWidget.prototype.handleDragStartEvent = function(event) {
if(event.target === this.domNodes[0]) {
if(this.to) {
$tw.dragInProgress = true;
// Set the dragging class on the element being dragged
$tw.utils.addClass(event.target,"tc-tiddlylink-dragging");
// Create the drag image elements
this.dragImage = this.document.createElement("div");
this.dragImage.className = "tc-tiddler-dragger";
var inner = this.document.createElement("div");
inner.className = "tc-tiddler-dragger-inner";
inner.appendChild(this.document.createTextNode(this.to));
this.dragImage.appendChild(inner);
this.document.body.appendChild(this.dragImage);
// Astoundingly, we need to cover the dragger up: http://www.kryogenix.org/code/browser/custom-drag-image.html
var cover = this.document.createElement("div");
cover.className = "tc-tiddler-dragger-cover";
cover.style.left = (inner.offsetLeft - 16) + "px";
cover.style.top = (inner.offsetTop - 16) + "px";
cover.style.width = (inner.offsetWidth + 32) + "px";
cover.style.height = (inner.offsetHeight + 32) + "px";
this.dragImage.appendChild(cover);
// Set the data transfer properties
var dataTransfer = event.dataTransfer;
// First the image
dataTransfer.effectAllowed = "copy";
if(dataTransfer.setDragImage) {
dataTransfer.setDragImage(this.dragImage.firstChild,-16,-16);
}
// Then the data
dataTransfer.clearData();
var jsonData = this.wiki.getTiddlerAsJson(this.to),
textData = this.wiki.getTiddlerText(this.to,""),
title = (new RegExp("^" + $tw.config.textPrimitives.wikiLink + "$","mg")).exec(this.to) ? this.to : "[[" + this.to + "]]";
// IE doesn't like these content types
if(!$tw.browser.isIE) {
dataTransfer.setData("text/vnd.tiddler",jsonData);
dataTransfer.setData("text/plain",title);
dataTransfer.setData("text/x-moz-url","data:text/vnd.tiddler," + encodeURIComponent(jsonData));
}
dataTransfer.setData("URL","data:text/vnd.tiddler," + encodeURIComponent(jsonData));
dataTransfer.setData("Text",title);
event.stopPropagation();
} else {
event.preventDefault();
}
}
};
LinkWidget.prototype.handleDragEndEvent = function(event) {
if(event.target === this.domNodes[0]) {
$tw.dragInProgress = false;
// Remove the dragging class on the element being dragged
$tw.utils.removeClass(event.target,"tc-tiddlylink-dragging");
// Delete the drag image element
if(this.dragImage) {
this.dragImage.parentNode.removeChild(this.dragImage);
}
}
};
/*
Compute the internal state of the widget
*/
LinkWidget.prototype.execute = function() {
// Pick up our attributes
this.to = this.getAttribute("to",this.getVariable("currentTiddler"));
this.tooltip = this.getAttribute("tooltip");
this["aria-label"] = this.getAttribute("aria-label");
this.linkClasses = this.getAttribute("class");
this.tabIndex = this.getAttribute("tabindex");
this.draggable = this.getAttribute("draggable","yes");
this.linkTag = this.getAttribute("tag","a");
// Determine the link characteristics
this.isMissing = !this.wiki.tiddlerExists(this.to);
this.isShadow = this.wiki.isShadowTiddler(this.to);
this.hideMissingLinks = ($tw.wiki.getTiddlerText(MISSING_LINK_CONFIG_TITLE,"yes") === "no");
// Make the child widgets
this.makeChildWidgets();
};
/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
LinkWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.to || changedTiddlers[this.to] || changedAttributes["aria-label"] || changedAttributes.tooltip || changedTiddlers[MISSING_LINK_CONFIG_TITLE]) {
this.refreshSelf();
return true;
}
return this.refreshChildren(changedTiddlers);
};
exports.link = LinkWidget;
})();