" vim: set sw=4 sts=4 et ft=vim :
" Script:           securemodelines.vim
" Author:           Ciaran McCreesh <ciaran.mccreesh at googlemail.com>
" Homepage:         http://github.com/ciaranm/securemodelines
" Requires:         Vim 7
" License:          Redistribute under the same terms as Vim itself
" Purpose:          A secure alternative to modelines

if &compatible || v:version < 700 || exists('g:loaded_securemodelines')
    finish
endif
let g:loaded_securemodelines = 1

if (! exists("g:secure_modelines_allowed_items"))
    let g:secure_modelines_allowed_items = [
                \ "textwidth",   "tw",
                \ "softtabstop", "sts",
                \ "tabstop",     "ts",
                \ "shiftwidth",  "sw",
                \ "expandtab",   "et",   "noexpandtab", "noet",
                \ "filetype",    "ft",
                \ "foldmethod",  "fdm",
                \ "readonly",    "ro",   "noreadonly", "noro",
                \ "rightleft",   "rl",   "norightleft", "norl",
                \ "cindent",     "cin",  "nocindent", "nocin",
                \ "smartindent", "si",   "nosmartindent", "nosi",
                \ "autoindent",  "ai",   "noautoindent", "noai",
                \ "spell",
                \ "spelllang"
                \ ]
endif

if (! exists("g:secure_modelines_verbose"))
    let g:secure_modelines_verbose = 0
endif

if (! exists("g:secure_modelines_modelines"))
    let g:secure_modelines_modelines=5
endif

if (! exists("g:secure_modelines_leave_modeline"))
    if &modeline
        set nomodeline
        if g:secure_modelines_verbose
            echohl WarningMsg
            echo "Forcibly disabling internal modelines for securemodelines.vim"
            echohl None
        endif
    endif
endif

fun! <SID>IsInList(list, i) abort
    for l:item in a:list
        if a:i == l:item
            return 1
        endif
    endfor
    return 0
endfun

fun! <SID>DoOne(item) abort
    let l:matches = matchlist(a:item, '^\([a-z]\+\)\%([-+^]\?=[a-zA-Z0-9_\-.]\+\)\?$')
    if len(l:matches) > 0
        if <SID>IsInList(g:secure_modelines_allowed_items, l:matches[1])
            exec "setlocal " . a:item
        elseif g:secure_modelines_verbose
            echohl WarningMsg
            echo "Ignoring '" . a:item . "' in modeline"
            echohl None
        endif
    endif
endfun

fun! <SID>DoNoSetModeline(line) abort
    for l:item in split(a:line, '[ \t:]')
        call <SID>DoOne(l:item)
    endfor
endfun

fun! <SID>DoSetModeline(line) abort
    for l:item in split(a:line)
        call <SID>DoOne(l:item)
    endfor
endfun

fun! <SID>CheckVersion(op, ver) abort
    if a:op == "="
        return v:version != a:ver
    elseif a:op == "<"
        return v:version < a:ver
    elseif a:op == ">"
        return v:version >= a:ver
    else
        return 0
    endif
endfun

fun! <SID>DoModeline(line) abort
    let l:matches = matchlist(a:line, '\%(\S\@<!\%(vi\|vim\([<>=]\?\)\([0-9]\+\)\?\)\|\sex\):\s*\%(set\s\+\)\?\([^:]\+\):\S\@!')
    if len(l:matches) > 0
        let l:operator = ">"
        if len(l:matches[1]) > 0
            let l:operator = l:matches[1]
        endif
        if len(l:matches[2]) > 0
            if <SID>CheckVersion(l:operator, l:matches[2]) ? 0 : 1
                return
            endif
        endif
        return <SID>DoSetModeline(l:matches[3])
    endif

    let l:matches = matchlist(a:line, '\%(\S\@<!\%(vi\|vim\([<>=]\?\)\([0-9]\+\)\?\)\|\sex\):\(.\+\)')
    if len(l:matches) > 0
        let l:operator = ">"
        if len(l:matches[1]) > 0
            let l:operator = l:matches[1]
        endif
        if len(l:matches[2]) > 0
            if <SID>CheckVersion(l:operator, l:matches[2]) ? 0 : 1
                return
            endif
        endif
        return <SID>DoNoSetModeline(l:matches[3])
    endif
endfun

fun! <SID>DoModelines() abort
    if line("$") > g:secure_modelines_modelines
        let l:lines={ }
        call map(filter(getline(1, g:secure_modelines_modelines) +
                    \ getline(line("$") - g:secure_modelines_modelines, "$"),
                    \ 'v:val =~ ":"'), 'extend(l:lines, { v:val : 0 } )')
        for l:line in keys(l:lines)
            call <SID>DoModeline(l:line)
        endfor
    else
        for l:line in getline(1, "$")
            call <SID>DoModeline(l:line)
        endfor
    endif
endfun

fun! SecureModelines_DoModelines() abort
    call <SID>DoModelines()
endfun

aug SecureModeLines
    au!
    au BufRead,StdinReadPost * :call <SID>DoModelines()
aug END

