mirror of
https://github.com/kiwix/libkiwix.git
synced 2026-01-10 07:18:04 -05:00
Compare commits
146 Commits
13.1.0
...
feature/ip
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
528a532b27 | ||
|
|
fa410f739c | ||
|
|
5f1f37bd2a | ||
|
|
5855791899 | ||
|
|
4a1498d8df | ||
|
|
f6df2342cf | ||
|
|
8bbda99cab | ||
|
|
95529d2c0a | ||
|
|
b80699916d | ||
|
|
534916929d | ||
|
|
02ab2ce5a5 | ||
|
|
8930095c52 | ||
|
|
bef3ec7694 | ||
|
|
9057686a25 | ||
|
|
723dd977fe | ||
|
|
0b87d4fe04 | ||
|
|
ea31e2f42f | ||
|
|
01bda6b2c0 | ||
|
|
de64a5a724 | ||
|
|
90dd1cb3f0 | ||
|
|
0b14fda94d | ||
|
|
fe965faf1b | ||
|
|
6ad1776242 | ||
|
|
cbfd3ec7c4 | ||
|
|
f6765137e7 | ||
|
|
c24e04c8da | ||
|
|
327fec1877 | ||
|
|
c8524b95bc | ||
|
|
0ac3130b0d | ||
|
|
425ae1efae | ||
|
|
920d603a89 | ||
|
|
f5c91cc272 | ||
|
|
3cdc036858 | ||
|
|
29bfaa5c5b | ||
|
|
bec80e8091 | ||
|
|
2da9801bac | ||
|
|
16ebc6611b | ||
|
|
d5a44b913e | ||
|
|
a63a162c58 | ||
|
|
c29cd8cf3b | ||
|
|
04bf1be9d6 | ||
|
|
59054aa5ad | ||
|
|
1b8dde0115 | ||
|
|
9d0f6a3170 | ||
|
|
c16ed0aa4c | ||
|
|
3d95b386c6 | ||
|
|
a3f5a654f2 | ||
|
|
801b1df304 | ||
|
|
2b8a071c6f | ||
|
|
00fae37f2d | ||
|
|
846404e959 | ||
|
|
fbcd160efd | ||
|
|
196185dd73 | ||
|
|
affb996769 | ||
|
|
418abbcefa | ||
|
|
00867a13f6 | ||
|
|
e096c7e2fd | ||
|
|
69341eab47 | ||
|
|
082727ebb6 | ||
|
|
75a4f8b806 | ||
|
|
2eaa1c4649 | ||
|
|
199a10d093 | ||
|
|
4812fb18f6 | ||
|
|
940818d801 | ||
|
|
5182a66b19 | ||
|
|
b688aa294a | ||
|
|
27ad77c566 | ||
|
|
7677f76854 | ||
|
|
513a8d1383 | ||
|
|
be464a5986 | ||
|
|
c2042c3be8 | ||
|
|
8d480c8b6d | ||
|
|
82ff88f5d8 | ||
|
|
2535f210b3 | ||
|
|
cb0a2c234a | ||
|
|
5a73a75798 | ||
|
|
0ea756c42a | ||
|
|
7108dfa9c2 | ||
|
|
9fd8e81de2 | ||
|
|
566b40a2f8 | ||
|
|
ff6d8a4b30 | ||
|
|
f456ce3e93 | ||
|
|
ece40966f1 | ||
|
|
65a777d4ed | ||
|
|
42295c9010 | ||
|
|
e8afcbe6ae | ||
|
|
c46cd403ae | ||
|
|
af96b19bd1 | ||
|
|
8a00e9383d | ||
|
|
964131ce47 | ||
|
|
97832c8436 | ||
|
|
beab8d7041 | ||
|
|
75bddbf725 | ||
|
|
5927550a36 | ||
|
|
135c6f875d | ||
|
|
83101679a0 | ||
|
|
ae4b652fb2 | ||
|
|
01b94418eb | ||
|
|
a1ce3d10b1 | ||
|
|
b7eadf95bf | ||
|
|
a6cf161341 | ||
|
|
618a718645 | ||
|
|
c2cc4c39f1 | ||
|
|
8477e04ffa | ||
|
|
5345d43017 | ||
|
|
843adb3397 | ||
|
|
4fe4a88574 | ||
|
|
6ee09114eb | ||
|
|
7366938785 | ||
|
|
84405b1318 | ||
|
|
a0c4118fd3 | ||
|
|
72147aec5b | ||
|
|
016072292c | ||
|
|
2964cc5e92 | ||
|
|
8d766335b4 | ||
|
|
5450bcd3c2 | ||
|
|
a0b66eae0c | ||
|
|
22c75245a5 | ||
|
|
f40c3426a5 | ||
|
|
8e6569362c | ||
|
|
eb328ed73d | ||
|
|
21e3c5c19f | ||
|
|
f0927fec49 | ||
|
|
66693cd73e | ||
|
|
be8a60c330 | ||
|
|
8009edd349 | ||
|
|
3733e506c1 | ||
|
|
9fe81e9bce | ||
|
|
4ab6215046 | ||
|
|
ff88430227 | ||
|
|
922c138809 | ||
|
|
fa9ebf55fc | ||
|
|
bc9b5a0354 | ||
|
|
719e947ddf | ||
|
|
e3fffd9b23 | ||
|
|
6ef4f6396e | ||
|
|
d8b4c1584c | ||
|
|
1fc006f639 | ||
|
|
a8368b3a0d | ||
|
|
068555de38 | ||
|
|
0168764f4c | ||
|
|
181893d31a | ||
|
|
5b9daf0d9d | ||
|
|
4e64d26ede | ||
|
|
5e669cd65c | ||
|
|
2749564424 |
143
.github/workflows/ci.yml
vendored
143
.github/workflows/ci.yml
vendored
@@ -14,9 +14,19 @@ jobs:
|
||||
os:
|
||||
- macos-13
|
||||
target:
|
||||
- native_dyn
|
||||
- iOS_arm64
|
||||
- iOS_x86_64
|
||||
- macos-x86_64-dyn
|
||||
- ios-arm64-dyn
|
||||
- ios-x86_64-dyn
|
||||
include:
|
||||
- target: macos-x86_64-dyn
|
||||
arch_name: x86_64-apple-darwin
|
||||
run_test: true
|
||||
- target: ios-arm64-dyn
|
||||
arch_name: aarch64-apple-ios
|
||||
run_test: true
|
||||
- target: ios-x86_64-dyn
|
||||
arch_name: x86-apple-ios-simulator
|
||||
run_test: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
env:
|
||||
@@ -38,66 +48,109 @@ jobs:
|
||||
- name: Install dependencies
|
||||
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
|
||||
with:
|
||||
os_name: macos
|
||||
target_platform: ${{ matrix.target }}
|
||||
|
||||
- name: Compile
|
||||
env:
|
||||
PKG_CONFIG_PATH: ${{env.HOME}}/BUILD_${{matrix.target}}/INSTALL/lib/pkgconfig
|
||||
CPPFLAGS: -I${{env.HOME}}/BUILD_native_dyn/INSTALL/include
|
||||
PKG_CONFIG_PATH: ${{env.HOME}}/BUILD_${{matrix.arch_name}}/INSTALL/lib/pkgconfig
|
||||
CPPFLAGS: -I${{env.HOME}}/BUILD_${{matrix.arch_name}}/INSTALL/include
|
||||
MESON_OPTION: --default-library=shared -Db_coverage=true
|
||||
MESON_CROSSFILE: ${{env.HOME}}/BUILD_${{matrix.target}}/meson_cross_file.txt
|
||||
MESON_CROSSFILE: ${{env.HOME}}/BUILD_${{matrix.arch_name}}/meson_cross_file.txt
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ ! "${{matrix.target}}" =~ native_.* ]]; then
|
||||
if [ -e $MESON_CROSSFILE ]; then
|
||||
MESON_OPTION="$MESON_OPTION --cross-file $MESON_CROSSFILE -Dstatic-linkage=true"
|
||||
fi
|
||||
meson . build ${MESON_OPTION}
|
||||
ninja -C build
|
||||
|
||||
- name: Test libkiwix
|
||||
if: startsWith(matrix.target, 'native_')
|
||||
if: matrix.run_test
|
||||
env:
|
||||
SKIP_BIG_MEMORY_TEST: 1
|
||||
LD_LIBRARY_PATH: ${{env.HOME}}/BUILD_native_dyn/INSTALL/lib:${{env.HOME}}/BUILD_native_dyn/INSTALL/lib64
|
||||
LD_LIBRARY_PATH: ${{env.HOME}}/BUILD_${{matrix.arch_name}}/INSTALL/lib:${{env.HOME}}/BUILD_${{matrix.arch_name}}/INSTALL/lib64
|
||||
run: meson test -C build --verbose
|
||||
|
||||
Windows:
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup python 3.10
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install packages
|
||||
run:
|
||||
choco install pkgconfiglite ninja
|
||||
|
||||
- name: Install python modules
|
||||
run: pip3 install meson
|
||||
|
||||
- name: Setup MSVC compiler
|
||||
uses: bus1/cabuild/action/msdevshell@v1
|
||||
with:
|
||||
architecture: x64
|
||||
|
||||
- name: Install dependencies
|
||||
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
|
||||
with:
|
||||
target_platform: win-x86_64-static
|
||||
|
||||
- name: Compile
|
||||
shell: cmd
|
||||
run: |
|
||||
set PKG_CONFIG_PATH=%cd%\BUILD_win-amd64\INSTALL\lib\pkgconfig
|
||||
set CPPFLAGS=-I%cd%\BUILD_win-amd64\INSTALL\include
|
||||
meson.exe setup . build -Dwerror=false --default-library=static --buildtype=debug
|
||||
cd build
|
||||
ninja.exe
|
||||
|
||||
- name: Test
|
||||
shell: cmd
|
||||
run: |
|
||||
cd build
|
||||
meson.exe test --verbose
|
||||
env:
|
||||
WAIT_TIME_FACTOR_TEST: 10
|
||||
|
||||
Linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name:
|
||||
- native_static
|
||||
- native_dyn
|
||||
- android_arm
|
||||
- android_arm64
|
||||
- win32_static
|
||||
- win32_dyn
|
||||
target:
|
||||
- linux-x86_64-static
|
||||
- linux-x86_64-dyn
|
||||
- android-arm
|
||||
- android-arm64
|
||||
include:
|
||||
- name: native_static
|
||||
target: native_static
|
||||
- target: linux-x86_64-static
|
||||
image_variant: focal
|
||||
lib_postfix: '/x86_64-linux-gnu'
|
||||
- name: native_dyn
|
||||
target: native_dyn
|
||||
arch_name: linux-x86_64
|
||||
run_test: true
|
||||
coverage: true
|
||||
- target: linux-x86_64-dyn
|
||||
image_variant: focal
|
||||
lib_postfix: '/x86_64-linux-gnu'
|
||||
- name: android_arm
|
||||
target: android_arm
|
||||
arch_name: linux-x86_64
|
||||
run_test: true
|
||||
coverage: true
|
||||
- target: android-arm
|
||||
image_variant: focal
|
||||
lib_postfix: '/arm-linux-androideabi'
|
||||
- name: android_arm64
|
||||
target: android_arm64
|
||||
arch_name: arm-linux-androideabi
|
||||
run_test: false
|
||||
coverage: false
|
||||
- target: android-arm64
|
||||
image_variant: focal
|
||||
lib_postfix: '/aarch64-linux-android'
|
||||
- name: win32_static
|
||||
target: win32_static
|
||||
image_variant: f35
|
||||
lib_postfix: '64'
|
||||
- name: win32_dyn
|
||||
target: win32_dyn
|
||||
image_variant: f35
|
||||
lib_postfix: '64'
|
||||
arch_name: aarch64-linux-android
|
||||
run_test: false
|
||||
coverage: false
|
||||
env:
|
||||
HOME: /home/runner
|
||||
runs-on: ubuntu-20.04
|
||||
@@ -114,38 +167,40 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
meson --version
|
||||
if [[ "${{matrix.target}}" =~ .*_dyn ]]; then
|
||||
if [[ "${{matrix.target}}" =~ .*-dyn ]]; then
|
||||
MESON_OPTION="--default-library=shared"
|
||||
else
|
||||
MESON_OPTION="--default-library=static"
|
||||
fi
|
||||
if [[ "${{matrix.target}}" =~ native_.* ]]; then
|
||||
MESON_OPTION="$MESON_OPTION -Db_coverage=true"
|
||||
if [ -e "${{env.HOME}}/BUILD_${{matrix.arch_name}}/meson_cross_file.txt" ]; then
|
||||
MESON_OPTION="$MESON_OPTION --cross-file ${{env.HOME}}/BUILD_${{matrix.arch_name}}/meson_cross_file.txt"
|
||||
else
|
||||
MESON_OPTION="$MESON_OPTION --cross-file $HOME/BUILD_${{matrix.target}}/meson_cross_file.txt"
|
||||
MESON_OPTION="$MESON_OPTION -Db_coverage=true"
|
||||
fi
|
||||
if [[ "${{matrix.target}}" =~ android_.* ]]; then
|
||||
if [[ "${{matrix.target}}" =~ android-.* ]]; then
|
||||
MESON_OPTION="$MESON_OPTION -Dstatic-linkage=true"
|
||||
fi
|
||||
meson . build ${MESON_OPTION}
|
||||
cd build
|
||||
ninja
|
||||
env:
|
||||
PKG_CONFIG_PATH: "/home/runner/BUILD_${{matrix.target}}/INSTALL/lib/pkgconfig:/home/runner/BUILD_${{matrix.target}}/INSTALL/lib${{matrix.lib_postfix}}/pkgconfig"
|
||||
CPPFLAGS: "-I/home/runner/BUILD_${{matrix.target}}/INSTALL/include"
|
||||
PKG_CONFIG_PATH: "/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib/pkgconfig:/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib${{matrix.lib_postfix}}/pkgconfig"
|
||||
CPPFLAGS: "-I/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/include"
|
||||
- name: Test
|
||||
if: startsWith(matrix.target, 'native_')
|
||||
if: matrix.run_test
|
||||
shell: bash
|
||||
run: |
|
||||
cd build
|
||||
meson test --verbose
|
||||
ninja coverage
|
||||
if [[ "${{matrix.coverage}}" = "true" ]]; then
|
||||
ninja coverage
|
||||
fi
|
||||
env:
|
||||
LD_LIBRARY_PATH: "/home/runner/BUILD_${{matrix.target}}/INSTALL/lib:/home/runner/BUILD_${{matrix.target}}/INSTALL/lib${{matrix.lib_postfix}}"
|
||||
LD_LIBRARY_PATH: "/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib:/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib${{matrix.lib_postfix}}"
|
||||
SKIP_BIG_MEMORY_TEST: 1
|
||||
|
||||
- name: Publish coverage
|
||||
if: startsWith(matrix.target, 'native_')
|
||||
if: matrix.coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
52
.github/workflows/package.yml
vendored
52
.github/workflows/package.yml
vendored
@@ -15,11 +15,16 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
distro:
|
||||
- debian-unstable
|
||||
# - debian-unstable
|
||||
# - debian-trixie
|
||||
# - debian-bookworm
|
||||
# - debian-bullseye
|
||||
- ubuntu-noble
|
||||
- ubuntu-jammy
|
||||
- ubuntu-focal
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Determine which PPA we should upload to
|
||||
- name: PPA
|
||||
@@ -34,18 +39,47 @@ jobs:
|
||||
env:
|
||||
REF: ${{ github.ref }}
|
||||
|
||||
- uses: legoktm/gh-action-auto-dch@master
|
||||
- uses: legoktm/gh-action-auto-dch@main
|
||||
with:
|
||||
fullname: Kiwix builder
|
||||
email: release+launchpad@kiwix.org
|
||||
distro: ${{ matrix.distro }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@debian-unstable
|
||||
if: matrix.distro == 'debian-unstable'
|
||||
name: Build package for debian-unstable
|
||||
id: build-debian-unstable
|
||||
# - uses: legoktm/gh-action-build-deb@debian-unstable
|
||||
# if: matrix.distro == 'debian-unstable'
|
||||
# name: Build package for debian-unstable
|
||||
# id: build-debian-unstable
|
||||
# with:
|
||||
# args: --no-sign
|
||||
#
|
||||
# - uses: legoktm/gh-action-build-deb@b47978ba8498dc8b8153cc3b5f99a5fc1afa5de1 # pin@debian-trixie
|
||||
# if: matrix.distro == 'debian-trixie'
|
||||
# name: Build package for debian-trixie
|
||||
# id: build-debian-trixie
|
||||
# with:
|
||||
# args: --no-sign
|
||||
#
|
||||
# - uses: legoktm/gh-action-build-deb@1f4e86a6bb34aaad388167eaf5eb85d553935336 # pin@debian-bookworm
|
||||
# if: matrix.distro == 'debian-bookworm'
|
||||
# name: Build package for debian-bookworm
|
||||
# id: build-debian-bookworm
|
||||
# with:
|
||||
# args: --no-sign
|
||||
#
|
||||
# - uses: legoktm/gh-action-build-deb@084b4263209252ec80a75d2c78a586192c17f18d # pin@debian-bullseye
|
||||
# if: matrix.distro == 'debian-bullseye'
|
||||
# name: Build package for debian-bullseye
|
||||
# id: build-debian-bullseye
|
||||
# with:
|
||||
# args: --no-sign
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@9114a536498b65c40b932209b9833aa942bf108d # pin@ubuntu-noble
|
||||
if: matrix.distro == 'ubuntu-noble'
|
||||
name: Build package for ubuntu-noble
|
||||
id: build-ubuntu-noble
|
||||
with:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-jammy
|
||||
if: matrix.distro == 'ubuntu-jammy'
|
||||
@@ -68,7 +102,7 @@ jobs:
|
||||
name: Packages for ${{ matrix.distro }}
|
||||
path: output
|
||||
|
||||
- uses: legoktm/gh-action-dput@master
|
||||
- uses: legoktm/gh-action-dput@main
|
||||
name: Upload dev package
|
||||
# Only upload on pushes to main
|
||||
if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' && startswith(matrix.distro, 'ubuntu-')
|
||||
@@ -77,7 +111,7 @@ jobs:
|
||||
repository: ppa:kiwixteam/dev
|
||||
packages: output/*_source.changes
|
||||
|
||||
- uses: legoktm/gh-action-dput@master
|
||||
- uses: legoktm/gh-action-dput@main
|
||||
name: Upload release package
|
||||
if: github.event_name == 'release' && startswith(matrix.distro, 'ubuntu-')
|
||||
with:
|
||||
|
||||
28
ChangeLog
28
ChangeLog
@@ -1,3 +1,31 @@
|
||||
libkiwix 14.0.0
|
||||
===============
|
||||
|
||||
* Server:
|
||||
- Support of IPv6 (@veloman-yunkan @aryanA101a #1074 #1093)
|
||||
- Better public IP configuration/detection (@sgourdas #1132)
|
||||
- Fix API errors in catalog searches if Xapian keyword in used (@veloman-yunkan #1137)
|
||||
- Clearly define which Web browsers are supported (@kelson42 @rgaudin @jaifroid @benoit74 #1132)
|
||||
- Improve welcome page download buttons (@veloman-yunkan #1094)
|
||||
- Better handling of external (non-HTTP) links (@veloman-yunkan #1123)
|
||||
- Fix book illustration size on welcome page to 48x48 pixels (@veloman-yunkan #1127)
|
||||
- Remove "Multiple Languages" in language filter (@veloman-yunkan #1098)
|
||||
- Stop transforming tags casing (@kelson42 @veloman-yunkan #1079 #1121)
|
||||
- ZIM file size consistently advertised in MiB (@harsha-mangena #1132)
|
||||
- Few new supported languages in the filter (@kelson42 #1080)
|
||||
- Improve accesskeys (@kelson42 #1075)
|
||||
- Add OpenSearch <link> to head of pages (@kelson42 #1070)
|
||||
* Compilation/Packaging:
|
||||
- Multiple fixes around deb packaging (@kelson42 #1108 #1114 #1135)
|
||||
- Generating of libkiwix.pc via Meson (@veloman-yunkan #1133)
|
||||
- Native Windows CI/CD (@mgautierfr @kelson42 #1113 #1125)
|
||||
- Better check (maximum) libzim version (@kelson42 #1124)
|
||||
- Multiple automated tests improvements (@veloman-yunkan #1068 #1067)
|
||||
* Other:
|
||||
- Deleted supported env. variable `$KIWIX_DATA_DIR` and `kiwix::getDataDirectory()` (@sgourdas #1107)
|
||||
- New string slugification for filenames (@shaopenglin #1105)
|
||||
- Multiple improvements around aria2c download mgmt. (@veloman-yunkan #1097)
|
||||
|
||||
libkiwix 13.1.0
|
||||
===============
|
||||
|
||||
|
||||
17
debian/control
vendored
17
debian/control
vendored
@@ -3,13 +3,12 @@ Priority: optional
|
||||
Maintainer: Kiwix team <kiwix@kiwix.org>
|
||||
Build-Depends: debhelper-compat (= 13),
|
||||
meson,
|
||||
pkg-config,
|
||||
libzim-dev (>= 7.2.0~),
|
||||
pkgconf,
|
||||
libzim-dev (>= 9.0.0), libzim-dev (<< 10.0.0),
|
||||
libcurl4-gnutls-dev,
|
||||
libicu-dev,
|
||||
libgtest-dev,
|
||||
libkainjow-mustache-dev,
|
||||
liblzma-dev,
|
||||
libmicrohttpd-dev,
|
||||
libpugixml-dev,
|
||||
zlib1g-dev
|
||||
@@ -22,12 +21,13 @@ Package: libkiwix-dev
|
||||
Section: libdevel
|
||||
Architecture: any
|
||||
Multi-Arch: same
|
||||
Depends: libkiwix10 (= ${binary:Version}), ${misc:Depends}, python3,
|
||||
libzim-dev (>= 7.2.0~),
|
||||
Depends: libkiwix14 (= ${binary:Version}), ${misc:Depends}, python3,
|
||||
libzim-dev (>= 9.0.0), libzim-dev (<< 10.0.0),
|
||||
libicu-dev,
|
||||
libpugixml-dev,
|
||||
libcurl4-gnutls-dev,
|
||||
libmicrohttpd-dev
|
||||
libmicrohttpd-dev,
|
||||
zlib1g-dev
|
||||
Description: library of common code for Kiwix (development)
|
||||
Kiwix is an offline Wikipedia reader. libkiwix provides the
|
||||
software core for Kiwix, and contains the code shared by all
|
||||
@@ -35,11 +35,12 @@ Description: library of common code for Kiwix (development)
|
||||
.
|
||||
This package contains development files.
|
||||
|
||||
Package: libkiwix10
|
||||
Package: libkiwix14
|
||||
Architecture: any
|
||||
Multi-Arch: same
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}, aria2
|
||||
Conflicts: libkiwix0, libkiwix3, libkiwix9
|
||||
Conflicts: libkiwix0, libkiwix3, libkiwix9, libkiwix10, libkiwix11, libkiwix12, libkiwix13
|
||||
Replaces: libkiwix0, libkiwix3, libkiwix9, libkiwix10, libkiwix11, libkiwix12, libkiwix13
|
||||
Description: library of common code for Kiwix
|
||||
Kiwix is an offline Wikipedia reader. libkiwix provides the
|
||||
software core for Kiwix, and contains the code shared by all
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
enum class IpMode { IPV4, IPV6, ALL, AUTO }; // AUTO: Server decides the protocol
|
||||
typedef zim::size_type size_type;
|
||||
typedef zim::offset_type offset_type;
|
||||
|
||||
|
||||
@@ -168,8 +168,16 @@ class Download {
|
||||
*/
|
||||
class Downloader
|
||||
{
|
||||
public:
|
||||
Downloader();
|
||||
public: // types
|
||||
typedef std::vector<std::pair<std::string, std::string>> Options;
|
||||
|
||||
public: // functions
|
||||
/*
|
||||
* Create a new Downloader object.
|
||||
*
|
||||
* @param sessionFileDir: The directory where aria2 will store its session file.
|
||||
*/
|
||||
explicit Downloader(std::string sessionFileDir);
|
||||
virtual ~Downloader();
|
||||
|
||||
void close();
|
||||
@@ -177,14 +185,22 @@ class Downloader
|
||||
/**
|
||||
* Start a new download.
|
||||
*
|
||||
* This method is thread safe and return a pointer to a newly created `Download`.
|
||||
* This method is thread safe and returns a pointer to a newly created
|
||||
* `Download` or an existing one with a matching URI. In the latter case
|
||||
* the options parameter is ignored, which can lead to surprising results.
|
||||
* For example, if the old and new download requests (sharing the same URI)
|
||||
* have different values for the download directory or output file name
|
||||
* options, after the download is reported to be complete the downloaded file
|
||||
* will be present only at the location specified for the first request.
|
||||
*
|
||||
* User should call `update` on the returned `Download` to have an accurate status.
|
||||
*
|
||||
* @param uri: The uri of the thing to download.
|
||||
* @param downloadDir: The download directory where the thing should be stored (takes precedence over any "dir" in `options`).
|
||||
* @param options: A series of pair <option_name, option_value> to pass to aria.
|
||||
* @return: The newly created Download.
|
||||
*/
|
||||
std::shared_ptr<Download> startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options = {});
|
||||
std::shared_ptr<Download> startDownload(const std::string& uri, const std::string& downloadDir, Options options = {});
|
||||
|
||||
/**
|
||||
* Get a download corrsponding to a download id (did)
|
||||
@@ -206,7 +222,7 @@ class Downloader
|
||||
*/
|
||||
std::vector<std::string> getDownloadIds() const;
|
||||
|
||||
private:
|
||||
private: // data
|
||||
mutable std::mutex m_lock;
|
||||
std::map<std::string, std::shared_ptr<Download>> m_knownDownloads;
|
||||
std::shared_ptr<Aria2> mp_aria;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 Veloman Yunkan <veloman.yunkan@gmail.com>
|
||||
* Copyright 2024 Veloman Yunkan <veloman.yunkan@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,29 +17,15 @@
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef KIWIX_SERVER_I18N
|
||||
#define KIWIX_SERVER_I18N
|
||||
#ifndef KIWIX_I18N
|
||||
#define KIWIX_I18N
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <mustache.hpp>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
struct I18nString {
|
||||
const char* const key;
|
||||
const char* const value;
|
||||
};
|
||||
|
||||
struct I18nStringTable {
|
||||
const char* const lang;
|
||||
const size_t entryCount;
|
||||
const I18nString* const entries;
|
||||
|
||||
const char* get(const std::string& key) const;
|
||||
};
|
||||
|
||||
std::string getTranslatedString(const std::string& lang, const std::string& key);
|
||||
|
||||
namespace i18n
|
||||
@@ -70,28 +56,6 @@ private:
|
||||
const std::string m_lang;
|
||||
};
|
||||
|
||||
class GetTranslatedStringWithMsgId
|
||||
{
|
||||
typedef kainjow::mustache::basic_data<std::string> MustacheString;
|
||||
typedef std::pair<std::string, MustacheString> MsgIdAndTranslation;
|
||||
|
||||
public:
|
||||
explicit GetTranslatedStringWithMsgId(const std::string& lang) : m_lang(lang) {}
|
||||
|
||||
MsgIdAndTranslation operator()(const std::string& key) const
|
||||
{
|
||||
return {key, getTranslatedString(m_lang, key)};
|
||||
}
|
||||
|
||||
MsgIdAndTranslation operator()(const std::string& key, const Parameters& params) const
|
||||
{
|
||||
return {key, expandParameterizedString(m_lang, key, params)};
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string m_lang;
|
||||
};
|
||||
|
||||
} // namespace i18n
|
||||
|
||||
class ParameterizedMessage
|
||||
@@ -121,18 +85,8 @@ inline ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
|
||||
return ParameterizedMessage(msgId, noParams);
|
||||
}
|
||||
|
||||
struct LangPreference
|
||||
{
|
||||
const std::string lang;
|
||||
const float preference;
|
||||
};
|
||||
|
||||
typedef std::vector<LangPreference> UserLangPreferences;
|
||||
|
||||
UserLangPreferences parseUserLanguagePreferences(const std::string& s);
|
||||
|
||||
std::string selectMostSuitableLanguage(const UserLangPreferences& prefs);
|
||||
std::string translateBookCategory(const std::string& lang, const std::string& category);
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
#endif // KIWIX_SERVER_I18N
|
||||
#endif // KIWIX_I18N
|
||||
@@ -10,7 +10,8 @@ headers = [
|
||||
'kiwixserve.h',
|
||||
'name_mapper.h',
|
||||
'tools.h',
|
||||
'version.h'
|
||||
'version.h',
|
||||
'i18n.h'
|
||||
]
|
||||
|
||||
install_headers(headers, subdir:'kiwix')
|
||||
|
||||
@@ -54,6 +54,9 @@ class HumanReadableNameMapper : public NameMapper {
|
||||
virtual ~HumanReadableNameMapper() = default;
|
||||
virtual std::string getNameForId(const std::string& id) const;
|
||||
virtual std::string getIdForName(const std::string& name) const;
|
||||
|
||||
private:
|
||||
void mapName(const kiwix::Library& lib, std::string name, std::string id);
|
||||
};
|
||||
|
||||
class UpdatableNameMapper : public NameMapper {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include "tools.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
@@ -51,7 +52,7 @@ namespace kiwix
|
||||
void stop();
|
||||
|
||||
void setRoot(const std::string& root);
|
||||
void setAddress(const std::string& addr) { m_addr = addr; }
|
||||
void setAddress(const std::string& addr);
|
||||
void setPort(int port) { m_port = port; }
|
||||
void setNbThreads(int threads) { m_nbThreads = threads; }
|
||||
void setMultiZimSearchLimit(unsigned int limit) { m_multizimSearchLimit = limit; }
|
||||
@@ -62,14 +63,16 @@ namespace kiwix
|
||||
{ m_withTaskbar = withTaskbar; m_withLibraryButton = withLibraryButton; }
|
||||
void setBlockExternalLinks(bool blockExternalLinks)
|
||||
{ m_blockExternalLinks = blockExternalLinks; }
|
||||
int getPort();
|
||||
std::string getAddress();
|
||||
void setIpMode(IpMode mode) { m_ipMode = mode; }
|
||||
int getPort() const;
|
||||
IpAddress getAddress() const;
|
||||
IpMode getIpMode() const;
|
||||
|
||||
protected:
|
||||
std::shared_ptr<Library> mp_library;
|
||||
std::shared_ptr<NameMapper> mp_nameMapper;
|
||||
std::string m_root = "";
|
||||
std::string m_addr = "";
|
||||
IpAddress m_addr;
|
||||
std::string m_indexTemplateString = "";
|
||||
int m_port = 80;
|
||||
int m_nbThreads = 1;
|
||||
@@ -78,6 +81,7 @@ namespace kiwix
|
||||
bool m_withTaskbar = true;
|
||||
bool m_withLibraryButton = true;
|
||||
bool m_blockExternalLinks = false;
|
||||
IpMode m_ipMode = IpMode::AUTO;
|
||||
int m_ipConnectionLimit = 0;
|
||||
std::unique_ptr<InternalServer> mp_server;
|
||||
};
|
||||
|
||||
@@ -24,8 +24,17 @@
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <cstdint>
|
||||
#include "common.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
struct IpAddress
|
||||
{
|
||||
std::string addr; // IPv4 address
|
||||
std::string addr6; // IPv6 address
|
||||
};
|
||||
|
||||
namespace kiwix {
|
||||
typedef std::pair<std::string, std::string> LangNameCodePair;
|
||||
typedef std::vector<LangNameCodePair> FeedLanguages;
|
||||
typedef std::vector<std::string> FeedCategories;
|
||||
@@ -37,26 +46,6 @@ typedef std::vector<std::string> FeedCategories;
|
||||
*/
|
||||
std::string getCurrentDirectory();
|
||||
|
||||
/**
|
||||
* Return the data directory.
|
||||
*
|
||||
* The data directory is a directory where to put data (zim files, ...)
|
||||
* It depends of the platform and it may be changed by user using environment variable.
|
||||
*
|
||||
* The resolution order is :
|
||||
* - `KIWIX_DATA_DIR` env variable (if set).
|
||||
* - On Windows :
|
||||
* . `$APPDATA/kiwix` if $APPDATA is set
|
||||
* . `$USERPROFILE/kiwix` if $USERPROFILE is set
|
||||
* - Else :
|
||||
* . `$XDG_DATA_HOME/kiwix`if $XDG_DATA_HOME is set
|
||||
* . `$HOME/.local/share/kiwx` if $HOWE is set
|
||||
* - current directory
|
||||
*
|
||||
* @return the path of the data directory (utf8 encoded)
|
||||
*/
|
||||
std::string getDataDirectory();
|
||||
|
||||
/** Return the path of the executable
|
||||
*
|
||||
* Some application may be packaged in auto extractible archive (Appimage) and the
|
||||
@@ -210,13 +199,30 @@ bool fileReadable(const std::string& path);
|
||||
std::string getMimeTypeForFile(const std::string& filename);
|
||||
|
||||
/** Provides all available network interfaces
|
||||
*
|
||||
* This function provides the available IPv4 and IPv6 network interfaces
|
||||
* as a map from the interface name to its IPv4 and/or IPv6 address(es).
|
||||
*/
|
||||
std::map<std::string, IpAddress> getNetworkInterfacesIPv4Or6();
|
||||
|
||||
/** Provides all available IPv4 network interfaces
|
||||
*
|
||||
* This function provides the available IPv4 network interfaces
|
||||
* as a map from the interface name to its IPv4 address.
|
||||
*
|
||||
* Provided for backward compatibility with libkiwix v13.1.0.
|
||||
*/
|
||||
std::map<std::string, std::string> getNetworkInterfaces();
|
||||
|
||||
/** Provides the best IP address
|
||||
* This function provides the best IP address from the list given by getNetworkInterfaces
|
||||
* This function provides the best IP addresses for both ipv4 and ipv6 protocols,
|
||||
* in an IpAddress struct, based on the list given by getNetworkInterfacesIPv4Or6()
|
||||
*/
|
||||
IpAddress getBestPublicIps();
|
||||
|
||||
/** Provides the best IPv4 adddress
|
||||
* Equivalent to getBestPublicIp(false). Provided for backward compatibility
|
||||
* with libkiwix v13.1.0.
|
||||
*/
|
||||
std::string getBestPublicIp();
|
||||
|
||||
@@ -231,15 +237,15 @@ std::string beautifyFileSize(uint64_t number);
|
||||
|
||||
/**
|
||||
* Load languages stored in an OPDS stream.
|
||||
*
|
||||
*
|
||||
* @param content the OPDS stream.
|
||||
* @return vector containing pairs of language code and their corresponding full language name.
|
||||
* @return vector containing pairs of language code and their corresponding full language name.
|
||||
*/
|
||||
FeedLanguages readLanguagesFromFeed(const std::string& content);
|
||||
|
||||
/**
|
||||
* Load categories stored in an OPDS stream .
|
||||
*
|
||||
*
|
||||
* @param content the OPDS stream.
|
||||
* @return vector containing category strings.
|
||||
*/
|
||||
@@ -247,10 +253,19 @@ FeedCategories readCategoriesFromFeed(const std::string& content);
|
||||
|
||||
/**
|
||||
* Retrieve the full language name associated with a given ISO 639-3 language code.
|
||||
*
|
||||
*
|
||||
* @param lang ISO 639-3 language code.
|
||||
* @return full language name.
|
||||
*/
|
||||
std::string getLanguageSelfName(const std::string& lang);
|
||||
|
||||
/**
|
||||
* Slugifies the filename by converting any characters reserved by the operating
|
||||
* system to '_'. Note filename is only the file name and not a path.
|
||||
*
|
||||
* @param filename Valid UTF-8 encoded file name string.
|
||||
* @return slugified string.
|
||||
*/
|
||||
std::string getSlugifiedFileName(const std::string& filename);
|
||||
}
|
||||
#endif // KIWIX_TOOLS_H
|
||||
|
||||
10
kiwix.pc.in
10
kiwix.pc.in
@@ -1,10 +0,0 @@
|
||||
prefix=@prefix@
|
||||
libdir=${prefix}/lib64
|
||||
includedir=${prefix}/include
|
||||
|
||||
Name: libkiwix
|
||||
Description: A library that contains a lot of things used by used by other kiwix programs
|
||||
Version: @version@
|
||||
Requires: @requires@
|
||||
Libs: -L${libdir} -lkiwix @extra_libs@
|
||||
Cflags: -I${includedir}/ @extra_cflags@
|
||||
40
meson.build
40
meson.build
@@ -1,5 +1,5 @@
|
||||
project('libkiwix', 'cpp',
|
||||
version : '13.1.0',
|
||||
version : '14.0.0',
|
||||
license : 'GPLv3+',
|
||||
default_options : ['c_std=c11', 'cpp_std=c++17', 'werror=true'])
|
||||
|
||||
@@ -35,9 +35,10 @@ else
|
||||
error('Cannot found header mustache.hpp')
|
||||
endif
|
||||
|
||||
libzim_dep = dependency('libzim', version : '>=8.1.0', static:static_deps)
|
||||
libzim_dep = dependency('libzim', version:['>=9.0.0', '<10.0.0'], static:static_deps)
|
||||
|
||||
if not compiler.has_header_symbol('zim/zim.h', 'LIBZIM_WITH_XAPIAN', dependencies: libzim_dep)
|
||||
error('Libzim seems to be compiled without xapian. Xapian support is mandatory.')
|
||||
error('Libzim seems to be compiled without Xapian. Xapian support is mandatory.')
|
||||
endif
|
||||
|
||||
|
||||
@@ -49,8 +50,14 @@ endif
|
||||
|
||||
if host_machine.system() == 'windows'
|
||||
add_project_arguments('-DNOMINMAX', language: 'cpp')
|
||||
extra_libs += ['-liphlpapi']
|
||||
endif
|
||||
|
||||
if build_machine.system() == 'windows'
|
||||
extra_libs += ['-lshlwapi', '-lwinmm']
|
||||
endif
|
||||
|
||||
|
||||
all_deps = [thread_dep, libicu_dep, libzim_dep, pugixml_dep, libcurl_dep, microhttpd_dep, zlib_dep, xapian_dep]
|
||||
|
||||
inc = include_directories('include', extra_include)
|
||||
@@ -58,12 +65,6 @@ inc = include_directories('include', extra_include)
|
||||
conf = configuration_data()
|
||||
conf.set('LIBKIWIX_VERSION', '"@0@"'.format(meson.project_version()))
|
||||
|
||||
if build_machine.system() == 'windows'
|
||||
extra_link_args = ['-lshlwapi', '-lwinmm']
|
||||
else
|
||||
extra_link_args = []
|
||||
endif
|
||||
|
||||
subdir('include')
|
||||
subdir('scripts')
|
||||
subdir('static')
|
||||
@@ -73,17 +74,10 @@ if get_option('doc')
|
||||
subdir('docs')
|
||||
endif
|
||||
|
||||
pkg_requires = ['libzim', 'icu-i18n', 'pugixml', 'libcurl', 'libmicrohttpd', 'xapian-core']
|
||||
|
||||
pkg_conf = configuration_data()
|
||||
pkg_conf.set('prefix', get_option('prefix'))
|
||||
pkg_conf.set('requires', ' '.join(pkg_requires))
|
||||
pkg_conf.set('extra_libs', ' '.join(extra_libs))
|
||||
pkg_conf.set('extra_cflags', extra_cflags)
|
||||
pkg_conf.set('version', meson.project_version())
|
||||
configure_file(output : 'kiwix.pc',
|
||||
configuration : pkg_conf,
|
||||
input : 'kiwix.pc.in',
|
||||
install_dir: get_option('libdir')+'/pkgconfig'
|
||||
)
|
||||
|
||||
pkg_mod = import('pkgconfig')
|
||||
pkg_mod.generate(libraries : [libkiwix] + extra_libs,
|
||||
version : meson.project_version(),
|
||||
name : 'libkiwix',
|
||||
filebase : 'libkiwix',
|
||||
description : 'A library that contains useful primitives that Kiwix readers have in common',
|
||||
extra_cflags: extra_cflags)
|
||||
|
||||
@@ -61,7 +61,7 @@ lang_table_entry_cxx_template = '''
|
||||
|
||||
cxxfile_template = '''// This file is automatically generated. Do not modify it.
|
||||
|
||||
#include "server/i18n.h"
|
||||
#include "server/i18n_utils.h"
|
||||
|
||||
namespace kiwix {
|
||||
namespace i18n {
|
||||
|
||||
@@ -61,6 +61,32 @@ resource_decl_template = """{namespaces_open}
|
||||
extern const std::string {identifier};
|
||||
{namespaces_close}"""
|
||||
|
||||
BINARY_RESOURCE_EXTENSIONS = {'.ico', '.png', '.ttf'}
|
||||
|
||||
TEXT_RESOURCE_EXTENSIONS = {
|
||||
'.css',
|
||||
'.html',
|
||||
'.js',
|
||||
'.json',
|
||||
'.svg',
|
||||
'.tmpl',
|
||||
'.webmanifest',
|
||||
'.xml',
|
||||
}
|
||||
|
||||
if not BINARY_RESOURCE_EXTENSIONS.isdisjoint(TEXT_RESOURCE_EXTENSIONS):
|
||||
raise RuntimeError(f"The following file type extensions are declared to be both binary and text: {BINARY_RESOURCE_EXTENSIONS.intersection(TEXT_RESOURCE_EXTENSIONS)}")
|
||||
|
||||
def is_binary_resource(filename):
|
||||
_, extension = os.path.splitext(filename)
|
||||
is_binary = extension in BINARY_RESOURCE_EXTENSIONS
|
||||
is_text = extension in TEXT_RESOURCE_EXTENSIONS
|
||||
if not is_binary and not is_text:
|
||||
# all file type extensions of static resources must be listed
|
||||
# in either BINARY_RESOURCE_EXTENSIONS or TEXT_RESOURCE_EXTENSIONS
|
||||
raise RuntimeError(f"Unknown file type extension: {extension}")
|
||||
return is_binary
|
||||
|
||||
class Resource:
|
||||
def __init__(self, base_dirs, filename, cacheid=None):
|
||||
filename = filename
|
||||
@@ -72,6 +98,8 @@ class Resource:
|
||||
try:
|
||||
with open(os.path.join(base_dir, filename), 'rb') as f:
|
||||
self.data = f.read()
|
||||
if not is_binary_resource(filename):
|
||||
self.data = self.data.replace(b"\r\n", b"\n")
|
||||
found = True
|
||||
break
|
||||
except FileNotFoundError:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "xmlrpc.h"
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
@@ -29,18 +30,41 @@
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
Aria2::Aria2():
|
||||
namespace {
|
||||
|
||||
void pauseAnyActiveDownloads(const std::string& ariaSessionFilePath)
|
||||
{
|
||||
std::ifstream inputFile(ariaSessionFilePath);
|
||||
if ( !inputFile )
|
||||
return;
|
||||
|
||||
std::ostringstream ss;
|
||||
std::string line;
|
||||
while ( std::getline(inputFile, line) ) {
|
||||
if ( !startsWith(line, " pause=") ) {
|
||||
ss << line << "\n";
|
||||
}
|
||||
if ( !line.empty() && line[0] != ' ' && line[0] != '#' ) {
|
||||
ss << " pause=true\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream outputFile(ariaSessionFilePath);
|
||||
outputFile << ss.str();
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
Aria2::Aria2(std::string sessionFileDir):
|
||||
mp_aria(nullptr),
|
||||
m_port(42042),
|
||||
m_secret(getNewRpcSecret())
|
||||
{
|
||||
m_downloadDir = getDataDirectory();
|
||||
makeDirectory(m_downloadDir);
|
||||
std::vector<const char*> callCmd;
|
||||
|
||||
std::string rpc_port = "--rpc-listen-port=" + to_string(m_port);
|
||||
std::string download_dir = "--dir=" + getDataDirectory();
|
||||
std::string session_file = appendToDirectory(getDataDirectory(), "kiwix.session");
|
||||
std::string session_file = appendToDirectory(sessionFileDir, "kiwix.session");
|
||||
pauseAnyActiveDownloads(session_file);
|
||||
std::string session = "--save-session=" + session_file;
|
||||
std::string inputFile = "--input-file=" + session_file;
|
||||
// std::string log_dir = "--log=\"" + logDir + "\"";
|
||||
@@ -67,7 +91,6 @@ Aria2::Aria2():
|
||||
callCmd.push_back("--enable-rpc");
|
||||
callCmd.push_back(rpc_secret.c_str());
|
||||
callCmd.push_back(rpc_port.c_str());
|
||||
callCmd.push_back(download_dir.c_str());
|
||||
if (fileReadable(session_file)) {
|
||||
callCmd.push_back(inputFile.c_str());
|
||||
}
|
||||
@@ -97,20 +120,30 @@ Aria2::Aria2():
|
||||
curl_easy_setopt(p_curl, CURLOPT_PORT, m_port);
|
||||
curl_easy_setopt(p_curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(p_curl, CURLOPT_ERRORBUFFER, curlErrorBuffer);
|
||||
curl_easy_setopt(p_curl, CURLOPT_TIMEOUT_MS, 100);
|
||||
|
||||
int watchdog = 50;
|
||||
while(--watchdog) {
|
||||
typedef std::chrono::duration<double> Seconds;
|
||||
|
||||
const double MAX_WAITING_TIME_SECONDS = 0.5;
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
bool maxWaitingTimeWasExceeded = false;
|
||||
|
||||
CURLcode res = CURLE_OK;
|
||||
while ( !maxWaitingTimeWasExceeded ) {
|
||||
sleep(10);
|
||||
curlErrorBuffer[0] = 0;
|
||||
auto res = curl_easy_perform(p_curl);
|
||||
res = curl_easy_perform(p_curl);
|
||||
if (res == CURLE_OK) {
|
||||
break;
|
||||
} else if (watchdog == 1) {
|
||||
LOG_ARIA_ERROR();
|
||||
}
|
||||
|
||||
const auto dt = std::chrono::steady_clock::now() - t0;
|
||||
const double elapsedTime = std::chrono::duration_cast<Seconds>(dt).count();
|
||||
maxWaitingTimeWasExceeded = elapsedTime > MAX_WAITING_TIME_SECONDS;
|
||||
}
|
||||
curl_easy_cleanup(p_curl);
|
||||
if (!watchdog) {
|
||||
if ( maxWaitingTimeWasExceeded ) {
|
||||
LOG_ARIA_ERROR();
|
||||
throw std::runtime_error("Cannot connect to aria2c rpc. Aria2c launch cmd : " + launchCmd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,10 @@ class Aria2
|
||||
std::unique_ptr<Subprocess> mp_aria;
|
||||
int m_port;
|
||||
std::string m_secret;
|
||||
std::string m_downloadDir;
|
||||
std::string doRequest(const MethodCall& methodCall);
|
||||
|
||||
public:
|
||||
Aria2();
|
||||
explicit Aria2(std::string sessionFileDir);
|
||||
virtual ~Aria2() = default;
|
||||
void close();
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "downloader.h"
|
||||
#include "tools.h"
|
||||
#include "tools/pathTools.h"
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
@@ -124,8 +125,8 @@ void Download::cancelDownload()
|
||||
}
|
||||
|
||||
/* Constructor */
|
||||
Downloader::Downloader() :
|
||||
mp_aria(new Aria2())
|
||||
Downloader::Downloader(std::string sessionFileDir) :
|
||||
mp_aria(new Aria2(sessionFileDir))
|
||||
{
|
||||
try {
|
||||
for (auto gid : mp_aria->tellWaiting()) {
|
||||
@@ -150,11 +151,20 @@ Downloader::Downloader() :
|
||||
/* Destructor */
|
||||
Downloader::~Downloader()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
void Downloader::close()
|
||||
{
|
||||
mp_aria->close();
|
||||
if ( mp_aria ) {
|
||||
try {
|
||||
mp_aria->close();
|
||||
} catch (const std::exception& err) {
|
||||
std::cerr << "ERROR: Failed to save the downloader state: "
|
||||
<< err.what() << std::endl;
|
||||
}
|
||||
mp_aria.reset();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> Downloader::getDownloadIds() const {
|
||||
@@ -166,13 +176,49 @@ std::vector<std::string> Downloader::getDownloadIds() const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::shared_ptr<Download> Downloader::startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options)
|
||||
namespace
|
||||
{
|
||||
|
||||
bool downloadCanBeReused(const Download& d,
|
||||
const std::string& uri,
|
||||
const Downloader::Options& /*options*/)
|
||||
{
|
||||
const auto& uris = d.getUris();
|
||||
const bool sameURI = std::find(uris.begin(), uris.end(), uri) != uris.end();
|
||||
|
||||
if ( !sameURI )
|
||||
return false;
|
||||
|
||||
switch ( d.getStatus() ) {
|
||||
case Download::K_ERROR:
|
||||
case Download::K_UNKNOWN:
|
||||
case Download::K_REMOVED:
|
||||
return false;
|
||||
|
||||
case Download::K_ACTIVE:
|
||||
case Download::K_WAITING:
|
||||
case Download::K_PAUSED:
|
||||
return true; // XXX: what if options are different?
|
||||
|
||||
case Download::K_COMPLETE:
|
||||
return fileExists(d.getPath()); // XXX: what if options are different?
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::shared_ptr<Download> Downloader::startDownload(const std::string& uri, const std::string& downloadDir, Options options)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
options.erase(std::remove_if(options.begin(), options.end(), [](const auto& option) {
|
||||
return option.first == "dir";
|
||||
}), options.end());
|
||||
options.push_back({"dir", downloadDir});
|
||||
for (auto& p: m_knownDownloads) {
|
||||
auto& d = p.second;
|
||||
auto& uris = d->getUris();
|
||||
if (std::find(uris.begin(), uris.end(), uri) != uris.end())
|
||||
if ( downloadCanBeReused(*d, uri, options) )
|
||||
return d;
|
||||
}
|
||||
std::vector<std::string> uris = {uri};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "tools/otherTools.h"
|
||||
#include "tools.h"
|
||||
#include "tools/regexTools.h"
|
||||
#include "server/i18n.h"
|
||||
#include "server/i18n_utils.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
@@ -77,7 +77,7 @@ std::string HTMLDumper::dumpPlainHTML(kiwix::Filter filter) const
|
||||
const auto tags = bookObj.getTags();
|
||||
const auto downloadAvailable = (bookObj.getUrl() != "");
|
||||
std::string faviconAttr = "style=background-image:url(" + bookIconUrl + ")";
|
||||
|
||||
|
||||
booksData.push_back(kainjow::mustache::object{
|
||||
{"id", contentId},
|
||||
{"title", bookTitle},
|
||||
|
||||
@@ -645,8 +645,6 @@ Xapian::Query buildXapianQueryFromFilterQuery(const Filter& filter)
|
||||
//queryParser.set_stemmer(Xapian::Stem(iso639_3ToXapian(???)));
|
||||
//queryParser.set_stemming_strategy(Xapian::QueryParser::STEM_SOME);
|
||||
const auto flags = Xapian::QueryParser::FLAG_PHRASE
|
||||
| Xapian::QueryParser::FLAG_BOOLEAN
|
||||
| Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE
|
||||
| Xapian::QueryParser::FLAG_LOVEHATE
|
||||
| Xapian::QueryParser::FLAG_WILDCARD
|
||||
| partialQueryFlag;
|
||||
|
||||
@@ -29,28 +29,32 @@ HumanReadableNameMapper::HumanReadableNameMapper(kiwix::Library& library, bool w
|
||||
auto& currentBook = library.getBookById(bookId);
|
||||
auto bookName = currentBook.getHumanReadableIdFromPath();
|
||||
m_idToName[bookId] = bookName;
|
||||
m_nameToId[bookName] = bookId;
|
||||
mapName(library, bookName, bookId);
|
||||
|
||||
if (!withAlias)
|
||||
continue;
|
||||
|
||||
auto aliasName = replaceRegex(bookName, "", "_[[:digit:]]{4}-[[:digit:]]{2}$");
|
||||
if (aliasName == bookName) {
|
||||
continue;
|
||||
}
|
||||
if (m_nameToId.find(aliasName) == m_nameToId.end()) {
|
||||
m_nameToId[aliasName] = bookId;
|
||||
} else {
|
||||
auto alreadyPresentPath = library.getBookById(m_nameToId[aliasName]).getPath();
|
||||
std::cerr << "Path collision: " << alreadyPresentPath
|
||||
<< " and " << currentBook.getPath()
|
||||
<< " can't share the same URL path '" << aliasName << "'."
|
||||
<< " Therefore, only " << alreadyPresentPath
|
||||
<< " will be served." << std::endl;
|
||||
if (aliasName != bookName) {
|
||||
mapName(library, aliasName, bookId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HumanReadableNameMapper::mapName(const Library& library, std::string name, std::string bookId) {
|
||||
if (m_nameToId.find(name) == m_nameToId.end()) {
|
||||
m_nameToId[name] = bookId;
|
||||
} else {
|
||||
const auto& currentBook = library.getBookById(bookId);
|
||||
auto alreadyPresentPath = library.getBookById(m_nameToId[name]).getPath();
|
||||
std::cerr << "Path collision: '" << alreadyPresentPath
|
||||
<< "' and '" << currentBook.getPath()
|
||||
<< "' can't share the same URL path '" << name << "'."
|
||||
<< " Therefore, only '" << alreadyPresentPath
|
||||
<< "' will be served." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
std::string HumanReadableNameMapper::getNameForId(const std::string& id) const {
|
||||
return m_idToName.at(id);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
#include "libkiwix-resources.h"
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
#include "server/i18n.h"
|
||||
#include "server/i18n_utils.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
@@ -51,6 +51,7 @@ bool Server::start() {
|
||||
m_withTaskbar,
|
||||
m_withLibraryButton,
|
||||
m_blockExternalLinks,
|
||||
m_ipMode,
|
||||
m_indexTemplateString,
|
||||
m_ipConnectionLimit));
|
||||
return mp_server->start();
|
||||
@@ -74,14 +75,33 @@ void Server::setRoot(const std::string& root)
|
||||
}
|
||||
}
|
||||
|
||||
int Server::getPort()
|
||||
void Server::setAddress(const std::string& addr)
|
||||
{
|
||||
m_addr.addr.clear();
|
||||
m_addr.addr6.clear();
|
||||
|
||||
if (addr.empty()) return;
|
||||
|
||||
if (addr.find(':') != std::string::npos) { // IPv6
|
||||
m_addr.addr6 = (addr[0] == '[') ? addr.substr(1, addr.length() - 2) : addr; // Remove brackets if any
|
||||
} else {
|
||||
m_addr.addr = addr;
|
||||
}
|
||||
}
|
||||
|
||||
int Server::getPort() const
|
||||
{
|
||||
return mp_server->getPort();
|
||||
}
|
||||
|
||||
std::string Server::getAddress()
|
||||
IpAddress Server::getAddress() const
|
||||
{
|
||||
return mp_server->getAddress();
|
||||
}
|
||||
|
||||
IpMode Server::getIpMode() const
|
||||
{
|
||||
return mp_server->getIpMode();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "i18n.h"
|
||||
#include "i18n_utils.h"
|
||||
|
||||
#include "tools/otherTools.h"
|
||||
|
||||
@@ -193,4 +193,13 @@ std::string selectMostSuitableLanguage(const UserLangPreferences& prefs)
|
||||
return bestLangSoFar;
|
||||
}
|
||||
|
||||
std::string translateBookCategory(const std::string& lang, const std::string& category)
|
||||
{
|
||||
try {
|
||||
return getTranslatedString(lang, "book-category." + category);
|
||||
} catch (...) {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
83
src/server/i18n_utils.h
Normal file
83
src/server/i18n_utils.h
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2022 Veloman Yunkan <veloman.yunkan@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef KIWIX_SERVER_I18N_UTILS
|
||||
#define KIWIX_SERVER_I18N_UTILS
|
||||
|
||||
#include "i18n.h"
|
||||
#include <mustache.hpp>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
struct I18nString {
|
||||
const char* const key;
|
||||
const char* const value;
|
||||
};
|
||||
|
||||
struct I18nStringTable {
|
||||
const char* const lang;
|
||||
const size_t entryCount;
|
||||
const I18nString* const entries;
|
||||
|
||||
const char* get(const std::string& key) const;
|
||||
};
|
||||
|
||||
namespace i18n
|
||||
{
|
||||
|
||||
class GetTranslatedStringWithMsgId
|
||||
{
|
||||
typedef kainjow::mustache::basic_data<std::string> MustacheString;
|
||||
typedef std::pair<std::string, MustacheString> MsgIdAndTranslation;
|
||||
|
||||
public:
|
||||
explicit GetTranslatedStringWithMsgId(const std::string& lang) : m_lang(lang) {}
|
||||
|
||||
MsgIdAndTranslation operator()(const std::string& key) const
|
||||
{
|
||||
return {key, getTranslatedString(m_lang, key)};
|
||||
}
|
||||
|
||||
MsgIdAndTranslation operator()(const std::string& key, const Parameters& params) const
|
||||
{
|
||||
return {key, expandParameterizedString(m_lang, key, params)};
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string m_lang;
|
||||
};
|
||||
|
||||
} // namespace i18n
|
||||
|
||||
struct LangPreference
|
||||
{
|
||||
const std::string lang;
|
||||
const float preference;
|
||||
};
|
||||
|
||||
typedef std::vector<LangPreference> UserLangPreferences;
|
||||
|
||||
UserLangPreferences parseUserLanguagePreferences(const std::string& s);
|
||||
|
||||
std::string selectMostSuitableLanguage(const UserLangPreferences& prefs);
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
#endif // KIWIX_SERVER_I18N_UTILS
|
||||
@@ -54,7 +54,7 @@ extern "C" {
|
||||
#include "search_renderer.h"
|
||||
#include "opds_dumper.h"
|
||||
#include "html_dumper.h"
|
||||
#include "i18n.h"
|
||||
#include "i18n_utils.h"
|
||||
|
||||
#include <zim/uuid.h>
|
||||
#include <zim/error.h>
|
||||
@@ -85,6 +85,20 @@ namespace kiwix {
|
||||
namespace
|
||||
{
|
||||
|
||||
bool ipAvailable(const std::string addr)
|
||||
{
|
||||
auto interfaces = kiwix::getNetworkInterfacesIPv4Or6();
|
||||
|
||||
for (const auto& kv : interfaces) {
|
||||
const auto& interfaceIps = kv.second;
|
||||
if ((interfaceIps.addr == addr) || (interfaceIps.addr6 == addr)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
inline std::string normalizeRootUrl(std::string rootUrl)
|
||||
{
|
||||
while ( !rootUrl.empty() && rootUrl.back() == '/' )
|
||||
@@ -407,7 +421,7 @@ public:
|
||||
|
||||
InternalServer::InternalServer(LibraryPtr library,
|
||||
std::shared_ptr<NameMapper> nameMapper,
|
||||
std::string addr,
|
||||
IpAddress addr,
|
||||
int port,
|
||||
std::string root,
|
||||
int nbThreads,
|
||||
@@ -416,6 +430,7 @@ InternalServer::InternalServer(LibraryPtr library,
|
||||
bool withTaskbar,
|
||||
bool withLibraryButton,
|
||||
bool blockExternalLinks,
|
||||
IpMode ipMode,
|
||||
std::string indexTemplateString,
|
||||
int ipConnectionLimit) :
|
||||
m_addr(addr),
|
||||
@@ -428,6 +443,7 @@ InternalServer::InternalServer(LibraryPtr library,
|
||||
m_withTaskbar(withTaskbar),
|
||||
m_withLibraryButton(withLibraryButton),
|
||||
m_blockExternalLinks(blockExternalLinks),
|
||||
m_ipMode(ipMode),
|
||||
m_indexTemplateString(indexTemplateString.empty() ? RESOURCE::templates::index_html : indexTemplateString),
|
||||
m_ipConnectionLimit(ipConnectionLimit),
|
||||
mp_daemon(nullptr),
|
||||
@@ -451,36 +467,98 @@ bool InternalServer::start() {
|
||||
if (m_verbose.load())
|
||||
flags |= MHD_USE_DEBUG;
|
||||
|
||||
struct sockaddr_in sockAddr;
|
||||
memset(&sockAddr, 0, sizeof(sockAddr));
|
||||
sockAddr.sin_family = AF_INET;
|
||||
sockAddr.sin_port = htons(m_port);
|
||||
if (m_addr.empty()) {
|
||||
if (0 != INADDR_ANY) {
|
||||
sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
}
|
||||
m_addr = kiwix::getBestPublicIp();
|
||||
|
||||
struct sockaddr_in sockAddr4={0};
|
||||
sockAddr4.sin_family = AF_INET;
|
||||
sockAddr4.sin_port = htons(m_port);
|
||||
struct sockaddr_in6 sockAddr6={0};
|
||||
sockAddr6.sin6_family = AF_INET6;
|
||||
sockAddr6.sin6_port = htons(m_port);
|
||||
|
||||
if (m_addr.addr.empty() && m_addr.addr6.empty()) { // No ip address provided
|
||||
if (m_ipMode == IpMode::AUTO) m_ipMode = IpMode::ALL;
|
||||
sockAddr6.sin6_addr = in6addr_any;
|
||||
sockAddr4.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
IpAddress bestIps = kiwix::getBestPublicIps();
|
||||
if (m_ipMode == IpMode::IPV4 || m_ipMode == IpMode::ALL) m_addr.addr = bestIps.addr;
|
||||
if (m_ipMode == IpMode::IPV6 || m_ipMode == IpMode::ALL) m_addr.addr6 = bestIps.addr6;
|
||||
} else {
|
||||
if (inet_pton(AF_INET, m_addr.c_str(), &(sockAddr.sin_addr.s_addr)) == 0) {
|
||||
std::cerr << "Ip address " << m_addr << " is not a valid ip address" << std::endl;
|
||||
const std::string addr = !m_addr.addr.empty() ? m_addr.addr : m_addr.addr6;
|
||||
|
||||
if (m_ipMode != kiwix::IpMode::AUTO) {
|
||||
std::cerr << "ERROR: When an IP address is provided the IP mode must not be set" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool validV4 = inet_pton(AF_INET, m_addr.addr.c_str(), &(sockAddr4.sin_addr.s_addr)) == 1;
|
||||
bool validV6 = inet_pton(AF_INET6, m_addr.addr6.c_str(), &(sockAddr6.sin6_addr.s6_addr)) == 1;
|
||||
|
||||
if (!validV4 && !validV6) {
|
||||
std::cerr << "ERROR: invalid IP address: " << addr << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ipAvailable(addr)) {
|
||||
std::cerr << "ERROR: IP address is not available on this system: " << addr << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ipMode = !m_addr.addr.empty() ? IpMode::IPV4 : IpMode::IPV6;
|
||||
}
|
||||
|
||||
if (m_ipMode == IpMode::ALL) {
|
||||
flags|=MHD_USE_DUAL_STACK;
|
||||
} else if (m_ipMode == IpMode::IPV6) {
|
||||
flags|=MHD_USE_IPv6;
|
||||
}
|
||||
|
||||
struct sockaddr* sockaddr = (m_ipMode==IpMode::ALL || m_ipMode==IpMode::IPV6)
|
||||
? (struct sockaddr*)&sockAddr6
|
||||
: (struct sockaddr*)&sockAddr4;
|
||||
#ifdef _WIN32
|
||||
SOCKET sock = INVALID_SOCKET;
|
||||
if (m_ipMode == IpMode::ALL || m_ipMode == IpMode::IPV6) {
|
||||
if ((sock = socket(AF_INET6, SOCK_STREAM, 0)) == INVALID_SOCKET) {
|
||||
std::cerr << "ERROR: Failed to create IPv6 socket" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
int opt = 0;
|
||||
if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&opt, sizeof(opt)) != 0) {
|
||||
std::cerr << "ERROR: Failed to set IPV6_V6ONLY option" << std::endl;
|
||||
closesocket(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (::bind(sock, (struct sockaddr*)&sockAddr6, sizeof(sockAddr6)) == SOCKET_ERROR) {
|
||||
std::cerr << "ERROR: Failed to bind IPv6 socket" << std::endl;
|
||||
closesocket(sock);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
mp_daemon = MHD_start_daemon(flags,
|
||||
m_port,
|
||||
NULL,
|
||||
NULL,
|
||||
&staticHandlerCallback,
|
||||
this,
|
||||
MHD_OPTION_SOCK_ADDR, &sockAddr,
|
||||
MHD_OPTION_SOCK_ADDR, sockaddr,
|
||||
#ifdef _WIN32
|
||||
(sock == INVALID_SOCKET) ? MHD_OPTION_END : MHD_OPTION_LISTEN_SOCKET, sock,
|
||||
#endif
|
||||
MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
|
||||
MHD_OPTION_PER_IP_CONNECTION_LIMIT, m_ipConnectionLimit,
|
||||
MHD_OPTION_END);
|
||||
|
||||
if (mp_daemon == nullptr) {
|
||||
std::cerr << "Unable to instantiate the HTTP daemon. The port " << m_port
|
||||
<< " is maybe already occupied or need more permissions to be open. "
|
||||
std::cerr << "ERROR: Unable to instantiate the HTTP daemon. The port " << m_port
|
||||
<< " may already be in use, or more permissions are required to open it. "
|
||||
"Please try as root or with a port number higher or equal to 1024."
|
||||
<< std::endl;
|
||||
#ifdef _WIN32
|
||||
if (sock != INVALID_SOCKET) closesocket(sock);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
auto server_start_time = std::chrono::system_clock::now().time_since_epoch();
|
||||
@@ -937,7 +1015,7 @@ std::unique_ptr<Response> InternalServer::handle_search_request(const RequestCon
|
||||
} catch(std::runtime_error& e) {
|
||||
// Searcher->search will throw a runtime error if there is no valid xapian database to do the search.
|
||||
// (in case of zim file not containing a index)
|
||||
const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css);
|
||||
const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css_tmpl);
|
||||
HTTPErrorResponse response(request, MHD_HTTP_NOT_FOUND,
|
||||
"fulltext-search-unavailable",
|
||||
"404-page-heading",
|
||||
|
||||
@@ -27,6 +27,7 @@ extern "C" {
|
||||
|
||||
#include "library.h"
|
||||
#include "name_mapper.h"
|
||||
#include "tools.h"
|
||||
|
||||
#include <zim/search.h>
|
||||
#include <zim/suggestion.h>
|
||||
@@ -94,7 +95,7 @@ class InternalServer {
|
||||
public:
|
||||
InternalServer(LibraryPtr library,
|
||||
std::shared_ptr<NameMapper> nameMapper,
|
||||
std::string addr,
|
||||
IpAddress addr,
|
||||
int port,
|
||||
std::string root,
|
||||
int nbThreads,
|
||||
@@ -103,6 +104,7 @@ class InternalServer {
|
||||
bool withTaskbar,
|
||||
bool withLibraryButton,
|
||||
bool blockExternalLinks,
|
||||
IpMode ipMode,
|
||||
std::string indexTemplateString,
|
||||
int ipConnectionLimit);
|
||||
virtual ~InternalServer();
|
||||
@@ -116,8 +118,9 @@ class InternalServer {
|
||||
void** cont_cls);
|
||||
bool start();
|
||||
void stop();
|
||||
std::string getAddress() { return m_addr; }
|
||||
int getPort() { return m_port; }
|
||||
IpAddress getAddress() const { return m_addr; }
|
||||
int getPort() const { return m_port; }
|
||||
IpMode getIpMode() const { return m_ipMode; }
|
||||
|
||||
private: // functions
|
||||
std::unique_ptr<Response> handle_request(const RequestContext& request);
|
||||
@@ -164,7 +167,7 @@ class InternalServer {
|
||||
typedef ConcurrentCache<std::string, std::shared_ptr<LockableSuggestionSearcher>> SuggestionSearcherCache;
|
||||
|
||||
private: // data
|
||||
std::string m_addr;
|
||||
IpAddress m_addr;
|
||||
int m_port;
|
||||
std::string m_root; // URI-encoded
|
||||
std::string m_rootPrefixOfDecodedURL; // URI-decoded
|
||||
@@ -174,6 +177,7 @@ class InternalServer {
|
||||
bool m_withTaskbar;
|
||||
bool m_withLibraryButton;
|
||||
bool m_blockExternalLinks;
|
||||
IpMode m_ipMode;
|
||||
std::string m_indexTemplateString;
|
||||
int m_ipConnectionLimit;
|
||||
struct MHD_Daemon* mp_daemon;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#include <cctype>
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
#include "i18n.h"
|
||||
#include "i18n_utils.h"
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
#include <mustache.hpp>
|
||||
#include "byte_range.h"
|
||||
#include "etag.h"
|
||||
#include "i18n.h"
|
||||
#include "i18n_utils.h"
|
||||
|
||||
#include <zim/item.h>
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@ namespace
|
||||
|
||||
// These mappings are not provided by the ICU library, any such mappings can be manually added here
|
||||
std::map<std::string, std::string> iso639_3 = {
|
||||
{"ami", "Amis"},
|
||||
{"atj", "atikamekw"},
|
||||
{"azb", "آذربایجان دیلی"},
|
||||
{"bcl", "central bikol"},
|
||||
{"bgs", "tagabawa"},
|
||||
{"blk", "ပအိုဝ်ႏ"},
|
||||
{"bxr", "буряад хэлэн"},
|
||||
{"cbk", "chavacano"},
|
||||
{"cdo", "閩東語"},
|
||||
@@ -23,13 +25,16 @@ std::map<std::string, std::string> iso639_3 = {
|
||||
{"eml", "emiliân-rumagnōl"},
|
||||
{"fbs", "српскохрватски"},
|
||||
{"fon", "fɔ̀ngbè"},
|
||||
{"gcr", "Kriyòl gwiyannen"},
|
||||
{"guw", "Gungbe"},
|
||||
{"hbs", "srpskohrvatski"},
|
||||
{"hyw", "հայերէն/հայերեն"},
|
||||
{"ido", "ido"},
|
||||
{"kbp", "kabɩyɛ"},
|
||||
{"kld", "Gamilaraay"},
|
||||
{"lbe", "лакку маз"},
|
||||
{"lbj", "ལ་དྭགས་སྐད་"},
|
||||
{"lld", "ladin"},
|
||||
{"map", "Austronesian"},
|
||||
{"mhr", "марий йылме"},
|
||||
{"mnw", "ဘာသာမန်"},
|
||||
@@ -41,10 +46,15 @@ std::map<std::string, std::string> iso639_3 = {
|
||||
{"olo", "livvi"},
|
||||
{"pih", "Pitcairn-Norfolk"},
|
||||
{"pnb", "Western Panjabi"},
|
||||
{"pwn", "Pinayuanan"},
|
||||
{"rmr", "Caló"},
|
||||
{"rmy", "romani shib"},
|
||||
{"roa", "romance languages"},
|
||||
{"twi", "twi"},
|
||||
{"skr", "سرائیکی"},
|
||||
{"szy", "Sakizaya"},
|
||||
{"tay", "Tayal"},
|
||||
{"tgl", "Wikang Tagalog"},
|
||||
{"twi", "Akwapem Twi"},
|
||||
// ICU for Ubuntu versions <= focal (20.04) returns "" for the language code ""
|
||||
// unlike the later versions - which returns "und". We map this value to "Undetermined" for a common ground.
|
||||
{"", "Undetermined"},
|
||||
@@ -58,6 +68,7 @@ void fillLanguagesMap()
|
||||
const kiwix::ICULanguageInfo lang(*icuLangPtr);
|
||||
iso639_3.insert({lang.iso3Code(), lang.selfName()});
|
||||
}
|
||||
iso639_3.erase("mul");
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
|
||||
#include "tools.h"
|
||||
#include "stringTools.h"
|
||||
#include <tools/networkTools.h>
|
||||
|
||||
#include <stdio.h>
|
||||
@@ -32,12 +33,14 @@
|
||||
#include <stdexcept>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <iphlpapi.h>
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <iostream>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <sys/socket.h>
|
||||
#include <net/if.h>
|
||||
#include <netdb.h>
|
||||
@@ -47,6 +50,12 @@
|
||||
#include <sys/sockio.h>
|
||||
#endif
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
size_t write_callback_to_iss(char* ptr, size_t size, size_t nmemb, void* userdata)
|
||||
{
|
||||
auto str = static_cast<std::stringstream*>(userdata);
|
||||
@@ -54,7 +63,15 @@ size_t write_callback_to_iss(char* ptr, size_t size, size_t nmemb, void* userdat
|
||||
return nmemb;
|
||||
}
|
||||
|
||||
std::string kiwix::download(const std::string& url) {
|
||||
void updatePublicIpAddress(IpAddress& publicIpAddr, const IpAddress& interfaceIpAddr)
|
||||
{
|
||||
if (publicIpAddr.addr.empty()) publicIpAddr.addr = interfaceIpAddr.addr;
|
||||
if (publicIpAddr.addr6.empty()) publicIpAddr.addr6 = interfaceIpAddr.addr6;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::string download(const std::string& url) {
|
||||
auto curl = curl_easy_init();
|
||||
std::stringstream ss;
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
@@ -75,103 +92,161 @@ std::string kiwix::download(const std::string& url) {
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> kiwix::getNetworkInterfaces() {
|
||||
std::map<std::string, std::string> interfaces;
|
||||
namespace
|
||||
{
|
||||
|
||||
#ifdef _WIN32
|
||||
SOCKET sd = WSASocket(AF_INET, SOCK_DGRAM, 0, 0, 0, 0);
|
||||
if (sd == INVALID_SOCKET) {
|
||||
std::cerr << "Failed to get a socket. Error " << WSAGetLastError() << std::endl;
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
INTERFACE_INFO InterfaceList[20];
|
||||
unsigned long nBytesReturned;
|
||||
if (WSAIoctl(sd, SIO_GET_INTERFACE_LIST, 0, 0, &InterfaceList,
|
||||
sizeof(InterfaceList), &nBytesReturned, 0, 0) == SOCKET_ERROR) {
|
||||
std::cerr << "Failed calling WSAIoctl: error " << WSAGetLastError() << std::endl;
|
||||
return interfaces;
|
||||
}
|
||||
std::map<std::string, IpAddress> getNetworkInterfacesWin() {
|
||||
std::map<std::string, IpAddress> interfaces;
|
||||
|
||||
int nNumInterfaces = nBytesReturned / sizeof(INTERFACE_INFO);
|
||||
for (int i = 0; i < nNumInterfaces; ++i) {
|
||||
sockaddr_in *pAddress;
|
||||
pAddress = (sockaddr_in *) & (InterfaceList[i].iiAddress.AddressIn);
|
||||
if(pAddress->sin_family == AF_INET) {
|
||||
/* Add to the map */
|
||||
std::string interfaceName = std::string(inet_ntoa(pAddress->sin_addr));
|
||||
interfaces[interfaceName] = interfaceName;
|
||||
}
|
||||
}
|
||||
#else
|
||||
/* Get Network interfaces information */
|
||||
char buf[16384];
|
||||
struct ifconf ifconf;
|
||||
int fd = socket(PF_INET, SOCK_DGRAM, 0); /* Only IPV4 */
|
||||
ifconf.ifc_len = sizeof(buf);
|
||||
ifconf.ifc_buf=buf;
|
||||
if(ioctl(fd, SIOCGIFCONF, &ifconf)!=0) {
|
||||
perror("ioctl(SIOCGIFCONF)");
|
||||
}
|
||||
const int working_buffer_size = 15000;
|
||||
const int max_tries = 3;
|
||||
|
||||
/* Go through each interface */
|
||||
struct ifreq *ifreq;
|
||||
ifreq = ifconf.ifc_req;
|
||||
for (int i = 0; i < ifconf.ifc_len; ) {
|
||||
if (ifreq->ifr_addr.sa_family == AF_INET) {
|
||||
/* Get the network interface ip */
|
||||
char host[128] = { 0 };
|
||||
const int error = getnameinfo(&(ifreq->ifr_addr), sizeof(ifreq->ifr_addr),
|
||||
host, sizeof(host),
|
||||
0, 0, NI_NUMERICHOST);
|
||||
if (!error) {
|
||||
std::string interfaceName = std::string(ifreq->ifr_name);
|
||||
std::string interfaceIp = std::string(host);
|
||||
/* Add to the map */
|
||||
interfaces[interfaceName] = interfaceIp;
|
||||
} else {
|
||||
perror("getnameinfo()");
|
||||
}
|
||||
ULONG flags = GAA_FLAG_INCLUDE_PREFIX;
|
||||
|
||||
// default to unspecified address family (both)
|
||||
ULONG family = AF_UNSPEC;
|
||||
|
||||
ULONG outBufLen = working_buffer_size;
|
||||
ULONG Iterations = 0;
|
||||
DWORD dwRetVal = 0;
|
||||
PIP_ADAPTER_ADDRESSES interfacesHead = NULL;
|
||||
|
||||
// Successively allocate the required memory until GetAdaptersAddresses does not
|
||||
// results in ERROR_BUFFER_OVERFLOW for a maximum of max_tries
|
||||
do{
|
||||
interfacesHead = (IP_ADAPTER_ADDRESSES *) malloc(outBufLen);
|
||||
if (interfacesHead == NULL) {
|
||||
std::cerr << "Memory allocation failed for IP_ADAPTER_ADDRESSES struct" << std::endl;
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
/* some systems have ifr_addr.sa_len and adjust the length that
|
||||
* way, but not mine. weird */
|
||||
size_t len;
|
||||
#ifndef __linux__
|
||||
len = IFNAMSIZ + ifreq->ifr_addr.sa_len;
|
||||
#else
|
||||
len = sizeof(*ifreq);
|
||||
#endif
|
||||
ifreq = (struct ifreq*)((char*)ifreq+len);
|
||||
i += len;
|
||||
dwRetVal = GetAdaptersAddresses(family, flags, NULL, interfacesHead, &outBufLen);
|
||||
} while ((dwRetVal == ERROR_BUFFER_OVERFLOW) && (Iterations < max_tries));
|
||||
|
||||
if (dwRetVal == NO_ERROR) {
|
||||
PIP_ADAPTER_UNICAST_ADDRESS pUnicast = NULL;
|
||||
unsigned int i = 0;
|
||||
for (PIP_ADAPTER_ADDRESSES temp = interfacesHead; temp != NULL;
|
||||
temp = temp->Next) {
|
||||
pUnicast = temp->FirstUnicastAddress;
|
||||
if (pUnicast != NULL) {
|
||||
for (i = 0; pUnicast != NULL; i++){
|
||||
if (pUnicast->Address.lpSockaddr->sa_family == AF_INET)
|
||||
{
|
||||
sockaddr_in *si = (sockaddr_in *)(pUnicast->Address.lpSockaddr);
|
||||
char host[INET_ADDRSTRLEN]={0};
|
||||
inet_ntop(AF_INET, &(si->sin_addr), host, sizeof(host));
|
||||
interfaces[temp->AdapterName].addr=host;
|
||||
}
|
||||
else if (pUnicast->Address.lpSockaddr->sa_family == AF_INET6)
|
||||
{
|
||||
sockaddr_in6 *si = (sockaddr_in6 *)(pUnicast->Address.lpSockaddr);
|
||||
char host[INET6_ADDRSTRLEN]={0};
|
||||
inet_ntop(AF_INET6, &(si->sin6_addr), host, sizeof(host));
|
||||
if (!IN6_IS_ADDR_LINKLOCAL(&(si->sin6_addr)))
|
||||
interfaces[temp->AdapterName].addr6=host;
|
||||
}
|
||||
pUnicast = pUnicast->Next;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Call to GetAdaptersAddresses failed with error: "<< dwRetVal << std::endl;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (interfacesHead) free(interfacesHead);
|
||||
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
std::string kiwix::getBestPublicIp() {
|
||||
auto interfaces = getNetworkInterfaces();
|
||||
#else
|
||||
|
||||
std::map<std::string, IpAddress> getNetworkInterfacesPosix() {
|
||||
std::map<std::string, IpAddress> interfaces;
|
||||
|
||||
struct ifaddrs *interfacesHead;
|
||||
if (getifaddrs(&interfacesHead) == -1) {
|
||||
perror("getifaddrs");
|
||||
}
|
||||
|
||||
for (ifaddrs *temp = interfacesHead; temp != NULL; temp = temp->ifa_next) {
|
||||
if (temp->ifa_addr == NULL) continue;
|
||||
|
||||
if (temp->ifa_addr->sa_family == AF_INET) {
|
||||
sockaddr_in *si = (sockaddr_in *)(temp->ifa_addr);
|
||||
char host[INET_ADDRSTRLEN] = {0};
|
||||
inet_ntop(AF_INET, &(si->sin_addr), host, sizeof(host));
|
||||
interfaces[temp->ifa_name].addr=host;
|
||||
} else if (temp->ifa_addr->sa_family == AF_INET6) {
|
||||
sockaddr_in6 *si = (sockaddr_in6 *)(temp->ifa_addr);
|
||||
char host[INET6_ADDRSTRLEN] = {0};
|
||||
inet_ntop(AF_INET6, &(si->sin6_addr), host, sizeof(host));
|
||||
if (!IN6_IS_ADDR_LINKLOCAL(&(si->sin6_addr)))
|
||||
interfaces[temp->ifa_name].addr6=host;
|
||||
}
|
||||
}
|
||||
|
||||
freeifaddrs(interfacesHead);
|
||||
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::map<std::string, IpAddress> getNetworkInterfacesIPv4Or6() {
|
||||
#ifdef _WIN32
|
||||
return getNetworkInterfacesWin();
|
||||
#else
|
||||
return getNetworkInterfacesPosix();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> getNetworkInterfaces() {
|
||||
std::map<std::string, std::string> result;
|
||||
for ( const auto& kv : getNetworkInterfacesIPv4Or6() ) {
|
||||
const std::string& interfaceName = kv.first;
|
||||
const auto& ipAddresses = kv.second;
|
||||
if ( !ipAddresses.addr.empty() ) {
|
||||
result[interfaceName] = ipAddresses.addr;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
IpAddress getBestPublicIps() {
|
||||
IpAddress bestPublicIps;
|
||||
std::map<std::string, IpAddress> interfaces = getNetworkInterfacesIPv4Or6();
|
||||
#ifndef _WIN32
|
||||
const char* const prioritizedNames[] =
|
||||
{ "eth0", "eth1", "wlan0", "wlan1", "en0", "en1" };
|
||||
for(auto name: prioritizedNames) {
|
||||
auto it = interfaces.find(name);
|
||||
if(it != interfaces.end()) {
|
||||
return it->second;
|
||||
const char* const prioritizedNames[] = { "eth0", "eth1", "wlan0", "wlan1", "en0", "en1" };
|
||||
for (const auto& name : prioritizedNames) {
|
||||
const auto it = interfaces.find(name);
|
||||
if (it != interfaces.end()) {
|
||||
updatePublicIpAddress(bestPublicIps, it->second);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const char* const prefixes[] = { "192.168", "172.16.", "10.0" };
|
||||
for(auto prefix : prefixes){
|
||||
for(auto& itr : interfaces) {
|
||||
auto interfaceIp = itr.second;
|
||||
if (interfaceIp.find(prefix) == 0) {
|
||||
return interfaceIp;
|
||||
const char* const v4prefixes[] = { "192.168", "172.16", "10.0", "169.254" };
|
||||
for (const auto& prefix : v4prefixes) {
|
||||
for (const auto& kv : interfaces) {
|
||||
const auto& interfaceIps = kv.second;
|
||||
if (kiwix::startsWith(interfaceIps.addr, prefix)) {
|
||||
updatePublicIpAddress(bestPublicIps, interfaceIps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "127.0.0.1";
|
||||
updatePublicIpAddress(bestPublicIps, {"127.0.0.1", "::1"});
|
||||
|
||||
return bestPublicIps;
|
||||
}
|
||||
|
||||
std::string getBestPublicIp()
|
||||
{
|
||||
return getBestPublicIps().addr;
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
#endif
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
#include "server/i18n.h"
|
||||
#include "server/i18n_utils.h"
|
||||
#include "libkiwix-resources.h"
|
||||
|
||||
#include <map>
|
||||
|
||||
@@ -320,16 +320,6 @@ bool kiwix::fileReadable(const std::string& path)
|
||||
#endif
|
||||
}
|
||||
|
||||
bool makeDirectory(const std::string& path)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
int status = _wmkdir(Utf8ToWide(path).c_str());
|
||||
#else
|
||||
int status = mkdir(path.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
|
||||
#endif
|
||||
return status == 0;
|
||||
}
|
||||
|
||||
std::string makeTmpDirectory()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
@@ -438,52 +428,6 @@ std::string kiwix::getCurrentDirectory()
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string kiwix::getDataDirectory()
|
||||
{
|
||||
// Try to get the dataDir from the `KIWIX_DATA_DIR` env var
|
||||
#ifdef _WIN32
|
||||
wchar_t* cDataDir = ::_wgetenv(L"KIWIX_DATA_DIR");
|
||||
if (cDataDir != nullptr) {
|
||||
return WideToUtf8(cDataDir);
|
||||
}
|
||||
#else
|
||||
char* cDataDir = ::getenv("KIWIX_DATA_DIR");
|
||||
if (cDataDir != nullptr) {
|
||||
return cDataDir;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Compute the dataDir from the user directory.
|
||||
std::string dataDir;
|
||||
#ifdef _WIN32
|
||||
cDataDir = ::_wgetenv(L"APPDATA");
|
||||
if (cDataDir == nullptr)
|
||||
cDataDir = ::_wgetenv(L"USERPROFILE");
|
||||
if (cDataDir != nullptr)
|
||||
dataDir = WideToUtf8(cDataDir);
|
||||
#else
|
||||
cDataDir = ::getenv("XDG_DATA_HOME");
|
||||
if (cDataDir != nullptr) {
|
||||
dataDir = cDataDir;
|
||||
} else {
|
||||
cDataDir = ::getenv("HOME");
|
||||
if (cDataDir != nullptr) {
|
||||
dataDir = cDataDir;
|
||||
dataDir = appendToDirectory(dataDir, ".local");
|
||||
dataDir = appendToDirectory(dataDir, "share");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!dataDir.empty()) {
|
||||
dataDir = appendToDirectory(dataDir, "kiwix");
|
||||
makeDirectory(dataDir);
|
||||
return dataDir;
|
||||
}
|
||||
|
||||
// Let's use the currentDirectory
|
||||
return getCurrentDirectory();
|
||||
}
|
||||
|
||||
static std::map<std::string, std::string> extMimeTypes = {
|
||||
{ "html", "text/html"},
|
||||
{ "htm", "text/html"},
|
||||
|
||||
@@ -29,7 +29,6 @@ std::wstring Utf8ToWide(const std::string& str);
|
||||
|
||||
unsigned int getFileSize(const std::string& path);
|
||||
std::string getFileSizeAsString(const std::string& path);
|
||||
bool makeDirectory(const std::string& path);
|
||||
std::string makeTmpDirectory();
|
||||
bool copyFile(const std::string& sourcePath, const std::string& destPath);
|
||||
bool writeTextFile(const std::string& path, const std::string& content);
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <regex>
|
||||
|
||||
/* tell ICU where to find its dat file (tables) */
|
||||
void kiwix::loadICUExternalTables()
|
||||
@@ -256,7 +257,7 @@ std::string kiwix::urlDecode(const std::string& value, bool component)
|
||||
|
||||
// If there aren't enough characters left for this to be a
|
||||
// valid escape code, just use the character and move on
|
||||
if (it > value.end() - 3) {
|
||||
if (value.end() - it < 3) {
|
||||
os << *it;
|
||||
continue;
|
||||
}
|
||||
@@ -439,3 +440,13 @@ template<>
|
||||
std::string kiwix::extractFromString(const std::string& str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string kiwix::getSlugifiedFileName(const std::string& filename)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
const std::regex reservedCharsReg(R"([<>:"/\\|?*])");
|
||||
#else
|
||||
const std::regex reservedCharsReg("/");
|
||||
#endif
|
||||
return std::regex_replace(filename, reservedCharsReg, "_");
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ skin/i18n/nl.json
|
||||
skin/i18n/nqo.json
|
||||
skin/i18n/or.json
|
||||
skin/i18n/pl.json
|
||||
skin/i18n/pt-br.json
|
||||
skin/i18n/ru.json
|
||||
skin/i18n/sc.json
|
||||
skin/i18n/sk.json
|
||||
@@ -34,6 +35,7 @@ skin/i18n/skr-arab.json
|
||||
skin/i18n/sl.json
|
||||
skin/i18n/sq.json
|
||||
skin/i18n/sv.json
|
||||
skin/i18n/sw.json
|
||||
skin/i18n/te.json
|
||||
skin/i18n/test.json
|
||||
skin/i18n/tr.json
|
||||
|
||||
@@ -4,6 +4,7 @@ skin/magnet.png
|
||||
skin/feed.svg
|
||||
skin/langSelector.svg
|
||||
skin/download.png
|
||||
skin/download-white.svg
|
||||
skin/hash.png
|
||||
skin/search-icon.svg
|
||||
skin/iso6391To3.js
|
||||
@@ -37,7 +38,7 @@ templates/catalog_v2_entry.xml
|
||||
templates/catalog_v2_partial_entry.xml
|
||||
templates/catalog_v2_categories.xml
|
||||
templates/catalog_v2_languages.xml
|
||||
templates/url_of_search_results_css
|
||||
templates/url_of_search_results_css.tmpl
|
||||
templates/viewer_settings.js
|
||||
templates/no_js_library_page.html
|
||||
templates/no_js_download.html
|
||||
|
||||
6
static/skin/download-white.svg
Normal file
6
static/skin/download-white.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <g id="Interface / Download"> <path id="Vector" d="M6 21H18M12 3V17M12 17L17 12M12 17L7 12" stroke="#f6f5f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </g> </g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 546 B |
@@ -16,5 +16,15 @@
|
||||
"search": "অনুসন্ধান",
|
||||
"welcome-to-kiwix-server": "কিউইক্স সার্ভারে স্বাগতম",
|
||||
"download-links-title": "বই ডাউনলোড করুন",
|
||||
"preview-book": "প্রাকদর্শন"
|
||||
"preview-book": "প্রাকদর্শন",
|
||||
"book-category.wikibooks": "উইকিবই",
|
||||
"book-category.wikinews": "উইকিসংবাদ",
|
||||
"book-category.wikipedia": "উইকিপিডিয়া",
|
||||
"book-category.wikiquote": "উইকিউক্তি",
|
||||
"book-category.wikisource": "উইকিসংকলন",
|
||||
"book-category.wikispecies": "উইকিপ্রজাতি",
|
||||
"book-category.wikiversity": "উইকিবিশ্ববিদ্যালয়",
|
||||
"book-category.wikivoyage": "উইকিভ্রমণ",
|
||||
"book-category.wiktionary": "উইকিঅভিধান",
|
||||
"book-category.other": "অন্যান্য"
|
||||
}
|
||||
|
||||
@@ -42,14 +42,14 @@
|
||||
"book-filtering-all-categories": "Pubu zaa",
|
||||
"book-filtering-all-languages": "Bala zaa",
|
||||
"count-of-matching-books": "{{COUNT}} Buku(nima)",
|
||||
"download": "Yihibu",
|
||||
"download": "Deebu",
|
||||
"direct-download-link-text": "Tibi",
|
||||
"direct-download-alt-text": "Tibi deebu",
|
||||
"hash-download-link-text": "Sha256 hash",
|
||||
"hash-download-alt-text": "Deebu daliŋ",
|
||||
"welcome-to-kiwix-server": "Maraba Kiwix tum tumda",
|
||||
"download-links-heading": "Deemi soli zaŋ n-ti <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Yaa mi buku",
|
||||
"preview-book": "Labi lihi",
|
||||
"download-links-title": "Deemi buku",
|
||||
"preview-book": "Daŋyuli",
|
||||
"unknown-error": "Chiriŋ din bi tooi baŋ"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"IMayBeABitShy",
|
||||
"Justman10000",
|
||||
"Lucas Werkmeister",
|
||||
"Rofiatmustapha12",
|
||||
"ThisCarthing"
|
||||
@@ -47,13 +48,13 @@
|
||||
"count-of-matching-books": "{{COUNT}} Bücher",
|
||||
"download": "Herunterladen",
|
||||
"direct-download-link-text": "Direkt",
|
||||
"direct-download-alt-text": "direkt herunterladen",
|
||||
"hash-download-link-text": "Sha256 Hash",
|
||||
"hash-download-alt-text": "Hash herunterladen",
|
||||
"direct-download-alt-text": "Direktes Herunterladen über HTTP(S)",
|
||||
"hash-download-link-text": "SHA-256 Prüfsumme",
|
||||
"hash-download-alt-text": "SHA-256-Dateiprüfsumme anzeigen",
|
||||
"magnet-link-text": "Magnet Link",
|
||||
"magnet-alt-text": "Magnet Link herunterladen",
|
||||
"torrent-download-link-text": "Torrent-Datei",
|
||||
"torrent-download-alt-text": "Torrent herunterladen",
|
||||
"magnet-alt-text": "Download über Magnet-Link",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Herunterladen über BitTorrent",
|
||||
"library-opds-feed-all-entries": "ODPS Feed der Bibliothek - Alle Einträge",
|
||||
"filter-by-tag": "Nach Tag \"{{TAG}}\" filtern",
|
||||
"stop-filtering-by-tag": "Filterung nach Tag \"{{TAG}}\" aufheben",
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kelson",
|
||||
"Norhorn"
|
||||
"Norhorn",
|
||||
"Ανώνυμος Βικιπαιδιστής"
|
||||
]
|
||||
},
|
||||
"name": "Αγγλικά",
|
||||
"no-such-book": "Δεν υπάρχει τέτοιο βιβλίο: {{BOOK_NAME}}",
|
||||
"welcome-page-overzealous-filter": "Κανένα αποτέλεσμα. Θέλετε να <a href=\"{{URL}}\">επαναφέρετε το φίλτρο</a>;",
|
||||
"powered-by-kiwix-html": "Με την υποστήριξη by <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Αναζήτηση",
|
||||
@@ -15,8 +18,14 @@
|
||||
"direct-download-link-text": "Απευθείας",
|
||||
"direct-download-alt-text": "άμεση λήψη",
|
||||
"hash-download-alt-text": "λήψη αναγνωριστικού",
|
||||
"magnet-alt-text": "λήψη μαγνήτη",
|
||||
"torrent-download-link-text": "Αρχείο torrent",
|
||||
"torrent-download-alt-text": "λήψη torrent",
|
||||
"filter-by-tag": "Φίλτρο ανά ετικέτα \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Διακοπή φίλτρου ανά ετικέτα \"{{TAG}}\""
|
||||
"stop-filtering-by-tag": "Διακοπή φίλτρου ανά ετικέτα \"{{TAG}}\"",
|
||||
"welcome-to-kiwix-server": "Καλώς ορίσατε στον διακομιστή Kiwix",
|
||||
"download-links-heading": "Λήψη συνδέσμων για <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Κατεβάστε το βιβλίο",
|
||||
"preview-book": "Προεπισκόπηση",
|
||||
"unknown-error": "Άγνωστο σφάλμα"
|
||||
}
|
||||
|
||||
@@ -43,16 +43,16 @@
|
||||
, "count-of-matching-books": "{{COUNT}} book(s)"
|
||||
, "download": "Download"
|
||||
, "direct-download-link-text": "Direct"
|
||||
, "direct-download-alt-text": "direct download"
|
||||
, "hash-download-link-text": "Sha256 hash"
|
||||
, "hash-download-alt-text": "download hash"
|
||||
, "direct-download-alt-text": "Download directly via HTTP(S)"
|
||||
, "hash-download-link-text": "SHA-256 checksum"
|
||||
, "hash-download-alt-text": "Display SHA-256 file checksum"
|
||||
, "magnet-link-text": "Magnet link"
|
||||
, "magnet-alt-text": "download magnet"
|
||||
, "torrent-download-link-text": "Torrent file"
|
||||
, "torrent-download-alt-text": "download torrent"
|
||||
, "magnet-alt-text": "Download via Magnet link"
|
||||
, "torrent-download-link-text": "BitTorrent"
|
||||
, "torrent-download-alt-text": "Download via BitTorrent"
|
||||
, "library-opds-feed-all-entries": "Library OPDS Feed - All entries"
|
||||
, "filter-by-tag": "Filter by tag \"{{TAG}}\""
|
||||
, "stop-filtering-by-tag": "Stop filtering by tag \"{{TAG}}\""
|
||||
, "filter-by-tag": "Filter by tag \"{{{TAG}}}\""
|
||||
, "stop-filtering-by-tag": "Stop filtering by tag \"{{{TAG}}}\""
|
||||
, "library-opds-feed-parameterised": "Library OPDS Feed - entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}"
|
||||
, "welcome-to-kiwix-server": "Welcome to Kiwix Server"
|
||||
, "download-links-heading": "Download links for <b><i>{{BOOK_TITLE}}</i></b>"
|
||||
@@ -60,4 +60,22 @@
|
||||
, "preview-book": "Preview"
|
||||
, "non-translated-text": "{{MSG}}"
|
||||
, "unknown-error": "Unknown error"
|
||||
, "book-category.gutenberg": "Gutenberg"
|
||||
, "book-category.iFixit": "iFixit"
|
||||
, "book-category.mooc": "MOOC"
|
||||
, "book-category.phet": "Phet"
|
||||
, "book-category.stack_exchange": "Stack Exchange"
|
||||
, "book-category.ted": "Ted"
|
||||
, "book-category.vikidia": "Vikidia"
|
||||
, "book-category.wikibooks": "Wikibooks"
|
||||
, "book-category.wikihow": "wikiHow"
|
||||
, "book-category.wikinews": "Wikinews"
|
||||
, "book-category.wikipedia": "Wikipedia"
|
||||
, "book-category.wikiquote": "Wikiquote"
|
||||
, "book-category.wikisource": "Wikisource"
|
||||
, "book-category.wikispecies": "Wikispecies"
|
||||
, "book-category.wikiversity": "Wikiversity"
|
||||
, "book-category.wikivoyage": "Wikivoyage"
|
||||
, "book-category.wiktionary": "Wiktionary"
|
||||
, "book-category.other": "Other"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"AlexanderFF",
|
||||
"Fitoschido",
|
||||
"Ovruni",
|
||||
"SpikeShroom",
|
||||
@@ -45,13 +46,14 @@
|
||||
"hash-download-alt-text": "descargar hash",
|
||||
"magnet-link-text": "Enlace magnético",
|
||||
"magnet-alt-text": "Descargar link magnético",
|
||||
"torrent-download-link-text": "Archivo de torrent",
|
||||
"torrent-download-alt-text": "descargar torrent",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Descargar a través de BitTorrent",
|
||||
"filter-by-tag": "Filtrar por etiqueta \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Dejar de filtrar por etiqueta \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": "Feed OPDS de la biblioteca: entradas que coinciden con {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nEtiqueta: {{TAG}} {{/TAG}}{{#Q}}\nConsulta: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Bienvenido al servidor Kiwix",
|
||||
"download-links-heading": "Enlaces de descarga para <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Descargar libro",
|
||||
"preview-book": "Previsualizar"
|
||||
"preview-book": "Previsualizar",
|
||||
"unknown-error": "Error desconocido"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
},
|
||||
"name": "suomi",
|
||||
"suggest-full-text-search": "sisältää '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Kirjaa {{BOOK_NAME}} ei ole olemassa",
|
||||
"url-not-found": "Pyydettyä URL-osoitetta \"{{url}}\" ei löytynyt tältä palvelimelta.",
|
||||
"400-page-title": "Virheellinen pyyntö",
|
||||
"400-page-heading": "Virheellinen pyyntö",
|
||||
@@ -15,6 +16,7 @@
|
||||
"404-page-heading": "Ei löytynyt",
|
||||
"500-page-title": "Sisäinen palvelinvirhe",
|
||||
"500-page-heading": "Sisäinen palvelinvirhe",
|
||||
"word-count": "{{COUNT}} sanaa",
|
||||
"library-button-text": "Siirry tervetulosivulle",
|
||||
"home-button-text": "Siirry kirjan '{{BOOK_TITLE}}' etusivulle",
|
||||
"random-page-button-text": "Siirry satunnaiselle sivulle",
|
||||
@@ -22,9 +24,14 @@
|
||||
"search": "Hae",
|
||||
"book-filtering-all-categories": "Kaikki luokat",
|
||||
"book-filtering-all-languages": "Kaikki kielet",
|
||||
"count-of-matching-books": "{{COUNT}} kirja(a)",
|
||||
"download": "Lataa",
|
||||
"magnet-link-text": "Magnet-linkki",
|
||||
"magnet-alt-text": "lataa magnet",
|
||||
"torrent-download-link-text": "Torrent-tiedosto",
|
||||
"torrent-download-alt-text": "lataa torrent-tiedosto",
|
||||
"filter-by-tag": "Suodata tunnisteen ”{{TAG}}” mukaan",
|
||||
"download-links-title": "Lataa kirja",
|
||||
"preview-book": "Esikatsele"
|
||||
"preview-book": "Esikatsele",
|
||||
"unknown-error": "Tuntematon virhe"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Adriendelucca",
|
||||
"Benoit74",
|
||||
"Gomoko",
|
||||
"Goombiis",
|
||||
"Melimeli",
|
||||
"Stephane",
|
||||
"Thibaut120094",
|
||||
@@ -52,20 +54,30 @@
|
||||
"count-of-matching-books": "{{COUNT}} livre(s)",
|
||||
"download": "Télécharger",
|
||||
"direct-download-link-text": "Direct",
|
||||
"direct-download-alt-text": "téléchargement direct",
|
||||
"hash-download-link-text": "Hachage sha256",
|
||||
"hash-download-alt-text": "télécharger le hachage",
|
||||
"direct-download-alt-text": "Télécharger directement via HTTP(S)",
|
||||
"hash-download-link-text": "Hachage SHA-256",
|
||||
"hash-download-alt-text": "Affiche le hachage SHA-256 du fichier",
|
||||
"magnet-link-text": "Lien Magnet",
|
||||
"magnet-alt-text": "télécharger le lien Magnet",
|
||||
"torrent-download-link-text": "Fichier torrent",
|
||||
"torrent-download-alt-text": "télécharger le torrent",
|
||||
"magnet-alt-text": "Télécharger via le lien Magnet",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Télécharger via BitTorrent",
|
||||
"library-opds-feed-all-entries": "Flux OPDS de la bibliothèque – Toutes les entrées",
|
||||
"filter-by-tag": "Filtrer par la balise « {{TAG}} »",
|
||||
"stop-filtering-by-tag": "Arrêter le filtrage par la balise « {{TAG}} »",
|
||||
"filter-by-tag": "Filtrer par le tag \"{{{TAG}}}\"",
|
||||
"stop-filtering-by-tag": "Arrêter de filtrer par le tag \"{{{TAG}}}\"",
|
||||
"library-opds-feed-parameterised": "Flux OPDS de la bibliothèque – Entrées correspondant à {{#LANG}} :\n ▪ Langue : {{LANG}} {{/LANG}}{{#CATEGORY}}\n ▪ Catégorie : {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\n ▪ Étiquette : {{TAG}} {{/TAG}}{{#Q}}\n ▪ Requête : {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Bienvenue sur le Serveur Kiwix",
|
||||
"download-links-heading": "Liens de téléchargement pour <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Télécharger le livre",
|
||||
"preview-book": "Aperçu",
|
||||
"unknown-error": "Erreur inconnue"
|
||||
"unknown-error": "Erreur inconnue",
|
||||
"book-category.wikibooks": "Wikilivres",
|
||||
"book-category.wikinews": "Wikinews",
|
||||
"book-category.wikipedia": "Wikipédia",
|
||||
"book-category.wikiquote": "Wikiquote",
|
||||
"book-category.wikisource": "Wikisource",
|
||||
"book-category.wikispecies": "Wikispecies",
|
||||
"book-category.wikiversity": "Wikiversité",
|
||||
"book-category.wikivoyage": "Wikivoyage",
|
||||
"book-category.wiktionary": "Wiktionnaire",
|
||||
"book-category.other": "Autre"
|
||||
}
|
||||
|
||||
@@ -45,20 +45,32 @@
|
||||
"count-of-matching-books": "{{COUNT}} ספרים",
|
||||
"download": "הורדה",
|
||||
"direct-download-link-text": "ישירה",
|
||||
"direct-download-alt-text": "הורדה ישירה",
|
||||
"hash-download-link-text": "גיבוב Sha256",
|
||||
"hash-download-alt-text": "הורדת גיבוב",
|
||||
"direct-download-alt-text": "הורדה ישירה דרך HTTP(S)",
|
||||
"hash-download-link-text": "סיכום ביקורת Sha256",
|
||||
"hash-download-alt-text": "הצגת סיכום ביקורת SHA-256",
|
||||
"magnet-link-text": "קישור Magnet",
|
||||
"magnet-alt-text": "הורדת magnet",
|
||||
"torrent-download-link-text": "קובץ טורנט",
|
||||
"torrent-download-alt-text": "הורדת טורנט",
|
||||
"magnet-alt-text": "הורדה באמצעות קישור magnet",
|
||||
"torrent-download-link-text": "ביטורנט",
|
||||
"torrent-download-alt-text": "הורדה באמצעות ביטורנט",
|
||||
"library-opds-feed-all-entries": "הזנת ספריית OPDS - כל הרשומות",
|
||||
"filter-by-tag": "סינון לפי התג \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "להפסיק סינון לפי התג \"{{TAG}}\"",
|
||||
"filter-by-tag": "לסנן לפי התג \"{{{TAG}}}\"",
|
||||
"stop-filtering-by-tag": "להפסיק סינון לפי התג \"{{{TAG}}}\"",
|
||||
"library-opds-feed-parameterised": "הזנת ספריית OPDS - רשומות שתואמות ל{{#LANG}}\nשפה: {{LANG}} {{/LANG}}{{#CATEGORY}}\nקטגוריה: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nתג: {{TAG}} {{/TAG}}{{#Q}}\nשאילתה: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "ברוך בואך לשרת קיוויקס",
|
||||
"download-links-heading": "הורדת קישורים עבור <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "הורדת ספר",
|
||||
"preview-book": "תצוגה מקדימה",
|
||||
"unknown-error": "שגיאה בלתי־ידועה"
|
||||
"unknown-error": "שגיאה בלתי־ידועה",
|
||||
"book-category.gutenberg": "גוטנברג",
|
||||
"book-category.vikidia": "ויקידיה",
|
||||
"book-category.wikibooks": "ויקיספר",
|
||||
"book-category.wikinews": "ויקיחדשות",
|
||||
"book-category.wikipedia": "ויקיפדיה",
|
||||
"book-category.wikiquote": "ויקיציטוט",
|
||||
"book-category.wikisource": "ויקיטקסט (Wikisource)",
|
||||
"book-category.wikispecies": "ויקימינים",
|
||||
"book-category.wikiversity": "ויקיברסיטה",
|
||||
"book-category.wikivoyage": "ויקימסע",
|
||||
"book-category.wiktionary": "ויקימילון",
|
||||
"book-category.other": "אחר"
|
||||
}
|
||||
|
||||
@@ -52,5 +52,15 @@
|
||||
"welcome-to-kiwix-server": "कीविक्स सर्वर में आपका स्वागत है",
|
||||
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b> के लिए डाउनलोड लिंक",
|
||||
"download-links-title": "पुस्तक डाउनलोड करें",
|
||||
"preview-book": "पूर्वावलोकन"
|
||||
"preview-book": "पूर्वावलोकन",
|
||||
"book-category.wikibooks": "विकिपुस्तक",
|
||||
"book-category.wikinews": "विकिसमाचार",
|
||||
"book-category.wikipedia": "विकिपीडिया",
|
||||
"book-category.wikiquote": "विकिसूक्ति",
|
||||
"book-category.wikisource": "विकिस्रोत",
|
||||
"book-category.wikispecies": "विकिप्रजाति",
|
||||
"book-category.wikiversity": "विकिविश्वविद्यालय",
|
||||
"book-category.wikivoyage": "विकियात्रा",
|
||||
"book-category.wiktionary": "विकिकोश",
|
||||
"book-category.other": "अन्य"
|
||||
}
|
||||
|
||||
@@ -17,5 +17,15 @@
|
||||
"home-button-text": "Դեպի '{{BOOK_TITLE}}'֊ի գլխավոր էջը",
|
||||
"random-page-button-text": "Բացել պատահական էջ",
|
||||
"searchbox-tooltip": "Որոնել '{{BOOK_TITLE}}'֊ում",
|
||||
"book-filtering-all-categories": "Բոլոր կատեգորիաներ"
|
||||
"book-filtering-all-categories": "Բոլոր կատեգորիաներ",
|
||||
"book-category.wikibooks": "Վիքիգրքեր",
|
||||
"book-category.wikinews": "Վիքիլուրեր",
|
||||
"book-category.wikipedia": "Վիքիպեդիա",
|
||||
"book-category.wikiquote": "Վիքիքաղվածք",
|
||||
"book-category.wikisource": "Վիքիդարան",
|
||||
"book-category.wikispecies": "Վիքիցեղեր",
|
||||
"book-category.wikiversity": "Վիքիլսարան",
|
||||
"book-category.wikivoyage": "Վիքիճամփորդ",
|
||||
"book-category.wiktionary": "Վիքիբառարան",
|
||||
"book-category.other": "Այլ"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"suggest-search": "Facer un recerca in texto complete de <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ups! Non poteva eliger un articulo aleatori :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} non es un request valide pro contento crude.",
|
||||
"invalid-request": "Le URL requestate “{{{url}}}” non es un requesta valide.",
|
||||
"no-value-for-arg": "Necun valor fornite pro le argumento {{ARGUMENT}}",
|
||||
"no-query": "Necun consulta fornite.",
|
||||
"raw-entry-not-found": "Non pote trovar le entrata {{ENTRY}} del typo {{DATATYPE}}",
|
||||
@@ -23,8 +24,14 @@
|
||||
"404-page-heading": "Non trovate",
|
||||
"500-page-title": "Error interne del servitor",
|
||||
"500-page-heading": "Error interne del servitor",
|
||||
"500-page-text": "Un error interne del servitor ha occurrite. Nos lo regretta :/",
|
||||
"fulltext-search-unavailable": "Le recerca in texto complete es indisponibile",
|
||||
"no-search-results": "Le motor de recerca in texto complete non es disponibile pro iste contento.",
|
||||
"search-results-page-title": "Cercar: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Resultatos <b>{{START}}-{{END}}</b> de <b>{{COUNT}}</b> pro <b>“{{{SEARCH_PATTERN}}}”</b>",
|
||||
"empty-search-results-page-header": "Necun resultato ha essite trovate pro <b>“{{{SEARCH_PATTERN}}}”</b>",
|
||||
"search-result-book-info": "de {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} parolas",
|
||||
"library-button-text": "Ir al pagina de benvenita",
|
||||
"home-button-text": "Ir al pagina principal de ''{{BOOK_TITLE}}",
|
||||
"random-page-button-text": "Ir a un pagina seligite aleatorimente",
|
||||
@@ -38,19 +45,20 @@
|
||||
"count-of-matching-books": "{{COUNT}} libro(s)",
|
||||
"download": "Discargar",
|
||||
"direct-download-link-text": "Directe",
|
||||
"direct-download-alt-text": "discargamento directe",
|
||||
"hash-download-link-text": "Hash SHA256",
|
||||
"hash-download-alt-text": "hash del discargamento",
|
||||
"direct-download-alt-text": "Discargamento directe per HTTP(S)",
|
||||
"hash-download-link-text": "Summa de controlo SHA-256",
|
||||
"hash-download-alt-text": "Monstrar le summa de controlo SHA-256 del file",
|
||||
"magnet-link-text": "Ligamine Magnet",
|
||||
"magnet-alt-text": "ligamine \"magnet\" de discargamento",
|
||||
"torrent-download-link-text": "File Torrent",
|
||||
"torrent-download-alt-text": "discargar Torrent",
|
||||
"magnet-alt-text": "Discargar con ligamine Magnet",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Discargar per medio de BitTorrent",
|
||||
"library-opds-feed-all-entries": "Fluxo OPDS del bibliotheca – Tote le entratas",
|
||||
"filter-by-tag": "Filtrar per etiquetta \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Non plus filtrar per etiquetta \"{{TAG}}\"",
|
||||
"filter-by-tag": "Filtrar per etiquetta “{{{TAG}}}”",
|
||||
"stop-filtering-by-tag": "Non plus filtrar per etiquetta “{{{TAG}}}”",
|
||||
"library-opds-feed-parameterised": "Fluxo OPDS del bibliotheca – Entratas correspondente a {{#LANG}}\nLingua {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategoria: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nEtiquetta: {{TAG}} {{/TAG}}{{#Q}}\nConsulta: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Benvenite al servitor Kiwix",
|
||||
"download-links-heading": "Discargar ligamines pro <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Discargar libro",
|
||||
"preview-book": "Previsualisation"
|
||||
"preview-book": "Previsualisation",
|
||||
"unknown-error": "Error incognite"
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
"book-filtering-all-languages": "Tutte le lingue",
|
||||
"count-of-matching-books": "{{COUNT}} libro/i",
|
||||
"download": "Scarica",
|
||||
"direct-download-alt-text": "Scarica direttamente tramite HTTP(S)",
|
||||
"magnet-alt-text": "Scarica tramite collegamento Magnet",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Scarica tramite BitTorrent",
|
||||
"download-links-title": "Scarica libro",
|
||||
"preview-book": "Anteprima",
|
||||
"unknown-error": "Errore sconosciuto"
|
||||
|
||||
@@ -15,5 +15,7 @@
|
||||
"500-page-heading": "내부 서버 오류",
|
||||
"fulltext-search-unavailable": "전문 검색을 사용할 수 없습니다",
|
||||
"random-page-button-text": "무작위로 선택된 문서로 이동",
|
||||
"hash-download-link-text": "SHA-256 체크섬",
|
||||
"torrent-download-link-text": "비트토렌트",
|
||||
"preview-book": "미리 보기"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
]
|
||||
},
|
||||
"name": "Lëtzebuergesch",
|
||||
"suggest-full-text-search": "enthält '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Buch net fonnt: {{BOOK_NAME}}",
|
||||
"too-many-books": "Ze vill Bicher ugefrot ({{NB_BOOKS}}), d'Limitt läit bei {{LIMIT}}",
|
||||
"url-not-found": "Déi ugefroten URL „{{url}}“ gouf op dësem Server net fonnt.",
|
||||
"suggest-search": "Maacht eng Volltext-Sich fir <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ups! Et konnt keen zoufällegen Artikel ausgewielt ginn :(",
|
||||
"404-page-title": "Inhalt net fonnt",
|
||||
@@ -15,6 +19,11 @@
|
||||
"500-page-heading": "Interne Feeler um Server",
|
||||
"500-page-text": "Et ass en interne Serverfeeler opgetrueden. Mir entschëllegen eis dofir :/",
|
||||
"fulltext-search-unavailable": "Volltext-Sich net verfügbar",
|
||||
"search-results-page-title": "Sichen: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Resultater <b>{{START}}-{{END}}</b> vu(n) <b>{{COUNT}}</b> fir <b>„{{{SEARCH_PATTERN}}}“</b>",
|
||||
"empty-search-results-page-header": "Keng Resultater fonnt fir <b>„{{{SEARCH_PATTERN}}}“</b>",
|
||||
"search-result-book-info": "aus {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} Wierder",
|
||||
"home-button-text": "Gitt op d'Haaptsäit vun '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Gitt op eng zoufälleg gewielte Säit",
|
||||
"searchbox-tooltip": "No '{{BOOK_TITLE}}' sichen",
|
||||
@@ -25,6 +34,18 @@
|
||||
"count-of-matching-books": "{{COUNT}} Buch/Bicher",
|
||||
"download": "Eroflueden",
|
||||
"direct-download-link-text": "Direkt",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"download-links-title": "Buch eroflueden",
|
||||
"unknown-error": "Onbekannte Feeler"
|
||||
"unknown-error": "Onbekannte Feeler",
|
||||
"book-category.stack_exchange": "Stack Exchange",
|
||||
"book-category.wikibooks": "Wikibooks",
|
||||
"book-category.wikinews": "Wikinews",
|
||||
"book-category.wikipedia": "Wikipedia",
|
||||
"book-category.wikiquote": "Wikiquote",
|
||||
"book-category.wikisource": "Wikisource",
|
||||
"book-category.wikispecies": "Wikispecies",
|
||||
"book-category.wikiversity": "Wikiversity",
|
||||
"book-category.wikivoyage": "Wikivoyage",
|
||||
"book-category.wiktionary": "Wiktionnaire",
|
||||
"book-category.other": "Anerer"
|
||||
}
|
||||
|
||||
@@ -45,20 +45,38 @@
|
||||
"count-of-matching-books": "{{COUNT}} книги",
|
||||
"download": "Преземи",
|
||||
"direct-download-link-text": "Непосредно",
|
||||
"direct-download-alt-text": "непосредно преземање",
|
||||
"hash-download-link-text": "Sha256-тараба",
|
||||
"hash-download-alt-text": "преземи тараба",
|
||||
"direct-download-alt-text": "Непосредно преземање преку HTTP(S)",
|
||||
"hash-download-link-text": "Контролен збир Sha256",
|
||||
"hash-download-alt-text": "Прикажи контролен збир SHA-256 на податотеката",
|
||||
"magnet-link-text": "Магнетна врска",
|
||||
"magnet-alt-text": "преземи магнет",
|
||||
"torrent-download-link-text": "Торентна податотека",
|
||||
"torrent-download-alt-text": "преземи торент",
|
||||
"magnet-alt-text": "Преземи преку Magnet-врска",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Преземи преку BitTorrent",
|
||||
"library-opds-feed-all-entries": "Библиотечен тековник на OPDS — Сите ставки",
|
||||
"filter-by-tag": "Филтрирај по ознаката „{{TAG}}“",
|
||||
"stop-filtering-by-tag": "Запри филтрирање по ознаката „{{TAG}}“",
|
||||
"filter-by-tag": "Филтрирај по ознаката „{{{TAG}}}“",
|
||||
"stop-filtering-by-tag": "Запри филтрирање по ознаката „{{{TAG}}}“",
|
||||
"library-opds-feed-parameterised": "Библиотечен тековник на OPDS — ставки што одговараат на {{#LANG}}\nЈазик: {{LANG}} {{/LANG}}{{#CATEGORY}}\nКатегорија: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nОзнака: {{TAG}} {{/TAG}}{{#Q}}\nБарање: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Добре дојдовте на Опслужувачот на Кивикс",
|
||||
"download-links-heading": "Врски за преземање на <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Преземи книга",
|
||||
"preview-book": "Преглед",
|
||||
"unknown-error": "Непозната грешка"
|
||||
"unknown-error": "Непозната грешка",
|
||||
"book-category.gutenberg": "Гутенберг",
|
||||
"book-category.iFixit": "iFixit",
|
||||
"book-category.mooc": "MOOC",
|
||||
"book-category.phet": "Phet",
|
||||
"book-category.stack_exchange": "Stack Exchange",
|
||||
"book-category.ted": "Ted",
|
||||
"book-category.vikidia": "Викидија",
|
||||
"book-category.wikibooks": "Викикниги",
|
||||
"book-category.wikihow": "wikiHow",
|
||||
"book-category.wikinews": "Викивести",
|
||||
"book-category.wikipedia": "Википедија",
|
||||
"book-category.wikiquote": "Викицитат",
|
||||
"book-category.wikisource": "Викиизвор",
|
||||
"book-category.wikispecies": "Викивидови",
|
||||
"book-category.wikiversity": "Викиуниверзитет",
|
||||
"book-category.wikivoyage": "Википатување",
|
||||
"book-category.wiktionary": "Викиречник",
|
||||
"book-category.other": "друго"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authors": [
|
||||
"Kelson",
|
||||
"McDutchie",
|
||||
"Siebrand",
|
||||
"Vistaus"
|
||||
]
|
||||
},
|
||||
@@ -24,8 +25,12 @@
|
||||
"404-page-heading": "Niet gevonden",
|
||||
"500-page-title": "Interne serverfout",
|
||||
"500-page-heading": "Interne serverfout",
|
||||
"500-page-text": "Er is een interne serverfout opgetreden. Dat spijt ons",
|
||||
"fulltext-search-unavailable": "Zoeken in volledige tekst is niet beschikbaar",
|
||||
"no-search-results": "De zoekmachine voor volledige tekst is niet beschikbaar voor deze inhoud.",
|
||||
"search-results-page-title": "Zoeken: {{SEARCH_PATTERN}}",
|
||||
"search-result-book-info": "uit {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} woorden",
|
||||
"library-button-text": "Naar de welkomstpagina",
|
||||
"home-button-text": "Naar de hoofdpagina van ‘{{BOOK_TITLE}}’",
|
||||
"random-page-button-text": "Naar een willekeurig geselecteerde pagina gaan",
|
||||
@@ -39,13 +44,13 @@
|
||||
"count-of-matching-books": "{{COUNT}} boek(en)",
|
||||
"download": "Downloaden",
|
||||
"direct-download-link-text": "Direct",
|
||||
"direct-download-alt-text": "directe download",
|
||||
"hash-download-link-text": "SHA256-hash",
|
||||
"hash-download-alt-text": "controlesom (hash) van de download",
|
||||
"direct-download-alt-text": "Direct downloaden via HTTP(S)",
|
||||
"hash-download-link-text": "SHA-256-controlesom",
|
||||
"hash-download-alt-text": "De SHA-256-controlesom van het bestand weergeven",
|
||||
"magnet-link-text": "Magnet-link",
|
||||
"magnet-alt-text": "magnet-link van de download",
|
||||
"torrent-download-link-text": "Torrent-bestand",
|
||||
"torrent-download-alt-text": "torrent downloaden",
|
||||
"magnet-alt-text": "Downloaden via Magnet-link",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Downloaden via BitTorrent",
|
||||
"library-opds-feed-all-entries": "OPDS-feed bibliotheek: alle vermeldingen",
|
||||
"filter-by-tag": "Filteren op label “{{TAG}}”",
|
||||
"stop-filtering-by-tag": "Niet meer filteren op label “{{TAG}}”",
|
||||
@@ -53,5 +58,6 @@
|
||||
"welcome-to-kiwix-server": "Welkom bij de Kiwix-server",
|
||||
"download-links-heading": "Downloadkoppelingen voor <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Boek downloaden",
|
||||
"preview-book": "Voorvertoning"
|
||||
"preview-book": "Voorvertoning",
|
||||
"unknown-error": "Onbekende fout"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Gouri"
|
||||
"Gouri",
|
||||
"Psubhashish"
|
||||
]
|
||||
},
|
||||
"name": "ଓଡ଼ିଆ",
|
||||
@@ -10,7 +11,7 @@
|
||||
"too-many-books": "ଅତ୍ୟଧିକ ବହି ଅନୁରୋଧ (${{NB_BOOKS}}) ଯେଉଁଠାରେ ସୀମା ${{LIMIT}} |",
|
||||
"no-book-found": "କୌଣସି ପୁସ୍ତକ ଚଯ଼ନ ମାନଦଣ୍ଡ ସହ ମେଳ ଖାଉନାହିଁ ।",
|
||||
"url-not-found": "ଅନୁରୋଧ କରାଯାଇଥିବା URL \"{{url}}\" ଏହି ସର୍ଭରରେ ମିଳିଲା ନାହିଁ |",
|
||||
"suggest-search": "<a href=\"${{{SEARCH_URL}}}\">${{PATTERN}} for</a> ପାଇଁ ଏକ ସମ୍ପୂର୍ଣ୍ଣ ପାଠ ସନ୍ଧାନ କର |",
|
||||
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> ପାଇଁ ପୂରା ପାଠ ଖୋଜନ୍ତୁ |",
|
||||
"random-article-failure": "ଓହୋ! ଏକ ଅନିୟମିତ ପ୍ରବନ୍ଧ ବାଛିବାରେ ବିଫଳ :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} କଞ୍ଚା ବିଷୟବସ୍ତୁ ପାଇଁ ଏକ ବ valid ଧ ଅନୁରୋଧ ନୁହେଁ |",
|
||||
"no-value-for-arg": "ଯୁକ୍ତି ପାଇଁ କୌଣସି ମୂଲ୍ଯ଼ ପ୍ରଦାନ କରାଯାଇନାହିଁ ${{ARGUMENT}}",
|
||||
|
||||
@@ -34,5 +34,6 @@
|
||||
"torrent-download-link-text": "Plik torrent",
|
||||
"welcome-to-kiwix-server": "Witamy na serwerze Kiwix",
|
||||
"download-links-title": "Pobierz książkę",
|
||||
"preview-book": "Podgląd"
|
||||
"preview-book": "Podgląd",
|
||||
"book-category.other": "Inne"
|
||||
}
|
||||
|
||||
44
static/skin/i18n/pt-br.json
Normal file
44
static/skin/i18n/pt-br.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Eduardoaddad",
|
||||
"Obiru",
|
||||
"Re demz",
|
||||
"YoReaper"
|
||||
]
|
||||
},
|
||||
"name": "Português",
|
||||
"suggest-full-text-search": "Contendo '{{{SEARCH_TERMS}}}'...",
|
||||
"400-page-title": "Requisição inválida",
|
||||
"400-page-heading": "Requisição inválida",
|
||||
"404-page-title": "Conteúdo não encontrado",
|
||||
"404-page-heading": "Não encontrado",
|
||||
"500-page-title": "Erro interno do servidor",
|
||||
"500-page-heading": "Erro interno do servidor",
|
||||
"500-page-text": "Aconteceu um erro interno do servidor. Nós pedimos desculpas sobre isso :/",
|
||||
"fulltext-search-unavailable": "Busca por texto completo está indisponível",
|
||||
"search-results-page-title": "Buscar: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Resultados <b>{{START}}-{{END}}</b> de <b>{{COUNT}}</b> para <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Nenhum resultado encontrado para <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "de {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} palavras",
|
||||
"library-button-text": "Ir para página inicial",
|
||||
"home-button-text": "Ir para página principal de '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Ir para uma página aleatória",
|
||||
"searchbox-tooltip": "Buscar '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Dois ou mais livros em diferentes idiomas podem participar da pesquisa, isso pode proporcionar resultados confusos.",
|
||||
"search": "Pesquisar",
|
||||
"book-filtering-all-categories": "Todas as categorias",
|
||||
"book-filtering-all-languages": "Todos os idiomas",
|
||||
"count-of-matching-books": "{{COUNT}} livro(s)",
|
||||
"download": "Baixar",
|
||||
"hash-download-link-text": "Verificação SHA-256",
|
||||
"hash-download-alt-text": "Exibir arquivo de verificação SHA-256",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Baixar via BitTorrent",
|
||||
"welcome-to-kiwix-server": "Bem vindo ao servidor Kiwix",
|
||||
"download-links-heading": "Links para baixar <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Download do livro",
|
||||
"preview-book": "Pré-visualizar",
|
||||
"unknown-error": "Erro desconhecido"
|
||||
}
|
||||
73
static/skin/i18n/pt.json
Normal file
73
static/skin/i18n/pt.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"B3rnas"
|
||||
]
|
||||
},
|
||||
"name": "português",
|
||||
"suggest-full-text-search": "contendo '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Não existe o livro: {{BOOK_NAME}}",
|
||||
"too-many-books": "Demasiadas solicitações de livros ({{NB_BOOKS}}) onde o limite é {{LIMIT}}",
|
||||
"no-book-found": "Nenhum livro corresponde aos critérios de seleção",
|
||||
"url-not-found": "A URL solicitada \"{{url}}\" não foi encontrada neste servidor.",
|
||||
"suggest-search": "Faça uma pesquisa de texto completo para <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ups! Erro ao eleger um artigo aleatório :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} não é uma solicitação válida para conteúdo bruto.",
|
||||
"invalid-request": "A URL solicitada \"{{{url}}}\" não é uma solicitação válida.",
|
||||
"no-value-for-arg": "Nenhum valor fornecido para o argumento {{ARGUMENT}}",
|
||||
"no-query": "Nenhuma consulta fornecida.",
|
||||
"raw-entry-not-found": "Não é possível encontrar a entrada {{DATATYPE}} {{ENTRY}}",
|
||||
"400-page-title": "Solicitação inválida",
|
||||
"400-page-heading": "Solicitação inválida",
|
||||
"404-page-title": "Conteúdo não encontrado",
|
||||
"404-page-heading": "Não encontrado",
|
||||
"500-page-title": "Erro interno do servidor",
|
||||
"500-page-heading": "Erro interno do servidor",
|
||||
"500-page-text": "Ocorreu um erro interno do servidor. Lamentamos por isso :/",
|
||||
"fulltext-search-unavailable": "Pesquisa de texto completo indisponível",
|
||||
"no-search-results": "O motor de busca de texto completo não está disponível para este conteúdo.",
|
||||
"search-results-page-title": "Pesquisar: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Resultados <b>{{START}}-{{END}}</b> de <b>{{COUNT}}</b> para <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Nenhum resultado foi encontrado para <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "de {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} palavras",
|
||||
"library-button-text": "Ir para página inicial",
|
||||
"home-button-text": "Vá para a página principal de '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Vá para uma página selecionada aleatoriamente",
|
||||
"searchbox-tooltip": "Procurar '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Dois ou mais livros em idiomas diferentes participariam da pesquisa, o que poderia levar a resultados confusos.",
|
||||
"welcome-page-overzealous-filter": "Nenhum resultado. Gostaria de <a href=\"{{URL}}\">redefinir o filtro</a> ?",
|
||||
"powered-by-kiwix-html": "Desenvolvido por <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Pesquisar",
|
||||
"book-filtering-all-categories": "Todas as categorias",
|
||||
"book-filtering-all-languages": "Todos os idiomas",
|
||||
"count-of-matching-books": "{{COUNT}} livro(s)",
|
||||
"download": "Transferir",
|
||||
"direct-download-link-text": "Direto",
|
||||
"direct-download-alt-text": "Descarregar diretamente através de HTTP (S)",
|
||||
"hash-download-link-text": "Verificação SHA-256",
|
||||
"hash-download-alt-text": "Exibir arquivo de verificação SHA-256",
|
||||
"magnet-link-text": "Link magnético",
|
||||
"magnet-alt-text": "Descarregar através do link Magnet",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Descarregar através de BitTorrent",
|
||||
"library-opds-feed-all-entries": "Feed OPDS da biblioteca - Todas as entradas",
|
||||
"filter-by-tag": "Filtrar por tag \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Pare de filtrar pela tag \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": "Feed OPDS da biblioteca - entradas que correspondem a {{#LANG}}\nIdioma: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategoria: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nConsulta: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Bem-vindo ao Kiwix Server",
|
||||
"download-links-heading": "Links para download de <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Descarregar livros",
|
||||
"preview-book": "Pré-visualização",
|
||||
"unknown-error": "Erro desconhecido",
|
||||
"book-category.wikibooks": "Wikilivros",
|
||||
"book-category.wikinews": "Wikinotícias",
|
||||
"book-category.wikipedia": "Wikipédia",
|
||||
"book-category.wikiquote": "Wikiquote",
|
||||
"book-category.wikisource": "Wikisource",
|
||||
"book-category.wikispecies": "Wikispecies",
|
||||
"book-category.wikiversity": "Wikiversidade",
|
||||
"book-category.wikivoyage": "Wikivoyage",
|
||||
"book-category.wiktionary": "Wikcionário",
|
||||
"book-category.other": "Outro"
|
||||
}
|
||||
@@ -62,5 +62,23 @@
|
||||
"download-links-title": "Title for no-js download page",
|
||||
"preview-book": "Tooltip of book-tile leading to the book",
|
||||
"non-translated-text": "{{ignored}}\nUsed to display text that is generated at runtime and cannot be translated. Nothing to translate about this one.",
|
||||
"unknown-error": "Unknown error"
|
||||
"unknown-error": "Unknown error",
|
||||
"book-category.gutenberg": "Name for the category of books from the Gutenberg project",
|
||||
"book-category.iFixit": "Name for the category of iFixit books",
|
||||
"book-category.mooc": "Name for the category of MOOC books",
|
||||
"book-category.phet": "Name for the category of Phet books",
|
||||
"book-category.stack_exchange": "Name for the category of books from the Stack Exchange network books",
|
||||
"book-category.ted": "Name for the category of Ted books",
|
||||
"book-category.vikidia": "Name for the category of Vikidia books",
|
||||
"book-category.wikibooks": "Name for the category of Wikibooks books books",
|
||||
"book-category.wikihow": "Name for the category of wikiHow books",
|
||||
"book-category.wikinews": "Name for the category of Wikinews books",
|
||||
"book-category.wikipedia": "Name for the category of Wikipedia books",
|
||||
"book-category.wikiquote": "Name for the category of Wikiquote books",
|
||||
"book-category.wikisource": "Name for the category of Wikisource books",
|
||||
"book-category.wikispecies": "Name for the category of Wikispecies books",
|
||||
"book-category.wikiversity": "Name for the category of Wikiversity books",
|
||||
"book-category.wikivoyage": "Name for the category of Wikivoyage books",
|
||||
"book-category.wiktionary": "Name for the category of Wiktionary books",
|
||||
"book-category.other": "Books not belonging to any special category are listed under this one"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authors": [
|
||||
"Fenixs-ru",
|
||||
"Kareyac",
|
||||
"Lutece398",
|
||||
"Okras",
|
||||
"Pacha Tchernof",
|
||||
"Razno0",
|
||||
@@ -50,20 +51,30 @@
|
||||
"count-of-matching-books": "{{COUNT}} книг(и)",
|
||||
"download": "Скачать",
|
||||
"direct-download-link-text": "Прямой",
|
||||
"direct-download-alt-text": "прямая загрузка",
|
||||
"hash-download-link-text": "Хэш Sha256",
|
||||
"hash-download-alt-text": "скачать хэш",
|
||||
"direct-download-alt-text": "Загрузка напрямую через HTTP(S)",
|
||||
"hash-download-link-text": "Контрольная сумма SHA-256",
|
||||
"hash-download-alt-text": "Показать контрольную сумму SHA-256 у файла",
|
||||
"magnet-link-text": "Магнитная ссылка",
|
||||
"magnet-alt-text": "скачать магнит",
|
||||
"torrent-download-link-text": "Торрент-файл",
|
||||
"torrent-download-alt-text": "скачать торрент",
|
||||
"magnet-alt-text": "Скачать по Magnet-ссылке",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Скачать через BitTorrent",
|
||||
"library-opds-feed-all-entries": "Канал библиотеки OPDS – все записи",
|
||||
"filter-by-tag": "Фильтровать по тегу \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Прекратить фильтрацию по тегу \"{{TAG}}\"",
|
||||
"filter-by-tag": "Фильтровать по тегу \"{{{TAG}}}\"",
|
||||
"stop-filtering-by-tag": "Прекратить фильтрацию по тегу \"{{{TAG}}}\"",
|
||||
"library-opds-feed-parameterised": "Канал OPDS библиотеки – записи, соответствующие {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nЗапрос: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Добро пожаловать на сервер Kiwix",
|
||||
"download-links-heading": "Ссылки для скачивания <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Скачать книгу",
|
||||
"preview-book": "Предпросмотр",
|
||||
"unknown-error": "Неизвестная ошибка"
|
||||
"unknown-error": "Неизвестная ошибка",
|
||||
"book-category.wikibooks": "Викиучебник",
|
||||
"book-category.wikinews": "Викиновости",
|
||||
"book-category.wikipedia": "Википедия",
|
||||
"book-category.wikiquote": "Викицитатник",
|
||||
"book-category.wikisource": "Викитека",
|
||||
"book-category.wikispecies": "Викивиды",
|
||||
"book-category.wikiversity": "Викиверситет",
|
||||
"book-category.wikivoyage": "Викигид",
|
||||
"book-category.wiktionary": "Викисловарь",
|
||||
"book-category.other": "Другое"
|
||||
}
|
||||
|
||||
@@ -22,5 +22,16 @@
|
||||
"torrent-download-link-text": "ٹورنٹ فائل",
|
||||
"torrent-download-alt-text": "ٹورںٹ ݙاؤن لوڈ کرو",
|
||||
"download-links-title": "کتاب ڈاؤن لوڈ کرو",
|
||||
"preview-book": "پیشگی ݙکھالا"
|
||||
"preview-book": "پیشگی ݙکھالا",
|
||||
"book-category.vikidia": "وکی ڈیا",
|
||||
"book-category.wikibooks": "وکی کتاباں",
|
||||
"book-category.wikinews": "وکی خبراں",
|
||||
"book-category.wikipedia": "وکیپیڈیا",
|
||||
"book-category.wikiquote": "وکی ٻول",
|
||||
"book-category.wikisource": "وکی ماخذ",
|
||||
"book-category.wikispecies": "وکی سپیشیز",
|
||||
"book-category.wikiversity": "وکی ورسٹی",
|
||||
"book-category.wikivoyage": "وکی سیرسپاٹا",
|
||||
"book-category.wiktionary": "وکشنری",
|
||||
"book-category.other": "ٻیا"
|
||||
}
|
||||
|
||||
@@ -47,13 +47,13 @@
|
||||
"count-of-matching-books": "{{COUNT}} böcker",
|
||||
"download": "Ladda ned",
|
||||
"direct-download-link-text": "Direkt",
|
||||
"direct-download-alt-text": "direktnedladdning",
|
||||
"hash-download-link-text": "Sha256-hash",
|
||||
"hash-download-alt-text": "ladda ned hash",
|
||||
"direct-download-alt-text": "Ladda ner direkt via HTTP(S)",
|
||||
"hash-download-link-text": "SHA-256-kontrollsiffra",
|
||||
"hash-download-alt-text": "Visa SHA-256-filens kontrollsiffra",
|
||||
"magnet-link-text": "Magnetlänk",
|
||||
"magnet-alt-text": "ladda ned magnet",
|
||||
"torrent-download-link-text": "Torrent-fil",
|
||||
"torrent-download-alt-text": "ladda ned torrent",
|
||||
"magnet-alt-text": "Ladda ner via Magnet-länk",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Ladda ner via BitTorrent",
|
||||
"library-opds-feed-all-entries": "Library OPDS Feed - Alla poster",
|
||||
"filter-by-tag": "Filtrera efter taggen \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Sluta filtrera efter taggen \"{{TAG}}\"",
|
||||
|
||||
64
static/skin/i18n/sw.json
Normal file
64
static/skin/i18n/sw.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Peggy",
|
||||
"Wangombe"
|
||||
]
|
||||
},
|
||||
"name": "Kiswahili",
|
||||
"suggest-full-text-search": "ina '{{{SEARCH_TERMS}}}}'...",
|
||||
"no-such-book": "Hakuna kitabu kama hiki: {{BOOK_NAME}}",
|
||||
"too-many-books": "Vitabu vingi mno vimeombwa ({{NB_BOOKS}}) ambapo kikomo ni {{LIMIT}}",
|
||||
"no-book-found": "Hakuna kitabu kinacholingana na vigezo vya uteuzi",
|
||||
"url-not-found": "URL iliyoombwa \"{{url}}\" haikupatikana kwenye seva hii.",
|
||||
"suggest-search": "Tafuta maandishi kamili ya <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Lo! Imeshindwa kuchagua makala nasibu :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} si ombi halali la maudhui ghafi.",
|
||||
"invalid-request": "URL iliyoombwa \"{{{url}}}\" si ombi halali.",
|
||||
"no-value-for-arg": "Hakuna thamani iliyotolewa kwa hoja {{ARGUMENT}}",
|
||||
"no-query": "Hakuna swali lililotolewa.",
|
||||
"raw-entry-not-found": "Haiwezi kupata ingizo la {{DATATYPE}} {{ENTRY}}",
|
||||
"400-page-title": "Ombi batili",
|
||||
"400-page-heading": "Ombi batili",
|
||||
"404-page-title": "Maudhui hayajapatikana",
|
||||
"404-page-heading": "Haijapatikana",
|
||||
"500-page-title": "Hitilafu ya Ndani ya Seva",
|
||||
"500-page-heading": "Hitilafu ya Ndani ya Seva",
|
||||
"500-page-text": "Hitilafu ya ndani ya seva imetokea. Tunasikitika kwa hilo:/",
|
||||
"fulltext-search-unavailable": "Utafutaji wa maandishi kamili haupatikani",
|
||||
"no-search-results": "Injini ya utafutaji ya maandishi kamili haipatikani kwa maudhui haya.",
|
||||
"search-results-page-title": "Tafuta: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Matokeo <b>{{START}}-{{END}}</b> ya <b>{{COUNT}}</b> ya <b>\"{{{SEARCH_PATTERN}}}}\"</b>",
|
||||
"empty-search-results-page-header": "Hakuna matokeo yaliyopatikana ya <b>\"{{{SEARCH_PATTERN}}}}\"</b>",
|
||||
"search-result-book-info": "kutoka kwa {{BOOK_TITLE}}",
|
||||
"word-count": "Maneno {{COUNT}}",
|
||||
"library-button-text": "Nenda katika wiki ya mwanzo",
|
||||
"home-button-text": "Nenda kwenye ukurasa mkuu wa '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Nenda kwa ukurasa uliochaguliwa kwa nasibu",
|
||||
"searchbox-tooltip": "Tafuta '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Vitabu viwili au zaidi katika lugha tofauti vitashiriki katika utafutaji, jambo ambalo linaweza kusababisha matokeo ya kutatanisha.",
|
||||
"welcome-page-overzealous-filter": "Hakuna matokeo. Je, ungependa <a href=\"{{URL}}\">kuweka upya kichujio</a> ?",
|
||||
"powered-by-kiwix-html": "Inaendeshwa na <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Tafuta",
|
||||
"book-filtering-all-categories": "Kategoria Zote",
|
||||
"book-filtering-all-languages": "Lugha zote",
|
||||
"count-of-matching-books": "Vitabu {{COUNT}}",
|
||||
"download": "Pakua",
|
||||
"direct-download-link-text": "Moja kwa moja",
|
||||
"direct-download-alt-text": "kupakua moja kwa moja",
|
||||
"hash-download-link-text": "Sha256 heshi",
|
||||
"hash-download-alt-text": "pakua heshi",
|
||||
"magnet-link-text": "Kiungo cha sumaku",
|
||||
"magnet-alt-text": "sumaku ya kupakua",
|
||||
"torrent-download-link-text": "Faili ya Torrent",
|
||||
"torrent-download-alt-text": "pakua torrent",
|
||||
"library-opds-feed-all-entries": "Mlisho wa OPDS wa Maktaba - Maingizo yote",
|
||||
"filter-by-tag": "Chuja kwa lebo \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Acha kuchuja kwa lebo \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": "Mlisho wa OPDS wa Maktaba - maingizo yanayolingana {{#LANG}}\nLugha: {{LANG}} {{/LANG}}{{#CATEGORY}}\nKitengo: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nSwali: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Karibu kwenye Seva ya Kiwix",
|
||||
"download-links-heading": "Pakua viungo vya <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Pakua vitabu",
|
||||
"preview-book": "Hakiki",
|
||||
"unknown-error": "Hitilafu isiyojulikana"
|
||||
}
|
||||
@@ -47,4 +47,22 @@
|
||||
, "empty-search-results-page-header": "[I18N TESTING] No results were found for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
|
||||
, "search-result-book-info": "from [I18N TESTING] {{BOOK_TITLE}}"
|
||||
, "word-count": "{{COUNT}} [I18N TESTING] words"
|
||||
, "book-category.gutenberg": "[I18N] Gutenberg [TESTING]"
|
||||
, "book-category.iFixit": "[I18N] iFixit [TESTING]"
|
||||
, "book-category.mooc": "[I18N] MOOC [TESTING]"
|
||||
, "book-category.phet": "[I18N] Phet [TESTING]"
|
||||
, "book-category.stack_exchange": "[I18N] Stack Exchange [TESTING]"
|
||||
, "book-category.ted": "[I18N] Ted [TESTING]"
|
||||
, "book-category.vikidia": "[I18N] Vikidia [TESTING]"
|
||||
, "book-category.wikibooks": "[I18N] Wikibooks [TESTING]"
|
||||
, "book-category.wikihow": "[I18N] wikiHow [TESTING]"
|
||||
, "book-category.wikinews": "[I18N] Wikinews [TESTING]"
|
||||
, "book-category.wikipedia": "[I18N] Wikipedia [TESTING]"
|
||||
, "book-category.wikiquote": "[I18N] Wikiquote [TESTING]"
|
||||
, "book-category.wikisource": "[I18N] Wikisource [TESTING]"
|
||||
, "book-category.wikispecies": "[I18N] Wikispecies [TESTING]"
|
||||
, "book-category.wikiversity": "[I18N] Wikiversity [TESTING]"
|
||||
, "book-category.wikivoyage": "[I18N] Wikivoyage [TESTING]"
|
||||
, "book-category.wiktionary": "[I18N] Wiktionary [TESTING]"
|
||||
, "book-category.other": "[I18N] Other [TESTING]"
|
||||
}
|
||||
|
||||
@@ -3,19 +3,21 @@
|
||||
"authors": [
|
||||
"GuoPC",
|
||||
"IceButBin",
|
||||
"Kichin",
|
||||
"StarrySky",
|
||||
"Sunai",
|
||||
"XtexChooser"
|
||||
"XtexChooser",
|
||||
"沈澄心"
|
||||
]
|
||||
},
|
||||
"name": "英语",
|
||||
"name": "简体中文",
|
||||
"suggest-full-text-search": "正在查找「{{{SEARCH_TERMS}}}」…",
|
||||
"no-such-book": "没有名为“{{BOOK_NAME}}”的图书",
|
||||
"too-many-books": "请求的图书过多 ({{NB_BOOKS}}),上限为 {{LIMIT}}",
|
||||
"no-book-found": "没有符合搜索要求的图书",
|
||||
"url-not-found": "在此服务器上找不到请求的 URL:{{url}}",
|
||||
"suggest-search": "对<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>进行全文搜索",
|
||||
"random-article-failure": "抱歉!随机条目失败了 (〒﹏〒)",
|
||||
"random-article-failure": "抱歉! 随机条目失败了 (〒﹏〒) 【好生草的表情(】",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} 对原请求无效。",
|
||||
"invalid-request": "请求的URL无效:{{{url}}}",
|
||||
"no-value-for-arg": "参数{{ARGUMENT}}无值",
|
||||
@@ -27,7 +29,7 @@
|
||||
"404-page-heading": "未找到",
|
||||
"500-page-title": "内部服务器错误",
|
||||
"500-page-heading": "内部服务器错误",
|
||||
"500-page-text": "内部服务器出现错误。真的十分抱歉 (;ŏ﹏ŏ)",
|
||||
"500-page-text": "内部服务器出现错误。真的十分抱歉 (;ŏ﹏ŏ~)",
|
||||
"fulltext-search-unavailable": "全文搜索不可用",
|
||||
"no-search-results": "全文搜索引擎不适用于该内容。",
|
||||
"search-results-page-title": "搜索:{{SEARCH_PATTERN}}",
|
||||
@@ -35,7 +37,7 @@
|
||||
"empty-search-results-page-header": "未找到<b>“{{{SEARCH_PATTERN}}}”</b>的结果",
|
||||
"search-result-book-info": "来自{{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} 个字",
|
||||
"library-button-text": "前往欢迎页面",
|
||||
"library-button-text": "转到欢迎页面",
|
||||
"home-button-text": "转到“{{BOOK_TITLE}}”的主页",
|
||||
"random-page-button-text": "前往随机选择的页面",
|
||||
"searchbox-tooltip": "搜索“{{BOOK_TITLE}}”",
|
||||
@@ -48,17 +50,30 @@
|
||||
"count-of-matching-books": "{{COUNT}} 本书",
|
||||
"download": "下载",
|
||||
"direct-download-link-text": "直接",
|
||||
"direct-download-alt-text": "直接下載",
|
||||
"hash-download-link-text": "Sha256 哈希值",
|
||||
"hash-download-alt-text": "下载哈希值",
|
||||
"direct-download-alt-text": "通过 HTTP(S) 直接下載",
|
||||
"hash-download-link-text": "SHA-256 校验和",
|
||||
"hash-download-alt-text": "显示 SHA-256 文件校验和",
|
||||
"magnet-link-text": "磁力链接",
|
||||
"magnet-alt-text": "下载磁力链接",
|
||||
"torrent-download-link-text": "种子文件",
|
||||
"torrent-download-alt-text": "下载种子文件",
|
||||
"magnet-alt-text": "通过磁力链接下载",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "通过 BitTorrent 下载",
|
||||
"library-opds-feed-all-entries": "图书馆 OPDS Feed - 所有条目",
|
||||
"filter-by-tag": "按标签“{{TAG}}”过滤",
|
||||
"stop-filtering-by-tag": "停止按标签“{{TAG}}”过滤",
|
||||
"filter-by-tag": "按标签“{{{TAG}}}”过滤",
|
||||
"stop-filtering-by-tag": "停止按标签“{{{TAG}}}”过滤",
|
||||
"library-opds-feed-parameterised": "图书馆 OPDS Feed - 匹配的项目 {{#LANG}}\n语言:{{LANG}} {{/LANG}}{{#CATEGORY}}\n分类:{{CATEGORY}} {{/CATEGORY}}{{#TAG}}\n标签:{{TAG}} {{/TAG}}{{#Q}}\n查询:{{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "欢迎来到 Kiwix 服务器",
|
||||
"preview-book": "预览"
|
||||
"download-links-heading": "下载<b><i>{{BOOK_TITLE}}</i></b>的链接",
|
||||
"download-links-title": "下载书籍",
|
||||
"preview-book": "预览",
|
||||
"unknown-error": "未知错误",
|
||||
"book-category.wikibooks": "维基教科书",
|
||||
"book-category.wikinews": "维基新闻",
|
||||
"book-category.wikipedia": "维基百科",
|
||||
"book-category.wikiquote": "维基语录",
|
||||
"book-category.wikisource": "维基文库",
|
||||
"book-category.wikispecies": "维基物种",
|
||||
"book-category.wikiversity": "维基学院",
|
||||
"book-category.wikivoyage": "维基导游",
|
||||
"book-category.wiktionary": "维基词典",
|
||||
"book-category.other": "其他"
|
||||
}
|
||||
|
||||
@@ -46,13 +46,13 @@
|
||||
"count-of-matching-books": "{{COUNT}} 本書籍",
|
||||
"download": "下載",
|
||||
"direct-download-link-text": "直接",
|
||||
"direct-download-alt-text": "直接下載",
|
||||
"hash-download-link-text": "Sha256 雜湊",
|
||||
"hash-download-alt-text": "下載雜湊",
|
||||
"direct-download-alt-text": "直接透過 HTTP(S)下載",
|
||||
"hash-download-link-text": "SHA-256 核對和",
|
||||
"hash-download-alt-text": "顯示 SHA-256 檔案核對和",
|
||||
"magnet-link-text": "Magnet 連結",
|
||||
"magnet-alt-text": "下載 magnet",
|
||||
"torrent-download-link-text": "Torrent 檔案",
|
||||
"torrent-download-alt-text": "下載 torrent",
|
||||
"magnet-alt-text": "透過磁力連結下載",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "透過 BitTorrent 下載",
|
||||
"library-opds-feed-all-entries": "圖書館 OPDS 摘要 - 所有項目",
|
||||
"filter-by-tag": "依標籤「{{TAG}}」篩選",
|
||||
"stop-filtering-by-tag": "停止依標籤「{{TAG}}」篩選",
|
||||
@@ -61,5 +61,23 @@
|
||||
"download-links-heading": "下載<b><i>{{BOOK_TITLE}}</i></b>的連結",
|
||||
"download-links-title": "下載書籍",
|
||||
"preview-book": "預覽",
|
||||
"unknown-error": "不明錯誤"
|
||||
"unknown-error": "不明錯誤",
|
||||
"book-category.gutenberg": "古騰堡計劃",
|
||||
"book-category.iFixit": "iFixit",
|
||||
"book-category.mooc": "MOOC",
|
||||
"book-category.phet": "PhET",
|
||||
"book-category.stack_exchange": "Stack Exchange",
|
||||
"book-category.ted": "Ted",
|
||||
"book-category.vikidia": "Vikidia",
|
||||
"book-category.wikibooks": "維基教科書",
|
||||
"book-category.wikihow": "wikiHow",
|
||||
"book-category.wikinews": "維基新聞",
|
||||
"book-category.wikipedia": "維基百科",
|
||||
"book-category.wikiquote": "維基語錄",
|
||||
"book-category.wikisource": "維基文庫",
|
||||
"book-category.wikispecies": "維基物種",
|
||||
"book-category.wikiversity": "維基學院",
|
||||
"book-category.wikivoyage": "維基導遊",
|
||||
"book-category.wiktionary": "維基詞典",
|
||||
"book-category.other": "其他"
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
|
||||
.tagFilterLabel {
|
||||
width: max-content;
|
||||
padding: 10px;
|
||||
padding: 7px;
|
||||
font-family: roboto;
|
||||
font-size: 12px;
|
||||
margin: 0 0 0 17px;
|
||||
@@ -152,23 +152,24 @@
|
||||
|
||||
.book__link {
|
||||
text-decoration: none;
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
.book__wrapper {
|
||||
--tile-width: 250px;
|
||||
--tile-border-width: 1px;
|
||||
--tile-border-radius: 3px;
|
||||
color: #444343;
|
||||
height: 248px;
|
||||
width: 250px;
|
||||
height: 258px;
|
||||
width: var(--tile-width);
|
||||
margin: 15px;
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #ececec;
|
||||
border-radius: 3px;
|
||||
display: grid;
|
||||
grid-template-columns: 65px 1fr;
|
||||
grid-template-rows: 70px 120px 1fr 1fr;
|
||||
grid-gap: 5px;
|
||||
border: var(--tile-border-width) solid #ececec;
|
||||
border-radius: var(--tile-border-radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
transition: transform 0.25s;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.book__wrapper:hover {
|
||||
@@ -177,8 +178,12 @@
|
||||
|
||||
.book__link__wrapper {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"bookIcon bookHeader"
|
||||
"bookDesc bookDesc"
|
||||
;
|
||||
grid-template-columns: 65px 1fr;
|
||||
grid-template-rows: 70px 120px 1fr 1fr;
|
||||
grid-template-rows: 70px 120px;
|
||||
}
|
||||
|
||||
.book__icon {
|
||||
@@ -189,7 +194,10 @@
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 10px 0 0 10px;
|
||||
grid-area: bookIcon;
|
||||
}
|
||||
|
||||
.book__header {
|
||||
@@ -201,6 +209,7 @@
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
grid-area: bookHeader;
|
||||
}
|
||||
|
||||
.book__title {
|
||||
@@ -211,28 +220,36 @@
|
||||
}
|
||||
|
||||
.book__download {
|
||||
font-size: 1.1rem;
|
||||
margin: 3px 0;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
background: #00b4e4;
|
||||
padding: 8px;
|
||||
border-radius: 0 0 var(--tile-border-radius) var(--tile-border-radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: var(--tile-width);
|
||||
left: calc(0px - var(--tile-border-width));
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.book__download > img {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.book__download > span {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
padding: 0 3px 1px;
|
||||
font-family: roboto;
|
||||
background: #00b4e4;
|
||||
color: white;
|
||||
box-shadow: 0 0 0 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.book__download > span:hover {
|
||||
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.1)
|
||||
.book__download:hover {
|
||||
background: royalblue;
|
||||
}
|
||||
|
||||
.book__description {
|
||||
grid-column: 1 / 3;
|
||||
margin: 10px 10px 5px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
@@ -243,6 +260,13 @@
|
||||
font-size: 1.2rem;
|
||||
color: #4d4d4d;
|
||||
line-height: 1.25;
|
||||
grid-area: bookDesc;
|
||||
}
|
||||
|
||||
.book__meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px 4px;
|
||||
}
|
||||
|
||||
.book__languageTag {
|
||||
@@ -255,7 +279,6 @@
|
||||
color: black;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
margin: 10px auto 0 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
@@ -266,8 +289,6 @@
|
||||
font-size: 1.1rem;
|
||||
justify-content: flex-end;
|
||||
font-family: roboto;
|
||||
margin-right: 10px;
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,15 +78,21 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
function humanFriendlyNumStr(num, precision) {
|
||||
const n = Math.abs(num).toFixed().length;
|
||||
return num.toFixed(Math.max(0, precision - n));
|
||||
}
|
||||
|
||||
const humanFriendlySize = (fileSize) => {
|
||||
if (fileSize === 0) {
|
||||
return '';
|
||||
}
|
||||
const units = ['bytes', 'kB', 'MB', 'GB', 'TB'];
|
||||
let quotient = Math.floor(Math.log10(fileSize) / 3);
|
||||
quotient = quotient < units.length ? quotient : units.length - 1;
|
||||
fileSize /= (1000 ** quotient);
|
||||
return `${+fileSize.toFixed(2)} ${units[quotient]}`;
|
||||
const units = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||
let quotient = Math.floor(Math.log2(fileSize) / 10);
|
||||
quotient = Math.min(quotient, units.length - 1);
|
||||
fileSize /= (1024 ** quotient);
|
||||
const fileSizeStr = humanFriendlyNumStr(fileSize, 3);
|
||||
return `${fileSizeStr} ${units[quotient]}`;
|
||||
};
|
||||
|
||||
const humanFriendlyTitle = (title) => {
|
||||
@@ -99,6 +105,14 @@
|
||||
return '';
|
||||
}
|
||||
|
||||
// Borrowed from https://stackoverflow.com/a/1912522
|
||||
function htmlDecode(input){
|
||||
var e = document.createElement('textarea');
|
||||
e.innerHTML = input;
|
||||
// handle case of empty input
|
||||
return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
|
||||
}
|
||||
|
||||
function htmlEncode(str) {
|
||||
return str.replace(/[\u00A0-\u9999<>\&]/gim, (i) => `&#${i.charCodeAt(0)};`);
|
||||
}
|
||||
@@ -115,9 +129,27 @@
|
||||
|
||||
function generateTagLink(tagValue) {
|
||||
tagValue = tagValue.toLowerCase();
|
||||
const humanFriendlyTagValue = humanFriendlyTitle(tagValue);
|
||||
const tagMessage = $t("filter-by-tag", {TAG: humanFriendlyTagValue});
|
||||
return `<span class='tag__link' aria-label='${tagMessage}' title='${tagMessage}' data-tag=${tagValue}>${humanFriendlyTagValue}</span>`
|
||||
const tagMessage = $t("filter-by-tag", {TAG: tagValue});
|
||||
const spanElement = document.createElement("span");
|
||||
spanElement.className = 'tag__link';
|
||||
spanElement.setAttribute('aria-label', tagMessage);
|
||||
spanElement.setAttribute('title', tagMessage);
|
||||
spanElement.setAttribute('data-tag', tagValue);
|
||||
spanElement.innerHTML = htmlEncode(tagValue);
|
||||
return spanElement.outerHTML;
|
||||
}
|
||||
|
||||
function downloadButtonText(zimSize) {
|
||||
return $t("download") + (zimSize ? ` - ${zimSize}`: '');
|
||||
}
|
||||
|
||||
function downloadButtonHtml(url, zimSize) {
|
||||
return url
|
||||
? `<div class="book__download" data-link="${url}" data-size="${zimSize}">
|
||||
<img src="${root}/skin/download-white.svg?KIWIXCACHEID">
|
||||
<span ">${downloadButtonText(zimSize)}</span>
|
||||
</div>`
|
||||
: '';
|
||||
}
|
||||
|
||||
function generateBookHtml(book, sort = false) {
|
||||
@@ -138,7 +170,7 @@
|
||||
const mulLangList = langCodesList.filter(x => languages.hasOwnProperty(x)).map(x => languages[x]);
|
||||
language = mulLangList.join(', ');
|
||||
}
|
||||
const tags = getInnerHtml(book, 'tags');
|
||||
const tags = htmlDecode(getInnerHtml(book, 'tags'));
|
||||
const tagList = tags.split(';').filter(tag => {return !(tag.startsWith('_'))});
|
||||
const tagFilterLinks = tagList.map((tagValue) => generateTagLink(tagValue));
|
||||
const tagHtml = tagFilterLinks.join(' | ');
|
||||
@@ -164,6 +196,7 @@
|
||||
}
|
||||
const faviconAttr = iconUrl != undefined ? `style="background-image: url('${iconUrl}')"` : '';
|
||||
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
|
||||
|
||||
divTag.innerHTML = `
|
||||
<div class="book__wrapper">
|
||||
<a class="book__link" href="${viewerLink}" data-hover="Preview">
|
||||
@@ -171,13 +204,15 @@
|
||||
<div class="book__icon" ${faviconAttr}></div>
|
||||
<div class="book__header">
|
||||
<div id="book__title">${title}</div>
|
||||
${downloadLink ? `<div class="book__download"><span data-link="${downloadLink}">${$t("download")} ${humanFriendlyZimSize ? ` - ${humanFriendlyZimSize}</span></div>`: ''}` : ''}
|
||||
</div>
|
||||
<div class="book__description" title="${description}">${description}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="book__meta">
|
||||
<div class="book__languageTag" ${languageAttr}>${getLanguageCodeToDisplay(langCode)}</div>
|
||||
<div class="book__tags"><div class="book__tags--wrapper">${tagHtml}</div></div>
|
||||
</div>
|
||||
${downloadButtonHtml(downloadLink, humanFriendlyZimSize)}
|
||||
</div></div>`;
|
||||
return divTag;
|
||||
}
|
||||
@@ -263,6 +298,7 @@
|
||||
}
|
||||
|
||||
function insertModal(button) {
|
||||
const downloadSize = button.getAttribute('data-size');
|
||||
const downloadLink = button.getAttribute('data-link');
|
||||
button.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
@@ -272,7 +308,7 @@
|
||||
<div class="modal-heading">
|
||||
<div class="modal-title">
|
||||
<div>
|
||||
Download
|
||||
${downloadButtonText(downloadSize)}
|
||||
</div>
|
||||
</div>
|
||||
<div onclick="closeModal()" class="modal-close-button">
|
||||
@@ -432,7 +468,7 @@
|
||||
booksToDelete.forEach(book => {iso.remove(book);});
|
||||
books.forEach((book) => {
|
||||
iso.insert(generateBookHtml(book, sort))
|
||||
const downloadButton = document.querySelector(`[data-id="${getInnerHtml(book, 'id')}"] .book__download span`);
|
||||
const downloadButton = document.querySelector(`[data-id="${getInnerHtml(book, 'id')}"] .book__download`);
|
||||
if (downloadButton) {
|
||||
insertModal(downloadButton);
|
||||
}
|
||||
@@ -486,9 +522,8 @@
|
||||
function addTagElement(tagValue, resetFilter) {
|
||||
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
|
||||
tagElement.style.display = 'inline-block';
|
||||
const humanFriendlyTagValue = humanFriendlyTitle(tagValue);
|
||||
tagElement.innerHTML = `${humanFriendlyTagValue}`;
|
||||
const tagMessage = $t("stop-filtering-by-tag", {TAG: humanFriendlyTagValue});
|
||||
tagElement.innerHTML = htmlEncode(tagValue);
|
||||
const tagMessage = $t("stop-filtering-by-tag", {TAG: tagValue});
|
||||
tagElement.setAttribute('aria-label', tagMessage);
|
||||
tagElement.setAttribute('title', tagMessage);
|
||||
if (resetFilter)
|
||||
|
||||
@@ -22,13 +22,18 @@ const uiLanguages = [
|
||||
{
|
||||
"iso_code": "dag",
|
||||
"self_name": "Silimiinsili",
|
||||
"translation_count": 24
|
||||
"translation_count": 48
|
||||
},
|
||||
{
|
||||
"iso_code": "de",
|
||||
"self_name": "Deutsch",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "el",
|
||||
"self_name": "Αγγλικά",
|
||||
"translation_count": 23
|
||||
},
|
||||
{
|
||||
"iso_code": "en",
|
||||
"self_name": "English",
|
||||
@@ -37,12 +42,12 @@ const uiLanguages = [
|
||||
{
|
||||
"iso_code": "es",
|
||||
"self_name": "español",
|
||||
"translation_count": 48
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"iso_code": "fi",
|
||||
"self_name": "suomi",
|
||||
"translation_count": 22
|
||||
"translation_count": 29
|
||||
},
|
||||
{
|
||||
"iso_code": "fr",
|
||||
@@ -72,7 +77,7 @@ const uiLanguages = [
|
||||
{
|
||||
"iso_code": "ia",
|
||||
"self_name": "interlingua",
|
||||
"translation_count": 49
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "ig",
|
||||
@@ -82,7 +87,7 @@ const uiLanguages = [
|
||||
{
|
||||
"iso_code": "it",
|
||||
"self_name": "italiano",
|
||||
"translation_count": 34
|
||||
"translation_count": 38
|
||||
},
|
||||
{
|
||||
"iso_code": "ja",
|
||||
@@ -92,7 +97,7 @@ const uiLanguages = [
|
||||
{
|
||||
"iso_code": "ko",
|
||||
"self_name": "한국어",
|
||||
"translation_count": 13
|
||||
"translation_count": 15
|
||||
},
|
||||
{
|
||||
"iso_code": "ku-latn",
|
||||
@@ -134,6 +139,11 @@ const uiLanguages = [
|
||||
"self_name": "Polski",
|
||||
"translation_count": 31
|
||||
},
|
||||
{
|
||||
"iso_code": "pt-br",
|
||||
"self_name": "Português",
|
||||
"translation_count": 35
|
||||
},
|
||||
{
|
||||
"iso_code": "ru",
|
||||
"self_name": "русский",
|
||||
@@ -169,6 +179,11 @@ const uiLanguages = [
|
||||
"self_name": "Svenska",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "sw",
|
||||
"self_name": "Kiswahili",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "te",
|
||||
"self_name": "ఇంగ్లీషు",
|
||||
@@ -181,8 +196,8 @@ const uiLanguages = [
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hans",
|
||||
"self_name": "英语",
|
||||
"translation_count": 54
|
||||
"self_name": "简体中文",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hant",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
max-width: 30ch;
|
||||
}
|
||||
|
||||
.kiwix .kiwix_centered {
|
||||
|
||||
@@ -310,6 +310,12 @@ function blockLink(url) {
|
||||
: url;
|
||||
}
|
||||
|
||||
function urlMustBeHandledByAnExternalApp(url) {
|
||||
const WHITELISTED_URL_SCHEMATA = ['http:', 'https:', 'about:', 'javascript:'];
|
||||
|
||||
return WHITELISTED_URL_SCHEMATA.indexOf(url.protocol) == -1;
|
||||
}
|
||||
|
||||
function isExternalUrl(url) {
|
||||
if ( url.startsWith(window.location.origin) )
|
||||
return false;
|
||||
@@ -329,20 +335,34 @@ function getRealHref(target) {
|
||||
return target_href;
|
||||
}
|
||||
|
||||
function setHrefAvoidingWombatRewriting(target, url) {
|
||||
const old_no_rewrite = target._no_rewrite;
|
||||
target._no_rewrite = true;
|
||||
target.setAttribute("href", url);
|
||||
target._no_rewrite = old_no_rewrite;
|
||||
}
|
||||
|
||||
function onClickEvent(e) {
|
||||
const iframeDocument = contentIframe.contentDocument;
|
||||
const target = matchingAncestorElement(e.target, iframeDocument, "a");
|
||||
if (target !== null && "href" in target) {
|
||||
const target_href = getRealHref(target);
|
||||
if (isExternalUrl(target_href)) {
|
||||
const target_url = new URL(target_href, iframeDocument.location);
|
||||
const isExternalAppUrl = urlMustBeHandledByAnExternalApp(target_url);
|
||||
if ( isExternalAppUrl && !viewerSettings.linkBlockingEnabled ) {
|
||||
target.setAttribute("target", "_blank");
|
||||
}
|
||||
|
||||
if (isExternalAppUrl || isExternalUrl(target_href)) {
|
||||
const possiblyBlockedLink = blockLink(target_href);
|
||||
if ( e.ctrlKey || e.shiftKey ) {
|
||||
// The link will be loaded in a new tab/window - update the link
|
||||
// and let the browser handle the rest.
|
||||
target.setAttribute("href", possiblyBlockedLink);
|
||||
setHrefAvoidingWombatRewriting(target, possiblyBlockedLink);
|
||||
} else {
|
||||
// Load the external URL in the viewer window (rather than iframe)
|
||||
contentIframe.contentWindow.parent.location = possiblyBlockedLink;
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
id="headFeedLink"
|
||||
href="{{root}}/catalog/v2/entries"
|
||||
/>
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="{{root}}/catalog/searchdescription.xml" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{root}}/skin/favicon/apple-touch-icon.png?KIWIXCACHEID">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{root}}/skin/favicon/favicon-32x32.png?KIWIXCACHEID">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{root}}/skin/favicon/favicon-16x16.png?KIWIXCACHEID">
|
||||
@@ -92,7 +93,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<form id='kiwixSearchForm' class='kiwixNav__SearchForm'>
|
||||
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
|
||||
<input type="text" name="q" accesskey="s" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
|
||||
<span class="kiwixButton tagFilterLabel"></span>
|
||||
<input type="submit" class="kiwixButton kiwixButtonHover" id="searchButton" value="Search"/>
|
||||
</form>
|
||||
|
||||
@@ -50,20 +50,11 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.book__link__wrapper {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
.book__link {
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
.kiwixHomeBody__results {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
#book__title>a, .book__download a {
|
||||
#book__title>a {
|
||||
text-decoration: none;
|
||||
all: unset;
|
||||
}
|
||||
@@ -90,7 +81,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<form id='kiwixSearchForm' class='kiwixNav__SearchForm' action="{{root}}/nojs">
|
||||
<input type="text" name="q" placeholder="{{translations.search}}" id="searchFilter" class='kiwixSearch filter' value="{{searchQuery}}">
|
||||
<input type="text" name="q" accesskey="s" placeholder="{{translations.search}}" id="searchFilter" class='kiwixSearch filter' value="{{searchQuery}}">
|
||||
<input type="submit" class="kiwixButton kiwixButtonHover" value="{{translations.search}}"/>
|
||||
</form>
|
||||
</div>
|
||||
@@ -117,25 +108,32 @@
|
||||
<h3 class="kiwixHomeBody__results">{{translations.count-of-matching-books}}</h3>
|
||||
{{#books}}
|
||||
<div class="book__wrapper">
|
||||
<div class="book__link__wrapper">
|
||||
<div class="book__icon" {{faviconAttr}}></div>
|
||||
<div class="book__header">
|
||||
<div id="book__title"><a href="{{root}}/content/{{id}}">{{title}}</a></div>
|
||||
{{#downloadAvailable}}
|
||||
<div class="book__download"><span><a href="{{root}}/nojs/download/{{id}}">{{translations.download}}</a></span></div>
|
||||
{{/downloadAvailable}}
|
||||
</div>
|
||||
<a class="book__link" href="{{root}}/content/{{id}}" title="{{translations.preview-book}}" aria-label="{{translations.preview-book}}">
|
||||
<div class="book__description" title="{{description}}">{{description}}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="book__languageTag" {{languageAttr}}>{{langCode}}</div>
|
||||
<div class="book__tags"><div class="book__tags--wrapper">
|
||||
{{#tagList}}
|
||||
<span class="tag__link" aria-label='{{tag}}' title='{{tag}}'>{{tag}}</span>
|
||||
{{/tagList}}
|
||||
<a class="book__link" href="{{root}}/content/{{id}}" title="{{translations.preview-book}}" aria-label="{{translations.preview-book}}">
|
||||
<div class="book__link__wrapper">
|
||||
<div class="book__icon" {{faviconAttr}}></div>
|
||||
<div class="book__header">
|
||||
<div id="book__title">{{title}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="book__description" title="{{description}}">{{description}}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="book__meta">
|
||||
<div class="book__languageTag" {{languageAttr}}>{{langCode}}</div>
|
||||
<div class="book__tags"><div class="book__tags--wrapper">
|
||||
{{#tagList}}
|
||||
<span class="tag__link" aria-label='{{tag}}' title='{{tag}}'>{{tag}}</span>
|
||||
{{/tagList}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#downloadAvailable}}
|
||||
<div>
|
||||
<a class="book__download" href="{{root}}/nojs/download/{{id}}">
|
||||
<img src="{{root}}/skin/download-white.svg?KIWIXCACHEID">
|
||||
<span>{{translations.download}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{/downloadAvailable}}
|
||||
</div>
|
||||
{{/books}}
|
||||
</div>
|
||||
|
||||
@@ -45,14 +45,22 @@
|
||||
<input type="checkbox" id="kiwix_button_show_toggle">
|
||||
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?KIWIXCACHEID" alt=""></label>
|
||||
<div class="kiwix_button_cont">
|
||||
<a id="kiwix_serve_taskbar_library_button" title="Go to welcome page" aria-label="Go to welcome page" href="./"><button>🏠</button></a>
|
||||
<a id="kiwix_serve_taskbar_library_button"
|
||||
title="Go to welcome page"
|
||||
accesskey="w"
|
||||
aria-label="Go to welcome page"
|
||||
href="./">
|
||||
<button>🏠</button>
|
||||
</a>
|
||||
<span id="kiwix_serve_taskbar_book_ui_group">
|
||||
<a id="kiwix_serve_taskbar_home_button"
|
||||
title="Go to the main page of the current book"
|
||||
accesskey="h"
|
||||
aria-label="Go to the main page of the current book"
|
||||
onclick="gotoMainPageOfCurrentBook()"></a>
|
||||
<a id="kiwix_serve_taskbar_random_button"
|
||||
title="Go to a randomly selected page"
|
||||
accesskey="r"
|
||||
aria-label="Go to a randomly selected page"
|
||||
onclick="gotoRandomPage()">
|
||||
<button>🎲</button>
|
||||
|
||||
@@ -15,11 +15,19 @@ struct XMLDoc : pugi::xml_document
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
#if _WIN32
|
||||
# define DATA_ABS_PATH "C:\\data\\zim"
|
||||
# define ZARA_ABS_PATH "C:\\data\\zim\\zara.zim"
|
||||
#else
|
||||
# define DATA_ABS_PATH "/data/zim"
|
||||
# define ZARA_ABS_PATH "/data/zim/zara.zim"
|
||||
#endif
|
||||
|
||||
TEST(BookTest, updateFromXMLTest)
|
||||
{
|
||||
const XMLDoc xml(R"(
|
||||
<book id="zara"
|
||||
path="./zara.zim"
|
||||
path="zara.zim"
|
||||
url="https://who.org/zara.zim"
|
||||
title="Catch an infection in 24 hours"
|
||||
description="Complete guide to contagious diseases"
|
||||
@@ -40,9 +48,9 @@ TEST(BookTest, updateFromXMLTest)
|
||||
)");
|
||||
|
||||
kiwix::Book book;
|
||||
book.updateFromXml(xml.child("book"), "/data/zim");
|
||||
book.updateFromXml(xml.child("book"), DATA_ABS_PATH);
|
||||
|
||||
EXPECT_EQ(book.getPath(), "/data/zim/zara.zim");
|
||||
EXPECT_EQ(book.getPath(), ZARA_ABS_PATH);
|
||||
EXPECT_EQ(book.getUrl(), "https://who.org/zara.zim");
|
||||
EXPECT_EQ(book.getTitle(), "Catch an infection in 24 hours");
|
||||
EXPECT_EQ(book.getDescription(), "Complete guide to contagious diseases");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<library version="20110515">
|
||||
<book id="5dc0b3af-5df2-0925-f0ca-d2bf75e78af6" path="example.zim" title="Wikibooks" description="testZim" language="eng" creator="test" publisher="test" tags="_ftindex:yes;_ftindex:yes;_pictures:yes;_videos:yes;_details:yes" date="2021-04-17" mediaCount="22" size="253" />
|
||||
<book id="5dc0b3af-5df2-0925-f0ca-d2bf75e78af6" path="example.zim" title="Wikibooks" description="testZim" language="eng" creator="test" publisher="test" name="bookname_of_example_zim" tags="_ftindex:yes;_ftindex:yes;_pictures:yes;_videos:yes;_details:yes" date="2021-04-17" mediaCount="22" size="253" />
|
||||
<book id="6f1d19d0-633f-087b-fb55-7ac324ff9baf" path="zimfile.zim" title="Ray Charles" description="Wikipedia articles about Ray Charles" language="eng" creator="Wikipedia" publisher="Kiwix" name="wikipedia_en_ray_charles" flavour="_mini" tags="wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes" date="2020-03-31" articleCount="129" mediaCount="45" size="555" />
|
||||
</library>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<library version="1.0">
|
||||
<book
|
||||
id="raycharles"
|
||||
path="./zimfile.zim"
|
||||
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim"
|
||||
path="./zimfile_raycharles.zim"
|
||||
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile_raycharles.zim"
|
||||
title="Ray Charles"
|
||||
description="Wikipedia articles about Ray Charles"
|
||||
description="Wikipedia articles about Ray Charles (not all of them but near to what an average newborn may find more than enough)"
|
||||
language="eng"
|
||||
creator="Wikipedia"
|
||||
publisher="Kiwix"
|
||||
@@ -19,10 +19,10 @@
|
||||
></book>
|
||||
<book
|
||||
id="raycharles_uncategorized"
|
||||
path="./zimfile.zim"
|
||||
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim"
|
||||
path="./zimfile_raycharles_uncategorized.zim"
|
||||
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile_raycharles_uncategorized.zim"
|
||||
title="Ray (uncategorized) Charles"
|
||||
description="No category is assigned to this library entry."
|
||||
description="No category is assigned to this library entry (neither adj nor xor was considered a good option)"
|
||||
language="rus,eng"
|
||||
creator="Wikipedia"
|
||||
publisher="Kiwix"
|
||||
@@ -39,7 +39,7 @@
|
||||
path="./zimfile&other.zim"
|
||||
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile%26other.zim"
|
||||
title="Charles, Ray"
|
||||
description="Wikipedia articles about Ray Charles"
|
||||
description="Wikipedia articles about Ray Charles or why and when one should go to library"
|
||||
language="fra"
|
||||
creator="Wikipedia"
|
||||
publisher="Kiwix"
|
||||
|
||||
1
test/data/zimfile_raycharles.zim
Symbolic link
1
test/data/zimfile_raycharles.zim
Symbolic link
@@ -0,0 +1 @@
|
||||
zimfile.zim
|
||||
1
test/data/zimfile_raycharles_uncategorized.zim
Symbolic link
1
test/data/zimfile_raycharles_uncategorized.zim
Symbolic link
@@ -0,0 +1 @@
|
||||
zimfile.zim
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "../src/server/i18n.h"
|
||||
#include "../src/server/i18n_utils.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace kiwix;
|
||||
@@ -48,3 +48,17 @@ TEST(ParameterizedMessage, messagesWithParameters)
|
||||
EXPECT_EQ(msg.getText("test"), "Filter [I18N] by [TESTING] tag \"\"");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(I18n, translateBookCategory)
|
||||
{
|
||||
|
||||
EXPECT_EQ(translateBookCategory("en", "ted"), "Ted");
|
||||
EXPECT_EQ(translateBookCategory("test", "ted"), "[I18N] Ted [TESTING]");
|
||||
|
||||
EXPECT_EQ(translateBookCategory("en", "stack_exchange"), "Stack Exchange");
|
||||
EXPECT_EQ(translateBookCategory("test", "stack_exchange"), "[I18N] Stack Exchange [TESTING]");
|
||||
|
||||
// unknown categories are simply not translated
|
||||
EXPECT_EQ(translateBookCategory("en", "Qwerty"), "Qwerty");
|
||||
EXPECT_EQ(translateBookCategory("test", "Qwerty"), "Qwerty");
|
||||
}
|
||||
|
||||
@@ -267,11 +267,21 @@ const char * sampleOpdsStream = R"(
|
||||
|
||||
)";
|
||||
|
||||
#ifdef _WIN32
|
||||
# define ZIMFILE_PATH ".\\zimfile.zim"
|
||||
# define EXAMPLE_PATH ".\\example.zim"
|
||||
# define LIBRARY_PATH ".\\test\\library.xml"
|
||||
#else
|
||||
# define ZIMFILE_PATH "./zimfile.zim"
|
||||
# define EXAMPLE_PATH "./example.zim"
|
||||
# define LIBRARY_PATH "./test/library.xml"
|
||||
#endif
|
||||
|
||||
const char sampleLibraryXML[] = R"(
|
||||
<library version="1.0">
|
||||
<book
|
||||
id="raycharles"
|
||||
path="./zimfile.zim"
|
||||
path=")" ZIMFILE_PATH R"("
|
||||
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim"
|
||||
title="Ray Charles"
|
||||
description="Wikipedia articles about Ray Charles"
|
||||
@@ -287,7 +297,7 @@ const char sampleLibraryXML[] = R"(
|
||||
></book>
|
||||
<book
|
||||
id="example"
|
||||
path="./example.zim"
|
||||
path=")" EXAMPLE_PATH R"("
|
||||
title="An example ZIM archive"
|
||||
description="An eXaMpLe book added to the catalog via XML"
|
||||
language="deu"
|
||||
@@ -383,7 +393,7 @@ class LibraryTest : public ::testing::Test {
|
||||
void SetUp() override {
|
||||
kiwix::Manager manager(lib);
|
||||
manager.readOpds(sampleOpdsStream, "foo.urlHost");
|
||||
manager.readXml(sampleLibraryXML, false, "./test/library.xml", true);
|
||||
manager.readXml(sampleLibraryXML, false, LIBRARY_PATH, true);
|
||||
}
|
||||
|
||||
kiwix::Bookmark createBookmark(const std::string &id, const std::string& url="", const std::string& title="") {
|
||||
|
||||
@@ -103,7 +103,7 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||
#define _CHARLES_RAY_CATALOG_ENTRY(CONTENT_NAME) CATALOG_ENTRY( \
|
||||
"charlesray", \
|
||||
"Charles, Ray", \
|
||||
"Wikipedia articles about Ray Charles", \
|
||||
"Wikipedia articles about Ray Charles or why and when one should go to library", \
|
||||
"fra", \
|
||||
"wikipedia_fr_ray_charles",\
|
||||
"jazz",\
|
||||
@@ -120,7 +120,7 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||
#define _RAY_CHARLES_CATALOG_ENTRY(CONTENT_NAME) CATALOG_ENTRY(\
|
||||
"raycharles",\
|
||||
"Ray Charles",\
|
||||
"Wikipedia articles about Ray Charles",\
|
||||
"Wikipedia articles about Ray Charles (not all of them but near to what an average newborn may find more than enough)",\
|
||||
"eng",\
|
||||
"wikipedia_en_ray_charles",\
|
||||
"wikipedia",\
|
||||
@@ -129,24 +129,24 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||
" href=\"/ROOT%23%3F/catalog/v2/illustration/raycharles/?size=48\"\n" \
|
||||
" type=\"image/png;width=48;height=48;scale=1\"/>\n ", \
|
||||
CONTENT_NAME, \
|
||||
"zimfile", \
|
||||
"zimfile_raycharles", \
|
||||
"569344"\
|
||||
)
|
||||
|
||||
#define RAY_CHARLES_CATALOG_ENTRY _RAY_CHARLES_CATALOG_ENTRY("zimfile")
|
||||
#define RAY_CHARLES_CATALOG_ENTRY _RAY_CHARLES_CATALOG_ENTRY("zimfile_raycharles")
|
||||
#define RAY_CHARLES_CATALOG_ENTRY_NO_MAPPER _RAY_CHARLES_CATALOG_ENTRY("raycharles")
|
||||
|
||||
#define UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY CATALOG_ENTRY(\
|
||||
"raycharles_uncategorized",\
|
||||
"Ray (uncategorized) Charles",\
|
||||
"No category is assigned to this library entry.",\
|
||||
"No category is assigned to this library entry (neither adj nor xor was considered a good option)",\
|
||||
"rus,eng",\
|
||||
"wikipedia_ru_ray_charles",\
|
||||
"",\
|
||||
"public_tag_with_a_value:value_of_a_public_tag;_private_tag_with_a_value:value_of_a_private_tag;wikipedia;_pictures:no;_videos:no;_details:no",\
|
||||
"",\
|
||||
"zimfile", \
|
||||
"zimfile", \
|
||||
"zimfile_raycharles_uncategorized", \
|
||||
"zimfile_raycharles_uncategorized", \
|
||||
"125952"\
|
||||
)
|
||||
|
||||
@@ -199,8 +199,8 @@ TEST_F(LibraryServerTest, catalog_search_by_phrase)
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
@@ -218,8 +218,8 @@ TEST_F(LibraryServerTest, catalog_search_by_words)
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>3</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
@@ -239,8 +239,8 @@ TEST_F(LibraryServerTest, catalog_prefix_search)
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
@@ -275,8 +275,8 @@ TEST_F(LibraryServerTest, catalog_search_with_word_exclusion)
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
@@ -331,8 +331,8 @@ TEST_F(LibraryServerTest, catalog_search_by_category)
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
@@ -772,10 +772,171 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms)
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_entries_filtering_special_queries)
|
||||
{
|
||||
{
|
||||
// 'or' doesn't act as a Xapian boolean operator
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=Or");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=Or")
|
||||
" <title>Filtered Entries (q=Or)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// 'and' doesn't act as a Xapian boolean operator
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=and");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=and")
|
||||
" <title>Filtered Entries (q=and)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// 'not' doesn't act as a Xapian boolean operator
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=not");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=not")
|
||||
" <title>Filtered Entries (q=not)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// 'xor' doesn't act as a Xapian boolean operator
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=xor");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=xor")
|
||||
" <title>Filtered Entries (q=xor)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// 'or' acts as a Xapian boolean operator
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=wikipedia%20or%20library");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=wikipedia%20or%20library")
|
||||
" <title>Filtered Entries (q=wikipedia%20or%20library)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// 'and' acts as a Xapian boolean operator
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=wikipedia%20and%20articles");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=wikipedia%20and%20articles")
|
||||
" <title>Filtered Entries (q=wikipedia%20and%20articles)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// 'near' doesn't act as a Xapian query operator
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=near");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=near")
|
||||
" <title>Filtered Entries (q=near)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// 'adj' doesn't act as a Xapian query operator
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=adj");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=adj")
|
||||
" <title>Filtered Entries (q=adj)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// 'near' doesn't act as a Xapian query operator
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=charles%20near%20why");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=charles%20near%20why")
|
||||
" <title>Filtered Entries (q=charles%20near%20why)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>0</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>0</itemsPerPage>\n"
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// 'adj' doesn't act as a Xapian query operator
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=charles%20adj%20why");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=charles%20adj%20why")
|
||||
" <title>Filtered Entries (q=charles%20adj%20why)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>0</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>0</itemsPerPage>\n"
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_language)
|
||||
@@ -841,8 +1002,8 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_category)
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
@@ -1033,7 +1194,7 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
|
||||
" />\n" \
|
||||
" <link\n" \
|
||||
" type=\"text/css\"\n" \
|
||||
" href=\"/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf\"\n" \
|
||||
" href=\"/ROOT%23%3F/skin/index.css?cacheid=ae79e41a\"\n" \
|
||||
" rel=\"Stylesheet\"\n" \
|
||||
" />\n" \
|
||||
" <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/ROOT%23%3F/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3\">\n" \
|
||||
@@ -1066,17 +1227,10 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
|
||||
" .tag__link {\n" \
|
||||
" pointer-events: none;\n" \
|
||||
" }\n\n" \
|
||||
" .book__link__wrapper {\n" \
|
||||
" grid-column: 1 / 3;\n" \
|
||||
" grid-row: 1 / 3;\n" \
|
||||
" }\n\n" \
|
||||
" .book__link {\n" \
|
||||
" grid-row: 2 / 3;\n" \
|
||||
" }\n\n" \
|
||||
" .kiwixHomeBody__results {\n" \
|
||||
" flex-basis: 100%;\n" \
|
||||
" }\n\n" \
|
||||
" #book__title>a, .book__download a {\n" \
|
||||
" #book__title>a {\n" \
|
||||
" text-decoration: none;\n" \
|
||||
" all: unset;\n" \
|
||||
" }\n" \
|
||||
@@ -1087,62 +1241,83 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
|
||||
|
||||
#define CHARLES_RAY_BOOK_HTML \
|
||||
" <div class=\"book__wrapper\">\n" \
|
||||
" <div class=\"book__link__wrapper\">\n" \
|
||||
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/charlesray/?size=48)></div>\n" \
|
||||
" <div class=\"book__header\">\n" \
|
||||
" <div id=\"book__title\"><a href=\"/ROOT%23%3F/content/zimfile%26other\">Charles, Ray</a></div>\n" \
|
||||
" <div class=\"book__download\"><span><a href=\"/ROOT%23%3F/nojs/download/zimfile%26other\">Download</a></span></div>\n" \
|
||||
" </div>\n" \
|
||||
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile%26other\" title=\"Preview\" aria-label=\"Preview\">\n" \
|
||||
" <div class=\"book__description\" title=\"Wikipedia articles about Ray Charles\">Wikipedia articles about Ray Charles</div>\n" \
|
||||
" </a>\n" \
|
||||
" </div>\n" \
|
||||
" <div class=\"book__languageTag\" >fra</div>\n" \
|
||||
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
|
||||
" <span class=\"tag__link\" aria-label='unittest' title='unittest'>unittest</span>\n" \
|
||||
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
|
||||
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile%26other\" title=\"Preview\" aria-label=\"Preview\">\n" \
|
||||
" <div class=\"book__link__wrapper\">\n" \
|
||||
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/charlesray/?size=48)></div>\n" \
|
||||
" <div class=\"book__header\">\n" \
|
||||
" <div id=\"book__title\">Charles, Ray</div>\n" \
|
||||
" </div>\n" \
|
||||
" <div class=\"book__description\" title=\"Wikipedia articles about Ray Charles or why and when one should go to library\">Wikipedia articles about Ray Charles or why and when one should go to library</div>\n" \
|
||||
" </div>\n" \
|
||||
" </a>\n" \
|
||||
" <div class=\"book__meta\">\n" \
|
||||
" <div class=\"book__languageTag\" >fra</div>\n" \
|
||||
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
|
||||
" <span class=\"tag__link\" aria-label='unittest' title='unittest'>unittest</span>\n" \
|
||||
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" <div>\n" \
|
||||
" <a class=\"book__download\" href=\"/ROOT%23%3F/nojs/download/zimfile%26other\">\n" \
|
||||
" <img src=\"/ROOT%23%3F/skin/download-white.svg?cacheid=079ab989\">\n" \
|
||||
" <span>Download</span>\n" \
|
||||
" </a>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n"
|
||||
|
||||
#define RAY_CHARLES_BOOK_HTML \
|
||||
" <div class=\"book__wrapper\">\n" \
|
||||
" <div class=\"book__link__wrapper\">\n" \
|
||||
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/raycharles/?size=48)></div>\n" \
|
||||
" <div class=\"book__header\">\n" \
|
||||
" <div id=\"book__title\"><a href=\"/ROOT%23%3F/content/zimfile\">Ray Charles</a></div>\n" \
|
||||
" <div class=\"book__download\"><span><a href=\"/ROOT%23%3F/nojs/download/zimfile\">Download</a></span></div>\n" \
|
||||
" </div>\n" \
|
||||
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile\" title=\"Preview\" aria-label=\"Preview\">\n" \
|
||||
" <div class=\"book__description\" title=\"Wikipedia articles about Ray Charles\">Wikipedia articles about Ray Charles</div>\n" \
|
||||
" </a>\n" \
|
||||
" </div>\n" \
|
||||
" <div class=\"book__languageTag\" >eng</div>\n" \
|
||||
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
|
||||
" <span class=\"tag__link\" aria-label='public_tag_without_a_value' title='public_tag_without_a_value'>public_tag_without_a_value</span>\n" \
|
||||
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
|
||||
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile_raycharles\" title=\"Preview\" aria-label=\"Preview\">\n" \
|
||||
" <div class=\"book__link__wrapper\">\n" \
|
||||
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/raycharles/?size=48)></div>\n" \
|
||||
" <div class=\"book__header\">\n" \
|
||||
" <div id=\"book__title\">Ray Charles</div>\n" \
|
||||
" </div>\n" \
|
||||
" <div class=\"book__description\" title=\"Wikipedia articles about Ray Charles (not all of them but near to what an average newborn may find more than enough)\">Wikipedia articles about Ray Charles (not all of them but near to what an average newborn may find more than enough)</div>\n" \
|
||||
" </div>\n" \
|
||||
" </a>\n" \
|
||||
" <div class=\"book__meta\">\n" \
|
||||
" <div class=\"book__languageTag\" >eng</div>\n" \
|
||||
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
|
||||
" <span class=\"tag__link\" aria-label='public_tag_without_a_value' title='public_tag_without_a_value'>public_tag_without_a_value</span>\n" \
|
||||
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" <div>\n" \
|
||||
" <a class=\"book__download\" href=\"/ROOT%23%3F/nojs/download/zimfile_raycharles\">\n" \
|
||||
" <img src=\"/ROOT%23%3F/skin/download-white.svg?cacheid=079ab989\">\n" \
|
||||
" <span>Download</span>\n" \
|
||||
" </a>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n"
|
||||
|
||||
#define RAY_CHARLES_UNCTZ_BOOK_HTML \
|
||||
" <div class=\"book__wrapper\">\n" \
|
||||
" <div class=\"book__link__wrapper\">\n" \
|
||||
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/raycharles_uncategorized/?size=48)></div>\n" \
|
||||
" <div class=\"book__header\">\n" \
|
||||
" <div id=\"book__title\"><a href=\"/ROOT%23%3F/content/zimfile\">Ray (uncategorized) Charles</a></div>\n" \
|
||||
" <div class=\"book__download\"><span><a href=\"/ROOT%23%3F/nojs/download/zimfile\">Download</a></span></div>\n" \
|
||||
" </div>\n" \
|
||||
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile\" title=\"Preview\" aria-label=\"Preview\">\n" \
|
||||
" <div class=\"book__description\" title=\"No category is assigned to this library entry.\">No category is assigned to this library entry.</div>\n" \
|
||||
" </a>\n" \
|
||||
" </div>\n" \
|
||||
" <div class=\"book__languageTag\" >rus,eng</div>\n" \
|
||||
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
|
||||
" <span class=\"tag__link\" aria-label='public_tag_with_a_value:value_of_a_public_tag' title='public_tag_with_a_value:value_of_a_public_tag'>public_tag_with_a_value:value_of_a_public_tag</span>\n" \
|
||||
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
|
||||
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile_raycharles_uncategorized\" title=\"Preview\" aria-label=\"Preview\">\n" \
|
||||
" <div class=\"book__link__wrapper\">\n" \
|
||||
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/raycharles_uncategorized/?size=48)></div>\n" \
|
||||
" <div class=\"book__header\">\n" \
|
||||
" <div id=\"book__title\">Ray (uncategorized) Charles</div>\n" \
|
||||
" </div>\n" \
|
||||
" <div class=\"book__description\" title=\"No category is assigned to this library entry (neither adj nor xor was considered a good option)\">No category is assigned to this library entry (neither adj nor xor was considered a good option)</div>\n" \
|
||||
" </div>\n" \
|
||||
" </a>\n" \
|
||||
" <div class=\"book__meta\">\n" \
|
||||
" <div class=\"book__languageTag\" >rus,eng</div>\n" \
|
||||
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
|
||||
" <span class=\"tag__link\" aria-label='public_tag_with_a_value:value_of_a_public_tag' title='public_tag_with_a_value:value_of_a_public_tag'>public_tag_with_a_value:value_of_a_public_tag</span>\n" \
|
||||
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" <div>\n" \
|
||||
" <a class=\"book__download\" href=\"/ROOT%23%3F/nojs/download/zimfile_raycharles_uncategorized\">\n" \
|
||||
" <img src=\"/ROOT%23%3F/skin/download-white.svg?cacheid=079ab989\">\n" \
|
||||
" <span>Download</span>\n" \
|
||||
" </a>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n"
|
||||
|
||||
#define FINAL_HTML_TEXT \
|
||||
@@ -1171,7 +1346,7 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" <form id='kiwixSearchForm' class='kiwixNav__SearchForm' action=\"/ROOT%23%3F/nojs\">\n" \
|
||||
" <input type=\"text\" name=\"q\" placeholder=\"Search\" id=\"searchFilter\" class='kiwixSearch filter' value=\"\">\n" \
|
||||
" <input type=\"text\" name=\"q\" accesskey=\"s\" placeholder=\"Search\" id=\"searchFilter\" class='kiwixSearch filter' value=\"\">\n" \
|
||||
" <input type=\"submit\" class=\"kiwixButton kiwixButtonHover\" value=\"Search\"/>\n" \
|
||||
" </form>\n" \
|
||||
" </div>\n"
|
||||
@@ -1224,17 +1399,17 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
|
||||
" <div class=\"downloadLinksTitle\">\n" \
|
||||
" Download links for <b><i>Ray (uncategorized) Charles</i></b>\n" \
|
||||
" </div>\n" \
|
||||
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim\" download>\n" \
|
||||
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile_raycharles_uncategorized.zim\" download>\n" \
|
||||
" <div>Direct</div>\n" \
|
||||
" </a>\n" \
|
||||
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim.sha256\" download>\n" \
|
||||
" <div>Sha256 hash</div>\n" \
|
||||
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile_raycharles_uncategorized.zim.sha256\" download>\n" \
|
||||
" <div>SHA-256 checksum</div>\n" \
|
||||
" </a>\n" \
|
||||
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim.magnet\" target=\"_blank\">\n" \
|
||||
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile_raycharles_uncategorized.zim.magnet\" target=\"_blank\">\n" \
|
||||
" <div>Magnet link</div>\n" \
|
||||
" </a>\n" \
|
||||
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim.torrent\" download>\n" \
|
||||
" <div>Torrent file</div>\n" \
|
||||
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile_raycharles_uncategorized.zim.torrent\" download>\n" \
|
||||
" <div>BitTorrent</div>\n" \
|
||||
" </a>\n" \
|
||||
"</body>\n" \
|
||||
"</html>"
|
||||
@@ -1273,7 +1448,7 @@ TEST_F(LibraryServerTest, noJS) {
|
||||
FINAL_HTML_TEXT);
|
||||
|
||||
// no_js_download
|
||||
r = zfs1_->GET("/ROOT%23%3F/nojs/download/zimfile");
|
||||
r = zfs1_->GET("/ROOT%23%3F/nojs/download/zimfile_raycharles_uncategorized");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(r->body, RAY_CHARLES_UNCTZ_DOWNLOAD);
|
||||
}
|
||||
|
||||
@@ -25,11 +25,23 @@ TEST(ManagerTest, addBookFromPathAndGetIdTest)
|
||||
EXPECT_EQ(book.getUrl(), url);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#if _WIN32
|
||||
# define UNITTEST_ZIM_PATH "zimfiles\\unittest.zim"
|
||||
# define LIB_ABS_PATH "C:\\data\\lib.xml"
|
||||
# define ZIM_ABS_PATH "C:\\data\\zimfiles\\unittest.zim"
|
||||
#else
|
||||
# define UNITTEST_ZIM_PATH "zimfiles/unittest.zim"
|
||||
# define LIB_ABS_PATH "/data/lib.xml"
|
||||
# define ZIM_ABS_PATH "/data/zimfiles/unittest.zim"
|
||||
#endif
|
||||
|
||||
const char sampleLibraryXML[] = R"(
|
||||
<library version="1.0">
|
||||
<book
|
||||
id="0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2"
|
||||
path="zimfiles/unittest.zim"
|
||||
path=")" UNITTEST_ZIM_PATH R"("
|
||||
url="https://example.com/zimfiles/unittest.zim"
|
||||
title="Unit Test"
|
||||
description="Wikipedia articles about unit testing"
|
||||
@@ -51,9 +63,9 @@ TEST(ManagerTest, readXml)
|
||||
auto lib = kiwix::Library::create();
|
||||
kiwix::Manager manager = kiwix::Manager(lib);
|
||||
|
||||
EXPECT_EQ(true, manager.readXml(sampleLibraryXML, true, "/data/lib.xml", true));
|
||||
EXPECT_EQ(true, manager.readXml(sampleLibraryXML, true, LIB_ABS_PATH, true));
|
||||
kiwix::Book book = lib->getBookById("0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2");
|
||||
EXPECT_EQ("/data/zimfiles/unittest.zim", book.getPath());
|
||||
EXPECT_EQ(ZIM_ABS_PATH, book.getPath());
|
||||
EXPECT_EQ("https://example.com/zimfiles/unittest.zim", book.getUrl());
|
||||
EXPECT_EQ("Unit Test", book.getTitle());
|
||||
EXPECT_EQ("Wikipedia articles about unit testing", book.getDescription());
|
||||
|
||||
@@ -38,6 +38,8 @@ if gtest_dep.found() and not meson.is_cross_build()
|
||||
'example.zim',
|
||||
'zimfile.zim',
|
||||
'zimfile&other.zim',
|
||||
'zimfile_raycharles.zim',
|
||||
'zimfile_raycharles_uncategorized.zim',
|
||||
'corner_cases#&.zim',
|
||||
'poor.zim',
|
||||
'library.xml',
|
||||
@@ -75,7 +77,7 @@ if gtest_dep.found() and not meson.is_cross_build()
|
||||
implicit_include_directories: false,
|
||||
include_directories : inc,
|
||||
link_with : libkiwix,
|
||||
link_args: extra_link_args,
|
||||
link_args: extra_libs,
|
||||
dependencies : all_deps + [gtest_dep],
|
||||
build_rpath : '$ORIGIN')
|
||||
test(test_name, test_exe, timeout : 160)
|
||||
|
||||
@@ -7,6 +7,23 @@
|
||||
namespace
|
||||
{
|
||||
|
||||
#if _WIN32
|
||||
const char libraryXML[] = R"(
|
||||
<library version="1.0">
|
||||
<book id="01" path="C:\data\zero_one.zim"> </book>
|
||||
<book id="02" path="C:\data\zero two.zim"> </book>
|
||||
<book id="03" path="C:\data\ZERO thrêë.zim"> </book>
|
||||
<book id="04-2021-10" path="C:\data\zero_four_2021-10.zim"></book>
|
||||
<book id="04-2021-11" path="C:\data\zero_four_2021-11.zim"></book>
|
||||
<book id="05-a" path="C:\data\zero_five-a.zim" name="zero_five"></book>
|
||||
<book id="05-b" path="C:\data\zero_five-b.zim" name="zero_five"></book>
|
||||
<book id="06+" path="C:\data\zërô + SIX.zim"></book>
|
||||
<book id="06plus" path="C:\data\zero_plus_six.zim"></book>
|
||||
<book id="07-super" path="C:\data\zero_seven.zim"></book>
|
||||
<book id="07-sub" path="C:\data\subdir\zero_seven.zim"></book>
|
||||
</library>
|
||||
)";
|
||||
#else
|
||||
const char libraryXML[] = R"(
|
||||
<library version="1.0">
|
||||
<book id="01" path="/data/zero_one.zim"> </book>
|
||||
@@ -14,8 +31,15 @@ const char libraryXML[] = R"(
|
||||
<book id="03" path="/data/ZERO thrêë.zim"> </book>
|
||||
<book id="04-2021-10" path="/data/zero_four_2021-10.zim"></book>
|
||||
<book id="04-2021-11" path="/data/zero_four_2021-11.zim"></book>
|
||||
<book id="05-a" path="/data/zero_five-a.zim" name="zero_five"></book>
|
||||
<book id="05-b" path="/data/zero_five-b.zim" name="zero_five"></book>
|
||||
<book id="06+" path="/data/zërô + SIX.zim"></book>
|
||||
<book id="06plus" path="/data/zero_plus_six.zim"></book>
|
||||
<book id="07-super" path="/data/zero_seven.zim"></book>
|
||||
<book id="07-sub" path="/data/subdir/zero_seven.zim"></book>
|
||||
</library>
|
||||
)";
|
||||
#endif
|
||||
|
||||
class NameMapperTest : public ::testing::Test {
|
||||
public:
|
||||
@@ -55,6 +79,47 @@ public:
|
||||
operator std::string() const { return buffer.str(); }
|
||||
};
|
||||
|
||||
#if _WIN32
|
||||
const std::string ZERO_FOUR_NAME_CONFLICT_MSG =
|
||||
"Path collision: 'C:\\data\\zero_four_2021-10.zim' and"
|
||||
" 'C:\\data\\zero_four_2021-11.zim' can't share the same URL path 'zero_four'."
|
||||
" Therefore, only 'C:\\data\\zero_four_2021-10.zim' will be served.\n";
|
||||
|
||||
const std::string ZERO_SIX_NAME_CONFLICT_MSG =
|
||||
"Path collision: 'C:\\data\\zërô + SIX.zim' and "
|
||||
"'C:\\data\\zero_plus_six.zim' can't share the same URL path 'zero_plus_six'."
|
||||
" Therefore, only 'C:\\data\\zërô + SIX.zim' will be served.\n";
|
||||
|
||||
const std::string ZERO_SEVEN_NAME_CONFLICT_MSG =
|
||||
"Path collision: 'C:\\data\\subdir\\zero_seven.zim' and"
|
||||
" 'C:\\data\\zero_seven.zim' can't share the same URL path 'zero_seven'."
|
||||
" Therefore, only 'C:\\data\\subdir\\zero_seven.zim' will be served.\n";
|
||||
#else
|
||||
const std::string ZERO_FOUR_NAME_CONFLICT_MSG =
|
||||
"Path collision: '/data/zero_four_2021-10.zim' and"
|
||||
" '/data/zero_four_2021-11.zim' can't share the same URL path 'zero_four'."
|
||||
" Therefore, only '/data/zero_four_2021-10.zim' will be served.\n";
|
||||
|
||||
const std::string ZERO_SIX_NAME_CONFLICT_MSG =
|
||||
"Path collision: '/data/zërô + SIX.zim' and "
|
||||
"'/data/zero_plus_six.zim' can't share the same URL path 'zero_plus_six'."
|
||||
" Therefore, only '/data/zërô + SIX.zim' will be served.\n";
|
||||
|
||||
const std::string ZERO_SEVEN_NAME_CONFLICT_MSG =
|
||||
"Path collision: '/data/subdir/zero_seven.zim' and"
|
||||
" '/data/zero_seven.zim' can't share the same URL path 'zero_seven'."
|
||||
" Therefore, only '/data/subdir/zero_seven.zim' will be served.\n";
|
||||
#endif
|
||||
|
||||
// Name conflicts in the default mode (without the --nodatealiases is off
|
||||
const std::string DEFAULT_NAME_CONFLICTS = ZERO_SIX_NAME_CONFLICT_MSG
|
||||
+ ZERO_SEVEN_NAME_CONFLICT_MSG;
|
||||
|
||||
// Name conflicts in --nodatealiases mode
|
||||
const std::string ALL_NAME_CONFLICTS = ZERO_FOUR_NAME_CONFLICT_MSG
|
||||
+ ZERO_SIX_NAME_CONFLICT_MSG
|
||||
+ ZERO_SEVEN_NAME_CONFLICT_MSG;
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
void checkUnaliasedEntriesInNameMapper(const kiwix::NameMapper& nm)
|
||||
@@ -64,19 +129,37 @@ void checkUnaliasedEntriesInNameMapper(const kiwix::NameMapper& nm)
|
||||
EXPECT_EQ("zero_three", nm.getNameForId("03"));
|
||||
EXPECT_EQ("zero_four_2021-10", nm.getNameForId("04-2021-10"));
|
||||
EXPECT_EQ("zero_four_2021-11", nm.getNameForId("04-2021-11"));
|
||||
EXPECT_EQ("zero_five-a", nm.getNameForId("05-a"));
|
||||
EXPECT_EQ("zero_five-b", nm.getNameForId("05-b"));
|
||||
|
||||
// unreported conflict
|
||||
EXPECT_EQ("zero_plus_six", nm.getNameForId("06+"));
|
||||
EXPECT_EQ("zero_plus_six", nm.getNameForId("06plus"));
|
||||
|
||||
// unreported conflict
|
||||
EXPECT_EQ("zero_seven", nm.getNameForId("07-super"));
|
||||
EXPECT_EQ("zero_seven", nm.getNameForId("07-sub"));
|
||||
|
||||
EXPECT_EQ("01", nm.getIdForName("zero_one"));
|
||||
EXPECT_EQ("02", nm.getIdForName("zero_two"));
|
||||
EXPECT_EQ("03", nm.getIdForName("zero_three"));
|
||||
EXPECT_EQ("04-2021-10", nm.getIdForName("zero_four_2021-10"));
|
||||
EXPECT_EQ("04-2021-11", nm.getIdForName("zero_four_2021-11"));
|
||||
|
||||
// book name doesn't participate in name mapping
|
||||
EXPECT_THROW(nm.getIdForName("zero_five"), std::out_of_range);
|
||||
EXPECT_EQ("05-a", nm.getIdForName("zero_five-a"));
|
||||
EXPECT_EQ("05-b", nm.getIdForName("zero_five-b"));
|
||||
|
||||
EXPECT_EQ("06+", nm.getIdForName("zero_plus_six"));
|
||||
EXPECT_EQ("07-sub", nm.getIdForName("zero_seven"));
|
||||
}
|
||||
|
||||
TEST_F(NameMapperTest, HumanReadableNameMapperWithoutAliases)
|
||||
{
|
||||
CapturedStderr stderror;
|
||||
kiwix::HumanReadableNameMapper nm(*lib, false);
|
||||
EXPECT_EQ("", std::string(stderror));
|
||||
EXPECT_EQ(DEFAULT_NAME_CONFLICTS, std::string(stderror));
|
||||
|
||||
checkUnaliasedEntriesInNameMapper(nm);
|
||||
EXPECT_THROW(nm.getIdForName("zero_four"), std::out_of_range);
|
||||
@@ -91,12 +174,7 @@ TEST_F(NameMapperTest, HumanReadableNameMapperWithAliases)
|
||||
{
|
||||
CapturedStderr stderror;
|
||||
kiwix::HumanReadableNameMapper nm(*lib, true);
|
||||
EXPECT_EQ(
|
||||
"Path collision: /data/zero_four_2021-10.zim and"
|
||||
" /data/zero_four_2021-11.zim can't share the same URL path 'zero_four'."
|
||||
" Therefore, only /data/zero_four_2021-10.zim will be served.\n"
|
||||
, std::string(stderror)
|
||||
);
|
||||
EXPECT_EQ(ALL_NAME_CONFLICTS, std::string(stderror));
|
||||
|
||||
checkUnaliasedEntriesInNameMapper(nm);
|
||||
EXPECT_EQ("04-2021-10", nm.getIdForName("zero_four"));
|
||||
@@ -111,7 +189,7 @@ TEST_F(NameMapperTest, UpdatableNameMapperWithoutAliases)
|
||||
{
|
||||
CapturedStderr stderror;
|
||||
kiwix::UpdatableNameMapper nm(lib, false);
|
||||
EXPECT_EQ("", std::string(stderror));
|
||||
EXPECT_EQ(DEFAULT_NAME_CONFLICTS, std::string(stderror));
|
||||
|
||||
checkUnaliasedEntriesInNameMapper(nm);
|
||||
EXPECT_THROW(nm.getIdForName("zero_four"), std::out_of_range);
|
||||
@@ -127,12 +205,7 @@ TEST_F(NameMapperTest, UpdatableNameMapperWithAliases)
|
||||
{
|
||||
CapturedStderr stderror;
|
||||
kiwix::UpdatableNameMapper nm(lib, true);
|
||||
EXPECT_EQ(
|
||||
"Path collision: /data/zero_four_2021-10.zim and"
|
||||
" /data/zero_four_2021-11.zim can't share the same URL path 'zero_four'."
|
||||
" Therefore, only /data/zero_four_2021-10.zim will be served.\n"
|
||||
, std::string(stderror)
|
||||
);
|
||||
EXPECT_EQ(ALL_NAME_CONFLICTS, std::string(stderror));
|
||||
|
||||
checkUnaliasedEntriesInNameMapper(nm);
|
||||
EXPECT_EQ("04-2021-10", nm.getIdForName("zero_four"));
|
||||
@@ -141,7 +214,7 @@ TEST_F(NameMapperTest, UpdatableNameMapperWithAliases)
|
||||
CapturedStderr nmUpdateStderror;
|
||||
lib->removeBookById("04-2021-10");
|
||||
nm.update();
|
||||
EXPECT_EQ("", std::string(nmUpdateStderror));
|
||||
EXPECT_EQ(DEFAULT_NAME_CONFLICTS, std::string(nmUpdateStderror));
|
||||
}
|
||||
EXPECT_EQ("04-2021-11", nm.getIdForName("zero_four"));
|
||||
EXPECT_THROW(nm.getNameForId("04-2021-10"), std::out_of_range);
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "../include/tools.h"
|
||||
#include "../src/tools/otherTools.h"
|
||||
#include "zim/suggestion_iterator.h"
|
||||
#include "../src/server/i18n.h"
|
||||
#include "../src/server/i18n_utils.h"
|
||||
|
||||
#include <regex>
|
||||
|
||||
@@ -233,3 +234,27 @@ TEST(I18n, parseUserLanguagePreferences)
|
||||
"{fr, 1}{en, 0.5}"
|
||||
);
|
||||
}
|
||||
|
||||
#include "../include/tools.h"
|
||||
|
||||
TEST(networkTools, getNetworkInterfacesIPv4Or6)
|
||||
{
|
||||
for ( const auto& kv : kiwix::getNetworkInterfacesIPv4Or6() ) {
|
||||
std::cout << kv.first << " : IPv4 addr = " << kv.second.addr
|
||||
<< " ; IPv6 addr = " << kv.second.addr6
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(networkTools, getNetworkInterfaces)
|
||||
{
|
||||
for ( const auto& kv : kiwix::getNetworkInterfaces() ) {
|
||||
std::cout << kv.first << " : IPv4 addr = " << kv.second << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(networkTools, getBestPublicIps)
|
||||
{
|
||||
std::cout << "getBestPublicIps(): " << "[" << kiwix::getBestPublicIps().addr << ", " << kiwix::getBestPublicIps().addr6 << "]" << std::endl;
|
||||
std::cout << "getBestPublicIp(): " << kiwix::getBestPublicIp() << std::endl;
|
||||
}
|
||||
|
||||
109
test/server.cpp
109
test/server.cpp
@@ -61,9 +61,9 @@ const ResourceCollection resources200Compressible{
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=ae79e41a" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=ce19da2a" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=8f4b6a1e" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js" },
|
||||
@@ -73,9 +73,9 @@ const ResourceCollection resources200Compressible{
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js?cacheid=bd23c4fb" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=e014a885" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=80d56607" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=5fc4badf" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=aca897b0" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf" },
|
||||
@@ -84,7 +84,7 @@ const ResourceCollection resources200Compressible{
|
||||
// TODO: implement cache management of i18n resources
|
||||
//{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json?cacheid=unknown" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/languages.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=5be77f5c" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=ee7d95b5" },
|
||||
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/search" },
|
||||
|
||||
@@ -114,6 +114,8 @@ const ResourceCollection resources200Uncompressible{
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/caret.png?cacheid=22b942b4" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/download.png" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/download.png?cacheid=a39aa502" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/download-white.svg" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/download-white.svg?cacheid=079ab989"},
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/android-chrome-192x192.png" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/android-chrome-192x192.png?cacheid=bfac158b" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/android-chrome-512x512.png" },
|
||||
@@ -279,7 +281,7 @@ TEST_F(ServerTest, CacheIdsOfStaticResources)
|
||||
{
|
||||
/* url */ "/ROOT%23%3F/",
|
||||
R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9"
|
||||
href="/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf"
|
||||
href="/ROOT%23%3F/skin/index.css?cacheid=ae79e41a"
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/ROOT%23%3F/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/ROOT%23%3F/skin/favicon/favicon-32x32.png?cacheid=79ded625">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/ROOT%23%3F/skin/favicon/favicon-16x16.png?cacheid=a986fedc">
|
||||
@@ -289,10 +291,10 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9"
|
||||
<meta name="msapplication-config" content="/ROOT%23%3F/skin/favicon/browserconfig.xml?cacheid=f29a7c4a">
|
||||
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script>
|
||||
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" defer></script>
|
||||
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=5be77f5c" defer></script>
|
||||
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=ee7d95b5" defer></script>
|
||||
<script src="/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" defer></script>
|
||||
<script src="/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3"></script>
|
||||
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=ce19da2a" defer></script>
|
||||
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=8f4b6a1e" defer></script>
|
||||
<img src="/ROOT%23%3F/skin/feed.svg?cacheid=055b333f"
|
||||
<img src="/ROOT%23%3F/skin/langSelector.svg?cacheid=00b59961"
|
||||
)EXPECTEDRESULT"
|
||||
@@ -310,7 +312,8 @@ R"EXPECTEDRESULT( background-image: url('../skin/search-icon.svg?cacheid=b10a
|
||||
},
|
||||
{
|
||||
/* url */ "/ROOT%23%3F/skin/index.js",
|
||||
R"EXPECTEDRESULT( <img src="${root}/skin/download.png?cacheid=a39aa502" alt="${$t("direct-download-alt-text")}" />
|
||||
R"EXPECTEDRESULT( <img src="${root}/skin/download-white.svg?cacheid=079ab989">
|
||||
<img src="${root}/skin/download.png?cacheid=a39aa502" alt="${$t("direct-download-alt-text")}" />
|
||||
<img src="${root}/skin/hash.png?cacheid=f836e872" alt="${$t("hash-download-alt-text")}" />
|
||||
<img src="${root}/skin/magnet.png?cacheid=73b6bddf" alt="${$t("magnet-alt-text")}" />
|
||||
<img src="${root}/skin/bittorrent.png?cacheid=4f5c6882" alt="${$t("torrent-download-alt-text")}" />
|
||||
@@ -319,12 +322,12 @@ R"EXPECTEDRESULT( <img src="${root}/skin/download
|
||||
{
|
||||
/* url */ "/ROOT%23%3F/viewer",
|
||||
R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=2158fad9" rel="Stylesheet" />
|
||||
<link type="text/css" href="./skin/taskbar.css?cacheid=e014a885" rel="Stylesheet" />
|
||||
<link type="text/css" href="./skin/taskbar.css?cacheid=80d56607" rel="Stylesheet" />
|
||||
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script>
|
||||
<script type="module" src="./skin/i18n.js?cacheid=071abc9a" defer></script>
|
||||
<script type="text/javascript" src="./skin/languages.js?cacheid=5be77f5c" defer></script>
|
||||
<script type="text/javascript" src="./skin/viewer.js?cacheid=5fc4badf" defer></script>
|
||||
<script type="text/javascript" src="./skin/languages.js?cacheid=ee7d95b5" defer></script>
|
||||
<script type="text/javascript" src="./skin/viewer.js?cacheid=aca897b0" defer></script>
|
||||
<script type="text/javascript" src="./skin/autoComplete/autoComplete.min.js?cacheid=1191aaaf"></script>
|
||||
const blankPageUrl = root + "/skin/blank.html?cacheid=6b1fa032";
|
||||
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?cacheid=22b942b4" alt=""></label>
|
||||
@@ -356,6 +359,57 @@ R"EXPECTEDRESULT( <link type="text/css" href="/ROOT%23%3F/skin/search_results
|
||||
}
|
||||
}
|
||||
|
||||
std::string getCacheIdFromUrl(const std::string& url)
|
||||
{
|
||||
const std::string q("?cacheid=");
|
||||
const auto i = url.find(q);
|
||||
return i == std::string::npos ? "" : url.substr(i + q.size());
|
||||
}
|
||||
|
||||
std::string runExternalCmdAndGetItsOutput(const std::string& cmd)
|
||||
{
|
||||
std::string cmdOutput;
|
||||
|
||||
#ifdef _WIN32
|
||||
#define popen _popen
|
||||
#define pclose _pclose
|
||||
#endif
|
||||
|
||||
if (FILE* pPipe = popen(cmd.c_str(), "r"))
|
||||
{
|
||||
char buf[128];
|
||||
while (fgets(buf, 128, pPipe)) {
|
||||
cmdOutput += std::string(buf, buf+128);
|
||||
}
|
||||
|
||||
pclose(pPipe);
|
||||
}
|
||||
|
||||
return cmdOutput;
|
||||
}
|
||||
|
||||
std::string getSha1OfResponseData(const std::string& url)
|
||||
{
|
||||
const std::string pythonScript =
|
||||
"import urllib.request as req; "
|
||||
"import hashlib; "
|
||||
"print(hashlib.sha1(req.urlopen('" + url + "').read()).hexdigest())";
|
||||
const std::string cmd = "python3 -c \"" + pythonScript + "\"";
|
||||
return runExternalCmdAndGetItsOutput(cmd);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, CacheIdsOfStaticResourcesMatchTheSha1HashOfResourceContent)
|
||||
{
|
||||
for ( const Resource& res : all200Resources() ) {
|
||||
if ( res.kind == STATIC_CONTENT ) {
|
||||
const TestContext ctx{ {"url", res.url} };
|
||||
const std::string fullUrl = "http://localhost:" + std::to_string(SERVER_PORT) + res.url;
|
||||
const std::string sha1 = getSha1OfResponseData(fullUrl);
|
||||
ASSERT_EQ(sha1.substr(0, 8), getCacheIdFromUrl(res.url)) << ctx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* urls400[] = {
|
||||
"/ROOT%23%3F/search",
|
||||
"/ROOT%23%3F/search?content=zimfile",
|
||||
@@ -1152,13 +1206,18 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
|
||||
{
|
||||
"iso_code": "dag",
|
||||
"self_name": "Silimiinsili",
|
||||
"translation_count": 24
|
||||
"translation_count": 48
|
||||
},
|
||||
{
|
||||
"iso_code": "de",
|
||||
"self_name": "Deutsch",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "el",
|
||||
"self_name": "Αγγλικά",
|
||||
"translation_count": 23
|
||||
},
|
||||
{
|
||||
"iso_code": "en",
|
||||
"self_name": "English",
|
||||
@@ -1167,12 +1226,12 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
|
||||
{
|
||||
"iso_code": "es",
|
||||
"self_name": "español",
|
||||
"translation_count": 48
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"iso_code": "fi",
|
||||
"self_name": "suomi",
|
||||
"translation_count": 22
|
||||
"translation_count": 29
|
||||
},
|
||||
{
|
||||
"iso_code": "fr",
|
||||
@@ -1202,7 +1261,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
|
||||
{
|
||||
"iso_code": "ia",
|
||||
"self_name": "interlingua",
|
||||
"translation_count": 49
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "ig",
|
||||
@@ -1212,7 +1271,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
|
||||
{
|
||||
"iso_code": "it",
|
||||
"self_name": "italiano",
|
||||
"translation_count": 34
|
||||
"translation_count": 38
|
||||
},
|
||||
{
|
||||
"iso_code": "ja",
|
||||
@@ -1222,7 +1281,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
|
||||
{
|
||||
"iso_code": "ko",
|
||||
"self_name": "한국어",
|
||||
"translation_count": 13
|
||||
"translation_count": 15
|
||||
},
|
||||
{
|
||||
"iso_code": "ku-latn",
|
||||
@@ -1264,6 +1323,11 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
|
||||
"self_name": "Polski",
|
||||
"translation_count": 31
|
||||
},
|
||||
{
|
||||
"iso_code": "pt-br",
|
||||
"self_name": "Português",
|
||||
"translation_count": 35
|
||||
},
|
||||
{
|
||||
"iso_code": "ru",
|
||||
"self_name": "русский",
|
||||
@@ -1299,6 +1363,11 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
|
||||
"self_name": "Svenska",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "sw",
|
||||
"self_name": "Kiswahili",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "te",
|
||||
"self_name": "ఇంగ్లీషు",
|
||||
@@ -1311,8 +1380,8 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hans",
|
||||
"self_name": "英语",
|
||||
"translation_count": 54
|
||||
"self_name": "简体中文",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hant",
|
||||
|
||||
@@ -194,6 +194,23 @@ struct SearchResult
|
||||
SearchResult{LINK, TITLE, SNIPPET, BOOK_TITLE, WORDCOUNT}
|
||||
|
||||
|
||||
const SearchResult SEARCH_RESULT_FOR_TRAVEL_IN_RAYCHARLESZIM {
|
||||
/*link*/ "/ROOT%23%3F/content/zimfile/A/If_You_Go_Away",
|
||||
/*title*/ "If You Go Away",
|
||||
/*snippet*/ R"SNIPPET(...<b>Travel</b> On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the Billboard Hot 100, and went to #8 in the UK. The complex melody is partly derivative of classical music - the poignant "But if you stay..." passage comes from Franz Liszt's......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
/*wordCount*/ "204"
|
||||
};
|
||||
|
||||
|
||||
const SearchResult SEARCH_RESULT_FOR_TRAVEL_IN_EXAMPLEZIM {
|
||||
/*link*/ "/ROOT%23%3F/content/example/Wikibooks.html",
|
||||
/*title*/ "Wikibooks",
|
||||
/*snippet*/ R"SNIPPET(...<b>Travel</b> guide Wikidata Knowledge database Commons Media repository Meta Coordination MediaWiki MediaWiki software Phabricator MediaWiki bug tracker Wikimedia Labs MediaWiki development The Wikimedia Foundation is a non-profit organization that depends on your voluntarism and donations to operate. If you find Wikibooks or other projects hosted by the Wikimedia Foundation useful, please volunteer or make a donation. Your donations primarily helps to purchase server equipment, launch new projects......)SNIPPET",
|
||||
/*bookTitle*/ "Wikibooks",
|
||||
/*wordCount*/ "538"
|
||||
};
|
||||
|
||||
|
||||
const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
SEARCH_RESULT(
|
||||
@@ -1342,21 +1359,8 @@ TEST(ServerSearchTest, searchResults)
|
||||
/* totalResultCount */ 2,
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT%23%3F/content/zimfile/A/If_You_Go_Away",
|
||||
/*title*/ "If You Go Away",
|
||||
/*snippet*/ R"SNIPPET(...<b>Travel</b> On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the Billboard Hot 100, and went to #8 in the UK. The complex melody is partly derivative of classical music - the poignant "But if you stay..." passage comes from Franz Liszt's......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
/*wordCount*/ "204"
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT%23%3F/content/example/Wikibooks.html",
|
||||
/*title*/ "Wikibooks",
|
||||
/*snippet*/ R"SNIPPET(...<b>Travel</b> guide Wikidata Knowledge database Commons Media repository Meta Coordination MediaWiki MediaWiki software Phabricator MediaWiki bug tracker Wikimedia Labs MediaWiki development The Wikimedia Foundation is a non-profit organization that depends on your voluntarism and donations to operate. If you find Wikibooks or other projects hosted by the Wikimedia Foundation useful, please volunteer or make a donation. Your donations primarily helps to purchase server equipment, launch new projects......)SNIPPET",
|
||||
/*bookTitle*/ "Wikibooks",
|
||||
/*wordCount*/ "538"
|
||||
)
|
||||
SEARCH_RESULT_FOR_TRAVEL_IN_RAYCHARLESZIM,
|
||||
SEARCH_RESULT_FOR_TRAVEL_IN_EXAMPLEZIM
|
||||
},
|
||||
/* pagination */ {}
|
||||
},
|
||||
@@ -1369,25 +1373,72 @@ TEST(ServerSearchTest, searchResults)
|
||||
/* totalResultCount */ 2,
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT%23%3F/content/zimfile/A/If_You_Go_Away",
|
||||
/*title*/ "If You Go Away",
|
||||
/*snippet*/ R"SNIPPET(...<b>Travel</b> On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the Billboard Hot 100, and went to #8 in the UK. The complex melody is partly derivative of classical music - the poignant "But if you stay..." passage comes from Franz Liszt's......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
/*wordCount*/ "204"
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT%23%3F/content/example/Wikibooks.html",
|
||||
/*title*/ "Wikibooks",
|
||||
/*snippet*/ R"SNIPPET(...<b>Travel</b> guide Wikidata Knowledge database Commons Media repository Meta Coordination MediaWiki MediaWiki software Phabricator MediaWiki bug tracker Wikimedia Labs MediaWiki development The Wikimedia Foundation is a non-profit organization that depends on your voluntarism and donations to operate. If you find Wikibooks or other projects hosted by the Wikimedia Foundation useful, please volunteer or make a donation. Your donations primarily helps to purchase server equipment, launch new projects......)SNIPPET",
|
||||
/*bookTitle*/ "Wikibooks",
|
||||
/*wordCount*/ "538"
|
||||
)
|
||||
SEARCH_RESULT_FOR_TRAVEL_IN_RAYCHARLESZIM,
|
||||
SEARCH_RESULT_FOR_TRAVEL_IN_EXAMPLEZIM
|
||||
},
|
||||
/* pagination */ {}
|
||||
},
|
||||
|
||||
// books.name filters by the name of the ZIM file
|
||||
{
|
||||
/* query */ "pattern=travel"
|
||||
"&books.name=zimfile",
|
||||
/* start */ 0,
|
||||
/* resultsPerPage */ 10,
|
||||
/* totalResultCount */ 1,
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT_FOR_TRAVEL_IN_RAYCHARLESZIM
|
||||
},
|
||||
|
||||
/* pagination */ {}
|
||||
},
|
||||
|
||||
// books.name filters by the name of the ZIM file
|
||||
{
|
||||
/* query */ "pattern=travel"
|
||||
"&books.name=example",
|
||||
/* start */ 0,
|
||||
/* resultsPerPage */ 10,
|
||||
/* totalResultCount */ 1,
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT_FOR_TRAVEL_IN_EXAMPLEZIM
|
||||
},
|
||||
|
||||
/* pagination */ {}
|
||||
},
|
||||
|
||||
// books.filter.name filters by the book name
|
||||
{
|
||||
/* query */ "pattern=travel"
|
||||
"&books.filter.name=wikipedia_en_ray_charles",
|
||||
/* start */ 0,
|
||||
/* resultsPerPage */ 10,
|
||||
/* totalResultCount */ 1,
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT_FOR_TRAVEL_IN_RAYCHARLESZIM
|
||||
},
|
||||
|
||||
/* pagination */ {}
|
||||
},
|
||||
|
||||
// books.filter.name filters by the book name
|
||||
{
|
||||
/* query */ "pattern=travel"
|
||||
"&books.filter.name=bookname_of_example_zim",
|
||||
/* start */ 0,
|
||||
/* resultsPerPage */ 10,
|
||||
/* totalResultCount */ 1,
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT_FOR_TRAVEL_IN_EXAMPLEZIM
|
||||
},
|
||||
|
||||
/* pagination */ {}
|
||||
},
|
||||
|
||||
// Adding a book (without match) doesn't change the results
|
||||
{
|
||||
/* query */ "pattern=jazz"
|
||||
@@ -1521,7 +1572,10 @@ TEST(ServerSearchTest, searchResults)
|
||||
}
|
||||
}
|
||||
|
||||
std::string expectedConfusionOfTonguesErrorHtml(std::string url)
|
||||
std::string invalidRequestErrorHtml(std::string url,
|
||||
std::string errorMsgId,
|
||||
std::string errorMsgParamsJSON,
|
||||
std::string errorText)
|
||||
{
|
||||
return R"(<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
@@ -1530,7 +1584,7 @@ std::string expectedConfusionOfTonguesErrorHtml(std::string url)
|
||||
<title>Invalid request</title>
|
||||
<script>
|
||||
window.KIWIX_RESPONSE_TEMPLATE = )" + ERROR_HTML_TEMPLATE_JS_STRING + R"(;
|
||||
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : ")" + url + R"(" } } }, { "p" : { "msgid" : "confusion-of-tongues", "params" : { } } } ] };
|
||||
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : ")" + url + R"(" } } }, { "p" : { "msgid" : ")" + errorMsgId + R"(", "params" : )" + errorMsgParamsJSON + R"( } } ] };
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1539,19 +1593,30 @@ std::string expectedConfusionOfTonguesErrorHtml(std::string url)
|
||||
The requested URL ")" + url + R"(" is not a valid request.
|
||||
</p>
|
||||
<p>
|
||||
Two or more books in different languages would participate in search, which may lead to confusing results.
|
||||
)" + errorText + R"(
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
)";
|
||||
}
|
||||
|
||||
const char CONFUSION_OF_TONGUES_ERROR_TEXT[] = "Two or more books in different languages would participate in search, which may lead to confusing results.";
|
||||
|
||||
std::string expectedConfusionOfTonguesErrorHtml(std::string url)
|
||||
{
|
||||
return invalidRequestErrorHtml(url,
|
||||
/* errorMsgId */ "confusion-of-tongues",
|
||||
/* errorMsgParamsJSON */ "{ }",
|
||||
/* errorText */ CONFUSION_OF_TONGUES_ERROR_TEXT
|
||||
);
|
||||
}
|
||||
|
||||
std::string expectedConfusionOfTonguesErrorXml(std::string url)
|
||||
{
|
||||
return R"(<?xml version="1.0" encoding="UTF-8">
|
||||
<error>Invalid request</error>
|
||||
<detail>The requested URL ")" + url + R"(" is not a valid request.</detail>
|
||||
<detail>Two or more books in different languages would participate in search, which may lead to confusing results.</detail>
|
||||
<detail>)" + CONFUSION_OF_TONGUES_ERROR_TEXT + R"(</detail>
|
||||
)";
|
||||
}
|
||||
|
||||
@@ -1591,3 +1656,50 @@ TEST(ServerSearchTest, searchInMultilanguageBookSetIsDenied)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string noSuchBookErrorHtml(std::string url, std::string bookName)
|
||||
{
|
||||
return invalidRequestErrorHtml(url,
|
||||
/* errorMsgId */ "no-such-book",
|
||||
/* errorMsgParamsJSON */ "{ \"BOOK_NAME\" : \"" + bookName + "\" }",
|
||||
/* errorText */ "No such book: " + bookName
|
||||
);
|
||||
}
|
||||
|
||||
std::string noBookFoundErrorHtml(std::string url)
|
||||
{
|
||||
return invalidRequestErrorHtml(url,
|
||||
/* errorMsgId */ "no-book-found",
|
||||
/* errorMsgParamsJSON */ "{ }",
|
||||
/* errorText */ "No book matches selection criteria"
|
||||
);
|
||||
}
|
||||
|
||||
TEST(ServerSearchTest, bookSelectionNegativeTests)
|
||||
{
|
||||
ZimFileServer zfs(SERVER_PORT, ZimFileServer::DEFAULT_OPTIONS,
|
||||
"./test/lib_for_server_search_test.xml");
|
||||
|
||||
{
|
||||
// books.name (unlike books.filter.name) DOESN'T consider the book name
|
||||
// and reports an error (surprise!)
|
||||
const std::string bookName = "wikipedia_en_ray_charles";
|
||||
const std::string q = "pattern=travel&books.name=" + bookName;
|
||||
const std::string url = "/ROOT%23%3F/search?" + q;
|
||||
|
||||
const auto r = zfs.GET(url.c_str());
|
||||
EXPECT_EQ(r->status, 400);
|
||||
EXPECT_EQ(r->body, noSuchBookErrorHtml(url, bookName));
|
||||
}
|
||||
|
||||
{
|
||||
// books.filter.name (unlike books.name) DOESN'T consider the ZIM file name
|
||||
// and reports an error (differently from books.name)
|
||||
const std::string q = "pattern=travel&books.filter.name=zimfile";
|
||||
const std::string url = "/ROOT%23%3F/search?" + q;
|
||||
|
||||
const auto r = zfs.GET(url.c_str());
|
||||
EXPECT_EQ(r->status, 400);
|
||||
EXPECT_EQ(r->body, noBookFoundErrorHtml(url));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "../src/tools/stringTools.h"
|
||||
#include "../include/tools.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -170,4 +171,17 @@ TEST(stringTools, stripSuffix)
|
||||
EXPECT_EQ(stripSuffix("abc123", "987"), "abc123");
|
||||
}
|
||||
|
||||
TEST(stringTools, getSlugifiedFileName)
|
||||
{
|
||||
EXPECT_EQ(getSlugifiedFileName("abc123.png"), "abc123.png");
|
||||
EXPECT_EQ(getSlugifiedFileName("/"), "_");
|
||||
EXPECT_EQ(getSlugifiedFileName("abc/123.pdf"), "abc_123.pdf");
|
||||
EXPECT_EQ(getSlugifiedFileName("abc//123.yaml"), "abc__123.yaml");
|
||||
EXPECT_EQ(getSlugifiedFileName("//abc//123//"), "__abc__123__");
|
||||
#ifdef _WIN32
|
||||
EXPECT_EQ(getSlugifiedFileName(R"(<>:"/\\|?*)"), "__________");
|
||||
EXPECT_EQ(getSlugifiedFileName(R"(<abc>:"/123\\|?*<.txt>)"), "_abc____123______.txt_");
|
||||
#endif
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user