mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-27 20:00:31 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ed1b08220 |
+1
-3
@@ -171,9 +171,7 @@ RUN set -eux \
|
|||||||
&& cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \
|
&& cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \
|
||||||
&& echo "Cleaning up image layer" \
|
&& echo "Cleaning up image layer" \
|
||||||
&& rm --force --verbose *.deb \
|
&& rm --force --verbose *.deb \
|
||||||
&& rm --recursive --force --verbose /var/lib/apt/lists/* \
|
&& rm --recursive --force --verbose /var/lib/apt/lists/*
|
||||||
&& echo "Configuring interactive shells to source the s6 container environment" \
|
|
||||||
&& echo '. /etc/profile.d/contenv.sh' >> /etc/bash.bashrc
|
|
||||||
|
|
||||||
WORKDIR /usr/src/paperless/src/
|
WORKDIR /usr/src/paperless/src/
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Source s6 container environment for interactive shells.
|
|
||||||
# Ensures variables resolved from *_FILE secret injection are visible
|
|
||||||
# when using 'docker exec bash'. Does not affect s6 services (those
|
|
||||||
# use with-contenv directly). Has no effect in non-container contexts
|
|
||||||
# because the directory will not exist.
|
|
||||||
# Note: sh/dash shells opened via 'docker exec sh' are not covered;
|
|
||||||
# only bash-based sessions benefit from this file.
|
|
||||||
_pngx_contenv="/run/s6/container_environment"
|
|
||||||
if [ -d "${_pngx_contenv}" ]; then
|
|
||||||
for _pngx_f in "${_pngx_contenv}"/*; do
|
|
||||||
[ -f "${_pngx_f}" ] || continue
|
|
||||||
_pngx_name=$(basename "${_pngx_f}")
|
|
||||||
_pngx_val=$(cat "${_pngx_f}")
|
|
||||||
export "${_pngx_name}=${_pngx_val}"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
unset _pngx_contenv _pngx_f _pngx_name _pngx_val
|
|
||||||
@@ -154,29 +154,11 @@
|
|||||||
& section {
|
& section {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
text-align: initial;
|
text-align: initial;
|
||||||
pointer-events: auto;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
font-size: 1em;
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder)
|
|
||||||
> a:hover {
|
|
||||||
opacity: 0.2;
|
|
||||||
background-color: rgb(255 255 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .annotationTextContent {
|
& .annotationTextContent {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep .textLayer.selecting ~ .annotationLayer section {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { SimpleChange } from '@angular/core'
|
import { SimpleChange } from '@angular/core'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs'
|
import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs'
|
||||||
import {
|
import { PDFSinglePageViewer, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs'
|
||||||
LinkTarget,
|
|
||||||
PDFSinglePageViewer,
|
|
||||||
PDFViewer,
|
|
||||||
} from 'pdfjs-dist/web/pdf_viewer.mjs'
|
|
||||||
import { PngxPdfViewerComponent } from './pdf-viewer.component'
|
import { PngxPdfViewerComponent } from './pdf-viewer.component'
|
||||||
import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types'
|
import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types'
|
||||||
|
|
||||||
@@ -62,16 +58,6 @@ describe('PngxPdfViewerComponent', () => {
|
|||||||
expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
|
expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens external links in a new tab', () => {
|
|
||||||
const linkService = (component as any).linkService
|
|
||||||
expect(linkService.options).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
externalLinkTarget: LinkTarget.BLANK,
|
|
||||||
externalLinkRel: 'noopener noreferrer nofollow',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('resolves the worker source relative to the document base URI', async () => {
|
it('resolves the worker source relative to the document base URI', async () => {
|
||||||
setBaseHref('/paperless/')
|
setBaseHref('/paperless/')
|
||||||
const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument')
|
const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument')
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
} from 'pdfjs-dist/legacy/build/pdf.mjs'
|
} from 'pdfjs-dist/legacy/build/pdf.mjs'
|
||||||
import {
|
import {
|
||||||
EventBus,
|
EventBus,
|
||||||
LinkTarget,
|
|
||||||
PDFFindController,
|
PDFFindController,
|
||||||
PDFLinkService,
|
PDFLinkService,
|
||||||
PDFSinglePageViewer,
|
PDFSinglePageViewer,
|
||||||
@@ -76,11 +75,7 @@ export class PngxPdfViewerComponent
|
|||||||
private lastViewerPage?: number
|
private lastViewerPage?: number
|
||||||
|
|
||||||
private readonly eventBus = new EventBus()
|
private readonly eventBus = new EventBus()
|
||||||
private readonly linkService = new PDFLinkService({
|
private readonly linkService = new PDFLinkService({ eventBus: this.eventBus })
|
||||||
eventBus: this.eventBus,
|
|
||||||
externalLinkTarget: LinkTarget.BLANK,
|
|
||||||
externalLinkRel: 'noopener noreferrer nofollow',
|
|
||||||
})
|
|
||||||
private readonly findController = new PDFFindController({
|
private readonly findController = new PDFFindController({
|
||||||
eventBus: this.eventBus,
|
eventBus: this.eventBus,
|
||||||
linkService: this.linkService,
|
linkService: this.linkService,
|
||||||
|
|||||||
@@ -25,20 +25,10 @@ export class PDFFindController {
|
|||||||
onIsPageVisible?: () => boolean
|
onIsPageVisible?: () => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkTarget = {
|
|
||||||
NONE: 0,
|
|
||||||
SELF: 1,
|
|
||||||
BLANK: 2,
|
|
||||||
PARENT: 3,
|
|
||||||
TOP: 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PDFLinkService {
|
export class PDFLinkService {
|
||||||
private document?: unknown
|
private document?: unknown
|
||||||
private viewer?: unknown
|
private viewer?: unknown
|
||||||
|
|
||||||
constructor(readonly options: Record<string, unknown> = {}) {}
|
|
||||||
|
|
||||||
setDocument(document: unknown): void {
|
setDocument(document: unknown): void {
|
||||||
this.document = document
|
this.document = document
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -857,9 +857,8 @@ class ConsumerPlugin(
|
|||||||
self.log.debug(f"Creation date from parse_date: {create_date}")
|
self.log.debug(f"Creation date from parse_date: {create_date}")
|
||||||
else:
|
else:
|
||||||
stats = Path(self.input_doc.original_file).stat()
|
stats = Path(self.input_doc.original_file).stat()
|
||||||
create_date = datetime.datetime.fromtimestamp(
|
create_date = timezone.make_aware(
|
||||||
stats.st_mtime,
|
datetime.datetime.fromtimestamp(stats.st_mtime),
|
||||||
tz=timezone.get_current_timezone(),
|
|
||||||
)
|
)
|
||||||
self.log.debug(f"Creation date from st_mtime: {create_date}")
|
self.log.debug(f"Creation date from st_mtime: {create_date}")
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ from documents.permissions import get_objects_for_user_owner_aware
|
|||||||
from documents.plugins.helpers import DocumentsStatusManager
|
from documents.plugins.helpers import DocumentsStatusManager
|
||||||
from documents.templating.utils import convert_format_str_to_template_format
|
from documents.templating.utils import convert_format_str_to_template_format
|
||||||
from documents.utils import compute_checksum
|
from documents.utils import compute_checksum
|
||||||
from documents.utils import copy_file_with_basic_stats
|
|
||||||
from documents.workflows.actions import build_workflow_action_context
|
from documents.workflows.actions import build_workflow_action_context
|
||||||
from documents.workflows.actions import execute_email_action
|
from documents.workflows.actions import execute_email_action
|
||||||
from documents.workflows.actions import execute_move_to_trash_action
|
from documents.workflows.actions import execute_move_to_trash_action
|
||||||
@@ -364,11 +363,7 @@ def cleanup_document_deletion(sender, instance, **kwargs) -> None:
|
|||||||
|
|
||||||
logger.debug(f"Moving {instance.source_path} to trash at {new_file_path}")
|
logger.debug(f"Moving {instance.source_path} to trash at {new_file_path}")
|
||||||
try:
|
try:
|
||||||
shutil.move(
|
shutil.move(instance.source_path, new_file_path)
|
||||||
instance.source_path,
|
|
||||||
new_file_path,
|
|
||||||
copy_function=copy_file_with_basic_stats,
|
|
||||||
)
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Failed to move {instance.source_path} to trash at "
|
f"Failed to move {instance.source_path} to trash at "
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-27 14:07+0000\n"
|
"POT-Creation-Date: 2026-09-23 19:00+0000\n"
|
||||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: English\n"
|
"Language-Team: English\n"
|
||||||
@@ -2410,7 +2410,7 @@ msgstr ""
|
|||||||
msgid "Chinese Traditional"
|
msgid "Chinese Traditional"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/urls.py:438
|
#: paperless/urls.py:435
|
||||||
msgid "Paperless-ngx administration"
|
msgid "Paperless-ngx administration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
from allauth.mfa import app_settings as mfa_settings
|
|
||||||
from allauth.mfa.totp.internal import auth as totp_auth
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from paperless_testing.factories import UserFactory
|
|
||||||
|
|
||||||
|
|
||||||
class TestAdminAuth(TestCase):
|
|
||||||
def test_admin_login_redirects_to_allauth(self):
|
|
||||||
user = UserFactory(staff=True, password="testpassword")
|
|
||||||
admin_url = reverse("admin:index")
|
|
||||||
login_url = reverse("admin:login")
|
|
||||||
expected_url = f"{reverse('account_login')}?next={admin_url}"
|
|
||||||
|
|
||||||
response = self.client.get(login_url, {"next": admin_url})
|
|
||||||
self.assertRedirects(response, expected_url)
|
|
||||||
|
|
||||||
response = self.client.post(
|
|
||||||
login_url,
|
|
||||||
{"username": user.username, "password": "testpassword", "next": admin_url},
|
|
||||||
)
|
|
||||||
self.assertRedirects(response, expected_url)
|
|
||||||
self.assertNotIn("_auth_user_id", self.client.session)
|
|
||||||
|
|
||||||
def test_admin_access_requires_totp_for_enrolled_staff(self):
|
|
||||||
user = UserFactory(staff=True, password="testpassword")
|
|
||||||
secret = totp_auth.generate_totp_secret()
|
|
||||||
totp_auth.TOTP.activate(user, secret)
|
|
||||||
admin_url = reverse("admin:index")
|
|
||||||
mfa_url = reverse("mfa_authenticate")
|
|
||||||
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("account_login"),
|
|
||||||
{"login": user.username, "password": "testpassword", "next": admin_url},
|
|
||||||
)
|
|
||||||
self.assertRedirects(response, mfa_url)
|
|
||||||
self.assertNotIn("_auth_user_id", self.client.session)
|
|
||||||
self.assertRedirects(
|
|
||||||
self.client.get(admin_url),
|
|
||||||
f"{reverse('admin:login')}?next={admin_url}",
|
|
||||||
fetch_redirect_response=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.client.post(mfa_url, {"code": "invalid"})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertNotIn("_auth_user_id", self.client.session)
|
|
||||||
|
|
||||||
code = totp_auth.format_hotp_value(
|
|
||||||
totp_auth.hotp_value(secret, int(time.time()) // mfa_settings.TOTP_PERIOD),
|
|
||||||
)
|
|
||||||
response = self.client.post(mfa_url, {"code": code})
|
|
||||||
self.assertRedirects(response, admin_url)
|
|
||||||
self.assertEqual(self.client.session["_auth_user_id"], str(user.pk))
|
|
||||||
|
|
||||||
def test_staff_without_totp_can_still_log_in(self):
|
|
||||||
user = UserFactory(staff=True, password="testpassword")
|
|
||||||
admin_url = reverse("admin:index")
|
|
||||||
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("account_login"),
|
|
||||||
{"login": user.username, "password": "testpassword", "next": admin_url},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertRedirects(response, admin_url)
|
|
||||||
self.assertEqual(self.client.session["_auth_user_id"], str(user.pk))
|
|
||||||
+364
-601
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
|||||||
from allauth.account import views as allauth_account_views
|
from allauth.account import views as allauth_account_views
|
||||||
from allauth.account.decorators import secure_admin_login
|
|
||||||
from allauth.mfa.base import views as allauth_mfa_views
|
from allauth.mfa.base import views as allauth_mfa_views
|
||||||
from allauth.socialaccount import views as allauth_social_account_views
|
from allauth.socialaccount import views as allauth_social_account_views
|
||||||
from allauth.urls import build_provider_urlpatterns
|
from allauth.urls import build_provider_urlpatterns
|
||||||
@@ -69,8 +68,6 @@ from paperless_mail.views import MailRuleViewSet
|
|||||||
from paperless_mail.views import OauthCallbackView
|
from paperless_mail.views import OauthCallbackView
|
||||||
from paperless_mail.views import ProcessedMailViewSet
|
from paperless_mail.views import ProcessedMailViewSet
|
||||||
|
|
||||||
admin.site.login = secure_admin_login(admin.site.login)
|
|
||||||
|
|
||||||
api_router = DefaultRouter()
|
api_router = DefaultRouter()
|
||||||
api_router.register(r"correspondents", CorrespondentViewSet)
|
api_router.register(r"correspondents", CorrespondentViewSet)
|
||||||
api_router.register(r"document_types", DocumentTypeViewSet)
|
api_router.register(r"document_types", DocumentTypeViewSet)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import factory
|
import factory
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from factory.django import DjangoModelFactory
|
from factory.django import DjangoModelFactory
|
||||||
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
@@ -71,6 +72,13 @@ class DocumentFactory(DjangoModelFactory[Document]):
|
|||||||
storage_path = None
|
storage_path = None
|
||||||
|
|
||||||
|
|
||||||
|
class GroupFactory(DjangoModelFactory[Group]):
|
||||||
|
class Meta:
|
||||||
|
model = Group
|
||||||
|
|
||||||
|
name = factory.Sequence(lambda n: f"group{n}")
|
||||||
|
|
||||||
|
|
||||||
class UserFactory(DjangoModelFactory[UserModelT]):
|
class UserFactory(DjangoModelFactory[UserModelT]):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserModelT
|
model = UserModelT
|
||||||
|
|||||||
Reference in New Issue
Block a user