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('/^\\[(.+?)\\]:[ ]*(\\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 . '+)[ ]*(.+?)[ ]*(? \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 .= '' . $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) === '
') { + $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