<?php
/*
CodeColorer plugin core part
https://kpumuk.info/projects/wordpress-plugins/codecolorer
*/
/*
    Copyright 2006 - 2026  Dmytro Shteflyuk <kpumuk@kpumuk.info>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

if (!defined('ABSPATH')) {
    exit;
}

class CodeColorer
{
    private $blocks = array();
    private $comments = array();

    private $geshiExternal = false;

    private $geshi;
    private $optionsPage;

    private $samplePhpCode = '
        [cc_php]
        /**
         * Comment
         */
        function hello() {
          echo "Hello!";
          return null;
        }
        exit();
        [/cc_php]
    ';

    /** Search content for code tags and replace it */
    public function beforeHighlightCodeBlock($content)
    {
        $helper = $this;

        $content = preg_replace_callback(
            '#(\s*)\[cc([^\s\]_]*(?:_[^\s\]]*)?)([^\]]*)\](.*?)\[/cc\2\](\s*)#si',
            function ($matches) use ($helper, $content) {
                return $helper->performHighlightCodeBlock(
                    $matches[4],
                    $matches[3],
                    $content,
                    $matches[2],
                    $matches[1],
                    $matches[5]
                );
            },
            $content
        );

        $content = preg_replace_callback(
            '#(\s*)\<code(.*?)\>(.*?)\</code\>(\s*)#si',
            function ($matches) use ($helper, $content) {
                return $helper->performHighlightCodeBlock(
                    $matches[3],
                    $matches[2],
                    $content,
                    '',
                    $matches[1],
                    $matches[4]
                );
            },
            $content
        );

        return $content;
    }

    public function afterHighlightCodeBlock($content)
    {
        $content = str_replace(array_keys($this->blocks), array_values($this->blocks), $content);

        return $content;
    }

    public function beforeProtectComment($content)
    {
        $helper = $this;
        $content = preg_replace_callback(
            '#(\s*)(\[cc([^\s\]_]*(?:_[^\s\]]*)?)[^\]]*\].*?\[/cc\3\])(\s*)#si',
            function ($matches) use ($helper, $content) {
                return $helper->performProtectComment($matches[2], $content, $matches[1], $matches[4]);
            },
            $content
        );
        $content = preg_replace_callback(
            '#(\s*)(\<code.*?\>.*?\</code\>)(\s*)#si',
            function ($matches) use ($helper, $content) {
                return $helper->performProtectComment($matches[2], $content, $matches[1], $matches[3]);
            },
            $content
        );

        return $content;
    }

    public function afterProtectComment($content)
    {
        $content = str_replace(array_keys($this->comments), array_values($this->comments), $content);
        $this->comments = array();

        return $content;
    }

    /**
     * Perform code highlighting.
     * @SuppressWarnings(PHPMD.ElseExpression)
     */
    private function performHighlightCodeBlock($text, $opts, $content, $suffix = '', $before = '', $after = '')
    {
        // Parse options
        $options = CodeColorerOptions::parseOptions($opts, $suffix);

        // Load code from a file
        if (isset($options['file'])) {
            $uploadPath = wp_upload_dir();
            $baseDir = realpath($uploadPath['basedir']);
            $filePath = realpath(path_join($baseDir, $options['file']));
            # Security check: do not allow to display arbitrary files, only the ones from
            # uploads folder.
            if (false === $filePath || 0 !== strncmp($baseDir, $filePath, strlen($baseDir)) || !is_file($filePath)) {
                $text = 'Specified file is not in uploads folder, does not exists, or not a file.';
                $options['lang'] = 'text';
            } else {
                $text = file_get_contents($filePath);
            }
        }

        // Preprocess source text
        $text = str_replace(array("\\\"", "\\\'"), array ("\"", "\'"), $text);
        $text = preg_replace('/(< \?php)/i', '<?php', $text);
        $text = preg_replace('/(?:^(?:\s*[\r\n])+|\s+$)/', '', $text);

        if ($options['escaped']) {
            $text = html_entity_decode($text, ENT_QUOTES);
            $text = preg_replace_callback(
                '~&#x0*([0-9a-f]+);~i',
                function ($matches) {
                    return chr(hexdec($matches[1]));
                },
                $text
            );
            $text = preg_replace_callback(
                '~&#0*([0-9]+);~',
                function ($matches) {
                    return chr($matches[1]);
                },
                $text
            );
        }

        $result = '';
        // Check if CodeColorer has been disabled for this particular block
        if (!$options['enabled']) {
            $result = '<code>' . esc_html($text) . '</code>';
        } else {
          // See if we should force a height
            $numLines = count(explode("\n", $text));

            $result = $this->performHighlightGeshi($text, $options);

            $result = $this->addContainer($result, $options, $numLines);
        }

        if ($options['inline']) {
            $blockID = $this->getBlockID($content, false, '<span>', '</span>');
        } else {
            $blockID = $this->getBlockID($content);
        }
        $this->blocks[$blockID] = $result;

        if ($options['inline']) {
            $result = $before . $blockID . $after;
        } else {
            $result = "\n\n$blockID\n\n";
        }

        return $result;
    }

    /**
     * Perform code protecting from mangling by Wordpress (used in Comments)
     */
    private function performProtectComment($text, $content, $before, $after)
    {
        $text = str_replace(array("\\\"", "\\\'"), array ("\"", "\'"), $text);

        $blockID = $this->getBlockID($content, true, '', '');
        $this->comments[$blockID] = $text;

        return $before . $blockID . $after;
    }

    /**
     * Perform code highlighting using GESHi engine
     */
    private function performHighlightGeshi($content, $options)
    {
        /* Geshi configuration */
        if (!$this->geshi) {
            $this->geshi = new GeSHi();
            $this->geshi->enable_line_numbers(GESHI_NO_LINE_NUMBERS, 1);
            if (is_feed()) {
                $this->geshi->set_overall_style(CodeColorerOptions::feedOverallStyle());
            }
        }

        $geshi = $this->geshi;
        $geshi->set_source($content);
        $geshi->set_language($options['lang']);
        $geshi->set_overall_class('codecolorer');
        $geshi->set_tab_width($options['tab_size']);
        if (!is_feed()) {
            $geshi->enable_classes($options['theme'] != 'geshi');
            $geshi->set_overall_style('');
            if ($options['nowrap']) {
                $geshi->set_overall_style('white-space:nowrap');
            }
        } else {
            $geshi->enable_classes(false);
        }
        if (!is_null($options['strict'])) {
            $geshi->enable_strict_mode($options['strict']);
        }
        if (isset($options['no_links']) && $options['no_links']) {
            $geshi->enable_keyword_links(false);
        }
        if (isset($options['highlight'])) {
            $hlines = explode(',', $options['highlight']);
            $highlight = array(); /* Empty array to store processed line numbers*/
            foreach ($hlines as $v) {
                list($from, $to) = array_pad(explode('-', $v, 2), 2, null);
                if (is_null($to)) {
                    $to = $from;
                }
                for ($i = $from; $i <= $to; $i++) {
                    array_push($highlight, $i);
                }
            }
            /* Sort the array in ascending numerical order */
            sort($highlight);
            $geshi->highlight_lines_extra($highlight);
            $geshi->set_highlight_lines_extra_style('background-color:#ffff66');
        }
        $geshi->set_header_type($options['inline'] ? GESHI_HEADER_NONE : GESHI_HEADER_DIV);

        $result = $geshi->parse_code();

        if ($geshi->error()) {
            return $geshi->error();
        }

        if ($options['line_numbers'] && !$options['inline']) {
            $table = '<table cellspacing="0" cellpadding="0"><tbody><tr><td ';
            if (is_feed()) {
                $table .= 'style="' . CodeColorerOptions::feedLineNumbersStyle() . '"';
            } else {
                $table .= 'class="line-numbers"';
            }
            $table .= '><div>';
            for ($i = 0, $count = substr_count($result, '<br />') + 1; $i < $count; $i++) {
                $table .= ($i + $options['first_line']) . '<br />';
            }
            $result = $table . '</div></td><td>' . $result . '</td></tr></tbody></table>';
        }

        return $result;
    }

    private function addContainer($html, $options, $numLines)
    {
        $lang = $options['lang'];
        $customCSSClass = empty($options['class']) ? '' : ' ' . $options['class'];

        if ($options['inline']) {
            $theme = empty($options['inline_theme']) ? 'default' : $options['inline_theme'];
            $cssClass = 'codecolorer ' . $lang . ' ' . $theme . $customCSSClass;
            $result  = '<code class="' . esc_attr($cssClass) . '">';
            $result .= '<span class="' . esc_attr($lang) . '">' . $html . '</span>';
            $result .= '</code>';
        } else {
            $theme = empty($options['theme']) ? 'default' : $options['theme'];
            $style = '';
            if ($options['nowrap']) {
                $style .= 'overflow:auto;white-space:nowrap;';
            }
            if (is_feed()) {
                $style .= 'border:1px solid #9F9F9F;';
            }
            $style .= $this->getDimensionRule('width', is_feed() ? $options['rss_width'] : $options['width']);
            if ($numLines > $options['lines'] && $options['lines'] > 0) {
                $style .= $this->getDimensionRule('height', $options['height']);
            }

            $cssClass = 'codecolorer-container ' . $lang . ' ' . $theme . $customCSSClass;
            if ($options['noborder']) {
                $cssClass .= ' codecolorer-noborder';
            }
            $result = '<div class="' . esc_attr($cssClass) . '" style="' . esc_attr($style) . '">' . $html . '</div>';
        }

        return $result;
    }

    /**
     * Generate a block ID that will be replaced at the end (after all that
     * crazy WP text work!) with the right code
     */
    private function getBlockID($content, $comment = false, $before = '<div>', $after = '</div>')
    {
        static $num = 0;

        $block = $comment ? 'COMMENT' : 'BLOCK';
        $before = $before . '::CODECOLORER_' . $block . '_';
        $after = '::' . $after;

        // Just do a check to make sure the user
        // hasn't (however unlikely) input block replacements
        // as legit text
        do {
            ++$num;
            $blockID = $before . $num . $after;
        } while (strpos($content, $blockID) !== false);

        return $blockID;
    }

    private function getDimensionRule($dimension, $value)
    {
        if (empty($value)) {
            return '';
        }
        return "$dimension:$value" . (is_numeric($value) ? 'px;' : ';');
    }

    public function showWarning($type, $title, $message)
    {
        $type = sanitize_key($type);
        $dismissUrl = CodeColorerLoader::getDismissNoticeUrl($type);

        echo '<div id="codecolorer-' . esc_attr($type) . '" class="notice notice-warning"><p><strong>' .
            esc_html($title) .
            '</strong> ' .
            wp_kses($message, array('a' => array('href' => array()))) .
            ' <a href="' .
            esc_url($dismissUrl) .
            '">' .
            esc_html__('Close', 'codecolorer') .
            "</a></p></div>\n";
    }

    public function showGeshiWarning()
    {
        if ($this->geshiExternal) {
            $this->showWarning('concurrent', __('CodeColorer has detected a problem.', 'codecolorer'), sprintf(__('We found another plugin based on GeSHi library in your system. CodeColorer will work, but our version of GeSHi contain some patches, so we can\'t guarantee an ideal code highlighting now. Please review your <a href="%1$s">plugins</a>, maybe you don\'t need them all.', 'codecolorer'), admin_url('plugins.php')));
        }
    }

    public function showOptionsPage()
    {
        $page = $this->getOptionsPage();
        $page->show();
    }

    private function getOptionsPage()
    {
        if (!$this->optionsPage) {
            if (!class_exists('CodeColorerAdmin')) {
                $path = dirname(__FILE__);
                if (!file_exists("$path/codecolorer-admin.php")) return false;
                require_once("$path/codecolorer-admin.php");
            }
            $this->optionsPage = new CodeColorerAdmin($this);
        }
        return $this->optionsPage;
    }

    public function getCodeHighlighted($code)
    {
        $content = $this->beforeHighlightCodeBlock($code);
        return $this->afterHighlightCodeBlock($content);
    }

    public function getSampleCodeHighlighted()
    {
        return $this->getCodeHighlighted($this->samplePhpCode);
    }

    public static function &getInstance()
    {
        static $instance = null;

        if (null === $instance) {
            $path = dirname(__FILE__);
            if (!class_exists('CodeColorerOptions')) {
                if (!file_exists("$path/codecolorer-options.php")) {
                    return null;
                }
                require_once("$path/codecolorer-options.php");
            }

            $instance = new CodeColorer();

            # Maybe GeSHi has been loaded by some another plugin?
            if (!class_exists('GeSHi')) {
                if (!file_exists("$path/lib/geshi.php")) {
                    return null;
                }
                require_once("$path/lib/geshi.php");
            } else {
                $instance->geshiExternal = true;
            }
        }

        return $instance;
    }
}
