diff --git a/StudipReleasesPlugin.php b/StudipReleasesPlugin.php new file mode 100644 index 0000000000000000000000000000000000000000..e15f9b5dd6d39708720fe27c8f345f0fd22cac99 --- /dev/null +++ b/StudipReleasesPlugin.php @@ -0,0 +1,24 @@ +<?php +require_once __DIR__ . '/bootstrap.php'; + +final class StudipReleasesPlugin extends TracToGitlab\Plugin implements SystemPlugin +{ + public function __construct() + { + parent::__construct(); + + $navigation = new Navigation( + _('Stud.IP Releases (Beta)'), + PluginEngine::getURL($this, [], 'releases') + ); + $navigation->setImage(Icon::create('download', Icon::ROLE_NAVIGATION)); + Navigation::addItem('/studip-releases', $navigation); + } + + public function perform($unconsumed_path) + { + $this->addStylesheet('assets/releases.scss'); + + parent::perform($unconsumed_path); + } +} diff --git a/TracToGitlabPlugin.php b/TracToGitlabPlugin.php index 5b17310f920e05f833e322272384a9613684836f..9be2f837fab0f0d972f37e136eca0a00c86eced8 100644 --- a/TracToGitlabPlugin.php +++ b/TracToGitlabPlugin.php @@ -1,10 +1,8 @@ <?php require_once __DIR__ . '/bootstrap.php'; -final class TracToGitlabPlugin extends StudIPPlugin implements StandardPlugin, SystemPlugin +final class TracToGitlabPlugin extends TracToGitlab\Plugin implements StandardPlugin, SystemPlugin { - private $container = null; - public function __construct() { parent::__construct(); @@ -23,48 +21,6 @@ final class TracToGitlabPlugin extends StudIPPlugin implements StandardPlugin, S $this->buildNavigation(); } - public function getDIContainer() - { - if ($this->container === null) { - require_once __DIR__ . '/vendor/autoload.php'; - - $this->container = new Pimple\Container(); - - $this->container['trac'] = function () { - return new TracToGitlab\TracLookup(Config::get()->TRAC2GITLAB_TRAC_URL); - }; - - $this->container['gitlab'] = function () { - $builder = new Gitlab\HttpClient\Builder( - new GuzzleHttp\Client(), - new Http\Factory\Guzzle\RequestFactory(), - new Http\Factory\Guzzle\StreamFactory(), - new Http\Factory\Guzzle\UriFactory() - ); - - $client = new Gitlab\Client($builder); - $client->setUrl(Config::get()->TRAC2GITLAB_GITLAB_URL); - $client->authenticate(Config::get()->TRAC2GITLAB_GITLAB_TOKEN, Gitlab\Client::AUTH_HTTP_TOKEN); - return $client; - }; - - $this->container['gitlabProjectId'] = function () { - return Config::get()->TRAC2GITLAB_GITLAB_PROJECT_ID; - }; - - $this->container['gitlabPager'] = function ($c) { - return new Gitlab\ResultPager($c['gitlab']); - }; - - $this->container['parsedown'] = function () { - $parsedown = new Parsedown(); - $parsedown->setSafeMode(true); - return $parsedown; - }; - } - return $this->container; - } - public function getIconNavigation($course_id, $last_visit, $user_id) { return null; diff --git a/assets/releases.scss b/assets/releases.scss new file mode 100644 index 0000000000000000000000000000000000000000..d1f8c10b35e0f902b13f5527422c833967df1a6a --- /dev/null +++ b/assets/releases.scss @@ -0,0 +1,7 @@ +.release-filesize { + white-space: nowrap; +} + +tr:not(:last-child) td.table-divider { + border-bottom-color: var(--light-gray-color-80); +} diff --git a/composer.json b/composer.json index 7e4a589603be88f5fc052b9dd86cadeac248b5c7..acae4f3f3cec0c29eea66a110be14e169d393517 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "http-interop/http-factory-guzzle": "^1.0", "erusev/parsedown": "^1.7", "ext-json": "*", - "pimple/pimple": "^3.4" + "pimple/pimple": "^3.4", + "ext-curl": "*" } } diff --git a/composer.lock b/composer.lock index b4182ad808a802d368deedaffe37ca8ccefb1a72..0efd7ad6f03a6dbfbfe4722c005c9ad4aa415ca1 100644 --- a/composer.lock +++ b/composer.lock @@ -459,32 +459,32 @@ }, { "name": "m4tthumphrey/php-gitlab-api", - "version": "11.4.0", + "version": "11.5.1", "source": { "type": "git", "url": "https://github.com/GitLabPHP/Client.git", - "reference": "4aa7d2af5614bca3f3473dae59fe930dbe73b496" + "reference": "d3364fd373f1ec8588dbd65afb527ea4cdcad8b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GitLabPHP/Client/zipball/4aa7d2af5614bca3f3473dae59fe930dbe73b496", - "reference": "4aa7d2af5614bca3f3473dae59fe930dbe73b496", + "url": "https://api.github.com/repos/GitLabPHP/Client/zipball/d3364fd373f1ec8588dbd65afb527ea4cdcad8b4", + "reference": "d3364fd373f1ec8588dbd65afb527ea4cdcad8b4", "shasum": "" }, "require": { "ext-json": "*", "ext-xml": "*", "php": "^7.2.5 || ^8.0", - "php-http/cache-plugin": "^1.7.1", + "php-http/cache-plugin": "^1.7.2", "php-http/client-common": "^2.3", "php-http/discovery": "^1.12", "php-http/httplug": "^2.2", "php-http/multipart-stream-builder": "^1.1.2", - "psr/cache": "^1.0", + "psr/cache": "^1.0 || ^2.0", "psr/http-client-implementation": "^1.0", "psr/http-factory-implementation": "^1.0", "psr/http-message": "^1.0", - "symfony/options-resolver": "^3.4 || ^4.0 || ^5.0", + "symfony/options-resolver": "^3.4 || ^4.0 || ^5.0 || ^6.0", "symfony/polyfill-php80": "^1.17" }, "require-dev": { @@ -505,19 +505,23 @@ "authors": [ { "name": "Fabien Bourigault", - "email": "bourigaultfabien@gmail.com" + "email": "bourigaultfabien@gmail.com", + "homepage": "https://github.com/fbourigault" }, { "name": "Graham Campbell", - "email": "graham@alt-three.com" + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" }, { "name": "Matt Humphrey", - "email": "matth@windsor-telecom.co.uk" + "email": "matth@windsor-telecom.co.uk", + "homepage": "https://github.com/m4tthumphrey" }, { "name": "Miguel Piedrafita", - "email": "github@miguelpiedrafita.com" + "email": "github@miguelpiedrafita.com", + "homepage": "https://github.com/m1guelpf" } ], "description": "GitLab API v4 client for PHP", @@ -527,7 +531,7 @@ ], "support": { "issues": "https://github.com/GitLabPHP/Client/issues", - "source": "https://github.com/GitLabPHP/Client/tree/11.4.0" + "source": "https://github.com/GitLabPHP/Client/tree/11.5.1" }, "funding": [ { @@ -535,7 +539,7 @@ "type": "github" } ], - "time": "2021-03-27T23:40:20+00:00" + "time": "2022-01-23T18:43:46+00:00" }, { "name": "php-http/cache-plugin", @@ -1711,5 +1715,5 @@ "ext-json": "*" }, "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.3.0" } diff --git a/controllers/releases.php b/controllers/releases.php new file mode 100644 index 0000000000000000000000000000000000000000..1a9ba6f48d50b26b406a16a03dd0c59eeca95cf7 --- /dev/null +++ b/controllers/releases.php @@ -0,0 +1,231 @@ +<?php +final class ReleasesController extends \TracToGitlab\Controller +{ + const CACHE_KEY = 'studip-releases'; + + private static $curl_handle = null; + + public function before_filter(&$action, &$args) + { + parent::before_filter($action, $args); + + Navigation::activateItem('/studip-releases'); + PageLayout::setTitle(_('Stud.IP Releases')); + + $widget = new LinksWidget(); + $widget->setTitle(_('Weiterführende Links')); + $widget->addLink( + _('Releases auf gitlab.studip.de'), + 'https://gitlab.studip.de/studip/studip/-/releases', + Icon::create('link2'), + ['target' => '_blank'] + ); + $widget->addLink( + _('Ältere Releases bei SourceForge'), + 'https://sourceforge.net/projects/studip/files/Stud.IP/', + Icon::create('link2'), + ['target' => '_blank'] + ); + Sidebar::get()->addWidget($widget); + } + + public function after_filter($action, $args) + { + if (self::$curl_handle !== null) { + curl_close(self::$curl_handle); + } + + parent::after_filter($action, $args); + } + + public function index_action() + { + $version = Request::get('version'); + + $this->releases = $this->getReleases(); + + $this->addVersionFilter($this->releases, $version); + $this->addResetAction(); + + if ($version && isset($this->releases[$version])) { + $this->releases = [ + $version => $this->releases[$version], + ]; + } + } + + public function reset_action() + { + if ($this->isAdmin()) { + StudipCacheFactory::getCache()->expire(self::CACHE_KEY); + } + + $this->relocate($this->indexURL()); + } + + private function getReleases(): array + { + $cache = StudipCacheFactory::getCache(); + + $releases = $cache->read(self::CACHE_KEY); + if ($releases === false) { + $temp = array_map( + function (array $release): array { + $links = array_filter( + $release['assets']['links'] ?? [], + function (array $asset): bool { + return $asset['link_type'] === 'package'; + } + ); + $links = array_map( + function (array $link): array { + return [ + 'name' => $link['name'], + 'url' => $link['url'], + 'size' => $this->getFileSize($link['url']), + ]; + }, + $links + ); + usort($links, function ($a, $b) { + return strcmp($a['name'], $b['name']); + }); + + $sources = array_map( + function (array $asset): array { + $asset['size'] = $this->getFileSize($asset['url']); + return $asset; + }, + $release['assets']['sources'] ?? [] + ); + + $changelog = sprintf( + 'https://gitlab.studip.de/studip/studip/-/blob/%s/ChangeLog', + $release['tag_name'] + ); + + return [ + 'name' => $release['name'], + 'description' => $release['description'], + 'url' => $release['_links']['self'], + 'changelog' => $changelog, + 'released' => strtotime($release['released_at']), + 'sources' => $release['assets']['sources'], + 'links' => $links + ]; + }, + $this->gitlab->repositories()->releases($this->gitlabProjectId) + ); + + $releases = []; + foreach ($temp as $release) { + $version = $release['name']; + if (preg_match('/^v?(\d+(\.\d+)+)$/', $release['name'], $match)) { + $version = $match[1]; + } + + $major_version = implode('.', array_slice(explode('.', $version), 0, 2)); + + if (!isset($releases[$major_version])) { + $releases[$major_version] = []; + } + + $releases[$major_version][$version] = $release; + } + + uksort($releases, function ($a, $b) { + return version_compare($b, $a); + }); + foreach ($releases as $version => $subversions) { + uksort($subversions, function ($a, $b) { + return version_compare($b, $a); + }); + + $releases[$version] = $subversions; + } + + $cache->write(self::CACHE_KEY, $releases); + } + + return $releases; + } + + private function getFileSize(string $url): int + { + // Init curl if necessary + if (self::$curl_handle === null) { + self::$curl_handle = curl_init(null); + + curl_setopt(self::$curl_handle, CURLOPT_HEADER, true); + curl_setopt(self::$curl_handle, CURLOPT_NOBODY, true); + curl_setopt(self::$curl_handle, CURLOPT_RETURNTRANSFER, true); + curl_setopt(self::$curl_handle, CURLOPT_FOLLOWLOCATION, true); + } + + // Set url and send request + curl_setopt(self::$curl_handle, CURLOPT_URL, $url); + $response = curl_exec(self::$curl_handle); + + // Get status + $line = strtok($response, "\r\n"); + $status = trim($line); + + // Parse headers + $headers = []; + while (($line = strtok("\r\n")) !== false) { + $chunks = explode(':', $line, 2); + if ($chunks !== false) { + $headers[$chunks[0]] = $chunks[1]; + } + } + + // TODO: Check status + + // Get filesize by Content-Length header + return (int) ($headers['Content-Length'] ?? -1); + } + + private function addVersionFilter(array $releases, ?string $version): void + { + if (count($releases) > 1) { + $widget = new SelectWidget( + _('Nur Releases einer Version anzeigen'), + $this->indexURL(), + 'version' + ); + $widget->addElement(new SelectElement( + null, + _('Alle Versionen anzeigen'), + !isset($releases[$version]) + )); + foreach (array_keys($releases) as $v) { + $widget->addElement(new SelectElement( + $v, + $v, + $v === $version + )); + } + Sidebar::get()->addWidget($widget); + } + + } + + private function addResetAction(): void + { + if (!$this->isAdmin()) { + return; + } + + Sidebar::get()->addWidget(new ActionsWidget())->addLink( + _('Cache leeren'), + $this->resetURL(), + Icon::create('refresh') + ); + } + + private function isAdmin(): bool + { + $user = User::findCurrent(); + return $user && $user->perms === 'root'; + } +} diff --git a/lib/Plugin.php b/lib/Plugin.php new file mode 100644 index 0000000000000000000000000000000000000000..5bcdda90b77283d9d0c0125d3cdb01dca9f554de --- /dev/null +++ b/lib/Plugin.php @@ -0,0 +1,55 @@ +<?php +namespace TracToGitlab; + +use Gitlab; +use GuzzleHttp; +use Http; +use Parsedown; +use Pimple; + +abstract class Plugin extends \StudIPPlugin +{ + private $container = null; + + public function getDIContainer() + { + if ($this->container === null) { + require_once $this->getPluginPath() . '/vendor/autoload.php'; + + $this->container = new Pimple\Container(); + + $this->container['trac'] = function () { + return new TracLookup(Config::get()->TRAC2GITLAB_TRAC_URL); + }; + + $this->container['gitlab'] = function () { + $builder = new Gitlab\HttpClient\Builder( + new GuzzleHttp\Client(), + new Http\Factory\Guzzle\RequestFactory(), + new Http\Factory\Guzzle\StreamFactory(), + new Http\Factory\Guzzle\UriFactory() + ); + + $client = new Gitlab\Client($builder); + $client->setUrl(\Config::get()->TRAC2GITLAB_GITLAB_URL); + $client->authenticate(\Config::get()->TRAC2GITLAB_GITLAB_TOKEN, Gitlab\Client::AUTH_HTTP_TOKEN); + return $client; + }; + + $this->container['gitlabProjectId'] = function () { + return \Config::get()->TRAC2GITLAB_GITLAB_PROJECT_ID; + }; + + $this->container['gitlabPager'] = function ($c) { + return new Gitlab\ResultPager($c['gitlab']); + }; + + $this->container['parsedown'] = function () { + $parsedown = new Parsedown(); + $parsedown->setSafeMode(true); + return $parsedown; + }; + } + return $this->container; + } +} diff --git a/plugin.manifest b/plugin.manifest index 559e946ee7c16a443da80fdbb60af85a07642388..8f3baa364c295003f33d83e1194c538830add421 100644 --- a/plugin.manifest +++ b/plugin.manifest @@ -1,5 +1,6 @@ pluginname=Trac to gitlab converter pluginclassname=TracToGitlabPlugin +pluginclassname=StudipReleasesPlugin origin=UOL -version=1.4 +version=1.4.1 studipMinVersion=5.0 diff --git a/views/releases/index.php b/views/releases/index.php new file mode 100644 index 0000000000000000000000000000000000000000..d060e2ae039b9505e7cbecb9f2136f13e1e6ec84 --- /dev/null +++ b/views/releases/index.php @@ -0,0 +1,54 @@ +<?php +/** + * @var array<string, array> $releases + */ +?> +<table class="default"> + <colgroup> + <col> + <col class="hidden-tiny-down"> + <col> + <col> + <col> + </colgroup> +<? foreach ($releases as $major_version => $versions): ?> + <tbody> + <tr> + <th colspan="5"><?= htmlReady($major_version) ?></th> + </tr> + <? foreach ($versions as $version => $release): ?> + <tr> + <td> + <a href="<?= URLHelper::getLink($release['url']) ?>" target="_blank" rel="noreferrer noopener"> + <?= htmlReady($release['name']) ?> + </a> + </td> + <td class="hidden-tiny-down"><?= strftime('%x', $release['released']) ?></td> + <? foreach ($release['links'] as $link): ?> + <td> + <a href="<?= URLHelper::getLink($link['url']) ?>" target="_blank" rel="noreferrer noopener"> + <?= htmlReady($link['name']) ?> + </a> + + <span class="release-filesize hidden-tiny-down"> + (<?= relsize($link['size'], false) ?>) + </span> + </td> + <? endforeach; ?> + <td class="hidden-small-down"> + <a href="<?= URLHelper::getLink($release['changelog']) ?>" target="_blank" rel="noopener noreferrer"> + ChangeLog + </a> + </td> + </tr> + <tr class="hidden-medium-up"> + <td colspan="5" class="table-divider"> + <a href="<?= URLHelper::getLink($release['changelog']) ?>" target="_blank" rel="noopener noreferrer"> + ChangeLog + </a> + </td> + </tr> + <? endforeach; ?> + </tbody> +<? endforeach; ?> +</table>