<?php

/**
 * Send component of the DokuWiki Linkback action plugin.
 *
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     Gina Haeussge <osd@foosel.net>
 * @link       http://wiki.foosel.net/snippets/dokuwiki/linkback
 */

use dokuwiki\Form\Form;
use IXR\Client\Client;

require_once (DOKU_PLUGIN . 'linkback/http.php');

class action_plugin_linkback_send extends DokuWiki_Action_Plugin {

    var $preact;

    /**
     * Register the eventhandlers.
     */
    function register(Doku_Event_Handler $controller) {
        $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_editform_output', array ());
        $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handle_editform_output', array ());
        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_action_act_preprocess_before', array());
        $controller->register_hook('ACTION_ACT_PREPROCESS', 'AFTER', $this, 'handle_action_act_preprocess_after', array());
    }

    /**
     * Handler for the ACTION_ACT_PREPROCESS event and BEFORE advise.
     *
     * Saves current action.
     */
    function handle_action_act_preprocess_before(Doku_Event $event, $params) {
        if (is_array($event->data))
            list($this->preact) = array_keys($event->data);
        else
            $this->preact = $event->data;
    }

    /**
     * Handler for the ACTION_ACT_PREPROCESS event and AFTER advise.
     *
     * Sends linkback if previous action was 'save' and new one is show.
     */
    function handle_action_act_preprocess_after(Doku_Event $event, $params) {
        global $ID;
        global $conf;

        // only perform linkbacks on save of a wikipage
        if ($this->preact != 'save' || $event->data != 'show')
            return;

        // if guests are not allowed to perform linkbacks, return
        if (!$this->getConf('allow_guests') && !$_SERVER['REMOTE_USER'])
            return;

        // get linkback meta file name
        $file = metaFN($ID, '.linkbacks');
        $data = array (
            'send' => false,
            'receive' => false,
            'display' => false,
            'sentpings' => array (),
            'receivedpings' => array (),
            'number' => 0,

        );
        if (@ file_exists($file)) {
            $data = unserialize(io_readFile($file, false));
        }
        $data['send'] = (bool)$_REQUEST['plugin__linkback_toggle'];

        if (!$data['send'])
            return;

        $meta = p_get_metadata($ID);

        // prepare linkback info
        $linkback_info = array ();
        $linkback_info['title'] = tpl_pagetitle($ID, true);
        $linkback_info['url'] = wl($ID, '', true);
        $linkback_info['blog_name'] = $conf['title'];
        $linkback_info['excerpt'] = $meta['description']['abstract'];

        // get links
        $ilist = p_cached_instructions(wikiFN($ID),false,$ID);
        if (!is_array($ilist))
            return;
        $pages = $this->_parse_instructionlist($ilist);

        $sentpings = array ();
        foreach ($pages as $page) {
            if (!$data['sentpings'][$page]) {
                // try to ping pages not already pinged
                $this->_ping_page($page, $linkback_info);
            }
            $sentpings[$page] = true;
        }
        $data['sentpings'] = $sentpings;

        // save sent ping info
        io_saveFile($file, serialize($data));
    }

    /**
     * Parses a given instruction list and extracts external and -- if configured
     * that way -- internal links.
     *
     * @param array $list    instruction list as generated by the DokuWiki parser
     */
    function _parse_instructionlist($list) {
        $pages = array ();

        foreach ($list as $item) {
            if ($item[0] == 'externallink') {
                $pages[] = $item[1][0];
            } else
                if ($item[0] == 'internallink' && $this->getConf('ping_internal')) {
                    $pages[] = wl($item[1][0], '', true);
                }
        }

        return $pages;
    }

    /**
     * Handles HTML_EDITFORM_OUTPUT event.
     */
    function handle_editform_output(Doku_Event $event, $params) {
        global $ID;
        global $ACT;
        global $INFO;

        // Not in edit mode? Quit
        if ($ACT != 'edit' && $ACT != 'preview')
            return;

        // page not writable? Quit
        if (!$INFO['writable'])
            return;

        // if guests are not allowed to perform linkbacks, return
        if (!$this->getConf('allow_guests') && !$_SERVER['REMOTE_USER'])
            return;

        // get linkback meta file name
        $file = metaFN($ID, '.linkbacks');
        $data = array (
            'send' => false,
            'receive' => false,
            'display' => false,
            'sentpings' => array (),
            'receivedpings' => array (),
            'number' => 0,

        );
        if (@ file_exists($file)) {
            $data = unserialize(io_readFile($file, false));
        } else {
            $namespace_conf = $this->getConf('enabled_namespaces');
            if ($namespace_conf == '*') {
                $data['send'] = true;
            } else {
                $namespaces = explode(',', $namespace_conf);
                $ns = getNS($ID);
                foreach($namespaces as $namespace) {
                    if ($namespace == '') {
                        continue;
                    } else if ($namespace == '*') {
                        $data['send'] = true;
                        break;
                    } else if (strstr($ns, $namespace) === $ns) {
                        $data['send'] = true;
                        break;
                    }
                }
            }
        }

        /** @var Form|Doku_Form $form */
        $form = $event->data;
        if(is_a($form, Form::class)) {
            /** @var Form $pos */
            $pos = $form->findPositionByAttribute('id','wiki__editbar');

            $form->addTagOpen('div', $pos);

            //value of an unchecked checkbox is not submitted, set an extra hidden different value to fix prefilling
            $form->setHiddenField('plugin__linkback_toggle','');
            $checkbox = $form->addCheckbox('plugin__linkback_toggle', $this->getLang('linkback_enabledisable'), $pos + 1)
                ->id('plugin__linkback_toggle')
                ->addClass('edit')
                ->val('1');
            if($data['send']) {
                $checkbox->attr('checked', 'checked');
            }

            $form->addTagClose('div', $pos + 2);

        } else {
            $pos = $form->findElementById('wiki__editbar');
            $form->insertElement($pos, form_makeOpenTag('div', array('id'=>'plugin__linkback_wrapper')));
            $form->insertElement($pos + 1, form_makeCheckboxField('plugin__linkback_toggle', '1', $this->getLang('linkback_enabledisable'), 'plugin__linkback_toggle', 'edit', (($data['send']) ? array('checked' => 'checked') : array())));
            $form->insertElement($pos + 2, form_makeCloseTag('div'));
        }
    }

    /**
     * Pings a given page with the given info.
     */
    function _ping_page($page, $linkback_info) {
        $range = $this->getConf('range') * 1024;

        $http_client = new LinkbackHTTPClient();
        $http_client->headers['Range'] = 'bytes=0-' . $range;
        $http_client->max_bodysize = $range;
        $http_client->max_bodysize_limit = true;

        $data = $http_client->get($page, true);
        if (!$data)
            return false;

        $order = explode(',', $this->getConf('order'));
        foreach ($order as $type) {
            if ($this->_ping_page_linkback(trim($type), $page, $http_client->resp_headers, $data, $linkback_info))
                return true;
        }

        return false;
    }

    /**
     * Discovers and executes the actual linkback of given type
     *
     * @param string $type type of linkback to send, can be "pingback" or "trackback"
     * @param string $page URL of the page to ping
     * @param array $headers headers received from page
     * @param string $body first range bytes of the pages body
     * @param array $linkback_info linkback info
     */
    function _ping_page_linkback($type, $page, $headers, $body, $linkback_info) {
        if (!$this->getConf('enable_' . $type)) {
            return false;
        }

        switch ($type) {
            case 'trackback' :
                {
                    $pingurl = $this->_autodiscover_trackback($page, $body);
                    if (!$pingurl) {
                        return false;
                    }
                    return (bool) $this->_ping_page_trackback($pingurl, $linkback_info);
                }
            case 'pingback' :
                {
                    $xmlrpc_server = $this->_autodiscover_pingback($headers, $body);
                    if (!$xmlrpc_server) {
                        return false;
                    }
                    return (bool) $this->_ping_page_pingback($xmlrpc_server, $linkback_info['url'], $page);
                }
        }
        return false;
    }

    /**
     * Sends a Pingback to the given url, using the supplied data.
     *
     * @param string $xmlrpc_server URL of remote XML-RPC server
     * @param string $source_url URL from which to ping
     * @param string $target_url URL to ping
     */
    function _ping_page_pingback($xmlrpc_server, $source_url, $target_url) {
        $client = new Client($xmlrpc_server);
        return $client->query('pingback.ping', $source_url, $target_url);
    }

    /**
     * Sends a Trackback to the given url, using the supplied data.
     *
     * @param string $pingurl URL to ping
     * @param array $linkback_info Hash containing title, url and blog_name of linking post
     * @return bool
     */
    function _ping_page_trackback($pingurl, $linkback_info) {
        $http_client = new \dokuwiki\HTTP\DokuHTTPClient();
        $success = $http_client->post($pingurl, $linkback_info);
        return ($success !== false);
    }

    /**
     * Autodiscovers a pingback URL in the given HTTP headers and body.
     *
     * @param array $headers the headers received from to be pinged page.
     * @param string $data the body received from the pinged page.
     * @return false|string
     */
    function _autodiscover_pingback(array $headers, string $data) {
        if (isset ($headers['X-Pingback'])) {
            return $headers['X-Pingback'];
        }
        $regex = '!<link rel="pingback" href="([^"]+)" ?/?>!';
        if (!preg_match($regex, $data, $match)) {
            return false;
        }
        return $match[1];
    }

    /**
     * Autodiscovers a trackback URL for the given page URL and site body.
     *
     * @param string $page the url of the page to be pinged.
     * @param string $data the body received from the page to be pinged.
     */
    function _autodiscover_trackback($page, $data) {
        $page_anchorless = substr($page, 0, strrpos($page, '#'));

        $regex = '!<rdf:RDF.*?</rdf:RDF>!is';
        if (preg_match_all($regex, $data, $matches)) {
            foreach ($matches[0] as $rdf) {
                if (!preg_match('!dc:identifier="([^"]+)"!is', $rdf, $match_id))
                    continue;
                $perm_link = $match_id[1];
                if (!($perm_link == $page || $perm_link == $page_anchorless))
                    continue;
                if (!(preg_match('!trackback:ping="([^"]+)"!is', $rdf, $match_plink) || preg_match('!about="([^"]+)"!is', $rdf, $match_plink)))
                    continue;
                return $match_plink[1];
            }
        } else {
            // fix for wordpress
            $regex = '!<a href="([^"]*?)" rel="trackback">!is';
            if (preg_match($regex, $data, $match))
                return $match[1];
        }
        return false;
    }

}
