-
-
Save inuitviking/bf3f0836e25a7043ef55f08f0be3a408 to your computer and use it in GitHub Desktop.
A League\CommonMark extension for matching very particular link containing images
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Imms\plugins\imms\gitlab\src\Extensions\DownloadLastTag; | |
use League\CommonMark\Environment\EnvironmentBuilderInterface; | |
use League\CommonMark\Extension\ExtensionInterface; | |
final class DownloadLastTagExtension implements ExtensionInterface{ | |
/** | |
* Simply registers the extension and the included parser | |
* | |
* @param EnvironmentBuilderInterface $environment | |
* @return void | |
*/ | |
public function register(EnvironmentBuilderInterface $environment): void { | |
$environment->addInlineParser(new DownloadLastTagParser, priority: 10); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Imms\plugins\imms\gitlab\src\Extensions\DownloadLastTag; | |
use CzProject\GitPhp\GitException; | |
use CzProject\GitPhp\Runners\CliRunner; | |
use Gitlab\Client; | |
use Imms\Classes\Bootstrapper; | |
use League\CommonMark\Extension\CommonMark\Node\Inline\Code; | |
use League\CommonMark\Extension\CommonMark\Node\Inline\Link; | |
use League\CommonMark\Node\Inline\Text; | |
use League\CommonMark\Parser\Cursor; | |
use League\CommonMark\Parser\Inline\InlineParserInterface; | |
use League\CommonMark\Parser\Inline\InlineParserMatch; | |
use League\CommonMark\Parser\InlineParserContext; | |
class DownloadLastTagParser implements InlineParserInterface { | |
private static string $urlRegex = '(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._+~#=]{2,256}\.[a-z]{2,6}\b[-a-zA-Z0-9@:%_+.~#?&\/=]*'; | |
/** | |
* Gets the match definition of an image link that looks something like this: | |
* | |
* - `![latest tag](https://gitlab.com/inuitviking/imms)` | |
* - `![latest release](https://gitlab.com/inuitviking/imms/-/badges/release.svg?order_by=release_at&value_width=110)` | |
* | |
* @return InlineParserMatch | |
*/ | |
public function getMatchDefinition(): InlineParserMatch { | |
$regex = InlineParserMatch::regex('\[!\[(latest|last) (tag|release)\]\(' . self::$urlRegex . '\)\]\(' . self::$urlRegex . '\)'); | |
echo $regex->getRegex().'<br>'; | |
return $regex; | |
} | |
/** | |
* Parses the matched text. | |
* | |
* If everything below matches, replace the text with the latest version found in the remote git repository | |
* | |
* @param InlineParserContext $inlineContext | |
* @return bool | |
*/ | |
public function parse(InlineParserContext $inlineContext): bool { | |
$cursor = $inlineContext->getCursor(); | |
$fullMatch = strtolower($inlineContext->getFullMatch()); | |
return $this->elementToLastTag($cursor, $inlineContext, $fullMatch); | |
} | |
/** | |
* Gets the last release/tag from the match. | |
* Returns `false` if the version wasn't found. | |
* | |
* @param string $match | |
* @return string|false | |
*/ | |
private function getLatestRelease (string $match): string|false { | |
// Remove all the unnecessary fluff from the match | |
$replacements = [ | |
'![latest tag](', | |
'![latest release](', | |
'![last tag](', | |
'![last release](', | |
')' | |
]; | |
$match = str_replace($replacements, '', $match); | |
$match = preg_replace('/\/-.*/', '', $match); | |
// Make use of the CliRunner within GitPhp | |
$runner = new CliRunner(); | |
$cwd = Bootstrapper::rootDirectory(); | |
// If the remote address isn't reachable, we'll try with tokens! | |
$match = $this->nonReachableRemoteMatch($match); | |
if (!str_ends_with($match, '.git')) { | |
$match = $match . '.git'; | |
} | |
$gitArgs = ['-c', 'versionsort.suffix=-', 'ls-remote', '--tags',$match]; | |
try { | |
// Run the command and get the output as an array (and then sanitize it) | |
$output = $runner->run($cwd, $gitArgs)->getOutput(); | |
$output = $this->sanitizeOutput($output); | |
// Return the last output | |
return end($output); | |
} catch (GitException) { | |
echo "Failed to fetch latest tag"; | |
} | |
return false; | |
} | |
/** | |
* Checks whether the provided url has an HTTP code greater than or equal to 200 and lower than 300. | |
* | |
* @param string $match | |
* @return bool | |
*/ | |
private function checkRemoteReachable (string $match): bool { | |
$remoteRepo = curl_init($match); | |
@curl_setopt($remoteRepo, CURLOPT_HEADER , true); | |
@curl_setopt($remoteRepo, CURLOPT_NOBODY , true); | |
@curl_setopt($remoteRepo, CURLOPT_RETURNTRANSFER , true); | |
@curl_setopt($remoteRepo, CURLOPT_TIMEOUT, 20); | |
curl_exec($remoteRepo); | |
$httpCode = curl_getinfo($remoteRepo, CURLINFO_HTTP_CODE); | |
curl_close($remoteRepo); | |
if ($httpCode >= 200 && $httpCode < 300) { | |
return true; | |
} | |
return false; | |
} | |
/** | |
* If the remote is not reachable, we'll change the $match to use tokens. | |
* If it is reachable, we'll not change anything, and simply return the $match in its original state. | |
* | |
* @param string $match | |
* @return string | |
*/ | |
private function nonReachableRemoteMatch (string $match): string { | |
if (!$this->checkRemoteReachable($match)) { | |
$config = Bootstrapper::getIni(); | |
$tokenUser = $config['git']['token_name']; | |
$tokenPass = $config['git']['token_pass']; | |
$authString = "$tokenUser:$tokenPass@"; | |
$url = parse_url($match); | |
$matchReplacements = [ | |
$url['scheme'], | |
'://' | |
]; | |
$match = str_replace($matchReplacements, '', $match); | |
return $url['scheme'] . '://' . $authString . $match; | |
} | |
return $match; | |
} | |
/** | |
* Is meant to fix the output array of the CliRunner. | |
* | |
* Strips ref strings from tag numbers, removes unwanted tags, and lastly sorts it | |
* | |
* @param array $output | |
* @return array | |
*/ | |
private function sanitizeOutput (array $output): array { | |
foreach ($output as &$item) { | |
$item = strrchr($item, '/'); | |
$item = str_replace('/', '', $item); | |
} | |
$output = array_filter($output, function ($var) { return stripos($var, '^{}') === false; }); | |
sort($output, SORT_NATURAL); | |
return $output; | |
} | |
/** | |
* Converts the matching element to the tag we want it to be. | |
* | |
* It returns true or false depending on whether it succeeds. | |
* | |
* @param Cursor $cursor | |
* @param InlineParserContext $inlineContext | |
* @param string $fullMatch | |
* @return bool|void | |
*/ | |
private function elementToLastTag (Cursor $cursor, InlineParserContext $inlineContext, string $fullMatch) { | |
// Save the cursor state in case we need to rewind and bail | |
$previousState = $cursor->saveState(); | |
// The symbol must not have any other characters immediately prior | |
$previousChar = $cursor->peek(-1); | |
if ($previousChar !== null && $previousChar !== ' ' && $previousChar !== '[') { | |
// peek() doesn't modify the cursor, so no need to restore state first | |
return false; | |
} | |
// Advance past the symbol to keep parsing simpler | |
$cursor->advance(); | |
// Parse the match value | |
$identifier = $cursor->match("^\[!\[(latest|last) (tag|release)]\((?:http[s]?://.)?(?:www\.)?[-a-zA-Z0-9@%._+~#=]{2,256}\.[a-z]{2,6}\b[-a-zA-Z0-9@:%_+.~#?&/=]*\)^i"); | |
if ($identifier === null) { | |
// Regex failed to match; this isn't a valid link | |
$cursor->restoreState($previousState); | |
return false; | |
} | |
$identifier = strtolower(substr($identifier, 0, -1)); | |
if (str_contains($identifier, 'latest tag') || str_contains($identifier, 'latest release') | |
|| str_contains($identifier, 'last tag') || str_contains($identifier, 'last release')) { | |
$container = $inlineContext->getContainer(); | |
$container->appendChild(new Code($this->getLatestRelease($fullMatch))); | |
$token = Bootstrapper::getIni()['gitlab']['token']; | |
$groupID = Bootstrapper::getIni()['gitlab']['group_id']; | |
$project = explode('(', $fullMatch)[1]; | |
$project = explode(')', $project)[0]; | |
$project = str_replace('/-/badges/release.svg', '', $project); | |
$projectArr = explode('/', $project); | |
$project = end($projectArr); | |
$client = new Client(); | |
$client->authenticate($token, Client::AUTH_HTTP_TOKEN); | |
$projectId = $client->groups()->search($groupID, ['scope' => 'projects', 'search' => $project])[0]['id']; | |
$packages = $this->getPackages($projectId, $token); | |
$link = $this->getPackageLink(end($packages), $projectId, $token); | |
$container->prependChild(new Text(" - ")); | |
$container->prependChild(new Link($link, "(Download)", "Download")); | |
return true; | |
} | |
} | |
/** | |
* Gets all packages from projectID | |
* | |
* @param int|string $projectId | |
* @param string $token | |
* @return mixed | |
*/ | |
private function getPackages (int|string $projectId, string $token): mixed { | |
$handle = curl_init(); | |
curl_setopt($handle, CURLOPT_URL, "https://gitlab.com/api/v4/projects/$projectId/packages"); | |
curl_setopt($handle, CURLOPT_HTTPHEADER, array('PRIVATE-TOKEN: ' . $token)); | |
curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1); | |
$result = json_decode(curl_exec($handle), true); | |
curl_close($handle); | |
return $result; | |
} | |
/** | |
* Generates a generic download link to the package - only works for the generic package registry. | |
* | |
* Source: https://docs.gitlab.com/ee/api/packages.html | |
* | |
* @param array $packageMetadata | |
* @param int|string $projectId | |
* @param string $token | |
* @return string | |
*/ | |
private function getPackageLink (array $packageMetadata, int|string $projectId, string $token): string { | |
$tokenName = Bootstrapper::getIni()['git']['token_name']; | |
$tokenPass = Bootstrapper::getIni()['git']['token_pass']; | |
$handle = curl_init(); | |
curl_setopt($handle, CURLOPT_URL, "https://gitlab.com/api/v4/projects/$projectId/packages/" . $packageMetadata['id'] . "/package_files"); | |
curl_setopt($handle, CURLOPT_HTTPHEADER, array('PRIVATE-TOKEN: ' . $token)); | |
curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1); | |
$packageFiles = json_decode(curl_exec($handle), true); | |
$package = end($packageFiles); | |
$link = "https://$tokenName:$tokenPass@gitlab.com/api/v4/projects/$projectId/packages/generic/" . $packageMetadata['name'] . '/' . $package['pipelines'][0]['ref'] . '/' . $package['file_name']; | |
curl_close($handle); | |
return $link; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment