Writing Plugins

A plugin is a Python file that appears in Leo’s plugin directory. Plugins modify how Leo works. With plugins you can give Leo new commands, modify how existing commands work, or change any other aspect of Leo’s look and feel. leoPlugins.leo contains all of Leo’s official plugins. Studying this file is a good way to learn how to write plugins.

You enable plugins using @enabled-plugins nodes in leoSettings.leo or myLeoSettings.leo. For more details, see the @enabled-plugins node in leoSettings.leo. Leo imports all enabled plugins at startup time. Plugins become active if importing the plugin was successful.

Writing plugins is quite similar to writing any other Leo script. See Scripting Leo with Python. In particular:

  1. Plugins can use any of Leo’s source code simply by importing any module defined in leoPy.leo.
  2. Plugins can register event handlers just like any other Leo script. For full details, see the section called event handlers in Leo’s scripting chapter.

The rest of this chapters discusses topics related specifically to plugins.

enabled-plugins

@enabled-plugins node is as list of plugins to load. If you have @enabled-plugins node in your myLeoSettings.leo, the plugins are loaded from there. If such a node doesn’t exist, the global leoSettings.leo is used instead.

The @enabled-plugins bundled in leoSettings.leo contains a list of default (recommended) plugins. For your own @enabled-plugins in myLeoSettings.leo, you should use the node in leoSettings.leo as a starting point unless you are certain you want to disable a recommended plugin.

@enabled-plugins nodes contain the list of enabled plugins, one per line.

Comment lines starting with ‘#’ are ignored.

Plugins are essentially normal python modules, and loading a plugin basically means importing it and running the “init” function in the module’s root level namespace. A line in @enabled-plugins is a module name that leo should import.

Here’s an example @enabled-plugins node:

# Standard plugins enabled in official distributions....

plugins_menu.py
quicksearch.py

# third party plugins

# 'leoplugin' module inside python package 'foo'
foo.leoplugin

# top-level module
barplugin

Note that some entries end with .py. This is done to retain backwards compatibility - if an entry ends with .py, it means a plugin in Leo’s ‘plugins’ directory (package) and is translated to e.g. “leo.plugins.plugins_menu” before importing.

Normally, a third party plugin should be a basic python module that is installed globally for the python interpreter with “python setup.py install”. Installing plugins to Leo’s ‘plugins’ directory is not recommended, as such plugins can disappear when Leo is upgraded.

Support for unit testing

The plugins test suite creates a new convention: if a plugin has a function at the outer (module) level called unitTest, Leo will call that function when doing unit testing for plugins. So it would be good if writers of plugins would create such a unitTest function. To indicate a failure the unitTest just throws an exception. Leo’s plugins test suite takes care of the rest.

Important security warnings

Naively using plugins can expose you and your .leo files to malicious attacks. The fundamental principles are:

Scripts and plugins must never blindly execute code from untrusted sources.

and:

.leo files obtained from other people may potentially contain hostile code.

Stephen Schaefer summarizes the danger this way:

I foresee a future in which the majority of leo projects come from
marginally trusted sources...a world of leo documents sent hither and yon -
resumes, project proposals, textbooks, magazines, contracts - and as a race
of Pandora's, we cannot resist wanting to see "What's in the box?" And are
we going to fire up a text editor to make a detailed examination of the
ASCII XML? Never! We're going to double click on the cute leo file icon, and
leo will fire up in all its raging glory. Just like Word (and its macros) or
Excel (and its macros).

In other words:

When we share "our" .leo files we can NOT assume that
we know what is in our "own" documents!

Not all environments are untrustworthy. Code in a commercial cvs repository is probably trustworthy: employees might be terminated for posting malicious code. Still, the potential for abuse exists anywhere.

In Python it is very easy to write a script that will blindly execute other scripts:

# Warning: extremely dangerous code

# Execute the body text of all nodes that start with `@script`.
def onLoadFile():
    for p in c.all_positions():
        h = p.h.lower()
        if g.match_word(h,0,"@script"):
            s = p.b
            if s and len(s) > 0:
                try: # SECURITY BREACH: s may be malicious!
                    exec(s + '\n')
                except:
                    es_exception()

Executing this kind of code is typically an intolerable security risk. Important: rexec provides no protection whatever. Leo is a repository of source code, so any text operation is potentially malicious. For example, consider the following script, which is valid in rexec mode:

badNode = c.p
for p in c.all_positions():
    << change `rexec` to `exec` in p's body >>
<< delete badNode >>
<< clear the undo stack >>

This script will introduce a security hole the .leo file without doing anything prohibited by rexec, and without leaving any traces of the perpetrating script behind. The damage will become permanent outside this script when the user saves the .leo file.

Previous topic

Plugins

Next topic

Unit testing with Leo