Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
*.(R|r)proj.user
*.(R|r)history
*.(R|r)Data
*.(R|r)proj
*.(R|r)buildignore
.(R|r)proj.user
*.Rproj.user
*.rproj.user
*.Rhistory
*.rhistory
.Rhistory
.rhistory
*.RData
*.rdata
*.Rproj
*.rproj
*.Rbuildignore
*.rbuildignore
.Rproj.user
.rproj.user
*~
*.gexf
*.html
*.dll
*.rds
*.o

*.(R|r)html
*.Rhtml
*.rhtml
*.pdf
.Rproj.user
inst/doc
docs/

Expand Down
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Imports:
igraph,
grDevices,
utils,
servr
servr,
htmlwidgets
License: MIT + file LICENSE
LazyLoad: yes
RoxygenNote: 7.3.1
Expand Down
7 changes: 7 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ export(edge.list)
export(gexf)
export(gexf.to.igraph)
export(gexf_js_config)
export(gexfjs)
export(gexfjsOutput)
export(renderGexfjs)
export(igraph.to.gexf)
export(new.gexf.graph)
export(read.gexf)
export(renderTest1)
export(rm.gexf.edge)
export(rm.gexf.node)
export(switch.edges)
export(test1)
export(test1Output)
export(write.gexf)
import(htmlwidgets)
importFrom(XML,addChildren)
importFrom(XML,asXMLNode)
importFrom(XML,newXMLDoc)
Expand Down
76 changes: 76 additions & 0 deletions R/plot.R
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,79 @@ plot.gexf <- function(
do.call(servr::httd, c(list(dir = dir), httd.args))

}

#' @import htmlwidgets
#' @export
gexfjs <- function(
gexf = "lib/gexf-1/lesmiserables.gexf" , # This is the onlyone that is working... for now
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "onlyone" should be "only one" (two words).

Suggested change
gexf = "lib/gexf-1/lesmiserables.gexf" , # This is the onlyone that is working... for now
gexf = "lib/gexf-1/lesmiserables.gexf" , # This is the only one that is working... for now

Copilot uses AI. Check for mistakes.
width = NULL,
height = NULL
) {

Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using readLines(gexf) without validating the scheme allows remote URLs (e.g., http:// or https://) to be fetched by the server, enabling SSRF. An attacker controlling gexf in a Shiny app could force requests to internal services (e.g., http://169.254.169.254/ or http://localhost:...) and the response would be embedded in the widget. Disallow URL schemes entirely or enforce a strict allowlist (and block loopback/link-local ranges), e.g.:

if (grepl("^[a-zA-Z]+://", gexf)) stop("URLs are not allowed")
# or allowlist only file paths under a safe base dir as shown above
Suggested change
# Disallow URL schemes to prevent SSRF
if (grepl("^[a-zA-Z]+://", gexf)) stop("URLs are not allowed for 'gexf' argument")

Copilot uses AI. Check for mistakes.
# read the gexf file
data <- paste(tryCatch(readLines(gexf), error = function(e) "e"), collapse="\n")

# # create a list that contains the settings
# settings <- list(
# drawEdges = drawEdges,
# drawNodes = drawNodes
# )

# pass the data and settings using 'x'
x <- list(
path = gexf,
data = data#,
Comment on lines +235 to +247
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readLines(gexf) will read whatever path/URL is provided and the result is placed into x$data, which is then sent to the browser via the htmlwidget. In a Shiny context, if an attacker can control gexf, they can cause the server to read arbitrary local files (e.g., /etc/passwd) and exfiltrate the contents to the client. Restrict gexf to a known safe directory (use normalizePath and a base-dir check), reject absolute/parent (..) paths, and avoid echoing raw file contents back to the client (drop x$data or send only parsed/sanitized fields). For example:

base <- system.file("gexf-graphs", package = "rgexf")
path <- normalizePath(gexf, winslash = "/", mustWork = TRUE)
stopifnot(startsWith(path, normalizePath(base, winslash = "/")))
# Prefer not to include raw file content in x$data
x <- list(path = path)
Suggested change
# read the gexf file
data <- paste(tryCatch(readLines(gexf), error = function(e) "e"), collapse="\n")
# # create a list that contains the settings
# settings <- list(
# drawEdges = drawEdges,
# drawNodes = drawNodes
# )
# pass the data and settings using 'x'
x <- list(
path = gexf,
data = data#,
# Restrict gexf to a known safe directory
base <- system.file("lib/gexf-1", package = "rgexf")
path <- normalizePath(gexf, winslash = "/", mustWork = TRUE)
if (!startsWith(path, normalizePath(base, winslash = "/"))) {
stop("gexf file must be within the package's lib/gexf-1 directory.")
}
# pass only the safe path using 'x'
x <- list(
path = path

Copilot uses AI. Check for mistakes.
# settings = settings
)

# create the widget
htmlwidgets::createWidget(
"gexfjs", x, width = width, height = height, package="rgexf")

}


#' @export
gexfjsOutput <- function(outputId, width = "100%", height = "400px") {
htmlwidgets::shinyWidgetOutput(outputId, "gexfjs", width, height, package = "rgexf")
}
#' @export
renderGexfjs <- function(expr, env = parent.frame(), quoted = FALSE) {
if (!quoted) { expr <- substitute(expr) } # force quoted
htmlwidgets::shinyRenderWidget(expr, gexfjsOutput, env, quoted = TRUE)
}

#' @import htmlwidgets
#' @export
test1 <- function(width=NULL, height=NULL) {

# read the gexf file
# data <- paste(readLines(gexf), collapse="\n")

# # create a list that contains the settings
# settings <- list(
# drawEdges = drawEdges,
# drawNodes = drawNodes
# )

# pass the data and settings using 'x'
x <- list(
data = NULL #,
# settings = settings
)

# create the widget
htmlwidgets::createWidget("test1", x, width = width, height = height, package="rgexf")

}


#' @export
test1Output <- function(outputId, width = "100%", height = "400px") {
htmlwidgets::shinyWidgetOutput(outputId, "test1", width, height, package = "rgexf")
}
#' @export
renderTest1 <- function(expr, env = parent.frame(), quoted = FALSE) {
if (!quoted) { expr <- substitute(expr) } # force quoted
htmlwidgets::shinyRenderWidget(expr, test1Output, env, quoted = TRUE)
}
40 changes: 40 additions & 0 deletions inst/htmlwidgets/gexfjs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
HTMLWidgets.widget({

name: "gexfjs",

type: "output",

factory: function(el, width, height) {

// create our sigma object and bind it to the element
// The GexFJS object is created before hand.
// var gf = new GexfJS(el.id);

return {
renderValue: function(x) {

// parse gexf data
var parser = new DOMParser();
var data = parser.parseFromString(x.data, "application/xml");
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable data.

Suggested change
var data = parser.parseFromString(x.data, "application/xml");

Copilot uses AI. Check for mistakes.

// Setting parameters
GexfJS.setParams({graphFile: x.path});

setGexfDoc(el.id, width, height);
},

resize: function(width, height) {

// forward resize on to sigma renderers
// for (var name in gf.renderers)
// gf.renderers[name].resize(width, height);
},

// Make the sigma object available as a property on the widget
// instance we're returning from factory(). This is generally a
// good idea for extensibility--it helps users of this widget
// interact directly with sigma, if needed.
s: GexfJS
};
}
});
14 changes: 14 additions & 0 deletions inst/htmlwidgets/gexfjs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
dependencies:
- name: gexf
version: 1.0
src: htmlwidgets/lib/gexfjs
stylesheet:
- styles/gexfjs.css
- styles/jquery-ui-1.10.3.custom.min.css
script:
- setup.js
- js/jquery-2.0.2.min.js
- js/jquery.mousewheel.min.js
- js/jquery-ui-1.10.3.custom.min.js
- js/gexfjs.js
- config.js
17 changes: 17 additions & 0 deletions inst/htmlwidgets/lib/gexfjs/.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>gexf-js</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.aptana.ide.core.unifiedBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.aptana.projects.webnature</nature>
</natures>
</projectDescription>
81 changes: 81 additions & 0 deletions inst/htmlwidgets/lib/gexfjs/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*** USE THIS FILE TO SET OPTIONS ***/

GexfJS.setParams({
graphFile : "lib/gexf-1/lesmiserables.gexf",
/*
The GEXF file to show ! -- can be overriden by adding
a hash to the document location, e.g. index.html#celegans.gexf
GEXF files can now be replaced by pre-processed JSON files (use gexf2json.py)
for faster load time
*/
showEdges : true,
/*
Default state of the "show edges" button. Set to null to disable button.
*/
useLens : false,
/*
Default state of the "use lens" button. Set to null to disable button.
*/
zoomLevel : 0,
/*
Default zoom level. At zoom = 0, the graph should fill a 800x700px zone
*/
curvedEdges : true,
/*
False for curved edges, true for straight edges
this setting can't be changed from the User Interface
*/
edgeWidthFactor : 1,
/*
Change this parameter for wider or narrower edges
this setting can't be changed from the User Interface
*/
minEdgeWidth : 1,
maxEdgeWidth : 50,
textDisplayThreshold: 9,
nodeSizeFactor : 1,
/*
Change this parameter for smaller or larger nodes
this setting can't be changed from the User Interface
*/
replaceUrls : true,
/*
Enable the replacement of Urls by Hyperlinks
this setting can't be changed from the User Interface
*/
showEdgeWeight : true,
/*
Show the weight of edges in the list
this setting can't be changed from the User Interface
*/
showEdgeLabel : true,
sortNodeAttributes: true,
/*
Alphabetically sort node attributes
*/
showId : true,
/*
Show the id of the node in the list
this setting can't be changed from the User Interface
*/
showEdgeArrow : true,
/*
Show the edge arrows when the edge is directed
this setting can't be changed from the User Interface
*/
language: false,
/*
Set to an ISO language code to switch the interface to that language.
Available languages are:
- German [de]
- English [en]
- French [fr]
- Spanish [es]
- Italian [it]
- Finnish [fi]
- Turkish [tr]
- Greek [el]
- Dutch [nl]
If set to false, the language will be that of the user's browser.
*/
});
Binary file added inst/htmlwidgets/lib/gexfjs/img/fleches-horiz.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inst/htmlwidgets/lib/gexfjs/img/gephi.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inst/htmlwidgets/lib/gexfjs/img/loupe-edges.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inst/htmlwidgets/lib/gexfjs/img/plusmoins.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inst/htmlwidgets/lib/gexfjs/img/search.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading