All Downloads are FREE. Search and download functionalities are using the official Maven repository.

info.bliki.extensions.scribunto.engine.lua.ScribuntoLuaEngine Maven / Gradle / Ivy

The newest version!
package info.bliki.extensions.scribunto.engine.lua;

import info.bliki.extensions.scribunto.ScribuntoException;
import info.bliki.extensions.scribunto.engine.ScribuntoEngineBase;
import info.bliki.extensions.scribunto.engine.ScribuntoModule;
import info.bliki.extensions.scribunto.engine.lua.interfaces.MwHtml;
import info.bliki.extensions.scribunto.engine.lua.interfaces.MwInit;
import info.bliki.extensions.scribunto.engine.lua.interfaces.MwInterface;
import info.bliki.extensions.scribunto.engine.lua.interfaces.MwLanguage;
import info.bliki.extensions.scribunto.engine.lua.interfaces.MwMessage;
import info.bliki.extensions.scribunto.engine.lua.interfaces.MwSite;
import info.bliki.extensions.scribunto.engine.lua.interfaces.MwText;
import info.bliki.extensions.scribunto.engine.lua.interfaces.MwTitle;
import info.bliki.extensions.scribunto.engine.lua.interfaces.MwUri;
import info.bliki.extensions.scribunto.engine.lua.interfaces.MwUstring;
import info.bliki.extensions.scribunto.template.Frame;
import info.bliki.wiki.filter.ParsedPageName;
import info.bliki.wiki.model.IWikiModel;
import org.luaj.vm2.Globals;
import org.luaj.vm2.LuaClosure;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaFunction;
import org.luaj.vm2.LuaString;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Prototype;
import org.luaj.vm2.Varargs;
import org.luaj.vm2.lib.OneArgFunction;
import org.luaj.vm2.lib.ResourceFinder;
import org.luaj.vm2.lib.ThreeArgFunction;
import org.luaj.vm2.lib.TwoArgFunction;
import org.luaj.vm2.lib.VarArgFunction;
import org.luaj.vm2.lib.ZeroArgFunction;
import org.luaj.vm2.lib.jse.JsePlatform;

import javax.annotation.Nonnull;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.Map;

/**
 * scribunto/engines/LuaCommon/LuaCommon.php
 */
public class ScribuntoLuaEngine extends ScribuntoEngineBase implements MwInterface {
    private static final int MAX_EXPENSIVE_CALLS = 10;
    private static final boolean ENABLE_LUA_DEBUG_LIBRARY = false;
    private final Globals globals;
    private Frame currentFrame;
    private int expensiveFunctionCount;

    private final CompiledScriptCache compiledScriptCache;
    private final MwInterface[] interfaces;

    public ScribuntoLuaEngine(IWikiModel model, CompiledScriptCache cache) {
        this(model, cache, ENABLE_LUA_DEBUG_LIBRARY);
    }

    public ScribuntoLuaEngine(IWikiModel model, CompiledScriptCache cache, boolean debug) {
        this(model, cache, debug ? JsePlatform.debugGlobals() : JsePlatform.standardGlobals());
    }

    private ScribuntoLuaEngine(IWikiModel model, CompiledScriptCache compiledScriptCache, Globals globals) {
        super(model);
        this.compiledScriptCache = compiledScriptCache;
        this.globals = globals;
        this.globals.finder = new LuaResourceFinder(globals.finder);
        extendGlobals(globals);

        this.interfaces = new MwInterface[] {
            new MwSite(model),
            new MwUstring(),
            new MwTitle(model),
            new MwText(),
            new MwUri(),
            new MwMessage(),
            new MwHtml(),
            new MwLanguage(model),
        };

        try {
            load();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override public ScribuntoModule fetchModuleFromParser(String moduleName) throws ScribuntoException {
        ParsedPageName pageName = pageNameForModule(moduleName);

        Prototype prototype = compiledScriptCache.getPrototypeForChunkname(pageName);
        if (prototype == null) {
            try (InputStream is = getRawWikiContentStream(pageName)) {
                prototype = loadAndCache(is, pageName);
            } catch (IOException e) {
                throw new ScribuntoException(e);
            }
        }
        return new ScribuntoLuaModule(this, prototype, pageName);
    }

    @Override
    public String name() {
        return "mw";
    }

    protected Globals getGlobals() {
        return globals;
    }

    protected LuaValue loadFunction(String functionName, Prototype prototype, Frame frame) throws ScribuntoException {
        try {
            currentFrame = frame;
            LuaValue function =  new LuaClosure(prototype, globals).checkfunction().call().get(functionName);
            if (function.isnil()) {
                throw new ScribuntoException("no such function '"+functionName+"'");
            }
            return function;
        } catch (LuaError e) {
            throw new ScribuntoException(e);
        } finally {
            currentFrame = null;
        }
    }

    protected String executeFunctionChunk(LuaValue luaFunction, Frame frame) {
        assertFunction(luaFunction);
        try {
            currentFrame = frame;
            LuaValue executeFunction = globals.get("mw").get("executeFunction");
            logger.trace("executing "+luaFunction);
            final LuaString result = executeFunction.call(luaFunction).checkstring();
            return new String(result.m_bytes, result.m_offset, result.m_length, UTF8);
        } finally {
            currentFrame = null;
        }
    }

    private void assertFunction(LuaValue luaFunction) {
        if (luaFunction == null || luaFunction.isnil()) {
            throw new AssertionError("luaFunction is nil");
        }
    }

    private void load() throws IOException {
        load(new MwInit());
        load(this);
        for (MwInterface iface : interfaces) {
            load(iface);
        }

        stubTitleBlacklist();
        stubExecuteModule();
        stubWikiBase();
    }

    private void stubTitleBlacklist() {
        // TODO move to separate file
        final LuaValue mw = globals.get("mw");
        LuaValue ext = mw.get("ext");
        if (ext.isnil()) {
            ext = new LuaTable();
            mw.set("ext", ext);
        }
        LuaTable blacklist = new LuaTable();
        blacklist.set("test", new TwoArgFunction() {
            @Override
            public LuaValue call(LuaValue action, LuaValue title) {
                return NIL;
            }
        });
        ext.set("TitleBlacklist", blacklist);
    }


    private void stubExecuteModule() {
        // don't need module isolation
        final LuaValue mw = globals.get("mw");
        mw.set("executeModule", new VarArgFunction() {
            @Override public Varargs invoke(Varargs args) {
                LuaFunction chunk = args.arg(1).checkfunction();
                LuaValue name     = args.arg(2);

                final LuaValue res = chunk.call();

                if (name.isnil()) {
                    return LuaValue.varargsOf(new LuaValue[]{LuaValue.TRUE, res});
                } else {
                    if (!res.istable()) {
                        return LuaValue.varargsOf(new LuaValue[]{FALSE, toLuaString(res.typename())});
                    } else {
                        return LuaValue.varargsOf(new LuaValue[]{LuaValue.TRUE, res.checktable().get(name)});
                    }
                }
            }
        });
    }

    private void stubWikiBase() {
        // fake http://www.mediawiki.org/wiki/Extension:Wikibase
        final LuaValue mw = globals.get("mw");
        final LuaTable wikibase = new LuaTable();
        wikibase.set("getEntity", new ZeroArgFunction() {
            @Override public LuaValue call() {
                return NIL;
            }
        });
        wikibase.set("getEntityObject", new ZeroArgFunction() {
            @Override
            public LuaValue call() {
                return NIL;
            }
        });
        mw.set("wikibase", wikibase);
    }

    private void load(MwInterface luaInterface) throws IOException {
        final String filename = fileNameForInterface(luaInterface);

        try (InputStream is = globals.finder.findResource(filename)) {
            if (is == null) {
                throw new FileNotFoundException("could not find '"+filename+"'. Make sure it is on the classpath.");
            }
            final LuaValue pkg = globals.load(is, "@"+filename, "bt", globals).call();
            final LuaValue setupInterface = pkg.get("setupInterface");

            if (!setupInterface.isnil()) {
                globals.set("mw_interface", luaInterface.getInterface());
                setupInterface.call(luaInterface.getSetupOptions());
            }
        }
    }

    @Override
    public LuaTable getInterface() {
        final LuaTable table = new LuaTable();
        table.set("loadPackage", loadPackage());
        table.set("loadPHPLibrary", loadPHPLibrary());
        table.set("frameExists", frameExists());
        table.set("newChildFrame", newChildFrame());
        table.set("getExpandedArgument", getExpandedArgument());
        table.set("getAllExpandedArguments", getAllExpandedArguments());
        table.set("getFrameTitle", getFrameTitle());
        table.set("expandTemplate", expandTemplate());
        table.set("callParserFunction", callParserFunction());
        table.set("preprocess", preprocess());
        table.set("incrementExpensiveFunctionCount", incrementExpensiveFunctionCount());
        table.set("isSubsting", isSubsting());
        return table;
    }

    private String fileNameForInterface(MwInterface luaInterface) {
        return luaInterface.name() + (luaInterface.name().endsWith(".lua") ? "" : ".lua");
    }

    private LuaValue callParserFunction() {
        return new ThreeArgFunction() {
            @Override
            public LuaValue call(LuaValue frameId, LuaValue function, LuaValue args) {
                if ("filepath".equals(function.checkjstring())) {
                    String path = args.get(1).checkjstring();
                    return toLuaString(path);
                }
                return LuaValue.NIL;
            }
        };
    }

    private LuaValue isSubsting() {
        return new ZeroArgFunction() {
            @Override public LuaValue call() {
                return LuaValue.valueOf(getFrameById(toLuaString("current")).isSubsting());
            }
        };
    }

    private LuaValue incrementExpensiveFunctionCount() {
        return new ZeroArgFunction() {
            @Override public LuaValue call() {
                if (++expensiveFunctionCount > MAX_EXPENSIVE_CALLS) {
                    error("too many expensive function calls");
                }
                return NIL;
            }
        };
    }

    private LuaValue preprocess() {
        return new TwoArgFunction() {
            @Override public LuaValue call(LuaValue frameId, LuaValue text) {
                try {
                    return toLuaString(model.render(text.checkjstring()));
                } catch (IOException e) {
                    logger.error("error rendering", e);
                    return LuaValue.NIL;
                }
            }
        };
    }

    private LuaValue expandTemplate() {
        return new ThreeArgFunction() {
            @Override
            public LuaValue call(LuaValue frameId, LuaValue title, LuaValue args) {
                final Frame frame = getFrameById(frameId);
                final Map parameterMap = frame.getTemplateParameters();
                final LuaTable table = args.checktable();
                LuaValue key = LuaValue.NIL;
                while (true) {
                    Varargs next = table.next(key);
                    if ((key = next.arg1()).isnil())
                        break;

                    LuaValue value = next.arg(2);
                    parameterMap.put(key.checkjstring(), value.checkjstring());
                }
                StringWriter writer = new StringWriter();
                try {
                    model.substituteTemplateCall(title.tojstring(), parameterMap, writer);
                    return toLuaString(writer.toString());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }

    private LuaValue getExpandedArgument() {
        return new TwoArgFunction() {
            @Override
            public LuaValue call(LuaValue frameId, LuaValue name) {
                return getFrameById(frameId).getArgument(name.tojstring());
            }
        };
    }

    private Frame getFrameById(LuaValue frameId) {
        if (currentFrame == null) {
            throw new AssertionError("No current frame set: "+ frameId);
        }
        Frame frame;
        if (frameId.tojstring().equals("parent")) {
            frame = currentFrame.getParent();
        } else {
            frame = currentFrame;
        }

        if (frame == null) {
            throw new AssertionError("No frame set: "+ frameId);
        }
        return frame;
    }

    private LuaValue getFrameTitle() {
        return new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                return toLuaString(getFrameById(arg).getTitle());
            }
        };
    }

    private LuaValue getAllExpandedArguments() {
        return new OneArgFunction() {
            @Override public LuaValue call(LuaValue frameId) {
                return getFrameById(frameId).getAllArguments();
            }
        };
    }

    private LuaValue newChildFrame() {
        return new ThreeArgFunction() {
            @Override
            public LuaValue call(LuaValue frameId, LuaValue title, LuaValue args) {
                return NIL;
            }
        };
    }

    private LuaValue frameExists() {
        return new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                return TRUE;
            }
        };
    }

    private OneArgFunction loadPackage() {
        return new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue packageName) {
                return loadModule(pageNameForModule(packageName.tojstring()));
            }
        };
    }

    private LuaValue loadModule(ParsedPageName chunkName) throws LuaError {
        Prototype prototype = compiledScriptCache.getPrototypeForChunkname(chunkName);
        if (prototype != null) {
            return new LuaClosure(prototype, globals);
        } else {
            try (InputStream is = findPackage(chunkName)) {
                return new LuaClosure(
                    loadAndCache(is, chunkName),
                    globals);
            } catch (ScribuntoException | IOException e) {
                logger.error("error loading '"+chunkName+"'", e);
                throw new LuaError(e);
            }
        }
    }

    private Prototype loadAndCache(InputStream code, ParsedPageName chunkName) throws ScribuntoException {
        try {
            logger.debug("compiling " + chunkName);
            Prototype prototype = globals.compilePrototype(code, chunkName.fullPagename());
            compiledScriptCache.cachePrototype(chunkName, prototype);

            return prototype;
        } catch (LuaError | IOException e) {
            throw new ScribuntoException(e);
        }
    }

    private OneArgFunction loadPHPLibrary() {
        return new OneArgFunction() {
            @Override public LuaValue call(LuaValue arg) {
                return LuaValue.NIL;
            }
        };
    }

    private @Nonnull InputStream findPackage(ParsedPageName name) throws IOException {
        logger.debug("findPackage("+name+")");
        final InputStream is = globals.finder.findResource(name.pagename+".lua");
        if (is != null) {
            return is;
        } else {
            return findModule(name);
        }
    }

    private InputStream findModule(final ParsedPageName moduleName) throws IOException {
        final String name = moduleName.pagename.replaceAll("[/:]", "_");
        InputStream is = globals.finder.findResource(name);
        if (is != null) {
            return is;
        } else {
            return getRawWikiContentStream(moduleName);
        }
    }

    @Override
    public LuaValue getSetupOptions() {
        return new LuaTable();
    }

    private void extendGlobals(final Globals globals) {
        globals.set("setfenv", new TwoArgFunction() {
            @Override
            public LuaValue call(LuaValue f, LuaValue env) {
                return f;
            }
        });
        globals.set("gefenv", new OneArgFunction() {
            public LuaValue call(LuaValue f) {
                return globals;
            }
        });
        globals.set("unpack", new unpack());

        // math.log10 got removed in 5.2
        LuaValue math = globals.get("math");
        math.set("log10", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue luaValue) {
                return valueOf(Math.log10(luaValue.checkdouble()));
            }
        });

        // table.maxn got removed in 5.2
        LuaValue table = globals.get("table");
        table.set("maxn", new OneArgFunction() {
            @Override public LuaValue call(LuaValue arg) {
                // TODO: is this correct?
                return arg.checktable().len();
            }
        });

        // table.getn got removed in 5.2
        table.set("getn", new OneArgFunction() {
            @Override public LuaValue call(LuaValue arg) {
                if (arg.isnil()) {
                    return LuaValue.error("bad argument #1 to 'getn' (table expected, got nil)");
                } else {
                    return arg.checktable().len();
                }
            }
        });
    }

    /**
     * Use Java's UTF-8 decoding instead of luaj's broken implementation,
     * since it does not handle surrogates properly.
     */
    public static LuaString toLuaString(String string) {
        return LuaString.valueOf(string.getBytes(UTF8));
    }

    private static class unpack extends VarArgFunction {
        public Varargs invoke(Varargs args) {
            LuaTable t = args.checktable(1);
            switch (args.narg()) {
                case 1: return t.unpack();
                case 2: return t.unpack(args.checkint(2));
                default: return t.unpack(args.checkint(2), args.checkint(3));
            }
        }
    }

    static class LuaResourceFinder implements ResourceFinder {
        private static final String[] LIBRARY_PATH = new String[] {
            "",
            "luabit",
            "ustring",
        };

        private final ResourceFinder delegate;

        LuaResourceFinder(ResourceFinder delegate) {
            this.delegate = delegate;
        }

        @Override
        public InputStream findResource(String filename) {
            for (String path : LIBRARY_PATH) {
                InputStream is = delegate.findResource(path + "/" + filename);
                if (is != null) {
                    return is;
                }
            }
            return null;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy