diff --git a/.gitignore b/.gitignore index 845e56f..a0e303c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ # Composer vendor -third-party # Cypress cypress/screenshots diff --git a/third-party/erusev/parsedown/Parsedown.php b/third-party/erusev/parsedown/Parsedown.php new file mode 100644 index 0000000..77e1877 --- /dev/null +++ b/third-party/erusev/parsedown/Parsedown.php @@ -0,0 +1,923 @@ +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('/^\\[(.+?)\\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $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 . '+)[ ]*(.+?)[ ]*(? \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/', "
\n", $text); + } else { + $text = \preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\\n/', "
\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']; + } 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 .= ''; + } 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) === '

') { + $markup = $trimmedMarkup; + $markup = \substr($markup, 3); + $position = \strpos($markup, "

"); + $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]); + } 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'); +} +# +# +# 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_alias('GWiz_GF_Code_Chest\\Dependencies\\Parsedown', 'Parsedown', \false); diff --git a/third-party/inc2734/wp-github-plugin-updater/package.json b/third-party/inc2734/wp-github-plugin-updater/package.json new file mode 100644 index 0000000..8b70a29 --- /dev/null +++ b/third-party/inc2734/wp-github-plugin-updater/package.json @@ -0,0 +1,26 @@ +{ + "author": "inc2734", + "license": "GPL-2.0+", + "devDependencies": { + "@wordpress/env": "^4.2.0", + "npm-run-all": "^4.1.5" + }, + "scripts": { + "wp-env": "wp-env", + "start": "wp-env start", + "stop": "wp-env stop", + "cli": "wp-env run cli", + "wp": "wp-env run cli wp", + "pretest": "wp-env start && wp-env run composer 'install --no-interaction'", + "test:lint:php": "wp-env run composer run-script lint", + "test:lint": "run-s test:lint:*", + "test:unit:php": "wp-env run phpunit 'phpunit -c /var/www/html/wp-content/plugins/wp-github-plugin-updater/.phpunit.xml.dist --verbose'", + "test:unit": "run-s test:unit:*", + "test": "run-s test:*", + "makepot": "npm run wp 'i18n make-pot ./wp-content/plugins/wp-github-plugin-updater/src ./wp-content/plugins/wp-github-plugin-updater/src/languages/generic.pot --exclude=./wp-content/plugins/wp-github-plugin-updater/src/assets --domain=inc2734-wp-github-plugin-updater'", + "build": "npm-run-all -p makepot" + }, + "dependencies": { + "@inc2734/for-each-html-nodes": "^0.4.0" + } +} diff --git a/third-party/inc2734/wp-github-plugin-updater/src/App/Model/Fields.php b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/Fields.php new file mode 100644 index 0000000..47d7ad5 --- /dev/null +++ b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/Fields.php @@ -0,0 +1,229 @@ + $value) { + if (\property_exists($this, $field)) { + $this->{$field} = $value; + } + } + } + /** + * Return specific property. + * + * @param string $field Field. + * @return mixed + */ + public function get($field) + { + if (\property_exists($this, $field)) { + return $this->{$field}; + } + return \false; + } +} diff --git a/third-party/inc2734/wp-github-plugin-updater/src/App/Model/GitHubReleases.php b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/GitHubReleases.php new file mode 100644 index 0000000..a4c0f4f --- /dev/null +++ b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/GitHubReleases.php @@ -0,0 +1,173 @@ +plugin_name = $plugin_name; + $this->user_name = $user_name; + $this->repository = $repository; + $this->transient_name = \sprintf('wp_github_plugin_updater_%1$s', $this->plugin_name); + } + /** + * Get release data. + * + * @return array + */ + public function get() + { + $transient = \get_transient($this->transient_name); + if (\false !== $transient) { + return $transient; + } + $response = $this->_request(); + $response = $this->_retrieve($response); + \set_transient($this->transient_name, $response, 60 * 5); + return $response; + } + /** + * Delete transient. + */ + public function delete_transient() + { + \delete_transient($this->transient_name); + } + /** + * Retrieve only the body from the raw response. + * + * @param array|WP_Error $response HTTP response. + * @return array|null|WP_Error + */ + protected function _retrieve($response) + { + global $pagenow; + if (\is_wp_error($response)) { + return $response; + } + $response_code = \wp_remote_retrieve_response_code($response); + if ('' === $response_code) { + return null; + } + $body = \json_decode(\wp_remote_retrieve_body($response)); + if (200 === (int) $response_code) { + $body->package = $body->tag_name ? $this->_get_zip_url($body) : \false; + return $body; + } + $message = null !== $body && \property_exists($body, 'message') ? $body->message : \__('Failed to get update response.', 'inc2734-wp-github-plugin-updater'); + $error_message = \sprintf( + /* Translators: 1: Plugin name, 2: Error message */ + \__('[%1$s] %2$s', 'inc2734-wp-github-plugin-updater'), + $this->plugin_name, + $message + ); + if (\defined('WP_DEBUG') && \WP_DEBUG) { + \error_log('Inc2734_WP_GitHub_Plugin_Updater error. [' . $response_code . '] ' . $error_message); + } + if (!\in_array($pagenow, ['update-core.php', 'plugins.php'], \true)) { + return null; + } + return new WP_Error($response_code, $error_message); + } + /** + * Request to GitHub contributors API. + * + * @return array|WP_Error + */ + protected function _request() + { + $url = \sprintf('https://api.github.com/repos/%1$s/%2$s/releases/latest', $this->user_name, $this->repository); + // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores + $url = \apply_filters(\sprintf('inc2734_github_plugin_updater_request_url_%1$s/%2$s', $this->user_name, $this->repository), $url, $this->user_name, $this->repository); + // phpcs:enable + return Requester::request($url); + } + /** + * Return zip url. + * + * @param object $response Response. + * @return string + */ + protected function _get_zip_url($response) + { + $url = \false; + if (!empty($response->assets) && \is_array($response->assets)) { + if (!empty($response->assets[0]) && \is_object($response->assets[0])) { + if (!empty($response->assets[0]->browser_download_url)) { + $url = $response->assets[0]->browser_download_url; + } + } + } + $tag_name = isset($response->tag_name) ? $response->tag_name : null; + if (!$url && $tag_name) { + $url = \sprintf('https://github.com/%1$s/%2$s/releases/download/%3$s/%2$s.zip', $this->user_name, $this->repository, $tag_name); + $http_status_code = $this->_get_http_status_code($url); + if (!\in_array($http_status_code, [200, 302], \true)) { + $url = \sprintf('https://github.com/%1$s/%2$s/archive/%3$s.zip', $this->user_name, $this->repository, $tag_name); + } + } + // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores + $url = \apply_filters(\sprintf('inc2734_github_plugin_updater_zip_url_%1$s/%2$s', $this->user_name, $this->repository), $url, $this->user_name, $this->repository, $tag_name); + // phpcs:enable + if (!$url) { + \error_log('Inc2734_WP_GitHub_Plugin_Updater error. zip url not found.'); + return \false; + } + $http_status_code = $this->_get_http_status_code($url); + if (!\in_array($http_status_code, [200, 302], \true)) { + \error_log('Inc2734_WP_GitHub_Plugin_Updater error. zip url not found. ' . $http_status_code . ' ' . $url); + return \false; + } + return $url; + } + /** + * Return http status code from $url. + * + * @param string $url Target url. + * @return int + */ + protected function _get_http_status_code($url) + { + $response = Requester::request($url); + return \wp_remote_retrieve_response_code($response); + } +} diff --git a/third-party/inc2734/wp-github-plugin-updater/src/App/Model/GitHubRepositoryContent.php b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/GitHubRepositoryContent.php new file mode 100644 index 0000000..9c9ed35 --- /dev/null +++ b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/GitHubRepositoryContent.php @@ -0,0 +1,136 @@ +plugin_name = $plugin_name; + $this->user_name = $user_name; + $this->repository = $repository; + $this->transient_name = \sprintf('wp_github_plugin_updater_repository_data_%1$s', $this->plugin_name); + } + /** + * Get repository content. + * + * @return string + */ + public function get() + { + $transient = \get_transient($this->transient_name); + if (\false !== $transient) { + return $transient; + } + $response = $this->_request(); + $response = $this->_retrieve($response); + \set_transient($this->transient_name, $response, 0); + return $response; + } + /** + * Delete transient. + */ + public function delete_transient() + { + \delete_transient($this->transient_name); + } + /** + * Return plugin headers. + * + * @see https://developer.wordpress.org/reference/functions/get_file_data/ + * @see https://developer.wordpress.org/reference/functions/get_plugin_data/ + * + * @return array + */ + public function get_headers() + { + $headers = []; + $content = $this->get(); + $target_headers = ['RequiresWP' => 'Requires at least', 'RequiresPHP' => 'Requires PHP', 'Tested up to' => 'Tested up to']; + if (null !== $content) { + $content = \substr($content, 0, 8 * \KB_IN_BYTES); + $content = \str_replace("\r", "\n", $content); + } + foreach ($target_headers as $field => $regex) { + if (\preg_match('/^[ \\t\\/*#@]*' . \preg_quote($regex, '/') . ':(.*)$/mi', $content, $match) && $match[1]) { + $headers[$field] = \_cleanup_header_comment($match[1]); + } else { + $headers[$field] = ''; + } + } + // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores + return \apply_filters(\sprintf('inc2734_github_plugin_updater_repository_content_headers_%1$s/%2$s', $this->user_name, $this->repository), $headers); + // phpcs:enable + } + /** + * Retrieve only the body from the raw response. + * + * @param array|WP_Error $response HTTP response. + * @return string|null|WP_Error + */ + protected function _retrieve($response) + { + if (\is_wp_error($response)) { + return null; + } + $response_code = \wp_remote_retrieve_response_code($response); + if (200 !== $response_code) { + return null; + } + $body = \json_decode(\wp_remote_retrieve_body($response)); + if (!isset($body->content)) { + return null; + } + return \base64_decode($body->content); + } + /** + * Request to GitHub contributors API. + * + * @return array|WP_Error + */ + protected function _request() + { + $url = \sprintf('https://api.github.com/repos/%1$s/%2$s/contents/%3$s', $this->user_name, $this->repository, \basename($this->plugin_name)); + // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores + $url = \apply_filters(\sprintf('inc2734_github_plugin_updater_repository_content_url_%1$s/%2$s', $this->user_name, $this->repository), $url, $this->user_name, $this->repository, \basename($this->plugin_name)); + // phpcs:enable + return Requester::request($url); + } +} diff --git a/third-party/inc2734/wp-github-plugin-updater/src/App/Model/GitHubRepositoryContributors.php b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/GitHubRepositoryContributors.php new file mode 100644 index 0000000..d39980c --- /dev/null +++ b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/GitHubRepositoryContributors.php @@ -0,0 +1,128 @@ +plugin_name = $plugin_name; + $this->user_name = $user_name; + $this->repository = $repository; + $this->transient_name = \sprintf('wp_github_plugin_updater_repository_contributors_data_%1$s', $this->plugin_name); + } + /** + * Get contributors. + * + * @return array + */ + public function get() + { + $transient = \get_transient($this->transient_name); + if (\false !== $transient) { + return $transient; + } + $response = $this->_request(); + $response = $this->_retrieve($response); + $contributors = []; + if (null !== $response) { + foreach ($response as $contributor) { + $contributors[] = ['display_name' => $contributor->login, 'avatar' => $contributor->avatar_url, 'profile' => $contributor->html_url]; + } + } + \set_transient($this->transient_name, $contributors, 0); + return $contributors; + } + /** + * Delete transient. + */ + public function delete_transient() + { + \delete_transient($this->transient_name); + } + /** + * Retrieve only the body from the raw response. + * + * @param array|WP_Error $response HTTP response. + * @return array|null|WP_Error + */ + protected function _retrieve($response) + { + global $pagenow; + if (\is_wp_error($response)) { + return null; + } + $response_code = \wp_remote_retrieve_response_code($response); + if ('' === $response_code) { + return null; + } + $body = \json_decode(\wp_remote_retrieve_body($response)); + if (200 === (int) $response_code) { + return $body; + } + $message = null !== $body && \property_exists($body, 'message') ? $body->message : \__('Failed to get update response.', 'inc2734-wp-github-plugin-updater'); + $error_message = \sprintf( + /* Translators: 1: Plugin name, 2: Error message */ + \__('[%1$s] %2$s', 'inc2734-wp-github-plugin-updater'), + $this->plugin_name, + $message + ); + if (\defined('WP_DEBUG') && \WP_DEBUG) { + \error_log('Inc2734_WP_GitHub_Plugin_Updater error. [' . $response_code . '] ' . $error_message); + } + if (!\in_array($pagenow, ['update-core.php', 'plugins.php'], \true)) { + return null; + } + return new WP_Error($response_code, $error_message); + } + /** + * Request to GitHub contributors API. + * + * @return array|WP_Error + */ + protected function _request() + { + $url = \sprintf('https://api.github.com/repos/%1$s/%2$s/contributors', $this->user_name, $this->repository); + // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores + $url = \apply_filters(\sprintf('inc2734_github_plugin_updater_contributors_url_%1$s/%2$s', $this->user_name, $this->repository), $url, $this->user_name, $this->repository); + // phpcs:enable + return Requester::request($url); + } +} diff --git a/third-party/inc2734/wp-github-plugin-updater/src/App/Model/Requester.php b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/Requester.php new file mode 100644 index 0000000..0152dfa --- /dev/null +++ b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/Requester.php @@ -0,0 +1,23 @@ + 'WordPress/' . $wp_version, 'timeout' => 30, 'headers' => ['Accept-Encoding' => '']]); + } +} diff --git a/third-party/inc2734/wp-github-plugin-updater/src/App/Model/Upgrader.php b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/Upgrader.php new file mode 100644 index 0000000..d3695ad --- /dev/null +++ b/third-party/inc2734/wp-github-plugin-updater/src/App/Model/Upgrader.php @@ -0,0 +1,47 @@ +plugin_name = $plugin_name; + } + /** + * Filters the install response before the installation has started. + * + * @param bool|WP_Error $bool Response. + * @param array $hook_extra Extra arguments passed to hooked filters. + * @return bool|WP_Error. + */ + public function pre_install($bool, $hook_extra) + { + if (!isset($hook_extra['plugin']) || $this->plugin_name !== $hook_extra['plugin']) { + return $bool; + } + global $wp_filesystem; + $plugin_dir = \trailingslashit(\WP_PLUGIN_DIR) . $this->plugin_name; + if (!$wp_filesystem->is_writable($plugin_dir)) { + return new WP_Error(); + } + return $bool; + } +} diff --git a/third-party/inc2734/wp-github-plugin-updater/src/Bootstrap.php b/third-party/inc2734/wp-github-plugin-updater/src/Bootstrap.php new file mode 100644 index 0000000..33a7b2f --- /dev/null +++ b/third-party/inc2734/wp-github-plugin-updater/src/Bootstrap.php @@ -0,0 +1,263 @@ +plugin_name = $plugin_name; + $this->slug = \preg_replace('|^([^/]+)?/.+$|', '$1', $plugin_name); + $this->user_name = $user_name; + $this->repository = $repository; + $this->fields = new Fields($fields); + \load_textdomain('inc2734-wp-github-plugin-updater', __DIR__ . '/languages/' . \get_locale() . '.mo'); + $upgrader = new App\Model\Upgrader($plugin_name); + $this->github_releases = new GitHubReleases($plugin_name, $user_name, $repository); + $this->github_repository_content = new GitHubRepositoryContent($plugin_name, $user_name, $repository); + $this->github_repository_contributors = new GitHubRepositoryContributors($plugin_name, $user_name, $repository); + \add_filter('extra_plugin_headers', [$this, '_extra_plugin_headers']); + \add_filter('pre_set_site_transient_update_plugins', [$this, '_pre_set_site_transient_update_plugins']); + \add_filter('upgrader_pre_install', [$upgrader, 'pre_install'], 10, 2); + \add_filter('plugins_api', [$this, '_plugins_api'], 10, 3); + \add_action('upgrader_process_complete', [$this, '_upgrader_process_complete'], 10, 2); + } + /** + * Filters extra file headers by plugin. + * + * @param array $headers Array of plugin headers. + */ + public function _extra_plugin_headers($headers) + { + if (!\in_array('Tested up to', $headers, \true)) { + $headers[] = 'Tested up to'; + } + return $headers; + } + /** + * Overwrite site_transient_update_plugins. + * + * @see https://make.wordpress.org/core/2020/07/30/recommended-usage-of-the-updates-api-to-support-the-auto-updates-ui-for-plugins-and-themes-in-wordpress-5-5/ + * + * @param false|array $transient Transient of update_plugins. + * @return false|array + */ + public function _pre_set_site_transient_update_plugins($transient) + { + if (!\file_exists(\WP_PLUGIN_DIR . '/' . $this->plugin_name)) { + return $transient; + } + $response = $this->github_releases->get(); + if (\is_wp_error($response)) { + \error_log($response->get_error_message()); + return $transient; + } + if (!isset($response->tag_name)) { + return $transient; + } + if (!$response->package) { + return $transient; + } + $remote = $this->github_repository_content->get_headers(); + $update = (object) ['id' => $this->user_name . '/' . $this->repository . '/' . $this->plugin_name, 'slug' => $this->slug, 'plugin' => $this->plugin_name, 'new_version' => $response->tag_name, 'url' => $this->fields->get('homepage'), 'package' => $response->package, 'icons' => $this->fields->get('icons') ? (array) $this->fields->get('icons') : \false, 'banners' => $this->fields->get('banners'), 'tested' => $this->fields->get('tested') ? $this->fields->get('tested') : $remote['Tested up to'], 'requires_php' => $this->fields->get('requires_php') ? $this->fields->get('requires_php') : $remote['RequiresPHP'], 'requires' => $this->fields->get('requires') ? $this->fields->get('requires') : $remote['RequiresWP']]; + // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores + $update = \apply_filters(\sprintf('inc2734_github_plugin_updater_transient_response_%1$s/%2$s', $this->user_name, $this->repository), $update); + // phpcs:enable + $current = \get_plugin_data(\WP_PLUGIN_DIR . '/' . $this->plugin_name); + if (!$this->_should_update($current['Version'], $response->tag_name)) { + if (\false === $transient) { + $transient = new stdClass(); + $transient->no_update = []; + } + $transient->no_update[$this->plugin_name] = $update; + } else { + if (\false === $transient) { + $transient = new stdClass(); + $transient->response = []; + } + $transient->response[$this->plugin_name] = $update; + } + return $transient; + } + /** + * Filters the object of the plugin which need to be upgraded. + * + * @param object $obj The object of the plugins-api. + * @param string $action The type of information being requested from the Plugin Install API. + * @param object $arg The arguments for the plugins-api. + * @return object The object of the plugins-api which is gotten from GitHub API. + */ + public function _plugins_api($obj, $action, $arg) + { + if ('query_plugins' !== $action && 'plugin_information' !== $action) { + return $obj; + } + if (!isset($arg->slug) || $arg->slug !== $this->slug) { + return $obj; + } + $response = $this->github_releases->get(); + if (\is_wp_error($response)) { + return $obj; + } + $current = \get_plugin_data(\WP_PLUGIN_DIR . '/' . $this->plugin_name); + $remote = $this->github_repository_content->get_headers(); + $obj = new stdClass(); + $obj->slug = $this->plugin_name; + $obj->name = \esc_html($current['Name']); + $obj->plugin_name = \esc_html($current['Name']); + $obj->author = !empty($response->author) ? \sprintf('%2$s', \esc_url($response->author->html_url), \esc_html($response->author->login)) : null; + $obj->version = !empty($response->html_url) && !empty($response->tag_name) ? \sprintf('%2$s', \esc_url($response->html_url), \esc_html($response->tag_name)) : null; + $obj->last_updated = $response->published_at; + $obj->requires = \esc_html($remote['RequiresWP']); + $obj->requires_php = \esc_html($remote['RequiresPHP']); + $obj->tested = \esc_html($remote['Tested up to']); + if (!empty($response->assets) && \is_array($response->assets)) { + if (!empty($response->assets[0]) && \is_object($response->assets[0])) { + if (!empty($response->assets[0]->download_count)) { + $obj->active_installs = $response->assets[0]->download_count; + } + } + } + $contributors = $this->github_repository_contributors->get(); + $obj->contributors = $contributors; + $fields = \array_keys(\get_object_vars($this->fields)); + foreach ($fields as $field) { + if (isset($obj->{$field})) { + continue; + } + $obj->{$field} = $this->fields->get($field); + } + if (empty($obj->sections)) { + $description_url = $this->fields->get('description_url') ? $this->fields->get('description_url') : \WP_PLUGIN_DIR . '/' . \dirname($this->plugin_name) . '/README.md'; + $obj->sections = ['description' => $this->_get_content_text($description_url), 'installation' => $this->_get_content_text($this->fields->get('installation_url')), 'faq' => $this->_get_content_text($this->fields->get('faq_url')), 'screenshots' => $this->_get_content_text($this->fields->get('screenshots_url')), 'changelog' => $this->_get_content_text($this->fields->get('changelog_url')), 'reviews' => $this->_get_content_text($this->fields->get('reviews_url')), 'other_notes' => $this->_get_content_text($this->fields->get('other_notes_url'))]; + } + // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores + $obj = \apply_filters(\sprintf('inc2734_github_plugin_updater_plugins_api_%1$s/%2$s', $this->user_name, $this->repository), $obj, $response); + // phpcs:enable + $obj->external = \true; + $obj->download_link = \false; + return $obj; + } + /** + * Fires when the upgrader process is complete. + * + * @param WP_Upgrader $upgrader_object WP_Upgrader instance. In other contexts this might be a Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or Language_Pack_Upgrader instance. + * @param array $hook_extra Array of bulk item update data. + */ + public function _upgrader_process_complete($upgrader_object, $hook_extra) + { + if ('update' === $hook_extra['action'] && 'plugin' === $hook_extra['type']) { + foreach ($hook_extra['plugins'] as $plugin) { + if ($plugin === $this->plugin_name) { + $this->github_releases->delete_transient(); + $this->github_repository_content->delete_transient(); + } + } + } + } + /** + * Sanitize version. + * + * @param string $version Version to check. + * @return string + */ + protected function _sanitize_version($version) + { + $version = \preg_replace('/^v(.*)$/', '$1', $version); + return $version; + } + /** + * If remote version is newer, return true. + * + * @param string $current_version Current version. + * @param string $remote_version Remove version. + * @return bool + */ + protected function _should_update($current_version, $remote_version) + { + return \version_compare($this->_sanitize_version($current_version), $this->_sanitize_version($remote_version), '<'); + } + /** + * Return content text. + * + * @param string $url Name of the file to read. + * @return string + */ + protected function _get_content_text($url) + { + if (empty($url)) { + return ''; + } + $text = \file_get_contents($url); + if (\false === $text) { + return ''; + } + if ('md' === \substr($url, \strrpos($url, '.') + 1)) { + $parsedown = new Parsedown(); + $text = $parsedown->text($text); + } + return $text; + } +} diff --git a/third-party/inc2734/wp-github-plugin-updater/src/languages/generic.pot b/third-party/inc2734/wp-github-plugin-updater/src/languages/generic.pot new file mode 100644 index 0000000..d345a0d --- /dev/null +++ b/third-party/inc2734/wp-github-plugin-updater/src/languages/generic.pot @@ -0,0 +1,24 @@ +# Copyright (C) 2019 {package-name} +# This file is distributed under the same license as the {package-name} package. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://make.wordpress.org/polyglots/\n" +"POT-Creation-Date: 2019-09-02 11:17+0900\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2019-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"X-Generator: Poedit 2.0.4\n" + +#: Bootstrap.php:287 +msgid "Failed to get update response." +msgstr "" + +#. Translators: 1: Plugin name, 2: Error message +#: Bootstrap.php:292 +msgid "[%1$s] %2$s" +msgstr ""