diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/bootstrap.php | 6 | ||||
-rw-r--r-- | lib/parsedown/LICENSE.txt | 20 | ||||
-rw-r--r-- | lib/parsedown/Parsedown.php | 1712 | ||||
-rw-r--r-- | lib/php-urljoin/LICENSE | 21 | ||||
-rw-r--r-- | lib/php-urljoin/src/urljoin.php | 143 | ||||
-rw-r--r-- | lib/simplehtmldom/LICENSE | 21 | ||||
-rw-r--r-- | lib/simplehtmldom/simple_html_dom.php | 2361 |
7 files changed, 4281 insertions, 3 deletions
diff --git a/lib/bootstrap.php b/lib/bootstrap.php index 6465f5f9..29c8b97d 100644 --- a/lib/bootstrap.php +++ b/lib/bootstrap.php @@ -18,9 +18,9 @@ $files = [ __DIR__ . '/../lib/url.php', __DIR__ . '/../lib/seotags.php', // Vendor - __DIR__ . '/../vendor/parsedown/Parsedown.php', - __DIR__ . '/../vendor/php-urljoin/src/urljoin.php', - __DIR__ . '/../vendor/simplehtmldom/simple_html_dom.php', + __DIR__ . '/../lib/parsedown/Parsedown.php', + __DIR__ . '/../lib/php-urljoin/src/urljoin.php', + __DIR__ . '/../lib/simplehtmldom/simple_html_dom.php', ]; foreach ($files as $file) { require_once $file; diff --git a/lib/parsedown/LICENSE.txt b/lib/parsedown/LICENSE.txt new file mode 100644 index 00000000..8e7c764d --- /dev/null +++ b/lib/parsedown/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013-2018 Emanuil Rusev, erusev.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/parsedown/Parsedown.php b/lib/parsedown/Parsedown.php new file mode 100644 index 00000000..1b9d6d5b --- /dev/null +++ b/lib/parsedown/Parsedown.php @@ -0,0 +1,1712 @@ +<?php + +# +# +# Parsedown +# http://parsedown.org +# +# (c) Emanuil Rusev +# http://erusev.com +# +# For the full license information, view the LICENSE file that was distributed +# with this source code. +# +# + +class Parsedown +{ + # ~ + + const version = '1.7.4'; + + # ~ + + function text($text) + { + # make sure no definitions are set + $this->DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + $markup = $this->lines($lines); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + # + # Setters + # + + function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + $CurrentBlock = null; + + foreach ($lines as $line) + { + if (chop($line) === '') + { + if (isset($CurrentBlock)) + { + $CurrentBlock['interrupted'] = true; + } + + continue; + } + + if (strpos($line, "\t") !== false) + { + $parts = explode("\t", $line); + + $line = $parts[0]; + + unset($parts[0]); + + foreach ($parts as $part) + { + $shortage = 4 - mb_strlen($line, 'utf-8') % 4; + + $line .= str_repeat(' ', $shortage); + $line .= $part; + } + } + + $indent = 0; + + while (isset($line[$indent]) and $line[$indent] === ' ') + { + $indent ++; + } + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) + { + $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); + + if (isset($Block)) + { + $CurrentBlock = $Block; + + continue; + } + else + { + if ($this->isBlockCompletable($CurrentBlock['type'])) + { + $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + $Blocks []= $CurrentBlock; + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) + { + $CurrentBlock['element']['text'] .= "\n".$text; + } + else + { + $Blocks []= $CurrentBlock; + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) + { + $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + } + + # ~ + + $Blocks []= $CurrentBlock; + + unset($Blocks[0]); + + # ~ + + $markup = ''; + + foreach ($Blocks as $Block) + { + if (isset($Block['hidden'])) + { + continue; + } + + $markup .= "\n"; + $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); + } + + $markup .= "\n"; + + # ~ + + return $markup; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block'.$Type.'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block'.$Type.'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] >= 4) + { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['element']['text']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['text']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') + { + $Block = array( + 'markup' => $Line['body'], + ); + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['markup'] .= "\n" . $Line['body']; + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches)) + { + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if (isset($matches[1])) + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r")); + + $class = 'language-'.$language; + + $Element['attributes'] = array( + 'class' => $class, + ); + } + + $Block = array( + 'char' => $Line['text'][0], + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => $Element, + ), + ); + + return $Block; + } + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) + { + $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['text']['text'] .= "\n".$Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + if (isset($Line['text'][1])) + { + $level = 1; + + while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') + { + $level ++; + } + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '# '); + + $Block = array( + 'element' => array( + 'name' => 'h' . min(6, $level), + 'text' => $text, + 'handler' => 'line', + ), + ); + + return $Block; + } + } + + # + # List + + protected function blockList($Line) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); + + if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'element' => array( + 'name' => $name, + 'handler' => 'elements', + ), + ); + + if($name === 'ol') + { + $listStart = stristr($matches[0], '.', true); + + if($listStart !== '1') + { + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $matches[2], + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['li']['text'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $text, + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + return $Block; + } + + if ($Line['indent'] > 0) + { + $Block['li']['text'] []= ''; + + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + unset($Block['interrupted']); + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['text'] as &$li) + { + if (end($li['text']) !== '') + { + $li['text'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => 'lines', + 'text' => (array) $matches[1], + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text'] []= ''; + + unset($Block['interrupted']); + } + + $Block['element']['text'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['text'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) + { + $Block = array( + 'element' => array( + 'name' => 'hr' + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (chop($Line['text'], $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'markup' => $Line['text'], + ); + + $length = strlen($matches[0]); + + $remainder = substr($Line['text'], $length); + + if (trim($remainder) === '') + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + $Block['closed'] = true; + + $Block['void'] = true; + } + } + else + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + return; + } + + if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) + { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open + { + $Block['depth'] ++; + } + + if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close + { + if ($Block['depth'] > 0) + { + $Block['depth'] --; + } + else + { + $Block['closed'] = true; + } + } + + if (isset($Block['interrupted'])) + { + $Block['markup'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['markup'] .= "\n".$Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) + { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => null, + ); + + if (isset($matches[3])) + { + $Data['title'] = $matches[3]; + } + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') + { + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + continue; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['text']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'text' => $headerCell, + 'handler' => 'line', + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => 'text-align: '.$alignment.';', + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'handler' => 'elements', + ), + ); + + $Block['element']['text'] []= array( + 'name' => 'thead', + 'handler' => 'elements', + ); + + $Block['element']['text'] []= array( + 'name' => 'tbody', + 'handler' => 'elements', + 'text' => array(), + ); + + $Block['element']['text'][0]['text'] []= array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $HeaderElements, + ); + + return $Block; + } + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); + + foreach ($matches[0] as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => 'line', + 'text' => $cell, + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: '.$Block['alignments'][$index].';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $Elements, + ); + + $Block['element']['text'][1]['text'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + $Block = array( + 'element' => array( + 'name' => 'p', + 'text' => $Line['text'], + 'handler' => 'line', + ), + ); + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '"' => array('SpecialCharacter'), + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), + '>' => array('SpecialCharacter'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!"*_&[:<>`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables=array()) + { + $markup = ''; + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strpos($text, $marker); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + # check to see if the current inline type is nestable in the current context + + if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables)) + { + continue; + } + + $Inline = $this->{'inline'.$inlineType}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + foreach ($nonNestables as $non_nestable) + { + $Inline['element']['nonNestables'][] = $non_nestable; + } + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $markup .= $this->unmarkedText($unmarkedText); + + # compile the inline + $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $markup .= $this->unmarkedText($unmarkedText); + + $text = substr($text, $markerPosition + 1); + } + + $markup .= $this->unmarkedText($text); + + return $markup; + } + + # + # ~ + # + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches)) + { + $text = $matches[2]; + $text = preg_replace("/[ ]*\n/", ' ', $text); + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = 'mailto:' . $url; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => 'line', + 'text' => $matches[1], + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'markup' => $Excerpt['text'][1], + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['text'], + ), + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => 'line', + 'nonNestables' => array('Url', 'Link'), + 'text' => null, + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['text'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['text']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) + { + return array( + 'markup' => '&', + 'extent' => 1, + ); + } + + $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); + + if (isset($SpecialCharacter[$Excerpt['text'][0]])) + { + return array( + 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', + 'extent' => 1, + ); + } + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'text' => $matches[1], + 'handler' => 'line', + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) + { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + if ($this->breaksEnabled) + { + $text = preg_replace('/[ ]*\n/', "<br />\n", $text); + } + else + { + $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text); + $text = str_replace(" \n", "\n", $text); + } + + return $text; + } + + # + # Handlers + # + + protected function element(array $Element) + { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + + $markup = '<'.$Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= ' '.$name.'="'.self::escape($value).'"'; + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + if (isset($text)) + { + $markup .= '>'; + + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (isset($Element['handler'])) + { + $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']); + } + elseif (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + + $markup .= '</'.$Element['name'].'>'; + } + else + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + foreach ($Elements as $Element) + { + $markup .= "\n" . $this->element($Element); + } + + $markup .= "\n"; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $markup = $this->lines($lines); + + $trimmedMarkup = trim($markup); + + if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>') + { + $markup = $trimmedMarkup; + $markup = substr($markup, 3); + + $position = strpos($markup, "</p>"); + + $markup = substr_replace($markup, '', $position, 4); + } + + return $markup; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/lib/php-urljoin/LICENSE b/lib/php-urljoin/LICENSE new file mode 100644 index 00000000..033fc3c7 --- /dev/null +++ b/lib/php-urljoin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 j. shagam <fluffy@beesbuzz.biz> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/php-urljoin/src/urljoin.php b/lib/php-urljoin/src/urljoin.php new file mode 100644 index 00000000..026b767e --- /dev/null +++ b/lib/php-urljoin/src/urljoin.php @@ -0,0 +1,143 @@ +<?php + +/* + +A spiritual port of Python's urlparse.urljoin() function to PHP. Why this isn't in the standard library is anyone's guess. + +Author: fluffy, http://beesbuzz.biz/ +Latest version at: https://github.com/plaidfluff/php-urljoin + + */ + +function urljoin($base, $rel) { + if (!$base) { + return $rel; + } + + if (!$rel) { + return $base; + } + + $uses_relative = array('', 'ftp', 'http', 'gopher', 'nntp', 'imap', + 'wais', 'file', 'https', 'shttp', 'mms', + 'prospero', 'rtsp', 'rtspu', 'sftp', + 'svn', 'svn+ssh', 'ws', 'wss'); + + $pbase = parse_url($base); + $prel = parse_url($rel); + + if ($prel === false || preg_match('/^[a-z0-9\-.]*[^a-z0-9\-.:][a-z0-9\-.]*:/i', $rel)) { + /* + Either parse_url couldn't parse this, or the original URL + fragment had an invalid scheme character before the first :, + which can confuse parse_url + */ + $prel = array('path' => $rel); + } + + if (array_key_exists('path', $pbase) && $pbase['path'] === '/') { + unset($pbase['path']); + } + + if (isset($prel['scheme'])) { + if ( + $prel['scheme'] != ($pbase['scheme'] ?? null) + || in_array($prel['scheme'], $uses_relative) == false + ) { + return $rel; + } + } + + $merged = array_merge($pbase, $prel); + + // Handle relative paths: + // 'path/to/file.ext' + // './path/to/file.ext' + if (array_key_exists('path', $prel) && substr($prel['path'], 0, 1) != '/') { + + // Normalize: './path/to/file.ext' => 'path/to/file.ext' + if (substr($prel['path'], 0, 2) === './') { + $prel['path'] = substr($prel['path'], 2); + } + + if (array_key_exists('path', $pbase)) { + $dir = preg_replace('@/[^/]*$@', '', $pbase['path']); + $merged['path'] = $dir . '/' . $prel['path']; + } else { + $merged['path'] = '/' . $prel['path']; + } + + } + + if(array_key_exists('path', $merged)) { + // Get the path components, and remove the initial empty one + $pathParts = explode('/', $merged['path']); + array_shift($pathParts); + + $path = []; + $prevPart = ''; + foreach ($pathParts as $part) { + if ($part == '..' && count($path) > 0) { + // Cancel out the parent directory (if there's a parent to cancel) + $parent = array_pop($path); + // But if it was also a parent directory, leave it in + if ($parent == '..') { + array_push($path, $parent); + array_push($path, $part); + } + } else if ($prevPart != '' || ($part != '.' && $part != '')) { + // Don't include empty or current-directory components + if ($part == '.') { + $part = ''; + } + array_push($path, $part); + } + $prevPart = $part; + } + $merged['path'] = '/' . implode('/', $path); + } + + $ret = ''; + if (isset($merged['scheme'])) { + $ret .= $merged['scheme'] . ':'; + } + + if (isset($merged['scheme']) || isset($merged['host'])) { + $ret .= '//'; + } + + if (isset($prel['host'])) { + $hostSource = $prel; + } else { + $hostSource = $pbase; + } + + // username, password, and port are associated with the hostname, not merged + if (isset($hostSource['host'])) { + if (isset($hostSource['user'])) { + $ret .= $hostSource['user']; + if (isset($hostSource['pass'])) { + $ret .= ':' . $hostSource['pass']; + } + $ret .= '@'; + } + $ret .= $hostSource['host']; + if (isset($hostSource['port'])) { + $ret .= ':' . $hostSource['port']; + } + } + + if (isset($merged['path'])) { + $ret .= $merged['path']; + } + + if (isset($prel['query'])) { + $ret .= '?' . $prel['query']; + } + + if (isset($prel['fragment'])) { + $ret .= '#' . $prel['fragment']; + } + + return $ret; +} diff --git a/lib/simplehtmldom/LICENSE b/lib/simplehtmldom/LICENSE new file mode 100644 index 00000000..6040f77b --- /dev/null +++ b/lib/simplehtmldom/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 S.C. Chen, John Schlick, logmanoriginal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
\ No newline at end of file diff --git a/lib/simplehtmldom/simple_html_dom.php b/lib/simplehtmldom/simple_html_dom.php new file mode 100644 index 00000000..3fc95760 --- /dev/null +++ b/lib/simplehtmldom/simple_html_dom.php @@ -0,0 +1,2361 @@ +<?php +/** + * Website: http://sourceforge.net/projects/simplehtmldom/ + * Additional projects: http://sourceforge.net/projects/debugobject/ + * Acknowledge: Jose Solorzano (https://sourceforge.net/projects/php-html/) + * + * Licensed under The MIT License + * See the LICENSE file in the project root for more information. + * + * Authors: + * S.C. Chen + * John Schlick + * Rus Carroll + * logmanoriginal + * + * Contributors: + * Yousuke Kumakura + * Vadim Voituk + * Antcs + * + * Version Rev. 1.9.1 (291) + */ + +define('HDOM_TYPE_ELEMENT', 1); +define('HDOM_TYPE_COMMENT', 2); +define('HDOM_TYPE_TEXT', 3); +define('HDOM_TYPE_ENDTAG', 4); +define('HDOM_TYPE_ROOT', 5); +define('HDOM_TYPE_UNKNOWN', 6); +define('HDOM_QUOTE_DOUBLE', 0); +define('HDOM_QUOTE_SINGLE', 1); +define('HDOM_QUOTE_NO', 3); +define('HDOM_INFO_BEGIN', 0); +define('HDOM_INFO_END', 1); +define('HDOM_INFO_QUOTE', 2); +define('HDOM_INFO_SPACE', 3); +define('HDOM_INFO_TEXT', 4); +define('HDOM_INFO_INNER', 5); +define('HDOM_INFO_OUTER', 6); +define('HDOM_INFO_ENDSPACE', 7); + +defined('DEFAULT_TARGET_CHARSET') || define('DEFAULT_TARGET_CHARSET', 'UTF-8'); +defined('DEFAULT_BR_TEXT') || define('DEFAULT_BR_TEXT', "\r\n"); +defined('DEFAULT_SPAN_TEXT') || define('DEFAULT_SPAN_TEXT', ' '); +defined('MAX_FILE_SIZE') || define('MAX_FILE_SIZE', 600000); +define('HDOM_SMARTY_AS_TEXT', 1); + +function file_get_html( + $url, + $use_include_path = false, + $context = null, + $offset = 0, + $maxLen = -1, + $lowercase = true, + $forceTagsClosed = true, + $target_charset = DEFAULT_TARGET_CHARSET, + $stripRN = true, + $defaultBRText = DEFAULT_BR_TEXT, + $defaultSpanText = DEFAULT_SPAN_TEXT) +{ + if($maxLen <= 0) { $maxLen = MAX_FILE_SIZE; } + + $dom = new simple_html_dom( + null, + $lowercase, + $forceTagsClosed, + $target_charset, + $stripRN, + $defaultBRText, + $defaultSpanText + ); + + /** + * For sourceforge users: uncomment the next line and comment the + * retrieve_url_contents line 2 lines down if it is not already done. + */ + $contents = file_get_contents( + $url, + $use_include_path, + $context, + $offset, + $maxLen + ); + // $contents = retrieve_url_contents($url); + + if (empty($contents) || strlen($contents) > $maxLen) { + $dom->clear(); + return false; + } + + return $dom->load($contents, $lowercase, $stripRN); +} + +function str_get_html( + $str, + $lowercase = true, + $forceTagsClosed = true, + $target_charset = DEFAULT_TARGET_CHARSET, + $stripRN = true, + $defaultBRText = DEFAULT_BR_TEXT, + $defaultSpanText = DEFAULT_SPAN_TEXT) +{ + $dom = new simple_html_dom( + null, + $lowercase, + $forceTagsClosed, + $target_charset, + $stripRN, + $defaultBRText, + $defaultSpanText + ); + + // The following two if statements are rss-bridge patch + if (empty($str)) { + throw new \Exception('Refusing to parse empty string input'); + } + if (strlen($str) > MAX_FILE_SIZE) { + throw new \Exception('Refusing to parse too big input'); + } + + if (empty($str) || strlen($str) > MAX_FILE_SIZE) { + $dom->clear(); + return false; + } + + return $dom->load($str, $lowercase, $stripRN); +} + +function dump_html_tree($node, $show_attr = true, $deep = 0) +{ + $node->dump($node); +} + +class simple_html_dom_node +{ + public $nodetype = HDOM_TYPE_TEXT; + public $tag = 'text'; + public $attr = array(); + public $children = array(); + public $nodes = array(); + public $parent = null; + public $_ = array(); + public $tag_start = 0; + private $dom = null; + + function __construct($dom) + { + $this->dom = $dom; + $dom->nodes[] = $this; + } + + function __destruct() + { + $this->clear(); + } + + function __toString() + { + return $this->outertext(); + } + + function clear() + { + $this->dom = null; + $this->nodes = null; + $this->parent = null; + $this->children = null; + } + + function dump($show_attr = true, $depth = 0) + { + echo str_repeat("\t", $depth) . $this->tag; + + if ($show_attr && count($this->attr) > 0) { + echo '('; + foreach ($this->attr as $k => $v) { + echo "[$k]=>\"$v\", "; + } + echo ')'; + } + + echo "\n"; + + if ($this->nodes) { + foreach ($this->nodes as $node) { + $node->dump($show_attr, $depth + 1); + } + } + } + + function dump_node($echo = true) + { + $string = $this->tag; + + if (count($this->attr) > 0) { + $string .= '('; + foreach ($this->attr as $k => $v) { + $string .= "[$k]=>\"$v\", "; + } + $string .= ')'; + } + + if (count($this->_) > 0) { + $string .= ' $_ ('; + foreach ($this->_ as $k => $v) { + if (is_array($v)) { + $string .= "[$k]=>("; + foreach ($v as $k2 => $v2) { + $string .= "[$k2]=>\"$v2\", "; + } + $string .= ')'; + } else { + $string .= "[$k]=>\"$v\", "; + } + } + $string .= ')'; + } + + if (isset($this->text)) { + $string .= " text: ({$this->text})"; + } + + $string .= ' HDOM_INNER_INFO: '; + + if (isset($node->_[HDOM_INFO_INNER])) { + $string .= "'" . $node->_[HDOM_INFO_INNER] . "'"; + } else { + $string .= ' NULL '; + } + + $string .= ' children: ' . count($this->children); + $string .= ' nodes: ' . count($this->nodes); + $string .= ' tag_start: ' . $this->tag_start; + $string .= "\n"; + + if ($echo) { + echo $string; + return; + } else { + return $string; + } + } + + function parent($parent = null) + { + // I am SURE that this doesn't work properly. + // It fails to unset the current node from it's current parents nodes or + // children list first. + if ($parent !== null) { + $this->parent = $parent; + $this->parent->nodes[] = $this; + $this->parent->children[] = $this; + } + + return $this->parent; + } + + function has_child() + { + return !empty($this->children); + } + + function children($idx = -1) + { + if ($idx === -1) { + return $this->children; + } + + if (isset($this->children[$idx])) { + return $this->children[$idx]; + } + + return null; + } + + function first_child() + { + if (count($this->children) > 0) { + return $this->children[0]; + } + return null; + } + + function last_child() + { + if (count($this->children) > 0) { + return end($this->children); + } + return null; + } + + function next_sibling() + { + if ($this->parent === null) { + return null; + } + + $idx = array_search($this, $this->parent->children, true); + + if ($idx !== false && isset($this->parent->children[$idx + 1])) { + return $this->parent->children[$idx + 1]; + } + + return null; + } + + function prev_sibling() + { + if ($this->parent === null) { + return null; + } + + $idx = array_search($this, $this->parent->children, true); + + if ($idx !== false && $idx > 0) { + return $this->parent->children[$idx - 1]; + } + + return null; + } + + function find_ancestor_tag($tag) + { + global $debug_object; + if (is_object($debug_object)) { $debug_object->debug_log_entry(1); } + + if ($this->parent === null) { + return null; + } + + $ancestor = $this->parent; + + while (!is_null($ancestor)) { + if (is_object($debug_object)) { + $debug_object->debug_log(2, 'Current tag is: ' . $ancestor->tag); + } + + if ($ancestor->tag === $tag) { + break; + } + + $ancestor = $ancestor->parent; + } + + return $ancestor; + } + + function innertext() + { + if (isset($this->_[HDOM_INFO_INNER])) { + return $this->_[HDOM_INFO_INNER]; + } + + if (isset($this->_[HDOM_INFO_TEXT])) { + return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); + } + + $ret = ''; + + foreach ($this->nodes as $n) { + $ret .= $n->outertext(); + } + + return $ret; + } + + function outertext() + { + global $debug_object; + + if (is_object($debug_object)) { + $text = ''; + + if ($this->tag === 'text') { + if (!empty($this->text)) { + $text = ' with text: ' . $this->text; + } + } + + $debug_object->debug_log(1, 'Innertext of tag: ' . $this->tag . $text); + } + + if ($this->tag === 'root') { + return $this->innertext(); + } + + // todo: What is the use of this callback? Remove? + if ($this->dom && $this->dom->callback !== null) { + call_user_func_array($this->dom->callback, array($this)); + } + + if (isset($this->_[HDOM_INFO_OUTER])) { + return $this->_[HDOM_INFO_OUTER]; + } + + if (isset($this->_[HDOM_INFO_TEXT])) { + return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); + } + + $ret = ''; + + if ($this->dom && $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]) { + $ret = $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]->makeup(); + } + + if (isset($this->_[HDOM_INFO_INNER])) { + // todo: <br> should either never have HDOM_INFO_INNER or always + if ($this->tag !== 'br') { + $ret .= $this->_[HDOM_INFO_INNER]; + } + } elseif ($this->nodes) { + foreach ($this->nodes as $n) { + $ret .= $this->convert_text($n->outertext()); + } + } + + if (isset($this->_[HDOM_INFO_END]) && $this->_[HDOM_INFO_END] != 0) { + $ret .= '</' . $this->tag . '>'; + } + + return $ret; + } + + function text() + { + if (isset($this->_[HDOM_INFO_INNER])) { + return $this->_[HDOM_INFO_INNER]; + } + + switch ($this->nodetype) { + case HDOM_TYPE_TEXT: return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); + case HDOM_TYPE_COMMENT: return ''; + case HDOM_TYPE_UNKNOWN: return ''; + } + + if (strcasecmp($this->tag, 'script') === 0) { return ''; } + if (strcasecmp($this->tag, 'style') === 0) { return ''; } + + $ret = ''; + + // In rare cases, (always node type 1 or HDOM_TYPE_ELEMENT - observed + // for some span tags, and some p tags) $this->nodes is set to NULL. + // NOTE: This indicates that there is a problem where it's set to NULL + // without a clear happening. + // WHY is this happening? + if (!is_null($this->nodes)) { + foreach ($this->nodes as $n) { + // Start paragraph after a blank line + if ($n->tag === 'p') { + $ret = trim($ret) . "\n\n"; + } + + $ret .= $this->convert_text($n->text()); + + // If this node is a span... add a space at the end of it so + // multiple spans don't run into each other. This is plaintext + // after all. + if ($n->tag === 'span') { + $ret .= $this->dom->default_span_text; + } + } + } + return $ret; + } + + function xmltext() + { + $ret = $this->innertext(); + $ret = str_ireplace('<![CDATA[', '', $ret); + $ret = str_replace(']]>', '', $ret); + return $ret; + } + + function makeup() + { + // text, comment, unknown + if (isset($this->_[HDOM_INFO_TEXT])) { + return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); + } + + $ret = '<' . $this->tag; + $i = -1; + + foreach ($this->attr as $key => $val) { + ++$i; + + // skip removed attribute + if ($val === null || $val === false) { continue; } + + $ret .= $this->_[HDOM_INFO_SPACE][$i][0]; + + //no value attr: nowrap, checked selected... + if ($val === true) { + $ret .= $key; + } else { + switch ($this->_[HDOM_INFO_QUOTE][$i]) + { + case HDOM_QUOTE_DOUBLE: $quote = '"'; break; + case HDOM_QUOTE_SINGLE: $quote = '\''; break; + default: $quote = ''; + } + + $ret .= $key + . $this->_[HDOM_INFO_SPACE][$i][1] + . '=' + . $this->_[HDOM_INFO_SPACE][$i][2] + . $quote + . $val + . $quote; + } + } + + $ret = $this->dom->restore_noise($ret); + return $ret . $this->_[HDOM_INFO_ENDSPACE] . '>'; + } + + function find($selector, $idx = null, $lowercase = false) + { + $selectors = $this->parse_selector($selector); + if (($count = count($selectors)) === 0) { return array(); } + $found_keys = array(); + + // find each selector + for ($c = 0; $c < $count; ++$c) { + // The change on the below line was documented on the sourceforge + // code tracker id 2788009 + // used to be: if (($levle=count($selectors[0]))===0) return array(); + if (($levle = count($selectors[$c])) === 0) { return array(); } + if (!isset($this->_[HDOM_INFO_BEGIN])) { return array(); } + + $head = array($this->_[HDOM_INFO_BEGIN] => 1); + $cmd = ' '; // Combinator + + // handle descendant selectors, no recursive! + for ($l = 0; $l < $levle; ++$l) { + $ret = array(); + + foreach ($head as $k => $v) { + $n = ($k === -1) ? $this->dom->root : $this->dom->nodes[$k]; + //PaperG - Pass this optional parameter on to the seek function. + $n->seek($selectors[$c][$l], $ret, $cmd, $lowercase); + } + + $head = $ret; + $cmd = $selectors[$c][$l][4]; // Next Combinator + } + + foreach ($head as $k => $v) { + if (!isset($found_keys[$k])) { + $found_keys[$k] = 1; + } + } + } + + // sort keys + ksort($found_keys); + + $found = array(); + foreach ($found_keys as $k => $v) { + $found[] = $this->dom->nodes[$k]; + } + + // return nth-element or array + if (is_null($idx)) { return $found; } + elseif ($idx < 0) { $idx = count($found) + $idx; } + return (isset($found[$idx])) ? $found[$idx] : null; + } + + protected function seek($selector, &$ret, $parent_cmd, $lowercase = false) + { + global $debug_object; + if (is_object($debug_object)) { $debug_object->debug_log_entry(1); } + + list($tag, $id, $class, $attributes, $cmb) = $selector; + $nodes = array(); + + if ($parent_cmd === ' ') { // Descendant Combinator + // Find parent closing tag if the current element doesn't have a closing + // tag (i.e. void element) + $end = (!empty($this->_[HDOM_INFO_END])) ? $this->_[HDOM_INFO_END] : 0; + if ($end == 0) { + $parent = $this->parent; + while (!isset($parent->_[HDOM_INFO_END]) && $parent !== null) { + $end -= 1; + $parent = $parent->parent; + } + $end += $parent->_[HDOM_INFO_END]; + } + + // Get list of target nodes + $nodes_start = $this->_[HDOM_INFO_BEGIN] + 1; + $nodes_count = $end - $nodes_start; + $nodes = array_slice($this->dom->nodes, $nodes_start, $nodes_count, true); + } elseif ($parent_cmd === '>') { // Child Combinator + $nodes = $this->children; + } elseif ($parent_cmd === '+' + && $this->parent + && in_array($this, $this->parent->children)) { // Next-Sibling Combinator + $index = array_search($this, $this->parent->children, true) + 1; + if ($index < count($this->parent->children)) + $nodes[] = $this->parent->children[$index]; + } elseif ($parent_cmd === '~' + && $this->parent + && in_array($this, $this->parent->children)) { // Subsequent Sibling Combinator + $index = array_search($this, $this->parent->children, true); + $nodes = array_slice($this->parent->children, $index); + } + + // Go throgh each element starting at this element until the end tag + // Note: If this element is a void tag, any previous void element is + // skipped. + foreach($nodes as $node) { + $pass = true; + + // Skip root nodes + if(!$node->parent) { + $pass = false; + } + + // Handle 'text' selector + if($pass && $tag === 'text' && $node->tag === 'text') { + $ret[array_search($node, $this->dom->nodes, true)] = 1; + unset($node); + continue; + } + + // Skip if node isn't a child node (i.e. text nodes) + if($pass && !in_array($node, $node->parent->children, true)) { + $pass = false; + } + + // Skip if tag doesn't match + if ($pass && $tag !== '' && $tag !== $node->tag && $tag !== '*') { + $pass = false; + } + + // Skip if ID doesn't exist + if ($pass && $id !== '' && !isset($node->attr['id'])) { + $pass = false; + } + + // Check if ID matches + if ($pass && $id !== '' && isset($node->attr['id'])) { + // Note: Only consider the first ID (as browsers do) + $node_id = explode(' ', trim($node->attr['id']))[0]; + + if($id !== $node_id) { $pass = false; } + } + + // Check if all class(es) exist + if ($pass && $class !== '' && is_array($class) && !empty($class)) { + if (isset($node->attr['class'])) { + $node_classes = explode(' ', $node->attr['class']); + + if ($lowercase) { + $node_classes = array_map('strtolower', $node_classes); + } + + foreach($class as $c) { + if(!in_array($c, $node_classes)) { + $pass = false; + break; + } + } + } else { + $pass = false; + } + } + + // Check attributes + if ($pass + && $attributes !== '' + && is_array($attributes) + && !empty($attributes)) { + foreach($attributes as $a) { + list ( + $att_name, + $att_expr, + $att_val, + $att_inv, + $att_case_sensitivity + ) = $a; + + // Handle indexing attributes (i.e. "[2]") + /** + * Note: This is not supported by the CSS Standard but adds + * the ability to select items compatible to XPath (i.e. + * the 3rd element within it's parent). + * + * Note: This doesn't conflict with the CSS Standard which + * doesn't work on numeric attributes anyway. + */ + if (is_numeric($att_name) + && $att_expr === '' + && $att_val === '') { + $count = 0; + + // Find index of current element in parent + foreach ($node->parent->children as $c) { + if ($c->tag === $node->tag) ++$count; + if ($c === $node) break; + } + + // If this is the correct node, continue with next + // attribute + if ($count === (int)$att_name) continue; + } + + // Check attribute availability + if ($att_inv) { // Attribute should NOT be set + if (isset($node->attr[$att_name])) { + $pass = false; + break; + } + } else { // Attribute should be set + // todo: "plaintext" is not a valid CSS selector! + if ($att_name !== 'plaintext' + && !isset($node->attr[$att_name])) { + $pass = false; + break; + } + } + + // Continue with next attribute if expression isn't defined + if ($att_expr === '') continue; + + // If they have told us that this is a "plaintext" + // search then we want the plaintext of the node - right? + // todo "plaintext" is not a valid CSS selector! + if ($att_name === 'plaintext') { + $nodeKeyValue = $node->text(); + } else { + $nodeKeyValue = $node->attr[$att_name]; + } + + if (is_object($debug_object)) { + $debug_object->debug_log(2, + 'testing node: ' + . $node->tag + . ' for attribute: ' + . $att_name + . $att_expr + . $att_val + . ' where nodes value is: ' + . $nodeKeyValue + ); + } + + // If lowercase is set, do a case insensitive test of + // the value of the selector. + if ($lowercase) { + $check = $this->match( + $att_expr, + strtolower($att_val), + strtolower($nodeKeyValue), + $att_case_sensitivity + ); + } else { + $check = $this->match( + $att_expr, + $att_val, + $nodeKeyValue, + $att_case_sensitivity + ); + } + + if (is_object($debug_object)) { + $debug_object->debug_log(2, + 'after match: ' + . ($check ? 'true' : 'false') + ); + } + + if (!$check) { + $pass = false; + break; + } + } + } + + // Found a match. Add to list and clear node + if ($pass) $ret[$node->_[HDOM_INFO_BEGIN]] = 1; + unset($node); + } + // It's passed by reference so this is actually what this function returns. + if (is_object($debug_object)) { + $debug_object->debug_log(1, 'EXIT - ret: ', $ret); + } + } + + protected function match($exp, $pattern, $value, $case_sensitivity) + { + global $debug_object; + if (is_object($debug_object)) {$debug_object->debug_log_entry(1);} + + if ($case_sensitivity === 'i') { + $pattern = strtolower($pattern); + $value = strtolower($value); + } + + switch ($exp) { + case '=': + return ($value === $pattern); + case '!=': + return ($value !== $pattern); + case '^=': + return preg_match('/^' . preg_quote($pattern, '/') . '/', $value); + case '$=': + return preg_match('/' . preg_quote($pattern, '/') . '$/', $value); + case '*=': + return preg_match('/' . preg_quote($pattern, '/') . '/', $value); + case '|=': + /** + * [att|=val] + * + * Represents an element with the att attribute, its value + * either being exactly "val" or beginning with "val" + * immediately followed by "-" (U+002D). + */ + return strpos($value, $pattern) === 0; + case '~=': + /** + * [att~=val] + * + * Represents an element with the att attribute whose value is a + * whitespace-separated list of words, one of which is exactly + * "val". If "val" contains whitespace, it will never represent + * anything (since the words are separated by spaces). Also if + * "val" is the empty string, it will never represent anything. + */ + return in_array($pattern, explode(' ', trim($value)), true); + } + return false; + } + + protected function parse_selector($selector_string) + { + global $debug_object; + if (is_object($debug_object)) { $debug_object->debug_log_entry(1); } + + /** + * Pattern of CSS selectors, modified from mootools (https://mootools.net/) + * + * Paperg: Add the colon to the attribute, so that it properly finds + * <tag attr:ibute="something" > like google does. + * + * Note: if you try to look at this attribute, you MUST use getAttribute + * since $dom->x:y will fail the php syntax check. + * + * Notice the \[ starting the attribute? and the @? following? This + * implies that an attribute can begin with an @ sign that is not + * captured. This implies that an html attribute specifier may start + * with an @ sign that is NOT captured by the expression. Farther study + * is required to determine of this should be documented or removed. + * + * Matches selectors in this order: + * + * [0] - full match + * + * [1] - tag name + * ([\w:\*-]*) + * Matches the tag name consisting of zero or more words, colons, + * asterisks and hyphens. + * + * [2] - id name + * (?:\#([\w-]+)) + * Optionally matches a id name, consisting of an "#" followed by + * the id name (one or more words and hyphens). + * + * [3] - class names (including dots) + * (?:\.([\w\.-]+))? + * Optionally matches a list of classs, consisting of an "." + * followed by the class name (one or more words and hyphens) + * where multiple classes can be chained (i.e. ".foo.bar.baz") + * + * [4] - attributes + * ((?:\[@?(?:!?[\w:-]+)(?:(?:[!*^$|~]?=)[\"']?(?:.*?)[\"']?)?(?:\s*?(?:[iIsS])?)?\])+)? + * Optionally matches the attributes list + * + * [5] - separator + * ([\/, >+~]+) + * Matches the selector list separator + */ + // phpcs:ignore Generic.Files.LineLength + $pattern = "/([\w:\*-]*)(?:\#([\w-]+))?(?:|\.([\w\.-]+))?((?:\[@?(?:!?[\w:-]+)(?:(?:[!*^$|~]?=)[\"']?(?:.*?)[\"']?)?(?:\s*?(?:[iIsS])?)?\])+)?([\/, >+~]+)/is"; + + preg_match_all( + $pattern, + trim($selector_string) . ' ', // Add final ' ' as pseudo separator + $matches, + PREG_SET_ORDER + ); + + if (is_object($debug_object)) { + $debug_object->debug_log(2, 'Matches Array: ', $matches); + } + + $selectors = array(); + $result = array(); + + foreach ($matches as $m) { + $m[0] = trim($m[0]); + + // Skip NoOps + if ($m[0] === '' || $m[0] === '/' || $m[0] === '//') { continue; } + + // Convert to lowercase + if ($this->dom->lowercase) { + $m[1] = strtolower($m[1]); + } + + // Extract classes + if ($m[3] !== '') { $m[3] = explode('.', $m[3]); } + + /* Extract attributes (pattern based on the pattern above!) + + * [0] - full match + * [1] - attribute name + * [2] - attribute expression + * [3] - attribute value + * [4] - case sensitivity + * + * Note: Attributes can be negated with a "!" prefix to their name + */ + if($m[4] !== '') { + preg_match_all( + "/\[@?(!?[\w:-]+)(?:([!*^$|~]?=)[\"']?(.*?)[\"']?)?(?:\s+?([iIsS])?)?\]/is", + trim($m[4]), + $attributes, + PREG_SET_ORDER + ); + + // Replace element by array + $m[4] = array(); + + foreach($attributes as $att) { + // Skip empty matches + if(trim($att[0]) === '') { continue; } + + $inverted = (isset($att[1][0]) && $att[1][0] === '!'); + $m[4][] = array( + $inverted ? substr($att[1], 1) : $att[1], // Name + (isset($att[2])) ? $att[2] : '', // Expression + (isset($att[3])) ? $att[3] : '', // Value + $inverted, // Inverted Flag + (isset($att[4])) ? strtolower($att[4]) : '', // Case-Sensitivity + ); + } + } + + // Sanitize Separator + if ($m[5] !== '' && trim($m[5]) === '') { // Descendant Separator + $m[5] = ' '; + } else { // Other Separator + $m[5] = trim($m[5]); + } + + // Clear Separator if it's a Selector List + if ($is_list = ($m[5] === ',')) { $m[5] = ''; } + + // Remove full match before adding to results + array_shift($m); + $result[] = $m; + + if ($is_list) { // Selector List + $selectors[] = $result; + $result = array(); + } + } + + if (count($result) > 0) { $selectors[] = $result; } + return $selectors; + } + + function __get($name) + { + if (isset($this->attr[$name])) { + return $this->convert_text($this->attr[$name]); + } + switch ($name) { + case 'outertext': return $this->outertext(); + case 'innertext': return $this->innertext(); + case 'plaintext': return $this->text(); + case 'xmltext': return $this->xmltext(); + default: return array_key_exists($name, $this->attr); + } + } + + function __set($name, $value) + { + global $debug_object; + if (is_object($debug_object)) { $debug_object->debug_log_entry(1); } + + switch ($name) { + case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value; + case 'innertext': + if (isset($this->_[HDOM_INFO_TEXT])) { + return $this->_[HDOM_INFO_TEXT] = $value; + } + return $this->_[HDOM_INFO_INNER] = $value; + } + + if (!isset($this->attr[$name])) { + $this->_[HDOM_INFO_SPACE][] = array(' ', '', ''); + $this->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE; + } + + $this->attr[$name] = $value; + } + + function __isset($name) + { + switch ($name) { + case 'outertext': return true; + case 'innertext': return true; + case 'plaintext': return true; + } + //no value attr: nowrap, checked selected... + return (array_key_exists($name, $this->attr)) ? true : isset($this->attr[$name]); + } + + function __unset($name) + { + if (isset($this->attr[$name])) { unset($this->attr[$name]); } + } + + function convert_text($text) + { + global $debug_object; + if (is_object($debug_object)) { $debug_object->debug_log_entry(1); } + + $converted_text = $text; + + $sourceCharset = ''; + $targetCharset = ''; + + if ($this->dom) { + $sourceCharset = strtoupper($this->dom->_charset); + $targetCharset = strtoupper($this->dom->_target_charset); + } + + if (is_object($debug_object)) { + $debug_object->debug_log(3, + 'source charset: ' + . $sourceCharset + . ' target charaset: ' + . $targetCharset + ); + } + + if (!empty($sourceCharset) + && !empty($targetCharset) + && (strcasecmp($sourceCharset, $targetCharset) != 0)) { + // Check if the reported encoding could have been incorrect and the text is actually already UTF-8 + if ((strcasecmp($targetCharset, 'UTF-8') == 0) + && ($this->is_utf8($text))) { + $converted_text = $text; + } else { + $converted_text = iconv($sourceCharset, $targetCharset, $text); + } + } + + // Lets make sure that we don't have that silly BOM issue with any of the utf-8 text we output. + if ($targetCharset === 'UTF-8') { + if (substr($converted_text, 0, 3) === "\xef\xbb\xbf") { + $converted_text = substr($converted_text, 3); + } + + if (substr($converted_text, -3) === "\xef\xbb\xbf") { + $converted_text = substr($converted_text, 0, -3); + } + } + + return $converted_text; + } + + static function is_utf8($str) + { + $c = 0; $b = 0; + $bits = 0; + $len = strlen($str); + for($i = 0; $i < $len; $i++) { + $c = ord($str[$i]); + if($c > 128) { + if(($c >= 254)) { return false; } + elseif($c >= 252) { $bits = 6; } + elseif($c >= 248) { $bits = 5; } + elseif($c >= 240) { $bits = 4; } + elseif($c >= 224) { $bits = 3; } + elseif($c >= 192) { $bits = 2; } + else { return false; } + if(($i + $bits) > $len) { return false; } + while($bits > 1) { + $i++; + $b = ord($str[$i]); + if($b < 128 || $b > 191) { return false; } + $bits--; + } + } + } + return true; + } + + function get_display_size() + { + global $debug_object; + + $width = -1; + $height = -1; + + if ($this->tag !== 'img') { + return false; + } + + // See if there is aheight or width attribute in the tag itself. + if (isset($this->attr['width'])) { + $width = $this->attr['width']; + } + + if (isset($this->attr['height'])) { + $height = $this->attr['height']; + } + + // Now look for an inline style. + if (isset($this->attr['style'])) { + // Thanks to user gnarf from stackoverflow for this regular expression. + $attributes = array(); + + preg_match_all( + '/([\w-]+)\s*:\s*([^;]+)\s*;?/', + $this->attr['style'], + $matches, + PREG_SET_ORDER + ); + + foreach ($matches as $match) { + $attributes[$match[1]] = $match[2]; + } + + // If there is a width in the style attributes: + if (isset($attributes['width']) && $width == -1) { + // check that the last two characters are px (pixels) + if (strtolower(substr($attributes['width'], -2)) === 'px') { + $proposed_width = substr($attributes['width'], 0, -2); + // Now make sure that it's an integer and not something stupid. + if (filter_var($proposed_width, FILTER_VALIDATE_INT)) { + $width = $proposed_width; + } + } + } + + // If there is a width in the style attributes: + if (isset($attributes['height']) && $height == -1) { + // check that the last two characters are px (pixels) + if (strtolower(substr($attributes['height'], -2)) == 'px') { + $proposed_height = substr($attributes['height'], 0, -2); + // Now make sure that it's an integer and not something stupid. + if (filter_var($proposed_height, FILTER_VALIDATE_INT)) { + $height = $proposed_height; + } + } + } + + } + + // Future enhancement: + // Look in the tag to see if there is a class or id specified that has + // a height or width attribute to it. + + // Far future enhancement + // Look at all the parent tags of this image to see if they specify a + // class or id that has an img selector that specifies a height or width + // Note that in this case, the class or id will have the img subselector + // for it to apply to the image. + + // ridiculously far future development + // If the class or id is specified in a SEPARATE css file thats not on + // the page, go get it and do what we were just doing for the ones on + // the page. + + $result = array( + 'height' => $height, + 'width' => $width + ); + + return $result; + } + + function save($filepath = '') + { + $ret = $this->outertext(); + + if ($filepath !== '') { + file_put_contents($filepath, $ret, LOCK_EX); + } + + return $ret; + } + + function addClass($class) + { + if (is_string($class)) { + $class = explode(' ', $class); + } + + if (is_array($class)) { + foreach($class as $c) { + if (isset($this->class)) { + if ($this->hasClass($c)) { + continue; + } else { + $this->class .= ' ' . $c; + } + } else { + $this->class = $c; + } + } + } else { + if (is_object($debug_object)) { + $debug_object->debug_log(2, 'Invalid type: ', gettype($class)); + } + } + } + + function hasClass($class) + { + if (is_string($class)) { + if (isset($this->class)) { + return in_array($class, explode(' ', $this->class), true); + } + } else { + if (is_object($debug_object)) { + $debug_object->debug_log(2, 'Invalid type: ', gettype($class)); + } + } + + return false; + } + + function removeClass($class = null) + { + if (!isset($this->class)) { + return; + } + + if (is_null($class)) { + $this->removeAttribute('class'); + return; + } + + if (is_string($class)) { + $class = explode(' ', $class); + } + + if (is_array($class)) { + $class = array_diff(explode(' ', $this->class), $class); + if (empty($class)) { + $this->removeAttribute('class'); + } else { + $this->class = implode(' ', $class); + } + } + } + + function getAllAttributes() + { + return $this->attr; + } + + function getAttribute($name) + { + return $this->__get($name); + } + + function setAttribute($name, $value) + { + $this->__set($name, $value); + } + + function hasAttribute($name) + { + return $this->__isset($name); + } + + function removeAttribute($name) + { + $this->__set($name, null); + } + + function remove() + { + if ($this->parent) { + $this->parent->removeChild($this); + } + } + + function removeChild($node) + { + $nidx = array_search($node, $this->nodes, true); + $cidx = array_search($node, $this->children, true); + $didx = array_search($node, $this->dom->nodes, true); + + if ($nidx !== false && $cidx !== false && $didx !== false) { + + foreach($node->children as $child) { + $node->removeChild($child); + } + + foreach($node->nodes as $entity) { + $enidx = array_search($entity, $node->nodes, true); + $edidx = array_search($entity, $node->dom->nodes, true); + + if ($enidx !== false && $edidx !== false) { + unset($node->nodes[$enidx]); + unset($node->dom->nodes[$edidx]); + } + } + + unset($this->nodes[$nidx]); + unset($this->children[$cidx]); + unset($this->dom->nodes[$didx]); + + $node->clear(); + + } + } + + function getElementById($id) + { + return $this->find("#$id", 0); + } + + function getElementsById($id, $idx = null) + { + return $this->find("#$id", $idx); + } + + function getElementByTagName($name) + { + return $this->find($name, 0); + } + + function getElementsByTagName($name, $idx = null) + { + return $this->find($name, $idx); + } + + function parentNode() + { + return $this->parent(); + } + + function childNodes($idx = -1) + { + return $this->children($idx); + } + + function firstChild() + { + return $this->first_child(); + } + + function lastChild() + { + return $this->last_child(); + } + + function nextSibling() + { + return $this->next_sibling(); + } + + function previousSibling() + { + return $this->prev_sibling(); + } + + function hasChildNodes() + { + return $this->has_child(); + } + + function nodeName() + { + return $this->tag; + } + + function appendChild($node) + { + $node->parent($this); + return $node; + } + +} + +class simple_html_dom +{ + public $root = null; + public $nodes = array(); + public $callback = null; + public $lowercase = false; + public $original_size; + public $size; + + protected $pos; + protected $doc; + protected $char; + + protected $cursor; + protected $parent; + protected $noise = array(); + protected $token_blank = " \t\r\n"; + protected $token_equal = ' =/>'; + protected $token_slash = " />\r\n\t"; + protected $token_attr = ' >'; + + public $_charset = ''; + public $_target_charset = ''; + + protected $default_br_text = ''; + + public $default_span_text = ''; + + protected $self_closing_tags = array( + 'area' => 1, + 'base' => 1, + 'br' => 1, + 'col' => 1, + 'embed' => 1, + 'hr' => 1, + 'img' => 1, + 'input' => 1, + 'link' => 1, + 'meta' => 1, + 'param' => 1, + 'source' => 1, + 'track' => 1, + 'wbr' => 1 + ); + protected $block_tags = array( + 'body' => 1, + 'div' => 1, + 'form' => 1, + 'root' => 1, + 'span' => 1, + 'table' => 1 + ); + protected $optional_closing_tags = array( + // Not optional, see + // https://www.w3.org/TR/html/textlevel-semantics.html#the-b-element + 'b' => array('b' => 1), + 'dd' => array('dd' => 1, 'dt' => 1), + // Not optional, see + // https://www.w3.org/TR/html/grouping-content.html#the-dl-element + 'dl' => array('dd' => 1, 'dt' => 1), + 'dt' => array('dd' => 1, 'dt' => 1), + 'li' => array('li' => 1), + 'optgroup' => array('optgroup' => 1, 'option' => 1), + 'option' => array('optgroup' => 1, 'option' => 1), + 'p' => array('p' => 1), + 'rp' => array('rp' => 1, 'rt' => 1), + 'rt' => array('rp' => 1, 'rt' => 1), + 'td' => array('td' => 1, 'th' => 1), + 'th' => array('td' => 1, 'th' => 1), + 'tr' => array('td' => 1, 'th' => 1, 'tr' => 1), + ); + + function __construct( + $str = null, + $lowercase = true, + $forceTagsClosed = true, + $target_charset = DEFAULT_TARGET_CHARSET, + $stripRN = true, + $defaultBRText = DEFAULT_BR_TEXT, + $defaultSpanText = DEFAULT_SPAN_TEXT, + $options = 0) + { + if ($str) { + if (preg_match('/^http:\/\//i', $str) || is_file($str)) { + $this->load_file($str); + } else { + $this->load( + $str, + $lowercase, + $stripRN, + $defaultBRText, + $defaultSpanText, + $options + ); + } + } + // Forcing tags to be closed implies that we don't trust the html, but + // it can lead to parsing errors if we SHOULD trust the html. + if (!$forceTagsClosed) { + $this->optional_closing_array = array(); + } + + $this->_target_charset = $target_charset; + } + + function __destruct() + { + $this->clear(); + } + + function load( + $str, + $lowercase = true, + $stripRN = true, + $defaultBRText = DEFAULT_BR_TEXT, + $defaultSpanText = DEFAULT_SPAN_TEXT, + $options = 0) + { + global $debug_object; + + // prepare + $this->prepare($str, $lowercase, $defaultBRText, $defaultSpanText); + + // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037 + // Script tags removal now preceeds style tag removal. + // strip out <script> tags + $this->remove_noise("'<\s*script[^>]*[^/]>(.*?)<\s*/\s*script\s*>'is"); + $this->remove_noise("'<\s*script\s*>(.*?)<\s*/\s*script\s*>'is"); + + // strip out the \r \n's if we are told to. + if ($stripRN) { + $this->doc = str_replace("\r", ' ', $this->doc); + $this->doc = str_replace("\n", ' ', $this->doc); + + // set the length of content since we have changed it. + $this->size = strlen($this->doc); + } + + // strip out cdata + $this->remove_noise("'<!\[CDATA\[(.*?)\]\]>'is", true); + // strip out comments + $this->remove_noise("'<!--(.*?)-->'is"); + // strip out <style> tags + $this->remove_noise("'<\s*style[^>]*[^/]>(.*?)<\s*/\s*style\s*>'is"); + $this->remove_noise("'<\s*style\s*>(.*?)<\s*/\s*style\s*>'is"); + // strip out preformatted tags + $this->remove_noise("'<\s*(?:code)[^>]*>(.*?)<\s*/\s*(?:code)\s*>'is"); + // strip out server side scripts + $this->remove_noise("'(<\?)(.*?)(\?>)'s", true); + + if($options & HDOM_SMARTY_AS_TEXT) { // Strip Smarty scripts + $this->remove_noise("'(\{\w)(.*?)(\})'s", true); + } + + // parsing + $this->parse(); + // end + $this->root->_[HDOM_INFO_END] = $this->cursor; + $this->parse_charset(); + + // make load function chainable + return $this; + } + + function load_file() + { + $args = func_get_args(); + + if(($doc = call_user_func_array('file_get_contents', $args)) !== false) { + $this->load($doc, true); + } else { + return false; + } + } + + function set_callback($function_name) + { + $this->callback = $function_name; + } + + function remove_callback() + { + $this->callback = null; + } + + function save($filepath = '') + { + $ret = $this->root->innertext(); + if ($filepath !== '') { file_put_contents($filepath, $ret, LOCK_EX); } + return $ret; + } + + function find($selector, $idx = null, $lowercase = false) + { + return $this->root->find($selector, $idx, $lowercase); + } + + function clear() + { + if (isset($this->nodes)) { + foreach ($this->nodes as $n) { + $n->clear(); + $n = null; + } + } + + // This add next line is documented in the sourceforge repository. + // 2977248 as a fix for ongoing memory leaks that occur even with the + // use of clear. + if (isset($this->children)) { + foreach ($this->children as $n) { + $n->clear(); + $n = null; + } + } + + if (isset($this->parent)) { + $this->parent->clear(); + unset($this->parent); + } + + if (isset($this->root)) { + $this->root->clear(); + unset($this->root); + } + + unset($this->doc); + unset($this->noise); + } + + function dump($show_attr = true) + { + $this->root->dump($show_attr); + } + + protected function prepare( + $str, $lowercase = true, + $defaultBRText = DEFAULT_BR_TEXT, + $defaultSpanText = DEFAULT_SPAN_TEXT) + { + $this->clear(); + + $this->doc = trim($str); + $this->size = strlen($this->doc); + $this->original_size = $this->size; // original size of the html + $this->pos = 0; + $this->cursor = 1; + $this->noise = array(); + $this->nodes = array(); + $this->lowercase = $lowercase; + $this->default_br_text = $defaultBRText; + $this->default_span_text = $defaultSpanText; + $this->root = new simple_html_dom_node($this); + $this->root->tag = 'root'; + $this->root->_[HDOM_INFO_BEGIN] = -1; + $this->root->nodetype = HDOM_TYPE_ROOT; + $this->parent = $this->root; + if ($this->size > 0) { $this->char = $this->doc[0]; } + } + + protected function parse() + { + while (true) { + // Read next tag if there is no text between current position and the + // next opening tag. + if (($s = $this->copy_until_char('<')) === '') { + if($this->read_tag()) { + continue; + } else { + return true; + } + } + + // Add a text node for text between tags + $node = new simple_html_dom_node($this); + ++$this->cursor; + $node->_[HDOM_INFO_TEXT] = $s; + $this->link_nodes($node, false); + } + } + + protected function parse_charset() + { + global $debug_object; + + $charset = null; + + if (function_exists('get_last_retrieve_url_contents_content_type')) { + $contentTypeHeader = get_last_retrieve_url_contents_content_type(); + $success = preg_match('/charset=(.+)/', $contentTypeHeader, $matches); + if ($success) { + $charset = $matches[1]; + if (is_object($debug_object)) { + $debug_object->debug_log(2, + 'header content-type found charset of: ' + . $charset + ); + } + } + } + + if (empty($charset)) { + // https://www.w3.org/TR/html/document-metadata.html#statedef-http-equiv-content-type + $el = $this->root->find('meta[http-equiv=Content-Type]', 0, true); + + if (!empty($el)) { + $fullvalue = $el->content; + if (is_object($debug_object)) { + $debug_object->debug_log(2, + 'meta content-type tag found' + . $fullvalue + ); + } + + if (!empty($fullvalue)) { + $success = preg_match( + '/charset=(.+)/i', + $fullvalue, + $matches + ); + + if ($success) { + $charset = $matches[1]; + } else { + // If there is a meta tag, and they don't specify the + // character set, research says that it's typically + // ISO-8859-1 + if (is_object($debug_object)) { + $debug_object->debug_log(2, + 'meta content-type tag couldn\'t be parsed. using iso-8859 default.' + ); + } + + $charset = 'ISO-8859-1'; + } + } + } + } + + if (empty($charset)) { + // https://www.w3.org/TR/html/document-metadata.html#character-encoding-declaration + if ($meta = $this->root->find('meta[charset]', 0)) { + $charset = $meta->charset; + if (is_object($debug_object)) { + $debug_object->debug_log(2, 'meta charset: ' . $charset); + } + } + } + + if (empty($charset)) { + // Try to guess the charset based on the content + // Requires Multibyte String (mbstring) support (optional) + if (function_exists('mb_detect_encoding')) { + /** + * mb_detect_encoding() is not intended to distinguish between + * charsets, especially single-byte charsets. Its primary + * purpose is to detect which multibyte encoding is in use, + * i.e. UTF-8, UTF-16, shift-JIS, etc. + * + * -- https://bugs.php.net/bug.php?id=38138 + * + * Adding both CP1251/ISO-8859-5 and CP1252/ISO-8859-1 will + * always result in CP1251/ISO-8859-5 and vice versa. + * + * Thus, only detect if it's either UTF-8 or CP1252/ISO-8859-1 + * to stay compatible. + */ + $encoding = mb_detect_encoding( + $this->doc, + array( 'UTF-8', 'CP1252', 'ISO-8859-1' ) + ); + + if ($encoding === 'CP1252' || $encoding === 'ISO-8859-1') { + // Due to a limitation of mb_detect_encoding + // 'CP1251'/'ISO-8859-5' will be detected as + // 'CP1252'/'ISO-8859-1'. This will cause iconv to fail, in + // which case we can simply assume it is the other charset. + if (!@iconv('CP1252', 'UTF-8', $this->doc)) { + $encoding = 'CP1251'; + } + } + + if ($encoding !== false) { + $charset = $encoding; + if (is_object($debug_object)) { + $debug_object->debug_log(2, 'mb_detect: ' . $charset); + } + } + } + } + + if (empty($charset)) { + // Assume it's UTF-8 as it is the most likely charset to be used + $charset = 'UTF-8'; + if (is_object($debug_object)) { + $debug_object->debug_log(2, 'No match found, assume ' . $charset); + } + } + + // Since CP1252 is a superset, if we get one of it's subsets, we want + // it instead. + if ((strtolower($charset) == 'iso-8859-1') + || (strtolower($charset) == 'latin1') + || (strtolower($charset) == 'latin-1')) { + $charset = 'CP1252'; + if (is_object($debug_object)) { + $debug_object->debug_log(2, + 'replacing ' . $charset . ' with CP1252 as its a superset' + ); + } + } + + if (is_object($debug_object)) { + $debug_object->debug_log(1, 'EXIT - ' . $charset); + } + + return $this->_charset = $charset; + } + + protected function read_tag() + { + // Set end position if no further tags found + if ($this->char !== '<') { + $this->root->_[HDOM_INFO_END] = $this->cursor; + return false; + } + + $begin_tag_pos = $this->pos; + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + + // end tag + if ($this->char === '/') { + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + + // Skip whitespace in end tags (i.e. in "</ html>") + $this->skip($this->token_blank); + $tag = $this->copy_until_char('>'); + + // Skip attributes in end tags + if (($pos = strpos($tag, ' ')) !== false) { + $tag = substr($tag, 0, $pos); + } + + $parent_lower = strtolower($this->parent->tag); + $tag_lower = strtolower($tag); + + // The end tag is supposed to close the parent tag. Handle situations + // when it doesn't + if ($parent_lower !== $tag_lower) { + // Parent tag does not have to be closed necessarily (optional closing tag) + // Current tag is a block tag, so it may close an ancestor + if (isset($this->optional_closing_tags[$parent_lower]) + && isset($this->block_tags[$tag_lower])) { + + $this->parent->_[HDOM_INFO_END] = 0; + $org_parent = $this->parent; + + // Traverse ancestors to find a matching opening tag + // Stop at root node + while (($this->parent->parent) + && strtolower($this->parent->tag) !== $tag_lower + ){ + $this->parent = $this->parent->parent; + } + + // If we don't have a match add current tag as text node + if (strtolower($this->parent->tag) !== $tag_lower) { + $this->parent = $org_parent; // restore origonal parent + + if ($this->parent->parent) { + $this->parent = $this->parent->parent; + } + + $this->parent->_[HDOM_INFO_END] = $this->cursor; + return $this->as_text_node($tag); + } + } elseif (($this->parent->parent) + && isset($this->block_tags[$tag_lower]) + ) { + // Grandparent exists and current tag is a block tag, so our + // parent doesn't have an end tag + $this->parent->_[HDOM_INFO_END] = 0; // No end tag + $org_parent = $this->parent; + + // Traverse ancestors to find a matching opening tag + // Stop at root node + while (($this->parent->parent) + && strtolower($this->parent->tag) !== $tag_lower + ) { + $this->parent = $this->parent->parent; + } + + // If we don't have a match add current tag as text node + if (strtolower($this->parent->tag) !== $tag_lower) { + $this->parent = $org_parent; // restore origonal parent + $this->parent->_[HDOM_INFO_END] = $this->cursor; + return $this->as_text_node($tag); + } + } elseif (($this->parent->parent) + && strtolower($this->parent->parent->tag) === $tag_lower + ) { // Grandparent exists and current tag closes it + $this->parent->_[HDOM_INFO_END] = 0; + $this->parent = $this->parent->parent; + } else { // Random tag, add as text node + return $this->as_text_node($tag); + } + } + + // Set end position of parent tag to current cursor position + $this->parent->_[HDOM_INFO_END] = $this->cursor; + + if ($this->parent->parent) { + $this->parent = $this->parent->parent; + } + + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + return true; + } + + // start tag + $node = new simple_html_dom_node($this); + $node->_[HDOM_INFO_BEGIN] = $this->cursor; + ++$this->cursor; + $tag = $this->copy_until($this->token_slash); // Get tag name + $node->tag_start = $begin_tag_pos; + + // doctype, cdata & comments... + // <!DOCTYPE html> + // <![CDATA[ ... ]]> + // <!-- Comment --> + if (isset($tag[0]) && $tag[0] === '!') { + $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until_char('>'); + + if (isset($tag[2]) && $tag[1] === '-' && $tag[2] === '-') { // Comment ("<!--") + $node->nodetype = HDOM_TYPE_COMMENT; + $node->tag = 'comment'; + } else { // Could be doctype or CDATA but we don't care + $node->nodetype = HDOM_TYPE_UNKNOWN; + $node->tag = 'unknown'; + } + + if ($this->char === '>') { $node->_[HDOM_INFO_TEXT] .= '>'; } + + $this->link_nodes($node, true); + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + return true; + } + + // The start tag cannot contain another start tag, if so add as text + // i.e. "<<html>" + if ($pos = strpos($tag, '<') !== false) { + $tag = '<' . substr($tag, 0, -1); + $node->_[HDOM_INFO_TEXT] = $tag; + $this->link_nodes($node, false); + $this->char = $this->doc[--$this->pos]; // prev + return true; + } + + // Handle invalid tag names (i.e. "<html#doc>") + if (!preg_match('/^\w[\w:-]*$/', $tag)) { + $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until('<>'); + + // Next char is the beginning of a new tag, don't touch it. + if ($this->char === '<') { + $this->link_nodes($node, false); + return true; + } + + // Next char closes current tag, add and be done with it. + if ($this->char === '>') { $node->_[HDOM_INFO_TEXT] .= '>'; } + $this->link_nodes($node, false); + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + return true; + } + + // begin tag, add new node + $node->nodetype = HDOM_TYPE_ELEMENT; + $tag_lower = strtolower($tag); + $node->tag = ($this->lowercase) ? $tag_lower : $tag; + + // handle optional closing tags + if (isset($this->optional_closing_tags[$tag_lower])) { + // Traverse ancestors to close all optional closing tags + while (isset($this->optional_closing_tags[$tag_lower][strtolower($this->parent->tag)])) { + $this->parent->_[HDOM_INFO_END] = 0; + $this->parent = $this->parent->parent; + } + $node->parent = $this->parent; + } + + $guard = 0; // prevent infinity loop + + // [0] Space between tag and first attribute + $space = array($this->copy_skip($this->token_blank), '', ''); + + // attributes + do { + // Everything until the first equal sign should be the attribute name + $name = $this->copy_until($this->token_equal); + + if ($name === '' && $this->char !== null && $space[0] === '') { + break; + } + + if ($guard === $this->pos) { // Escape infinite loop + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + continue; + } + + $guard = $this->pos; + + // handle endless '<' + // Out of bounds before the tag ended + if ($this->pos >= $this->size - 1 && $this->char !== '>') { + $node->nodetype = HDOM_TYPE_TEXT; + $node->_[HDOM_INFO_END] = 0; + $node->_[HDOM_INFO_TEXT] = '<' . $tag . $space[0] . $name; + $node->tag = 'text'; + $this->link_nodes($node, false); + return true; + } + + // handle mismatch '<' + // Attributes cannot start after opening tag + if ($this->doc[$this->pos - 1] == '<') { + $node->nodetype = HDOM_TYPE_TEXT; + $node->tag = 'text'; + $node->attr = array(); + $node->_[HDOM_INFO_END] = 0; + $node->_[HDOM_INFO_TEXT] = substr( + $this->doc, + $begin_tag_pos, + $this->pos - $begin_tag_pos - 1 + ); + $this->pos -= 2; + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + $this->link_nodes($node, false); + return true; + } + + if ($name !== '/' && $name !== '') { // this is a attribute name + // [1] Whitespace after attribute name + $space[1] = $this->copy_skip($this->token_blank); + + $name = $this->restore_noise($name); // might be a noisy name + + if ($this->lowercase) { $name = strtolower($name); } + + if ($this->char === '=') { // attribute with value + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + $this->parse_attr($node, $name, $space); // get attribute value + } else { + //no value attr: nowrap, checked selected... + $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_NO; + $node->attr[$name] = true; + if ($this->char != '>') { $this->char = $this->doc[--$this->pos]; } // prev + } + + $node->_[HDOM_INFO_SPACE][] = $space; + + // prepare for next attribute + $space = array( + $this->copy_skip($this->token_blank), + '', + '' + ); + } else { // no more attributes + break; + } + } while ($this->char !== '>' && $this->char !== '/'); // go until the tag ended + + $this->link_nodes($node, true); + $node->_[HDOM_INFO_ENDSPACE] = $space[0]; + + // handle empty tags (i.e. "<div/>") + if ($this->copy_until_char('>') === '/') { + $node->_[HDOM_INFO_ENDSPACE] .= '/'; + $node->_[HDOM_INFO_END] = 0; + } else { + // reset parent + if (!isset($this->self_closing_tags[strtolower($node->tag)])) { + $this->parent = $node; + } + } + + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + + // If it's a BR tag, we need to set it's text to the default text. + // This way when we see it in plaintext, we can generate formatting that the user wants. + // since a br tag never has sub nodes, this works well. + if ($node->tag === 'br') { + $node->_[HDOM_INFO_INNER] = $this->default_br_text; + } + + return true; + } + + protected function parse_attr($node, $name, &$space) + { + $is_duplicate = isset($node->attr[$name]); + + if (!$is_duplicate) // Copy whitespace between "=" and value + $space[2] = $this->copy_skip($this->token_blank); + + switch ($this->char) { + case '"': + $quote_type = HDOM_QUOTE_DOUBLE; + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + $value = $this->copy_until_char('"'); + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + break; + case '\'': + $quote_type = HDOM_QUOTE_SINGLE; + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + $value = $this->copy_until_char('\''); + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + break; + default: + $quote_type = HDOM_QUOTE_NO; + $value = $this->copy_until($this->token_attr); + } + + $value = $this->restore_noise($value); + + // PaperG: Attributes should not have \r or \n in them, that counts as + // html whitespace. + $value = str_replace("\r", '', $value); + $value = str_replace("\n", '', $value); + + // PaperG: If this is a "class" selector, lets get rid of the preceeding + // and trailing space since some people leave it in the multi class case. + if ($name === 'class') { + $value = trim($value); + } + + if (!$is_duplicate) { + $node->_[HDOM_INFO_QUOTE][] = $quote_type; + $node->attr[$name] = $value; + } + } + + protected function link_nodes(&$node, $is_child) + { + $node->parent = $this->parent; + $this->parent->nodes[] = $node; + if ($is_child) { + $this->parent->children[] = $node; + } + } + + protected function as_text_node($tag) + { + $node = new simple_html_dom_node($this); + ++$this->cursor; + $node->_[HDOM_INFO_TEXT] = '</' . $tag . '>'; + $this->link_nodes($node, false); + $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + return true; + } + + protected function skip($chars) + { + $this->pos += strspn($this->doc, $chars, $this->pos); + $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + } + + protected function copy_skip($chars) + { + $pos = $this->pos; + $len = strspn($this->doc, $chars, $pos); + $this->pos += $len; + $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + if ($len === 0) { return ''; } + return substr($this->doc, $pos, $len); + } + + protected function copy_until($chars) + { + $pos = $this->pos; + $len = strcspn($this->doc, $chars, $pos); + $this->pos += $len; + $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next + return substr($this->doc, $pos, $len); + } + + protected function copy_until_char($char) + { + if ($this->char === null) { return ''; } + + if (($pos = strpos($this->doc, $char, $this->pos)) === false) { + $ret = substr($this->doc, $this->pos, $this->size - $this->pos); + $this->char = null; + $this->pos = $this->size; + return $ret; + } + + if ($pos === $this->pos) { return ''; } + + $pos_old = $this->pos; + $this->char = $this->doc[$pos]; + $this->pos = $pos; + return substr($this->doc, $pos_old, $pos - $pos_old); + } + + protected function remove_noise($pattern, $remove_tag = false) + { + global $debug_object; + if (is_object($debug_object)) { $debug_object->debug_log_entry(1); } + + $count = preg_match_all( + $pattern, + $this->doc, + $matches, + PREG_SET_ORDER | PREG_OFFSET_CAPTURE + ); + + for ($i = $count - 1; $i > -1; --$i) { + $key = '___noise___' . sprintf('% 5d', count($this->noise) + 1000); + + if (is_object($debug_object)) { + $debug_object->debug_log(2, 'key is: ' . $key); + } + + $idx = ($remove_tag) ? 0 : 1; // 0 = entire match, 1 = submatch + $this->noise[$key] = $matches[$i][$idx][0]; + $this->doc = substr_replace($this->doc, $key, $matches[$i][$idx][1], strlen($matches[$i][$idx][0])); + } + + // reset the length of content + $this->size = strlen($this->doc); + + if ($this->size > 0) { + $this->char = $this->doc[0]; + } + } + + function restore_noise($text) + { + global $debug_object; + if (is_object($debug_object)) { $debug_object->debug_log_entry(1); } + + while (($pos = strpos($text, '___noise___')) !== false) { + // Sometimes there is a broken piece of markup, and we don't GET the + // pos+11 etc... token which indicates a problem outside of us... + + // todo: "___noise___1000" (or any number with four or more digits) + // in the DOM causes an infinite loop which could be utilized by + // malicious software + if (strlen($text) > $pos + 15) { + $key = '___noise___' + . $text[$pos + 11] + . $text[$pos + 12] + . $text[$pos + 13] + . $text[$pos + 14] + . $text[$pos + 15]; + + if (is_object($debug_object)) { + $debug_object->debug_log(2, 'located key of: ' . $key); + } + + if (isset($this->noise[$key])) { + $text = substr($text, 0, $pos) + . $this->noise[$key] + . substr($text, $pos + 16); + } else { + // do this to prevent an infinite loop. + $text = substr($text, 0, $pos) + . 'UNDEFINED NOISE FOR KEY: ' + . $key + . substr($text, $pos + 16); + } + } else { + // There is no valid key being given back to us... We must get + // rid of the ___noise___ or we will have a problem. + $text = substr($text, 0, $pos) + . 'NO NUMERIC NOISE KEY' + . substr($text, $pos + 11); + } + } + return $text; + } + + function search_noise($text) + { + global $debug_object; + if (is_object($debug_object)) { $debug_object->debug_log_entry(1); } + + foreach($this->noise as $noiseElement) { + if (strpos($noiseElement, $text) !== false) { + return $noiseElement; + } + } + } + + function __toString() + { + return $this->root->innertext(); + } + + function __get($name) + { + switch ($name) { + case 'outertext': + return $this->root->innertext(); + case 'innertext': + return $this->root->innertext(); + case 'plaintext': + return $this->root->text(); + case 'charset': + return $this->_charset; + case 'target_charset': + return $this->_target_charset; + } + } + + function childNodes($idx = -1) + { + return $this->root->childNodes($idx); + } + + function firstChild() + { + return $this->root->first_child(); + } + + function lastChild() + { + return $this->root->last_child(); + } + + function createElement($name, $value = null) + { + return @str_get_html("<$name>$value</$name>")->firstChild(); + } + + function createTextNode($value) + { + return @end(str_get_html($value)->nodes); + } + + function getElementById($id) + { + return $this->find("#$id", 0); + } + + function getElementsById($id, $idx = null) + { + return $this->find("#$id", $idx); + } + + function getElementByTagName($name) + { + return $this->find($name, 0); + } + + function getElementsByTagName($name, $idx = -1) + { + return $this->find($name, $idx); + } + + function loadFile() + { + $args = func_get_args(); + $this->load_file($args); + } +} |