Просмотр исходного кода

Merge remote-tracking branch 'tflori/feature-readme'

Jordi Boggiano 8 лет назад
Родитель
Сommit
95db2d880e

+ 3 - 2
composer.json

@@ -39,7 +39,7 @@
         "jms/security-extra-bundle": "^1.5",
         "jms/di-extra-bundle": "^1.4",
 
-        "composer/composer": "^1.0@dev",
+        "composer/composer": "^1.3",
         "friendsofsymfony/user-bundle": "^2.0@dev",
         "hwi/oauth-bundle": "^0.4",
         "nelmio/solarium-bundle": "^1.0",
@@ -54,7 +54,8 @@
         "pagerfanta/pagerfanta": "^1.0",
         "knplabs/knp-menu-bundle": "^2.1",
         "ezyang/htmlpurifier": "^4.6",
-        "nelmio/cors-bundle": "^1.4"
+        "nelmio/cors-bundle": "^1.4",
+        "cebe/markdown": "^1.1"
     },
     "_comment": ["fos user bundle 2.0.0 tag needed"],
     "require-dev": {

+ 66 - 7
composer.lock

@@ -4,8 +4,68 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "ca089bd944842e20cdbdf556dad7c4f0",
+    "content-hash": "adfc05475108333f135978a48d699f6c",
     "packages": [
+        {
+            "name": "cebe/markdown",
+            "version": "1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/cebe/markdown.git",
+                "reference": "c30eb5e01fe021cc5bba2f9ee0eeef96d4931166"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/cebe/markdown/zipball/c30eb5e01fe021cc5bba2f9ee0eeef96d4931166",
+                "reference": "c30eb5e01fe021cc5bba2f9ee0eeef96d4931166",
+                "shasum": ""
+            },
+            "require": {
+                "lib-pcre": "*",
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "cebe/indent": "*",
+                "facebook/xhprof": "*@dev",
+                "phpunit/phpunit": "4.1.*"
+            },
+            "bin": [
+                "bin/markdown"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "cebe\\markdown\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Carsten Brandt",
+                    "email": "mail@cebe.cc",
+                    "homepage": "http://cebe.cc/",
+                    "role": "Creator"
+                }
+            ],
+            "description": "A super fast, highly extensible markdown parser for PHP",
+            "homepage": "https://github.com/cebe/markdown#readme",
+            "keywords": [
+                "extensible",
+                "fast",
+                "gfm",
+                "markdown",
+                "markdown-extra"
+            ],
+            "time": "2016-09-14T20:40:20+00:00"
+        },
         {
             "name": "composer/ca-bundle",
             "version": "1.0.6",
@@ -66,16 +126,16 @@
         },
         {
             "name": "composer/composer",
-            "version": "dev-master",
+            "version": "1.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/composer.git",
-                "reference": "77ad36c067266337344b2e738f94f2240e341c87"
+                "reference": "e53f9e5381e70f76e098136343e27d92601eade7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/composer/zipball/77ad36c067266337344b2e738f94f2240e341c87",
-                "reference": "77ad36c067266337344b2e738f94f2240e341c87",
+                "url": "https://api.github.com/repos/composer/composer/zipball/e53f9e5381e70f76e098136343e27d92601eade7",
+                "reference": "e53f9e5381e70f76e098136343e27d92601eade7",
                 "shasum": ""
             },
             "require": {
@@ -139,7 +199,7 @@
                 "dependency",
                 "package"
             ],
-            "time": "2016-12-29 17:12:21"
+            "time": "2016-12-23T23:47:04+00:00"
         },
         {
             "name": "composer/semver",
@@ -5586,7 +5646,6 @@
     "aliases": [],
     "minimum-stability": "stable",
     "stability-flags": {
-        "composer/composer": 20,
         "friendsofsymfony/user-bundle": 20
     },
     "prefer-stable": false,

+ 147 - 69
src/Packagist/WebBundle/Package/Updater.php

@@ -12,6 +12,7 @@
 
 namespace Packagist\WebBundle\Package;
 
+use cebe\markdown\GithubMarkdown;
 use Composer\Package\AliasPackage;
 use Composer\Package\PackageInterface;
 use Composer\Repository\RepositoryInterface;
@@ -214,6 +215,8 @@ class Updater
 
         if (preg_match('{^(?:git://|git@|https?://)github.com[:/]([^/]+)/(.+?)(?:\.git|/)?$}i', $package->getRepository(), $match) && $repository instanceof VcsRepository) {
             $this->updateGitHubInfo($rfs, $package, $match[1], $match[2], $repository);
+        } else {
+            $this->updateReadme($io, $package, $repository);
         }
 
         $package->setUpdatedAt(new \DateTime);
@@ -456,6 +459,56 @@ class Updater
         return true;
     }
 
+    /**
+     * Update the readme for $package from $repository.
+     *
+     * @param IOInterface $io
+     * @param Package $package
+     * @param VcsRepository $repository
+     */
+    private function updateReadme(IOInterface $io, Package $package, VcsRepository $repository)
+    {
+        try {
+            $driver = $repository->getDriver();
+            $composerInfo = $driver->getComposerInformation($driver->getRootIdentifier());
+
+            if (isset($composerInfo['readme'])) {
+                $readmeFile = $composerInfo['readme'];
+                $ext = substr($readmeFile, strrpos($readmeFile, '.'));
+
+                if ($ext === $readmeFile) {
+                    $ext = '.txt';
+                }
+
+                switch ($ext) {
+                    case '.txt':
+                        $source = $driver->getFileContent($readmeFile, $driver->getRootIdentifier());
+                        $package->setReadme('<pre>' . htmlspecialchars($source) . '</pre>');
+                        break;
+
+                    case '.md':
+                        $source = $driver->getFileContent($readmeFile, $driver->getRootIdentifier());
+                        $parser = new GithubMarkdown();
+                        $readme = $parser->parse($source);
+
+                        if (!empty($readme)) {
+                            $package->setReadme($this->prepareReadme($readme));
+                        }
+                        break;
+                }
+
+            }
+
+        } catch (\Exception $e) {
+            // we ignore all errors for this minor function
+            $io->write(
+                'Can not update readme. Error: ' . $e->getMessage(),
+                true,
+                IOInterface::VERBOSE
+            );
+        }
+    }
+
     private function updateGitHubInfo(RemoteFilesystem $rfs, Package $package, $owner, $repo, VcsRepository $repository)
     {
         $baseApiUrl = 'https://api.github.com/repos/'.$owner.'/'.$repo;
@@ -478,75 +531,7 @@ class Updater
         }
 
         if (!empty($readme)) {
-            $elements = array(
-                'p',
-                'br',
-                'small',
-                'strong', 'b',
-                'em', 'i',
-                'strike',
-                'sub', 'sup',
-                'ins', 'del',
-                'ol', 'ul', 'li',
-                'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
-                'dl', 'dd', 'dt',
-                'pre', 'code', 'samp', 'kbd',
-                'q', 'blockquote', 'abbr', 'cite',
-                'table', 'thead', 'tbody', 'th', 'tr', 'td',
-                'a', 'span',
-                'img',
-            );
-            $config = \HTMLPurifier_Config::createDefault();
-            $config->set('HTML.AllowedElements', implode(',', $elements));
-            $config->set('HTML.AllowedAttributes',
-                'img.src,img.title,img.alt,img.width,img.height,img.style,'.
-                'a.href,a.target,a.rel,a.id,'.
-                'td.colspan,td.rowspan,th.colspan,th.rowspan,'.
-                '*.class'
-            );
-            $config->set('Attr.EnableID', true);
-            $config->set('Attr.AllowedFrameTargets', ['_blank']);
-            $purifier = new \HTMLPurifier($config);
-            $readme = $purifier->purify($readme);
-
-            $dom = new \DOMDocument();
-            $dom->loadHTML('<?xml encoding="UTF-8">' . $readme);
-
-            // Links can not be trusted, mark them nofollow and convert relative to absolute links
-            $links = $dom->getElementsByTagName('a');
-            foreach ($links as $link) {
-                $link->setAttribute('rel', 'nofollow noopener external');
-                if ('#' === substr($link->getAttribute('href'), 0, 1)) {
-                    $link->setAttribute('href', '#user-content-'.substr($link->getAttribute('href'), 1));
-                } elseif ('mailto:' === substr($link->getAttribute('href'), 0, 7)) {
-                    // do nothing
-                } elseif (false === strpos($link->getAttribute('href'), '//')) {
-                    $link->setAttribute('href', 'https://github.com/'.$owner.'/'.$repo.'/blob/HEAD/'.$link->getAttribute('href'));
-                }
-            }
-
-            // convert relative to absolute images
-            $images = $dom->getElementsByTagName('img');
-            foreach ($images as $img) {
-                if (false === strpos($img->getAttribute('src'), '//')) {
-                    $img->setAttribute('src', 'https://raw.github.com/'.$owner.'/'.$repo.'/HEAD/'.$img->getAttribute('src'));
-                }
-            }
-
-            // remove first title as it's usually the project name which we don't need
-            if ($dom->getElementsByTagName('h1')->length) {
-                $first = $dom->getElementsByTagName('h1')->item(0);
-                $first->parentNode->removeChild($first);
-            } elseif ($dom->getElementsByTagName('h2')->length) {
-                $first = $dom->getElementsByTagName('h2')->item(0);
-                $first->parentNode->removeChild($first);
-            }
-
-            $readme = $dom->saveHTML();
-            $readme = substr($readme, strpos($readme, '<body>')+6);
-            $readme = substr($readme, 0, strrpos($readme, '</body>'));
-
-            $package->setReadme($readme);
+            $package->setReadme($this->prepareReadme($readme, true, $owner, $repo));
         }
 
         if (!empty($repoData['language'])) {
@@ -566,6 +551,99 @@ class Updater
         }
     }
 
+    /**
+     * Prepare the readme by stripping elements and attributes that are not supported .
+     *
+     * @param string $readme
+     * @param bool $isGithub
+     * @param null $owner
+     * @param null $repo
+     * @return string
+     */
+    private function prepareReadme($readme, $isGithub = false, $owner = null, $repo = null)
+    {
+        $elements = array(
+            'p',
+            'br',
+            'small',
+            'strong', 'b',
+            'em', 'i',
+            'strike',
+            'sub', 'sup',
+            'ins', 'del',
+            'ol', 'ul', 'li',
+            'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+            'dl', 'dd', 'dt',
+            'pre', 'code', 'samp', 'kbd',
+            'q', 'blockquote', 'abbr', 'cite',
+            'table', 'thead', 'tbody', 'th', 'tr', 'td',
+            'a', 'span',
+            'img',
+        );
+
+        $attributes = array(
+            'img.src', 'img.title', 'img.alt', 'img.width', 'img.height', 'img.style',
+            'a.href', 'a.target', 'a.rel', 'a.id',
+            'td.colspan', 'td.rowspan', 'th.colspan', 'th.rowspan',
+            '*.class'
+        );
+
+        $config = \HTMLPurifier_Config::createDefault();
+        $config->set('HTML.AllowedElements', implode(',', $elements));
+        $config->set('HTML.AllowedAttributes', implode(',', $attributes));
+        $config->set('Attr.EnableID', true);
+        $config->set('Attr.AllowedFrameTargets', ['_blank']);
+        $purifier = new \HTMLPurifier($config);
+        $readme = $purifier->purify($readme);
+
+        $dom = new \DOMDocument();
+        $dom->loadHTML('<?xml encoding="UTF-8">' . $readme);
+
+        // Links can not be trusted, mark them nofollow and convert relative to absolute links
+        $links = $dom->getElementsByTagName('a');
+        foreach ($links as $link) {
+            $link->setAttribute('rel', 'nofollow noopener external');
+            if ('#' === substr($link->getAttribute('href'), 0, 1)) {
+                $link->setAttribute('href', '#user-content-'.substr($link->getAttribute('href'), 1));
+            } elseif ('mailto:' === substr($link->getAttribute('href'), 0, 7)) {
+                // do nothing
+            } elseif ($isGithub && false === strpos($link->getAttribute('href'), '//')) {
+                $link->setAttribute(
+                    'href',
+                    'https://github.com/'.$owner.'/'.$repo.'/blob/HEAD/'.$link->getAttribute('href')
+                );
+            }
+        }
+
+        if ($isGithub) {
+            // convert relative to absolute images
+            $images = $dom->getElementsByTagName('img');
+            foreach ($images as $img) {
+                if (false === strpos($img->getAttribute('src'), '//')) {
+                    $img->setAttribute(
+                        'src',
+                        'https://raw.github.com/'.$owner.'/'.$repo.'/HEAD/'.$img->getAttribute('src')
+                    );
+                }
+            }
+        }
+
+        // remove first title as it's usually the project name which we don't need
+        if ($dom->getElementsByTagName('h1')->length) {
+            $first = $dom->getElementsByTagName('h1')->item(0);
+            $first->parentNode->removeChild($first);
+        } elseif ($dom->getElementsByTagName('h2')->length) {
+            $first = $dom->getElementsByTagName('h2')->item(0);
+            $first->parentNode->removeChild($first);
+        }
+
+        $readme = $dom->saveHTML();
+        $readme = substr($readme, strpos($readme, '<body>')+6);
+        $readme = substr($readme, 0, strrpos($readme, '</body>'));
+
+        return $readme;
+    }
+
     private function sanitize($str)
     {
         // remove escape chars

+ 132 - 0
src/Packagist/WebBundle/Tests/Package/UpdaterTest.php

@@ -0,0 +1,132 @@
+<?php
+
+namespace Packagist\WebBundle\Tests\Package;
+
+use Composer\Config;
+use Composer\IO\IOInterface;
+use Composer\IO\NullIO;
+use Composer\Package\CompletePackage;
+use Composer\Repository\RepositoryInterface;
+use Composer\Repository\Vcs\GitDriver;
+use Composer\Repository\Vcs\VcsDriverInterface;
+use Composer\Repository\VcsRepository;
+use Doctrine\Bundle\DoctrineBundle\Registry;
+use Doctrine\ORM\EntityManager;
+use Packagist\WebBundle\Entity\Package;
+use Packagist\WebBundle\Package\Updater;
+use PHPUnit\Framework\TestCase;
+use PHPUnit_Framework_MockObject_MockObject;
+
+class UpdaterTest extends TestCase
+{
+    /** @var IOInterface */
+    private $ioMock;
+    /** @var Config */
+    private $config;
+    /** @var Package */
+    private $package;
+    /** @var Updater */
+    private $updater;
+    /** @var RepositoryInterface|PHPUnit_Framework_MockObject_MockObject */
+    private $repositoryMock;
+    /** @var VcsDriverInterface|PHPUnit_Framework_MockObject_MockObject */
+    private $driverMock;
+
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $this->config  = new Config();
+        $this->package = new Package();
+
+        $this->ioMock         = $this->getMockBuilder(NullIO::class)->disableOriginalConstructor()->getMock();
+        $this->repositoryMock = $this->getMockBuilder(VcsRepository::class)->disableOriginalConstructor()->getMock();
+        $registryMock         = $this->getMockBuilder(Registry::class)->disableOriginalConstructor()->getMock();
+        $emMock               = $this->getMockBuilder(EntityManager::class)->disableOriginalConstructor()->getMock();
+        $packageMock          = $this->getMockBuilder(CompletePackage::class)->disableOriginalConstructor()->getMock();
+        $this->driverMock     = $this->getMockBuilder(GitDriver::class)->disableOriginalConstructor()->getMock();
+
+        $registryMock->expects($this->any())->method('getManager')->willReturn($emMock);
+        $this->repositoryMock->expects($this->any())->method('getPackages')->willReturn([
+            $packageMock
+        ]);
+        $this->repositoryMock->expects($this->any())->method('getDriver')->willReturn($this->driverMock);
+        $packageMock->expects($this->any())->method('getRequires')->willReturn([]);
+        $packageMock->expects($this->any())->method('getConflicts')->willReturn([]);
+        $packageMock->expects($this->any())->method('getProvides')->willReturn([]);
+        $packageMock->expects($this->any())->method('getReplaces')->willReturn([]);
+        $packageMock->expects($this->any())->method('getDevRequires')->willReturn([]);
+
+        $this->updater = new Updater($registryMock);
+    }
+
+    public function testUpdatesTheReadme()
+    {
+        $this->driverMock->expects($this->any())->method('getRootIdentifier')->willReturn('master');
+        $this->driverMock->expects($this->any())->method('getComposerInformation')
+                         ->willReturn(['readme' => 'README.md']);
+        $this->driverMock->expects($this->once())->method('getFileContent')->with('README.md', 'master')
+                         ->willReturn('This is the readme');
+
+        $this->updater->update($this->ioMock, $this->config, $this->package, $this->repositoryMock);
+
+        self::assertContains('This is the readme', $this->package->getReadme());
+    }
+
+    public function testConvertsMarkdownForReadme()
+    {
+        $readme = <<<EOR
+# some package name
+
+Why you should use this package:
+ - it is easy to use
+ - no overhead
+ - minimal requirements
+
+EOR;
+        $readmeHtml = <<<EOR
+
+<p>Why you should use this package:</p>
+<ul><li>it is easy to use</li>
+<li>no overhead</li>
+<li>minimal requirements</li>
+</ul>
+EOR;
+
+        $this->driverMock->expects($this->any())->method('getRootIdentifier')->willReturn('master');
+        $this->driverMock->expects($this->any())->method('getComposerInformation')
+                         ->willReturn(['readme' => 'README.md']);
+        $this->driverMock->expects($this->once())->method('getFileContent')->with('README.md', 'master')
+                         ->willReturn($readme);
+
+        $this->updater->update($this->ioMock, $this->config, $this->package, $this->repositoryMock);
+
+        self::assertSame($readmeHtml, $this->package->getReadme());
+    }
+
+    public function testSurrondsTextReadme()
+    {
+        $this->driverMock->expects($this->any())->method('getRootIdentifier')->willReturn('master');
+        $this->driverMock->expects($this->any())->method('getComposerInformation')
+                         ->willReturn(['readme' => 'README.txt']);
+        $this->driverMock->expects($this->once())->method('getFileContent')->with('README.txt', 'master')
+                         ->willReturn('This is the readme');
+
+        $this->updater->update($this->ioMock, $this->config, $this->package, $this->repositoryMock);
+
+        self::assertSame('<pre>This is the readme</pre>', $this->package->getReadme());
+    }
+
+    public function testUnderstandsDifferentFileNames()
+    {
+        $this->driverMock->expects($this->any())->method('getRootIdentifier')->willReturn('master');
+        $this->driverMock->expects($this->any())->method('getComposerInformation')
+                         ->willReturn(['readme' => 'liesmich']);
+        $this->driverMock->expects($this->once())->method('getFileContent')->with('liesmich', 'master')
+                         ->willReturn('This is the readme');
+
+        $this->updater->update($this->ioMock, $this->config, $this->package, $this->repositoryMock);
+
+        self::assertSame('<pre>This is the readme</pre>', $this->package->getReadme());
+    }
+}