-
Mathieu Loiseau authoredaec71c70
class.R 23.32 KiB
library(R6)
library(httr)
library(stringr)
#'Mediawikir: write R output to mediawiki
#'
#'@docType class
#'@importFrom R6 R6Class
#'@importFrom httr POST
#'@importFrom httr stop_for_status
#'@importFrom httr upload_file
#'@importFrom httr content
#'@importFrom stringr str_replace
#'@export
#'@keywords mediawiki
#'@description A Mediawikir controller serves to connect to a mediawiki API as a registered user in order to edit automatically certain documents based on R scripts
#'@format An \code{\link{R6Class}} generator object
#'@return Object of \code{\link{R6Class}} with methods to interact with mediawiki instance
#'@field base_url the mediawiki instance base url
#'@field api the mediawiki instance API base url
#'@field auth_token the current token used to edit the wiki (cf. \href{https://www.mediawiki.org/wiki/Manual:Edit_token}{Mediawiki API documentation})
#'@field page the page (name or id) which will be modified (including its namespace, if the name is provided)
#'@field content the future content of \code{page}, which will be incrementally constructed using object methods
#'@field user_name the user to credit for the changes
#'@examples
#' mwHandler <- Mediawikir$new("http://my.wiki.org", "reg_user", "mypassword")
#' mwHandler$setPage("wikiPage")
#' mwHandler$addContent("This is an introduction to Mediawikir")
#' mwHandler$addContent("First step","h2")
#' mwHandler$addContent("First step of the first step","h3")
#' fileName <- mwHandler$uploadFile("path/toFile.png","My beautiful image.png")
#' mwHandler$addContent(paste("[[",fileName,"]]",sep=""))
#' mwHandler$postContent()
#' mwHandler$replaceSingleLineTemplateField("attribute", "newValue")
#'@section Methods:
#'\describe{
#' \item{\code{initialize(instance_url, user, pass)}}{Creates a new \pkg{Mediawikir} object out of:
#' \describe{
#' \item{\code{instance_url}}{The base url to the mediawiki instance}
#' \item{\code{user}}{The screen name of the user who will be credited for the changes in the wiki}
#' \item{\code{pass}}{The user's password}
#' \item{\code{dir}}{In case of \href{https://www.mediawiki.org/wiki/Manual:Short_URL}{short Url configuration},
#' the base url for mediawiki is not the actual directory containing the api and index.php files. In these cases,
#' the user should specify the installation directory of mediawiki from the base_url.}}}
#' \item{\code{setPage(pageNameOrId)}}{To set the page for the next post
#' \describe{
#' \item{\code{pageNameOrId}}{The name (or id) of the wiki page which contains the changes}
#' }}
#' \item{\code{connect(pass)}}{Should not be called directly (see \code{initialize} for detail)}
#' \item{\code{resetContent(baseContent="")}}{To reset the content of the page for the next post
#' \describe{
#' \item{\code{baseContent}}{The wikitext base for the next post}
#' }}
#' \item{\code{addContent(theContent, type="p")}}{To add content that will later on be posted to mediawiki. This is where structuring elements are provided
#' \describe{
#' \item{\code{theContent}}{Either text (using mediawiki syntax) or path to a file}
#' \item{\code{type}}{\describe{
#' \item{\code{p} → A plain paragraph.}{}
#' \item{\code{h2} → Level 2 title}{}
#' \item{\code{h3} → Level 3 title}{}
#' \item{\code{h4} → Level 4 title}{}
#' \item{\code{h5} → Level 5 title}{}
#' \item{\code{raw} → No editing of the content}{}
#' }}
#' }}
#' \item{\code{postContent(comment)}}{To replace the content of the \code{page} (attribute) with \code{content} (attribute).
#' Parameter \code{comment} specifies the description of the modification, default is \code{“R generated content”}}
#' \item{\code{uploadFile(file_path, file_name)}}{To upload a file to the server (will overwrite existing files)
#' \describe{
#' \item{\code{file_path}}{Path to the file to upload to the server}
#' \item{\code{file_name}}{Name under which to store it}
#' \item{Returns the title of the wiki page containing the file or \code{FALSE} on error}{}
#' }}
#' \item{\code{getContent()}}{Returns the \emph{wikitext} of the selected page (cf. \code{setPage})}
#' \item{\code{replaceSingleLineTemplateField(template, field, newValue)}}{To replace a field of a template in the selected \code{page}.
#'
#' This method does not work with numbered parameters. If the sought field does not exist, adds it at the beginning of the template.
#'
#' Right now it works only for pages that only contain one occurence of the parameter (it does not take outer context into account)
#'
#' Additionnally, this function is not made for templates the value of which spans accross multiple lines.
#' \describe{
#' \item{\code{template}}{The template to look for}
#' \item{\code{field}}{The name of the field concerned}
#' \item{\code{newValue}}{The new value of the field}
#' }}
#'}
Mediawikir <- R6Class("Mediawikir",
public = list(
base_url = NULL,
api = NULL,
user_name = NULL,
auth_token = NULL,
page = "",
content = "",
connect = function(pass){
token <- 1
"une super méthode"
tryCatch(
{ #get login token
query <- httr::POST(self$api,
body = list(action="query",
meta="tokens",
type="login",
lgname = self$user_name,
format= "json"))
httr::stop_for_status(query)
#parse query response to retrieve token
response <- httr::content(query, "parsed", "application/json")
token <- response$query$tokens$logintoken
#login query
query <- httr::POST(self$api,
body = list(action="clientlogin",
username = self$user_name,
password = pass,
logintoken = token,
loginreturnurl = self$base_url,
format= "json"))
httr::stop_for_status(query)
#parse query response to make sure login worked
response <- httr::content(query, "parsed", "application/json")
if(response$clientlogin$status!="PASS"){
stop(paste(response$clientlogin,"\n"))
}
},
error = function(e)
{
print(paste("Could not login",self$user_name,"to", self$api, e$message))
token <- 0
}
)
return(token)
},
######
# initialize
######
initialize = function (instance_url, user, pass, dir=""){
tryCatch({
if(dir !=""){
self$extra_sep <- paste(instance_url,"/",dir ,sep="")
}
else{
self$base_url <- instance_url
}
self$api <- paste(self$base_url,"/","api.php",sep="")
self$user_name <- user
self$auth_token <- self$connect(pass)
},
error = function(e){
print(e$message)
self <- FALSE
})
},
######
# setPage
######
setPage = function(pageNameOrId){
self$page <- pageNameOrId
},
######
# resetContent
######
resetContent = function(aContent=""){
self$content <- aContent
},
######
# addContent
######
addContent = function(theContent, type="p"){
switch(type,
h2 = theContent <- paste("==", theContent, "=="),
h3 = theContent <- paste("===", theContent, "==="),
h4 = theContent <- paste("====", theContent, "===="),
h5 = theContent <- paste("=====", theContent, "====="),
p = theContent <- paste(theContent, "\n", sep="")
)
if(type != "raw"){
self$content <- paste(self$content, theContent, sep="\n")
}
else{
self$content <- paste(self$content, theContent, sep="")
}
},
######
# postContent
######
postContent = function(comment="R generated content"){
success <- TRUE
tryCatch(
{ #Verify that a page has been selected
if(self$page == ""){
warning("No page to write to, use “setPage” method before calling “postContent”.")
}
else{
if(is.character(self$page)){
fields <- list(action = "query",
prop = "info|revisions",
meta = "tokens",
rvprop = "timestamp",
titles = self$page,
format = "json")
}
else{
fields <- list(action = "query",
prop = "info|revisions",
meta = "tokens",
rvprop = "timestamp",
pageids = self$page,
format = "json")
}
#get edit token
query <- httr::POST(self$api,
body = fields)
httr::stop_for_status(query)
response <- httr::content(query, "parsed", "application/json")
#perform edit
if(is.character(self$page)){
fields <- list(action = "edit",
title = self$page,
#basetimestamp = "TODO",
summary = comment,
format = "json",
text = self$content,
token = response$query$tokens$csrftoken)
}
else{
fields <- list(action = "edit",
pageid = self$page,
#basetimestamp = "TODO",
summary = comment,
format = "json",
text = self$content,
token = response$query$tokens$csrftoken)
}
query <- httr::POST(self$api,
body = fields)
httr::stop_for_status(query)
response <- httr::content(query, "parsed", "application/json")
if(response$edit$result != "Success"){
stop(paste("Failed to edit ",self$page,"\n",response))
}
}
},
error = function(e){
print(e$message)
success <- FALSE
})
return(success)
},
######
# getContent
######
getContent = function(){
success <- TRUE
tryCatch(
{ #Verify that a page has been selected
if(self$page == ""){
warning("No page to update to, use “setPage” method before calling “getContent”.")
}
else{
if(is.character(self$page)){
fields <- list(action = "raw",
title = self$page)
}
else{
fields <- list(action = "raw",
curid = self$page)
}
#get edit token
query <- httr::POST(paste(self$base_url,"index.php",sep="/"),
body = fields)
httr::stop_for_status(query)
response <- httr::content(query)
success <- response
}
},
error = function(e){
print(e$message)
success <- FALSE
})
return(success)
},
######
#' Replaces a field of a template in the selected \code{page}.
#'
#' This method does not work with numbered parameters. If the sought field does not exist, adds it at the beginning of the template.
#'
#' Right now it works only for pages that only contain one occurence of the parameter (it does not take outer context into account)
#'
#' Additionnally, this function is not made for templates the value of which spans accross multiple lines.
#' @param field The name of the field concerned
#' @param newValue The new value of the field
#' @return A message logging operations
#' @examples
#' obj <- Mediawikir$new("http://wiki.lezinter.net", "user","pass","installDir")
#' obj$setPage("Title of the page to modify (or its ID)")
#' obj$replaceSingleLineTemplateField("attribute","new value")
#' #Will replace |attribute=* by |attribute=new value (or create it)
######
replaceSingleLineTemplateField = function(field="", newValue=""){
success <- self$getContent()
if(success != FALSE){
search <- paste("\\|", field, "\\=[^\\|\\}\\n]*", sep="")
replace <- paste("|", field, "=",newValue, sep="")
formerText <- success
newText <- str_replace(formerText, search, replace)
if(newText == formerText){
#The parameter does not exist, adding it as a first parameter
success <- paste("Could not find field “", field, "”. Adding it as a first parameter → ", sep="")
newText <- str_replace(formerText, "(\\{\\{[^\\|\\n]*)(\\s|\\n)*\\|", paste("\\1\n |", field, "=",newValue,"\n |",sep=""))
}
else{
success <- ""
}
if(newText != formerText){
self$resetContent()
self$addContent(newText, "raw")
success <- paste(success,self$postContent(paste(field," → ", newValue, sep="")))
}
else{
success <- paste(success,"No modification of ",self$page," while trying to set “",field,"” to “",newValue,"”.",sep="")
}
}
return(success)
},
######
# uploadFile
######
uploadFile = function(file_path, file_name){
success <- TRUE
tryCatch(
{ #get edit token
query <- httr::POST(self$api,
body = list(action = "query",
prop = "info|revisions",
meta = "tokens",
rvprop = "timestamp",
titles = paste("file:",file_name,sep=""),
format = "json"))
httr::stop_for_status(query)
response <- httr::content(query, "parsed", "application/json")
#perform edit
the_file <- upload_file(file_path)
query <- httr::POST(self$api,
body = list(action = "upload",
filename = file_name,
comment = "R generated content",
format = "json",
ignorewarnings = 1,
file = the_file,
token = response$query$tokens$csrftoken,
ignorewarnings = 1),
encode = "multipart")
httr::stop_for_status(query)
response <- httr::content(query, "parsed", "application/json")
if(response$upload$result != "Success"){
stop(paste("Failed to upload ",file_path,"\n",response))
}
else{
success <- paste("File:",file_name,sep="")
}
},
error = function(e){
print(e$message)
success <- FALSE
})
return(success)
}
)
)
#'Batchator: Batch input to mediawiki based on a csv and a template
#'
#'@docType class
#'@export
#'@keywords mediawiki
#'@description Uses \link{Mediawikir} to create/edit a batch of mediawiki pages
#'@format An \code{\link{R6Class}} generator object
#'@return Object of \code{\link{R6Class}} with methods to interact with mediawiki instance
#'@field author the mediawiki user who will be the author of the pages/modifications
#'@field wiki a mediawikir object
#'@field page_name_pattern a pattern to name the pages generated
#'@field template the content of a text file with the template for the wikitext of the pages to create in a batch
#'@field field_list the list of symbols that will be replaced (the first line of the csv file that will be sent to the Batchator)
#'@field content a matrix of the words that will replace the symbols in the templates (both content and page names — the next lines of the csv file)
#'@examples
#' bachata <- Batchator$new("http://my.wiki", "user","pass")
#' bachata$loadTemplate("/path/to/template.wiki")
#' bachata$loadContent("/path/to/content.csv")
#' bachata$setPageNamePattern("Project:Media/Sounds/~word~ (~voice_artist~)")
#' bachata$applyTemplate()
#' #content of "template.wiki" : {{menuLUCIOLE}}
#' # {{Sound info
#' # |title=~word~ (~voice_artist~)
#' # |type=word
#' # |text=~word~
#' # |language=en
#' # |character=~char~
#' # |validation=recorded
#' # |theme=~theme~}}
#' #content of "content.csv"
#' #"~word~","~theme~","~char~","~voice_artist~" ← the field list (see they are used in the templates above)
#' #"Zero","numbers","Awful Owen","Carlos"
#' #"One","numbers","Awful Owen","Carlos"
#' #"One","numbers","Black Betty","Carla"
#' #3 pages are created in this example…
#' batchata$modifyWikiTemplateAttributes("list.csv")
#' #content of "list.csv"
#' #"Page","Att1","Att2"
#' #2204,"New value for att1","New value for att2"
#' #"Help:full/page/Name","New value for att1","New value for att2"
#'@section Methods:
#'\describe{
#' \item{\code{initialize(wiki,author,pass)}}{Creates a new Batchator object (and its Mediawikir object as an attribute)
#' \describe{
#' \item{\code{wiki}}{The base url to the mediawiki instance}
#' \item{\code{authr}}{The screen name of the user who will be credited for the changes in the wiki}
#' \item{\code{pass}}{The user's password}
#' \item{\code{dir}}{The installation directory of mediawiki, if needed}
#' }}
#' \item{\code{loadTemplate(file_path)}}{Takes the path of a text file containing a template that will serve as base
#' for all the pages generated. The template should contain occurrences of the fields defined in the content file.}
#' \item{\code{setPageNamePattern(naming_pattern)}}{Takes a string of the parttern for the page names that will be generated.
#' The template should contain occurrences of the fields defined in the content file.}
#' \item{\code{loadContent(file_path)}}{Takes the path to a csv file that contains both the list of fields used in the templates (1st line)
#' AND the data for each of those fields (following lines).}
#' \item{\code{applyTemplate()}}{Applies the templates defined using \code{loadTemplate}, \code{setPageNamePattern} to the data loaded with \code{loadContent}.
#' It should be noted that if the page exists it is replaced.}
#' \item{\code{modifyWikiTemplateAttributes(file_path)}}{Reads the csv file pointed by file_path and replaces in each page listed in the Page column the content
#' first occurence of each field with the corresponding value.
#'
#' \strong{Warning:} This function is dependant on \code{\link{replaceSingleLineTemplateField}}, which is very basic.}
#'}
#'@importFrom stringr str_replace_all
Batchator <- R6Class("Batchator",
public = list(
author = NULL,
wiki = NULL,
page_name_pattern=NULL,
template = NULL,
field_list = NULL,
content = NULL,
######
# initialize
######
initialize = function (wiki,author,pass,dir=""){
tryCatch({
self$wiki <- Mediawikir$new(wiki, author, pass, dir)
},
error = function(e){
print(e$message)
self <- FALSE
})
},
######
# loadTemplate
######
loadTemplate = function(file_path){
tryCatch({
self$template <- readChar(file_path, file.info(file_path)$size)
},
error = function(e){
print(paste(e$message, "No template loaded"))
})
},
######
# setPageNamePattern
######
setPageNamePattern = function(naming_pattern){
self$page_name_pattern = naming_pattern
},
######
# loadContent
######
loadContent = function(file_path){
tryCatch({
csvM <- read.csv(file=file_path, header = FALSE)
if(ncol(csvM) == 1){
self$field_list <- as.vector(csvM[1,])
self$content <- as.vector(csvM[-1,])
}
else{
self$field_list <- as.vector(t(csvM[1,]))
self$content <- csvM[-1,]
}
},
error = function(e){
print(paste(e$message, "Batchator:loadContent → No content loaded"))
})
},
######
# applyTemplate
######
applyTemplate = function(){
tryCatch({
if (is.null(self$content) |
is.null(self$field_list) |
is.null(self$page_name_pattern) |
is.null(self$template)) {
warning("Either no content or no templates, use loadContent, setPageNamePattern and/or loadTemplate.")
}
else{
if(!is.null(nrow(self$content))){
for(i in 1:nrow(self$content)){
self$wiki$resetContent(str_replace_all(self$template,
setNames(as.vector(t(self$content[i,])),
self$field_list)))
self$wiki$setPage(str_replace_all(self$page_name_pattern,
setNames(as.vector(t(self$content[i,])),
self$field_list)))
self$wiki$postContent()
}
}
else{
for(i in 1:length(self$content)){
self$wiki$resetContent(str_replace_all(self$template,
setNames(as.vector(t(self$content[i])),
self$field_list)))
self$wiki$setPage(str_replace_all(self$page_name_pattern,
setNames(as.vector(t(self$content[i])),
self$field_list)))
self$wiki$postContent()
}
}
}
},
error = function(e){
print(e$message)
})
},
######
# modifyWikiTemplateAttributes
######
modifyWikiTemplateAttributes = function(file_path){
tryCatch({
csvM <- read.csv(file=file_path, header = FALSE)
if(ncol(csvM) == 1){
stop(paste("'",file_path,"' only has one column.",sep=""))
}
else{
field_list <- as.vector(t(csvM[1,]))
if( (field_list[1] != "Page")
&& (field_list[1] != "page")){
stop(paste("1st column name of '",file_path,"' should be “Page” (",field_list[1],").",sep=""))
}
rows <- csvM[-1,]
for(i in 1:nrow(rows)){
pageId <- toString(rows[i,1])
pageIdType <- "page name"
if(grepl("^[0-9]+$", pageId, perl = T)){
pageId <- as.integer(pageId)
pageIdType <- "page ID"
}
self$wiki$setPage(pageId)
print(paste("processing page '",pageId,"' (",pageIdType,")", sep=""))#/**/
for(j in 2:length(field_list)){
#self$wiki$addContent("test") #debug
#test = self$wiki$postContent() #debug
#print(test) #debug
success = self$wiki$replaceSingleLineTemplateField(toString(field_list[j]),toString(rows[i,j]))
print(paste("new value : |",field_list[j],"=",rows[i,j]," → ",success,sep=""))
}
}
}
},
error = function(e){
print(paste(e$message, "Batchator.modifyWikiTemplateAttributes → No content loaded"))
})
}
)
)