Parcourir la source

Merge branch 'master' into t/security-advisories

Jordi Boggiano il y a 5 ans
Parent
commit
395b3d657f

+ 3 - 0
.github/FUNDING.yml

@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+custom: https://packagist.com

+ 7 - 0
app/Resources/FOSUserBundle/views/Profile/show.html.twig

@@ -45,5 +45,12 @@
             <h3 class="font-normal">{{ 'packages.yours'|trans }}</h3>
         {% endblock %}
     {% endembed %}
+
+    {%- if deleteForm is defined %}
+        <form class="delete action" action="{{ path('user_delete', {name: user.username}) }}" method="POST">
+            {{ form_widget(deleteForm._token) }}
+            <input class="btn btn-danger" type="submit" value="Delete Account Permanently" onclick="return confirm('Are you sure? There is no way back..');" />
+        </form>
+    {%- endif %}
 </section>
 {% endblock %}

+ 62 - 22
composer.lock

@@ -3720,53 +3720,75 @@
         },
         {
             "name": "scheb/two-factor-bundle",
-            "version": "v4.7.1",
+            "version": "v4.11.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/scheb/two-factor-bundle.git",
-                "reference": "170895e91bdbe2c21983f195271d42e2fcfb3d62"
+                "reference": "eadac02014233ab45dac215d42fd06aaf629b09a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/scheb/two-factor-bundle/zipball/170895e91bdbe2c21983f195271d42e2fcfb3d62",
-                "reference": "170895e91bdbe2c21983f195271d42e2fcfb3d62",
+                "url": "https://api.github.com/repos/scheb/two-factor-bundle/zipball/eadac02014233ab45dac215d42fd06aaf629b09a",
+                "reference": "eadac02014233ab45dac215d42fd06aaf629b09a",
                 "shasum": ""
             },
             "require": {
                 "lcobucci/jwt": "^3.2",
                 "paragonie/constant_time_encoding": "^2.2",
                 "php": "^7.1.3",
-                "spomky-labs/otphp": "^9.1",
-                "symfony/config": "^3.4|^4.0",
-                "symfony/dependency-injection": "^3.4|^4.0",
-                "symfony/event-dispatcher": "^3.4|^4.0",
-                "symfony/framework-bundle": "^3.4|^4.0",
-                "symfony/http-foundation": "^3.4|^4.0",
-                "symfony/http-kernel": "^3.4|^4.0",
-                "symfony/property-access": "^3.4|^4.0",
-                "symfony/security-bundle": "^3.4|^4.0",
-                "symfony/twig-bundle": "^3.4|^4.0"
+                "spomky-labs/otphp": "^9.1|^10.0",
+                "symfony/config": "^3.4|^4.0|^5.0",
+                "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+                "symfony/event-dispatcher": "^3.4|^4.0|^5.0",
+                "symfony/framework-bundle": "^3.4|^4.0|^5.0",
+                "symfony/http-foundation": "^3.4|^4.0|^5.0",
+                "symfony/http-kernel": "^3.4|^4.0|^5.0",
+                "symfony/property-access": "^3.4|^4.0|^5.0",
+                "symfony/security-bundle": "^3.4|^4.0|^5.0",
+                "symfony/twig-bundle": "^3.4|^4.0|^5.0"
             },
             "require-dev": {
                 "doctrine/lexer": "^1.0.1",
                 "doctrine/orm": "^2.6",
+                "escapestudios/symfony2-coding-standard": "^3.9",
                 "phpunit/phpunit": "^7.0|^8.0",
+                "squizlabs/php_codesniffer": "^3.5",
                 "swiftmailer/swiftmailer": "^6.0",
-                "symfony/yaml": "^3.4|^4.0"
+                "symfony/yaml": "^3.4|^4.0|^5.0"
             },
             "type": "symfony-bundle",
             "autoload": {
                 "psr-4": {
                     "Scheb\\TwoFactorBundle\\": ""
-                }
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "MIT"
             ],
+            "authors": [
+                {
+                    "name": "Christian Scheb",
+                    "email": "me@christianscheb.de"
+                }
+            ],
             "description": "Provides two-factor authentication for Symfony applications",
             "homepage": "https://github.com/scheb/two-factor-bundle",
-            "time": "2019-09-02T18:36:37+00:00"
+            "keywords": [
+                "Authentication",
+                "security",
+                "symfony",
+                "two-factor",
+                "two-step"
+            ],
+            "support": {
+                "issues": "https://github.com/scheb/two-factor-bundle/issues",
+                "source": "https://github.com/scheb/two-factor-bundle/tree/master"
+            },
+            "time": "2019-12-08T16:03:05+00:00"
         },
         {
             "name": "seld/jsonlint",
@@ -4960,16 +4982,16 @@
         },
         {
             "name": "symfony/symfony",
-            "version": "v3.4.32",
+            "version": "v3.4.36",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/symfony.git",
-                "reference": "2815d1fa34d417b8b87450667f166edbefff3177"
+                "reference": "0a6fccb5772ad2467253e6849d589bd09d9eb195"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/symfony/zipball/2815d1fa34d417b8b87450667f166edbefff3177",
-                "reference": "2815d1fa34d417b8b87450667f166edbefff3177",
+                "url": "https://api.github.com/repos/symfony/symfony/zipball/0a6fccb5772ad2467253e6849d589bd09d9eb195",
+                "reference": "0a6fccb5772ad2467253e6849d589bd09d9eb195",
                 "shasum": ""
             },
             "require": {
@@ -4991,6 +5013,7 @@
                 "twig/twig": "^1.41|^2.10"
             },
             "conflict": {
+                "monolog/monolog": ">=2",
                 "phpdocumentor/reflection-docblock": "<3.0||>=3.2.0,<3.2.2",
                 "phpdocumentor/type-resolver": "<0.3.0",
                 "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
@@ -5096,9 +5119,26 @@
             "license": [
                 "MIT"
             ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
             "description": "The Symfony PHP framework",
             "homepage": "https://symfony.com",
-            "time": "2019-10-07T14:42:16+00:00"
+            "keywords": [
+                "framework"
+            ],
+            "support": {
+                "issues": "https://github.com/symfony/symfony/issues",
+                "source": "https://github.com/symfony/symfony/tree/v3.4.36"
+            },
+            "time": "2019-12-01T13:50:53+00:00"
         },
         {
             "name": "twig/extensions",

+ 14 - 3
src/Packagist/WebBundle/Controller/PackageController.php

@@ -322,10 +322,10 @@ class PackageController extends Controller
         /** @var PackageRepository $repo */
         $repo = $this->getDoctrine()->getRepository(Package::class);
         $count = $repo->getSuspectPackageCount();
-        $packages = $repo->getSuspectPackages(($page - 1) * 15, 15);
+        $packages = $repo->getSuspectPackages(($page - 1) * 50, 50);
 
         $paginator = new Pagerfanta(new FixedAdapter($count, $packages));
-        $paginator->setMaxPerPage(15);
+        $paginator->setMaxPerPage(50);
         $paginator->setCurrentPage($page, false, true);
 
         $data['packages'] = $paginator;
@@ -677,7 +677,14 @@ class PackageController extends Controller
             return new JsonResponse(['status' => 'error', 'message' => 'Invalid credentials'], 403);
         }
 
-        if ($package->getMaintainers()->contains($user) || $this->isGranted('ROLE_UPDATE_PACKAGES')) {
+        $canUpdatePackage = $package->getMaintainers()->contains($user) || $this->isGranted('ROLE_UPDATE_PACKAGES');
+        if ($canUpdatePackage || !$package->wasUpdatedInTheLast24Hours()) {
+            // do not let non-maintainers execute update with those flags
+            if (!$canUpdatePackage) {
+                $autoUpdated = null;
+                $updateEqualRefs = false;
+            }
+
             if (null !== $autoUpdated) {
                 $package->setAutoUpdated($autoUpdated ? Package::AUTO_MANUAL_HOOK : 0);
                 $doctrine->getManager()->flush();
@@ -692,6 +699,10 @@ class PackageController extends Controller
             return new JsonResponse(['status' => 'success'], 202);
         }
 
+        if (!$canUpdatePackage && $package->wasUpdatedInTheLast24Hours()) {
+            return new JsonResponse(['status' => 'error', 'message' => 'Package was already updated in the last 24 hours',], 404);
+        }
+
         return new JsonResponse(array('status' => 'error', 'message' => 'Could not find a package that matches this request (does user maintain the package?)',), 404);
     }
 

+ 51 - 12
src/Packagist/WebBundle/Controller/UserController.php

@@ -152,14 +152,20 @@ class UserController extends Controller
         $packages = $this->getUserPackages($req, $user);
         $lastGithubSync = $this->getDoctrine()->getRepository(Job::class)->getLastGitHubSyncJob($user->getId());
 
+        $data = array(
+            'packages' => $packages,
+            'meta' => $this->getPackagesMetadata($packages),
+            'user' => $user,
+            'githubSync' => $lastGithubSync,
+        );
+
+        if (!count($packages)) {
+            $data['deleteForm'] = $this->createFormBuilder(array())->getForm()->createView();
+        }
+
         return $this->container->get('templating')->renderResponse(
             'FOSUserBundle:Profile:show.html.twig',
-            array(
-                'packages' => $packages,
-                'meta' => $this->getPackagesMetadata($packages),
-                'user' => $user,
-                'githubSync' => $lastGithubSync,
-            )
+            $data
         );
     }
 
@@ -181,6 +187,9 @@ class UserController extends Controller
         if ($this->isGranted('ROLE_ANTISPAM')) {
             $data['spammerForm'] = $this->createFormBuilder(array())->getForm()->createView();
         }
+        if (!count($packages) && ($this->isGranted('ROLE_ADMIN') || ($this->getUser() && $this->getUser()->getId() === $user->getId()))) {
+            $data['deleteForm'] = $this->createFormBuilder(array())->getForm()->createView();
+        }
 
         return $data;
     }
@@ -240,7 +249,7 @@ class UserController extends Controller
      */
     public function postFavoriteAction(Request $req, User $user)
     {
-        if ($user->getId() !== $this->getUser()->getId()) {
+        if (!$this->getUser() || $user->getId() !== $this->getUser()->getId()) {
             throw new AccessDeniedException('You can only change your own favorites');
         }
 
@@ -265,7 +274,7 @@ class UserController extends Controller
      */
     public function deleteFavoriteAction(User $user, Package $package)
     {
-        if ($user->getId() !== $this->getUser()->getId()) {
+        if (!$this->getUser() || $user->getId() !== $this->getUser()->getId()) {
             throw new AccessDeniedException('You can only change your own favorites');
         }
 
@@ -274,6 +283,36 @@ class UserController extends Controller
         return new Response('{"status": "success"}', 204);
     }
 
+    /**
+     * @Route("/users/{name}/delete", name="user_delete", defaults={"_format" = "json"}, methods={"POST"})
+     * @ParamConverter("user", options={"mapping": {"name": "username"}})
+     */
+    public function deleteUserAction(User $user, Request $req)
+    {
+        if (!($this->isGranted('ROLE_ADMIN') || ($this->getUser() && $user->getId() === $this->getUser()->getId()))) {
+            throw new AccessDeniedException('You cannot delete this user');
+        }
+
+        if (count($user->getPackages()) > 0) {
+            throw new AccessDeniedException('The user has packages so it can not be deleted');
+        }
+
+        $form = $this->createFormBuilder(array())->getForm();
+
+        $form->submit($req->request->get('form'));
+        if ($form->isValid()) {
+            $em = $this->getDoctrine()->getManager();
+            $em->remove($user);
+            $em->flush();
+
+            $this->container->get('security.token_storage')->setToken(null);
+
+            return $this->redirectToRoute('home');
+        }
+
+        return $this->redirectToRoute('user_profile', ['name' => $user->getName()]);
+    }
+
     /**
      * @Template()
      * @Route("/users/{name}/2fa/", name="user_2fa_configure", methods={"GET"})
@@ -281,7 +320,7 @@ class UserController extends Controller
      */
     public function configureTwoFactorAuthAction(User $user)
     {
-        if (!($this->isGranted('ROLE_DISABLE_2FA') || $user->getId() === $this->getUser()->getId())) {
+        if (!($this->isGranted('ROLE_DISABLE_2FA') || ($this->getUser() && $user->getId() === $this->getUser()->getId()))) {
             throw new AccessDeniedException('You cannot change this user\'s two-factor authentication settings');
         }
 
@@ -299,7 +338,7 @@ class UserController extends Controller
      */
     public function enableTwoFactorAuthAction(Request $req, User $user)
     {
-        if ($user->getId() !== $this->getUser()->getId()) {
+        if (!$this->getUser() || $user->getId() !== $this->getUser()->getId()) {
             throw new AccessDeniedException('You cannot change this user\'s two-factor authentication settings');
         }
 
@@ -342,7 +381,7 @@ class UserController extends Controller
      */
     public function confirmTwoFactorAuthAction(User $user)
     {
-        if ($user->getId() !== $this->getUser()->getId()) {
+        if (!$this->getUser() || $user->getId() !== $this->getUser()->getId()) {
             throw new AccessDeniedException('You cannot change this user\'s two-factor authentication settings');
         }
 
@@ -362,7 +401,7 @@ class UserController extends Controller
      */
     public function disableTwoFactorAuthAction(Request $req, User $user)
     {
-        if (!($this->isGranted('ROLE_DISABLE_2FA') || $user->getId() === $this->getUser()->getId())) {
+        if (!($this->isGranted('ROLE_DISABLE_2FA') || ($this->getUser() && $user->getId() === $this->getUser()->getId()))) {
             throw new AccessDeniedException('You cannot change this user\'s two-factor authentication settings');
         }
 

+ 6 - 1
src/Packagist/WebBundle/Entity/Package.php

@@ -294,7 +294,7 @@ class Package
 
             if (
                 preg_match('{(free.*watch|watch.*free|(stream|online).*anschauver.*pelicula|ver.*completa|pelicula.*complet|season.*episode.*online|film.*(complet|entier)|(voir|regarder|guarda|assistir).*(film|complet)|full.*movie|online.*(free|tv|full.*hd)|(free|full|gratuit).*stream|movie.*free|free.*(movie|hack)|watch.*movie|watch.*full|generate.*resource|generate.*unlimited|hack.*coin|coin.*(hack|generat)|vbucks|hack.*cheat|hack.*generat|generat.*hack|hack.*unlimited|cheat.*(unlimited|generat)|(mod|cheat|apk).*(hack|cheat|mod)|hack.*(apk|mod|free|gold|gems|diamonds|coin)|putlocker|generat.*free|coins.*generat|(download|telecharg).*album|album.*(download|telecharg)|album.*(free|gratuit)|generat.*coins|unlimited.*coins|(fortnite|pubg|apex.*legend|t[1i]k.*t[o0]k).*(free|gratuit|generat|unlimited|coins|mobile|hack|follow))}i', str_replace(array('.', '-'), '', $information['name']))
-                && !preg_match('{^(hexmode|calgamo)/}', $information['name'])
+                && !preg_match('{^(hexmode|calgamo|liberty_code)/}', $information['name'])
             ) {
                 $context->buildViolation('The package name '.htmlentities($information['name'], ENT_COMPAT, 'utf-8').' is blocked, if you think this is a mistake please get in touch with us.')
                     ->atPath($property)
@@ -721,6 +721,11 @@ class Package
         return $this->updatedAt;
     }
 
+    public function wasUpdatedInTheLast24Hours(): bool
+    {
+        return $this->updatedAt > new \DateTime('-24 hours');
+    }
+
     /**
      * Set crawledAt
      *

+ 1 - 1
src/Packagist/WebBundle/Entity/PackageRepository.php

@@ -218,7 +218,7 @@ class PackageRepository extends ServiceEntityRepository
         // this helps for packages like https://packagist.org/packages/ccxt/ccxt
         // with huge amounts of versions
         $qb = $this->getEntityManager()->createQueryBuilder();
-        $qb->select('partial p.{id}', 'partial v.{id, version, normalizedVersion, development, releasedAt}', 'partial m.{id, username, email}')
+        $qb->select('partial p.{id}', 'partial v.{id, version, normalizedVersion, development, releasedAt, extra}', 'partial m.{id, username, email}')
             ->from('Packagist\WebBundle\Entity\Package', 'p')
             ->leftJoin('p.versions', 'v')
             ->leftJoin('p.maintainers', 'm')

+ 13 - 11
src/Packagist/WebBundle/Package/SymlinkDumper.php

@@ -306,7 +306,7 @@ class SymlinkDumper
 
                     // store affected files to clean up properly in the next update
                     $this->fs->mkdir(dirname($buildDir.'/'.$name));
-                    $this->writeFile($buildDir.'/'.$name.'.files', json_encode(array_keys($affectedFiles)));
+                    $this->writeFileNonAtomic($buildDir.'/'.$name.'.files', json_encode(array_keys($affectedFiles)));
 
                     $dumpTimeUpdates[$dumpTime->format('Y-m-d H:i:s')][] = $package->getId();
                 }
@@ -613,14 +613,6 @@ class SymlinkDumper
             ksort($this->rootFile['packages'][$package]);
         }
 
-        if (file_exists($file)) {
-            $timedFile = $file.'-'.time();
-            rename($file, $timedFile);
-            if (file_exists($file.'.gz')) {
-                rename($file.'.gz', $timedFile.'.gz');
-            }
-        }
-
         $json = json_encode($this->rootFile, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
         $time = time();
 
@@ -860,16 +852,26 @@ class SymlinkDumper
 
     private function writeFile($path, $contents, $mtime = null)
     {
-        file_put_contents($path, $contents);
+        file_put_contents($path.'.tmp', $contents);
         if ($mtime !== null) {
-            touch($path, $mtime);
+            touch($path.'.tmp', $mtime);
         }
+        rename($path.'.tmp', $path);
 
         if (is_array($this->writeLog)) {
             $this->writeLog[$path] = array($contents, $mtime);
         }
     }
 
+    private function writeFileNonAtomic($path, $contents)
+    {
+        file_put_contents($path, $contents);
+
+        if (is_array($this->writeLog)) {
+            $this->writeLog[$path] = array($contents, null);
+        }
+    }
+
     private function copyWriteLog($from, $to)
     {
         foreach ($this->writeLog as $path => $op) {

+ 27 - 0
src/Packagist/WebBundle/Resources/public/css/main.css

@@ -90,6 +90,10 @@ strong {
     font-weight: 600;
 }
 
+.no-padding {
+    padding: 0;
+}
+
 .navbar-wrapper {
     left: 0;
     right: 0;
@@ -938,6 +942,16 @@ input:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:fo
     margin-bottom: -1px !important;
     position: relative;
 }
+.package .force-update-trigger {
+    cursor: pointer;
+}
+.package .force-update-trigger.loading:after {
+    content: "";
+    background-position: 7px center;
+    background-image: url("../img/loader-white.gif");
+    background-repeat: no-repeat;
+    padding-left: 30px;
+}
 .package .action input.loading, .package .action input.loading:hover, .package .action input.loading:active {
   background-position: 10px center;
   background-image: url("../img/loader.gif");
@@ -1121,6 +1135,19 @@ input:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:fo
 .package .package-aside.versions-wrapper, .package .version-details {
   margin-top: 20px;
 }
+.package .last-update {
+    padding: 15px;
+}
+.package .last-update p, .package .last-update a {
+    font-size: 75%;
+}
+.package .last-update .auto-update-success {
+    color: #69AD21;
+}
+.package .last-update .auto-update-alert {
+    color: #cd3729;
+}
+
 .package .version-details .title {
   border-top: 1px solid #f28d1a;
   padding-top: 10px;

+ 1 - 1
src/Packagist/WebBundle/Resources/public/js/view.js

@@ -69,7 +69,7 @@
     }
 
     function forceUpdatePackage(e, updateAll) {
-        var submit = $('input[type=submit]', '.package .force-update'), data;
+        var submit = $('input[type=submit], .force-update-trigger', '.package .force-update'), data;
         var showOutput = e && e.shiftKey;
         if (e) {
             e.preventDefault();

+ 0 - 1
src/Packagist/WebBundle/Resources/views/about/about.html.twig

@@ -39,7 +39,6 @@ monolog/monolog-drupal-module
 // As long as it's in their own vendor namespace it does not conflict with anyone else.
 acme/email</code></pre>
             <p>Vendor names on packagist are protected once a package with that name has been published. That means you can not publish packages with a vendor name that already exists on packagist without permission. To be able to publish packages for an already existing vendor name you need to be maintainer of at least one package within that vendor.</p>
-            <p>Note that package names are case-insensitive, but it's encouraged to use a dash (-) as separator instead of CamelCased names.</p>
         </section>
 
         <section class="col-md-6">

+ 2 - 2
src/Packagist/WebBundle/Resources/views/layout.html.twig

@@ -93,7 +93,7 @@
 
                                             <div class="signin-box-buttons">
                                                 <a href="{{ hwi_oauth_login_url('github') }}" class="pull-right btn btn-primary btn-github"><span class="icon-github"></span>Use Github</a>
-                                                <button type="submit" class="btn btn-success" id="_submit" name="_submit">{{ 'security.login.submit'|trans({}, 'FOSUserBundle') }}</button>
+                                                <button type="submit" class="btn btn-success" id="_submit_mini" name="_submit">{{ 'security.login.submit'|trans({}, 'FOSUserBundle') }}</button>
                                             </div>
                                         </form>
 
@@ -199,7 +199,7 @@
                 </div>
 
                 <div class="row">
-                    <p class="toran col-xs-12">Packagist maintenance and hosting is supported by <a href="https://packagist.com/">Private Packagist</a></p>
+                    <p class="toran col-xs-12">Packagist maintenance and hosting is provided by <a href="https://packagist.com/">Private Packagist</a></p>
                 </div>
             </nav>
         </footer>

+ 13 - 7
src/Packagist/WebBundle/Resources/views/mirrors/index.html.twig

@@ -12,13 +12,19 @@
 <p>Packagist currently provides mirrors in Europe, North America (Montreal), and Asia (Singapore).</p>
 <p>On top of this, we are aware of the following list of third-party-run mirrors, please refer to their website to see how to use them:</p>
 <ul>
-  <li>Africa, South Africa <a href="https://packagist.co.za/">packagist.co.za</a></li>
-  <li>Asia, China <a href="https://mirrors.aliyun.com/composer/">mirrors.aliyun.com/composer</a></li>
-  <li>Asia, China <a href="https://pkg.phpcomposer.com/">pkg.phpcomposer.com</a></li>
-  <li>Asia, Indonesia <a href="https://packagist.phpindonesia.id/">packagist.phpindonesia.id</a></li>
-  <li>Asia, India <a href="https://packagist.in/">packagist.in</a></li>
-  <li>Asia, Japan <a href="https://packagist.jp/">packagist.jp</a></li>
-  <li>South America, Brazil <a href="https://packagist.com.br/">packagist.com.br</a></li>
+  <li>Africa, South Africa <a href="https://packagist.co.za">packagist.co.za</a></li>
+  <li>Asia, China <a href="https://mirrors.huaweicloud.com/repository/php">mirrors.huaweicloud.com/repository/php</a></li>
+  <li>Asia, China <a href="https://developer.aliyun.com/composer">developer.aliyun.com/composer</a></li>
+  <li>Asia, China <a href="https://php.cnpkg.org">php.cnpkg.org</a></li>
+  <li>Asia, China <a href="https://packagist.phpcomposer.com">packagist.phpcomposer.com</a></li>
+  <li>Asia, China <a href="https://packagist.mirrors.sjtug.sjtu.edu.cn">packagist.mirrors.sjtug.sjtu.edu.cn</a></li>
+  <li>Asia, China <a href="https://mirrors.cloud.tencent.com/help/composer.html">mirrors.cloud.tencent.com/help/composer.html</a></li>
+  <li>Asia, India <a href="https://packagist.in">packagist.in</a></li>
+  <li>Asia, Indonesia <a href="https://packagist.phpindonesia.id">packagist.phpindonesia.id</a></li>
+  <li>Asia, Japan <a href="https://packagist.jp">packagist.jp</a></li>
+  <li>Asia, South Korea <a href="https://packagist.kr">packagist.kr</a></li>
+  <li>Asia, Thailand <a href="https://packagist.mycools.in.th/">packagist.mycools.in.th/</a></li>
+  <li>South America, Brazil <a href="https://packagist.com.br">packagist.com.br</a></li>
 </ul>
 
 <h3 id="list-packages-all">{{ 'mirrors.running_your_own'|trans }}</h3>

+ 11 - 0
src/Packagist/WebBundle/Resources/views/package/spam.html.twig

@@ -35,4 +35,15 @@
         {% endembed %}
         </section>
     </section>
+    <div class="row">
+        <div class="col-xs-3">
+            <form class="action" action="{{ path("mark_nospam") }}" method="POST">
+                {% for p in packages %}
+                    <input type="hidden" name="vendor[]" value="{{ p.name|vendor }}" />
+                {% endfor %}
+                <input type="hidden" name="token" value="{{ markSafeCsrfToken }}" />
+                <input class="btn btn-danger" type="submit" value="Mark Whole Page As Not Spam" />
+            </form>
+        </div>
+    </div>
 {% endblock %}

+ 45 - 27
src/Packagist/WebBundle/Resources/views/package/version_list.html.twig

@@ -1,31 +1,49 @@
-<div class="col-md-3 package-aside versions-wrapper">
-    <ul class="versions">
-        {% for version in versions %}
-            {% set expanded = version.id == expandedId|default(false) %}
-            <li class="details-toggler version{% if loop.last %} last{% endif %}{% if expanded %} open{% endif %}" data-version-id="{{ version.version }}" data-load-more="{{ path('view_version', {versionId: version.id, _format: 'json'}) }}">
-                <a href="#{{ version.version }}" class="version-number">
-                    {{- version.version -}}
-                    {% if version.hasVersionAlias() %}
-                        / {{ version.versionAlias }}
-                    {% endif -%}
-                </a>
-
-                {% if hasVersionSecurityAdvisories[version.id]|default(false) %}
-                    <a class="advisory-alert" href="{{ path('view_package_advisories', {name: package.name, version: version.id}) }}">
-                        <i class="glyphicon glyphicon-alert " title="Version has security advisories"></i>
+<div class="col-md-3 no-padding">
+    <div class="package-aside versions-wrapper">
+        <ul class="versions">
+            {% for version in versions %}
+                {% set expanded = version.id == expandedId|default(false) %}
+                <li class="details-toggler version{% if loop.last %} last{% endif %}{% if expanded %} open{% endif %}" data-version-id="{{ version.version }}" data-load-more="{{ path('view_version', {versionId: version.id, _format: 'json'}) }}">
+                    <a href="#{{ version.version }}" class="version-number">
+                        {{- version.version -}}
+                        {% if version.hasVersionAlias() %}
+                            / {{ version.versionAlias }}
+                        {% endif -%}
                     </a>
-                {% endif %}
 
-                {% if deleteVersionCsrfToken is defined and deleteVersionCsrfToken is not empty %}
-                <form class="delete-version" action="{{ path("delete_version", {"versionId": version.id}) }}" method="DELETE">
-                    <input type="hidden" name="_token" value="{{ deleteVersionCsrfToken }}" />
-                    <i class="submit glyphicon glyphicon-remove"></i>
+                    {% if hasVersionSecurityAdvisories[version.id]|default(false) %}
+                        <a class="advisory-alert" href="{{ path('view_package_advisories', {name: package.name, version: version.id}) }}">
+                            <i class="glyphicon glyphicon-alert " title="Version has security advisories"></i>
+                        </a>
+                    {% endif %}
+
+                    {% if deleteVersionCsrfToken is defined and deleteVersionCsrfToken is not empty %}
+                    <form class="delete-version" action="{{ path("delete_version", {"versionId": version.id}) }}" method="DELETE">
+                        <input type="hidden" name="_token" value="{{ deleteVersionCsrfToken }}" />
+                        <i class="submit glyphicon glyphicon-remove"></i>
+                    </form>
+                    {% endif %}
+                </li>
+            {% endfor %}
+        </ul>
+        <div class="hidden versions-expander">
+            <i class="glyphicon glyphicon-chevron-down"></i>
+        </div>
+
+    {% if showUpdated is defined and showUpdated and package.getUpdatedAt() %}
+        <div class="last-update">
+            {% if ("github.com" in package.repository and package.getAutoUpdated() == 1) or not package.isAutoUpdated() %}
+                <p class="auto-update-danger">This package is <strong>not</strong> auto-updated.</p>
+            {% else %}
+                <p class="auto-update-success">This package is auto-updated.</p>
+            {% endif %}
+            <p>Last update: {{ package.getUpdatedAt()|date('Y-m-d H:i:s') }} UTC</p>
+            {% if showUpdateButton %}
+                <form class="force-update action" action="{{ path('update_package', {name: package.name, type: 'public_update'}) }}" method="PUT">
+                    <input type="hidden" name="update" value="1" />
+                    <a class="force-update-trigger">Update Now</a>
                 </form>
-                {% endif %}
-            </li>
-        {% endfor %}
-    </ul>
-    <div class="hidden versions-expander">
-        <i class="glyphicon glyphicon-chevron-down"></i>
-    </div>
+            {% endif %}
+        </div>
+    {% endif %}
 </div>

+ 23 - 14
src/Packagist/WebBundle/Resources/views/package/view_package.html.twig

@@ -288,26 +288,35 @@
             {% endif %}
 
             {% if versions|length %}
-            <div class="row versions-section">
-                <div class="version-details col-md-9">
-                    {% if expandedVersion %}
-                        {% include 'PackagistWebBundle:package:version_details.html.twig' with {version: expandedVersion} %}
-                    {% endif %}
+                <div class="row versions-section">
+                    <div class="version-details col-md-9">
+                        {% if expandedVersion %}
+                            {% include 'PackagistWebBundle:package:version_details.html.twig' with {version: expandedVersion} %}
+                        {% endif %}
+                    </div>
+                    {% include 'PackagistWebBundle:package:version_list.html.twig' with {
+                        versions: versions,
+                        expandedId: expandedVersion.id,
+                        deleteVersionCsrfToken: deleteVersionCsrfToken|default(null),
+                        package: package,
+                        showUpdated: true,
+                        showUpdateButton: not hasActions and app.user and not package.wasUpdatedInTheLast24Hours(),
+                        hasVersionSecurityAdvisories: hasVersionSecurityAdvisories
+                    } %}
                 </div>
-                {% include 'PackagistWebBundle:package:version_list.html.twig' with {versions: versions, expandedId: expandedVersion.id, deleteVersionCsrfToken: deleteVersionCsrfToken|default(null), hasVersionSecurityAdvisories: hasVersionSecurityAdvisories} %}
             {% elseif package.crawledAt is null %}
                 <p class="col-xs-12">This package has not been crawled yet, some information is missing.</p>
             {% else %}
                 <p class="col-xs-12">This package has no released version yet, and little information is available.</p>
             {% endif %}
-        </div>
 
-        {% if package.readme != null and (not package.isSuspect() or hasActions) %}
-            <hr class="clearfix">
-            <div class="readme markdown-body">
-                <h1>README</h1>
-                {{ package.optimizedReadme|raw }}
-            </div>
-        {% endif %}
+            {% if package.readme != null and (not package.isSuspect() or hasActions) %}
+                <hr class="clearfix">
+                <div class="readme markdown-body">
+                    <h1>README</h1>
+                    {{ package.optimizedReadme|raw }}
+                </div>
+            {% endif %}
+        </div>
     </div>
 {% endblock %}

+ 6 - 0
src/Packagist/WebBundle/Resources/views/user/profile.html.twig

@@ -20,6 +20,12 @@
         {%- if is_granted('ROLE_ADMIN') %}
             <a href="mailto:{{ user.email }}">{{ user.email }}</a>
         {%- endif %}
+        {%- if deleteForm is defined and (is_granted('ROLE_ADMIN') or isActualUser) %}
+            <form class="delete action" action="{{ path('user_delete', {name: user.username}) }}" method="POST">
+                {{ form_widget(deleteForm._token) }}
+                <input class="btn btn-danger" type="submit" value="Delete Account Permanently" onclick="return confirm('Are you sure? There is no way back..');" />
+            </form>
+        {%- endif %}
         {%- if is_granted('ROLE_ANTISPAM') %}
             <form class="delete action" action="{{ path('mark_spammer', {name: user.username}) }}" method="POST">
                 {{ form_widget(spammerForm._token) }}

+ 21 - 0
src/Packagist/WebBundle/Tests/Entity/PackageTest.php

@@ -0,0 +1,21 @@
+<?php declare(strict_types=1);
+
+namespace Packagist\WebBundle\Tests\Entity;
+
+use Packagist\WebBundle\Entity\Package;
+use PHPUnit\Framework\TestCase;
+
+class PackageTest extends TestCase
+{
+    public function testWasUpdatedInTheLast24Hours(): void
+    {
+        $package = new Package();
+        $this->assertFalse($package->wasUpdatedInTheLast24Hours());
+
+        $package->setUpdatedAt(new \DateTime('2019-01-01'));
+        $this->assertFalse($package->wasUpdatedInTheLast24Hours());
+
+        $package->setUpdatedAt(new \DateTime('now'));
+        $this->assertTrue($package->wasUpdatedInTheLast24Hours());
+    }
+}