Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
actions.php 20.40 KiB
<?php
/**
 * DokuWiki Actions
 *
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     Andreas Gohr <andi@splitbrain.org>
 */

if(!defined('DOKU_INC')) die('meh.');

/**
 * Call the needed action handlers
 *
 * @author Andreas Gohr <andi@splitbrain.org>
 * @triggers ACTION_ACT_PREPROCESS
 * @triggers ACTION_HEADERS_SEND
 */
function act_dispatch(){
    global $ACT;
    global $ID;
    global $INFO;
    global $QUERY;
    global $lang;
    global $conf;

    $preact = $ACT;

    // give plugins an opportunity to process the action
    $evt = new Doku_Event('ACTION_ACT_PREPROCESS',$ACT);
    if ($evt->advise_before()) {

        //sanitize $ACT
        $ACT = act_clean($ACT);

        //check if searchword was given - else just show
        $s = cleanID($QUERY);
        if($ACT == 'search' && empty($s)){
            $ACT = 'show';
        }

        //login stuff
        if(in_array($ACT,array('login','logout'))){
            $ACT = act_auth($ACT);
        }

        //check if user is asking to (un)subscribe a page
        if($ACT == 'subscribe') {
            try {
                $ACT = act_subscription($ACT);
            } catch (Exception $e) {
                msg($e->getMessage(), -1);
            }
        }

        //display some infos
        if($ACT == 'check'){
            check();
            $ACT = 'show';
        }

        //check permissions
        $ACT = act_permcheck($ACT);

        //sitemap
        if ($ACT == 'sitemap'){
            $ACT = act_sitemap($ACT);
        }

        //register
        if($ACT == 'register' && $_POST['save'] && register()){
            $ACT = 'login';
        }

        if ($ACT == 'resendpwd' && act_resendpwd()) {
            $ACT = 'login';
        }

        //update user profile
        if ($ACT == 'profile') {
            if(!$_SERVER['REMOTE_USER']) {
                $ACT = 'login';
            } else {
                if(updateprofile()) {
                    msg($lang['profchanged'],1);
                    $ACT = 'show';
                }
            }
        }

        //revert
        if($ACT == 'revert'){
            if(checkSecurityToken()){
                $ACT = act_revert($ACT);
            }else{
                $ACT = 'show';
            }
        }

        //save
        if($ACT == 'save'){
            if(checkSecurityToken()){
                $ACT = act_save($ACT);
            }else{
                $ACT = 'preview';
            }
        }

        //cancel conflicting edit
        if($ACT == 'cancel')
            $ACT = 'show';

        //draft deletion
        if($ACT == 'draftdel')
            $ACT = act_draftdel($ACT);

        //draft saving on preview
        if($ACT == 'preview')
            $ACT = act_draftsave($ACT);

        //edit
        if(in_array($ACT, array('edit', 'preview', 'recover'))) {
            $ACT = act_edit($ACT);
        }else{
            unlock($ID); //try to unlock
        }

        //handle export
        if(substr($ACT,0,7) == 'export_')
            $ACT = act_export($ACT);

        //handle admin tasks
        if($ACT == 'admin'){
            // retrieve admin plugin name from $_REQUEST['page']
            if (!empty($_REQUEST['page'])) {
                $pluginlist = plugin_list('admin');
                if (in_array($_REQUEST['page'], $pluginlist)) {
                    // attempt to load the plugin
                    if ($plugin =& plugin_load('admin',$_REQUEST['page']) !== null){
                        if($plugin->forAdminOnly() && !$INFO['isadmin']){
                            // a manager tried to load a plugin that's for admins only
                            unset($_REQUEST['page']);
                            msg('For admins only',-1);
                        }else{
                            $plugin->handle();
                        }
                    }
                }
            }
        }

        // check permissions again - the action may have changed
        $ACT = act_permcheck($ACT);
    }  // end event ACTION_ACT_PREPROCESS default action
    $evt->advise_after();
    // Make sure plugs can handle 'denied'
    if($conf['send404'] && $ACT == 'denied') {
        header('HTTP/1.0 403 Forbidden');
    }
    unset($evt);

    // when action 'show', the intial not 'show' and POST, do a redirect
    if($ACT == 'show' && $preact != 'show' && strtolower($_SERVER['REQUEST_METHOD']) == 'post'){
        act_redirect($ID,$preact);
    }

    global $INFO;
    global $conf;
    global $license;

    //call template FIXME: all needed vars available?
    $headers[] = 'Content-Type: text/html; charset=utf-8';
    trigger_event('ACTION_HEADERS_SEND',$headers,'act_sendheaders');

    include(template('main.php'));
    // output for the commands is now handled in inc/templates.php
    // in function tpl_content()
}

function act_sendheaders($headers) {
    foreach ($headers as $hdr) header($hdr);
}

/**
 * Sanitize the action command
 *
 * Add all allowed commands here.
 *
 * @author Andreas Gohr <andi@splitbrain.org>
 */
function act_clean($act){
    global $lang;
    global $conf;
    global $INFO;

    // check if the action was given as array key
    if(is_array($act)){
        list($act) = array_keys($act);
    }

    //remove all bad chars
    $act = strtolower($act);
    $act = preg_replace('/[^1-9a-z_]+/','',$act);

    if($act == 'export_html') $act = 'export_xhtml';
    if($act == 'export_htmlbody') $act = 'export_xhtmlbody';

    if($act === '') $act = 'show';

    // check if action is disabled
    if(!actionOK($act)){
        msg('Command disabled: '.htmlspecialchars($act),-1);
        return 'show';
    }

    //disable all acl related commands if ACL is disabled
    if(!$conf['useacl'] && in_array($act,array('login','logout','register','admin',
                    'subscribe','unsubscribe','profile','revert',
                    'resendpwd'))){
        msg('Command unavailable: '.htmlspecialchars($act),-1);
        return 'show';
    }

    //is there really a draft?
    if($act == 'draft' && !file_exists($INFO['draft'])) return 'edit';

    if(!in_array($act,array('login','logout','register','save','cancel','edit','draft',
                    'preview','search','show','check','index','revisions',
                    'diff','recent','backlink','admin','subscribe','revert',
                    'unsubscribe','profile','resendpwd','recover',
                    'draftdel','sitemap','media')) && substr($act,0,7) != 'export_' ) {
        msg('Command unknown: '.htmlspecialchars($act),-1);
        return 'show';
    }
    return $act;
}

/**
 * Run permissionchecks
 *
 * @author Andreas Gohr <andi@splitbrain.org>
 */
function act_permcheck($act){
    global $INFO;
    global $conf;

    if(in_array($act,array('save','preview','edit','recover'))){
        if($INFO['exists']){
            if($act == 'edit'){
                //the edit function will check again and do a source show
                //when no AUTH_EDIT available
                $permneed = AUTH_READ;
            }else{
                $permneed = AUTH_EDIT;
            }
        }else{
            $permneed = AUTH_CREATE;
        }
    }elseif(in_array($act,array('login','search','recent','profile','index', 'sitemap'))){
        $permneed = AUTH_NONE;
    }elseif($act == 'revert'){
        $permneed = AUTH_ADMIN;
        if($INFO['ismanager']) $permneed = AUTH_EDIT;
    }elseif($act == 'register'){
        $permneed = AUTH_NONE;
    }elseif($act == 'resendpwd'){
        $permneed = AUTH_NONE;
    }elseif($act == 'admin'){
        if($INFO['ismanager']){
            // if the manager has the needed permissions for a certain admin
            // action is checked later
            $permneed = AUTH_READ;
        }else{
            $permneed = AUTH_ADMIN;
        }
    }else{
        $permneed = AUTH_READ;
    }
    if($INFO['perm'] >= $permneed) return $act;

    return 'denied';
}

/**
 * Handle 'draftdel'
 *
 * Deletes the draft for the current page and user
 */
function act_draftdel($act){
    global $INFO;
    @unlink($INFO['draft']);
    $INFO['draft'] = null;
    return 'show';
}

/**
 * Saves a draft on preview
 *
 * @todo this currently duplicates code from ajax.php :-/
 */
function act_draftsave($act){
    global $INFO;
    global $ID;
    global $conf;
    if($conf['usedraft'] && $_POST['wikitext']){
        $draft = array('id'     => $ID,
                'prefix' => substr($_POST['prefix'], 0, -1),
                'text'   => $_POST['wikitext'],
                'suffix' => $_POST['suffix'],
                'date'   => (int) $_POST['date'],
                'client' => $INFO['client'],
                );
        $cname = getCacheName($draft['client'].$ID,'.draft');
        if(io_saveFile($cname,serialize($draft))){
            $INFO['draft'] = $cname;
        }
    }
    return $act;
}

/**
 * Handle 'save'
 *
 * Checks for spam and conflicts and saves the page.
 * Does a redirect to show the page afterwards or
 * returns a new action.
 *
 * @author Andreas Gohr <andi@splitbrain.org>
 */
function act_save($act){
    global $ID;
    global $DATE;
    global $PRE;
    global $TEXT;
    global $SUF;
    global $SUM;
    global $lang;
    global $INFO;

    //spam check
    if(checkwordblock()) {
        msg($lang['wordblock'], -1);
        return 'edit';
    }
    //conflict check
    if($DATE != 0 && $INFO['meta']['date']['modified'] > $DATE )
        return 'conflict';

    //save it
    saveWikiText($ID,con($PRE,$TEXT,$SUF,1),$SUM,$_REQUEST['minor']); //use pretty mode for con
    //unlock it
    unlock($ID);

    //delete draft
    act_draftdel($act);
    session_write_close();

    // when done, show page
    return 'show';
}

/**
 * Revert to a certain revision
 *
 * @author Andreas Gohr <andi@splitbrain.org>
 */
function act_revert($act){
    global $ID;
    global $REV;
    global $lang;
    // FIXME $INFO['writable'] currently refers to the attic version
    // global $INFO;
    // if (!$INFO['writable']) {
    //     return 'show';
    // }

    // when no revision is given, delete current one
    // FIXME this feature is not exposed in the GUI currently
    $text = '';
    $sum  = $lang['deleted'];
    if($REV){
        $text = rawWiki($ID,$REV);
        if(!$text) return 'show'; //something went wrong
        $sum = sprintf($lang['restored'], dformat($REV));
    }

    // spam check

    if (checkwordblock($text)) {
        msg($lang['wordblock'], -1);
        return 'edit';
    }

    saveWikiText($ID,$text,$sum,false);
    msg($sum,1);

    //delete any draft
    act_draftdel($act);
    session_write_close();

    // when done, show current page
    $_SERVER['REQUEST_METHOD'] = 'post'; //should force a redirect
    $REV = '';
    return 'show';
}

/**
 * Do a redirect after receiving post data
 *
 * Tries to add the section id as hash mark after section editing
 */
function act_redirect($id,$preact){
    global $PRE;
    global $TEXT;

    $opts = array(
            'id'       => $id,
            'preact'   => $preact
            );
    //get section name when coming from section edit
    if($PRE && preg_match('/^\s*==+([^=\n]+)/',$TEXT,$match)){
        $check = false; //Byref
        $opts['fragment'] = sectionID($match[0], $check);
    }

    trigger_event('ACTION_SHOW_REDIRECT',$opts,'act_redirect_execute');
}

function act_redirect_execute($opts){
    $go = wl($opts['id'],'',true);
    if(isset($opts['fragment'])) $go .= '#'.$opts['fragment'];

    //show it
    send_redirect($go);
}

/**
 * Handle 'login', 'logout'
 *
 * @author Andreas Gohr <andi@splitbrain.org>
 */
function act_auth($act){
    global $ID;
    global $INFO;

    //already logged in?
    if(isset($_SERVER['REMOTE_USER']) && $act=='login'){
        return 'show';
    }

    //handle logout
    if($act=='logout'){
        $lockedby = checklock($ID); //page still locked?
        if($lockedby == $_SERVER['REMOTE_USER'])
            unlock($ID); //try to unlock

        // do the logout stuff
        auth_logoff();

        // rebuild info array
        $INFO = pageinfo();

        act_redirect($ID,'login');
    }

    return $act;
}

/**
 * Handle 'edit', 'preview', 'recover'
 *
 * @author Andreas Gohr <andi@splitbrain.org>
 */
function act_edit($act){
    global $ID;
    global $INFO;

    global $TEXT;
    global $RANGE;
    global $PRE;
    global $SUF;
    global $REV;
    global $SUM;
    global $lang;
    global $DATE;

    if (!isset($TEXT)) {
        if ($INFO['exists']) {
            if ($RANGE) {
                list($PRE,$TEXT,$SUF) = rawWikiSlices($RANGE,$ID,$REV);
            } else {
                $TEXT = rawWiki($ID,$REV);
            }
        } else {
            $TEXT = pageTemplate($ID);
        }
    }

    //set summary default
    if(!$SUM){
        if($REV){
            $SUM = $lang['restored'];
        }elseif(!$INFO['exists']){
            $SUM = $lang['created'];
        }
    }

    // Use the date of the newest revision, not of the revision we edit
    // This is used for conflict detection
    if(!$DATE) $DATE = @filemtime(wikiFN($ID));

    //check if locked by anyone - if not lock for my self
    //do not lock when the user can't edit anyway
    if ($INFO['writable']) {
        $lockedby = checklock($ID);
        if($lockedby) return 'locked';

        lock($ID);
    }

    return $act;
}

/**
 * Export a wiki page for various formats
 *
 * Triggers ACTION_EXPORT_POSTPROCESS
 *
 *  Event data:
 *    data['id']      -- page id
 *    data['mode']    -- requested export mode
 *    data['headers'] -- export headers
 *    data['output']  -- export output
 *
 * @author Andreas Gohr <andi@splitbrain.org>
 * @author Michael Klier <chi@chimeric.de>
 */
function act_export($act){
    global $ID;
    global $REV;
    global $conf;
    global $lang;

    $pre = '';
    $post = '';
    $output = '';
    $headers = array();

    // search engines: never cache exported docs! (Google only currently)
    $headers['X-Robots-Tag'] = 'noindex';

    $mode = substr($act,7);
    switch($mode) {
        case 'raw':
            $headers['Content-Type'] = 'text/plain; charset=utf-8';
            $headers['Content-Disposition'] = 'attachment; filename='.noNS($ID).'.txt';
            $output = rawWiki($ID,$REV);
            break;
        case 'xhtml':
            $pre .= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"' . DOKU_LF;
            $pre .= ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' . DOKU_LF;
            $pre .= '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="'.$conf['lang'].'"' . DOKU_LF;
            $pre .= ' lang="'.$conf['lang'].'" dir="'.$lang['direction'].'">' . DOKU_LF;
            $pre .= '<head>' . DOKU_LF;
            $pre .= '  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . DOKU_LF;
            $pre .= '  <title>'.$ID.'</title>' . DOKU_LF;

            // get metaheaders
            ob_start();
            tpl_metaheaders();
            $pre .= ob_get_clean();

            $pre .= '</head>' . DOKU_LF;
            $pre .= '<body>' . DOKU_LF;
            $pre .= '<div class="dokuwiki export">' . DOKU_LF;

            // get toc
            $pre .= tpl_toc(true);

            $headers['Content-Type'] = 'text/html; charset=utf-8';
            $output = p_wiki_xhtml($ID,$REV,false);

            $post .= '</div>' . DOKU_LF;
            $post .= '</body>' . DOKU_LF;
            $post .= '</html>' . DOKU_LF;
            break;
        case 'xhtmlbody':
            $headers['Content-Type'] = 'text/html; charset=utf-8';
            $output = p_wiki_xhtml($ID,$REV,false);
            break;
        default:
            $output = p_cached_output(wikiFN($ID,$REV), $mode);
            $headers = p_get_metadata($ID,"format $mode");
            break;
    }

    // prepare event data
    $data = array();
    $data['id'] = $ID;
    $data['mode'] = $mode;
    $data['headers'] = $headers;
    $data['output'] =& $output;

    trigger_event('ACTION_EXPORT_POSTPROCESS', $data);

    if(!empty($data['output'])){
        if(is_array($data['headers'])) foreach($data['headers'] as $key => $val){
            header("$key: $val");
        }
        print $pre.$data['output'].$post;
        exit;
    }
    return 'show';
}

/**
 * Handle sitemap delivery
 *
 * @author Michael Hamann <michael@content-space.de>
 */
function act_sitemap($act) {
    global $conf;

    if ($conf['sitemap'] < 1 || !is_numeric($conf['sitemap'])) {
        header("HTTP/1.0 404 Not Found");
        print "Sitemap generation is disabled.";
        exit;
    }

    $sitemap = Sitemapper::getFilePath();
    if(strrchr($sitemap, '.') === '.gz'){
        $mime = 'application/x-gzip';
    }else{
        $mime = 'application/xml; charset=utf-8';
    }

    // Check if sitemap file exists, otherwise create it
    if (!is_readable($sitemap)) {
        Sitemapper::generate();
    }

    if (is_readable($sitemap)) {
        // Send headers
        header('Content-Type: '.$mime);
        header('Content-Disposition: attachment; filename='.basename($sitemap));

        http_conditionalRequest(filemtime($sitemap));

        // Send file
        //use x-sendfile header to pass the delivery to compatible webservers
        if (http_sendfile($sitemap)) exit;

        readfile($sitemap);
        exit;
    }

    header("HTTP/1.0 500 Internal Server Error");
    print "Could not read the sitemap file - bad permissions?";
    exit;
}

/**
 * Handle page 'subscribe'
 *
 * Throws exception on error.
 *
 * @author Adrian Lang <lang@cosmocode.de>
 */
function act_subscription($act){
    global $lang;
    global $INFO;
    global $ID;

    // subcriptions work for logged in users only
    if(!$_SERVER['REMOTE_USER']) return 'show';

    // get and preprocess data.
    $params = array();
    foreach(array('target', 'style', 'action') as $param) {
        if (isset($_REQUEST["sub_$param"])) {
            $params[$param] = $_REQUEST["sub_$param"];
        }
    }

    // any action given? if not just return and show the subscription page
    if(!$params['action'] || !checkSecurityToken()) return $act;

    // Handle POST data, may throw exception.
    trigger_event('ACTION_HANDLE_SUBSCRIBE', $params, 'subscription_handle_post');

    $target = $params['target'];
    $style  = $params['style'];
    $data   = $params['data'];
    $action = $params['action'];

    // Perform action.
    if (!subscription_set($_SERVER['REMOTE_USER'], $target, $style, $data)) {
        throw new Exception(sprintf($lang["subscr_{$action}_error"],
                                    hsc($INFO['userinfo']['name']),
                                    prettyprint_id($target)));
    }
    msg(sprintf($lang["subscr_{$action}_success"], hsc($INFO['userinfo']['name']),
                prettyprint_id($target)), 1);
    act_redirect($ID, $act);

    // Assure that we have valid data if act_redirect somehow fails.
    $INFO['subscribed'] = get_info_subscribed();
    return 'show';
}

/**
 * Validate POST data
 *
 * Validates POST data for a subscribe or unsubscribe request. This is the
 * default action for the event ACTION_HANDLE_SUBSCRIBE.
 *
 * @author Adrian Lang <lang@cosmocode.de>
 */
function subscription_handle_post(&$params) {
    global $INFO;
    global $lang;

    // Get and validate parameters.
    if (!isset($params['target'])) {
        throw new Exception('no subscription target given');
    }
    $target = $params['target'];
    $valid_styles = array('every', 'digest');
    if (substr($target, -1, 1) === ':') {
        // Allow “list” subscribe style since the target is a namespace.
        $valid_styles[] = 'list';
    }
    $style  = valid_input_set('style', $valid_styles, $params,
                              'invalid subscription style given');
    $action = valid_input_set('action', array('subscribe', 'unsubscribe'),
                              $params, 'invalid subscription action given');

    // Check other conditions.
    if ($action === 'subscribe') {
        if ($INFO['userinfo']['mail'] === '') {
            throw new Exception($lang['subscr_subscribe_noaddress']);
        }
    } elseif ($action === 'unsubscribe') {
        $is = false;
        foreach($INFO['subscribed'] as $subscr) {
            if ($subscr['target'] === $target) {
                $is = true;
            }
        }
        if ($is === false) {
            throw new Exception(sprintf($lang['subscr_not_subscribed'],
                                        $_SERVER['REMOTE_USER'],
                                        prettyprint_id($target)));
        }
        // subscription_set deletes a subscription if style = null.
        $style = null;
    }

    $data = in_array($style, array('list', 'digest')) ? time() : null;
    $params = compact('target', 'style', 'data', 'action');
}

//Setup VIM: ex: et ts=2 :