| 1 |
-------------------------------------------------------------------------------- |
|---|
| 2 |
-- Title: WikiControlService.lua |
|---|
| 3 |
-- Description: Like a square peg in a round hole |
|---|
| 4 |
-- Author: Raphaël Szwarc http://alt.textdrive.com/lua/ |
|---|
| 5 |
-- Creation Date: January 30, 2007 |
|---|
| 6 |
-- Legal: Copyright (C) 2007 Raphaël Szwarc |
|---|
| 7 |
-- Under the terms of the MIT License |
|---|
| 8 |
-- http://www.opensource.org/licenses/mit-license.html |
|---|
| 9 |
-------------------------------------------------------------------------------- |
|---|
| 10 |
|
|---|
| 11 |
-- import dependencies |
|---|
| 12 |
local HTTP = require( 'HTTP' ) |
|---|
| 13 |
local HTTPExtra = require( 'HTTPExtra' ) |
|---|
| 14 |
local HTTPService = require( 'HTTPService' ) |
|---|
| 15 |
local Template = require( 'Template' ) |
|---|
| 16 |
local Wiki = require( 'Wiki' ) |
|---|
| 17 |
local WikiContent = require( 'WikiContent' ) |
|---|
| 18 |
local WikiContentService = require( 'WikiContentService' ) |
|---|
| 19 |
local WikiService = require( 'WikiService' ) |
|---|
| 20 |
|
|---|
| 21 |
local BaseLink = WikiService.BaseLink |
|---|
| 22 |
local DateLink = WikiService.DateLink |
|---|
| 23 |
local FeedLink = WikiService.FeedLink |
|---|
| 24 |
local IndexLink = WikiService.IndexLink |
|---|
| 25 |
|
|---|
| 26 |
local Encode = WikiService.Encode |
|---|
| 27 |
local Path = WikiService.Path |
|---|
| 28 |
|
|---|
| 29 |
local os = require( 'os' ) |
|---|
| 30 |
|
|---|
| 31 |
local error = error |
|---|
| 32 |
local getmetatable = getmetatable |
|---|
| 33 |
local ipairs = ipairs |
|---|
| 34 |
local pairs = pairs |
|---|
| 35 |
local pcall = pcall |
|---|
| 36 |
local require = require |
|---|
| 37 |
local setmetatable = setmetatable |
|---|
| 38 |
local tonumber = tonumber |
|---|
| 39 |
local tostring = tostring |
|---|
| 40 |
local type = type |
|---|
| 41 |
|
|---|
| 42 |
-------------------------------------------------------------------------------- |
|---|
| 43 |
-- WikiControlService |
|---|
| 44 |
-------------------------------------------------------------------------------- |
|---|
| 45 |
|
|---|
| 46 |
module( 'WikiControlService' ) |
|---|
| 47 |
_VERSION = '1.0' |
|---|
| 48 |
|
|---|
| 49 |
local self = setmetatable( _M, {} ) |
|---|
| 50 |
local meta = getmetatable( self ) |
|---|
| 51 |
|
|---|
| 52 |
-------------------------------------------------------------------------------- |
|---|
| 53 |
-- Utilities |
|---|
| 54 |
-------------------------------------------------------------------------------- |
|---|
| 55 |
|
|---|
| 56 |
local function Message( self ) |
|---|
| 57 |
local aMessage = self.message |
|---|
| 58 |
|
|---|
| 59 |
if aMessage then |
|---|
| 60 |
local someTypes = { authorization = 'warning', check = 'error', count = 'warning', delete = 'error', rename = 'error', time = 'warning', token = 'error', value = 'warning' } |
|---|
| 61 |
local aType = someTypes[ self.type ] or 'warning' |
|---|
| 62 |
local WikiMessage = require( 'WikiMessage' ) |
|---|
| 63 |
local aMessage = WikiMessage( aMessage, aType ) |
|---|
| 64 |
|
|---|
| 65 |
return aMessage |
|---|
| 66 |
end |
|---|
| 67 |
end |
|---|
| 68 |
|
|---|
| 69 |
local function Token() |
|---|
| 70 |
local Token = require( 'Token' ) |
|---|
| 71 |
local aToken = Token( HTTP.request.url ) |
|---|
| 72 |
local aList = { 'check', 'count', 'delete', 'rename', 'time', 'token', 'value' } |
|---|
| 73 |
local aMap = {} |
|---|
| 74 |
|
|---|
| 75 |
for _, aValue in ipairs( aList ) do |
|---|
| 76 |
aMap[ aValue ] = aToken[ aValue ] |
|---|
| 77 |
end |
|---|
| 78 |
|
|---|
| 79 |
return aMap |
|---|
| 80 |
end |
|---|
| 81 |
|
|---|
| 82 |
local function Context() |
|---|
| 83 |
local someTypes = { check = nil, count = 'number', delete = 'string', rename = 'string', time = 'number', token = 'string', value = 'string' } |
|---|
| 84 |
local someFunctions = { check = nil, count = tonumber, delete = tostring, rename = tostring, time = tonumber, token = tostring, value = tostring } |
|---|
| 85 |
local aToken = Token() |
|---|
| 86 |
local aParameter = HTTP.request.parameter |
|---|
| 87 |
local aContext = {} |
|---|
| 88 |
|
|---|
| 89 |
for aKey, aValue in pairs( aToken ) do |
|---|
| 90 |
local aType = someTypes[ aKey ] |
|---|
| 91 |
local aFunction = someFunctions[ aKey ] |
|---|
| 92 |
local aValue = aParameter[ aValue ] |
|---|
| 93 |
|
|---|
| 94 |
if aValue and aFunction then |
|---|
| 95 |
aValue = aFunction( aValue ) |
|---|
| 96 |
end |
|---|
| 97 |
|
|---|
| 98 |
if aValue and tostring( aValue ):len() == 0 then |
|---|
| 99 |
aValue = nil |
|---|
| 100 |
end |
|---|
| 101 |
|
|---|
| 102 |
if not aType or type( aValue ) == aType then |
|---|
| 103 |
aContext[ aKey ] = aValue |
|---|
| 104 |
end |
|---|
| 105 |
end |
|---|
| 106 |
|
|---|
| 107 |
return aContext |
|---|
| 108 |
end |
|---|
| 109 |
|
|---|
| 110 |
local function Validate( aKey, aValue, aName ) |
|---|
| 111 |
local aContext = Context() |
|---|
| 112 |
local aToken = Token() |
|---|
| 113 |
local anAuthorization = HTTP.request.authorization |
|---|
| 114 |
|
|---|
| 115 |
if aContext.check then |
|---|
| 116 |
return false, 'check', 'Invalid data. Try again.' |
|---|
| 117 |
end |
|---|
| 118 |
|
|---|
| 119 |
if not aContext.time or ( os.time() - aContext.time ) < 3 then |
|---|
| 120 |
return false, 'time', 'Too fast. Try again, but slowly.' |
|---|
| 121 |
end |
|---|
| 122 |
|
|---|
| 123 |
if not aContext.token or aContext.token ~= aToken.token then |
|---|
| 124 |
return false, 'token', 'Invalid data. Try again.' |
|---|
| 125 |
end |
|---|
| 126 |
|
|---|
| 127 |
if aKey == 'rename' |
|---|
| 128 |
and ( not aContext.value |
|---|
| 129 |
or not WikiContent[ aContext.value ] |
|---|
| 130 |
or WikiContent[ aContext.value ] == aName ) then |
|---|
| 131 |
return false, 'name', 'Invalid name. Try to a different one.' |
|---|
| 132 |
end |
|---|
| 133 |
|
|---|
| 134 |
if aValue ~= Token()[ aKey ] then |
|---|
| 135 |
return false, aKey, 'Invalid data. Try again.' |
|---|
| 136 |
end |
|---|
| 137 |
|
|---|
| 138 |
if aKey == 'delete' |
|---|
| 139 |
and ( not aContext.count or aContext.count < 2 ) then |
|---|
| 140 |
local aMessage = ( 'Are you sure you want to %s “%s”? If yes, press “%s” again.' ):format( aKey, aName, aKey ) |
|---|
| 141 |
|
|---|
| 142 |
return false, 'count', aMessage |
|---|
| 143 |
end |
|---|
| 144 |
|
|---|
| 145 |
if aKey == 'rename' |
|---|
| 146 |
and ( not aContext.count or aContext.count < 2 ) then |
|---|
| 147 |
local aValue = WikiContent[ aContext.value ] |
|---|
| 148 |
local aMessage = ( 'Are you sure you want to %s “%s” to “%s”? If yes, press “%s” again.' ):format( aKey, aName, aValue, aKey ) |
|---|
| 149 |
|
|---|
| 150 |
return false, 'count', aMessage |
|---|
| 151 |
end |
|---|
| 152 |
|
|---|
| 153 |
if not anAuthorization.user |
|---|
| 154 |
or not anAuthorization.password |
|---|
| 155 |
or anAuthorization.password ~= aName then |
|---|
| 156 |
local aMessage = ( 'Invalid authorization. Please identify yourself, using “%s” as the password.' ):format( aName ) |
|---|
| 157 |
|
|---|
| 158 |
HTTP.response.authorization.realm = 'Nanoki' |
|---|
| 159 |
|
|---|
| 160 |
return false, 'authorization', aMessage |
|---|
| 161 |
end |
|---|
| 162 |
|
|---|
| 163 |
if aName == 'log' then |
|---|
| 164 |
return false, aKey, 'Sorry. Cannot do that.' |
|---|
| 165 |
end |
|---|
| 166 |
|
|---|
| 167 |
return true |
|---|
| 168 |
end |
|---|
| 169 |
|
|---|
| 170 |
-------------------------------------------------------------------------------- |
|---|
| 171 |
-- Service methods |
|---|
| 172 |
-------------------------------------------------------------------------------- |
|---|
| 173 |
|
|---|
| 174 |
function self:get() |
|---|
| 175 |
local aLayoutTemplate = Template[ 'WikiLayout.txt' ] |
|---|
| 176 |
local aTemplate = Template[ 'WikiControlService.txt' ] |
|---|
| 177 |
local aContent = self.content |
|---|
| 178 |
local aDate = os.date( '!*t', aContent.creation ) |
|---|
| 179 |
local aDateLink = DateLink( aDate.year, aDate.month, aDate.day ) |
|---|
| 180 |
local aContext = Context() |
|---|
| 181 |
local aToken = Token() |
|---|
| 182 |
|
|---|
| 183 |
aTemplate[ 'actionPath' ] = Encode( HTTP.request.url.path ) |
|---|
| 184 |
aTemplate[ 'title' ] = Encode( aContent.data.title .. ' — Control' ) |
|---|
| 185 |
aTemplate[ 'message' ] = Message( self ) |
|---|
| 186 |
aTemplate[ 'name' ] = Encode( aContent.name ) |
|---|
| 187 |
|
|---|
| 188 |
aTemplate[ 'checkToken' ] = Encode( aToken.check ) |
|---|
| 189 |
aTemplate[ 'check' ] = Encode( aContext.check ) |
|---|
| 190 |
aTemplate[ 'countToken' ] = Encode( aToken.count ) |
|---|
| 191 |
aTemplate[ 'count' ] = Encode( ( aContext.count or 0 ) + 1 ) |
|---|
| 192 |
aTemplate[ 'deleteToken' ] = Encode( aToken.delete ) |
|---|
| 193 |
aTemplate[ 'renameToken' ] = Encode( aToken.rename ) |
|---|
| 194 |
aTemplate[ 'timeToken' ] = Encode( aToken.time ) |
|---|
| 195 |
aTemplate[ 'time' ] = Encode( os.time() ) |
|---|
| 196 |
aTemplate[ 'token' ] = Encode( aToken.token ) |
|---|
| 197 |
aTemplate[ 'valueToken' ] = Encode( aToken.value ) |
|---|
| 198 |
aTemplate[ 'value' ] = Encode( aContext.value or aContent.name ) |
|---|
| 199 |
|
|---|
| 200 |
aLayoutTemplate[ 'baseLink' ] = Encode( BaseLink() ) |
|---|
| 201 |
aLayoutTemplate[ 'indexLink' ] = Encode( IndexLink( aContent.prefix ) ) |
|---|
| 202 |
aLayoutTemplate[ 'dateLink' ] = Encode( aDateLink ) |
|---|
| 203 |
aLayoutTemplate[ 'feedLink' ] = FeedLink() |
|---|
| 204 |
aLayoutTemplate[ 'path' ] = Path( self ) |
|---|
| 205 |
aLayoutTemplate[ 'query' ] = nil |
|---|
| 206 |
aLayoutTemplate[ 'robot' ] = 'noindex,nofollow' |
|---|
| 207 |
aLayoutTemplate[ 'title' ] = Encode( aContent.data.title .. ' — Control' ) |
|---|
| 208 |
aLayoutTemplate[ 'content' ] = aTemplate |
|---|
| 209 |
|
|---|
| 210 |
return tostring( aLayoutTemplate ) |
|---|
| 211 |
end |
|---|
| 212 |
|
|---|
| 213 |
function self:postDelete( aToken ) |
|---|
| 214 |
local aContent = self.content |
|---|
| 215 |
local isValid, aType, aMessage = Validate( 'delete', aToken, aContent.name ) |
|---|
| 216 |
|
|---|
| 217 |
if isValid then |
|---|
| 218 |
local aName = aContent.name |
|---|
| 219 |
local aCall = function() |
|---|
| 220 |
local lfs = require( 'lfs' ) |
|---|
| 221 |
local aLogContent = WikiContent( 'log' ) |
|---|
| 222 |
local aLock = aLogContent.lock |
|---|
| 223 |
|
|---|
| 224 |
if aLock and lfs.lock( aLock, 'w' ) then |
|---|
| 225 |
local WikiEditorService = require( 'WikiEditorService' ) |
|---|
| 226 |
local aMessage = ( 'Deleted “%s”.' ):format( aName ) |
|---|
| 227 |
local aText = ( '%s \n_%s_ ' ):format( aLogContent.text, aMessage ) |
|---|
| 228 |
local aContext = { title = aLogContent.data.title, text = aText, version = aLogContent.version } |
|---|
| 229 |
local aService = WikiEditorService( aLogContent ) |
|---|
| 230 |
|
|---|
| 231 |
Wiki[ aName ] = false |
|---|
| 232 |
|
|---|
| 233 |
aService( aLogContent.name, aContext ) |
|---|
| 234 |
|
|---|
| 235 |
aLock:close() |
|---|
| 236 |
|
|---|
| 237 |
return nil, HTTPService[ WikiContentService( aLogContent ) ] |
|---|
| 238 |
end |
|---|
| 239 |
|
|---|
| 240 |
if aLock then |
|---|
| 241 |
aLock:close() |
|---|
| 242 |
end |
|---|
| 243 |
|
|---|
| 244 |
error( ( 'Failed to lock %q' ):format( aLogContent.name ) ) |
|---|
| 245 |
end |
|---|
| 246 |
local aStatus, aResult, aLocation = pcall( aCall ) |
|---|
| 247 |
|
|---|
| 248 |
if aStatus then |
|---|
| 249 |
return aResult, aLocation |
|---|
| 250 |
end |
|---|
| 251 |
|
|---|
| 252 |
self.message = aResult |
|---|
| 253 |
self.type = 'delete' |
|---|
| 254 |
end |
|---|
| 255 |
|
|---|
| 256 |
self.message = aMessage |
|---|
| 257 |
self.type = aType |
|---|
| 258 |
|
|---|
| 259 |
return self:get() |
|---|
| 260 |
end |
|---|
| 261 |
|
|---|
| 262 |
function self:postRename( aToken ) |
|---|
| 263 |
local aContent = self.content |
|---|
| 264 |
local isValid, aType, aMessage = Validate( 'rename', aToken, aContent.name ) |
|---|
| 265 |
|
|---|
| 266 |
if isValid then |
|---|
| 267 |
local aName = aContent.name |
|---|
| 268 |
local aNewName = Context()[ 'value' ] |
|---|
| 269 |
local aCall = function() |
|---|
| 270 |
Wiki[ aName ] = aNewName |
|---|
| 271 |
end |
|---|
| 272 |
local aStatus, aResult = pcall( aCall ) |
|---|
| 273 |
|
|---|
| 274 |
if aStatus then |
|---|
| 275 |
local WikiEditorService = require( 'WikiEditorService' ) |
|---|
| 276 |
local aNewContent = WikiContent( aNewName ) |
|---|
| 277 |
local aNewName = aNewContent.name |
|---|
| 278 |
local aMessage = ( 'Renamed from “%s” to “%s”.' ):format( aName, aNewName ) |
|---|
| 279 |
local aPattern = ( '%s/file/' ):format( aName ):gsub( '(%W)', '%%%1' ) |
|---|
| 280 |
local aNewPattern = ( '%s/file/' ):format( aNewName ):gsub( '%%', '%%%%' ) |
|---|
| 281 |
local aText = aNewContent.text:gsub( aPattern, aNewPattern ) |
|---|
| 282 |
local aText = ( '%s \n_%s_ ' ):format( aText, aMessage ) |
|---|
| 283 |
local aContext = { title = aNewContent.data.title, text = aText, version = aNewContent.version } |
|---|
| 284 |
local aService = WikiEditorService( aNewContent ) |
|---|
| 285 |
|
|---|
| 286 |
aService( aNewContent.name, aContext ) |
|---|
| 287 |
|
|---|
| 288 |
return nil, HTTPService[ WikiContentService( aNewContent ) ] |
|---|
| 289 |
end |
|---|
| 290 |
|
|---|
| 291 |
self.message = aResult |
|---|
| 292 |
self.type = 'rename' |
|---|
| 293 |
end |
|---|
| 294 |
|
|---|
| 295 |
self.message = aMessage |
|---|
| 296 |
self.type = aType |
|---|
| 297 |
|
|---|
| 298 |
return self:get() |
|---|
| 299 |
end |
|---|
| 300 |
|
|---|
| 301 |
-------------------------------------------------------------------------------- |
|---|
| 302 |
-- Metamethods |
|---|
| 303 |
-------------------------------------------------------------------------------- |
|---|
| 304 |
|
|---|
| 305 |
function meta:__call( aContent, aParent ) |
|---|
| 306 |
local aControl = { content = aContent, parent = aParent } |
|---|
| 307 |
|
|---|
| 308 |
setmetatable( aControl, self ) |
|---|
| 309 |
self.__index = self |
|---|
| 310 |
|
|---|
| 311 |
return aControl |
|---|
| 312 |
end |
|---|
| 313 |
|
|---|
| 314 |
function meta:__concat( aValue ) |
|---|
| 315 |
return tostring( self ) .. tostring( aValue ) |
|---|
| 316 |
end |
|---|
| 317 |
|
|---|
| 318 |
function meta:__tostring() |
|---|
| 319 |
return ( '%s/%s' ):format( self._NAME, self._VERSION ) |
|---|
| 320 |
end |
|---|
| 321 |
|
|---|
| 322 |
function self:__concat( aValue ) |
|---|
| 323 |
return tostring( self ) .. tostring( aValue ) |
|---|
| 324 |
end |
|---|
| 325 |
|
|---|
| 326 |
function self:__tostring() |
|---|
| 327 |
return 'Control' |
|---|
| 328 |
end |
|---|