summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Scribunto/includes/engines/LuaCommon/lualib/mw.lua
blob: 31102782d865e748451ab1d5874d055c00db2d8b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
mw = mw or {}

local packageCache
local packageModuleFunc
local php
local allowEnvFuncs = false
local logBuffer = ''
local currentFrame
local loadedData = {}
local executeFunctionDepth = 0

--- Put an isolation-friendly package module into the specified environment
-- table. The package module will have an empty cache, because caching of
-- module functions from other cloned environments would break module isolation.
--
-- @param env The cloned environment
local function makePackageModule( env )
	-- Remove loaders from env, we don't want it inheriting our loadPackage.
	if env.package then
		env.package.loaders = nil
	end

	-- Create the package globals in the given environment
	setfenv( packageModuleFunc, env )()

	-- Make a loader function
	local function loadPackage( modName )
		local init
		if packageCache[modName] == 'missing' then
			return nil
		elseif packageCache[modName] == nil then
			local lib = php.loadPHPLibrary( modName )
			if lib ~= nil then
				init = function ()
					return mw.clone( lib )
				end
			else
				init = php.loadPackage( modName )
				if init == nil then
					packageCache[modName] = 'missing'
					return nil
				end
			end
			packageCache[modName] = init
		else
			init = packageCache[modName]
		end

		setfenv( init, env )
		return init
	end

	table.insert( env.package.loaders, loadPackage )
end

--- Set up the base environment. The PHP host calls this function after any
-- necessary host-side initialisation has been done.
function mw.setupInterface( options )
	-- Don't allow any more calls
	mw.setupInterface = nil

	-- Don't allow getmetatable() on a non-table, since if you can get the metatable,
	-- you can set values in it, breaking isolation
	local old_getmetatable = getmetatable
	function getmetatable(obj)
		if type(obj) == 'table' then
			return old_getmetatable(obj)
		else
			return nil
		end
	end

	if options.allowEnvFuncs then
		allowEnvFuncs = true
	end

	-- Store the interface table
	--
	-- mw_interface.loadPackage() returns function values with their environment
	-- set to the base environment, which would violate module isolation if they
	-- were run from a cloned environment. We can only allow access to
	-- mw_interface.loadPackage via our environment-setting wrapper.
	--
	php = mw_interface
	mw_interface = nil

	packageModuleFunc = php.loadPackage( 'package' )
	makePackageModule( _G )
	package.loaded.mw = mw
	packageCache = {}
end

--- Create a table like the one os.date() returns, but with a metatable that sets TTLs as the values are looked at.
local function wrapDateTable( now )
	return setmetatable( {}, {
		__index = function( t, k )
			if k == 'sec' then
				php.setTTL( 1 )
			elseif k == 'min' then
				php.setTTL( 60 - now.sec )
			elseif k == 'hour' then
				php.setTTL( 3600 - now.min * 60 - now.sec )
			elseif now[k] ~= nil then
				php.setTTL( 86400 - now.hour * 3600 - now.min * 60 - now.sec )
			end
			t[k] = now[k]
			return now[k]
		end
	} )
end

--- Wrappers for os.date() and os.time() that set the TTL of the output, if necessary
local function ttlDate( format, time )
	if time == nil and ( format == nil or type( format ) == 'string' ) then
		local now = os.date( format and format:sub( 1, 1 ) == '!' and '!*t' or '*t' )
		if format == '!*t' or format == '*t' then
			return wrapDateTable( now )
		end
		local cleanedFormat = format and format:gsub( '%%%%', '' )
		if not format or cleanedFormat:find( '%%[EO]?[crsSTX+]' ) then
			php.setTTL( 1 ) -- second
		elseif cleanedFormat:find( '%%[EO]?[MR]' ) then
			php.setTTL( 60 - now.sec ) -- minute
		elseif cleanedFormat:find( '%%[EO]?[HIkl]' ) then
			php.setTTL( 3600 - now.min * 60 - now.sec ) -- hour
		elseif cleanedFormat:find( '%%[EO]?[pP]' ) then
			php.setTTL( 43200 - ( now.hour % 12 ) * 3600 - now.min * 60 - now.sec ) -- am/pm
		else
			-- It's not worth the complexity to figure out the exact TTL of larger units than days.
			-- If they haven't used anything shorter than days, then just set the TTL to expire at
			-- the end of today.
			php.setTTL( 86400 - now.hour * 3600 - now.min * 60 - now.sec )
		end
	end
	return os.date( format, time )
end

local function ttlTime( t )
	if t == nil then
		php.setTTL( 1 )
	end
	return os.time( t )
end

local function newFrame( frameId, ... )
	if not php.frameExists( frameId ) then
		return nil
	end

	local frame = {}
	local parentFrameIds = { ... }
	local argCache = {}
	local argNames
	local args_mt = {}

	local function checkSelf( self, method )
		if self ~= frame then
			error( "frame:" .. method .. ": invalid frame object. " ..
				"Did you call " .. method .. " with a dot instead of a colon, i.e. " ..
				"frame." .. method .. "() instead of frame:" .. method .. "()?",
				3 )
		end
	end

	-- Getter for args
	local function getExpandedArgument( dummy, name )
		name = tostring( name )
		if argCache[name] == nil then
			local arg = php.getExpandedArgument( frameId, name )
			if arg == nil then
				argCache[name] = false
			else
				argCache[name] = arg
			end
		end
		if argCache[name] == false then
			return nil
		else
			return argCache[name]
		end
	end

	args_mt.__index = getExpandedArgument

	-- pairs handler for args
	args_mt.__pairs = function ()
		if not argNames then
			local arguments = php.getAllExpandedArguments( frameId )
			argNames = {}
			for name, value in pairs( arguments ) do
				table.insert( argNames, name )
				argCache[name] = value
			end
		end

		local index = 0
		return function ()
			index = index + 1
			if argNames[index] then
				return argNames[index], argCache[argNames[index]]
			end
		end
	end

	-- ipairs 'next' function for args
	local function argsInext( dummy, i )
		local value = getExpandedArgument( dummy, i + 1 )
		if value then
			return i + 1, value
		end
	end

	args_mt.__ipairs = function () return argsInext, nil, 0 end

	frame.args = {}
	setmetatable( frame.args, args_mt )

	local function newCallbackParserValue( callback )
		local value = {}
		local cache

		function value:expand()
			if not cache then
				cache = callback()
			end
			return cache
		end

		return value
	end

	function frame:getArgument( opt )
		checkSelf( self, 'getArgument' )

		local name
		if type( opt ) == 'table' then
			name = opt.name
		else
			name = opt
		end

		return newCallbackParserValue(
			function ()
				return getExpandedArgument( nil, name )
			end
			)
	end

	function frame:getParent()
		checkSelf( self, 'getParent' )

		return newFrame( unpack( parentFrameIds ) )
	end

	local function checkArgs( name, args )
		local ret = {}
		for k, v in pairs( args ) do
			local tp = type( k )
			if tp ~= 'string' and tp ~= 'number' then
				error( name .. ": arg keys must be strings or numbers, " .. tp .. " given", 3 )
			end
			tp = type( v )
			if tp == 'boolean' then
				ret[k] = v and '1' or ''
			elseif tp == 'string' or tp == 'number' then
				ret[k] = tostring( v )
			else
				error( name .. ": invalid type " .. tp .. " for arg '" .. k .. "'", 3 )
			end
		end
		return ret
	end

	function frame:newChild( opt )
		checkSelf( self, 'newChild' )

		if type( opt ) ~= 'table' then
			error( "frame:newChild: the first parameter must be a table", 2 )
		end

		local title, args
		if opt.title == nil then
			title = false
		else
			title = tostring( opt.title )
		end
		if opt.args == nil then
			args = {}
		elseif type( opt.args ) ~= 'table' then
			error( "frame:newChild: args must be a table", 2 )
		else
			args = checkArgs( 'frame:newChild', opt.args )
		end

		local newFrameId = php.newChildFrame( frameId, title, args )
		return newFrame( newFrameId, frameId, unpack( parentFrameIds ) )
	end

	function frame:expandTemplate( opt )
		checkSelf( self, 'expandTemplate' )

		local title

		if type( opt ) ~= 'table' then
			error( "frame:expandTemplate: the first parameter must be a table" )
		end
		if opt.title == nil then
			error( "frame:expandTemplate: a title is required" )
		else
			if type( opt.title ) == 'table' and opt.title.namespace == 0 then
				title = ':' .. tostring( opt.title )
			else
				title = tostring( opt.title )
			end
		end
		local args
		if opt.args == nil then
			args = {}
		elseif type( opt.args ) ~= 'table' then
			error( "frame:expandTemplate: args must be a table" )
		else
			args = checkArgs( 'frame:expandTemplate', opt.args )
		end

		return php.expandTemplate( frameId, title, args )
	end

	function frame:callParserFunction( name, args, ... )
		checkSelf( self, 'callParserFunction' )

		if type( name ) == 'table' then
			name, args = name.name, name.args
			if type( args ) ~= 'table' then
				args = { args }
			end
		elseif type( args ) ~= 'table' then
			args = { args, ... }
		end

		if name == nil then
			error( "frame:callParserFunction: a function name is required", 2 )
		elseif type( name ) == 'string' or type( name ) == 'number' then
			name = tostring( name )
		else
			error( "frame:callParserFunction: function name must be a string or number", 2 )
		end

		args = checkArgs( 'frame:callParserFunction', args )

		return php.callParserFunction( frameId, name, args )
	end

	function frame:extensionTag( name, content, args )
		checkSelf( self, 'extensionTag' )

		if type( name ) == 'table' then
			name, content, args = name.name, name.content, name.args
		end

		if name == nil then
			error( "frame:extensionTag: a function name is required", 2 )
		elseif type( name ) == 'string' or type( name ) == 'number' then
			name = tostring( name )
		else
			error( "frame:extensionTag: tag name must be a string or number", 2 )
		end

		if content == nil then
			content = ''
		elseif type( content ) == 'string' or type( content ) == 'number' then
			content = tostring( content )
		else
			error( "frame:extensionTag: content must be a string or number", 2 )
		end

		if args == nil then
			args = { content }
		elseif type( args ) == 'string' or type( args ) == 'number' then
			args = { content, args }
		elseif type( args ) == 'table' then
			args = checkArgs( 'frame:extensionTag', args )
			table.insert( args, 1, content )
		else
			error( "frame:extensionTag: args must be a string, number, or table", 2 )
		end

		return php.callParserFunction( frameId, '#tag:' .. name, args )
	end

	function frame:preprocess( opt )
		checkSelf( self, 'preprocess' )

		local text
		if type( opt ) == 'table' then
			text = opt.text
		else
			text = opt
		end
		text = tostring( text )
		return php.preprocess( frameId, text )
	end

	function frame:newParserValue( opt )
		checkSelf( self, 'newParserValue' )

		local text
		if type( opt ) == 'table' then
			text = opt.text
		else
			text = opt
		end

		return newCallbackParserValue(
			function ()
				return self:preprocess( text )
			end
			)
	end

	function frame:newTemplateParserValue( opt )
		checkSelf( self, 'newTemplateParserValue' )

		if type( opt ) ~= 'table' then
			error( "frame:newTemplateParserValue: the first parameter must be a table" )
		end
		if opt.title == nil then
			error( "frame:newTemplateParserValue: a title is required" )
		end
		return newCallbackParserValue(
			function ()
				return self:expandTemplate( opt )
			end
		)
	end

	function frame:getTitle()
		checkSelf( self, 'getTitle' )
		return php.getFrameTitle( frameId )
	end

	-- For backwards compat
	function frame:argumentPairs()
		checkSelf( self, 'argumentPairs' )
		return pairs( self.args )
	end

	return frame
end

--- Set up a cloned environment for execution of a module chunk, then execute
-- the module in that environment. This is called by the host to implement
-- {{#invoke}}.
--
-- @param chunk The module chunk
-- @param name The name of the function to be returned. Nil or false causes the entire export table to be returned
-- @return boolean Whether the requested value was able to be returned
-- @return table|function|string The requested value, or if that was unable to be returned, the type of the value returned by the module
function mw.executeModule( chunk, name )
	local env = mw.clone( _G )
	makePackageModule( env )

	-- These are unsafe
	env.mw.makeProtectedEnvFuncs = nil
	env.mw.executeModule = nil
	if name ~= false then -- console sets name to false when evaluating its code and nil when evaluating a module's
		env.mw.getLogBuffer = nil
		env.mw.clearLogBuffer = nil
	end

	if allowEnvFuncs then
		env.setfenv, env.getfenv = mw.makeProtectedEnvFuncs( {[_G] = true}, {} )
	else
		env.setfenv = nil
		env.getfenv = nil
	end

	env.os.date = ttlDate
	env.os.time = ttlTime

	setfenv( chunk, env )

	local oldFrame = currentFrame
	if not currentFrame then
		currentFrame = newFrame( 'current', 'parent' )
	end
	local res = chunk()
	currentFrame = oldFrame

	if not name then -- catch console whether it's evaluating its own code or a module's
		return true, res
	end
	if type(res) ~= 'table' then
		return false, type(res)
	end
	return true, res[name]
end

function mw.executeFunction( chunk )
	local frame = newFrame( 'current', 'parent' )
	local oldFrame = currentFrame

	if executeFunctionDepth == 0 then
		-- math.random is defined as using C's rand(), and C's rand() uses 1 as
		-- a seed if not explicitly seeded. So reseed with 1 for each top-level
		-- #invoke to avoid people passing state via the RNG.
		math.randomseed( 1 )
	end
	executeFunctionDepth = executeFunctionDepth + 1

	currentFrame = frame
	local results = { chunk( frame ) }
	currentFrame = oldFrame

	local stringResults = {}
	for i, result in ipairs( results ) do
		stringResults[i] = tostring( result )
	end

	executeFunctionDepth = executeFunctionDepth - 1

	return table.concat( stringResults )
end

function mw.allToString( ... )
	local t = { ... }
	for i = 1, select( '#', ... ) do
		t[i] = tostring( t[i] )
	end
	return table.concat( t, '\t' )
end

function mw.log( ... )
	logBuffer = logBuffer .. mw.allToString( ... ) .. '\n'
end

function mw.dumpObject( object )
	local doneTable = {}
	local doneObj = {}
	local ct = {}
	local function sorter( a, b )
		local ta, tb = type( a ), type( b )
		if ta ~= tb then
			return ta < tb
		end
		if ta == 'string' or ta == 'number' then
			return a < b
		end
		if ta == 'boolean' then
			return tostring( a ) < tostring( b )
		end
		return false -- Incomparable
	end
	local function _dumpObject( object, indent, expandTable )
		local tp = type( object )
		if tp == 'number' or tp == 'nil' or tp == 'boolean' then
			return tostring( object )
		elseif tp == 'string' then
			return string.format( "%q", object )
		elseif tp == 'table' then
			if not doneObj[object] then
				local s = tostring( object )
				if s == 'table' then
					ct[tp] = ( ct[tp] or 0 ) + 1
					doneObj[object] = 'table#' .. ct[tp]
				else
					doneObj[object] = s
					doneTable[object] = true
				end
			end
			if doneTable[object] or not expandTable then
				return doneObj[object]
			end
			doneTable[object] = true

			local ret = { doneObj[object], ' {\n' }
			local mt = getmetatable( object )
			if mt then
				ret[#ret + 1] = string.rep( " ", indent + 2 )
				ret[#ret + 1] = 'metatable = '
				ret[#ret + 1] = _dumpObject( mt, indent + 2, false )
				ret[#ret + 1] = "\n"
			end

			local doneKeys = {}
			for key, value in ipairs( object ) do
				doneKeys[key] = true
				ret[#ret + 1] = string.rep( " ", indent + 2 )
				ret[#ret + 1] = _dumpObject( value, indent + 2, true )
				ret[#ret + 1] = ',\n'
			end
			local keys = {}
			for key in pairs( object ) do
				if not doneKeys[key] then
					keys[#keys + 1] = key
				end
			end
			table.sort( keys, sorter )
			for i = 1, #keys do
				local key = keys[i]
				ret[#ret + 1] = string.rep( " ", indent + 2 )
				ret[#ret + 1] = '['
				ret[#ret + 1] = _dumpObject( key, indent + 3, false )
				ret[#ret + 1] = '] = '
				ret[#ret + 1] = _dumpObject( object[key], indent + 2, true )
				ret[#ret + 1] = ",\n"
			end
			ret[#ret + 1] = string.rep( " ", indent )
			ret[#ret + 1] = '}'
			return table.concat( ret )
		else
			if not doneObj[object] then
				ct[tp] = ( ct[tp] or 0 ) + 1
				doneObj[object] = tostring( object ) .. '#' .. ct[tp]
			end
			return doneObj[object]
		end
	end
	return _dumpObject( object, 0, true )
end

function mw.logObject( object, prefix )
	if prefix and prefix ~= '' then
		logBuffer = logBuffer .. prefix .. ' = '
	end
	logBuffer = logBuffer .. mw.dumpObject( object ) .. '\n'
end

function mw.clearLogBuffer()
	logBuffer = ''
end

function mw.getLogBuffer()
	return logBuffer
end

function mw.getCurrentFrame()
	if not currentFrame then
		currentFrame = newFrame( 'current', 'parent' )
	end
	return currentFrame
end

function mw.isSubsting()
	return php.isSubsting()
end

function mw.incrementExpensiveFunctionCount()
	php.incrementExpensiveFunctionCount()
end

function mw.addWarning( text )
	php.addWarning( text )
end

---
-- Wrapper for mw.loadData. This creates the read-only dummy table for
-- accessing the real data.
--
-- @param data table Data to access
-- @param seen table|nil Table of already-seen tables.
-- @return table
local function dataWrapper( data, seen )
	local t = {}
	seen = seen or { [data] = t }

	local function pairsfunc( s, k )
		k = next( data, k )
		if k ~= nil then
			return k, t[k]
		end
		return nil
	end

	local function ipairsfunc( s, i )
		i = i + 1
		if data[i] ~= nil then
			return i, t[i]
		end
		return -- no nil to match default ipairs()
	end

	local mt = {
		mw_loadData = true,
		__index = function ( tt, k )
			assert( t == tt )
			local v = data[k]
			if type( v ) == 'table' then
				seen[v] = seen[v] or dataWrapper( v, seen )
				return seen[v]
			end
			return v
		end,
		__newindex = function ( t, k, v )
			error( "table from mw.loadData is read-only", 2 )
		end,
		__pairs = function ( tt )
			assert( t == tt )
			return pairsfunc, t, nil
		end,
		__ipairs = function ( tt )
			assert( t == tt )
			return ipairsfunc, t, 0
		end,
	}
	-- This is just to make setmetatable() fail
	mt.__metatable = mt

	return setmetatable( t, mt )
end

---
-- Validator for mw.loadData. This scans through the data looking for things
-- that are not supported, e.g. functions (which may be closures).
--
-- @param d table Data to access.
-- @param seen table|nil Table of already-seen tables.
-- @return string|nil Error message, if any
local function validateData( d, seen )
	seen = seen or {}
	local tp = type( d )
	if tp == 'nil' or tp == 'boolean' or tp == 'number' or tp == 'string' then
		return nil
	elseif tp == 'table' then
		if seen[d] then
			return nil
		end
		seen[d] = true
		if getmetatable( d ) ~= nil then
			return "data for mw.loadData contains a table with a metatable"
		end
		for k, v in pairs( d ) do
			if type( k ) == 'table' then
				return "data for mw.loadData contains a table as a key"
			end
			local err = validateData( k, seen ) or validateData( v, seen )
			if err then
				return err
			end
		end
		return nil
	else
		return "data for mw.loadData contains unsupported data type '" .. tp .. "'"
	end
end

function mw.loadData( module )
	local data = loadedData[module]
	if type( data ) == 'string' then
		-- No point in re-validating
		error( data, 2 )
	end
	if not data then
		-- Don't allow accessing the current frame's info (bug 65687)
		local oldFrame = currentFrame
		currentFrame = newFrame( 'empty' )

		-- The point of this is to load big data, so don't save it in package.loaded
		-- where it will have to be copied for all future modules.
		local l = package.loaded[module]
		local _

		_, data = mw.executeModule( function() return require( module ) end )

		package.loaded[module] = l
		currentFrame = oldFrame

		-- Validate data
		local err
		if type( data ) == 'table' then
			err = validateData( data )
		else
			err = module .. ' returned ' .. type( data ) .. ', table expected'
		end
		if err then
			loadedData[module] = err
			error( err, 2 )
		end
		loadedData[module] = data
	end

	return dataWrapper( data )
end

return mw