Compare commits

..

1 Commits

Author SHA1 Message Date
94303c92e6 Add a validator on URL entity 2023-08-29 14:17:53 +02:00
33 changed files with 643 additions and 1041 deletions

View File

@ -33,9 +33,7 @@ jobs:
run: make release VERSION=${{ github.event.release.tag_name }}
- name: Upload the package to the release
uses: pierrotdelalune/Form_Data_HTTP_POST_Action@main
uses: shogo82148/actions-upload-release-asset@v1
with:
url: ${{ github.event.release.upload_url }}
headers: "{\"Authorization\": \"token ${{ secrets.GITEA_TOKEN }}\"}"
file: /tmp/wllbg-release/wallabag-${{ github.event.release.tag_name }}.tar.gz
name: wallabag-${{ github.event.release.tag_name }}.tar.gz
upload_url: ${{ github.event.release.upload_url }}
asset_path: /tmp/wllbg-release/wallabag-${{ github.event.release.tag_name }}.tar.gz

View File

@ -1 +0,0 @@
GITEA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

View File

@ -1,28 +1,5 @@
# Changelog
## [2.6.7](https://github.com/wallabag/wallabag/tree/2.6.7)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.6...2.6.7)
### Security fix
* A user can disable her 2FA unintentionally by @kdecherf in https://github.com/wallabag/wallabag/commit/0cfdddc2eb0aee5ffb69bf499d377d75655ba157
### Fixes
* Fix deprecated null tag parameter by @Simounet in https://github.com/wallabag/wallabag/pull/6985
* Full clickable card on mass action by @Simounet in https://github.com/wallabag/wallabag/pull/6991
* Add tag form submit button always displayed by @Simounet in https://github.com/wallabag/wallabag/pull/6986
## [2.6.6](https://github.com/wallabag/wallabag/tree/2.6.6)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.5...2.6.6)
### Security fix
* Force secure cookie on HTTPS connection by @j0k3r in https://github.com/wallabag/wallabag/pull/6924
### Fixes
* Fix checkboxes pointer events issue by @Simounet in https://github.com/wallabag/wallabag/pull/6897
* Add Google mailer by @j0k3r in https://github.com/wallabag/wallabag/pull/6899
* Improve performance on homepage by @Simounet in https://github.com/wallabag/wallabag/pull/6909
* Mass action layout improved by @Simounet in https://github.com/wallabag/wallabag/pull/6912
## [2.6.5](https://github.com/wallabag/wallabag/tree/2.6.5)
[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.4...2.6.5)

View File

@ -1,7 +0,0 @@
1. Copy release_event.json.EXAMPLE to elease_event.json and adapt it
2. Copy .secrets.EXAMPLE to .secrets and adapt it
3. Run
$ act release -e release_event.json
or
$ act release -e release_event.json --pull=false
to avoid pulling act image at each run

View File

@ -235,12 +235,6 @@
z-index: 9999;
}
.tags-add-form {
display: flex;
align-items: center;
gap: 20px;
}
@media only screen and (max-width: 640px) {
.entry-info {
margin-bottom: 20px;
@ -264,12 +258,4 @@
#article .entry-info .chip-action {
min-width: 40px;
}
.tags-add-form {
display: block;
}
.tags-add-form-submit {
margin-top: 10px;
}
}

View File

@ -14,53 +14,44 @@
}
.mass-action {
margin: 20px 5px 10px 20px;
margin: 10px 5px 10px 20px;
}
.mass-action-group {
display: flex;
padding: 3px;
align-items: center;
gap: 30px;
gap: 10px;
}
.mass-action-button {
height: 36px;
line-height: 36px;
padding: 0 0.7rem;
height: 24px;
line-height: 24px;
padding: 0 0.5rem;
i {
font-size: 1rem;
}
}
.mass-action-button--tags {
border-radius: 2px 0 0 2px;
}
.card-stacked .entry-checkbox {
.entry-checkbox {
margin: 10px 15px 10px 5px;
}
.card .entry-checkbox {
position: absolute;
display: flex;
padding: 10px;
inset: 0;
justify-content: flex-end;
align-items: start;
background-color: rgb(0 172 193 / 20%);
cursor: pointer;
z-index: 10;
.card & {
float: right;
margin-right: 0;
padding: 10px;
}
}
.entries .entry-checkbox-input,
.mass-action .entry-checkbox-input {
position: relative;
left: 0;
width: 20px;
min-height: 25px;
height: 100%;
vertical-align: middle;
opacity: initial;
cursor: pointer;
z-index: 10;
}
@ -73,19 +64,11 @@
.mass-action-tags {
display: flex;
margin-top: 10px;
align-items: center;
gap: 10px;
.mass-action-tags-input.mass-action-tags-input {
.mass-action-tags-input {
margin: 0;
padding: 0 5px;
height: 34px;
background: white;
border-bottom: 3px solid #c5ebef;
}
.mass-action-tags-input.mass-action-tags-input.mass-action-tags-input:focus {
border-bottom: 3px solid $blue-accent-color;
box-shadow: none;
}
}
@ -105,16 +88,13 @@
.results {
display: flex;
margin-bottom: 10px;
padding: 1rem 1rem 0;
flex-wrap: wrap;
justify-content: space-between;
}
.nb-results {
display: inline-flex;
margin-bottom: 20px;
gap: 30px;
.nb-results {
display: inline-flex;
}
}
.results-item {
@ -193,38 +173,9 @@ footer {
}
@media screen and (min-width: 993px) {
.results {
margin-bottom: 0;
}
.nb-results {
margin-bottom: 0;
gap: 0;
}
.mass-action-button {
height: 24px;
line-height: 24px;
padding: 0 0.5rem;
}
.mass-action-group {
gap: 10px;
}
.mass-action-tags {
margin-top: 0;
margin-left: 7px;
flex-wrap: initial;
}
.mass-action {
display: flex;
margin-top: 10px;
align-items: center;
.mass-action-tags-input.mass-action-tags-input {
height: 21px;
}
gap: 30px;
}
}

View File

@ -29,7 +29,6 @@ framework:
# handler_id set to null will use default session handler from php.ini
handler_id: session.handler.native_file
save_path: "%kernel.project_dir%/var/sessions/%kernel.environment%"
cookie_secure: auto
fragments: ~
http_method_override: true
assets: ~

View File

@ -1,5 +1,5 @@
wallabag_core:
version: 2.6.7
version: 2.6.5
paypal_url: "https://liberapay.com/wallabag/donate"
languages:
en: 'English'

View File

@ -130,7 +130,6 @@
"symfony/form": "^4.4",
"symfony/framework-bundle": "^4.4",
"symfony/google-mailer": "^4.4",
"symfony/amazon-mailer": "^5.2",
"symfony/http-foundation": "^4.4",
"symfony/http-kernel": "^4.4",
"symfony/mailer": "^4.4",

1071
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -62,5 +62,5 @@ parameters:
-
message: "#^Method FOS\\\\UserBundle\\\\Model\\\\UserManagerInterface\\:\\:updateUser()#"
count: 6
count: 7
path: src/Wallabag/CoreBundle/Controller/ConfigController.php

View File

@ -1,7 +0,0 @@
{
"release" :
{
"tag_name" : "2.6.7_all_mailers",
"upload_url" : "https://gyokuro.ile-australe.eu/api/v1/repos/pierre/wallabag/releases/1918/assets"
}
}

View File

@ -9,7 +9,7 @@ ENV=$4
rm -rf "${TMP_FOLDER:?}"/"$RELEASE_FOLDER"
mkdir "$TMP_FOLDER"/"$RELEASE_FOLDER"
git clone https://gyokuro.ile-australe.eu/pierre/wallabag.git --single-branch --depth 1 --branch $1 "$TMP_FOLDER"/"$RELEASE_FOLDER"/"$VERSION"
git clone https://github.com/wallabag/wallabag.git --single-branch --depth 1 --branch $1 "$TMP_FOLDER"/"$RELEASE_FOLDER"/"$VERSION"
cd "$TMP_FOLDER"/"$RELEASE_FOLDER"/"$VERSION" && SYMFONY_ENV="$ENV" COMPOSER_MEMORY_LIMIT=-1 composer install -n --no-dev
cd "$TMP_FOLDER"/"$RELEASE_FOLDER"/"$VERSION" && php bin/console wallabag:install --env="$ENV" -n
cd "$TMP_FOLDER"/"$RELEASE_FOLDER"/"$VERSION" && php bin/console assets:install --env="$ENV" --symlink --relative

View File

@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Entity\Tag;
use Wallabag\CoreBundle\Event\EntryDeletedEvent;
@ -688,8 +689,15 @@ class EntryRestController extends WallabagRestController
*
* @return JsonResponse
*/
public function postEntriesAction(Request $request, EntryRepository $entryRepository, ContentProxy $contentProxy, LoggerInterface $logger, TagsAssigner $tagsAssigner, EventDispatcherInterface $eventDispatcher)
{
public function postEntriesAction(
Request $request,
EntryRepository $entryRepository,
ContentProxy $contentProxy,
LoggerInterface $logger,
TagsAssigner $tagsAssigner,
EventDispatcherInterface $eventDispatcher,
ValidatorInterface $validator
) {
$this->validateAuthentication();
$url = $request->request->get('url');
@ -760,6 +768,13 @@ class EntryRestController extends WallabagRestController
$contentProxy->setDefaultEntryTitle($entry);
}
$errors = $validator->validate($entry);
if (\count($errors) > 0) {
$errorsString = (string) $errors;
return $this->sendResponse($errorsString);
}
$this->entityManager->persist($entry);
$this->entityManager->flush();

View File

@ -254,14 +254,10 @@ class ConfigController extends AbstractController
/**
* Disable 2FA using email.
*
* @Route("/config/otp/email/disable", name="disable_otp_email", methods={"POST"})
* @Route("/config/otp/email/disable", name="disable_otp_email")
*/
public function disableOtpEmailAction(Request $request)
public function disableOtpEmailAction()
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
$user = $this->getUser();
$user->setEmailTwoFactor(false);
@ -278,14 +274,10 @@ class ConfigController extends AbstractController
/**
* Enable 2FA using email.
*
* @Route("/config/otp/email", name="config_otp_email", methods={"POST"})
* @Route("/config/otp/email", name="config_otp_email")
*/
public function otpEmailAction(Request $request)
public function otpEmailAction()
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
$user = $this->getUser();
$user->setGoogleAuthenticatorSecret(null);
@ -305,14 +297,10 @@ class ConfigController extends AbstractController
/**
* Disable 2FA using OTP app.
*
* @Route("/config/otp/app/disable", name="disable_otp_app", methods={"POST"})
* @Route("/config/otp/app/disable", name="disable_otp_app")
*/
public function disableOtpAppAction(Request $request)
public function disableOtpAppAction()
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
$user = $this->getUser();
$user->setGoogleAuthenticatorSecret('');
@ -331,14 +319,10 @@ class ConfigController extends AbstractController
/**
* Enable 2FA using OTP app, user will need to confirm the generated code from the app.
*
* @Route("/config/otp/app", name="config_otp_app", methods={"POST"})
* @Route("/config/otp/app", name="config_otp_app")
*/
public function otpAppAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator)
public function otpAppAction(GoogleAuthenticatorInterface $googleAuthenticator)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
$user = $this->getUser();
$secret = $googleAuthenticator->generateSecret();
@ -373,10 +357,8 @@ class ConfigController extends AbstractController
* Cancelling 2FA using OTP app.
*
* @Route("/config/otp/app/cancel", name="config_otp_app_cancel")
*
* XXX: commented until we rewrite 2fa with a real two-steps activation
*/
/*public function otpAppCancelAction()
public function otpAppCancelAction()
{
$user = $this->getUser();
$user->setGoogleAuthenticatorSecret(null);
@ -385,19 +367,15 @@ class ConfigController extends AbstractController
$this->userManager->updateUser($user, true);
return $this->redirect($this->generateUrl('config') . '#set3');
}*/
}
/**
* Validate OTP code.
*
* @Route("/config/otp/app/check", name="config_otp_app_check", methods={"POST"})
* @Route("/config/otp/app/check", name="config_otp_app_check")
*/
public function otpAppCheckAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
$isValid = $googleAuthenticator->checkCode(
$this->getUser(),
$request->get('_auth_code')
@ -417,12 +395,7 @@ class ConfigController extends AbstractController
'scheb_two_factor.code_invalid'
);
$this->addFlash(
'notice',
'scheb_two_factor.code_invalid'
);
return $this->redirect($this->generateUrl('config') . '#set3');
return $this->redirect($this->generateUrl('config_otp_app'));
}
/**

View File

@ -197,6 +197,8 @@ class EntryController extends AbstractController
// entry saved, dispatch event about it!
$this->eventDispatcher->dispatch(new EntrySavedEvent($entry), EntrySavedEvent::NAME);
return $this->redirect($this->generateUrl('homepage'));
} elseif ($form->isSubmitted() && !$form->isValid()) {
return $this->redirect($this->generateUrl('homepage'));
}
@ -675,6 +677,9 @@ class EntryController extends AbstractController
}
}
$nbEntriesUntagged = $this->entryRepository
->countUntaggedEntriesByUser($this->getUser()->getId());
return $this->render(
'@WallabagCore/Entry/entries.html.twig', [
'form' => $form->createView(),
@ -682,6 +687,7 @@ class EntryController extends AbstractController
'currentPage' => $page,
'searchTerm' => $searchTerm,
'isFiltered' => $form->isSubmitted(),
'nbEntriesUntagged' => $nbEntriesUntagged,
]
);
}

View File

@ -45,7 +45,7 @@ class TagController extends AbstractController
$form = $this->createForm(NewTagType::class, new Tag());
$form->handleRequest($request);
$tags = $form->get('label')->getData() ?? '';
$tags = $form->get('label')->getData();
$tagsExploded = explode(',', $tags);
// avoid too much tag to be added

View File

@ -78,6 +78,9 @@ class Entry
* @var string
*
* @Assert\NotBlank()
* @Assert\Url(
* message = "The url '{{ value }}' is not a valid url",
* )
* @ORM\Column(name="url", type="text", nullable=true)
*
* @Groups({"entries_for_user", "export_all"})

View File

@ -4,6 +4,7 @@ namespace Wallabag\CoreBundle\Form\Type;
use FOS\UserBundle\Form\Type\RegistrationFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@ -22,6 +23,15 @@ class UserInformationType extends AbstractType
->add('email', EmailType::class, [
'label' => 'config.form_user.email_label',
])
->add('emailTwoFactor', CheckboxType::class, [
'required' => false,
'label' => 'config.form_user.emailTwoFactor_label',
])
->add('googleTwoFactor', CheckboxType::class, [
'required' => false,
'label' => 'config.form_user.googleTwoFactor_label',
'mapped' => false,
])
->add('save', SubmitType::class, [
'label' => 'config.form.save',
])

View File

@ -37,20 +37,6 @@ class EntryRepository extends ServiceEntityRepository
;
}
/**
* Retrieves all entries count for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getCountBuilderForAllByUser($userId)
{
return $this
->getQueryBuilderByUser($userId)
;
}
/**
* Retrieves unread entries for a user.
*
@ -66,21 +52,6 @@ class EntryRepository extends ServiceEntityRepository
;
}
/**
* Retrieves unread entries count for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getCountBuilderForUnreadByUser($userId)
{
return $this
->getQueryBuilderByUser($userId)
->andWhere('e.isArchived = false')
;
}
/**
* Retrieves entries with the same domain.
*
@ -123,21 +94,6 @@ class EntryRepository extends ServiceEntityRepository
;
}
/**
* Retrieves read entries count for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getCountBuilderForArchiveByUser($userId)
{
return $this
->getQueryBuilderByUser($userId)
->andWhere('e.isArchived = true')
;
}
/**
* Retrieves starred entries for a user.
*
@ -153,21 +109,6 @@ class EntryRepository extends ServiceEntityRepository
;
}
/**
* Retrieves starred entries count for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getCountBuilderForStarredByUser($userId)
{
return $this
->getQueryBuilderByUser($userId)
->andWhere('e.isStarred = true')
;
}
/**
* Retrieves entries filtered with a search term for a user.
*
@ -226,21 +167,6 @@ class EntryRepository extends ServiceEntityRepository
;
}
/**
* Retrieve entries with annotations count for a user.
*
* @param int $userId
*
* @return QueryBuilder
*/
public function getCountBuilderForAnnotationsByUser($userId)
{
return $this
->getQueryBuilderByUser($userId)
->innerJoin('e.annotations', 'a')
;
}
/**
* Retrieve untagged entries for a user.
*
@ -637,23 +563,6 @@ class EntryRepository extends ServiceEntityRepository
return $qb->getQuery()->getArrayResult();
}
/**
* @param int $userId
*
* @return array
*/
public function findEmptyEntriesIdByUserId($userId = null)
{
$qb = $this->createQueryBuilder('e')
->select('e.id');
if (null !== $userId) {
$qb->where('e.user = :userid AND e.content IS NULL')->setParameter(':userid', $userId);
}
return $qb->getQuery()->getArrayResult();
}
/**
* Find all entries by url and owner.
*

View File

@ -209,66 +209,38 @@
{{ form_widget(form.user.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
<br/>
<br/>
<div class="row">
<h5>{{ 'config.otp.page_title'|trans }}</h5>
<p>{{ 'config.form_user.two_factor_description'|trans }}</p>
<table>
<thead>
<tr>
<th>{{ 'config.form_user.two_factor.table_method'|trans }}</th>
<th>{{ 'config.form_user.two_factor.table_state'|trans }}</th>
<th>{{ 'config.form_user.two_factor.table_action'|trans }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ 'config.form_user.two_factor.emailTwoFactor_label'|trans }}</td>
<td>{% if app.user.isEmailTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
<td><a href="{{ path('config_otp_email') }}" class="waves-effect waves-light btn{% if app.user.isEmailTwoFactor %} disabled{% endif %}">{{ 'config.form_user.two_factor.action_email'|trans }}</a> {% if app.user.isEmailTwoFactor %}<a href="{{ path('disable_otp_email') }}" class="waves-effect waves-light btn red">Disable</a>{% endif %}</td>
</tr>
<tr>
<td>{{ 'config.form_user.two_factor.googleTwoFactor_label'|trans }}</td>
<td>{% if app.user.isGoogleTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
<td><a href="{{ path('config_otp_app') }}" class="waves-effect waves-light btn{% if app.user.isGoogleTwoFactor %} disabled{% endif %}">{{ 'config.form_user.two_factor.action_app'|trans }}</a> {% if app.user.isGoogleTwoFactor %}<a href="{{ path('disable_otp_app') }}" class="waves-effect waves-light btn red">Disable</a>{% endif %}</td>
</tr>
</tbody>
</table>
</div>
{{ form_widget(form.user._token) }}
{{ form_end(form.user) }}
<br/>
<br/>
<div class="row">
<h5>{{ 'config.otp.page_title'|trans }}</h5>
<p>{{ 'config.form_user.two_factor_description'|trans }}</p>
<table>
<thead>
<tr>
<th>{{ 'config.form_user.two_factor.table_method'|trans }}</th>
<th>{{ 'config.form_user.two_factor.table_state'|trans }}</th>
<th>{{ 'config.form_user.two_factor.table_action'|trans }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ 'config.form_user.two_factor.emailTwoFactor_label'|trans }}</td>
<td>{% if app.user.isEmailTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
<td>
<form action="{{ path('config_otp_email') }}" method="post" name="config_otp_email">
<input type="hidden" name="token" value="{{ csrf_token('otp') }}" />
<button class="waves-effect waves-light btn{% if app.user.isEmailTwoFactor %} disabled{% endif %}" type="submit">{{ 'config.form_user.two_factor.action_email'|trans }}</button>
</form>
{% if app.user.isEmailTwoFactor %}
<form action="{{ path('disable_otp_email') }}" method="post" name="disable_otp_email">
<input type="hidden" name="token" value="{{ csrf_token('otp') }}" />
<button class="waves-effect waves-light btn red" type="submit">Disable</button>
</form>
{% endif %}
</td>
</tr>
<tr>
<td>{{ 'config.form_user.two_factor.googleTwoFactor_label'|trans }}</td>
<td>{% if app.user.isGoogleTwoFactor %}<b>{{ 'config.form_user.two_factor.state_enabled'|trans }}</b>{% else %}{{ 'config.form_user.two_factor.state_disabled'|trans }}{% endif %}</td>
<td>
<form action="{{ path('config_otp_app') }}" method="post" name="config_otp_app">
<input type="hidden" name="token" value="{{ csrf_token('otp') }}" />
<button class="waves-effect waves-light btn{% if app.user.isGoogleTwoFactor %} disabled{% endif %}" type="submit">{{ 'config.form_user.two_factor.action_app'|trans }}</button>
</form>
{% if app.user.isGoogleTwoFactor %}
<form action="{{ path('disable_otp_app') }}" method="post" name="disable_otp_app">
<input type="hidden" name="token" value="{{ csrf_token('otp') }}" />
<button class="waves-effect waves-light btn red" type="submit">Disable</button>
</form>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
</form>
</div>
<div id="set4" class="col s12">

View File

@ -40,7 +40,6 @@
{% endfor %}
<form class="form" action="{{ path("config_otp_app_check") }}" method="post">
<input type="hidden" name="token" value="{{ csrf_token('otp') }}" />
<div class="card-content">
<div class="row">
<div class="input-field col s12">
@ -50,6 +49,9 @@
</div>
</div>
<div class="card-action">
<a href="{{ path('config_otp_app_cancel') }}" class="waves-effect waves-light grey btn">
{{ 'config.otp.app.cancel'|trans }}
</a>
<button class="btn waves-effect waves-light" type="submit" name="send">
{{ 'config.otp.app.enable'|trans }}
<i class="material-icons right">send</i>

View File

@ -1,3 +1,3 @@
<label class="entry-checkbox">
<div class="entry-checkbox">
<input type="checkbox" class="entry-checkbox-input" data-js="entry-checkbox" name="entry-checkbox[]" value="{{ entry.id }}" />
</label>
</div>

View File

@ -1,7 +1,7 @@
<div class="card entry-card{% if currentRoute in routes and entry.isArchived %} archived{% endif %}">
{% include "@WallabagCore/Entry/Card/_mass_checkbox.html.twig" with {'entry': entry} only %}
<div class="card-body">
<div class="{% if app.user.config.displayThumbnails %}card-image{% endif %} waves-effect waves-block waves-light">
{% include "@WallabagCore/Entry/Card/_mass_checkbox.html.twig" with {'entry': entry} only %}
<ul class="card-entry-labels">
{% for tag in entry.tags|slice(0, 3) %}
<li title="{{ tag.label }}"><a href="{{ path('tag_entries', {'slug': tag.slug}) }}">{{ tag.label }}</a></li>

View File

@ -53,11 +53,13 @@
<button class="mass-action-button btn cyan darken-1" type="submit" name="toggle-read" title="{{ 'entry.list.toogle_as_read'|trans }}"><i class="material-icons">done</i></button>
<button class="mass-action-button btn cyan darken-1" type="submit" name="toggle-star" title="{{ 'entry.list.toogle_as_star'|trans }}" ><i class="material-icons">star</i></button>
<button class="mass-action-button btn cyan darken-1" type="submit" name="delete" onclick="return confirm('{{ 'entry.confirm.delete_entries'|trans|escape('js') }}')" title="{{ 'entry.list.delete'|trans }}"><i class="material-icons">delete</i></button>
<label for="mass-action-tags-displayed" class="mass-action-button btn cyan darken-1" type="button" title="{{ 'entry.list.add_tags'|trans }}"><i class="material-icons">label</i></label>
</div>
<input id="mass-action-tags-displayed" class="toggle-checkbox" type="checkbox" />
<div class="mass-action-tags">
<button class="btn cyan darken-1 mass-action-button mass-action-button--tags" type="submit" name="tag" title="{{ 'entry.list.add_tags'|trans }}"><i class="material-icons">label</i></button>
<input type="text" class="mass-action-tags-input" name="tags" placeholder="{{ 'entry.list.mass_action_tags_input_placeholder'|trans }}" />
<button class="btn cyan darken-1" type="submit" name="tag">{{ 'entry.list.add_tags'|trans }}</button>
</div>
</div>
@ -116,9 +118,9 @@
<h4 class="center">{{ 'entry.filters.title'|trans }}</h4>
<div class="row">
{% if current_route != 'untagged' %}
{% if current_route != 'untagged' and nbEntriesUntagged != 0 %}
<div class="col s12 center-align">
<a href="{{ path('untagged') }}">{{ 'tag.list.see_untagged_entries'|trans }}</a>
<a href="{{ path('untagged') }}">{{ 'tag.list.see_untagged_entries'|trans }} ({{ nbEntriesUntagged }})</a>
</div>
{% endif %}

View File

@ -1,4 +1,4 @@
<form class="tags-add-form" name="tag" method="post" action="{{ path('new_tag', {'entry': entry.id}) }}">
<form name="tag" method="post" action="{{ path('new_tag', {'entry': entry.id}) }}">
{% if form_errors(form) %}
<span class="black-text">{{ form_errors(form) }}</span>
{% endif %}
@ -9,6 +9,6 @@
{{ form_widget(form.label, {'attr': {'autocomplete': 'off'}}) }}
{{ form_widget(form.add, {'attr': {'class': 'btn waves-effect waves-light tags-add-form-submit'}}) }}
{{ form_widget(form.add, {'attr': {'class': 'btn waves-effect waves-light hide-on-large-only'}}) }}
{{ form_widget(form._token) }}
</form>

View File

@ -168,7 +168,7 @@
<div class="container">
<div class="row">
<div class="col m12 l8">
<p class="footer-text">
<p class="footer-text" title="{{ display_stats()|raw|striptags }}">
{{ display_stats() }}
</p>
</div>

View File

@ -88,32 +88,35 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
switch ($type) {
case 'starred':
$qb = $this->entryRepository->getCountBuilderForStarredByUser($user->getId());
$qb = $this->entryRepository->getBuilderForStarredByUser($user->getId());
break;
case 'archive':
$qb = $this->entryRepository->getCountBuilderForArchiveByUser($user->getId());
$qb = $this->entryRepository->getBuilderForArchiveByUser($user->getId());
break;
case 'unread':
$qb = $this->entryRepository->getCountBuilderForUnreadByUser($user->getId());
$qb = $this->entryRepository->getBuilderForUnreadByUser($user->getId());
break;
case 'annotated':
$qb = $this->entryRepository->getCountBuilderForAnnotationsByUser($user->getId());
$qb = $this->entryRepository->getBuilderForAnnotationsByUser($user->getId());
break;
case 'all':
$qb = $this->entryRepository->getCountBuilderForAllByUser($user->getId());
$qb = $this->entryRepository->getBuilderForAllByUser($user->getId());
break;
default:
throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type));
}
// THANKS to PostgreSQL we CAN'T make a DEAD SIMPLE count(e.id)
// ERROR: column "e0_.id" must appear in the GROUP BY clause or be used in an aggregate function
$query = $qb
->select('COUNT(e.id)')
->select('e.id')
->groupBy('e.id')
->getQuery();
$query->useQueryCache(true);
$query->enableResultCache($this->lifeTime);
return $query->getSingleScalarResult();
return \count($query->getArrayResult());
}
/**
@ -145,14 +148,15 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface
return 0;
}
$query = $this->entryRepository->getCountBuilderForArchiveByUser($user->getId())
->select('COUNT(e.id)')
$query = $this->entryRepository->getBuilderForArchiveByUser($user->getId())
->select('e.id')
->groupBy('e.id')
->getQuery();
$query->useQueryCache(true);
$query->enableResultCache($this->lifeTime);
$nbArchives = $query->getSingleScalarResult();
$nbArchives = \count($query->getArrayResult());
$interval = $user->getCreatedAt()->diff(new \DateTime('now'));
$nbDays = (int) $interval->format('%a') ?: 1;

View File

@ -509,6 +509,25 @@ class EntryRestControllerTest extends WallabagApiTestCase
$this->assertSame(400, $this->client->getResponse()->getStatusCode());
}
public function testBadFormatURL()
{
$this->client->request('POST', '/api/entries.json', [
'url' => 'wallabagIsAwesome',
'tags' => 'google',
'title' => 'New title for my article',
'content' => 'my content',
'language' => 'de',
'published_at' => '2016-09-08T11:55:58+0200',
'authors' => 'bob,helen',
'public' => 1,
]);
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
$content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertStringContainsString('The url \'"wallabagIsAwesome"\' is not a valid url', $content);
}
public function testPostEntry()
{
$this->client->request('POST', '/api/entries.json', [

View File

@ -1174,13 +1174,14 @@ class ConfigControllerTest extends WallabagCoreTestCase
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/config');
$form = $crawler->filter('form[name=config_otp_email]')->form();
$client->submit($form);
$crawler = $client->request('GET', '/config/otp/email');
$this->assertSame(302, $client->getResponse()->getStatusCode());
$this->assertStringContainsString('flashes.config.notice.otp_enabled', $client->getContainer()->get(SessionInterface::class)->getFlashBag()->get('notice')[0]);
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.config.notice.otp_enabled', $alert[0]);
// restore user
$em = $this->getEntityManager();
@ -1200,23 +1201,14 @@ class ConfigControllerTest extends WallabagCoreTestCase
$this->logInAs('admin');
$client = $this->getTestClient();
$em = $this->getEntityManager();
$user = $em
->getRepository(User::class)
->findOneByUsername('admin');
$user->setEmailTwoFactor(true);
$em->persist($user);
$em->flush();
$crawler = $client->request('GET', '/config');
$form = $crawler->filter('form[name=disable_otp_email]')->form();
$client->submit($form);
$crawler = $client->request('GET', '/config/otp/email/disable');
$this->assertSame(302, $client->getResponse()->getStatusCode());
$this->assertStringContainsString('flashes.config.notice.otp_disabled', $client->getContainer()->get(SessionInterface::class)->getFlashBag()->get('notice')[0]);
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.config.notice.otp_disabled', $alert[0]);
// restore user
$em = $this->getEntityManager();
@ -1232,10 +1224,7 @@ class ConfigControllerTest extends WallabagCoreTestCase
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/config');
$form = $crawler->filter('form[name=config_otp_app]')->form();
$client->submit($form);
$crawler = $client->request('GET', '/config/otp/app');
$this->assertSame(200, $client->getResponse()->getStatusCode());
@ -1254,28 +1243,49 @@ class ConfigControllerTest extends WallabagCoreTestCase
$em->flush();
}
public function testUserDisable2faGoogle()
public function testUserEnable2faGoogleCancel()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/config/otp/app');
$this->assertSame(200, $client->getResponse()->getStatusCode());
// restore user
$em = $this->getEntityManager();
$user = $em
->getRepository(User::class)
->findOneByUsername('admin');
$user->setGoogleAuthenticatorSecret('Google2FA');
$em->persist($user);
$em->flush();
$this->assertTrue($user->isGoogleTwoFactor());
$this->assertGreaterThan(0, $user->getBackupCodes());
$crawler = $client->request('GET', '/config');
$form = $crawler->filter('form[name=disable_otp_app]')->form();
$client->submit($form);
$crawler = $client->request('GET', '/config/otp/app/cancel');
$this->assertSame(302, $client->getResponse()->getStatusCode());
$this->assertStringContainsString('flashes.config.notice.otp_disabled', $client->getContainer()->get(SessionInterface::class)->getFlashBag()->get('notice')[0]);
$user = $em
->getRepository(User::class)
->findOneByUsername('admin');
$this->assertFalse($user->isGoogleTwoFactor());
$this->assertEmpty($user->getBackupCodes());
}
public function testUserDisable2faGoogle()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$crawler = $client->request('GET', '/config/otp/app/disable');
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$this->assertGreaterThan(1, $alert = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.config.notice.otp_disabled', $alert[0]);
// restore user
$em = $this->getEntityManager();

View File

@ -20,6 +20,7 @@ class EntryControllerTest extends WallabagCoreTestCase
public const AN_URL_CONTAINING_AN_ARTICLE_WITH_IMAGE = 'https://www.lemonde.fr/judo/article/2017/11/11/judo-la-decima-de-teddy-riner_5213605_1556020.html';
public $downloadImagesEnabled = false;
public $url = 'https://www.lemonde.fr/pixels/article/2019/06/18/ce-qu-il-faut-savoir-sur-le-libra-la-cryptomonnaie-de-facebook_5477887_4408996.html';
public $wrongUrl = 'wallabagIsAwesome';
private $entryDataTestAttribute = '[data-test="entry"]';
/**
@ -137,9 +138,7 @@ class EntryControllerTest extends WallabagCoreTestCase
$crawler = $client->submit($form);
$this->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertCount(1, $alert = $crawler->filter('form ul li')->extract(['_text']));
$this->assertSame('This value should not be blank.', $alert[0]);
$this->assertSame(302, $client->getResponse()->getStatusCode());
}
/**
@ -423,6 +422,38 @@ class EntryControllerTest extends WallabagCoreTestCase
$em->flush();
}
/**
* @group NetworkCalls
*/
public function testBadFormatURL()
{
$this->logInAs('admin');
$client = $this->getTestClient();
$client->getContainer()->get(Config::class)->set('store_article_headers', 1);
$crawler = $client->request('GET', '/new');
$this->assertSame(200, $client->getResponse()->getStatusCode());
$form = $crawler->filter('form[name=entry]')->form();
$data = [
'entry[url]' => $this->wrongUrl,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$content = $client->getContainer()
->get(EntityManagerInterface::class)
->getRepository(Entry::class)
->findByUrlAndUserId($this->wrongUrl, $this->getLoggedInUserId());
$this->assertFalse($content);
}
public function testArchive()
{
$this->logInAs('admin');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long